feat: add new undefined and null behavior flags (#11332)

- added configurable handling for null/undefined in where clauses (ignore, SQL NULL, or throw) across queries, query builders, and repository/entity-manager methods
This commit is contained in:
Naor Peled 2025-09-18 08:41:16 +03:00 committed by GitHub
parent 93aa5c4b74
commit 96ea431eb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 1498 additions and 48 deletions

View File

@ -84,6 +84,20 @@ Different RDBMS-es have their own specific options.
- `isolateWhereStatements` - Enables where statement isolation, wrapping each where clause in brackets automatically.
eg. `.where("user.firstName = :search OR user.lastName = :search")` becomes `WHERE (user.firstName = ? OR user.lastName = ?)` instead of `WHERE user.firstName = ? OR user.lastName = ?`
- `invalidWhereValuesBehavior` - Controls how null and undefined values are handled in where conditions across all TypeORM operations (find operations, query builders, repository methods).
- `null` behavior options:
- `'ignore'` (default) - skips null properties
- `'sql-null'` - transforms null to SQL NULL
- `'throw'` - throws an error
- `undefined` behavior options:
- `'ignore'` (default) - skips undefined properties
- `'throw'` - throws an error
Example: `invalidWhereValuesBehavior: { null: 'sql-null', undefined: 'throw' }`.
Learn more about [Null and Undefined Handling](./5-null-and-undefined-handling.md).
## Data Source Options example
Here is a small example of data source options for mysql:

View File

@ -0,0 +1,252 @@
# Handling null and undefined values in where conditions
In 'WHERE' conditions the values `null` and `undefined` are not strictly valid values in TypeORM.
Passing a known `null` value is disallowed by TypeScript (when you've enabled `strictNullChecks` in tsconfig.json) at compile time. But the default behavior is for `null` values encountered at runtime to be ignored. Similarly, `undefined` values are allowed by TypeScript and ignored at runtime.
The acceptance of `null` and `undefined` values can sometimes cause unexpected results and requires caution. This is especially a concern when values are passed from user input without adequate validation.
For example, calling `Repository.findOneBy({ id: undefined })` returns the first row from the table, and `Repository.findBy({ userId: null })` is unfiltered and returns all rows.
The way in which `null` and `undefined` values are handled can be customised through the `invalidWhereValuesBehavior` configuration option in your data source options. This applies to all operations that support 'WHERE' conditions, including find operations, query builders, and repository methods.
:::note
The current behavior will be changing in future versions of TypeORM,
we recommend setting both `null` and `undefined` behaviors to throw to prepare for these changes
:::
## Default Behavior
By default, TypeORM skips both `null` and `undefined` values in where conditions. This means that if you include a property with a `null` or `undefined` value in your where clause, it will be ignored:
```typescript
// Both queries will return all posts, ignoring the text property
const posts1 = await repository.find({
where: {
text: null,
},
})
const posts2 = await repository.find({
where: {
text: undefined,
},
})
```
The correct way to match null values in where conditions is to use the `IsNull` operator (for details see [Find Options](../working-with-entity-manager/3-find-options.md)):
```typescript
const posts = await repository.find({
where: {
text: IsNull(),
},
})
```
## Configuration
You can customize how null and undefined values are handled using the `invalidWhereValuesBehavior` option in your connection configuration:
```typescript
const dataSource = new DataSource({
// ... other options
invalidWhereValuesBehavior: {
null: "ignore" | "sql-null" | "throw",
undefined: "ignore" | "throw",
},
})
```
### Null Behavior Options
The `null` behavior can be set to one of three values:
#### `'ignore'` (default)
JavaScript `null` values in where conditions are ignored and the property is skipped:
```typescript
const dataSource = new DataSource({
// ... other options
invalidWhereValuesBehavior: {
null: "ignore",
},
})
// This will return all posts, ignoring the text property
const posts = await repository.find({
where: {
text: null,
},
})
```
#### `'sql-null'`
JavaScript `null` values are transformed into SQL `NULL` conditions:
```typescript
const dataSource = new DataSource({
// ... other options
invalidWhereValuesBehavior: {
null: "sql-null",
},
})
// This will only return posts where the text column is NULL in the database
const posts = await repository.find({
where: {
text: null,
},
})
```
#### `'throw'`
JavaScript `null` values cause a TypeORMError to be thrown:
```typescript
const dataSource = new DataSource({
// ... other options
invalidWhereValuesBehavior: {
null: "throw",
},
})
// This will throw an error
const posts = await repository.find({
where: {
text: null,
},
})
// Error: Null value encountered in property 'text' of a where condition.
// To match with SQL NULL, the IsNull() operator must be used.
// Set 'invalidWhereValuesBehavior.null' to 'ignore' or 'sql-null' in connection options to skip or handle null values.
```
### Undefined Behavior Options
The `undefined` behavior can be set to one of two values:
#### `'ignore'` (default)
JavaScript `undefined` values in where conditions are ignored and the property is skipped:
```typescript
const dataSource = new DataSource({
// ... other options
invalidWhereValuesBehavior: {
undefined: "ignore",
},
})
// This will return all posts, ignoring the text property
const posts = await repository.find({
where: {
text: undefined,
},
})
```
#### `'throw'`
JavaScript `undefined` values cause a TypeORMError to be thrown:
```typescript
const dataSource = new DataSource({
// ... other options
invalidWhereValuesBehavior: {
undefined: "throw",
},
})
// This will throw an error
const posts = await repository.find({
where: {
text: undefined,
},
})
// Error: Undefined value encountered in property 'text' of a where condition.
// Set 'invalidWhereValuesBehavior.undefined' to 'ignore' in connection options to skip properties with undefined values.
```
Note that this only applies to explicitly set `undefined` values, not omitted properties.
## Using Both Options Together
You can configure both behaviors independently for comprehensive control:
```typescript
const dataSource = new DataSource({
// ... other options
invalidWhereValuesBehavior: {
null: "sql-null",
undefined: "throw",
},
})
```
This configuration will:
1. Transform JavaScript `null` values to SQL `NULL` in where conditions
2. Throw an error if any `undefined` values are encountered
3. Still ignore properties that are not provided in the where clause
This combination is useful when you want to:
- Be explicit about searching for NULL values in the database
- Catch potential programming errors where undefined values might slip into your queries
## Works with all where operations
The `invalidWhereValuesBehavior` configuration applies to **all TypeORM operations** that support where conditions, not just repository find methods:
### Query Builders
```typescript
// UpdateQueryBuilder
await dataSource
.createQueryBuilder()
.update(Post)
.set({ title: "Updated" })
.where({ text: null }) // Respects invalidWhereValuesBehavior
.execute()
// DeleteQueryBuilder
await dataSource
.createQueryBuilder()
.delete()
.from(Post)
.where({ text: null }) // Respects invalidWhereValuesBehavior
.execute()
// SoftDeleteQueryBuilder
await dataSource
.createQueryBuilder()
.softDelete()
.from(Post)
.where({ text: null }) // Respects invalidWhereValuesBehavior
.execute()
```
### Repository Methods
```typescript
// Repository.update()
await repository.update({ text: null }, { title: "Updated" }) // Respects invalidWhereValuesBehavior
// Repository.delete()
await repository.delete({ text: null }) // Respects invalidWhereValuesBehavior
// EntityManager.update()
await manager.update(Post, { text: null }, { title: "Updated" }) // Respects invalidWhereValuesBehavior
// EntityManager.delete()
await manager.delete(Post, { text: null }) // Respects invalidWhereValuesBehavior
// EntityManager.softDelete()
await manager.softDelete(Post, { text: null }) // Respects invalidWhereValuesBehavior
```
All these operations will consistently apply your configured `invalidWhereValuesBehavior` settings.

View File

@ -214,4 +214,24 @@ export interface BaseDataSourceOptions {
* Allows automatic isolation of where clauses
*/
readonly isolateWhereStatements?: boolean
/**
* Controls how null and undefined values are handled in find operations.
*/
readonly invalidWhereValuesBehavior?: {
/**
* How to handle null values in where conditions.
* - 'ignore': Skip null properties (default)
* - 'sql-null': Transform null to SQL NULL
* - 'throw': Throw an error when null is encountered
*/
readonly null?: "ignore" | "sql-null" | "throw"
/**
* How to handle undefined values in where conditions.
* - 'ignore': Skip undefined properties (default)
* - 'throw': Throw an error when undefined is encountered
*/
readonly undefined?: "ignore" | "throw"
}
}

View File

@ -1581,18 +1581,37 @@ export abstract class QueryBuilder<Entity extends ObjectLiteral> {
parameters: [aliasPath, ...parameters],
}
}
// } else if (parameterValue === null) {
// return {
// operator: "isNull",
// parameters: [
// aliasPath,
// ]
// };
} else {
return {
operator: "equal",
parameters: [aliasPath, this.createParameter(parameterValue)],
} else if (parameterValue === null) {
const nullBehavior =
this.connection.options.invalidWhereValuesBehavior?.null ||
"ignore"
if (nullBehavior === "sql-null") {
return {
operator: "isNull",
parameters: [aliasPath],
}
} else if (nullBehavior === "throw") {
throw new TypeORMError(
`Null value encountered in property '${aliasPath}' of a where condition. ` +
`To match with SQL NULL, the IsNull() operator must be used. ` +
`Set 'invalidWhereValuesBehavior.null' to 'ignore' or 'sql-null' in connection options to skip or handle null values.`,
)
}
} else if (parameterValue === undefined) {
const undefinedBehavior =
this.connection.options.invalidWhereValuesBehavior?.undefined ||
"ignore"
if (undefinedBehavior === "throw") {
throw new TypeORMError(
`Undefined value encountered in property '${aliasPath}' of a where condition. ` +
`Set 'invalidWhereValuesBehavior.undefined' to 'ignore' in connection options to skip properties with undefined values.`,
)
}
}
return {
operator: "equal",
parameters: [aliasPath, this.createParameter(parameterValue)],
}
}

