metal-orm 1.0.16 → 1.0.18
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 +37 -40
- package/dist/decorators/index.cjs +344 -69
- package/dist/decorators/index.cjs.map +1 -1
- package/dist/decorators/index.d.cts +1 -1
- package/dist/decorators/index.d.ts +1 -1
- package/dist/decorators/index.js +344 -69
- package/dist/decorators/index.js.map +1 -1
- package/dist/index.cjs +567 -181
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -30
- package/dist/index.d.ts +66 -30
- package/dist/index.js +559 -181
- package/dist/index.js.map +1 -1
- package/dist/{select-BKZrMRCQ.d.cts → select-BuMpVcVt.d.cts} +265 -74
- package/dist/{select-BKZrMRCQ.d.ts → select-BuMpVcVt.d.ts} +265 -74
- package/package.json +5 -1
- package/src/codegen/naming-strategy.ts +15 -10
- package/src/core/ast/aggregate-functions.ts +50 -4
- package/src/core/ast/builders.ts +23 -3
- package/src/core/ast/expression-builders.ts +36 -16
- package/src/core/ast/expression-nodes.ts +17 -9
- package/src/core/ast/join-node.ts +5 -3
- package/src/core/ast/join.ts +16 -16
- package/src/core/ast/query.ts +44 -29
- package/src/core/ddl/dialects/mssql-schema-dialect.ts +18 -0
- package/src/core/ddl/dialects/mysql-schema-dialect.ts +11 -0
- package/src/core/ddl/dialects/postgres-schema-dialect.ts +9 -0
- package/src/core/ddl/dialects/sqlite-schema-dialect.ts +9 -0
- package/src/core/ddl/introspect/functions/postgres.ts +2 -6
- package/src/core/dialect/abstract.ts +12 -8
- package/src/core/dialect/base/sql-dialect.ts +58 -46
- package/src/core/dialect/mssql/functions.ts +24 -15
- package/src/core/dialect/mssql/index.ts +53 -28
- package/src/core/dialect/postgres/functions.ts +33 -24
- package/src/core/dialect/sqlite/functions.ts +19 -12
- package/src/core/dialect/sqlite/index.ts +22 -13
- package/src/core/functions/datetime.ts +2 -1
- package/src/core/functions/numeric.ts +2 -1
- package/src/core/functions/standard-strategy.ts +52 -12
- package/src/core/functions/text.ts +2 -1
- package/src/core/functions/types.ts +8 -8
- package/src/index.ts +5 -4
- package/src/orm/domain-event-bus.ts +43 -25
- package/src/orm/entity-meta.ts +40 -0
- package/src/orm/execution-context.ts +6 -0
- package/src/orm/hydration-context.ts +6 -4
- package/src/orm/orm-session.ts +35 -24
- package/src/orm/orm.ts +10 -10
- package/src/orm/query-logger.ts +15 -0
- package/src/orm/runtime-types.ts +60 -2
- package/src/orm/transaction-runner.ts +7 -0
- package/src/orm/unit-of-work.ts +1 -0
- package/src/query-builder/column-selector.ts +9 -7
- package/src/query-builder/insert-query-state.ts +13 -3
- package/src/query-builder/query-ast-service.ts +59 -38
- package/src/query-builder/relation-conditions.ts +38 -34
- package/src/query-builder/relation-manager.ts +8 -3
- package/src/query-builder/relation-service.ts +59 -46
- package/src/query-builder/select-helpers.ts +50 -0
- package/src/query-builder/select-query-state.ts +19 -7
- package/src/query-builder/select.ts +339 -167
- package/src/query-builder/update-query-state.ts +31 -9
- package/src/schema/column.ts +75 -39
- package/src/schema/types.ts +17 -6
|
@@ -84,18 +84,27 @@ export class MssqlFunctionStrategy extends StandardFunctionStrategy {
|
|
|
84
84
|
return `DATEPART(dw, ${compiledArgs[0]})`;
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
this.add('WEEK_OF_YEAR', ({ compiledArgs }) => {
|
|
88
|
-
if (compiledArgs.length !== 1) throw new Error('WEEK_OF_YEAR expects 1 argument');
|
|
89
|
-
return `DATEPART(wk, ${compiledArgs[0]})`;
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
this.add('DATE_TRUNC', ({ node, compiledArgs }) => {
|
|
93
|
-
if (compiledArgs.length !== 2) throw new Error('DATE_TRUNC expects 2 arguments (part, date)');
|
|
94
|
-
const [, date] = compiledArgs;
|
|
95
|
-
const partArg = node.args[0] as LiteralNode;
|
|
96
|
-
const partClean = String(partArg.value).replace(/['"]/g, '').toLowerCase();
|
|
97
|
-
// SQL Server 2022+ has DATETRUNC
|
|
98
|
-
return `DATETRUNC(${partClean}, ${date})`;
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
|
|
87
|
+
this.add('WEEK_OF_YEAR', ({ compiledArgs }) => {
|
|
88
|
+
if (compiledArgs.length !== 1) throw new Error('WEEK_OF_YEAR expects 1 argument');
|
|
89
|
+
return `DATEPART(wk, ${compiledArgs[0]})`;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
this.add('DATE_TRUNC', ({ node, compiledArgs }) => {
|
|
93
|
+
if (compiledArgs.length !== 2) throw new Error('DATE_TRUNC expects 2 arguments (part, date)');
|
|
94
|
+
const [, date] = compiledArgs;
|
|
95
|
+
const partArg = node.args[0] as LiteralNode;
|
|
96
|
+
const partClean = String(partArg.value).replace(/['"]/g, '').toLowerCase();
|
|
97
|
+
// SQL Server 2022+ has DATETRUNC
|
|
98
|
+
return `DATETRUNC(${partClean}, ${date})`;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
this.add('GROUP_CONCAT', ctx => {
|
|
102
|
+
const arg = ctx.compiledArgs[0];
|
|
103
|
+
const separatorOperand = this.getGroupConcatSeparatorOperand(ctx);
|
|
104
|
+
const separator = ctx.compileOperand(separatorOperand);
|
|
105
|
+
const orderClause = this.buildOrderByExpression(ctx);
|
|
106
|
+
const withinGroup = orderClause ? ` WITHIN GROUP (${orderClause})` : '';
|
|
107
|
+
return `STRING_AGG(${arg}, ${separator})${withinGroup}`;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { CompilerContext, Dialect } from '../abstract.js';
|
|
2
|
-
import { SelectQueryNode, InsertQueryNode, UpdateQueryNode, DeleteQueryNode } from '../../ast/query.js';
|
|
3
|
-
import { JsonPathNode } from '../../ast/expression.js';
|
|
4
|
-
import { MssqlFunctionStrategy } from './functions.js';
|
|
2
|
+
import { SelectQueryNode, InsertQueryNode, UpdateQueryNode, DeleteQueryNode, TableSourceNode, DerivedTableNode } from '../../ast/query.js';
|
|
3
|
+
import { JsonPathNode } from '../../ast/expression.js';
|
|
4
|
+
import { MssqlFunctionStrategy } from './functions.js';
|
|
5
|
+
import { FunctionTableFormatter } from '../base/function-table-formatter.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Microsoft SQL Server dialect implementation
|
|
@@ -94,13 +95,16 @@ export class SqlServerDialect extends Dialect {
|
|
|
94
95
|
return `UPDATE ${table} SET ${assignments}${whereClause};`;
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
protected compileDeleteAst(ast: DeleteQueryNode, ctx: CompilerContext): string {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
98
|
+
protected compileDeleteAst(ast: DeleteQueryNode, ctx: CompilerContext): string {
|
|
99
|
+
if (ast.from.type !== 'Table') {
|
|
100
|
+
throw new Error('DELETE only supports base tables in the MSSQL dialect.');
|
|
101
|
+
}
|
|
102
|
+
const table = this.quoteIdentifier(ast.from.name);
|
|
103
|
+
const whereClause = this.compileWhere(ast.where, ctx);
|
|
104
|
+
return `DELETE FROM ${table}${whereClause};`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private compileSelectCore(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
104
108
|
const columns = ast.columns.map(c => {
|
|
105
109
|
let expr = '';
|
|
106
110
|
if (c.type === 'Function') {
|
|
@@ -119,15 +123,15 @@ export class SqlServerDialect extends Dialect {
|
|
|
119
123
|
}
|
|
120
124
|
return expr;
|
|
121
125
|
}).join(', ');
|
|
122
|
-
|
|
123
|
-
const distinct = ast.distinct ? 'DISTINCT ' : '';
|
|
124
|
-
const from =
|
|
125
|
-
|
|
126
|
-
const joins = ast.joins.map(j => {
|
|
127
|
-
const table = this.
|
|
128
|
-
const cond = this.compileExpression(j.condition, ctx);
|
|
129
|
-
return `${j.kind} JOIN ${table} ON ${cond}`;
|
|
130
|
-
}).join(' ');
|
|
126
|
+
|
|
127
|
+
const distinct = ast.distinct ? 'DISTINCT ' : '';
|
|
128
|
+
const from = this.compileTableSource(ast.from, ctx);
|
|
129
|
+
|
|
130
|
+
const joins = ast.joins.map(j => {
|
|
131
|
+
const table = this.compileTableSource(j.table, ctx);
|
|
132
|
+
const cond = this.compileExpression(j.condition, ctx);
|
|
133
|
+
return `${j.kind} JOIN ${table} ON ${cond}`;
|
|
134
|
+
}).join(' ');
|
|
131
135
|
const whereClause = this.compileWhere(ast.where, ctx);
|
|
132
136
|
|
|
133
137
|
const groupBy = ast.groupBy && ast.groupBy.length > 0
|
|
@@ -155,19 +159,40 @@ export class SqlServerDialect extends Dialect {
|
|
|
155
159
|
.join(', ');
|
|
156
160
|
}
|
|
157
161
|
|
|
158
|
-
private compilePagination(ast: SelectQueryNode, orderBy: string): string {
|
|
159
|
-
const hasLimit = ast.limit !== undefined;
|
|
160
|
-
const hasOffset = ast.offset !== undefined;
|
|
161
|
-
if (!hasLimit && !hasOffset) return '';
|
|
162
|
+
private compilePagination(ast: SelectQueryNode, orderBy: string): string {
|
|
163
|
+
const hasLimit = ast.limit !== undefined;
|
|
164
|
+
const hasOffset = ast.offset !== undefined;
|
|
165
|
+
if (!hasLimit && !hasOffset) return '';
|
|
162
166
|
|
|
163
167
|
const off = ast.offset ?? 0;
|
|
164
168
|
const orderClause = orderBy || ' ORDER BY (SELECT NULL)';
|
|
165
169
|
let pagination = `${orderClause} OFFSET ${off} ROWS`;
|
|
166
|
-
if (hasLimit) {
|
|
167
|
-
pagination += ` FETCH NEXT ${ast.limit} ROWS ONLY`;
|
|
168
|
-
}
|
|
169
|
-
return pagination;
|
|
170
|
-
}
|
|
170
|
+
if (hasLimit) {
|
|
171
|
+
pagination += ` FETCH NEXT ${ast.limit} ROWS ONLY`;
|
|
172
|
+
}
|
|
173
|
+
return pagination;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private compileTableSource(table: TableSourceNode, ctx: CompilerContext): string {
|
|
177
|
+
if (table.type === 'FunctionTable') {
|
|
178
|
+
return FunctionTableFormatter.format(table, ctx, this as any);
|
|
179
|
+
}
|
|
180
|
+
if (table.type === 'DerivedTable') {
|
|
181
|
+
return this.compileDerivedTable(table, ctx);
|
|
182
|
+
}
|
|
183
|
+
const base = table.schema
|
|
184
|
+
? `${this.quoteIdentifier(table.schema)}.${this.quoteIdentifier(table.name)}`
|
|
185
|
+
: this.quoteIdentifier(table.name);
|
|
186
|
+
return table.alias ? `${base} AS ${this.quoteIdentifier(table.alias)}` : base;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private compileDerivedTable(table: DerivedTableNode, ctx: CompilerContext): string {
|
|
190
|
+
const sub = this.compileSelectAst(this.normalizeSelectAst(table.query), ctx).trim().replace(/;$/, '');
|
|
191
|
+
const cols = table.columnAliases?.length
|
|
192
|
+
? ` (${table.columnAliases.map(c => this.quoteIdentifier(c)).join(', ')})`
|
|
193
|
+
: '';
|
|
194
|
+
return `(${sub}) AS ${this.quoteIdentifier(table.alias)}${cols}`;
|
|
195
|
+
}
|
|
171
196
|
|
|
172
197
|
private compileCtes(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
173
198
|
if (!ast.ctes || ast.ctes.length === 0) return '';
|
|
@@ -69,27 +69,36 @@ export class PostgresFunctionStrategy extends StandardFunctionStrategy {
|
|
|
69
69
|
return `TO_CHAR(${date}, ${format})`;
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
this.add('END_OF_MONTH', ({ compiledArgs }) => {
|
|
73
|
-
if (compiledArgs.length !== 1) throw new Error('END_OF_MONTH expects 1 argument');
|
|
74
|
-
return `(date_trunc('month', ${compiledArgs[0]}) + interval '1 month' - interval '1 day')::DATE`;
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
this.add('DAY_OF_WEEK', ({ compiledArgs }) => {
|
|
78
|
-
if (compiledArgs.length !== 1) throw new Error('DAY_OF_WEEK expects 1 argument');
|
|
79
|
-
return `EXTRACT(DOW FROM ${compiledArgs[0]})`;
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
this.add('WEEK_OF_YEAR', ({ compiledArgs }) => {
|
|
83
|
-
if (compiledArgs.length !== 1) throw new Error('WEEK_OF_YEAR expects 1 argument');
|
|
84
|
-
return `EXTRACT(WEEK FROM ${compiledArgs[0]})`;
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
this.add('DATE_TRUNC', ({ node, compiledArgs }) => {
|
|
88
|
-
if (compiledArgs.length !== 2) throw new Error('DATE_TRUNC expects 2 arguments (part, date)');
|
|
89
|
-
const [, date] = compiledArgs;
|
|
90
|
-
const partArg = node.args[0] as LiteralNode;
|
|
91
|
-
const partClean = String(partArg.value).replace(/['"]/g, '').toLowerCase();
|
|
92
|
-
return `DATE_TRUNC('${partClean}', ${date})`;
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
|
|
72
|
+
this.add('END_OF_MONTH', ({ compiledArgs }) => {
|
|
73
|
+
if (compiledArgs.length !== 1) throw new Error('END_OF_MONTH expects 1 argument');
|
|
74
|
+
return `(date_trunc('month', ${compiledArgs[0]}) + interval '1 month' - interval '1 day')::DATE`;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
this.add('DAY_OF_WEEK', ({ compiledArgs }) => {
|
|
78
|
+
if (compiledArgs.length !== 1) throw new Error('DAY_OF_WEEK expects 1 argument');
|
|
79
|
+
return `EXTRACT(DOW FROM ${compiledArgs[0]})`;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
this.add('WEEK_OF_YEAR', ({ compiledArgs }) => {
|
|
83
|
+
if (compiledArgs.length !== 1) throw new Error('WEEK_OF_YEAR expects 1 argument');
|
|
84
|
+
return `EXTRACT(WEEK FROM ${compiledArgs[0]})`;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
this.add('DATE_TRUNC', ({ node, compiledArgs }) => {
|
|
88
|
+
if (compiledArgs.length !== 2) throw new Error('DATE_TRUNC expects 2 arguments (part, date)');
|
|
89
|
+
const [, date] = compiledArgs;
|
|
90
|
+
const partArg = node.args[0] as LiteralNode;
|
|
91
|
+
const partClean = String(partArg.value).replace(/['"]/g, '').toLowerCase();
|
|
92
|
+
return `DATE_TRUNC('${partClean}', ${date})`;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
this.add('GROUP_CONCAT', ctx => {
|
|
96
|
+
const arg = ctx.compiledArgs[0];
|
|
97
|
+
const orderClause = this.buildOrderByExpression(ctx);
|
|
98
|
+
const orderSegment = orderClause ? ` ${orderClause}` : '';
|
|
99
|
+
const separatorOperand = this.getGroupConcatSeparatorOperand(ctx);
|
|
100
|
+
const separator = ctx.compileOperand(separatorOperand);
|
|
101
|
+
return `STRING_AGG(${arg}, ${separator}${orderSegment})`;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { StandardFunctionStrategy } from '../../functions/standard-strategy.js';
|
|
2
|
-
import { FunctionRenderContext } from '../../functions/types.js';
|
|
3
|
-
import { LiteralNode } from '../../ast/expression.js';
|
|
4
|
-
|
|
5
|
-
export class SqliteFunctionStrategy extends StandardFunctionStrategy {
|
|
6
|
-
constructor() {
|
|
7
|
-
super();
|
|
8
|
-
this.registerOverrides();
|
|
9
|
-
}
|
|
1
|
+
import { StandardFunctionStrategy } from '../../functions/standard-strategy.js';
|
|
2
|
+
import { FunctionRenderContext } from '../../functions/types.js';
|
|
3
|
+
import { LiteralNode } from '../../ast/expression.js';
|
|
4
|
+
|
|
5
|
+
export class SqliteFunctionStrategy extends StandardFunctionStrategy {
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.registerOverrides();
|
|
9
|
+
}
|
|
10
10
|
|
|
11
11
|
private registerOverrides() {
|
|
12
12
|
// Override Standard/Abstract definitions with SQLite specifics
|
|
@@ -110,6 +110,13 @@ export class SqliteFunctionStrategy extends StandardFunctionStrategy {
|
|
|
110
110
|
return `date(${date})`;
|
|
111
111
|
}
|
|
112
112
|
return `date(${date}, 'start of ${partClean}')`;
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
this.add('GROUP_CONCAT', ctx => {
|
|
116
|
+
const arg = ctx.compiledArgs[0];
|
|
117
|
+
const separatorOperand = this.getGroupConcatSeparatorOperand(ctx);
|
|
118
|
+
const separator = ctx.compileOperand(separatorOperand);
|
|
119
|
+
return `GROUP_CONCAT(${arg}, ${separator})`;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -6,14 +6,14 @@ import { SqliteFunctionStrategy } from './functions.js';
|
|
|
6
6
|
/**
|
|
7
7
|
* SQLite dialect implementation
|
|
8
8
|
*/
|
|
9
|
-
export class SqliteDialect extends SqlDialectBase {
|
|
10
|
-
protected readonly dialect = 'sqlite';
|
|
11
|
-
/**
|
|
12
|
-
* Creates a new SqliteDialect instance
|
|
13
|
-
*/
|
|
14
|
-
public constructor() {
|
|
15
|
-
super(new SqliteFunctionStrategy());
|
|
16
|
-
}
|
|
9
|
+
export class SqliteDialect extends SqlDialectBase {
|
|
10
|
+
protected readonly dialect = 'sqlite';
|
|
11
|
+
/**
|
|
12
|
+
* Creates a new SqliteDialect instance
|
|
13
|
+
*/
|
|
14
|
+
public constructor() {
|
|
15
|
+
super(new SqliteFunctionStrategy());
|
|
16
|
+
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Quotes an identifier using SQLite double-quote syntax
|
|
@@ -35,11 +35,20 @@ export class SqliteDialect extends SqlDialectBase {
|
|
|
35
35
|
return `json_extract(${col}, '${node.path}')`;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
protected compileReturning(returning: ColumnNode[] | undefined, ctx: CompilerContext): string {
|
|
39
|
-
if (!returning || returning.length === 0) return '';
|
|
40
|
-
const columns = this.formatReturningColumns(returning);
|
|
41
|
-
return ` RETURNING ${columns}`;
|
|
42
|
-
}
|
|
38
|
+
protected compileReturning(returning: ColumnNode[] | undefined, ctx: CompilerContext): string {
|
|
39
|
+
if (!returning || returning.length === 0) return '';
|
|
40
|
+
const columns = this.formatReturningColumns(returning);
|
|
41
|
+
return ` RETURNING ${columns}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
protected formatReturningColumns(returning: ColumnNode[]): string {
|
|
45
|
+
return returning
|
|
46
|
+
.map(column => {
|
|
47
|
+
const alias = column.alias ? ` AS ${this.quoteIdentifier(column.alias)}` : '';
|
|
48
|
+
return `${this.quoteIdentifier(column.name)}${alias}`;
|
|
49
|
+
})
|
|
50
|
+
.join(', ');
|
|
51
|
+
}
|
|
43
52
|
|
|
44
53
|
supportsReturning(): boolean {
|
|
45
54
|
return true;
|
|
@@ -11,7 +11,8 @@ const isColumnDef = (val: any): val is ColumnDef => !!val && typeof val === 'obj
|
|
|
11
11
|
const toOperand = (input: OperandInput): OperandNode => {
|
|
12
12
|
if (isOperandNode(input)) return input;
|
|
13
13
|
if (isColumnDef(input)) return columnOperand(input);
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
return valueToOperand(input);
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
const fn = (key: string, args: OperandInput[]): FunctionNode => ({
|
|
@@ -11,7 +11,8 @@ const isColumnDef = (val: any): val is ColumnDef => !!val && typeof val === 'obj
|
|
|
11
11
|
const toOperand = (input: OperandInput): OperandNode => {
|
|
12
12
|
if (isOperandNode(input)) return input;
|
|
13
13
|
if (isColumnDef(input)) return columnOperand(input);
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
return valueToOperand(input);
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
const fn = (key: string, args: OperandInput[]): FunctionNode => ({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { FunctionStrategy, FunctionRenderer, FunctionRenderContext } from './types.js';
|
|
1
|
+
import { FunctionStrategy, FunctionRenderer, FunctionRenderContext } from './types.js';
|
|
2
|
+
import { LiteralNode, OperandNode } from '../ast/expression.js';
|
|
2
3
|
|
|
3
4
|
export class StandardFunctionStrategy implements FunctionStrategy {
|
|
4
5
|
protected renderers: Map<string, FunctionRenderer> = new Map();
|
|
@@ -7,11 +8,16 @@ export class StandardFunctionStrategy implements FunctionStrategy {
|
|
|
7
8
|
this.registerStandard();
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
protected registerStandard() {
|
|
11
|
-
// Register ANSI standard implementations
|
|
12
|
-
this.add('
|
|
13
|
-
this.add('
|
|
14
|
-
this.add('
|
|
11
|
+
protected registerStandard() {
|
|
12
|
+
// Register ANSI standard implementations
|
|
13
|
+
this.add('COUNT', ({ compiledArgs }) => `COUNT(${compiledArgs.join(', ')})`);
|
|
14
|
+
this.add('SUM', ({ compiledArgs }) => `SUM(${compiledArgs[0]})`);
|
|
15
|
+
this.add('AVG', ({ compiledArgs }) => `AVG(${compiledArgs[0]})`);
|
|
16
|
+
this.add('MIN', ({ compiledArgs }) => `MIN(${compiledArgs[0]})`);
|
|
17
|
+
this.add('MAX', ({ compiledArgs }) => `MAX(${compiledArgs[0]})`);
|
|
18
|
+
this.add('ABS', ({ compiledArgs }) => `ABS(${compiledArgs[0]})`);
|
|
19
|
+
this.add('UPPER', ({ compiledArgs }) => `UPPER(${compiledArgs[0]})`);
|
|
20
|
+
this.add('LOWER', ({ compiledArgs }) => `LOWER(${compiledArgs[0]})`);
|
|
15
21
|
this.add('LENGTH', ({ compiledArgs }) => `LENGTH(${compiledArgs[0]})`);
|
|
16
22
|
this.add('TRIM', ({ compiledArgs }) => `TRIM(${compiledArgs[0]})`);
|
|
17
23
|
this.add('LTRIM', ({ compiledArgs }) => `LTRIM(${compiledArgs[0]})`);
|
|
@@ -33,15 +39,49 @@ export class StandardFunctionStrategy implements FunctionStrategy {
|
|
|
33
39
|
this.add('FROM_UNIXTIME', ({ compiledArgs }) => `FROM_UNIXTIME(${compiledArgs[0]})`);
|
|
34
40
|
this.add('END_OF_MONTH', ({ compiledArgs }) => `LAST_DAY(${compiledArgs[0]})`);
|
|
35
41
|
this.add('DAY_OF_WEEK', ({ compiledArgs }) => `DAYOFWEEK(${compiledArgs[0]})`);
|
|
36
|
-
this.add('WEEK_OF_YEAR', ({ compiledArgs }) => `WEEKOFYEAR(${compiledArgs[0]})`);
|
|
37
|
-
this.add('DATE_TRUNC', ({ compiledArgs }) => `DATE_TRUNC(${compiledArgs[0]}, ${compiledArgs[1]})`);
|
|
42
|
+
this.add('WEEK_OF_YEAR', ({ compiledArgs }) => `WEEKOFYEAR(${compiledArgs[0]})`);
|
|
43
|
+
this.add('DATE_TRUNC', ({ compiledArgs }) => `DATE_TRUNC(${compiledArgs[0]}, ${compiledArgs[1]})`);
|
|
44
|
+
this.add('GROUP_CONCAT', ctx => this.renderGroupConcat(ctx));
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
protected add(name: string, renderer: FunctionRenderer) {
|
|
41
48
|
this.renderers.set(name, renderer);
|
|
42
49
|
}
|
|
43
50
|
|
|
44
|
-
getRenderer(name: string): FunctionRenderer | undefined {
|
|
45
|
-
return this.renderers.get(name);
|
|
46
|
-
}
|
|
47
|
-
|
|
51
|
+
getRenderer(name: string): FunctionRenderer | undefined {
|
|
52
|
+
return this.renderers.get(name);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private renderGroupConcat(ctx: FunctionRenderContext): string {
|
|
56
|
+
const arg = ctx.compiledArgs[0];
|
|
57
|
+
const orderClause = this.buildOrderByExpression(ctx);
|
|
58
|
+
const orderSegment = orderClause ? ` ${orderClause}` : '';
|
|
59
|
+
const separatorClause = this.formatGroupConcatSeparator(ctx);
|
|
60
|
+
return `GROUP_CONCAT(${arg}${orderSegment}${separatorClause})`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
protected buildOrderByExpression(ctx: FunctionRenderContext): string {
|
|
64
|
+
const orderBy = ctx.node.orderBy;
|
|
65
|
+
if (!orderBy || orderBy.length === 0) {
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
const parts = orderBy.map(order => `${ctx.compileOperand(order.column)} ${order.direction}`);
|
|
69
|
+
return `ORDER BY ${parts.join(', ')}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
protected formatGroupConcatSeparator(ctx: FunctionRenderContext): string {
|
|
73
|
+
if (!ctx.node.separator) {
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
76
|
+
return ` SEPARATOR ${ctx.compileOperand(ctx.node.separator)}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
protected getGroupConcatSeparatorOperand(ctx: FunctionRenderContext): OperandNode {
|
|
80
|
+
return ctx.node.separator ?? StandardFunctionStrategy.DEFAULT_GROUP_CONCAT_SEPARATOR;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
protected static readonly DEFAULT_GROUP_CONCAT_SEPARATOR: LiteralNode = {
|
|
84
|
+
type: 'Literal',
|
|
85
|
+
value: ','
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -11,7 +11,8 @@ const isColumnDef = (val: any): val is ColumnDef => !!val && typeof val === 'obj
|
|
|
11
11
|
const toOperand = (input: OperandInput): OperandNode => {
|
|
12
12
|
if (isOperandNode(input)) return input;
|
|
13
13
|
if (isColumnDef(input)) return columnOperand(input);
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
return valueToOperand(input);
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
const fn = (key: string, args: OperandInput[]): FunctionNode => ({
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { FunctionNode } from '../ast/expression.js';
|
|
2
|
-
|
|
3
|
-
export interface FunctionRenderContext {
|
|
4
|
-
node: FunctionNode;
|
|
5
|
-
compiledArgs: string[];
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
1
|
+
import { FunctionNode, OperandNode } from '../ast/expression.js';
|
|
2
|
+
|
|
3
|
+
export interface FunctionRenderContext {
|
|
4
|
+
node: FunctionNode;
|
|
5
|
+
compiledArgs: string[];
|
|
6
|
+
/** Helper to compile additional operands (e.g., separators or ORDER BY columns) */
|
|
7
|
+
compileOperand: (operand: OperandNode) => string;
|
|
8
|
+
}
|
|
9
9
|
|
|
10
10
|
export type FunctionRenderer = (ctx: FunctionRenderContext) => string;
|
|
11
11
|
|
package/src/index.ts
CHANGED
|
@@ -2,10 +2,11 @@ export * from './schema/table.js';
|
|
|
2
2
|
export * from './schema/column.js';
|
|
3
3
|
export * from './schema/relation.js';
|
|
4
4
|
export * from './schema/types.js';
|
|
5
|
-
export * from './query-builder/select.js';
|
|
6
|
-
export * from './query-builder/
|
|
7
|
-
export * from './query-builder/
|
|
8
|
-
export * from './query-builder/
|
|
5
|
+
export * from './query-builder/select.js';
|
|
6
|
+
export * from './query-builder/select-helpers.js';
|
|
7
|
+
export * from './query-builder/insert.js';
|
|
8
|
+
export * from './query-builder/update.js';
|
|
9
|
+
export * from './query-builder/delete.js';
|
|
9
10
|
export * from './core/ast/expression.js';
|
|
10
11
|
export * from './core/hydration/types.js';
|
|
11
12
|
export * from './core/dialect/mysql/index.js';
|
|
@@ -1,32 +1,53 @@
|
|
|
1
|
-
import type { HasDomainEvents, TrackedEntity } from './runtime-types.js';
|
|
1
|
+
import type { DomainEvent, HasDomainEvents, TrackedEntity } from './runtime-types.js';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
type EventOfType<E extends DomainEvent, TType extends E['type']> =
|
|
4
|
+
Extract<E, { type: TType }>;
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
-
|
|
6
|
+
export type DomainEventHandler<E extends DomainEvent, Context> =
|
|
7
|
+
(event: E, ctx: Context) => Promise<void> | void;
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
export type InitialHandlers<E extends DomainEvent, Context> = {
|
|
10
|
+
[K in E['type']]?: DomainEventHandler<EventOfType<E, K>, Context>[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export class DomainEventBus<E extends DomainEvent, Context> {
|
|
14
|
+
private readonly handlers = new Map<E['type'], DomainEventHandler<E, Context>[]>();
|
|
15
|
+
|
|
16
|
+
constructor(initialHandlers?: InitialHandlers<E, Context>) {
|
|
17
|
+
if (initialHandlers) {
|
|
18
|
+
for (const key in initialHandlers) {
|
|
19
|
+
const type = key as E['type'];
|
|
20
|
+
const list = initialHandlers[type] ?? [];
|
|
21
|
+
this.handlers.set(type, [...(list as DomainEventHandler<E, Context>[])]);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
13
24
|
}
|
|
14
25
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
on<TType extends E['type']>(
|
|
27
|
+
type: TType,
|
|
28
|
+
handler: DomainEventHandler<EventOfType<E, TType>, Context>
|
|
29
|
+
): void {
|
|
30
|
+
const key = type as E['type'];
|
|
31
|
+
const existing = this.handlers.get(key) ?? [];
|
|
32
|
+
existing.push(handler as unknown as DomainEventHandler<E, Context>);
|
|
33
|
+
this.handlers.set(key, existing);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
register<TType extends E['type']>(
|
|
37
|
+
type: TType,
|
|
38
|
+
handler: DomainEventHandler<EventOfType<E, TType>, Context>
|
|
39
|
+
): void {
|
|
40
|
+
this.on(type, handler);
|
|
19
41
|
}
|
|
20
42
|
|
|
21
43
|
async dispatch(trackedEntities: Iterable<TrackedEntity>, ctx: Context): Promise<void> {
|
|
22
44
|
for (const tracked of trackedEntities) {
|
|
23
|
-
const entity = tracked.entity as HasDomainEvents
|
|
24
|
-
if (!entity.domainEvents
|
|
45
|
+
const entity = tracked.entity as HasDomainEvents<E>;
|
|
46
|
+
if (!entity.domainEvents?.length) continue;
|
|
25
47
|
|
|
26
48
|
for (const event of entity.domainEvents) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
if (!handlers) continue;
|
|
49
|
+
const handlers = this.handlers.get(event.type as E['type']);
|
|
50
|
+
if (!handlers?.length) continue;
|
|
30
51
|
|
|
31
52
|
for (const handler of handlers) {
|
|
32
53
|
await handler(event, ctx);
|
|
@@ -36,15 +57,12 @@ export class DomainEventBus<Context> {
|
|
|
36
57
|
entity.domainEvents = [];
|
|
37
58
|
}
|
|
38
59
|
}
|
|
39
|
-
|
|
40
|
-
private getEventName(event: any): string {
|
|
41
|
-
if (!event) return 'Unknown';
|
|
42
|
-
if (typeof event === 'string') return event;
|
|
43
|
-
return event.constructor?.name ?? 'Unknown';
|
|
44
|
-
}
|
|
45
60
|
}
|
|
46
61
|
|
|
47
|
-
export const addDomainEvent =
|
|
62
|
+
export const addDomainEvent = <E extends DomainEvent>(
|
|
63
|
+
entity: HasDomainEvents<E>,
|
|
64
|
+
event: E
|
|
65
|
+
): void => {
|
|
48
66
|
if (!entity.domainEvents) {
|
|
49
67
|
entity.domainEvents = [];
|
|
50
68
|
}
|
package/src/orm/entity-meta.ts
CHANGED
|
@@ -2,19 +2,40 @@ import { TableDef } from '../schema/table.js';
|
|
|
2
2
|
import { EntityContext } from './entity-context.js';
|
|
3
3
|
import { RelationMap } from '../schema/types.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Symbol used to store entity metadata on entity instances
|
|
7
|
+
*/
|
|
5
8
|
export const ENTITY_META = Symbol('EntityMeta');
|
|
6
9
|
|
|
7
10
|
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
8
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Metadata stored on entity instances for ORM internal use
|
|
14
|
+
* @typeParam TTable - Table definition type
|
|
15
|
+
*/
|
|
9
16
|
export interface EntityMeta<TTable extends TableDef> {
|
|
17
|
+
/** Entity context */
|
|
10
18
|
ctx: EntityContext;
|
|
19
|
+
/** Table definition */
|
|
11
20
|
table: TTable;
|
|
21
|
+
/** Relations that should be loaded lazily */
|
|
12
22
|
lazyRelations: (keyof RelationMap<TTable>)[];
|
|
23
|
+
/** Cache for relation promises */
|
|
13
24
|
relationCache: Map<string, Promise<any>>;
|
|
25
|
+
/** Hydration data for relations */
|
|
14
26
|
relationHydration: Map<string, Map<string, any>>;
|
|
27
|
+
/** Relation wrapper instances */
|
|
15
28
|
relationWrappers: Map<string, unknown>;
|
|
16
29
|
}
|
|
17
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Gets hydration rows for a specific relation and key
|
|
33
|
+
* @param meta - Entity metadata
|
|
34
|
+
* @param relationName - Name of the relation
|
|
35
|
+
* @param key - Key to look up in the hydration map
|
|
36
|
+
* @returns Array of hydration rows or undefined if not found
|
|
37
|
+
* @typeParam TTable - Table definition type
|
|
38
|
+
*/
|
|
18
39
|
export const getHydrationRows = <TTable extends TableDef>(
|
|
19
40
|
meta: EntityMeta<TTable>,
|
|
20
41
|
relationName: string,
|
|
@@ -27,6 +48,14 @@ export const getHydrationRows = <TTable extends TableDef>(
|
|
|
27
48
|
return Array.isArray(rows) ? rows : undefined;
|
|
28
49
|
};
|
|
29
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Gets a single hydration record for a specific relation and key
|
|
53
|
+
* @param meta - Entity metadata
|
|
54
|
+
* @param relationName - Name of the relation
|
|
55
|
+
* @param key - Key to look up in the hydration map
|
|
56
|
+
* @returns Single hydration record or undefined if not found
|
|
57
|
+
* @typeParam TTable - Table definition type
|
|
58
|
+
*/
|
|
30
59
|
export const getHydrationRecord = <TTable extends TableDef>(
|
|
31
60
|
meta: EntityMeta<TTable>,
|
|
32
61
|
relationName: string,
|
|
@@ -42,11 +71,22 @@ export const getHydrationRecord = <TTable extends TableDef>(
|
|
|
42
71
|
return value;
|
|
43
72
|
};
|
|
44
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Gets entity metadata from an entity instance
|
|
76
|
+
* @param entity - Entity instance to get metadata from
|
|
77
|
+
* @returns Entity metadata or undefined if not found
|
|
78
|
+
* @typeParam TTable - Table definition type
|
|
79
|
+
*/
|
|
45
80
|
export const getEntityMeta = <TTable extends TableDef>(entity: any): EntityMeta<TTable> | undefined => {
|
|
46
81
|
if (!entity || typeof entity !== 'object') return undefined;
|
|
47
82
|
return (entity as any)[ENTITY_META];
|
|
48
83
|
};
|
|
49
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Checks if an entity has metadata attached
|
|
87
|
+
* @param entity - Entity instance to check
|
|
88
|
+
* @returns True if the entity has metadata, false otherwise
|
|
89
|
+
*/
|
|
50
90
|
export const hasEntityMeta = (entity: any): entity is { [ENTITY_META]: EntityMeta<TableDef> } => {
|
|
51
91
|
return Boolean(getEntityMeta(entity));
|
|
52
92
|
};
|
|
@@ -2,9 +2,15 @@ import type { Dialect } from '../core/dialect/abstract.js';
|
|
|
2
2
|
import type { DbExecutor } from '../core/execution/db-executor.js';
|
|
3
3
|
import { InterceptorPipeline } from './interceptor-pipeline.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Context for SQL query execution
|
|
7
|
+
*/
|
|
5
8
|
export interface ExecutionContext {
|
|
9
|
+
/** Database dialect to use for SQL generation */
|
|
6
10
|
dialect: Dialect;
|
|
11
|
+
/** Database executor for running SQL queries */
|
|
7
12
|
executor: DbExecutor;
|
|
13
|
+
/** Interceptor pipeline for query processing */
|
|
8
14
|
interceptors: InterceptorPipeline;
|
|
9
15
|
// plus anything *purely about executing SQL*:
|
|
10
16
|
// - logging
|