metal-orm 1.0.88 → 1.0.90
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 +3818 -3783
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +765 -236
- package/dist/index.d.ts +765 -236
- package/dist/index.js +3763 -3775
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/codegen/typescript.ts +29 -40
- package/src/core/ast/expression-builders.ts +34 -53
- package/src/core/ast/expression-nodes.ts +51 -72
- package/src/core/ast/expression-visitor.ts +219 -252
- package/src/core/ast/expression.ts +20 -21
- package/src/core/dialect/abstract.ts +55 -81
- package/src/core/execution/db-executor.ts +4 -5
- package/src/core/execution/executors/mysql-executor.ts +7 -9
- package/src/decorators/bootstrap.ts +11 -8
- package/src/dto/apply-filter.ts +281 -0
- package/src/dto/dto-types.ts +229 -0
- package/src/dto/filter-types.ts +193 -0
- package/src/dto/index.ts +97 -0
- package/src/dto/openapi/generators/base.ts +29 -0
- package/src/dto/openapi/generators/column.ts +34 -0
- package/src/dto/openapi/generators/dto.ts +94 -0
- package/src/dto/openapi/generators/filter.ts +74 -0
- package/src/dto/openapi/generators/nested-dto.ts +532 -0
- package/src/dto/openapi/generators/pagination.ts +111 -0
- package/src/dto/openapi/generators/relation-filter.ts +210 -0
- package/src/dto/openapi/index.ts +17 -0
- package/src/dto/openapi/type-mappings.ts +191 -0
- package/src/dto/openapi/types.ts +83 -0
- package/src/dto/openapi/utilities.ts +45 -0
- package/src/dto/pagination-utils.ts +150 -0
- package/src/dto/transform.ts +193 -0
- package/src/index.ts +67 -65
- package/src/orm/unit-of-work.ts +13 -25
- package/src/query-builder/query-ast-service.ts +287 -300
- package/src/query-builder/relation-filter-utils.ts +159 -160
- package/src/query-builder/select.ts +137 -192
- package/src/core/ast/ast-validation.ts +0 -19
- package/src/core/ast/param-proxy.ts +0 -47
- package/src/core/ast/query-visitor.ts +0 -273
- package/src/openapi/index.ts +0 -4
- package/src/openapi/query-parameters.ts +0 -207
- package/src/openapi/schema-extractor-input.ts +0 -139
- package/src/openapi/schema-extractor-output.ts +0 -427
- package/src/openapi/schema-extractor-utils.ts +0 -110
- package/src/openapi/schema-extractor.ts +0 -111
- package/src/openapi/schema-types.ts +0 -176
- package/src/openapi/type-mappers.ts +0 -227
|
@@ -26,28 +26,25 @@ import {
|
|
|
26
26
|
BetweenExpressionNode,
|
|
27
27
|
ArithmeticExpressionNode,
|
|
28
28
|
BitwiseExpressionNode,
|
|
29
|
-
CollateExpressionNode,
|
|
30
|
-
AliasRefNode,
|
|
31
|
-
isOperandNode
|
|
32
|
-
|
|
33
|
-
} from '../ast/expression.js';
|
|
29
|
+
CollateExpressionNode,
|
|
30
|
+
AliasRefNode,
|
|
31
|
+
isOperandNode
|
|
32
|
+
} from '../ast/expression.js';
|
|
34
33
|
import { DialectName } from '../sql/sql.js';
|
|
35
34
|
import type { FunctionStrategy } from '../functions/types.js';
|
|
36
35
|
import { StandardFunctionStrategy } from '../functions/standard-strategy.js';
|
|
37
36
|
import type { TableFunctionStrategy } from '../functions/table-types.js';
|
|
38
37
|
import { StandardTableFunctionStrategy } from '../functions/standard-table-strategy.js';
|
|
39
38
|
|
|
40
|
-
/**
|
|
41
|
-
* Context for SQL compilation with parameter management
|
|
42
|
-
*/
|
|
43
|
-
export interface CompilerContext {
|
|
44
|
-
/** Array of parameters */
|
|
45
|
-
params: unknown[];
|
|
46
|
-
/** Function to add a parameter and get its placeholder */
|
|
47
|
-
addParameter(value: unknown): string;
|
|
48
|
-
|
|
49
|
-
allowParams?: boolean;
|
|
50
|
-
}
|
|
39
|
+
/**
|
|
40
|
+
* Context for SQL compilation with parameter management
|
|
41
|
+
*/
|
|
42
|
+
export interface CompilerContext {
|
|
43
|
+
/** Array of parameters */
|
|
44
|
+
params: unknown[];
|
|
45
|
+
/** Function to add a parameter and get its placeholder */
|
|
46
|
+
addParameter(value: unknown): string;
|
|
47
|
+
}
|
|
51
48
|
|
|
52
49
|
/**
|
|
53
50
|
* Result of SQL compilation
|
|
@@ -88,29 +85,18 @@ export abstract class Dialect
|
|
|
88
85
|
* @param ast - Query AST to compile
|
|
89
86
|
* @returns Compiled query with SQL and parameters
|
|
90
87
|
*/
|
|
91
|
-
compileSelect(ast: SelectQueryNode): CompiledQuery {
|
|
92
|
-
const ctx = this.createCompilerContext();
|
|
93
|
-
const normalized = this.normalizeSelectAst(ast);
|
|
94
|
-
const rawSql = this.compileSelectAst(normalized, ctx).trim();
|
|
95
|
-
const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
|
|
96
|
-
return {
|
|
97
|
-
sql,
|
|
98
|
-
params: [...ctx.params]
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const ctx = this.createCompilerContext(options);
|
|
104
|
-
const normalized = this.normalizeSelectAst(ast);
|
|
105
|
-
const rawSql = this.compileSelectAst(normalized, ctx).trim();
|
|
106
|
-
const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
|
|
107
|
-
return {
|
|
108
|
-
sql,
|
|
109
|
-
params: [...ctx.params]
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
compileInsert(ast: InsertQueryNode): CompiledQuery {
|
|
88
|
+
compileSelect(ast: SelectQueryNode): CompiledQuery {
|
|
89
|
+
const ctx = this.createCompilerContext();
|
|
90
|
+
const normalized = this.normalizeSelectAst(ast);
|
|
91
|
+
const rawSql = this.compileSelectAst(normalized, ctx).trim();
|
|
92
|
+
const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
|
|
93
|
+
return {
|
|
94
|
+
sql,
|
|
95
|
+
params: [...ctx.params]
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
compileInsert(ast: InsertQueryNode): CompiledQuery {
|
|
114
100
|
const ctx = this.createCompilerContext();
|
|
115
101
|
const rawSql = this.compileInsertAst(ast, ctx).trim();
|
|
116
102
|
const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
|
|
@@ -211,24 +197,22 @@ export abstract class Dialect
|
|
|
211
197
|
return `SELECT 1${tail}`;
|
|
212
198
|
}
|
|
213
199
|
|
|
214
|
-
/**
|
|
215
|
-
* Creates a new compiler context
|
|
216
|
-
* @
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
counter
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
};
|
|
231
|
-
}
|
|
200
|
+
/**
|
|
201
|
+
* Creates a new compiler context
|
|
202
|
+
* @returns Compiler context with parameter management
|
|
203
|
+
*/
|
|
204
|
+
protected createCompilerContext(): CompilerContext {
|
|
205
|
+
const params: unknown[] = [];
|
|
206
|
+
let counter = 0;
|
|
207
|
+
return {
|
|
208
|
+
params,
|
|
209
|
+
addParameter: (value: unknown) => {
|
|
210
|
+
counter += 1;
|
|
211
|
+
params.push(value);
|
|
212
|
+
return this.formatPlaceholder(counter);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
232
216
|
|
|
233
217
|
/**
|
|
234
218
|
* Formats a parameter placeholder
|
|
@@ -387,17 +371,13 @@ export abstract class Dialect
|
|
|
387
371
|
* @param ctx - Compiler context
|
|
388
372
|
* @returns Compiled SQL operand
|
|
389
373
|
*/
|
|
390
|
-
protected compileOperand(node: OperandNode, ctx: CompilerContext): string {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
throw new Error(`Unsupported operand node type "${nodeType ?? 'unknown'}" for ${this.constructor.name}`);
|
|
398
|
-
}
|
|
399
|
-
return compiler(node, ctx);
|
|
400
|
-
}
|
|
374
|
+
protected compileOperand(node: OperandNode, ctx: CompilerContext): string {
|
|
375
|
+
const compiler = this.operandCompilers.get(node.type);
|
|
376
|
+
if (!compiler) {
|
|
377
|
+
throw new Error(`Unsupported operand node type "${node.type}" for ${this.constructor.name}`);
|
|
378
|
+
}
|
|
379
|
+
return compiler(node, ctx);
|
|
380
|
+
}
|
|
401
381
|
|
|
402
382
|
/**
|
|
403
383
|
* Compiles an ordering term (operand, expression, or alias reference).
|
|
@@ -472,19 +452,13 @@ export abstract class Dialect
|
|
|
472
452
|
});
|
|
473
453
|
}
|
|
474
454
|
|
|
475
|
-
private registerDefaultOperandCompilers(): void {
|
|
476
|
-
this.registerOperandCompiler('Literal', (literal: LiteralNode, ctx) => ctx.addParameter(literal.value));
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
this.registerOperandCompiler('AliasRef', (alias: AliasRefNode, _ctx) => {
|
|
485
|
-
void _ctx;
|
|
486
|
-
return this.quoteIdentifier(alias.name);
|
|
487
|
-
});
|
|
455
|
+
private registerDefaultOperandCompilers(): void {
|
|
456
|
+
this.registerOperandCompiler('Literal', (literal: LiteralNode, ctx) => ctx.addParameter(literal.value));
|
|
457
|
+
|
|
458
|
+
this.registerOperandCompiler('AliasRef', (alias: AliasRefNode, _ctx) => {
|
|
459
|
+
void _ctx;
|
|
460
|
+
return this.quoteIdentifier(alias.name);
|
|
461
|
+
});
|
|
488
462
|
|
|
489
463
|
this.registerOperandCompiler('Column', (column: ColumnNode, _ctx) => {
|
|
490
464
|
void _ctx;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
// src/core/execution/db-executor.ts
|
|
2
2
|
|
|
3
3
|
// low-level canonical shape
|
|
4
|
-
export type QueryResult = {
|
|
5
|
-
columns: string[];
|
|
6
|
-
values: unknown[][];
|
|
7
|
-
|
|
8
|
-
};
|
|
4
|
+
export type QueryResult = {
|
|
5
|
+
columns: string[];
|
|
6
|
+
values: unknown[][];
|
|
7
|
+
};
|
|
9
8
|
|
|
10
9
|
export interface DbExecutor {
|
|
11
10
|
/** Capability flags so the runtime can make correct decisions without relying on optional methods. */
|
|
@@ -31,15 +31,13 @@ export function createMysqlExecutor(
|
|
|
31
31
|
capabilities: {
|
|
32
32
|
transactions: supportsTransactions,
|
|
33
33
|
},
|
|
34
|
-
async executeSql(sql, params) {
|
|
35
|
-
const [rows] = await client.query(sql, params);
|
|
36
|
-
|
|
37
|
-
if (!Array.isArray(rows)) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return [{ columns: [], values: [], insertId: normalized }];
|
|
42
|
-
}
|
|
34
|
+
async executeSql(sql, params) {
|
|
35
|
+
const [rows] = await client.query(sql, params);
|
|
36
|
+
|
|
37
|
+
if (!Array.isArray(rows)) {
|
|
38
|
+
// e.g. insert/update returning only headers, treat as no rows
|
|
39
|
+
return [{ columns: [], values: [] }];
|
|
40
|
+
}
|
|
43
41
|
|
|
44
42
|
const result = rowsToQueryResult(
|
|
45
43
|
rows as Array<Record<string, unknown>>
|
|
@@ -176,14 +176,17 @@ export const bootstrapEntities = (): TableDef[] => {
|
|
|
176
176
|
* @param ctor - The entity constructor.
|
|
177
177
|
* @returns The table definition or undefined if not found.
|
|
178
178
|
*/
|
|
179
|
-
export const getTableDefFromEntity = <TTable extends TableDef = TableDef>(ctor: EntityConstructor): TTable | undefined => {
|
|
180
|
-
const meta = getEntityMetadata(ctor);
|
|
181
|
-
if (!meta) return undefined;
|
|
182
|
-
if (!meta.table) {
|
|
183
|
-
bootstrapEntities();
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
};
|
|
179
|
+
export const getTableDefFromEntity = <TTable extends TableDef = TableDef>(ctor: EntityConstructor): TTable | undefined => {
|
|
180
|
+
const meta = getEntityMetadata(ctor);
|
|
181
|
+
if (!meta) return undefined;
|
|
182
|
+
if (!meta.table) {
|
|
183
|
+
bootstrapEntities();
|
|
184
|
+
}
|
|
185
|
+
if (!meta.table) {
|
|
186
|
+
throw new Error(`Failed to build table definition for entity '${ctor.name}'`);
|
|
187
|
+
}
|
|
188
|
+
return meta.table as TTable;
|
|
189
|
+
};
|
|
187
190
|
|
|
188
191
|
/**
|
|
189
192
|
* Creates a select query builder for the given entity.
|
|
@@ -0,0 +1,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
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
169
|
+
where?: WhereInput<any> | null
|
|
170
|
+
): SelectQueryBuilder<T, TTable> {
|
|
171
|
+
if (!where) {
|
|
172
|
+
return qb;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const table = isEntityConstructor(tableOrEntity)
|
|
176
|
+
? getTableDefFromEntity(tableOrEntity)
|
|
177
|
+
: tableOrEntity;
|
|
178
|
+
|
|
179
|
+
if (!table) {
|
|
180
|
+
return qb;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const expressions: ExpressionNode[] = [];
|
|
184
|
+
|
|
185
|
+
for (const [fieldName, fieldFilter] of Object.entries(where)) {
|
|
186
|
+
if (fieldFilter === undefined || fieldFilter === null) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const column = table.columns[fieldName];
|
|
191
|
+
if (!column) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const expr = buildFieldExpression(column, fieldFilter as FilterValue);
|
|
196
|
+
if (expr) {
|
|
197
|
+
expressions.push(expr);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (expressions.length === 0) {
|
|
202
|
+
return qb;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (expressions.length === 1) {
|
|
206
|
+
return qb.where(expressions[0]);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return qb.where(and(...expressions));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function isEntityConstructor(value: unknown): value is EntityConstructor {
|
|
213
|
+
return typeof value === 'function' && value.prototype?.constructor === value;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Builds an expression tree from a filter object without applying it.
|
|
218
|
+
* Useful for combining with other conditions.
|
|
219
|
+
*
|
|
220
|
+
* @param tableOrEntity - The table definition or entity constructor
|
|
221
|
+
* @param where - The filter object
|
|
222
|
+
* @returns An expression node or null if no filters
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```ts
|
|
226
|
+
* // Using Entity class
|
|
227
|
+
* const filterExpr = buildFilterExpression(User, { name: { contains: "john" } });
|
|
228
|
+
*
|
|
229
|
+
* // Using TableDef directly
|
|
230
|
+
* const filterExpr = buildFilterExpression(usersTable, { name: { contains: "john" } });
|
|
231
|
+
*
|
|
232
|
+
* if (filterExpr) {
|
|
233
|
+
* qb = qb.where(and(filterExpr, eq(users.columns.active, true)));
|
|
234
|
+
* }
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
export function buildFilterExpression(
|
|
238
|
+
tableOrEntity: TableDef | EntityConstructor,
|
|
239
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
240
|
+
where?: WhereInput<any> | null
|
|
241
|
+
): ExpressionNode | null {
|
|
242
|
+
if (!where) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const table = isEntityConstructor(tableOrEntity)
|
|
247
|
+
? getTableDefFromEntity(tableOrEntity)
|
|
248
|
+
: tableOrEntity;
|
|
249
|
+
|
|
250
|
+
if (!table) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const expressions: ExpressionNode[] = [];
|
|
255
|
+
|
|
256
|
+
for (const [fieldName, fieldFilter] of Object.entries(where)) {
|
|
257
|
+
if (fieldFilter === undefined || fieldFilter === null) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const column = table.columns[fieldName];
|
|
262
|
+
if (!column) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const expr = buildFieldExpression(column, fieldFilter as FilterValue);
|
|
267
|
+
if (expr) {
|
|
268
|
+
expressions.push(expr);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (expressions.length === 0) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (expressions.length === 1) {
|
|
277
|
+
return expressions[0];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return and(...expressions);
|
|
281
|
+
}
|