metal-orm 1.0.90 → 1.0.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +214 -118
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +71 -32
- package/dist/index.d.ts +71 -32
- package/dist/index.js +206 -118
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/scripts/generate-entities/render.mjs +16 -3
- package/src/core/ddl/introspect/utils.ts +45 -45
- package/src/decorators/bootstrap.ts +37 -37
- package/src/decorators/column-decorator.ts +3 -1
- package/src/dto/apply-filter.ts +279 -281
- package/src/dto/dto-types.ts +229 -229
- package/src/dto/filter-types.ts +193 -193
- package/src/dto/index.ts +97 -97
- package/src/dto/openapi/generators/base.ts +29 -29
- package/src/dto/openapi/generators/column.ts +37 -34
- package/src/dto/openapi/generators/dto.ts +94 -94
- package/src/dto/openapi/generators/filter.ts +75 -74
- package/src/dto/openapi/generators/nested-dto.ts +618 -532
- package/src/dto/openapi/generators/pagination.ts +111 -111
- package/src/dto/openapi/generators/relation-filter.ts +228 -210
- package/src/dto/openapi/index.ts +17 -17
- package/src/dto/openapi/type-mappings.ts +191 -191
- package/src/dto/openapi/types.ts +101 -83
- package/src/dto/openapi/utilities.ts +90 -45
- package/src/dto/pagination-utils.ts +150 -150
- package/src/dto/transform.ts +197 -193
- package/src/index.ts +69 -69
- package/src/orm/entity-context.ts +9 -9
- package/src/orm/entity-metadata.ts +14 -14
- package/src/orm/entity.ts +74 -74
- package/src/orm/orm-session.ts +159 -159
- package/src/orm/relation-change-processor.ts +3 -3
- package/src/orm/runtime-types.ts +5 -5
- package/src/schema/column-types.ts +4 -4
- package/src/schema/types.ts +5 -1
package/src/dto/apply-filter.ts
CHANGED
|
@@ -1,281 +1,279 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runtime filter application - converts JSON filter objects to query builder conditions.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { TableDef } from '../schema/table.js';
|
|
6
|
-
import type { ColumnDef } from '../schema/column-types.js';
|
|
7
|
-
import type { SelectQueryBuilder } from '../query-builder/select.js';
|
|
8
|
-
import type { WhereInput, FilterValue, StringFilter } from './filter-types.js';
|
|
9
|
-
import type { EntityConstructor } from '../orm/entity-metadata.js';
|
|
10
|
-
import { getTableDefFromEntity } from '../decorators/bootstrap.js';
|
|
11
|
-
import {
|
|
12
|
-
eq,
|
|
13
|
-
neq,
|
|
14
|
-
gt,
|
|
15
|
-
gte,
|
|
16
|
-
lt,
|
|
17
|
-
lte,
|
|
18
|
-
like,
|
|
19
|
-
inList,
|
|
20
|
-
notInList,
|
|
21
|
-
and,
|
|
22
|
-
type ExpressionNode
|
|
23
|
-
} from '../core/ast/expression.js';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Builds an expression node from a single field filter.
|
|
27
|
-
*/
|
|
28
|
-
function buildFieldExpression(
|
|
29
|
-
column: ColumnDef,
|
|
30
|
-
filter: FilterValue
|
|
31
|
-
): ExpressionNode | null {
|
|
32
|
-
const expressions: ExpressionNode[] = [];
|
|
33
|
-
|
|
34
|
-
// String filters
|
|
35
|
-
if ('contains' in filter && filter.contains !== undefined) {
|
|
36
|
-
const pattern = `%${escapePattern(filter.contains)}%`;
|
|
37
|
-
const mode = (filter as StringFilter).mode;
|
|
38
|
-
if (mode === 'insensitive') {
|
|
39
|
-
expressions.push(caseInsensitiveLike(column, pattern));
|
|
40
|
-
} else {
|
|
41
|
-
expressions.push(like(column, pattern));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if ('startsWith' in filter && filter.startsWith !== undefined) {
|
|
46
|
-
const pattern = `${escapePattern(filter.startsWith)}%`;
|
|
47
|
-
const mode = (filter as StringFilter).mode;
|
|
48
|
-
if (mode === 'insensitive') {
|
|
49
|
-
expressions.push(caseInsensitiveLike(column, pattern));
|
|
50
|
-
} else {
|
|
51
|
-
expressions.push(like(column, pattern));
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if ('endsWith' in filter && filter.endsWith !== undefined) {
|
|
56
|
-
const pattern = `%${escapePattern(filter.endsWith)}`;
|
|
57
|
-
const mode = (filter as StringFilter).mode;
|
|
58
|
-
if (mode === 'insensitive') {
|
|
59
|
-
expressions.push(caseInsensitiveLike(column, pattern));
|
|
60
|
-
} else {
|
|
61
|
-
expressions.push(like(column, pattern));
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Common filters (equals, not, in, notIn)
|
|
66
|
-
if ('equals' in filter && filter.equals !== undefined) {
|
|
67
|
-
expressions.push(eq(column, filter.equals as string | number | boolean));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if ('not' in filter && filter.not !== undefined) {
|
|
71
|
-
expressions.push(neq(column, filter.not as string | number | boolean));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if ('in' in filter && filter.in !== undefined) {
|
|
75
|
-
expressions.push(inList(column, filter.in as (string | number)[]));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if ('notIn' in filter && filter.notIn !== undefined) {
|
|
79
|
-
expressions.push(notInList(column, filter.notIn as (string | number)[]));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Comparison filters (lt, lte, gt, gte)
|
|
83
|
-
if ('lt' in filter && filter.lt !== undefined) {
|
|
84
|
-
expressions.push(lt(column, filter.lt as string | number));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if ('lte' in filter && filter.lte !== undefined) {
|
|
88
|
-
expressions.push(lte(column, filter.lte as string | number));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if ('gt' in filter && filter.gt !== undefined) {
|
|
92
|
-
expressions.push(gt(column, filter.gt as string | number));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if ('gte' in filter && filter.gte !== undefined) {
|
|
96
|
-
expressions.push(gte(column, filter.gte as string | number));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (expressions.length === 0) {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (expressions.length === 1) {
|
|
104
|
-
return expressions[0];
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return and(...expressions);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Escapes special LIKE pattern characters.
|
|
112
|
-
*/
|
|
113
|
-
function escapePattern(value: string): string {
|
|
114
|
-
return value.replace(/[%_\\]/g, '\\$&');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Creates a case-insensitive LIKE expression using LOWER().
|
|
119
|
-
* This is portable across all SQL dialects.
|
|
120
|
-
*/
|
|
121
|
-
function caseInsensitiveLike(column: ColumnDef, pattern: string): ExpressionNode {
|
|
122
|
-
return {
|
|
123
|
-
type: 'BinaryExpression',
|
|
124
|
-
left: {
|
|
125
|
-
type: 'Function',
|
|
126
|
-
name: 'LOWER',
|
|
127
|
-
args: [{ type: 'Column', table: column.table!, name: column.name }]
|
|
128
|
-
},
|
|
129
|
-
operator: 'LIKE',
|
|
130
|
-
right: { type: 'Literal', value: pattern.toLowerCase() }
|
|
131
|
-
} as unknown as ExpressionNode;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Applies a filter object to a SelectQueryBuilder.
|
|
136
|
-
* All conditions are AND-ed together.
|
|
137
|
-
*
|
|
138
|
-
* @param qb - The query builder to apply filters to
|
|
139
|
-
* @param tableOrEntity - The table definition or entity constructor (used to resolve column references)
|
|
140
|
-
* @param where - The filter object from: API request
|
|
141
|
-
* @returns The query builder with filters applied
|
|
142
|
-
*
|
|
143
|
-
* @example
|
|
144
|
-
* ```ts
|
|
145
|
-
* // In a controller - using Entity class
|
|
146
|
-
* @Get()
|
|
147
|
-
* async list(@Query() where?: UserFilter): Promise<UserResponse[]> {
|
|
148
|
-
* let query = selectFromEntity(User);
|
|
149
|
-
* query = applyFilter(query, User, where);
|
|
150
|
-
* return query.execute(db);
|
|
151
|
-
* }
|
|
152
|
-
*
|
|
153
|
-
* // Using TableDef directly
|
|
154
|
-
* @Get()
|
|
155
|
-
* async list(@Query() where?: UserFilter): Promise<UserResponse[]> {
|
|
156
|
-
* let query = selectFrom(usersTable);
|
|
157
|
-
* query = applyFilter(query, usersTable, where);
|
|
158
|
-
* return query.execute(db);
|
|
159
|
-
* }
|
|
160
|
-
*
|
|
161
|
-
* // Request: { "name": { "contains": "john" }, "email": { "endsWith": "@gmail.com" } }
|
|
162
|
-
* // SQL: WHERE name LIKE '%john%' AND email LIKE '%@gmail.com'
|
|
163
|
-
* ```
|
|
164
|
-
*/
|
|
165
|
-
export function applyFilter<T, TTable extends TableDef>(
|
|
166
|
-
qb: SelectQueryBuilder<T, TTable>,
|
|
167
|
-
tableOrEntity: TTable | EntityConstructor,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* @param
|
|
221
|
-
* @
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
where
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return and(...expressions);
|
|
281
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Runtime filter application - converts JSON filter objects to query builder conditions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { TableDef } from '../schema/table.js';
|
|
6
|
+
import type { ColumnDef } from '../schema/column-types.js';
|
|
7
|
+
import type { SelectQueryBuilder } from '../query-builder/select.js';
|
|
8
|
+
import type { WhereInput, FilterValue, StringFilter } from './filter-types.js';
|
|
9
|
+
import type { EntityConstructor } from '../orm/entity-metadata.js';
|
|
10
|
+
import { getTableDefFromEntity } from '../decorators/bootstrap.js';
|
|
11
|
+
import {
|
|
12
|
+
eq,
|
|
13
|
+
neq,
|
|
14
|
+
gt,
|
|
15
|
+
gte,
|
|
16
|
+
lt,
|
|
17
|
+
lte,
|
|
18
|
+
like,
|
|
19
|
+
inList,
|
|
20
|
+
notInList,
|
|
21
|
+
and,
|
|
22
|
+
type ExpressionNode
|
|
23
|
+
} from '../core/ast/expression.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Builds an expression node from a single field filter.
|
|
27
|
+
*/
|
|
28
|
+
function buildFieldExpression(
|
|
29
|
+
column: ColumnDef,
|
|
30
|
+
filter: FilterValue
|
|
31
|
+
): ExpressionNode | null {
|
|
32
|
+
const expressions: ExpressionNode[] = [];
|
|
33
|
+
|
|
34
|
+
// String filters
|
|
35
|
+
if ('contains' in filter && filter.contains !== undefined) {
|
|
36
|
+
const pattern = `%${escapePattern(filter.contains)}%`;
|
|
37
|
+
const mode = (filter as StringFilter).mode;
|
|
38
|
+
if (mode === 'insensitive') {
|
|
39
|
+
expressions.push(caseInsensitiveLike(column, pattern));
|
|
40
|
+
} else {
|
|
41
|
+
expressions.push(like(column, pattern));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if ('startsWith' in filter && filter.startsWith !== undefined) {
|
|
46
|
+
const pattern = `${escapePattern(filter.startsWith)}%`;
|
|
47
|
+
const mode = (filter as StringFilter).mode;
|
|
48
|
+
if (mode === 'insensitive') {
|
|
49
|
+
expressions.push(caseInsensitiveLike(column, pattern));
|
|
50
|
+
} else {
|
|
51
|
+
expressions.push(like(column, pattern));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if ('endsWith' in filter && filter.endsWith !== undefined) {
|
|
56
|
+
const pattern = `%${escapePattern(filter.endsWith)}`;
|
|
57
|
+
const mode = (filter as StringFilter).mode;
|
|
58
|
+
if (mode === 'insensitive') {
|
|
59
|
+
expressions.push(caseInsensitiveLike(column, pattern));
|
|
60
|
+
} else {
|
|
61
|
+
expressions.push(like(column, pattern));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Common filters (equals, not, in, notIn)
|
|
66
|
+
if ('equals' in filter && filter.equals !== undefined) {
|
|
67
|
+
expressions.push(eq(column, filter.equals as string | number | boolean));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if ('not' in filter && filter.not !== undefined) {
|
|
71
|
+
expressions.push(neq(column, filter.not as string | number | boolean));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if ('in' in filter && filter.in !== undefined) {
|
|
75
|
+
expressions.push(inList(column, filter.in as (string | number)[]));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if ('notIn' in filter && filter.notIn !== undefined) {
|
|
79
|
+
expressions.push(notInList(column, filter.notIn as (string | number)[]));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Comparison filters (lt, lte, gt, gte)
|
|
83
|
+
if ('lt' in filter && filter.lt !== undefined) {
|
|
84
|
+
expressions.push(lt(column, filter.lt as string | number));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if ('lte' in filter && filter.lte !== undefined) {
|
|
88
|
+
expressions.push(lte(column, filter.lte as string | number));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if ('gt' in filter && filter.gt !== undefined) {
|
|
92
|
+
expressions.push(gt(column, filter.gt as string | number));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if ('gte' in filter && filter.gte !== undefined) {
|
|
96
|
+
expressions.push(gte(column, filter.gte as string | number));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (expressions.length === 0) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (expressions.length === 1) {
|
|
104
|
+
return expressions[0];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return and(...expressions);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Escapes special LIKE pattern characters.
|
|
112
|
+
*/
|
|
113
|
+
function escapePattern(value: string): string {
|
|
114
|
+
return value.replace(/[%_\\]/g, '\\$&');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Creates a case-insensitive LIKE expression using LOWER().
|
|
119
|
+
* This is portable across all SQL dialects.
|
|
120
|
+
*/
|
|
121
|
+
function caseInsensitiveLike(column: ColumnDef, pattern: string): ExpressionNode {
|
|
122
|
+
return {
|
|
123
|
+
type: 'BinaryExpression',
|
|
124
|
+
left: {
|
|
125
|
+
type: 'Function',
|
|
126
|
+
name: 'LOWER',
|
|
127
|
+
args: [{ type: 'Column', table: column.table!, name: column.name }]
|
|
128
|
+
},
|
|
129
|
+
operator: 'LIKE',
|
|
130
|
+
right: { type: 'Literal', value: pattern.toLowerCase() }
|
|
131
|
+
} as unknown as ExpressionNode;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Applies a filter object to a SelectQueryBuilder.
|
|
136
|
+
* All conditions are AND-ed together.
|
|
137
|
+
*
|
|
138
|
+
* @param qb - The query builder to apply filters to
|
|
139
|
+
* @param tableOrEntity - The table definition or entity constructor (used to resolve column references)
|
|
140
|
+
* @param where - The filter object from: API request
|
|
141
|
+
* @returns The query builder with filters applied
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```ts
|
|
145
|
+
* // In a controller - using Entity class
|
|
146
|
+
* @Get()
|
|
147
|
+
* async list(@Query() where?: UserFilter): Promise<UserResponse[]> {
|
|
148
|
+
* let query = selectFromEntity(User);
|
|
149
|
+
* query = applyFilter(query, User, where);
|
|
150
|
+
* return query.execute(db);
|
|
151
|
+
* }
|
|
152
|
+
*
|
|
153
|
+
* // Using TableDef directly
|
|
154
|
+
* @Get()
|
|
155
|
+
* async list(@Query() where?: UserFilter): Promise<UserResponse[]> {
|
|
156
|
+
* let query = selectFrom(usersTable);
|
|
157
|
+
* query = applyFilter(query, usersTable, where);
|
|
158
|
+
* return query.execute(db);
|
|
159
|
+
* }
|
|
160
|
+
*
|
|
161
|
+
* // Request: { "name": { "contains": "john" }, "email": { "endsWith": "@gmail.com" } }
|
|
162
|
+
* // SQL: WHERE name LIKE '%john%' AND email LIKE '%@gmail.com'
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
export function applyFilter<T, TTable extends TableDef>(
|
|
166
|
+
qb: SelectQueryBuilder<T, TTable>,
|
|
167
|
+
tableOrEntity: TTable | EntityConstructor,
|
|
168
|
+
where?: WhereInput<TTable | EntityConstructor> | null
|
|
169
|
+
): SelectQueryBuilder<T, TTable> {
|
|
170
|
+
if (!where) {
|
|
171
|
+
return qb;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const table = isEntityConstructor(tableOrEntity)
|
|
175
|
+
? getTableDefFromEntity(tableOrEntity)
|
|
176
|
+
: tableOrEntity;
|
|
177
|
+
|
|
178
|
+
if (!table) {
|
|
179
|
+
return qb;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const expressions: ExpressionNode[] = [];
|
|
183
|
+
|
|
184
|
+
for (const [fieldName, fieldFilter] of Object.entries(where)) {
|
|
185
|
+
if (fieldFilter === undefined || fieldFilter === null) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const column = table.columns[fieldName];
|
|
190
|
+
if (!column) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const expr = buildFieldExpression(column, fieldFilter as FilterValue);
|
|
195
|
+
if (expr) {
|
|
196
|
+
expressions.push(expr);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (expressions.length === 0) {
|
|
201
|
+
return qb;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (expressions.length === 1) {
|
|
205
|
+
return qb.where(expressions[0]);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return qb.where(and(...expressions));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function isEntityConstructor(value: unknown): value is EntityConstructor {
|
|
212
|
+
return typeof value === 'function' && value.prototype?.constructor === value;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Builds an expression tree from a filter object without applying it.
|
|
217
|
+
* Useful for combining with other conditions.
|
|
218
|
+
*
|
|
219
|
+
* @param tableOrEntity - The table definition or entity constructor
|
|
220
|
+
* @param where - The filter object
|
|
221
|
+
* @returns An expression node or null if no filters
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```ts
|
|
225
|
+
* // Using Entity class
|
|
226
|
+
* const filterExpr = buildFilterExpression(User, { name: { contains: "john" } });
|
|
227
|
+
*
|
|
228
|
+
* // Using TableDef directly
|
|
229
|
+
* const filterExpr = buildFilterExpression(usersTable, { name: { contains: "john" } });
|
|
230
|
+
*
|
|
231
|
+
* if (filterExpr) {
|
|
232
|
+
* qb = qb.where(and(filterExpr, eq(users.columns.active, true)));
|
|
233
|
+
* }
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
export function buildFilterExpression(
|
|
237
|
+
tableOrEntity: TableDef | EntityConstructor,
|
|
238
|
+
where?: WhereInput<TableDef | EntityConstructor> | null
|
|
239
|
+
): ExpressionNode | null {
|
|
240
|
+
if (!where) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const table = isEntityConstructor(tableOrEntity)
|
|
245
|
+
? getTableDefFromEntity(tableOrEntity)
|
|
246
|
+
: tableOrEntity;
|
|
247
|
+
|
|
248
|
+
if (!table) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const expressions: ExpressionNode[] = [];
|
|
253
|
+
|
|
254
|
+
for (const [fieldName, fieldFilter] of Object.entries(where)) {
|
|
255
|
+
if (fieldFilter === undefined || fieldFilter === null) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const column = table.columns[fieldName];
|
|
260
|
+
if (!column) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const expr = buildFieldExpression(column, fieldFilter as FilterValue);
|
|
265
|
+
if (expr) {
|
|
266
|
+
expressions.push(expr);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (expressions.length === 0) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (expressions.length === 1) {
|
|
275
|
+
return expressions[0];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return and(...expressions);
|
|
279
|
+
}
|