metal-orm 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/package.json +1 -1
- package/src/ast/expression.ts +433 -175
- package/src/ast/join.ts +8 -1
- package/src/ast/query.ts +64 -9
- package/src/builder/hydration-manager.ts +42 -11
- package/src/builder/hydration-planner.ts +80 -31
- package/src/builder/operations/column-selector.ts +37 -1
- package/src/builder/operations/cte-manager.ts +16 -0
- package/src/builder/operations/filter-manager.ts +32 -0
- package/src/builder/operations/join-manager.ts +17 -7
- package/src/builder/operations/pagination-manager.ts +19 -0
- package/src/builder/operations/relation-manager.ts +58 -3
- package/src/builder/query-ast-service.ts +100 -29
- package/src/builder/relation-conditions.ts +30 -1
- package/src/builder/relation-projection-helper.ts +43 -1
- package/src/builder/relation-service.ts +68 -13
- package/src/builder/relation-types.ts +6 -0
- package/src/builder/select-query-builder-deps.ts +64 -3
- package/src/builder/select-query-state.ts +72 -0
- package/src/builder/select.ts +166 -0
- package/src/codegen/typescript.ts +142 -44
- package/src/constants/sql-operator-config.ts +36 -0
- package/src/constants/sql.ts +125 -58
- package/src/dialect/abstract.ts +97 -22
- package/src/dialect/mssql/index.ts +27 -0
- package/src/dialect/mysql/index.ts +22 -0
- package/src/dialect/postgres/index.ts +22 -0
- package/src/dialect/sqlite/index.ts +22 -0
- package/src/runtime/als.ts +15 -1
- package/src/runtime/hydration.ts +20 -15
- package/src/schema/column.ts +45 -5
- package/src/schema/relation.ts +49 -2
- package/src/schema/table.ts +27 -3
- package/src/utils/join-node.ts +20 -0
- package/src/utils/raw-column-parser.ts +32 -0
- package/src/utils/relation-alias.ts +43 -0
package/src/dialect/abstract.ts
CHANGED
|
@@ -17,17 +17,35 @@ import {
|
|
|
17
17
|
BetweenExpressionNode
|
|
18
18
|
} from '../ast/expression';
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Context for SQL compilation with parameter management
|
|
22
|
+
*/
|
|
20
23
|
export interface CompilerContext {
|
|
24
|
+
/** Array of parameters */
|
|
21
25
|
params: unknown[];
|
|
26
|
+
/** Function to add a parameter and get its placeholder */
|
|
22
27
|
addParameter(value: unknown): string;
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Result of SQL compilation
|
|
32
|
+
*/
|
|
25
33
|
export interface CompiledQuery {
|
|
34
|
+
/** Generated SQL string */
|
|
26
35
|
sql: string;
|
|
36
|
+
/** Parameters for the query */
|
|
27
37
|
params: unknown[];
|
|
28
38
|
}
|
|
29
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Abstract base class for SQL dialect implementations
|
|
42
|
+
*/
|
|
30
43
|
export abstract class Dialect {
|
|
44
|
+
/**
|
|
45
|
+
* Compiles a SELECT query AST to SQL
|
|
46
|
+
* @param ast - Query AST to compile
|
|
47
|
+
* @returns Compiled query with SQL and parameters
|
|
48
|
+
*/
|
|
31
49
|
compileSelect(ast: SelectQueryNode): CompiledQuery {
|
|
32
50
|
const ctx = this.createCompilerContext();
|
|
33
51
|
const rawSql = this.compileSelectAst(ast, ctx).trim();
|
|
@@ -38,20 +56,40 @@ export abstract class Dialect {
|
|
|
38
56
|
};
|
|
39
57
|
}
|
|
40
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Compiles SELECT query AST to SQL (to be implemented by concrete dialects)
|
|
61
|
+
* @param ast - Query AST
|
|
62
|
+
* @param ctx - Compiler context
|
|
63
|
+
* @returns SQL string
|
|
64
|
+
*/
|
|
41
65
|
protected abstract compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string;
|
|
42
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Quotes an SQL identifier (to be implemented by concrete dialects)
|
|
69
|
+
* @param id - Identifier to quote
|
|
70
|
+
* @returns Quoted identifier
|
|
71
|
+
*/
|
|
43
72
|
abstract quoteIdentifier(id: string): string;
|
|
44
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Compiles a WHERE clause
|
|
76
|
+
* @param where - WHERE expression
|
|
77
|
+
* @param ctx - Compiler context
|
|
78
|
+
* @returns SQL WHERE clause or empty string
|
|
79
|
+
*/
|
|
45
80
|
protected compileWhere(where: ExpressionNode | undefined, ctx: CompilerContext): string {
|
|
46
81
|
if (!where) return '';
|
|
47
82
|
return ` WHERE ${this.compileExpression(where, ctx)}`;
|
|
48
83
|
}
|
|
49
84
|
|
|
50
85
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
86
|
+
* Generates subquery for EXISTS expressions
|
|
87
|
+
* Rule: Always forces SELECT 1, ignoring column list
|
|
88
|
+
* Maintains FROM, JOINs, WHERE, GROUP BY, ORDER BY, LIMIT/OFFSET
|
|
89
|
+
* Does not add ';' at the end
|
|
90
|
+
* @param ast - Query AST
|
|
91
|
+
* @param ctx - Compiler context
|
|
92
|
+
* @returns SQL for EXISTS subquery
|
|
55
93
|
*/
|
|
56
94
|
protected compileSelectForExists(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
57
95
|
const full = this.compileSelectAst(ast, ctx).trim().replace(/;$/, '');
|
|
@@ -65,6 +103,10 @@ export abstract class Dialect {
|
|
|
65
103
|
return `SELECT 1${tail}`;
|
|
66
104
|
}
|
|
67
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Creates a new compiler context
|
|
108
|
+
* @returns Compiler context with parameter management
|
|
109
|
+
*/
|
|
68
110
|
protected createCompilerContext(): CompilerContext {
|
|
69
111
|
const params: unknown[] = [];
|
|
70
112
|
let counter = 0;
|
|
@@ -78,6 +120,11 @@ export abstract class Dialect {
|
|
|
78
120
|
};
|
|
79
121
|
}
|
|
80
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Formats a parameter placeholder
|
|
125
|
+
* @param index - Parameter index
|
|
126
|
+
* @returns Formatted placeholder string
|
|
127
|
+
*/
|
|
81
128
|
protected formatPlaceholder(index: number): string {
|
|
82
129
|
return '?';
|
|
83
130
|
}
|
|
@@ -92,35 +139,63 @@ export abstract class Dialect {
|
|
|
92
139
|
this.registerDefaultExpressionCompilers();
|
|
93
140
|
}
|
|
94
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Registers an expression compiler for a specific node type
|
|
144
|
+
* @param type - Expression node type
|
|
145
|
+
* @param compiler - Compiler function
|
|
146
|
+
*/
|
|
95
147
|
protected registerExpressionCompiler<T extends ExpressionNode>(type: T['type'], compiler: (node: T, ctx: CompilerContext) => string): void {
|
|
96
148
|
this.expressionCompilers.set(type, compiler as (node: ExpressionNode, ctx: CompilerContext) => string);
|
|
97
149
|
}
|
|
98
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Registers an operand compiler for a specific node type
|
|
153
|
+
* @param type - Operand node type
|
|
154
|
+
* @param compiler - Compiler function
|
|
155
|
+
*/
|
|
99
156
|
protected registerOperandCompiler<T extends OperandNode>(type: T['type'], compiler: (node: T, ctx: CompilerContext) => string): void {
|
|
100
157
|
this.operandCompilers.set(type, compiler as (node: OperandNode, ctx: CompilerContext) => string);
|
|
101
158
|
}
|
|
102
159
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
160
|
+
/**
|
|
161
|
+
* Compiles an expression node
|
|
162
|
+
* @param node - Expression node to compile
|
|
163
|
+
* @param ctx - Compiler context
|
|
164
|
+
* @returns Compiled SQL expression
|
|
165
|
+
*/
|
|
166
|
+
protected compileExpression(node: ExpressionNode, ctx: CompilerContext): string {
|
|
167
|
+
const compiler = this.expressionCompilers.get(node.type);
|
|
168
|
+
if (!compiler) {
|
|
169
|
+
throw new Error(`Unsupported expression node type "${node.type}" for ${this.constructor.name}`);
|
|
170
|
+
}
|
|
171
|
+
return compiler(node, ctx);
|
|
172
|
+
}
|
|
107
173
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Compiles an operand node
|
|
176
|
+
* @param node - Operand node to compile
|
|
177
|
+
* @param ctx - Compiler context
|
|
178
|
+
* @returns Compiled SQL operand
|
|
179
|
+
*/
|
|
180
|
+
protected compileOperand(node: OperandNode, ctx: CompilerContext): string {
|
|
181
|
+
const compiler = this.operandCompilers.get(node.type);
|
|
182
|
+
if (!compiler) {
|
|
183
|
+
throw new Error(`Unsupported operand node type "${node.type}" for ${this.constructor.name}`);
|
|
184
|
+
}
|
|
185
|
+
return compiler(node, ctx);
|
|
186
|
+
}
|
|
112
187
|
|
|
113
188
|
private registerDefaultExpressionCompilers(): void {
|
|
114
|
-
this.registerExpressionCompiler('BinaryExpression', (binary: BinaryExpressionNode, ctx) => {
|
|
115
|
-
const left = this.compileOperand(binary.left, ctx);
|
|
116
|
-
const right = this.compileOperand(binary.right, ctx);
|
|
117
|
-
const base = `${left} ${binary.operator} ${right}`;
|
|
118
|
-
if (binary.escape) {
|
|
119
|
-
const escapeOperand = this.compileOperand(binary.escape, ctx);
|
|
120
|
-
return `${base} ESCAPE ${escapeOperand}`;
|
|
121
|
-
}
|
|
122
|
-
return base;
|
|
123
|
-
});
|
|
189
|
+
this.registerExpressionCompiler('BinaryExpression', (binary: BinaryExpressionNode, ctx) => {
|
|
190
|
+
const left = this.compileOperand(binary.left, ctx);
|
|
191
|
+
const right = this.compileOperand(binary.right, ctx);
|
|
192
|
+
const base = `${left} ${binary.operator} ${right}`;
|
|
193
|
+
if (binary.escape) {
|
|
194
|
+
const escapeOperand = this.compileOperand(binary.escape, ctx);
|
|
195
|
+
return `${base} ESCAPE ${escapeOperand}`;
|
|
196
|
+
}
|
|
197
|
+
return base;
|
|
198
|
+
});
|
|
124
199
|
|
|
125
200
|
this.registerExpressionCompiler('LogicalExpression', (logical: LogicalExpressionNode, ctx) => {
|
|
126
201
|
if (logical.operands.length === 0) return '';
|
|
@@ -2,25 +2,52 @@ import { CompilerContext, Dialect } from '../abstract';
|
|
|
2
2
|
import { SelectQueryNode } from '../../ast/query';
|
|
3
3
|
import { JsonPathNode } from '../../ast/expression';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Microsoft SQL Server dialect implementation
|
|
7
|
+
*/
|
|
5
8
|
export class SqlServerDialect extends Dialect {
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new SqlServerDialect instance
|
|
11
|
+
*/
|
|
6
12
|
public constructor() {
|
|
7
13
|
super();
|
|
8
14
|
}
|
|
9
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Quotes an identifier using SQL Server bracket syntax
|
|
18
|
+
* @param id - Identifier to quote
|
|
19
|
+
* @returns Quoted identifier
|
|
20
|
+
*/
|
|
10
21
|
quoteIdentifier(id: string): string {
|
|
11
22
|
return `[${id}]`;
|
|
12
23
|
}
|
|
13
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Compiles JSON path expression using SQL Server syntax
|
|
27
|
+
* @param node - JSON path node
|
|
28
|
+
* @returns SQL Server JSON path expression
|
|
29
|
+
*/
|
|
14
30
|
protected compileJsonPath(node: JsonPathNode): string {
|
|
15
31
|
const col = `${this.quoteIdentifier(node.column.table)}.${this.quoteIdentifier(node.column.name)}`;
|
|
16
32
|
// SQL Server uses JSON_VALUE(col, '$.path')
|
|
17
33
|
return `JSON_VALUE(${col}, '${node.path}')`;
|
|
18
34
|
}
|
|
19
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Formats parameter placeholders using SQL Server named parameter syntax
|
|
38
|
+
* @param index - Parameter index
|
|
39
|
+
* @returns Named parameter placeholder
|
|
40
|
+
*/
|
|
20
41
|
protected formatPlaceholder(index: number): string {
|
|
21
42
|
return `@p${index}`;
|
|
22
43
|
}
|
|
23
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Compiles SELECT query AST to SQL Server SQL
|
|
47
|
+
* @param ast - Query AST
|
|
48
|
+
* @param ctx - Compiler context
|
|
49
|
+
* @returns SQL Server SQL string
|
|
50
|
+
*/
|
|
24
51
|
protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
25
52
|
const columns = ast.columns.map(c => {
|
|
26
53
|
let expr = '';
|
|
@@ -2,21 +2,43 @@ import { CompilerContext, Dialect } from '../abstract';
|
|
|
2
2
|
import { SelectQueryNode } from '../../ast/query';
|
|
3
3
|
import { JsonPathNode } from '../../ast/expression';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* MySQL dialect implementation
|
|
7
|
+
*/
|
|
5
8
|
export class MySqlDialect extends Dialect {
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new MySqlDialect instance
|
|
11
|
+
*/
|
|
6
12
|
public constructor() {
|
|
7
13
|
super();
|
|
8
14
|
}
|
|
9
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Quotes an identifier using MySQL backtick syntax
|
|
18
|
+
* @param id - Identifier to quote
|
|
19
|
+
* @returns Quoted identifier
|
|
20
|
+
*/
|
|
10
21
|
quoteIdentifier(id: string): string {
|
|
11
22
|
return `\`${id}\``;
|
|
12
23
|
}
|
|
13
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Compiles JSON path expression using MySQL syntax
|
|
27
|
+
* @param node - JSON path node
|
|
28
|
+
* @returns MySQL JSON path expression
|
|
29
|
+
*/
|
|
14
30
|
protected compileJsonPath(node: JsonPathNode): string {
|
|
15
31
|
const col = `${this.quoteIdentifier(node.column.table)}.${this.quoteIdentifier(node.column.name)}`;
|
|
16
32
|
// MySQL 5.7+ uses col->'$.path'
|
|
17
33
|
return `${col}->'${node.path}'`;
|
|
18
34
|
}
|
|
19
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Compiles SELECT query AST to MySQL SQL
|
|
38
|
+
* @param ast - Query AST
|
|
39
|
+
* @param ctx - Compiler context
|
|
40
|
+
* @returns MySQL SQL string
|
|
41
|
+
*/
|
|
20
42
|
protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
21
43
|
const columns = ast.columns.map(c => {
|
|
22
44
|
let expr = '';
|
|
@@ -2,21 +2,43 @@ import { CompilerContext, Dialect } from '../abstract';
|
|
|
2
2
|
import { SelectQueryNode } from '../../ast/query';
|
|
3
3
|
import { JsonPathNode } from '../../ast/expression';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* PostgreSQL dialect implementation
|
|
7
|
+
*/
|
|
5
8
|
export class PostgresDialect extends Dialect {
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new PostgresDialect instance
|
|
11
|
+
*/
|
|
6
12
|
public constructor() {
|
|
7
13
|
super();
|
|
8
14
|
}
|
|
9
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Quotes an identifier using PostgreSQL double-quote syntax
|
|
18
|
+
* @param id - Identifier to quote
|
|
19
|
+
* @returns Quoted identifier
|
|
20
|
+
*/
|
|
10
21
|
quoteIdentifier(id: string): string {
|
|
11
22
|
return `"${id}"`;
|
|
12
23
|
}
|
|
13
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Compiles JSON path expression using PostgreSQL syntax
|
|
27
|
+
* @param node - JSON path node
|
|
28
|
+
* @returns PostgreSQL JSON path expression
|
|
29
|
+
*/
|
|
14
30
|
protected compileJsonPath(node: JsonPathNode): string {
|
|
15
31
|
const col = `${this.quoteIdentifier(node.column.table)}.${this.quoteIdentifier(node.column.name)}`;
|
|
16
32
|
// Postgres uses col->>'path' for text extraction
|
|
17
33
|
return `${col}->>'${node.path}'`;
|
|
18
34
|
}
|
|
19
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Compiles SELECT query AST to PostgreSQL SQL
|
|
38
|
+
* @param ast - Query AST
|
|
39
|
+
* @param ctx - Compiler context
|
|
40
|
+
* @returns PostgreSQL SQL string
|
|
41
|
+
*/
|
|
20
42
|
protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
21
43
|
const columns = ast.columns.map(c => {
|
|
22
44
|
let expr = '';
|
|
@@ -2,21 +2,43 @@ import { CompilerContext, Dialect } from '../abstract';
|
|
|
2
2
|
import { SelectQueryNode } from '../../ast/query';
|
|
3
3
|
import { JsonPathNode } from '../../ast/expression';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* SQLite dialect implementation
|
|
7
|
+
*/
|
|
5
8
|
export class SqliteDialect extends Dialect {
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new SqliteDialect instance
|
|
11
|
+
*/
|
|
6
12
|
public constructor() {
|
|
7
13
|
super();
|
|
8
14
|
}
|
|
9
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Quotes an identifier using SQLite double-quote syntax
|
|
18
|
+
* @param id - Identifier to quote
|
|
19
|
+
* @returns Quoted identifier
|
|
20
|
+
*/
|
|
10
21
|
quoteIdentifier(id: string): string {
|
|
11
22
|
return `"${id}"`;
|
|
12
23
|
}
|
|
13
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Compiles JSON path expression using SQLite syntax
|
|
27
|
+
* @param node - JSON path node
|
|
28
|
+
* @returns SQLite JSON path expression
|
|
29
|
+
*/
|
|
14
30
|
protected compileJsonPath(node: JsonPathNode): string {
|
|
15
31
|
const col = `${this.quoteIdentifier(node.column.table)}.${this.quoteIdentifier(node.column.name)}`;
|
|
16
32
|
// SQLite uses json_extract(col, '$.path')
|
|
17
33
|
return `json_extract(${col}, '${node.path}')`;
|
|
18
34
|
}
|
|
19
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Compiles SELECT query AST to SQLite SQL
|
|
38
|
+
* @param ast - Query AST
|
|
39
|
+
* @param ctx - Compiler context
|
|
40
|
+
* @returns SQLite SQL string
|
|
41
|
+
*/
|
|
20
42
|
protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
21
43
|
const columns = ast.columns.map(c => {
|
|
22
44
|
let expr = '';
|
package/src/runtime/als.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
// In a real Node environment: import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Browser-compatible implementation of AsyncLocalStorage
|
|
5
|
+
* Provides a simple in-memory store for browser environments
|
|
6
|
+
* @typeParam T - Type of the stored data
|
|
7
|
+
*/
|
|
4
8
|
export class AsyncLocalStorage<T> {
|
|
5
9
|
private store: T | undefined;
|
|
6
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Executes a callback with the specified store value
|
|
13
|
+
* @param store - Value to store during callback execution
|
|
14
|
+
* @param callback - Function to execute with the store value
|
|
15
|
+
* @returns Result of the callback function
|
|
16
|
+
*/
|
|
7
17
|
run<R>(store: T, callback: () => R): R {
|
|
8
18
|
this.store = store;
|
|
9
19
|
try {
|
|
@@ -13,6 +23,10 @@ export class AsyncLocalStorage<T> {
|
|
|
13
23
|
}
|
|
14
24
|
}
|
|
15
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Gets the currently stored value
|
|
28
|
+
* @returns Current store value or undefined if none exists
|
|
29
|
+
*/
|
|
16
30
|
getStore(): T | undefined {
|
|
17
31
|
return this.store;
|
|
18
32
|
}
|
package/src/runtime/hydration.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { HydrationPlan } from '../ast/query';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { HydrationPlan } from '../ast/query';
|
|
2
|
+
import { isRelationAlias, makeRelationAlias } from '../utils/relation-alias';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hydrates query results according to a hydration plan
|
|
6
|
+
* @param rows - Raw database rows
|
|
7
|
+
* @param plan - Hydration plan
|
|
8
|
+
* @returns Hydrated result objects with nested relations
|
|
9
|
+
*/
|
|
5
10
|
export const hydrateRows = (rows: Record<string, any>[], plan?: HydrationPlan): Record<string, any>[] => {
|
|
6
11
|
if (!plan || !rows.length) return rows;
|
|
7
12
|
|
|
@@ -12,28 +17,28 @@ export const hydrateRows = (rows: Record<string, any>[], plan?: HydrationPlan):
|
|
|
12
17
|
if (rootId === undefined) return;
|
|
13
18
|
|
|
14
19
|
if (!rootMap.has(rootId)) {
|
|
15
|
-
const base: Record<string, any> = {};
|
|
16
|
-
const baseKeys = plan.rootColumns.length ? plan.rootColumns : Object.keys(row).filter(k => !
|
|
17
|
-
baseKeys.forEach(key => { base[key] = row[key]; });
|
|
18
|
-
plan.relations.forEach(rel => { base[rel.name] = []; });
|
|
19
|
-
rootMap.set(rootId, base);
|
|
20
|
+
const base: Record<string, any> = {};
|
|
21
|
+
const baseKeys = plan.rootColumns.length ? plan.rootColumns : Object.keys(row).filter(k => !isRelationAlias(k));
|
|
22
|
+
baseKeys.forEach(key => { base[key] = row[key]; });
|
|
23
|
+
plan.relations.forEach(rel => { base[rel.name] = []; });
|
|
24
|
+
rootMap.set(rootId, base);
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
const parent = rootMap.get(rootId);
|
|
23
28
|
|
|
24
29
|
plan.relations.forEach(rel => {
|
|
25
|
-
const childPkKey =
|
|
26
|
-
const childPk = row[childPkKey];
|
|
30
|
+
const childPkKey = makeRelationAlias(rel.aliasPrefix, rel.targetPrimaryKey);
|
|
31
|
+
const childPk = row[childPkKey];
|
|
27
32
|
if (childPk === null || childPk === undefined) return;
|
|
28
33
|
|
|
29
34
|
const bucket = parent[rel.name] as any[];
|
|
30
35
|
if (bucket.some(item => item[rel.targetPrimaryKey] === childPk)) return;
|
|
31
36
|
|
|
32
37
|
const child: Record<string, any> = {};
|
|
33
|
-
rel.columns.forEach(col => {
|
|
34
|
-
const key =
|
|
35
|
-
child[col] = row[key];
|
|
36
|
-
});
|
|
38
|
+
rel.columns.forEach(col => {
|
|
39
|
+
const key = makeRelationAlias(rel.aliasPrefix, col);
|
|
40
|
+
child[col] = row[key];
|
|
41
|
+
});
|
|
37
42
|
|
|
38
43
|
bucket.push(child);
|
|
39
44
|
});
|
package/src/schema/column.ts
CHANGED
|
@@ -1,19 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supported column data types for database schema definitions
|
|
3
|
+
*/
|
|
1
4
|
export type ColumnType = 'INT' | 'VARCHAR' | 'JSON' | 'ENUM' | 'BOOLEAN';
|
|
2
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Definition of a database column
|
|
8
|
+
*/
|
|
3
9
|
export interface ColumnDef {
|
|
4
|
-
|
|
10
|
+
/** Column name (filled at runtime by defineTable) */
|
|
11
|
+
name: string;
|
|
12
|
+
/** Data type of the column */
|
|
5
13
|
type: ColumnType;
|
|
14
|
+
/** Whether this column is a primary key */
|
|
6
15
|
primary?: boolean;
|
|
16
|
+
/** Whether this column cannot be null */
|
|
7
17
|
notNull?: boolean;
|
|
8
|
-
|
|
9
|
-
|
|
18
|
+
/** Additional arguments for the column type (e.g., VARCHAR length) */
|
|
19
|
+
args?: any[];
|
|
20
|
+
/** Table name this column belongs to (filled at runtime by defineTable) */
|
|
21
|
+
table?: string;
|
|
10
22
|
}
|
|
11
23
|
|
|
12
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Factory for creating column definitions with common data types
|
|
26
|
+
*/
|
|
13
27
|
export const col = {
|
|
28
|
+
/**
|
|
29
|
+
* Creates an integer column definition
|
|
30
|
+
* @returns ColumnDef with INT type
|
|
31
|
+
*/
|
|
14
32
|
int: (): ColumnDef => ({ name: '', type: 'INT' }),
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates a variable character column definition
|
|
36
|
+
* @param length - Maximum length of the string
|
|
37
|
+
* @returns ColumnDef with VARCHAR type
|
|
38
|
+
*/
|
|
15
39
|
varchar: (length: number): ColumnDef => ({ name: '', type: 'VARCHAR', args: [length] }),
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Creates a JSON column definition
|
|
43
|
+
* @returns ColumnDef with JSON type
|
|
44
|
+
*/
|
|
16
45
|
json: (): ColumnDef => ({ name: '', type: 'JSON' }),
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates a boolean column definition
|
|
49
|
+
* @returns ColumnDef with BOOLEAN type
|
|
50
|
+
*/
|
|
17
51
|
boolean: (): ColumnDef => ({ name: '', type: 'BOOLEAN' }),
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Marks a column definition as a primary key
|
|
55
|
+
* @param def - Column definition to modify
|
|
56
|
+
* @returns Modified ColumnDef with primary: true
|
|
57
|
+
*/
|
|
18
58
|
primaryKey: (def: ColumnDef): ColumnDef => ({ ...def, primary: true })
|
|
19
|
-
};
|
|
59
|
+
};
|
package/src/schema/relation.ts
CHANGED
|
@@ -1,28 +1,63 @@
|
|
|
1
1
|
import { TableDef } from './table';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Types of relationships supported between tables
|
|
5
|
+
*/
|
|
3
6
|
export const RelationKinds = {
|
|
7
|
+
/** One-to-many relationship */
|
|
4
8
|
HasMany: 'HAS_MANY',
|
|
9
|
+
/** Many-to-one relationship */
|
|
5
10
|
BelongsTo: 'BELONGS_TO',
|
|
6
11
|
} as const;
|
|
7
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Type representing the supported relationship kinds
|
|
15
|
+
*/
|
|
8
16
|
export type RelationType = (typeof RelationKinds)[keyof typeof RelationKinds];
|
|
9
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Base properties common to all relationship types
|
|
20
|
+
*/
|
|
10
21
|
interface BaseRelation {
|
|
22
|
+
/** Target table of the relationship */
|
|
11
23
|
target: TableDef;
|
|
12
|
-
|
|
13
|
-
|
|
24
|
+
/** Foreign key column name on the child table */
|
|
25
|
+
foreignKey: string;
|
|
26
|
+
/** Local key column name (usually 'id') */
|
|
27
|
+
localKey?: string;
|
|
14
28
|
}
|
|
15
29
|
|
|
30
|
+
/**
|
|
31
|
+
* One-to-many relationship definition
|
|
32
|
+
*/
|
|
16
33
|
export interface HasManyRelation extends BaseRelation {
|
|
17
34
|
type: typeof RelationKinds.HasMany;
|
|
18
35
|
}
|
|
19
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Many-to-one relationship definition
|
|
39
|
+
*/
|
|
20
40
|
export interface BelongsToRelation extends BaseRelation {
|
|
21
41
|
type: typeof RelationKinds.BelongsTo;
|
|
22
42
|
}
|
|
23
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Union type representing any supported relationship definition
|
|
46
|
+
*/
|
|
24
47
|
export type RelationDef = HasManyRelation | BelongsToRelation;
|
|
25
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Creates a one-to-many relationship definition
|
|
51
|
+
* @param target - Target table of the relationship
|
|
52
|
+
* @param foreignKey - Foreign key column name on the child table
|
|
53
|
+
* @param localKey - Local key column name (defaults to 'id')
|
|
54
|
+
* @returns HasManyRelation definition
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* hasMany(usersTable, 'user_id')
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
26
61
|
export const hasMany = (target: TableDef, foreignKey: string, localKey: string = 'id'): HasManyRelation => ({
|
|
27
62
|
type: RelationKinds.HasMany,
|
|
28
63
|
target,
|
|
@@ -30,6 +65,18 @@ export const hasMany = (target: TableDef, foreignKey: string, localKey: string =
|
|
|
30
65
|
localKey,
|
|
31
66
|
});
|
|
32
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Creates a many-to-one relationship definition
|
|
70
|
+
* @param target - Target table of the relationship
|
|
71
|
+
* @param foreignKey - Foreign key column name on the child table
|
|
72
|
+
* @param localKey - Local key column name (defaults to 'id')
|
|
73
|
+
* @returns BelongsToRelation definition
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* belongsTo(usersTable, 'user_id')
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
33
80
|
export const belongsTo = (target: TableDef, foreignKey: string, localKey: string = 'id'): BelongsToRelation => ({
|
|
34
81
|
type: RelationKinds.BelongsTo,
|
|
35
82
|
target,
|
package/src/schema/table.ts
CHANGED
|
@@ -1,14 +1,38 @@
|
|
|
1
1
|
import { ColumnDef } from './column';
|
|
2
2
|
import { RelationDef } from './relation';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Definition of a database table with its columns and relationships
|
|
6
|
+
* @typeParam T - Type of the columns record
|
|
7
|
+
*/
|
|
4
8
|
export interface TableDef<T extends Record<string, ColumnDef> = Record<string, ColumnDef>> {
|
|
9
|
+
/** Name of the table */
|
|
5
10
|
name: string;
|
|
11
|
+
/** Record of column definitions keyed by column name */
|
|
6
12
|
columns: T;
|
|
13
|
+
/** Record of relationship definitions keyed by relation name */
|
|
7
14
|
relations: Record<string, RelationDef>;
|
|
8
15
|
}
|
|
9
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Creates a table definition with columns and relationships
|
|
19
|
+
* @typeParam T - Type of the columns record
|
|
20
|
+
* @param name - Name of the table
|
|
21
|
+
* @param columns - Record of column definitions
|
|
22
|
+
* @param relations - Record of relationship definitions (optional)
|
|
23
|
+
* @returns Complete table definition with runtime-filled column metadata
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const usersTable = defineTable('users', {
|
|
28
|
+
* id: col.primaryKey(col.int()),
|
|
29
|
+
* name: col.varchar(255),
|
|
30
|
+
* email: col.varchar(255)
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
10
34
|
export const defineTable = <T extends Record<string, ColumnDef>>(
|
|
11
|
-
name: string,
|
|
35
|
+
name: string,
|
|
12
36
|
columns: T,
|
|
13
37
|
relations: Record<string, RelationDef> = {}
|
|
14
38
|
): TableDef<T> => {
|
|
@@ -17,6 +41,6 @@ export const defineTable = <T extends Record<string, ColumnDef>>(
|
|
|
17
41
|
(acc as any)[key] = { ...def, name: key, table: name };
|
|
18
42
|
return acc;
|
|
19
43
|
}, {} as T);
|
|
20
|
-
|
|
44
|
+
|
|
21
45
|
return { name, columns: colsWithNames, relations };
|
|
22
|
-
};
|
|
46
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { JoinNode } from '../ast/join';
|
|
2
|
+
import { ExpressionNode } from '../ast/expression';
|
|
3
|
+
import { JoinKind } from '../constants/sql';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a JoinNode ready for AST insertion.
|
|
7
|
+
* Centralizing this avoids copy/pasted object literals when multiple services need to synthesize joins.
|
|
8
|
+
*/
|
|
9
|
+
export const createJoinNode = (
|
|
10
|
+
kind: JoinKind,
|
|
11
|
+
tableName: string,
|
|
12
|
+
condition: ExpressionNode,
|
|
13
|
+
relationName?: string
|
|
14
|
+
): JoinNode => ({
|
|
15
|
+
type: 'Join',
|
|
16
|
+
kind,
|
|
17
|
+
table: { type: 'Table', name: tableName },
|
|
18
|
+
condition,
|
|
19
|
+
relationName
|
|
20
|
+
});
|