Skip to content

Commit 96ea431

Browse files
authored
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
1 parent 93aa5c4 commit 96ea431

File tree

11 files changed

+1498
-48
lines changed

11 files changed

+1498
-48
lines changed

docs/docs/data-source/2-data-source-options.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,20 @@ Different RDBMS-es have their own specific options.
8484
- `isolateWhereStatements` - Enables where statement isolation, wrapping each where clause in brackets automatically.
8585
eg. `.where("user.firstName = :search OR user.lastName = :search")` becomes `WHERE (user.firstName = ? OR user.lastName = ?)` instead of `WHERE user.firstName = ? OR user.lastName = ?`
8686

87+
- `invalidWhereValuesBehavior` - Controls how null and undefined values are handled in where conditions across all TypeORM operations (find operations, query builders, repository methods).
88+
89+
- `null` behavior options:
90+
- `'ignore'` (default) - skips null properties
91+
- `'sql-null'` - transforms null to SQL NULL
92+
- `'throw'` - throws an error
93+
- `undefined` behavior options:
94+
- `'ignore'` (default) - skips undefined properties
95+
- `'throw'` - throws an error
96+
97+
Example: `invalidWhereValuesBehavior: { null: 'sql-null', undefined: 'throw' }`.
98+
99+
Learn more about [Null and Undefined Handling](./5-null-and-undefined-handling.md).
100+
87101
## Data Source Options example
88102

