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.
Files changed (37) hide show
  1. package/dist/index.cjs +214 -118
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +71 -32
  4. package/dist/index.d.ts +71 -32
  5. package/dist/index.js +206 -118
  6. package/dist/index.js.map +1 -1
  7. package/package.json +4 -2
  8. package/scripts/generate-entities/render.mjs +16 -3
  9. package/src/core/ddl/introspect/utils.ts +45 -45
  10. package/src/decorators/bootstrap.ts +37 -37
  11. package/src/decorators/column-decorator.ts +3 -1
  12. package/src/dto/apply-filter.ts +279 -281
  13. package/src/dto/dto-types.ts +229 -229
  14. package/src/dto/filter-types.ts +193 -193
  15. package/src/dto/index.ts +97 -97
  16. package/src/dto/openapi/generators/base.ts +29 -29
  17. package/src/dto/openapi/generators/column.ts +37 -34
  18. package/src/dto/openapi/generators/dto.ts +94 -94
  19. package/src/dto/openapi/generators/filter.ts +75 -74
  20. package/src/dto/openapi/generators/nested-dto.ts +618 -532
  21. package/src/dto/openapi/generators/pagination.ts +111 -111
  22. package/src/dto/openapi/generators/relation-filter.ts +228 -210
  23. package/src/dto/openapi/index.ts +17 -17
  24. package/src/dto/openapi/type-mappings.ts +191 -191
  25. package/src/dto/openapi/types.ts +101 -83
  26. package/src/dto/openapi/utilities.ts +90 -45
  27. package/src/dto/pagination-utils.ts +150 -150
  28. package/src/dto/transform.ts +197 -193
  29. package/src/index.ts +69 -69
  30. package/src/orm/entity-context.ts +9 -9
  31. package/src/orm/entity-metadata.ts +14 -14
  32. package/src/orm/entity.ts +74 -74
  33. package/src/orm/orm-session.ts +159 -159
  34. package/src/orm/relation-change-processor.ts +3 -3
  35. package/src/orm/runtime-types.ts +5 -5
  36. package/src/schema/column-types.ts +4 -4
  37. package/src/schema/types.ts +5 -1
@@ -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
- // 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
- }
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
+ }