metal-orm 1.0.89 → 1.0.91

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 (57) hide show
  1. package/dist/index.cjs +2968 -2983
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +784 -251
  4. package/dist/index.d.ts +784 -251
  5. package/dist/index.js +2913 -2975
  6. package/dist/index.js.map +1 -1
  7. package/package.json +6 -3
  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/ddl/introspect/utils.ts +45 -45
  14. package/src/core/dialect/abstract.ts +55 -81
  15. package/src/core/execution/db-executor.ts +4 -5
  16. package/src/core/execution/executors/mysql-executor.ts +7 -9
  17. package/src/decorators/bootstrap.ts +29 -26
  18. package/src/dto/apply-filter.ts +279 -0
  19. package/src/dto/dto-types.ts +229 -0
  20. package/src/dto/filter-types.ts +193 -0
  21. package/src/dto/index.ts +97 -0
  22. package/src/dto/openapi/generators/base.ts +29 -0
  23. package/src/dto/openapi/generators/column.ts +34 -0
  24. package/src/dto/openapi/generators/dto.ts +94 -0
  25. package/src/dto/openapi/generators/filter.ts +74 -0
  26. package/src/dto/openapi/generators/nested-dto.ts +532 -0
  27. package/src/dto/openapi/generators/pagination.ts +111 -0
  28. package/src/dto/openapi/generators/relation-filter.ts +210 -0
  29. package/src/dto/openapi/index.ts +17 -0
  30. package/src/dto/openapi/type-mappings.ts +191 -0
  31. package/src/dto/openapi/types.ts +90 -0
  32. package/src/dto/openapi/utilities.ts +45 -0
  33. package/src/dto/pagination-utils.ts +150 -0
  34. package/src/dto/transform.ts +197 -0
  35. package/src/index.ts +5 -3
  36. package/src/orm/entity-context.ts +9 -9
  37. package/src/orm/entity.ts +74 -74
  38. package/src/orm/orm-session.ts +159 -159
  39. package/src/orm/relation-change-processor.ts +3 -3
  40. package/src/orm/runtime-types.ts +5 -5
  41. package/src/orm/unit-of-work.ts +13 -25
  42. package/src/query-builder/query-ast-service.ts +287 -300
  43. package/src/query-builder/relation-filter-utils.ts +159 -160
  44. package/src/query-builder/select.ts +137 -192
  45. package/src/schema/column-types.ts +4 -4
  46. package/src/schema/types.ts +5 -1
  47. package/src/core/ast/ast-validation.ts +0 -19
  48. package/src/core/ast/param-proxy.ts +0 -47
  49. package/src/core/ast/query-visitor.ts +0 -273
  50. package/src/openapi/index.ts +0 -4
  51. package/src/openapi/query-parameters.ts +0 -207
  52. package/src/openapi/schema-extractor-input.ts +0 -193
  53. package/src/openapi/schema-extractor-output.ts +0 -427
  54. package/src/openapi/schema-extractor-utils.ts +0 -110
  55. package/src/openapi/schema-extractor.ts +0 -120
  56. package/src/openapi/schema-types.ts +0 -187
  57. 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>>
@@ -182,6 +182,9 @@ export const getTableDefFromEntity = <TTable extends TableDef = TableDef>(ctor:
182
182
  if (!meta.table) {
183
183
  bootstrapEntities();
184
184
  }
185
+ if (!meta.table) {
186
+ throw new Error(`Failed to build table definition for entity '${ctor.name}'`);
187
+ }
185
188
  return meta.table as TTable;
186
189
  };
187
190
 
@@ -245,29 +248,29 @@ export const selectFromEntity = <TEntity extends object>(
245
248
  * Lazily bootstraps entity metadata (via getTableDefFromEntity) and returns a
246
249
  * `tableRef(...)`-style proxy so users can write `u.id` instead of `u.columns.id`.
247
250
  */
248
- export const entityRef = <TEntity extends object>(
249
- ctor: EntityConstructor<TEntity>
250
- ): TableRef<EntityTable<TEntity>> => {
251
- const table = getTableDefFromEntity(ctor);
252
- if (!table) {
253
- throw new Error(`Entity '${ctor.name}' is not registered with decorators or has not been bootstrapped`);
254
- }
255
- return tableRef(table as EntityTable<TEntity>);
256
- };
257
-
258
- type EntityRefsTuple<T extends readonly EntityConstructor<object>[]> = {
259
- [K in keyof T]: T[K] extends EntityConstructor<infer TEntity>
260
- ? TableRef<EntityTable<TEntity & object>>
261
- : never;
262
- };
263
-
264
- /**
265
- * Public API: variadic entity references.
266
- * Usage:
267
- * const [u, p] = entityRefs(User, Post);
268
- */
269
- export const entityRefs = <T extends readonly EntityConstructor<object>[]>(
270
- ...ctors: T
271
- ): EntityRefsTuple<T> => {
272
- return ctors.map(ctor => entityRef(ctor)) as EntityRefsTuple<T>;
273
- };
251
+ export const entityRef = <TEntity extends object>(
252
+ ctor: EntityConstructor<TEntity>
253
+ ): TableRef<EntityTable<TEntity>> => {
254
+ const table = getTableDefFromEntity(ctor);
255
+ if (!table) {
256
+ throw new Error(`Entity '${ctor.name}' is not registered with decorators or has not been bootstrapped`);
257
+ }
258
+ return tableRef(table as EntityTable<TEntity>);
259
+ };
260
+
261
+ type EntityRefsTuple<T extends readonly EntityConstructor<object>[]> = {
262
+ [K in keyof T]: T[K] extends EntityConstructor<infer TEntity>
263
+ ? TableRef<EntityTable<TEntity & object>>
264
+ : never;
265
+ };
266
+
267
+ /**
268
+ * Public API: variadic entity references.
269
+ * Usage:
270
+ * const [u, p] = entityRefs(User, Post);
271
+ */
272
+ export const entityRefs = <T extends readonly EntityConstructor<object>[]>(
273
+ ...ctors: T
274
+ ): EntityRefsTuple<T> => {
275
+ return ctors.map(ctor => entityRef(ctor)) as EntityRefsTuple<T>;
276
+ };
@@ -0,0 +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
+ 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
+ }