89103
Here is a small example of data source options for mysql:
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
# Handling null and undefined values in where conditions
2+
3+
In 'WHERE' conditions the values `null` and `undefined` are not strictly valid values in TypeORM.
4+
5+
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.
6+
7+
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.
8+
9+
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.
10+
11+
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.
12+
13+
:::note
14+
The current behavior will be changing in future versions of TypeORM,
15+
we recommend setting both `null` and `undefined` behaviors to throw to prepare for these changes
16+
:::
17+
18+
## Default Behavior
19+
20+
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:
21+
22+
```typescript
23+
// Both queries will return all posts, ignoring the text property
24+
const posts1 = await repository.find({
25+
where: {
26+
text: null,
27+
},
28+
})
29+
30+
const posts2 = await repository.find({
31+
where: {
32+
text: undefined,
33+
},
34+
})
35+
```
36+
37+
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)):
38+
39+
```typescript
40+
const posts = await repository.find({
41+
where: {
42+
text: IsNull(),
43+
},
44+
})
45+
```
46+
47+
## Configuration
48+
49+
You can customize how null and undefined values are handled using the `invalidWhereValuesBehavior` option in your connection configuration:
50+
51+
```typescript
52+
const dataSource = new DataSource({
53+
// ... other options
54+
invalidWhereValuesBehavior: {
55+
null: "ignore" | "sql-null" | "throw",
56+
undefined: "ignore" | "throw",
57+
},
58+
})
59+
```
60+
61+
### Null Behavior Options
62+
63+
The `null` behavior can be set to one of three values:
64+
65+
#### `'ignore'` (default)
66+
67+
JavaScript `null` values in where conditions are ignored and the property is skipped:
68+
69+
```typescript
70+
const dataSource = new DataSource({
71+
// ... other options
72+
invalidWhereValuesBehavior: {
73+
null: "ignore",
74+
},
75+
})
76+
77+
// This will return all posts, ignoring the text property
78+
const posts = await repository.find({
79+
where: {
80+
text: null,
81+
},
82+
})
83+
```
84+
85+
#### `'sql-null'`
86+
87+
JavaScript `null` values are transformed into SQL `NULL` conditions:
88+
89+
```typescript
90+
const dataSource = new DataSource({
91+
// ... other options
92+
invalidWhereValuesBehavior: {
93+
null: "sql-null",
94+
},
95+
})
96+
97+
// This will only return posts where the text column is NULL in the database
98+
const posts = await repository.find({
99+
where: {
100+
text: null,
101+
},
102+
})
103+
```
104+
105+
#### `'throw'`
106+
107+
JavaScript `null` values cause a TypeORMError to be thrown:
108+
109+
```typescript
110+
const dataSource = new DataSource({
111+
// ... other options
112+
invalidWhereValuesBehavior: {
113+
null: "throw",
114+
},
115+
})
116+
117+
// This will throw an error
118+
const posts = await repository.find({
119+
where: {
120+
text: null,
121+
},
122+
})
123+
// Error: Null value encountered in property 'text' of a where condition.
124+
// To match with SQL NULL, the IsNull() operator must be used.
125+
// Set 'invalidWhereValuesBehavior.null' to 'ignore' or 'sql-null' in connection options to skip or handle null values.
126+
```
127+
128+
### Undefined Behavior Options
129+
130+
The `undefined` behavior can be set to one of two values:
131+
132+
#### `'ignore'` (default)
133+
134+
JavaScript `undefined` values in where conditions are ignored and the property is skipped:
135+
136+
```typescript
137+
const dataSource = new DataSource({
138+
// ... other options
139+
invalidWhereValuesBehavior: {
140+
undefined: "ignore",
141+
},
142+
})
143+
144+
// This will return all posts, ignoring the text property
145+
const posts = await repository.find({
146+
where: {
147+
text: undefined,
148+
},
149+
})
150+
```
151+
152+
#### `'throw'`
153+
154+
JavaScript `undefined` values cause a TypeORMError to be thrown:
155+
156+
```typescript
157+
const dataSource = new DataSource({
158+
// ... other options
159+
invalidWhereValuesBehavior: {
160+
undefined: "throw",
161+
},
162+
})
163+
164+
// This will throw an error
165+
const posts = await repository.find({
166+
where: {
167+
text: undefined,
168+
},
169+
})
170+
// Error: Undefined value encountered in property 'text' of a where condition.
171+
// Set 'invalidWhereValuesBehavior.undefined' to 'ignore' in connection options to skip properties with undefined values.
172+
```
173+
174+
Note that this only applies to explicitly set `undefined` values, not omitted properties.
175+
176+
## Using Both Options Together
177+
178+
You can configure both behaviors independently for comprehensive control:
179+
180+
```typescript
181+
const dataSource = new DataSource({
182+
// ... other options
183+
invalidWhereValuesBehavior: {
184+
null: "sql-null",
185+
undefined: "throw",
186+
},
187+
})
188+
```
189+
190+
This configuration will:
191+
192+
1. Transform JavaScript `null` values to SQL `NULL` in where conditions
193+
2. Throw an error if any `undefined` values are encountered
194+
3. Still ignore properties that are not provided in the where clause
195+
196+
This combination is useful when you want to:
197+
198+
- Be explicit about searching for NULL values in the database
199+
- Catch potential programming errors where undefined values might slip into your queries
200+
201+
## Works with all where operations
202+
203+
The `invalidWhereValuesBehavior` configuration applies to **all TypeORM operations** that support where conditions, not just repository find methods:
204+
205+
### Query Builders
206+
207+
```typescript
208+
// UpdateQueryBuilder
209+
await dataSource
210+
.createQueryBuilder()
211+
.update(Post)
212+
.set({ title: "Updated" })
213+
.where({ text: null }) // Respects invalidWhereValuesBehavior
214+
.execute()
215+
216+
// DeleteQueryBuilder
217+
await dataSource
218+
.createQueryBuilder()
219+
.delete()
220+
.from(Post)
221+
.where({ text: null }) // Respects invalidWhereValuesBehavior
222+
.execute()
223+
224+
// SoftDeleteQueryBuilder
225+
await dataSource
226+
.createQueryBuilder()
227+
.softDelete()
228+
.from(Post)
229+
.where({ text: null }) // Respects invalidWhereValuesBehavior
230+
.execute()
231+
```
232+
233+
### Repository Methods
234+
235+
```typescript
236+
// Repository.update()
237+
await repository.update({ text: null }, { title: "Updated" }) // Respects invalidWhereValuesBehavior
238+
239+
// Repository.delete()
240+
await repository.delete({ text: null }) // Respects invalidWhereValuesBehavior
241+
242+
// EntityManager.update()
243+
await manager.update(Post, { text: null }, { title: "Updated" }) // Respects invalidWhereValuesBehavior
244+
245+
// EntityManager.delete()
246+
await manager.delete(Post, { text: null }) // Respects invalidWhereValuesBehavior
247+
248+
// EntityManager.softDelete()
249+
await manager.softDelete(Post, { text: null }) // Respects invalidWhereValuesBehavior
250+
```
251+
252+
All these operations will consistently apply your configured `invalidWhereValuesBehavior` settings.

