metal-orm 1.0.8 → 1.0.9

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 (153) hide show
  1. package/README.md +12 -1
  2. package/dist/decorators/index.cjs +2564 -0
  3. package/dist/decorators/index.cjs.map +1 -0
  4. package/dist/decorators/index.d.cts +53 -0
  5. package/dist/decorators/index.d.ts +53 -0
  6. package/dist/decorators/index.js +2530 -0
  7. package/dist/decorators/index.js.map +1 -0
  8. package/dist/index.cjs +4227 -0
  9. package/dist/index.cjs.map +1 -0
  10. package/dist/index.d.cts +701 -0
  11. package/dist/index.d.ts +701 -0
  12. package/dist/index.js +4131 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/select-654m4qy8.d.cts +1522 -0
  15. package/dist/select-654m4qy8.d.ts +1522 -0
  16. package/package.json +27 -20
  17. package/src/codegen/typescript.ts +405 -393
  18. package/src/core/ast/aggregate-functions.ts +30 -0
  19. package/src/core/ast/builders.ts +43 -0
  20. package/src/core/ast/expression-builders.ts +310 -0
  21. package/src/core/ast/expression-nodes.ts +211 -0
  22. package/src/core/ast/expression-visitor.ts +99 -0
  23. package/src/core/ast/expression.ts +5 -0
  24. package/src/{utils → core/ast}/join-node.ts +20 -20
  25. package/src/{ast → core/ast}/join.ts +18 -18
  26. package/src/{ast → core/ast}/query.ts +113 -113
  27. package/src/core/ast/window-functions.ts +140 -0
  28. package/src/{dialect → core/dialect}/abstract.ts +94 -94
  29. package/src/{dialect → core/dialect}/mssql/index.ts +31 -31
  30. package/src/{dialect → core/dialect}/mysql/index.ts +31 -31
  31. package/src/{dialect → core/dialect}/postgres/index.ts +45 -45
  32. package/src/{dialect → core/dialect}/sqlite/index.ts +45 -45
  33. package/src/{constants → core/sql}/sql-operator-config.ts +39 -39
  34. package/src/decorators/bootstrap.ts +126 -0
  35. package/src/decorators/column.ts +78 -0
  36. package/src/decorators/entity.ts +36 -0
  37. package/src/decorators/index.ts +4 -0
  38. package/src/decorators/relations.ts +107 -0
  39. package/src/global.d.ts +1 -0
  40. package/src/index.ts +22 -22
  41. package/src/orm/db-executor.ts +11 -0
  42. package/src/orm/domain-event-bus.ts +52 -0
  43. package/src/{runtime → orm}/entity-meta.ts +52 -52
  44. package/src/orm/entity-metadata.ts +140 -0
  45. package/src/{runtime → orm}/entity.ts +252 -252
  46. package/src/{runtime → orm}/execute.ts +36 -36
  47. package/src/{runtime → orm}/hydration.ts +103 -103
  48. package/src/orm/identity-map.ts +37 -0
  49. package/src/{runtime → orm}/lazy-batch.ts +205 -205
  50. package/src/orm/orm-context.ts +154 -0
  51. package/src/orm/relation-change-processor.ts +140 -0
  52. package/src/{runtime → orm}/relations/belongs-to.ts +92 -92
  53. package/src/{runtime → orm}/relations/has-many.ts +111 -111
  54. package/src/{runtime → orm}/relations/many-to-many.ts +149 -149
  55. package/src/orm/runtime-types.ts +39 -0
  56. package/src/orm/transaction-runner.ts +17 -0
  57. package/src/orm/unit-of-work.ts +232 -0
  58. package/src/{builder/operations → query-builder}/column-selector.ts +78 -78
  59. package/src/{builder → query-builder}/delete-query-state.ts +38 -42
  60. package/src/{builder → query-builder}/delete.ts +46 -57
  61. package/src/{builder → query-builder}/hydration-manager.ts +87 -87
  62. package/src/{builder → query-builder}/hydration-planner.ts +182 -182
  63. package/src/{builder → query-builder}/insert-query-state.ts +51 -62
  64. package/src/{builder → query-builder}/insert.ts +48 -59
  65. package/src/{builder → query-builder}/query-ast-service.ts +208 -226
  66. package/src/{utils → query-builder}/raw-column-parser.ts +32 -32
  67. package/src/{builder → query-builder}/relation-conditions.ts +112 -112
  68. package/src/{builder/operations → query-builder}/relation-manager.ts +82 -82
  69. package/src/{builder → query-builder}/relation-projection-helper.ts +101 -101
  70. package/src/{builder → query-builder}/relation-service.ts +284 -284
  71. package/src/{builder → query-builder}/relation-types.ts +21 -21
  72. package/src/{builder → query-builder}/relation-utils.ts +12 -12
  73. package/src/{builder → query-builder}/select-query-builder-deps.ts +112 -94
  74. package/src/{builder → query-builder}/select-query-state.ts +179 -179
  75. package/src/{builder → query-builder}/select.ts +78 -69
  76. package/src/{builder → query-builder}/update-query-state.ts +55 -59
  77. package/src/{builder → query-builder}/update.ts +50 -61
  78. package/src/schema/column.ts +25 -25
  79. package/src/schema/relation.ts +116 -116
  80. package/src/schema/table.ts +34 -34
  81. package/src/schema/types.ts +76 -76
  82. package/.github/workflows/publish-metal-orm.yml +0 -38
  83. package/ROADMAP.md +0 -125
  84. package/docs/CHANGES.md +0 -104
  85. package/docs/advanced-features.md +0 -176
  86. package/docs/api-reference.md +0 -31
  87. package/docs/dml-operations.md +0 -156
  88. package/docs/getting-started.md +0 -171
  89. package/docs/hydration.md +0 -115
  90. package/docs/index.md +0 -36
  91. package/docs/multi-dialect-support.md +0 -59
  92. package/docs/query-builder.md +0 -135
  93. package/docs/runtime.md +0 -105
  94. package/docs/schema-definition.md +0 -112
  95. package/metadata.json +0 -5
  96. package/playground/api/playground-api.ts +0 -94
  97. package/playground/index.html +0 -15
  98. package/playground/src/App.css +0 -1
  99. package/playground/src/App.tsx +0 -114
  100. package/playground/src/components/CodeDisplay.tsx +0 -43
  101. package/playground/src/components/QueryExecutor.tsx +0 -189
  102. package/playground/src/components/ResultsTable.tsx +0 -67
  103. package/playground/src/components/ResultsTabs.tsx +0 -105
  104. package/playground/src/components/ScenarioList.tsx +0 -56
  105. package/playground/src/components/logo.svg +0 -45
  106. package/playground/src/data/scenarios.ts +0 -2
  107. package/playground/src/main.tsx +0 -9
  108. package/playground/src/services/PlaygroundApiService.ts +0 -60
  109. package/postcss.config.cjs +0 -5
  110. package/sql_sql-ansi-cheatsheet-2025.md +0 -264
  111. package/src/ast/expression.ts +0 -658
  112. package/src/builder/operations/cte-manager.ts +0 -34
  113. package/src/builder/operations/filter-manager.ts +0 -68
  114. package/src/builder/operations/join-manager.ts +0 -36
  115. package/src/builder/operations/pagination-manager.ts +0 -36
  116. package/src/playground/features/playground/api/types.ts +0 -16
  117. package/src/playground/features/playground/clients/MockClient.ts +0 -17
  118. package/src/playground/features/playground/clients/SqliteClient.ts +0 -57
  119. package/src/playground/features/playground/common/IDatabaseClient.ts +0 -10
  120. package/src/playground/features/playground/data/scenarios/aggregation.ts +0 -36
  121. package/src/playground/features/playground/data/scenarios/basics.ts +0 -25
  122. package/src/playground/features/playground/data/scenarios/edge_cases.ts +0 -57
  123. package/src/playground/features/playground/data/scenarios/filtering.ts +0 -94
  124. package/src/playground/features/playground/data/scenarios/hydration.ts +0 -27
  125. package/src/playground/features/playground/data/scenarios/index.ts +0 -29
  126. package/src/playground/features/playground/data/scenarios/ordering.ts +0 -25
  127. package/src/playground/features/playground/data/scenarios/pagination.ts +0 -16
  128. package/src/playground/features/playground/data/scenarios/relationships.ts +0 -75
  129. package/src/playground/features/playground/data/scenarios/types.ts +0 -70
  130. package/src/playground/features/playground/data/schema.ts +0 -91
  131. package/src/playground/features/playground/data/seed.ts +0 -104
  132. package/src/playground/features/playground/services/QueryExecutionService.ts +0 -121
  133. package/src/runtime/orm-context.ts +0 -539
  134. package/tests/belongs-to-many.test.ts +0 -57
  135. package/tests/between.test.ts +0 -43
  136. package/tests/case-expression.test.ts +0 -58
  137. package/tests/complex-exists.test.ts +0 -230
  138. package/tests/cte.test.ts +0 -118
  139. package/tests/dml.test.ts +0 -206
  140. package/tests/exists.test.ts +0 -127
  141. package/tests/like.test.ts +0 -33
  142. package/tests/orm-runtime.test.ts +0 -254
  143. package/tests/postgres.test.ts +0 -30
  144. package/tests/right-join.test.ts +0 -89
  145. package/tests/subquery-having.test.ts +0 -193
  146. package/tests/window-function.test.ts +0 -151
  147. package/tsconfig.json +0 -30
  148. package/tsup.config.ts +0 -10
  149. package/vite.config.ts +0 -22
  150. package/vitest.config.ts +0 -14
  151. /package/src/{constants → core/sql}/sql.ts +0 -0
  152. /package/src/{runtime → orm}/als.ts +0 -0
  153. /package/src/{utils → query-builder}/relation-alias.ts +0 -0
