mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
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:
parent
93aa5c4b74
commit
96ea431eb7
@ -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:
|
||||
|
||||
252
docs/docs/data-source/5-null-and-undefined-handling.md
Normal file
252
docs/docs/data-source/5-null-and-undefined-handling.md
Normal 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.
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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") {
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
22
test/functional/null-undefined-handling/entity/Category.ts
Normal file
22
test/functional/null-undefined-handling/entity/Category.ts
Normal 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[]
|
||||
}
|
||||
24
test/functional/null-undefined-handling/entity/Post.ts
Normal file
24
test/functional/null-undefined-handling/entity/Post.ts
Normal 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
|
||||
}
|
||||
729
test/functional/null-undefined-handling/find-options.ts
Normal file
729
test/functional/null-undefined-handling/find-options.ts
Normal 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.",
|
||||
)
|
||||
}
|
||||
}),
|
||||
))
|
||||
})
|
||||
})
|
||||
347
test/functional/null-undefined-handling/query-builders.ts
Normal file
347
test/functional/null-undefined-handling/query-builders.ts
Normal 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")
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user