src/data-source/BaseDataSourceOptions.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,4 +214,24 @@ export interface BaseDataSourceOptions {
214214
* Allows automatic isolation of where clauses
215215
*/
216216
readonly isolateWhereStatements?: boolean
217+
218+
/**
219+
* Controls how null and undefined values are handled in find operations.
220+
*/
221+
readonly invalidWhereValuesBehavior?: {
222+
/**
223+
* How to handle null values in where conditions.
224+
* - 'ignore': Skip null properties (default)
225+
* - 'sql-null': Transform null to SQL NULL
226+
* - 'throw': Throw an error when null is encountered
227+
*/
228+
readonly null?: "ignore" | "sql-null" | "throw"
229+
230+
/**
231+
* How to handle undefined values in where conditions.
232+
* - 'ignore': Skip undefined properties (default)
233+
* - 'throw': Throw an error when undefined is encountered
234+
*/
235+
readonly undefined?: "ignore" | "throw"
236+
}
217237
}

src/query-builder/QueryBuilder.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1581,18 +1581,37 @@ export abstract class QueryBuilder<Entity extends ObjectLiteral> {
15811581
parameters: [aliasPath, ...parameters],
15821582
}
15831583
}
1584-
// } else if (parameterValue === null) {
1585-
// return {
1586-
// operator: "isNull",
1587-
// parameters: [
1588-
// aliasPath,
1589-
// ]
1590-
// };
1591-
} else {
1592-
return {
1593-
operator: "equal",
1594-
parameters: [aliasPath, this.createParameter(parameterValue)],
1584+
} else if (parameterValue === null) {
1585+
const nullBehavior =
1586+
this.connection.options.invalidWhereValuesBehavior?.null ||
1587+
"ignore"
1588+
if (nullBehavior === "sql-null") {
1589+
return {
1590+
operator: "isNull",
1591+
parameters: [aliasPath],
1592+
}
1593+
} else if (nullBehavior === "throw") {
1594+
throw new TypeORMError(
1595+
`Null value encountered in property '${aliasPath}' of a where condition. ` +
1596+
`To match with SQL NULL, the IsNull() operator must be used. ` +
1597+
`Set 'invalidWhereValuesBehavior.null' to 'ignore' or 'sql-null' in connection options to skip or handle null values.`,
1598+
)
15951599
}
1600+
} else if (parameterValue === undefined) {
1601+
const undefinedBehavior =
1602+
this.connection.options.invalidWhereValuesBehavior?.undefined ||
1603+
"ignore"
1604+
if (undefinedBehavior === "throw") {
1605+
throw new TypeORMError(
1606+
`Undefined value encountered in property '${aliasPath}' of a where condition. ` +
1607+
`Set 'invalidWhereValuesBehavior.undefined' to 'ignore' in connection options to skip properties with undefined values.`,
1608+
)
1609+
}
1610+
}
1611+
1612+
return {
1613+
operator: "equal",
1614+
parameters: [aliasPath, this.createParameter(parameterValue)],
15961615
}
15971616
}
15981617

0 commit comments

Comments
 (0)