@@ -1,393 +1,405 @@
1
- import { SelectQueryNode } from '../ast/query';
2
- import {
3
- ExpressionNode,
4
- OperandNode,
5
- BinaryExpressionNode,
6
- LogicalExpressionNode,
7
- InExpressionNode,
8
- NullExpressionNode,
9
- JsonPathNode,
10
- ExistsExpressionNode,
11
- BetweenExpressionNode,
12
- ScalarSubqueryNode,
13
- CaseExpressionNode,
14
- WindowFunctionNode,
15
- ColumnNode,
16
- LiteralNode,
17
- FunctionNode
18
- } from '../ast/expression';
19
- import { SQL_OPERATOR_REGISTRY } from '../constants/sql-operator-config';
20
- import { SqlOperator } from '../constants/sql';
21
- import { isRelationAlias } from '../utils/relation-alias';
22
-
23
- /**
24
- * Capitalizes the first letter of a string
25
- * @param s - String to capitalize
26
- * @returns Capitalized string
27
- */
28
- const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
29
-
30
- const assertNever = (value: never): never => {
31
- throw new Error(`Unhandled SQL operator: ${value}`);
32
- };
33
-
34
- /**
35
- * Function type for printing expression nodes
36
- */
37
- type ExpressionPrinter = (expr: ExpressionNode) => string;
38
-
39
- /**
40
- * Function type for printing operand nodes
41
- */
42
- type OperandPrinter = (node: OperandNode) => string;
43
-
44
- /**
45
- * Generates TypeScript code from query AST nodes
46
- */
47
- export class TypeScriptGenerator {
48
- private readonly expressionPrinters: Partial<Record<ExpressionNode['type'], ExpressionPrinter>>;
49
- private readonly operandPrinters: Partial<Record<OperandNode['type'], OperandPrinter>>;
50
-
51
- /**
52
- * Creates a new TypeScriptGenerator instance
53
- */
54
- constructor() {
55
- this.expressionPrinters = {
56
- BinaryExpression: expr => this.printBinaryExpression(expr as BinaryExpressionNode),
57
- LogicalExpression: expr => this.printLogicalExpression(expr as LogicalExpressionNode),
58
- InExpression: expr => this.printInExpression(expr as InExpressionNode),
59
- NullExpression: expr => this.printNullExpression(expr as NullExpressionNode),
60
- BetweenExpression: expr => this.printBetweenExpression(expr as BetweenExpressionNode),
61
- ExistsExpression: expr => this.printExistsExpression(expr as ExistsExpressionNode)
62
- };
63
-
64
- this.operandPrinters = {
65
- Column: node => this.printColumnOperand(node as ColumnNode),
66
- Literal: node => this.printLiteralOperand(node as LiteralNode),
67
- Function: node => this.printFunctionOperand(node as FunctionNode),
68
- JsonPath: node => this.printJsonPathOperand(node as JsonPathNode),
69
- ScalarSubquery: node => this.printScalarSubqueryOperand(node as ScalarSubqueryNode),
70
- CaseExpression: node => this.printCaseExpressionOperand(node as CaseExpressionNode),
71
- WindowFunction: node => this.printWindowFunctionOperand(node as WindowFunctionNode)
72
- };
73
- }
74
-
75
- /**
76
- * Generates TypeScript code from a query AST
77
- * @param ast - Query AST to generate code from
78
- * @returns Generated TypeScript code
79
- */
80
- generate(ast: SelectQueryNode): string {
81
- const chainLines = this.buildSelectLines(ast);
82
- const lines = chainLines.map((line, index) => (index === 0 ? `const query = ${line}` : line));
83
- lines.push(';', '', 'await query.execute();');
84
- return lines.join('\n');
85
- }
86
-
87
- /**
88
- * Builds TypeScript method chain lines from query AST
89
- * @param ast - Query AST
90
- * @returns Array of TypeScript method chain lines
91
- */
92
- private buildSelectLines(ast: SelectQueryNode): string[] {
93
- const lines: string[] = [];
94
- const hydration = ast.meta?.hydration;
95
- const hydratedRelations = new Set(hydration?.relations?.map(r => r.name) ?? []);
96
-
97
- const selections = ast.columns
98
- .filter(col => !(hydration && isRelationAlias((col as any).alias)))
99
- .map(col => {
100
- const key = (col as any).alias || (col as any).name;
101
- const operand = col as OperandNode;
102
- return `${key}: ${this.printOperand(operand)}`;
103
- });
104
-
105
- lines.push(`db.select({`);
106
- selections.forEach((sel, index) => {
107
- lines.push(` ${sel}${index < selections.length - 1 ? ',' : ''}`);
108
- });
109
- lines.push(`})`);
110
- lines.push(`.from(${capitalize(ast.from.name)})`);
111
-
112
- if (ast.distinct && ast.distinct.length) {
113
- const cols = ast.distinct.map(c => `${capitalize(c.table)}.${c.name}`).join(', ');
114
- lines.push(`.distinct(${cols})`);
115
- }
116
-
117
- ast.joins.forEach(join => {
118
- if (join.relationName && hydratedRelations.has(join.relationName)) {
119
- return;
120
- }
121
-
122
- if (join.relationName) {
123
- if (join.kind === 'INNER') {
124
- lines.push(`.joinRelation('${join.relationName}')`);
125
- } else {
126
- lines.push(`.joinRelation('${join.relationName}', '${join.kind}')`);
127
- }
128
- } else {
129
- const table = capitalize(join.table.name);
130
- const cond = this.printExpression(join.condition);
131
- let method = 'innerJoin';
132
- if (join.kind === 'LEFT') method = 'leftJoin';
133
- if (join.kind === 'RIGHT') method = 'rightJoin';
134
- lines.push(`.${method}(${table}, ${cond})`);
135
- }
136
- });
137
-
138
- if (hydration?.relations?.length) {
139
- hydration.relations.forEach(rel => {
140
- const options: string[] = [];
141
- if (rel.columns.length) options.push(`columns: [${rel.columns.map(c => `'${c}'`).join(', ')}]`);
142
- if (rel.aliasPrefix !== rel.name) options.push(`aliasPrefix: '${rel.aliasPrefix}'`);
143
- const opts = options.length ? `, { ${options.join(', ')} }` : '';
144
- lines.push(`.include('${rel.name}'${opts})`);
145
- });
146
- }
147
-
148
- if (ast.where) {
149
- lines.push(`.where(${this.printExpression(ast.where)})`);
150
- }
151
-
152
- if (ast.groupBy && ast.groupBy.length) {
153
- const cols = ast.groupBy.map(c => `${capitalize(c.table)}.${c.name}`).join(', ');
154
- lines.push(`.groupBy(${cols})`);
155
- }
156
-
157
- if (ast.having) {
158
- lines.push(`.having(${this.printExpression(ast.having)})`);
159
- }
160
-
161
- if (ast.orderBy && ast.orderBy.length) {
162
- ast.orderBy.forEach(o => {
163
- lines.push(`.orderBy(${capitalize(o.column.table)}.${o.column.name}, '${o.direction}')`);
164
- });
165
- }
166
-
167
- if (ast.limit) lines.push(`.limit(${ast.limit})`);
168
- if (ast.offset) lines.push(`.offset(${ast.offset})`);
169
-
170
- return lines;
171
- }
172
-
173
- /**
174
- * Prints an expression node to TypeScript code
175
- * @param expr - Expression node to print
176
- * @returns TypeScript code representation
177
- */
178
- private printExpression(expr: ExpressionNode): string {
179
- const printer = this.expressionPrinters[expr.type];
180
- if (!printer) {
181
- throw new Error(`Unsupported expression type "${expr.type}" in TypeScript generator`);
182
- }
183
- return printer(expr);
184
- }
185
-
186
- /**
187
- * Prints an operand node to TypeScript code
188
- * @param node - Operand node to print
189
- * @returns TypeScript code representation
190
- */
191
- private printOperand(node: OperandNode): string {
192
- const printer = this.operandPrinters[node.type];
193
- if (!printer) {
194
- throw new Error(`Unsupported operand type "${node.type}" in TypeScript generator`);
195
- }
196
- return printer(node);
197
- }
198
-
199
- /**
200
- * Prints a binary expression to TypeScript code
201
- * @param binary - Binary expression node
202
- * @returns TypeScript code representation
203
- */
204
- private printBinaryExpression(binary: BinaryExpressionNode): string {
205
- const left = this.printOperand(binary.left);
206
- const right = this.printOperand(binary.right);
207
- const fn = this.mapOp(binary.operator);
208
- const args = [left, right];
209
- if (binary.escape) {
210
- args.push(this.printOperand(binary.escape));
211
- }
212
- return `${fn}(${args.join(', ')})`;
213
- }
214
-
215
- /**
216
- * Prints a logical expression to TypeScript code
217
- * @param logical - Logical expression node
218
- * @returns TypeScript code representation
219
- */
220
- private printLogicalExpression(logical: LogicalExpressionNode): string {
221
- if (logical.operands.length === 0) return '';
222
- const parts = logical.operands.map(op => {
223
- const compiled = this.printExpression(op);
224
- return op.type === 'LogicalExpression' ? `(${compiled})` : compiled;
225
- });
226
- return `${this.mapOp(logical.operator)}(\n ${parts.join(',\n ')}\n )`;
227
- }
228
-
229
- /**
230
- * Prints an IN expression to TypeScript code
231
- * @param inExpr - IN expression node
232
- * @returns TypeScript code representation
233
- */
234
- private printInExpression(inExpr: InExpressionNode): string {
235
- const left = this.printOperand(inExpr.left);
236
- const values = inExpr.right.map(v => this.printOperand(v)).join(', ');
237
- const fn = this.mapOp(inExpr.operator);
238
- return `${fn}(${left}, [${values}])`;
239
- }
240
-
241
- /**
242
- * Prints a null expression to TypeScript code
243
- * @param nullExpr - Null expression node
244
- * @returns TypeScript code representation
245
- */
246
- private printNullExpression(nullExpr: NullExpressionNode): string {
247
- const left = this.printOperand(nullExpr.left);
248
- const fn = this.mapOp(nullExpr.operator);
249
- return `${fn}(${left})`;
250
- }
251
-
252
- /**
253
- * Prints a BETWEEN expression to TypeScript code
254
- * @param betweenExpr - BETWEEN expression node
255
- * @returns TypeScript code representation
256
- */
257
- private printBetweenExpression(betweenExpr: BetweenExpressionNode): string {
258
- const left = this.printOperand(betweenExpr.left);
259
- const lower = this.printOperand(betweenExpr.lower);
260
- const upper = this.printOperand(betweenExpr.upper);
261
- return `${this.mapOp(betweenExpr.operator)}(${left}, ${lower}, ${upper})`;
262
- }
263
-
264
- /**
265
- * Prints an EXISTS expression to TypeScript code
266
- * @param existsExpr - EXISTS expression node
267
- * @returns TypeScript code representation
268
- */
269
- private printExistsExpression(existsExpr: ExistsExpressionNode): string {
270
- const subquery = this.inlineChain(this.buildSelectLines(existsExpr.subquery));
271
- return `${this.mapOp(existsExpr.operator)}(${subquery})`;
272
- }
273
-
274
- /**
275
- * Prints a column operand to TypeScript code
276
- * @param column - Column node
277
- * @returns TypeScript code representation
278
- */
279
- private printColumnOperand(column: ColumnNode): string {
280
- return `${capitalize(column.table)}.${column.name}`;
281
- }
282
-
283
- /**
284
- * Prints a literal operand to TypeScript code
285
- * @param literal - Literal node
286
- * @returns TypeScript code representation
287
- */
288
- private printLiteralOperand(literal: LiteralNode): string {
289
- if (literal.value === null) return 'null';
290
- return typeof literal.value === 'string' ? `'${literal.value}'` : String(literal.value);
291
- }
292
-
293
- /**
294
- * Prints a function operand to TypeScript code
295
- * @param fn - Function node
296
- * @returns TypeScript code representation
297
- */
298
- private printFunctionOperand(fn: FunctionNode): string {
299
- const args = fn.args.map(a => this.printOperand(a)).join(', ');
300
- return `${fn.name.toLowerCase()}(${args})`;
301
- }
302
-
303
- /**
304
- * Prints a JSON path operand to TypeScript code
305
- * @param json - JSON path node
306
- * @returns TypeScript code representation
307
- */
308
- private printJsonPathOperand(json: JsonPathNode): string {
309
- return `jsonPath(${capitalize(json.column.table)}.${json.column.name}, '${json.path}')`;
310
- }
311
-
312
- /**
313
- * Prints a scalar subquery operand to TypeScript code
314
- * @param node - Scalar subquery node
315
- * @returns TypeScript code representation
316
- */
317
- private printScalarSubqueryOperand(node: ScalarSubqueryNode): string {
318
- const subquery = this.inlineChain(this.buildSelectLines(node.query));
319
- return `(${subquery})`;
320
- }
321
-
322
- /**
323
- * Prints a CASE expression operand to TypeScript code
324
- * @param node - CASE expression node
325
- * @returns TypeScript code representation
326
- */
327
- private printCaseExpressionOperand(node: CaseExpressionNode): string {
328
- const clauses = node.conditions.map(
329
- condition =>
330
- `{ when: ${this.printExpression(condition.when)}, then: ${this.printOperand(condition.then)} }`
331
- );
332
- const elseValue = node.else ? `, ${this.printOperand(node.else)}` : '';
333
- return `caseWhen([${clauses.join(', ')}]${elseValue})`;
334
- }
335
-
336
- /**
337
- * Prints a window function operand to TypeScript code
338
- * @param node - Window function node
339
- * @returns TypeScript code representation
340
- */
341
- private printWindowFunctionOperand(node: WindowFunctionNode): string {
342
- let result = `${node.name}(`;
343
- if (node.args.length > 0) {
344
- result += node.args.map(arg => this.printOperand(arg)).join(', ');
345
- }
346
- result += ') OVER (';
347
-
348
- const parts: string[] = [];
349
-
350
- if (node.partitionBy && node.partitionBy.length > 0) {
351
- const partitionClause =
352
- 'PARTITION BY ' + node.partitionBy.map(col => `${capitalize(col.table)}.${col.name}`).join(', ');
353
- parts.push(partitionClause);
354
- }
355
-
356
- if (node.orderBy && node.orderBy.length > 0) {
357
- const orderClause =
358
- 'ORDER BY ' +
359
- node.orderBy.map(o => `${capitalize(o.column.table)}.${o.column.name} ${o.direction}`).join(', ');
360
- parts.push(orderClause);
361
- }
362
-
363
- result += parts.join(' ');
364
- result += ')';
365
-
366
- return result;
367
- }
368
-
369
- /**
370
- * Converts method chain lines to inline format
371
- * @param lines - Method chain lines
372
- * @returns Inline method chain string
373
- */
374
- private inlineChain(lines: string[]): string {
375
- return lines
376
- .map(line => line.trim())
377
- .filter(line => line.length > 0)
378
- .join(' ');
379
- }
380
-
381
- /**
382
- * Maps SQL operators to TypeScript function names
383
- * @param op - SQL operator
384
- * @returns TypeScript function name
385
- */
386
- private mapOp(op: SqlOperator): string {
387
- const config = SQL_OPERATOR_REGISTRY[op];
388
- if (!config) {
389
- return assertNever(op as never);
390
- }
391
- return config.tsName;
392
- }
393
- }
1
+ import { SelectQueryNode } from '../core/ast/query.js';
2
+ import {
3
+ ExpressionNode,
4
+ OperandNode,
5
+ BinaryExpressionNode,
6
+ LogicalExpressionNode,
7
+ InExpressionNode,
8
+ NullExpressionNode,
9
+ JsonPathNode,
10
+ ExistsExpressionNode,
11
+ BetweenExpressionNode,
12
+ ScalarSubqueryNode,
13
+ CaseExpressionNode,
14
+ WindowFunctionNode,
15
+ ColumnNode,
16
+ LiteralNode,
17
+ FunctionNode,
18
+ ExpressionVisitor,
19
+ OperandVisitor,
20
+ visitExpression,
21
+ visitOperand
22
+ } from '../core/ast/expression.js';
23
+ import { SQL_OPERATOR_REGISTRY } from '../core/sql/sql-operator-config.js';
24
+ import { SqlOperator } from '../core/sql/sql.js';
25
+ import { isRelationAlias } from '../query-builder/relation-alias.js';
26
+
27
+ /**
28
+ * Capitalizes the first letter of a string
29
+ * @param s - String to capitalize
30
+ * @returns Capitalized string
31
+ */
32
+ const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
33
+
34
+ const assertNever = (value: never): never => {
35
+ throw new Error(`Unhandled SQL operator: ${value}`);
36
+ };
37
+
38
+ /**
39
+ * Generates TypeScript code from query AST nodes
40
+ */
41
+ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVisitor<string> {
42
+
43
+ /**
44
+ * Generates TypeScript code from a query AST
45
+ * @param ast - Query AST to generate code from
46
+ * @returns Generated TypeScript code
47
+ */
48
+ generate(ast: SelectQueryNode): string {
49
+ const chainLines = this.buildSelectLines(ast);
50
+ const lines = chainLines.map((line, index) => (index === 0 ? `const query = ${line}` : line));
51
+ lines.push(';', '', 'await query.execute();');
52
+ return lines.join('\n');
53
+ }
54
+
55
+ /**
56
+ * Builds TypeScript method chain lines from query AST
57
+ * @param ast - Query AST
58
+ * @returns Array of TypeScript method chain lines
59
+ */
60
+ private buildSelectLines(ast: SelectQueryNode): string[] {
61
+ const lines: string[] = [];
62
+ const hydration = ast.meta?.hydration;
63
+ const hydratedRelations = new Set(hydration?.relations?.map(r => r.name) ?? []);
64
+
65
+ const selections = ast.columns
66
+ .filter(col => !(hydration && isRelationAlias((col as any).alias)))
67
+ .map(col => {
68
+ const key = (col as any).alias || (col as any).name;
69
+ const operand = col as OperandNode;
70
+ return `${key}: ${this.printOperand(operand)}`;
71
+ });
72
+
73
+ lines.push(`db.select({`);
74
+ selections.forEach((sel, index) => {
75
+ lines.push(` ${sel}${index < selections.length - 1 ? ',' : ''}`);
76
+ });
77
+ lines.push(`})`);
78
+ lines.push(`.from(${capitalize(ast.from.name)})`);
79
+
80
+ if (ast.distinct && ast.distinct.length) {
81
+ const cols = ast.distinct.map(c => `${capitalize(c.table)}.${c.name}`).join(', ');
82
+ lines.push(`.distinct(${cols})`);
83
+ }
84
+
85
+ ast.joins.forEach(join => {
86
+ if (join.relationName && hydratedRelations.has(join.relationName)) {
87
+ return;
88
+ }
89
+
90
+ if (join.relationName) {
91
+ if (join.kind === 'INNER') {
92
+ lines.push(`.joinRelation('${join.relationName}')`);
93
+ } else {
94
+ lines.push(`.joinRelation('${join.relationName}', '${join.kind}')`);
95
+ }
96
+ } else {
97
+ const table = capitalize(join.table.name);
98
+ const cond = this.printExpression(join.condition);
99
+ let method = 'innerJoin';
100
+ if (join.kind === 'LEFT') method = 'leftJoin';
101
+ if (join.kind === 'RIGHT') method = 'rightJoin';
102
+ lines.push(`.${method}(${table}, ${cond})`);
103
+ }
104
+ });
105
+
106
+ if (hydration?.relations?.length) {
107
+ hydration.relations.forEach(rel => {
108
+ const options: string[] = [];
109
+ if (rel.columns.length) options.push(`columns: [${rel.columns.map(c => `'${c}'`).join(', ')}]`);
110
+ if (rel.aliasPrefix !== rel.name) options.push(`aliasPrefix: '${rel.aliasPrefix}'`);
111
+ const opts = options.length ? `, { ${options.join(', ')} }` : '';
112
+ lines.push(`.include('${rel.name}'${opts})`);
113
+ });
114
+ }
115
+
116
+ if (ast.where) {
117
+ lines.push(`.where(${this.printExpression(ast.where)})`);
118
+ }
119
+
120
+ if (ast.groupBy && ast.groupBy.length) {
121
+ const cols = ast.groupBy.map(c => `${capitalize(c.table)}.${c.name}`).join(', ');
122
+ lines.push(`.groupBy(${cols})`);
123
+ }
124
+
125
+ if (ast.having) {
126
+ lines.push(`.having(${this.printExpression(ast.having)})`);
127
+ }
128
+
129
+ if (ast.orderBy && ast.orderBy.length) {
130
+ ast.orderBy.forEach(o => {
131
+ lines.push(`.orderBy(${capitalize(o.column.table)}.${o.column.name}, '${o.direction}')`);
132
+ });
133
+ }
134
+
135
+ if (ast.limit) lines.push(`.limit(${ast.limit})`);
136
+ if (ast.offset) lines.push(`.offset(${ast.offset})`);
137
+
138
+ return lines;
139
+ }
140
+
141
+ /**
142
+ * Prints an expression node to TypeScript code
143
+ * @param expr - Expression node to print
144
+ * @returns TypeScript code representation
145
+ */
146
+ private printExpression(expr: ExpressionNode): string {
147
+ return visitExpression(expr, this);
148
+ }
149
+
150
+ /**
151
+ * Prints an operand node to TypeScript code
152
+ * @param node - Operand node to print
153
+ * @returns TypeScript code representation
154
+ */
155
+ private printOperand(node: OperandNode): string {
156
+ return visitOperand(node, this);
157
+ }
158
+
159
+ public visitBinaryExpression(binary: BinaryExpressionNode): string {
160
+ return this.printBinaryExpression(binary);
161
+ }
162
+
163
+ public visitLogicalExpression(logical: LogicalExpressionNode): string {
164
+ return this.printLogicalExpression(logical);
165
+ }
166
+
167
+ public visitNullExpression(nullExpr: NullExpressionNode): string {
168
+ return this.printNullExpression(nullExpr);
169
+ }
170
+
171
+ public visitInExpression(inExpr: InExpressionNode): string {
172
+ return this.printInExpression(inExpr);
173
+ }
174
+
175
+ public visitExistsExpression(existsExpr: ExistsExpressionNode): string {
176
+ return this.printExistsExpression(existsExpr);
177
+ }
178
+
179
+ public visitBetweenExpression(betweenExpr: BetweenExpressionNode): string {
180
+ return this.printBetweenExpression(betweenExpr);
181
+ }
182
+
183
+ public visitColumn(node: ColumnNode): string {
184
+ return this.printColumnOperand(node);
185
+ }
186
+
187
+ public visitLiteral(node: LiteralNode): string {
188
+ return this.printLiteralOperand(node);
189
+ }
190
+
191
+ public visitFunction(node: FunctionNode): string {
192
+ return this.printFunctionOperand(node);
193
+ }
194
+
195
+ public visitJsonPath(node: JsonPathNode): string {
196
+ return this.printJsonPathOperand(node);
197
+ }
198
+
199
+ public visitScalarSubquery(node: ScalarSubqueryNode): string {
200
+ return this.printScalarSubqueryOperand(node);
201
+ }
202
+
203
+ public visitCaseExpression(node: CaseExpressionNode): string {
204
+ return this.printCaseExpressionOperand(node);
205
+ }
206
+
207
+ public visitWindowFunction(node: WindowFunctionNode): string {
208
+ return this.printWindowFunctionOperand(node);
209
+ }
210
+
211
+ /**
212
+ * Prints a binary expression to TypeScript code
213
+ * @param binary - Binary expression node
214
+ * @returns TypeScript code representation
215
+ */
216
+ private printBinaryExpression(binary: BinaryExpressionNode): string {
217
+ const left = this.printOperand(binary.left);
218
+ const right = this.printOperand(binary.right);
219
+ const fn = this.mapOp(binary.operator);
220
+ const args = [left, right];
221
+ if (binary.escape) {
222
+ args.push(this.printOperand(binary.escape));
223
+ }
224
+ return `${fn}(${args.join(', ')})`;
225
+ }
226
+
227
+ /**
228
+ * Prints a logical expression to TypeScript code
229
+ * @param logical - Logical expression node
230
+ * @returns TypeScript code representation
231
+ */
232
+ private printLogicalExpression(logical: LogicalExpressionNode): string {
233
+ if (logical.operands.length === 0) return '';
234
+ const parts = logical.operands.map(op => {
235
+ const compiled = this.printExpression(op);
236
+ return op.type === 'LogicalExpression' ? `(${compiled})` : compiled;
237
+ });
238
+ return `${this.mapOp(logical.operator)}(\n ${parts.join(',\n ')}\n )`;
239
+ }
240
+
241
+ /**
242
+ * Prints an IN expression to TypeScript code
243
+ * @param inExpr - IN expression node
244
+ * @returns TypeScript code representation
245
+ */
246
+ private printInExpression(inExpr: InExpressionNode): string {
247
+ const left = this.printOperand(inExpr.left);
248
+ const values = inExpr.right.map(v => this.printOperand(v)).join(', ');
249
+ const fn = this.mapOp(inExpr.operator);
250
+ return `${fn}(${left}, [${values}])`;
251
+ }
252
+
253
+ /**
254
+ * Prints a null expression to TypeScript code
255
+ * @param nullExpr - Null expression node
256
+ * @returns TypeScript code representation
257
+ */
258
+ private printNullExpression(nullExpr: NullExpressionNode): string {
259
+ const left = this.printOperand(nullExpr.left);
260
+ const fn = this.mapOp(nullExpr.operator);
261
+ return `${fn}(${left})`;
262
+ }
263
+
264
+ /**
265
+ * Prints a BETWEEN expression to TypeScript code
266
+ * @param betweenExpr - BETWEEN expression node
267
+ * @returns TypeScript code representation
268
+ */
269
+ private printBetweenExpression(betweenExpr: BetweenExpressionNode): string {
270
+ const left = this.printOperand(betweenExpr.left);
271
+ const lower = this.printOperand(betweenExpr.lower);
272
+ const upper = this.printOperand(betweenExpr.upper);
273
+ return `${this.mapOp(betweenExpr.operator)}(${left}, ${lower}, ${upper})`;
274
+ }
275
+
276
+ /**
277
+ * Prints an EXISTS expression to TypeScript code
278
+ * @param existsExpr - EXISTS expression node
279
+ * @returns TypeScript code representation
280
+ */
281
+ private printExistsExpression(existsExpr: ExistsExpressionNode): string {
282
+ const subquery = this.inlineChain(this.buildSelectLines(existsExpr.subquery));
283
+ return `${this.mapOp(existsExpr.operator)}(${subquery})`;
284
+ }
285
+
286
+ /**
287
+ * Prints a column operand to TypeScript code
288
+ * @param column - Column node
289
+ * @returns TypeScript code representation
290
+ */
291
+ private printColumnOperand(column: ColumnNode): string {
292
+ return `${capitalize(column.table)}.${column.name}`;
293
+ }
294
+
295
+ /**
296
+ * Prints a literal operand to TypeScript code
297
+ * @param literal - Literal node
298
+ * @returns TypeScript code representation
299
+ */
300
+ private printLiteralOperand(literal: LiteralNode): string {
301
+ if (literal.value === null) return 'null';
302
+ return typeof literal.value === 'string' ? `'${literal.value}'` : String(literal.value);
303
+ }
304
+
305
+ /**
306
+ * Prints a function operand to TypeScript code
307
+ * @param fn - Function node
308
+ * @returns TypeScript code representation
309
+ */
310
+ private printFunctionOperand(fn: FunctionNode): string {
311
+ const args = fn.args.map(a => this.printOperand(a)).join(', ');
312
+ return `${fn.name.toLowerCase()}(${args})`;
313
+ }
314
+
315
+ /**
316
+ * Prints a JSON path operand to TypeScript code
317
+ * @param json - JSON path node
318
+ * @returns TypeScript code representation
319
+ */
320
+ private printJsonPathOperand(json: JsonPathNode): string {
321
+ return `jsonPath(${capitalize(json.column.table)}.${json.column.name}, '${json.path}')`;
322
+ }
323
+
324
+ /**
325
+ * Prints a scalar subquery operand to TypeScript code
326
+ * @param node - Scalar subquery node
327
+ * @returns TypeScript code representation
328
+ */
329
+ private printScalarSubqueryOperand(node: ScalarSubqueryNode): string {
330
+ const subquery = this.inlineChain(this.buildSelectLines(node.query));
331
+ return `(${subquery})`;
332
+ }
333
+
334
+ /**
335
+ * Prints a CASE expression operand to TypeScript code
336
+ * @param node - CASE expression node
337
+ * @returns TypeScript code representation
338
+ */
339
+ private printCaseExpressionOperand(node: CaseExpressionNode): string {
340
+ const clauses = node.conditions.map(
341
+ condition =>
342
+ `{ when: ${this.printExpression(condition.when)}, then: ${this.printOperand(condition.then)} }`
343
+ );
344
+ const elseValue = node.else ? `, ${this.printOperand(node.else)}` : '';
345
+ return `caseWhen([${clauses.join(', ')}]${elseValue})`;
346
+ }
347
+
348
+ /**
349
+ * Prints a window function operand to TypeScript code
350
+ * @param node - Window function node
351
+ * @returns TypeScript code representation
352
+ */
353
+ private printWindowFunctionOperand(node: WindowFunctionNode): string {
354
+ let result = `${node.name}(`;
355
+ if (node.args.length > 0) {
356
+ result += node.args.map(arg => this.printOperand(arg)).join(', ');
357
+ }
358
+ result += ') OVER (';
359
+
360
+ const parts: string[] = [];
361
+
362
+ if (node.partitionBy && node.partitionBy.length > 0) {
363
+ const partitionClause =
364
+ 'PARTITION BY ' + node.partitionBy.map(col => `${capitalize(col.table)}.${col.name}`).join(', ');
365
+ parts.push(partitionClause);
366
+ }
367
+
368
+ if (node.orderBy && node.orderBy.length > 0) {
369
+ const orderClause =
370
+ 'ORDER BY ' +
371
+ node.orderBy.map(o => `${capitalize(o.column.table)}.${o.column.name} ${o.direction}`).join(', ');
372
+ parts.push(orderClause);
373
+ }
374
+
375
+ result += parts.join(' ');
376
+ result += ')';
377
+
378
+ return result;
379
+ }
380
+
381
+ /**
382
+ * Converts method chain lines to inline format
383
+ * @param lines - Method chain lines
384
+ * @returns Inline method chain string
385
+ */
386
+ private inlineChain(lines: string[]): string {
387
+ return lines
388
+ .map(line => line.trim())
389
+ .filter(line => line.length > 0)
390
+ .join(' ');
391
+ }
392
+
393
+ /**
394
+ * Maps SQL operators to TypeScript function names
395
+ * @param op - SQL operator
396
+ * @returns TypeScript function name
397
+ */
398
+ private mapOp(op: SqlOperator): string {
399
+ const config = SQL_OPERATOR_REGISTRY[op];
400
+ if (!config) {
401
+ return assertNever(op as never);
402
+ }
403
+ return config.tsName;
404
+ }
405
+ }