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.
Files changed (49) hide show
  1. package/dist/index.cjs +3818 -3783
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +765 -236
  4. package/dist/index.d.ts +765 -236
  5. package/dist/index.js +3763 -3775
  6. package/dist/index.js.map +1 -1
  7. package/package.json +3 -2
  8. package/src/codegen/typescript.ts +29 -40
  9. package/src/core/ast/expression-builders.ts +34 -53
  10. package/src/core/ast/expression-nodes.ts +51 -72
  11. package/src/core/ast/expression-visitor.ts +219 -252
  12. package/src/core/ast/expression.ts +20 -21
  13. package/src/core/dialect/abstract.ts +55 -81
  14. package/src/core/execution/db-executor.ts +4 -5
  15. package/src/core/execution/executors/mysql-executor.ts +7 -9
  16. package/src/decorators/bootstrap.ts +11 -8
  17. package/src/dto/apply-filter.ts +281 -0
  18. package/src/dto/dto-types.ts +229 -0
  19. package/src/dto/filter-types.ts +193 -0
  20. package/src/dto/index.ts +97 -0
  21. package/src/dto/openapi/generators/base.ts +29 -0
  22. package/src/dto/openapi/generators/column.ts +34 -0
  23. package/src/dto/openapi/generators/dto.ts +94 -0
  24. package/src/dto/openapi/generators/filter.ts +74 -0
  25. package/src/dto/openapi/generators/nested-dto.ts +532 -0
  26. package/src/dto/openapi/generators/pagination.ts +111 -0
  27. package/src/dto/openapi/generators/relation-filter.ts +210 -0
  28. package/src/dto/openapi/index.ts +17 -0
  29. package/src/dto/openapi/type-mappings.ts +191 -0
  30. package/src/dto/openapi/types.ts +83 -0
  31. package/src/dto/openapi/utilities.ts +45 -0
  32. package/src/dto/pagination-utils.ts +150 -0
  33. package/src/dto/transform.ts +193 -0
  34. package/src/index.ts +67 -65
  35. package/src/orm/unit-of-work.ts +13 -25
  36. package/src/query-builder/query-ast-service.ts +287 -300
  37. package/src/query-builder/relation-filter-utils.ts +159 -160
  38. package/src/query-builder/select.ts +137 -192
  39. package/src/core/ast/ast-validation.ts +0 -19
  40. package/src/core/ast/param-proxy.ts +0 -47
  41. package/src/core/ast/query-visitor.ts +0 -273
  42. package/src/openapi/index.ts +0 -4
  43. package/src/openapi/query-parameters.ts +0 -207
  44. package/src/openapi/schema-extractor-input.ts +0 -139
  45. package/src/openapi/schema-extractor-output.ts +0 -427
  46. package/src/openapi/schema-extractor-utils.ts +0 -110
  47. package/src/openapi/schema-extractor.ts +0 -111
  48. package/src/openapi/schema-types.ts +0 -176
  49. 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
- ParamNode
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
- /** Whether Param operands are allowed (for schema generation) */
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
- compileSelectWithOptions(ast: SelectQueryNode, options: { allowParams?: boolean } = {}): CompiledQuery {
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
- * @param options - Optional compiler context options
217
- * @returns Compiler context with parameter management
218
- */
219
- protected createCompilerContext(options: { allowParams?: boolean } = {}): CompilerContext {
220
- const params: unknown[] = [];
221
- let counter = 0;
222
- return {
223
- params,
224
- allowParams: options.allowParams ?? false,
225
- addParameter: (value: unknown) => {
226
- counter += 1;
227
- params.push(value);
228
- return this.formatPlaceholder(counter);
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 descriptor = Object.getOwnPropertyDescriptor(node, 'type');
392
- const nodeType = typeof descriptor?.value === 'string'
393
- ? descriptor.value
394
- : (typeof node.type === 'string' ? node.type : undefined);
395
- const compiler = nodeType ? this.operandCompilers.get(nodeType) : undefined;
396
- if (!compiler) {
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
- this.registerOperandCompiler('Param', (_param: ParamNode, ctx) => {
478
- if (!ctx.allowParams) {
479
- throw new Error('Cannot compile query with Param operands. Param proxies are only for schema generation (getSchema()). If you need real parameters, use literal values.');
480
- }
481
- return ctx.addParameter(null);
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
- insertId?: number;
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
- const insertId = (rows as { insertId?: number } | null)?.insertId;
39
- const normalized = typeof insertId === 'number' && insertId > 0 ? insertId : undefined;
40
- // e.g. insert/update returning only headers, treat as no rows
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
- return meta.table as TTable;
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
+ }