View File

@ -3165,6 +3165,7 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
}
this.selects = []
if (this.findOptions.relations) {
const relations = Array.isArray(this.findOptions.relations)
? OrmUtils.propertyPathsToTruthyObject(
@ -4304,7 +4305,7 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
} else {
const andConditions: string[] = []
for (const key in where) {
if (where[key] === undefined || where[key] === null) continue
let parameterValue = where[key]
const propertyPath = embedPrefix ? embedPrefix + "." + key : key
const column =
@ -4314,21 +4315,56 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
const relation =
metadata.findRelationWithPropertyPath(propertyPath)
if (!embed && !column && !relation)
if (!embed && !column && !relation) {
throw new EntityPropertyNotFoundError(
propertyPath,
metadata,
)
}
if (parameterValue === undefined) {
const undefinedBehavior =
this.connection.options.invalidWhereValuesBehavior
?.undefined || "ignore"
if (undefinedBehavior === "throw") {
throw new TypeORMError(
`Undefined value encountered in property '${alias}.${key}' of a where condition. ` +
`Set 'invalidWhereValuesBehavior.undefined' to 'ignore' in connection options to skip properties with undefined values.`,
)
}
continue
}
if (parameterValue === null) {
const nullBehavior =
this.connection.options.invalidWhereValuesBehavior
?.null || "ignore"
if (nullBehavior === "ignore") {
continue
} else if (nullBehavior === "throw") {
throw new TypeORMError(
`Null value encountered in property '${alias}.${key}' of a where condition. ` +
`To match with SQL NULL, the IsNull() operator must be used. ` +
`Set 'invalidWhereValuesBehavior.null' to 'ignore' or 'sql-null' in connection options to skip or handle null values.`,
)
}
// 'sql-null' behavior continues to the next logic
}
if (column) {
let aliasPath = `${alias}.${propertyPath}`
if (column.isVirtualProperty && column.query) {
aliasPath = `(${column.query(this.escape(alias))})`
}
if (parameterValue === null) {
andConditions.push(`${aliasPath} IS NULL`)
continue
}
// const parameterName = alias + "_" + propertyPath.split(".").join("_") + "_" + parameterIndex;
// todo: we need to handle other operators as well?
let parameterValue = where[key]
if (InstanceChecker.isEqualOperator(where[key])) {
parameterValue = where[key].value
}
@ -4351,38 +4387,6 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
).parametrizeValues(column, parameterValue)
}
// if (parameterValue === null) {
// andConditions.push(`${aliasPath} IS NULL`);
//
// } else if (parameterValue instanceof FindOperator) {
// // let parameters: any[] = [];
// // if (parameterValue.useParameter) {
// // const realParameterValues: any[] = parameterValue.multipleParameters ? parameterValue.value : [parameterValue.value];
// // realParameterValues.forEach((realParameterValue, realParameterValueIndex) => {
// //
// // // don't create parameters for number to prevent max number of variables issues as much as possible
// // if (typeof realParameterValue === "number") {
// // parameters.push(realParameterValue);
// //
// // } else {
// // this.expressionMap.nativeParameters[parameterName + realParameterValueIndex] = realParameterValue;
// // parameterIndex++;
// // parameters.push(this.connection.driver.createParameter(parameterName + realParameterValueIndex, parameterIndex - 1));
// // }
// // });
// // }
// andConditions.push(
// this.createWhereConditionExpression(this.getWherePredicateCondition(aliasPath, parameterValue))
// // parameterValue.toSql(this.connection, aliasPath, parameters));
// )
//
// } else {
// this.expressionMap.nativeParameters[parameterName] = parameterValue;
// parameterIndex++;
// const parameter = this.connection.driver.createParameter(parameterName, parameterIndex - 1);
// andConditions.push(`${aliasPath} = ${parameter}`);
// }
andConditions.push(
this.createWhereConditionExpression(
this.getWherePredicateCondition(
@ -4404,6 +4408,24 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
)
if (condition) andConditions.push(condition)
} else if (relation) {
if (where[key] === null) {
const nullBehavior =
this.connection.options.invalidWhereValuesBehavior
?.null || "ignore"
if (nullBehavior === "sql-null") {
andConditions.push(
`${alias}.${propertyPath} IS NULL`,
)
} else if (nullBehavior === "throw") {
throw new TypeORMError(
`Null value encountered in property '${alias}.${key}' of a where condition. ` +
`Set 'invalidWhereValuesBehavior.null' to 'ignore' or 'sql-null' in connection options to skip or handle null values.`,
)
}
// 'ignore' behavior falls through to continue
continue
}
// if all properties of where are undefined we don't need to join anything
// this can happen when user defines map with conditional queries inside
if (typeof where[key] === "object") {

View File

@ -61,7 +61,7 @@ describe("find options > where", () => {
const posts1 = await connection
.createQueryBuilder(Post, "post")
.setFindOptions({
// @ts-expect-error
// @ts-expect-error - null should be marked as unsafe by default
where: {
title: "Post #1",
text: null,
@ -79,7 +79,7 @@ describe("find options > where", () => {
const posts2 = await connection
.createQueryBuilder(Post, "post")
.setFindOptions({
// @ts-expect-error
// @ts-expect-error - null should be marked as unsafe by default
where: {
text: null,
},

View File

@ -0,0 +1,22 @@
import {
Column,
Entity,
PrimaryGeneratedColumn,
OneToMany,
} from "../../../../src"
import { Post } from "./Post"
@Entity()
export class Category {
@PrimaryGeneratedColumn()
id: number
@Column()
name: string
@Column({ nullable: true, type: "varchar" })
slug: string | null
@OneToMany(() => Post, (post) => post.category)
posts: Post[]
}

View File

@ -0,0 +1,24 @@
import {
Column,
Entity,
PrimaryGeneratedColumn,
ManyToOne,
JoinColumn,
} from "../../../../src"
import { Category } from "./Category"
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number
@Column()
title: string
@Column({ nullable: true, type: "varchar" })
text: string | null
@ManyToOne(() => Category, { nullable: true })
@JoinColumn()
category: Category | null
}

View File

@ -0,0 +1,729 @@
import "reflect-metadata"
import "../../utils/test-setup"
import { DataSource, TypeORMError } from "../../../src"
import {
closeTestingConnections,
createTestingConnections,
reloadTestingDatabases,
} from "../../utils/test-utils"
import { Post } from "./entity/Post"
import { Category } from "./entity/Category"
import { expect } from "chai"
describe("find options > null and undefined handling", () => {
let connections: DataSource[]
describe("with default behavior", () => {
before(async () => {
connections = await createTestingConnections({
entities: [Post, Category],
schemaCreate: true,
dropSchema: true,
})
})
beforeEach(() => reloadTestingDatabases(connections))
after(() => closeTestingConnections(connections))
async function prepareData(connection: DataSource) {
const category1 = new Category()
category1.name = "Category #1"
await connection.manager.save(category1)
const category2 = new Category()
category2.name = "Category #2"
await connection.manager.save(category2)
const post1 = new Post()
post1.title = "Post #1"
post1.text = "About post #1"
post1.category = category1
await connection.manager.save(post1)
const post2 = new Post()
post2.title = "Post #2"
post2.text = null
post2.category = category2
await connection.manager.save(post2)
const post3 = new Post()
post3.title = "Post #3"
post3.text = "About post #3"
post3.category = null
await connection.manager.save(post3)
}
it("should skip null properties by default", () =>
Promise.all(
connections.map(async (connection) => {
await prepareData(connection)
// Test with QueryBuilder
const postsWithQb = await connection
.createQueryBuilder(Post, "post")
.setFindOptions({
// @ts-expect-error - null should be marked as unsafe by default
where: {
title: "Post #1",
text: null,
},
})
.getMany()
// This should return post1 since null properties are skipped by default
postsWithQb.should.be.eql([
{ id: 1, title: "Post #1", text: "About post #1" },
])
// Test with Repository find
const postsWithRepo = await connection
.getRepository(Post)
.find({
// @ts-expect-error - null should be marked as unsafe by default
where: {
text: null,
},
})
// This should return all posts since null properties are skipped by default
postsWithRepo.length.should.be.equal(3)
}),
))
it("should skip undefined properties by default", () =>
Promise.all(
connections.map(async (connection) => {
await prepareData(connection)
// Test with QueryBuilder
const postsWithQb = await connection
.createQueryBuilder(Post, "post")
.setFindOptions({
where: {
title: "Post #1",
text: undefined,
},
})
.getMany()
// This should return post1 since undefined properties are skipped by default
postsWithQb.should.be.eql([
{ id: 1, title: "Post #1", text: "About post #1" },
])
// Test with Repository
const postsWithRepo = await connection
.getRepository(Post)
.find({
where: {
text: undefined,
},
})
// This should return all posts since undefined properties are skipped by default
postsWithRepo.length.should.be.equal(3)
}),
))
it("should skip null relation properties by default", () =>
Promise.all(
connections.map(async (connection) => {
await prepareData(connection)
// Test with QueryBuilder
const postsWithQb = await connection
.createQueryBuilder(Post, "post")
.setFindOptions({
// @ts-expect-error - null should be marked as unsafe by default
where: {
category: null,
},
})
.getMany()
// This should return all posts since null properties are skipped by default
postsWithQb.length.should.be.equal(3)
// Test with Repository
const postsWithRepo = await connection
.getRepository(Post)
.find({
// @ts-expect-error - null should be marked as unsafe by default
where: {
category: null,
},
})
// This should return all posts since null properties are skipped by default
postsWithRepo.length.should.be.equal(3)
}),
))
it("should skip undefined relation properties by default", () =>
Promise.all(
connections.map(async (connection) => {
await prepareData(connection)
// Test with QueryBuilder
const postsWithQb = await connection
.createQueryBuilder(Post, "post")
.setFindOptions({
where: {
category: undefined,
},
})
.getMany()
// This should return all posts since undefined properties are skipped by default
postsWithQb.length.should.be.equal(3)
// Test with Repository
const postsWithRepo = await connection
.getRepository(Post)
.find({
where: {
category: undefined,
},
})
// This should return all posts since undefined properties are skipped by default
postsWithRepo.length.should.be.equal(3)
}),
))
})
describe("with invalidWhereValuesBehavior.null set to 'sql-null'", () => {
before(async () => {
connections = await createTestingConnections({
entities: [Post, Category],
schemaCreate: true,
dropSchema: true,
driverSpecific: {
invalidWhereValuesBehavior: {
null: "sql-null",
},
},
})
})
beforeEach(() => reloadTestingDatabases(connections))
after(() => closeTestingConnections(connections))
async function prepareData(connection: DataSource) {
const category1 = new Category()
category1.name = "Category #1"
await connection.manager.save(category1)
const post1 = new Post()
post1.title = "Post #1"
post1.text = null
post1.category = null
await connection.manager.save(post1)
const post2 = new Post()
post2.title = "Post #2"
post2.text = "Some text"
post2.category = category1
await connection.manager.save(post2)
}
it("should transform JS null to SQL NULL when invalidWhereValuesBehavior.null is 'sql-null'", () =>
Promise.all(
connections.map(async (connection) => {
await prepareData(connection)
// Test QueryBuilder with null text
const posts1 = await connection
.createQueryBuilder(Post, "post")
.where({
text: null,
})
.getMany()
expect(posts1.length).to.equal(1)
expect(posts1[0].title).to.equal("Post #1")
// Test Repository with null text
const posts2 = await connection.getRepository(Post).find({
// @ts-expect-error - null should be marked as unsafe by default
where: {
text: null,
},
})
expect(posts2.length).to.equal(1)
expect(posts2[0].title).to.equal("Post #1")
// Test with Repository with null text and findOne
const postWithRepo = await connection
.getRepository(Post)
.findOne({
// @ts-expect-error - null should be marked as unsafe by default
where: {
text: null,
},
})
expect(postWithRepo?.title).to.equal("Post #1")
}),
))
it("should transform JS null to SQL NULL for relations when invalidWhereValuesBehavior.null is 'sql-null'", () =>
Promise.all(
connections.map(async (connection) => {
await prepareData(connection)
// Test QueryBuilder with null relation
const posts1 = await connection
.createQueryBuilder(Post, "post")
.where({
category: null,
})
.getMany()
expect(posts1.length).to.equal(1)
expect(posts1[0].title).to.equal("Post #1")
// Test Repository with null relation
const posts2 = await connection.getRepository(Post).find({
// @ts-expect-error - null should be marked as unsafe by default
where: {
category: null,
},
})
expect(posts2.length).to.equal(1)
expect(posts2[0].title).to.equal("Post #1")
// Test with Repository with null relation and findOne
const postWithRepo = await connection
.getRepository(Post)
.findOne({
// @ts-expect-error - null should be marked as unsafe by default
where: {
category: null,
},
})
expect(postWithRepo?.title).to.equal("Post #1")
const postWithRepo2 = await connection
.getRepository(Post)
.findOne({
// @ts-expect-error - null should be marked as unsafe by default
where: {
category: {
slug: null,
},
},
})
expect(postWithRepo2?.title).to.equal("Post #1")
}),
))
})
describe("with invalidWhereValuesBehavior.undefined set to 'throw'", () => {
before(async () => {
connections = await createTestingConnections({
entities: [Post, Category],
schemaCreate: true,
dropSchema: true,
driverSpecific: {
invalidWhereValuesBehavior: {
undefined: "throw",
},
},
})
})
beforeEach(() => reloadTestingDatabases(connections))
after(() => closeTestingConnections(connections))
it("should throw an error when undefined is encountered and invalidWhereValuesBehavior.undefined is 'throw'", async () => {
for (const connection of connections) {
try {
await connection
.createQueryBuilder(Post, "post")
.where({
text: undefined,
})
.getMany()
expect.fail("Expected query to throw an error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.equal(
"Undefined value encountered in property 'post.text' of a where condition. Set 'invalidWhereValuesBehavior.undefined' to 'ignore' in connection options to skip properties with undefined values.",
)
}
try {
await connection.getRepository(Post).find({
where: {
text: undefined,
},
})
expect.fail("Expected query to throw an error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.equal(
"Undefined value encountered in property 'Post.text' of a where condition. Set 'invalidWhereValuesBehavior.undefined' to 'ignore' in connection options to skip properties with undefined values.",
)
}
try {
await connection.getRepository(Post).findOne({
where: {
text: undefined,
},
})
expect.fail("Expected query to throw an error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.equal(
"Undefined value encountered in property 'Post.text' of a where condition. Set 'invalidWhereValuesBehavior.undefined' to 'ignore' in connection options to skip properties with undefined values.",
)
}
}
})
it("should throw an error when undefined is encountered in relations and invalidWhereValuesBehavior.undefined is 'throw'", () =>
Promise.all(
connections.map(async (connection) => {
try {
await connection
.createQueryBuilder(Post, "post")
.where({
category: undefined,
})
.getMany()
expect.fail("Expected query to throw an error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.equal(
"Undefined value encountered in property 'post.category.id' of a where condition. Set 'invalidWhereValuesBehavior.undefined' to 'ignore' in connection options to skip properties with undefined values.",
)
}
try {
await connection.getRepository(Post).find({
where: {
category: undefined,
},
})
expect.fail("Expected query to throw an error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.equal(
"Undefined value encountered in property 'Post.category' of a where condition. Set 'invalidWhereValuesBehavior.undefined' to 'ignore' in connection options to skip properties with undefined values.",
)
}
try {
await connection.getRepository(Post).findOne({
where: {
category: undefined,
},
})
expect.fail("Expected query to throw an error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.equal(
"Undefined value encountered in property 'Post.category' of a where condition. Set 'invalidWhereValuesBehavior.undefined' to 'ignore' in connection options to skip properties with undefined values.",
)
}
}),
))
it("should not throw when a property is not provided", () =>
Promise.all(
connections.map(async (connection) => {
// Create test data
const category = new Category()
category.name = "Category #1"
await connection.manager.save(category)
const post1 = new Post()
post1.title = "Post #1"
post1.text = "Some text"
post1.category = category
await connection.manager.save(post1)
// Test QueryBuilder
const posts1 = await connection
.createQueryBuilder(Post, "post")
.where({
title: "Post #1",
})
.getMany()
expect(posts1.length).to.equal(1)
expect(posts1[0].title).to.equal("Post #1")
// Test Repository
const posts2 = await connection.getRepository(Post).find({
where: {
title: "Post #1",
},
})
expect(posts2.length).to.equal(1)
expect(posts2[0].title).to.equal("Post #1")
// Test Repository with findOne
const postWithRepo = await connection
.getRepository(Post)
.findOne({
where: {
title: "Post #1",
},
})
expect(postWithRepo?.title).to.equal("Post #1")
}),
))
})
describe("with both invalidWhereValuesBehavior options enabled", () => {
before(async () => {
connections = await createTestingConnections({
entities: [Post, Category],
schemaCreate: true,
dropSchema: true,
driverSpecific: {
invalidWhereValuesBehavior: {
null: "sql-null",
undefined: "throw",
},
},
})
})
beforeEach(() => reloadTestingDatabases(connections))
after(() => closeTestingConnections(connections))
async function prepareData(connection: DataSource) {
const category1 = new Category()
category1.name = "Category #1"
await connection.manager.save(category1)
const post1 = new Post()
post1.title = "Post #1"
post1.text = null
post1.category = null
await connection.manager.save(post1)
const post2 = new Post()
post2.title = "Post #2"
post2.text = "Some text"
post2.category = category1
await connection.manager.save(post2)
}
it("should handle both null and undefined correctly", () =>
Promise.all(
connections.map(async (connection) => {
await prepareData(connection)
// Test null handling for text
const posts = await connection.getRepository(Post).find({
// @ts-expect-error - null should be marked as unsafe by default
where: {
text: null,
},
})
expect(posts.length).to.equal(1)
expect(posts[0].title).to.equal("Post #1")
// Test null handling for relations
const postsWithNullCategory = await connection
.getRepository(Post)
.find({
// @ts-expect-error - null should be marked as unsafe by default
where: {
category: null,
},
})
expect(postsWithNullCategory.length).to.equal(1)
expect(postsWithNullCategory[0].title).to.equal("Post #1")
// Test undefined handling for text
try {
await connection
.createQueryBuilder(Post, "post")
.where({
text: undefined,
})
.getMany()
expect.fail("Expected query to throw an error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.equal(
"Undefined value encountered in property 'post.text' of a where condition. Set 'invalidWhereValuesBehavior.undefined' to 'ignore' in connection options to skip properties with undefined values.",
)
}
// Test undefined handling for relations
try {
await connection
.createQueryBuilder(Post, "post")
.where({
category: undefined,
})
.getMany()
expect.fail("Expected query to throw an error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.equal(
"Undefined value encountered in property 'post.category.id' of a where condition. Set 'invalidWhereValuesBehavior.undefined' to 'ignore' in connection options to skip properties with undefined values.",
)
}
// Test omitted property
const posts2 = await connection.getRepository(Post).find({
where: {
title: "Post #2",
},
})
expect(posts2.length).to.equal(1)
expect(posts2[0].title).to.equal("Post #2")
// Test Repository with findOne
const postWithRepo = await connection
.getRepository(Post)
.findOne({
where: {
title: "Post #2",
},
})
expect(postWithRepo?.title).to.equal("Post #2")
}),
))
})
describe("with invalidWhereValuesBehavior.null set to 'throw'", () => {
before(async () => {
connections = await createTestingConnections({
entities: [Post, Category],
schemaCreate: true,
dropSchema: true,
driverSpecific: {
invalidWhereValuesBehavior: {
null: "throw",
},
},
})
})
beforeEach(() => reloadTestingDatabases(connections))
after(() => closeTestingConnections(connections))
it("should throw an error when null is encountered and invalidWhereValuesBehavior.null is 'throw'", async () => {
for (const connection of connections) {
try {
await connection
.createQueryBuilder(Post, "post")
.where({
text: null,
})
.getMany()
expect.fail("Expected query to throw an error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.equal(
"Null value encountered in property 'post.text' of a where condition. To match with SQL NULL, the IsNull() operator must be used. Set 'invalidWhereValuesBehavior.null' to 'ignore' or 'sql-null' in connection options to skip or handle null values.",
)
}
try {
await connection.getRepository(Post).find({
// @ts-expect-error - null should be marked as unsafe by default
where: {
text: null,
},
})
expect.fail("Expected query to throw an error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.equal(
"Null value encountered in property 'Post.text' of a where condition. To match with SQL NULL, the IsNull() operator must be used. Set 'invalidWhereValuesBehavior.null' to 'ignore' or 'sql-null' in connection options to skip or handle null values.",
)
}
try {
await connection.getRepository(Post).findOne({
// @ts-expect-error - null should be marked as unsafe by default
where: {
text: null,
},
})
expect.fail("Expected query to throw an error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.equal(
"Null value encountered in property 'Post.text' of a where condition. To match with SQL NULL, the IsNull() operator must be used. Set 'invalidWhereValuesBehavior.null' to 'ignore' or 'sql-null' in connection options to skip or handle null values.",
)
}
}
})
it("should throw an error when null is encountered in relations and invalidWhereValuesBehavior.null is 'throw'", () =>
Promise.all(
connections.map(async (connection) => {
try {
await connection
.createQueryBuilder(Post, "post")
.where({
category: null,
})
.getMany()
expect.fail("Expected query to throw an error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.equal(
"Null value encountered in property 'post.category.id' of a where condition. To match with SQL NULL, the IsNull() operator must be used. Set 'invalidWhereValuesBehavior.null' to 'ignore' or 'sql-null' in connection options to skip or handle null values.",
)
}
try {
await connection.getRepository(Post).find({
// @ts-expect-error - null should be marked as unsafe by default
where: {
category: null,
},
})
expect.fail("Expected query to throw an error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.equal(
"Null value encountered in property 'Post.category' of a where condition. To match with SQL NULL, the IsNull() operator must be used. Set 'invalidWhereValuesBehavior.null' to 'ignore' or 'sql-null' in connection options to skip or handle null values.",
)
}
try {
await connection.getRepository(Post).findOne({
// @ts-expect-error - null should be marked as unsafe by default
where: {
category: null,
},
})
expect.fail("Expected query to throw an error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.equal(
"Null value encountered in property 'Post.category' of a where condition. To match with SQL NULL, the IsNull() operator must be used. Set 'invalidWhereValuesBehavior.null' to 'ignore' or 'sql-null' in connection options to skip or handle null values.",
)
}
}),
))
})
})

View File

@ -0,0 +1,347 @@
import "reflect-metadata"
import "../../utils/test-setup"
import { DataSource, TypeORMError } from "../../../src"
import {
closeTestingConnections,
createTestingConnections,
reloadTestingDatabases,
} from "../../utils/test-utils"
import { Post } from "./entity/Post"
import { Category } from "./entity/Category"
import { expect } from "chai"
describe("query builder > invalidWhereValuesBehavior", () => {
let connections: DataSource[]
before(async () => {
connections = await createTestingConnections({
entities: [Post, Category],
schemaCreate: true,
dropSchema: true,
driverSpecific: {
invalidWhereValuesBehavior: {
null: "throw",
undefined: "throw",
},
},
})
})
beforeEach(() => reloadTestingDatabases(connections))
after(() => closeTestingConnections(connections))
async function prepareData(connection: DataSource) {
const category = new Category()
category.name = "Test Category"
await connection.manager.save(category)
const post = new Post()
post.title = "Test Post"
post.text = "Some text"
post.category = category
await connection.manager.save(post)
return { category, post }
}
it("should throw error for null values in UpdateQueryBuilder", async () => {
for (const connection of connections) {
await prepareData(connection)
try {
await connection
.createQueryBuilder()
.update(Post)
.set({ title: "Updated" })
.where({ text: null })
.execute()
expect.fail("Expected error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.include("Null value encountered")
}
}
})
it("should throw error for undefined values in UpdateQueryBuilder", async () => {
for (const connection of connections) {
await prepareData(connection)
try {
await connection
.createQueryBuilder()
.update(Post)
.set({ title: "Updated" })
.where({ text: undefined })
.execute()
expect.fail("Expected error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.include("Undefined value encountered")
}
}
})
it("should throw error for null values in DeleteQueryBuilder", async () => {
for (const connection of connections) {
await prepareData(connection)
try {
await connection
.createQueryBuilder()
.delete()
.from(Post)
.where({ text: null })
.execute()
expect.fail("Expected error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.include("Null value encountered")
}
}
})
it("should throw error for undefined values in DeleteQueryBuilder", async () => {
for (const connection of connections) {
await prepareData(connection)
try {
await connection
.createQueryBuilder()
.delete()
.from(Post)
.where({ text: undefined })
.execute()
expect.fail("Expected error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.include("Undefined value encountered")
}
}
})
it("should throw error for null values in SoftDeleteQueryBuilder", async () => {
for (const connection of connections) {
await prepareData(connection)
try {
await connection
.createQueryBuilder()
.softDelete()
.from(Post)
.where({ text: null })
.execute()
expect.fail("Expected error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.include("Null value encountered")
}
}
})
it("should throw error for undefined values in SoftDeleteQueryBuilder", async () => {
for (const connection of connections) {
await prepareData(connection)
try {
await connection
.createQueryBuilder()
.softDelete()
.from(Post)
.where({ text: undefined })
.execute()
expect.fail("Expected error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.include("Undefined value encountered")
}
}
})
})
describe("query builder > invalidWhereValuesBehavior sql-null", () => {
let connections: DataSource[]
before(async () => {
connections = await createTestingConnections({
entities: [Post, Category],
schemaCreate: true,
dropSchema: true,
driverSpecific: {
invalidWhereValuesBehavior: {
null: "sql-null",
},
},
})
})
beforeEach(() => reloadTestingDatabases(connections))
after(() => closeTestingConnections(connections))
async function prepareData(connection: DataSource) {
const category = new Category()
category.name = "Test Category"
await connection.manager.save(category)
const post1 = new Post()
post1.title = "Post 1"
post1.text = "Some text"
post1.category = category
await connection.manager.save(post1)
const post2 = new Post()
post2.title = "Post 2"
post2.text = null
post2.category = category
await connection.manager.save(post2)
return { category, post1, post2 }
}
it("should handle null as SQL NULL in UpdateQueryBuilder", async () => {
for (const connection of connections) {
const { post2 } = await prepareData(connection)
const result = await connection
.createQueryBuilder()
.update(Post)
.set({ title: "Updated Null Post" })
.where({ text: null })
.execute()
expect(result.affected).to.equal(1)
const updatedPost = await connection.manager.findOne(Post, {
where: { id: post2.id },
})
expect(updatedPost?.title).to.equal("Updated Null Post")
}
})
it("should handle null as SQL NULL in DeleteQueryBuilder", async () => {
for (const connection of connections) {
const { post1 } = await prepareData(connection)
const result = await connection
.createQueryBuilder()
.delete()
.from(Post)
.where({ text: null })
.execute()
expect(result.affected).to.equal(1)
const remainingPosts = await connection.manager.find(Post)
expect(remainingPosts).to.have.lengthOf(1)
expect(remainingPosts[0].id).to.equal(post1.id)
}
})
})
describe("repository methods > invalidWhereValuesBehavior", () => {
let connections: DataSource[]
before(async () => {
connections = await createTestingConnections({
entities: [Post, Category],
schemaCreate: true,
dropSchema: true,
driverSpecific: {
invalidWhereValuesBehavior: {
null: "throw",
undefined: "throw",
},
},
})
})
beforeEach(() => reloadTestingDatabases(connections))
after(() => closeTestingConnections(connections))
async function prepareData(connection: DataSource) {
const category = new Category()
category.name = "Test Category"
await connection.manager.save(category)
const post = new Post()
post.title = "Test Post"
post.text = "Some text"
post.category = category
await connection.manager.save(post)
return { category, post }
}
it("should throw error for null values in Repository.update()", async () => {
for (const connection of connections) {
await prepareData(connection)
try {
await connection
.getRepository(Post)
.update({ text: null } as any, { title: "Updated" })
expect.fail("Expected error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.include("Null value encountered")
}
}
})
it("should throw error for null values in EntityManager.update()", async () => {
for (const connection of connections) {
await prepareData(connection)
try {
await connection.manager.update(Post, { text: null } as any, {
title: "Updated",
})
expect.fail("Expected error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.include("Null value encountered")
}
}
})
it("should throw error for null values in EntityManager.delete()", async () => {
for (const connection of connections) {
await prepareData(connection)
try {
await connection.manager.delete(Post, { text: null } as any)
expect.fail("Expected error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.include("Null value encountered")
}
}
})
it("should throw error for null values in Repository.delete()", async () => {
for (const connection of connections) {
await prepareData(connection)
try {
await connection
.getRepository(Post)
.delete({ text: null } as any)
expect.fail("Expected error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.include("Null value encountered")
}
}
})
it("should throw error for null values in EntityManager.softDelete()", async () => {
for (const connection of connections) {
await prepareData(connection)
try {
await connection.manager.softDelete(Post, { text: null } as any)
expect.fail("Expected error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.include("Null value encountered")
}
}
})
})

View File

@ -485,6 +485,7 @@ export async function createTestingConnections(
* Closes testing connections if they are connected.
*/
export function closeTestingConnections(connections: DataSource[]) {
if (!connections || connections.length === 0) return Promise.resolve()
return Promise.all(
connections.map((connection) =>
connection && connection.isInitialized