metal-orm 1.1.9 → 1.1.11
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/README.md +769 -764
- package/dist/index.cjs +2255 -284
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +559 -39
- package/dist/index.d.ts +559 -39
- package/dist/index.js +2227 -284
- package/dist/index.js.map +1 -1
- package/package.json +17 -12
- package/scripts/generate-entities/render.mjs +21 -12
- package/scripts/generate-entities/schema.mjs +87 -73
- package/scripts/generate-entities/tree-detection.mjs +67 -61
- package/src/bulk/bulk-context.ts +83 -0
- package/src/bulk/bulk-delete-executor.ts +87 -0
- package/src/bulk/bulk-executor.base.ts +73 -0
- package/src/bulk/bulk-insert-executor.ts +74 -0
- package/src/bulk/bulk-types.ts +70 -0
- package/src/bulk/bulk-update-executor.ts +192 -0
- package/src/bulk/bulk-upsert-executor.ts +93 -0
- package/src/bulk/bulk-utils.ts +91 -0
- package/src/bulk/index.ts +18 -0
- package/src/codegen/typescript.ts +30 -21
- package/src/core/ast/expression-builders.ts +107 -10
- package/src/core/ast/expression-nodes.ts +52 -22
- package/src/core/ast/expression-visitor.ts +23 -13
- package/src/core/ddl/introspect/mysql.ts +113 -36
- package/src/core/dialect/abstract.ts +30 -17
- package/src/core/dialect/mysql/index.ts +20 -5
- package/src/core/execution/db-executor.ts +96 -64
- package/src/core/execution/executors/better-sqlite3-executor.ts +94 -0
- package/src/core/execution/executors/mssql-executor.ts +66 -34
- package/src/core/execution/executors/mysql-executor.ts +98 -66
- package/src/core/execution/executors/postgres-executor.ts +33 -11
- package/src/core/execution/executors/sqlite-executor.ts +86 -30
- package/src/decorators/bootstrap.ts +482 -398
- package/src/decorators/column-decorator.ts +87 -96
- package/src/decorators/decorator-metadata.ts +100 -24
- package/src/decorators/entity.ts +27 -24
- package/src/decorators/relations.ts +231 -149
- package/src/decorators/transformers/transformer-decorators.ts +26 -29
- package/src/decorators/validators/country-validators-decorators.ts +9 -15
- package/src/dto/apply-filter.ts +568 -551
- package/src/index.ts +16 -9
- package/src/orm/entity-hydration.ts +116 -72
- package/src/orm/entity-metadata.ts +347 -301
- package/src/orm/entity-relations.ts +264 -207
- package/src/orm/entity.ts +199 -199
- package/src/orm/execute.ts +13 -13
- package/src/orm/lazy-batch/morph-many.ts +70 -0
- package/src/orm/lazy-batch/morph-one.ts +69 -0
- package/src/orm/lazy-batch/morph-to.ts +59 -0
- package/src/orm/lazy-batch.ts +4 -1
- package/src/orm/orm-session.ts +170 -104
- package/src/orm/pooled-executor-factory.ts +99 -58
- package/src/orm/query-logger.ts +49 -40
- package/src/orm/relation-change-processor.ts +198 -96
- package/src/orm/relations/belongs-to.ts +143 -143
- package/src/orm/relations/has-many.ts +204 -204
- package/src/orm/relations/has-one.ts +174 -174
- package/src/orm/relations/many-to-many.ts +288 -288
- package/src/orm/relations/morph-many.ts +156 -0
- package/src/orm/relations/morph-one.ts +151 -0
- package/src/orm/relations/morph-to.ts +162 -0
- package/src/orm/save-graph.ts +116 -1
- package/src/query-builder/expression-table-mapper.ts +5 -0
- package/src/query-builder/hydration-manager.ts +345 -345
- package/src/query-builder/hydration-planner.ts +178 -148
- package/src/query-builder/relation-conditions.ts +171 -151
- package/src/query-builder/relation-cte-builder.ts +5 -1
- package/src/query-builder/relation-filter-utils.ts +9 -6
- package/src/query-builder/relation-include-strategies.ts +44 -2
- package/src/query-builder/relation-join-strategies.ts +8 -1
- package/src/query-builder/relation-service.ts +250 -241
- package/src/query-builder/select/select-operations.ts +110 -105
- package/src/query-builder/update-include.ts +4 -0
- package/src/schema/relation.ts +296 -188
- package/src/schema/types.ts +138 -123
- package/src/tree/tree-decorator.ts +127 -137
package/src/dto/apply-filter.ts
CHANGED
|
@@ -1,245 +1,247 @@
|
|
|
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 { SelectQueryBuilder } from '../query-builder/select.js';
|
|
8
|
-
import type { WhereInput, FilterValue, StringFilter, RelationFilter } 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
|
-
exists,
|
|
23
|
-
notExists,
|
|
24
|
-
type ColumnNode,
|
|
25
|
-
type ExpressionNode
|
|
26
|
-
} from '../core/ast/expression.js';
|
|
27
|
-
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
28
|
-
import { RelationKinds, type RelationDef, type BelongsToManyRelation } from '../schema/relation.js';
|
|
29
|
-
import type { SelectQueryNode, TableSourceNode } from '../core/ast/query.js';
|
|
30
|
-
import type { JoinNode } from '../core/ast/join.js';
|
|
31
|
-
import { createJoinNode } from '../core/ast/join-node.js';
|
|
32
|
-
import { JOIN_KINDS } from '../core/sql/sql.js';
|
|
33
|
-
import { buildRelationCorrelation } from '../query-builder/relation-conditions.js';
|
|
34
|
-
import { updateInclude } from '../query-builder/update-include.js';
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Options for applyFilter to control relation filtering behavior.
|
|
38
|
-
*/
|
|
39
|
-
export interface ApplyFilterOptions<TTable extends TableDef> {
|
|
40
|
-
relations?: {
|
|
41
|
-
[K in keyof TTable['relations']]?: boolean | {
|
|
42
|
-
relations?:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
expressions.push(
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
expressions.push(
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
expressions.push(
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* @param
|
|
163
|
-
* @param
|
|
164
|
-
* @
|
|
165
|
-
*
|
|
166
|
-
* @
|
|
167
|
-
*
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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 { SelectQueryBuilder } from '../query-builder/select.js';
|
|
8
|
+
import type { WhereInput, FilterValue, StringFilter, RelationFilter } 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
|
+
exists,
|
|
23
|
+
notExists,
|
|
24
|
+
type ColumnNode,
|
|
25
|
+
type ExpressionNode
|
|
26
|
+
} from '../core/ast/expression.js';
|
|
27
|
+
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
28
|
+
import { RelationKinds, type RelationDef, type BelongsToManyRelation, isSingleTargetRelation } from '../schema/relation.js';
|
|
29
|
+
import type { SelectQueryNode, TableSourceNode } from '../core/ast/query.js';
|
|
30
|
+
import type { JoinNode } from '../core/ast/join.js';
|
|
31
|
+
import { createJoinNode } from '../core/ast/join-node.js';
|
|
32
|
+
import { JOIN_KINDS } from '../core/sql/sql.js';
|
|
33
|
+
import { buildRelationCorrelation } from '../query-builder/relation-conditions.js';
|
|
34
|
+
import { updateInclude } from '../query-builder/update-include.js';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Options for applyFilter to control relation filtering behavior.
|
|
38
|
+
*/
|
|
39
|
+
export interface ApplyFilterOptions<TTable extends TableDef> {
|
|
40
|
+
relations?: {
|
|
41
|
+
[K in keyof TTable['relations']]?: boolean | {
|
|
42
|
+
relations?: TTable['relations'][K] extends { target: infer TTarget extends TableDef }
|
|
43
|
+
? ApplyFilterOptions<TTarget>['relations']
|
|
44
|
+
: never;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Builds an expression node from a single field filter.
|
|
51
|
+
*/
|
|
52
|
+
function buildFieldExpression(
|
|
53
|
+
column: ColumnDef,
|
|
54
|
+
filter: FilterValue
|
|
55
|
+
): ExpressionNode | null {
|
|
56
|
+
const expressions: ExpressionNode[] = [];
|
|
57
|
+
|
|
58
|
+
// String filters
|
|
59
|
+
if ('contains' in filter && filter.contains !== undefined) {
|
|
60
|
+
const pattern = `%${escapePattern(filter.contains)}%`;
|
|
61
|
+
const mode = (filter as StringFilter).mode;
|
|
62
|
+
if (mode === 'insensitive') {
|
|
63
|
+
expressions.push(caseInsensitiveLike(column, pattern));
|
|
64
|
+
} else {
|
|
65
|
+
expressions.push(like(column, pattern));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if ('startsWith' in filter && filter.startsWith !== undefined) {
|
|
70
|
+
const pattern = `${escapePattern(filter.startsWith)}%`;
|
|
71
|
+
const mode = (filter as StringFilter).mode;
|
|
72
|
+
if (mode === 'insensitive') {
|
|
73
|
+
expressions.push(caseInsensitiveLike(column, pattern));
|
|
74
|
+
} else {
|
|
75
|
+
expressions.push(like(column, pattern));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if ('endsWith' in filter && filter.endsWith !== undefined) {
|
|
80
|
+
const pattern = `%${escapePattern(filter.endsWith)}`;
|
|
81
|
+
const mode = (filter as StringFilter).mode;
|
|
82
|
+
if (mode === 'insensitive') {
|
|
83
|
+
expressions.push(caseInsensitiveLike(column, pattern));
|
|
84
|
+
} else {
|
|
85
|
+
expressions.push(like(column, pattern));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Common filters (equals, not, in, notIn)
|
|
90
|
+
if ('equals' in filter && filter.equals !== undefined) {
|
|
91
|
+
expressions.push(eq(column, filter.equals as string | number | boolean));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if ('not' in filter && filter.not !== undefined) {
|
|
95
|
+
expressions.push(neq(column, filter.not as string | number | boolean));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if ('in' in filter && filter.in !== undefined) {
|
|
99
|
+
expressions.push(inList(column, filter.in as (string | number)[]));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if ('notIn' in filter && filter.notIn !== undefined) {
|
|
103
|
+
expressions.push(notInList(column, filter.notIn as (string | number)[]));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Comparison filters (lt, lte, gt, gte)
|
|
107
|
+
if ('lt' in filter && filter.lt !== undefined) {
|
|
108
|
+
expressions.push(lt(column, filter.lt as string | number));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if ('lte' in filter && filter.lte !== undefined) {
|
|
112
|
+
expressions.push(lte(column, filter.lte as string | number));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if ('gt' in filter && filter.gt !== undefined) {
|
|
116
|
+
expressions.push(gt(column, filter.gt as string | number));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if ('gte' in filter && filter.gte !== undefined) {
|
|
120
|
+
expressions.push(gte(column, filter.gte as string | number));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (expressions.length === 0) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (expressions.length === 1) {
|
|
128
|
+
return expressions[0];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return and(...expressions);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Escapes special LIKE pattern characters.
|
|
136
|
+
*/
|
|
137
|
+
function escapePattern(value: string): string {
|
|
138
|
+
return value.replace(/[%_\\]/g, '\\$&');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Creates a case-insensitive LIKE expression using LOWER().
|
|
143
|
+
* This is portable across all SQL dialects.
|
|
144
|
+
*/
|
|
145
|
+
function caseInsensitiveLike(column: ColumnDef, pattern: string): ExpressionNode {
|
|
146
|
+
return {
|
|
147
|
+
type: 'BinaryExpression',
|
|
148
|
+
left: {
|
|
149
|
+
type: 'Function',
|
|
150
|
+
name: 'LOWER',
|
|
151
|
+
args: [{ type: 'Column', table: column.table!, name: column.name }]
|
|
152
|
+
},
|
|
153
|
+
operator: 'LIKE',
|
|
154
|
+
right: { type: 'Literal', value: pattern.toLowerCase() }
|
|
155
|
+
} as unknown as ExpressionNode;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Applies a filter object to a SelectQueryBuilder.
|
|
160
|
+
* All conditions are AND-ed together.
|
|
161
|
+
*
|
|
162
|
+
* @param qb - The query builder to apply filters to
|
|
163
|
+
* @param tableOrEntity - The table definition or entity constructor (used to resolve column references)
|
|
164
|
+
* @param where - The filter object from: API request
|
|
165
|
+
* @param options - Options to control relation filtering behavior
|
|
166
|
+
* @returns The query builder with filters applied
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```ts
|
|
170
|
+
* // In a controller - using Entity class
|
|
171
|
+
* @Get()
|
|
172
|
+
* async list(@Query() where?: UserFilter): Promise<UserResponse[]> {
|
|
173
|
+
* let query = selectFromEntity(User);
|
|
174
|
+
* query = applyFilter(query, User, where);
|
|
175
|
+
* return query.execute(db);
|
|
176
|
+
* }
|
|
177
|
+
*
|
|
178
|
+
* // Using TableDef directly
|
|
179
|
+
* @Get()
|
|
180
|
+
* async list(@Query() where?: UserFilter): Promise<UserResponse[]> {
|
|
181
|
+
* let query = selectFrom(usersTable);
|
|
182
|
+
* query = applyFilter(query, usersTable, where);
|
|
183
|
+
* return query.execute(db);
|
|
184
|
+
* }
|
|
185
|
+
*
|
|
186
|
+
* // Request: { "name": { "contains": "john" }, "email": { "endsWith": "@gmail.com" } }
|
|
187
|
+
* // SQL: WHERE name LIKE '%john%' AND email LIKE '%@gmail.com'
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
export function applyFilter<T, TTable extends TableDef>(
|
|
191
|
+
qb: SelectQueryBuilder<T, TTable>,
|
|
192
|
+
tableOrEntity: TTable | EntityConstructor,
|
|
193
|
+
where?: WhereInput<TTable | EntityConstructor> | null,
|
|
194
|
+
_options?: ApplyFilterOptions<TTable>
|
|
195
|
+
): SelectQueryBuilder<T, TTable> {
|
|
196
|
+
if (!where) {
|
|
197
|
+
return qb;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const table = (isEntityConstructor(tableOrEntity)
|
|
201
|
+
? getTableDefFromEntity(tableOrEntity)
|
|
202
|
+
: tableOrEntity) as TTable;
|
|
203
|
+
|
|
204
|
+
if (!table) {
|
|
205
|
+
return qb;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const expressions: ExpressionNode[] = [];
|
|
209
|
+
|
|
210
|
+
for (const [fieldName, fieldFilter] of Object.entries(where)) {
|
|
211
|
+
if (fieldFilter === undefined || fieldFilter === null) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const column = table.columns[fieldName as keyof typeof table.columns];
|
|
216
|
+
if (column) {
|
|
217
|
+
const expr = buildFieldExpression(column, fieldFilter as FilterValue);
|
|
218
|
+
if (expr) {
|
|
219
|
+
expressions.push(expr);
|
|
220
|
+
}
|
|
221
|
+
} else if (table.relations && fieldName in table.relations) {
|
|
222
|
+
const relationFilter = fieldFilter as RelationFilter;
|
|
223
|
+
const relationName = fieldName as keyof TTable['relations'] & string;
|
|
224
|
+
qb = applyRelationFilter(qb, table, relationName, relationFilter);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (expressions.length === 0) {
|
|
229
|
+
return qb;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (expressions.length === 1) {
|
|
233
|
+
return qb.where(expressions[0]);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return qb.where(and(...expressions));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function applyRelationFilter<T, TTable extends TableDef>(
|
|
240
|
+
qb: SelectQueryBuilder<T, TTable>,
|
|
241
|
+
table: TTable,
|
|
242
|
+
relationName: keyof TTable['relations'] & string,
|
|
243
|
+
filter: RelationFilter
|
|
244
|
+
): SelectQueryBuilder<T, TTable> {
|
|
243
245
|
const relation = table.relations[relationName];
|
|
244
246
|
if (!relation) {
|
|
245
247
|
return qb;
|
|
@@ -252,139 +254,150 @@ function applyRelationFilter<T, TTable extends TableDef>(
|
|
|
252
254
|
);
|
|
253
255
|
}
|
|
254
256
|
|
|
257
|
+
if (!isSingleTargetRelation(relation)) {
|
|
258
|
+
return qb;
|
|
259
|
+
}
|
|
260
|
+
|
|
255
261
|
if (filter.some) {
|
|
256
262
|
const predicate = buildFilterExpression(relation.target, filter.some);
|
|
257
263
|
if (predicate) {
|
|
258
|
-
qb = updateInclude(qb, relationName, opts => ({
|
|
259
|
-
...opts,
|
|
260
|
-
joinKind: JOIN_KINDS.INNER,
|
|
261
|
-
filter: opts.filter ? and(opts.filter, predicate) : predicate
|
|
262
|
-
}));
|
|
263
|
-
const pk = findPrimaryKey(table);
|
|
264
|
-
const pkColumn = table.columns[pk];
|
|
265
|
-
qb = pkColumn
|
|
266
|
-
? qb.distinct(pkColumn)
|
|
267
|
-
: qb.distinct({ type: 'Column', table: table.name, name: pk } as ColumnNode);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (filter.none) {
|
|
272
|
-
const predicate = buildFilterExpression(relation.target, filter.none);
|
|
273
|
-
if (predicate) {
|
|
274
|
-
qb = qb.whereHasNot(relationName, (subQb) => subQb.where(predicate));
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (filter.every) {
|
|
279
|
-
const predicate = buildFilterExpression(relation.target, filter.every);
|
|
280
|
-
if (predicate) {
|
|
281
|
-
const pk = findPrimaryKey(table);
|
|
282
|
-
qb = qb.joinRelation(relationName);
|
|
283
|
-
qb = qb.groupBy({ type: 'Column', table: table.name, name: pk }) as typeof qb;
|
|
284
|
-
qb = qb.having(buildEveryHavingClause(relationName, predicate, relation.target)) as typeof qb;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (filter.isEmpty !== undefined) {
|
|
289
|
-
if (filter.isEmpty) {
|
|
290
|
-
qb = qb.whereHasNot(relationName);
|
|
291
|
-
} else {
|
|
292
|
-
qb = qb.whereHas(relationName);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (filter.isNotEmpty !== undefined) {
|
|
297
|
-
if (filter.isNotEmpty) {
|
|
298
|
-
qb = qb.whereHas(relationName);
|
|
299
|
-
} else {
|
|
300
|
-
qb = qb.whereHasNot(relationName);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return qb;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
type RelationSubqueryBase = {
|
|
308
|
-
from: TableSourceNode;
|
|
309
|
-
joins: JoinNode[];
|
|
310
|
-
correlation: ExpressionNode;
|
|
311
|
-
groupByColumn: ColumnNode;
|
|
312
|
-
targetTable: TableDef;
|
|
313
|
-
targetTableName: string;
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
const buildRelationSubqueryBase = (
|
|
317
|
-
table: TableDef,
|
|
318
|
-
relation: RelationDef
|
|
319
|
-
): RelationSubqueryBase => {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
264
|
+
qb = updateInclude(qb, relationName, opts => ({
|
|
265
|
+
...opts,
|
|
266
|
+
joinKind: JOIN_KINDS.INNER,
|
|
267
|
+
filter: opts.filter ? and(opts.filter, predicate) : predicate
|
|
268
|
+
}));
|
|
269
|
+
const pk = findPrimaryKey(table);
|
|
270
|
+
const pkColumn = table.columns[pk];
|
|
271
|
+
qb = pkColumn
|
|
272
|
+
? qb.distinct(pkColumn)
|
|
273
|
+
: qb.distinct({ type: 'Column', table: table.name, name: pk } as ColumnNode);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (filter.none) {
|
|
278
|
+
const predicate = buildFilterExpression(relation.target, filter.none);
|
|
279
|
+
if (predicate) {
|
|
280
|
+
qb = qb.whereHasNot(relationName, (subQb) => subQb.where(predicate));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (filter.every) {
|
|
285
|
+
const predicate = buildFilterExpression(relation.target, filter.every);
|
|
286
|
+
if (predicate) {
|
|
287
|
+
const pk = findPrimaryKey(table);
|
|
288
|
+
qb = qb.joinRelation(relationName);
|
|
289
|
+
qb = qb.groupBy({ type: 'Column', table: table.name, name: pk }) as typeof qb;
|
|
290
|
+
qb = qb.having(buildEveryHavingClause(relationName, predicate, relation.target)) as typeof qb;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (filter.isEmpty !== undefined) {
|
|
295
|
+
if (filter.isEmpty) {
|
|
296
|
+
qb = qb.whereHasNot(relationName);
|
|
297
|
+
} else {
|
|
298
|
+
qb = qb.whereHas(relationName);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (filter.isNotEmpty !== undefined) {
|
|
303
|
+
if (filter.isNotEmpty) {
|
|
304
|
+
qb = qb.whereHas(relationName);
|
|
305
|
+
} else {
|
|
306
|
+
qb = qb.whereHasNot(relationName);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return qb;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
type RelationSubqueryBase = {
|
|
314
|
+
from: TableSourceNode;
|
|
315
|
+
joins: JoinNode[];
|
|
316
|
+
correlation: ExpressionNode;
|
|
317
|
+
groupByColumn: ColumnNode;
|
|
318
|
+
targetTable: TableDef;
|
|
319
|
+
targetTableName: string;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const buildRelationSubqueryBase = (
|
|
323
|
+
table: TableDef,
|
|
324
|
+
relation: RelationDef
|
|
325
|
+
): RelationSubqueryBase => {
|
|
326
|
+
if (!isSingleTargetRelation(relation)) {
|
|
327
|
+
throw new Error('MorphTo relations do not support subquery-based filtering');
|
|
328
|
+
}
|
|
329
|
+
const target = relation.target;
|
|
330
|
+
|
|
331
|
+
if (relation.type === RelationKinds.BelongsToMany) {
|
|
332
|
+
const many = relation as BelongsToManyRelation;
|
|
333
|
+
const localKey = many.localKey || findPrimaryKey(table);
|
|
334
|
+
const targetKey = many.targetKey || findPrimaryKey(target);
|
|
335
|
+
const pivot = many.pivotTable;
|
|
336
|
+
|
|
337
|
+
const from: TableSourceNode = {
|
|
338
|
+
type: 'Table',
|
|
339
|
+
name: pivot.name,
|
|
340
|
+
schema: pivot.schema
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const joins: JoinNode[] = [
|
|
344
|
+
createJoinNode(
|
|
345
|
+
JOIN_KINDS.INNER,
|
|
346
|
+
{ type: 'Table', name: target.name, schema: target.schema },
|
|
347
|
+
eq(
|
|
348
|
+
{ type: 'Column', table: target.name, name: targetKey },
|
|
349
|
+
{ type: 'Column', table: pivot.name, name: many.pivotForeignKeyToTarget }
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
const correlation = eq(
|
|
355
|
+
{ type: 'Column', table: pivot.name, name: many.pivotForeignKeyToRoot },
|
|
356
|
+
{ type: 'Column', table: table.name, name: localKey }
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
const groupByColumn: ColumnNode = {
|
|
360
|
+
type: 'Column',
|
|
361
|
+
table: pivot.name,
|
|
362
|
+
name: many.pivotForeignKeyToRoot
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
from,
|
|
367
|
+
joins,
|
|
368
|
+
correlation,
|
|
369
|
+
groupByColumn,
|
|
370
|
+
targetTable: target,
|
|
371
|
+
targetTableName: target.name
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const from: TableSourceNode = {
|
|
376
|
+
type: 'Table',
|
|
377
|
+
name: target.name,
|
|
378
|
+
schema: target.schema
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const correlation = buildRelationCorrelation(table, relation);
|
|
382
|
+
let groupByColumnName: string;
|
|
383
|
+
if (relation.type === RelationKinds.BelongsTo) {
|
|
384
|
+
groupByColumnName = relation.localKey || findPrimaryKey(target);
|
|
385
|
+
} else if (relation.type === RelationKinds.MorphOne || relation.type === RelationKinds.MorphMany) {
|
|
386
|
+
groupByColumnName = relation.idField;
|
|
387
|
+
} else {
|
|
388
|
+
groupByColumnName = relation.foreignKey;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
from,
|
|
393
|
+
joins: [],
|
|
394
|
+
correlation,
|
|
395
|
+
groupByColumn: { type: 'Column', table: target.name, name: groupByColumnName },
|
|
396
|
+
targetTable: target,
|
|
397
|
+
targetTableName: target.name
|
|
398
|
+
};
|
|
399
|
+
};
|
|
400
|
+
|
|
388
401
|
function buildRelationFilterExpression(
|
|
389
402
|
table: TableDef,
|
|
390
403
|
relationName: string,
|
|
@@ -401,112 +414,116 @@ function buildRelationFilterExpression(
|
|
|
401
414
|
+ '"some", "none", "every", "isEmpty", or "isNotEmpty".'
|
|
402
415
|
);
|
|
403
416
|
}
|
|
404
|
-
|
|
405
|
-
const expressions: ExpressionNode[] = [];
|
|
406
|
-
const subqueryBase = buildRelationSubqueryBase(table, relation);
|
|
407
|
-
|
|
408
|
-
const buildSubquery = (predicate?: ExpressionNode): SelectQueryNode => {
|
|
409
|
-
const where = predicate
|
|
410
|
-
? and(subqueryBase.correlation, predicate)
|
|
411
|
-
: subqueryBase.correlation;
|
|
412
|
-
return {
|
|
413
|
-
type: 'SelectQuery',
|
|
414
|
-
from: subqueryBase.from,
|
|
415
|
-
columns: [],
|
|
416
|
-
joins: subqueryBase.joins,
|
|
417
|
-
where
|
|
418
|
-
};
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
if (
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
if (expressions.length ===
|
|
466
|
-
return
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
type: '
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
type: '
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
type: '
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
417
|
+
|
|
418
|
+
const expressions: ExpressionNode[] = [];
|
|
419
|
+
const subqueryBase = buildRelationSubqueryBase(table, relation);
|
|
420
|
+
|
|
421
|
+
const buildSubquery = (predicate?: ExpressionNode): SelectQueryNode => {
|
|
422
|
+
const where = predicate
|
|
423
|
+
? and(subqueryBase.correlation, predicate)
|
|
424
|
+
: subqueryBase.correlation;
|
|
425
|
+
return {
|
|
426
|
+
type: 'SelectQuery',
|
|
427
|
+
from: subqueryBase.from,
|
|
428
|
+
columns: [],
|
|
429
|
+
joins: subqueryBase.joins,
|
|
430
|
+
where
|
|
431
|
+
};
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
if (!isSingleTargetRelation(relation)) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (filter.some) {
|
|
439
|
+
const predicate = buildFilterExpression(relation.target, filter.some);
|
|
440
|
+
if (predicate) {
|
|
441
|
+
expressions.push(exists(buildSubquery(predicate)));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (filter.none) {
|
|
446
|
+
const predicate = buildFilterExpression(relation.target, filter.none);
|
|
447
|
+
if (predicate) {
|
|
448
|
+
expressions.push(notExists(buildSubquery(predicate)));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (filter.every) {
|
|
453
|
+
const predicate = buildFilterExpression(relation.target, filter.every);
|
|
454
|
+
if (predicate) {
|
|
455
|
+
const subquery: SelectQueryNode = {
|
|
456
|
+
type: 'SelectQuery',
|
|
457
|
+
from: subqueryBase.from,
|
|
458
|
+
columns: [],
|
|
459
|
+
joins: subqueryBase.joins,
|
|
460
|
+
where: subqueryBase.correlation,
|
|
461
|
+
groupBy: [subqueryBase.groupByColumn],
|
|
462
|
+
having: buildEveryHavingClause(subqueryBase.targetTableName, predicate, subqueryBase.targetTable)
|
|
463
|
+
};
|
|
464
|
+
expressions.push(exists(subquery));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (filter.isEmpty !== undefined) {
|
|
469
|
+
const subquery = buildSubquery();
|
|
470
|
+
expressions.push(filter.isEmpty ? notExists(subquery) : exists(subquery));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (filter.isNotEmpty !== undefined) {
|
|
474
|
+
const subquery = buildSubquery();
|
|
475
|
+
expressions.push(filter.isNotEmpty ? exists(subquery) : notExists(subquery));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (expressions.length === 0) {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (expressions.length === 1) {
|
|
483
|
+
return expressions[0];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return and(...expressions);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function buildEveryHavingClause(
|
|
490
|
+
relationName: string,
|
|
491
|
+
predicate: ExpressionNode,
|
|
492
|
+
targetTable: TableDef
|
|
493
|
+
): ExpressionNode {
|
|
494
|
+
const pk = findPrimaryKey(targetTable);
|
|
495
|
+
|
|
496
|
+
const whenClause = {
|
|
497
|
+
when: predicate,
|
|
498
|
+
then: { type: 'Literal' as const, value: 1 }
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const caseExpr = {
|
|
502
|
+
type: 'CaseExpression' as const,
|
|
503
|
+
conditions: [whenClause],
|
|
504
|
+
else: { type: 'Literal' as const, value: null }
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const countMatching = {
|
|
508
|
+
type: 'Function' as const,
|
|
509
|
+
name: 'COUNT',
|
|
510
|
+
args: [caseExpr]
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const countAll = {
|
|
514
|
+
type: 'Function' as const,
|
|
515
|
+
name: 'COUNT',
|
|
516
|
+
args: [{ type: 'Column' as const, table: relationName, name: pk }]
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
type: 'BinaryExpression' as const,
|
|
521
|
+
operator: '=',
|
|
522
|
+
left: countAll,
|
|
523
|
+
right: countMatching
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
510
527
|
function isEntityConstructor(value: unknown): value is EntityConstructor {
|
|
511
528
|
return typeof value === 'function' && value.prototype?.constructor === value;
|
|
512
529
|
}
|
|
@@ -520,76 +537,76 @@ function hasRelationFilterOperator(filter: RelationFilter): boolean {
|
|
|
520
537
|
|| filter.isNotEmpty !== undefined
|
|
521
538
|
);
|
|
522
539
|
}
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* Builds an expression tree from a filter object without applying it.
|
|
526
|
-
* Useful for combining with other conditions.
|
|
527
|
-
*
|
|
528
|
-
* @param tableOrEntity - The table definition or entity constructor
|
|
529
|
-
* @param where - The filter object
|
|
530
|
-
* @returns An expression node or null if no filters
|
|
531
|
-
*
|
|
532
|
-
* @example
|
|
533
|
-
* ```ts
|
|
534
|
-
* // Using Entity class
|
|
535
|
-
* const filterExpr = buildFilterExpression(User, { name: { contains: "john" } });
|
|
536
|
-
*
|
|
537
|
-
* // Using TableDef directly
|
|
538
|
-
* const filterExpr = buildFilterExpression(usersTable, { name: { contains: "john" } });
|
|
539
|
-
*
|
|
540
|
-
* if (filterExpr) {
|
|
541
|
-
* qb = qb.where(and(filterExpr, eq(users.columns.active, true)));
|
|
542
|
-
* }
|
|
543
|
-
* ```
|
|
544
|
-
*/
|
|
545
|
-
export function buildFilterExpression(
|
|
546
|
-
tableOrEntity: TableDef | EntityConstructor,
|
|
547
|
-
where?: WhereInput<TableDef | EntityConstructor> | null
|
|
548
|
-
): ExpressionNode | null {
|
|
549
|
-
if (!where) {
|
|
550
|
-
return null;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const table = isEntityConstructor(tableOrEntity)
|
|
554
|
-
? getTableDefFromEntity(tableOrEntity)
|
|
555
|
-
: tableOrEntity;
|
|
556
|
-
|
|
557
|
-
if (!table) {
|
|
558
|
-
return null;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const expressions: ExpressionNode[] = [];
|
|
562
|
-
|
|
563
|
-
for (const [fieldName, fieldFilter] of Object.entries(where)) {
|
|
564
|
-
if (fieldFilter === undefined || fieldFilter === null) {
|
|
565
|
-
continue;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
const column = table.columns[fieldName as keyof typeof table.columns];
|
|
569
|
-
if (column) {
|
|
570
|
-
const expr = buildFieldExpression(column, fieldFilter as FilterValue);
|
|
571
|
-
if (expr) {
|
|
572
|
-
expressions.push(expr);
|
|
573
|
-
}
|
|
574
|
-
} else if (table.relations && fieldName in table.relations) {
|
|
575
|
-
const relationExpr = buildRelationFilterExpression(
|
|
576
|
-
table,
|
|
577
|
-
fieldName,
|
|
578
|
-
fieldFilter as RelationFilter
|
|
579
|
-
);
|
|
580
|
-
if (relationExpr) {
|
|
581
|
-
expressions.push(relationExpr);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
if (expressions.length === 0) {
|
|
587
|
-
return null;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
if (expressions.length === 1) {
|
|
591
|
-
return expressions[0];
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
return and(...expressions);
|
|
595
|
-
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Builds an expression tree from a filter object without applying it.
|
|
543
|
+
* Useful for combining with other conditions.
|
|
544
|
+
*
|
|
545
|
+
* @param tableOrEntity - The table definition or entity constructor
|
|
546
|
+
* @param where - The filter object
|
|
547
|
+
* @returns An expression node or null if no filters
|
|
548
|
+
*
|
|
549
|
+
* @example
|
|
550
|
+
* ```ts
|
|
551
|
+
* // Using Entity class
|
|
552
|
+
* const filterExpr = buildFilterExpression(User, { name: { contains: "john" } });
|
|
553
|
+
*
|
|
554
|
+
* // Using TableDef directly
|
|
555
|
+
* const filterExpr = buildFilterExpression(usersTable, { name: { contains: "john" } });
|
|
556
|
+
*
|
|
557
|
+
* if (filterExpr) {
|
|
558
|
+
* qb = qb.where(and(filterExpr, eq(users.columns.active, true)));
|
|
559
|
+
* }
|
|
560
|
+
* ```
|
|
561
|
+
*/
|
|
562
|
+
export function buildFilterExpression(
|
|
563
|
+
tableOrEntity: TableDef | EntityConstructor,
|
|
564
|
+
where?: WhereInput<TableDef | EntityConstructor> | null
|
|
565
|
+
): ExpressionNode | null {
|
|
566
|
+
if (!where) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const table = isEntityConstructor(tableOrEntity)
|
|
571
|
+
? getTableDefFromEntity(tableOrEntity)
|
|
572
|
+
: tableOrEntity;
|
|
573
|
+
|
|
574
|
+
if (!table) {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const expressions: ExpressionNode[] = [];
|
|
579
|
+
|
|
580
|
+
for (const [fieldName, fieldFilter] of Object.entries(where)) {
|
|
581
|
+
if (fieldFilter === undefined || fieldFilter === null) {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const column = table.columns[fieldName as keyof typeof table.columns];
|
|
586
|
+
if (column) {
|
|
587
|
+
const expr = buildFieldExpression(column, fieldFilter as FilterValue);
|
|
588
|
+
if (expr) {
|
|
589
|
+
expressions.push(expr);
|
|
590
|
+
}
|
|
591
|
+
} else if (table.relations && fieldName in table.relations) {
|
|
592
|
+
const relationExpr = buildRelationFilterExpression(
|
|
593
|
+
table,
|
|
594
|
+
fieldName,
|
|
595
|
+
fieldFilter as RelationFilter
|
|
596
|
+
);
|
|
597
|
+
if (relationExpr) {
|
|
598
|
+
expressions.push(relationExpr);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (expressions.length === 0) {
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (expressions.length === 1) {
|
|
608
|
+
return expressions[0];
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return and(...expressions);
|
|
612
|
+
}
|