metal-orm 1.0.15 → 1.0.16
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 +34 -27
- package/dist/decorators/index.cjs +339 -153
- package/dist/decorators/index.cjs.map +1 -1
- package/dist/decorators/index.d.cts +1 -5
- package/dist/decorators/index.d.ts +1 -5
- package/dist/decorators/index.js +339 -153
- package/dist/decorators/index.js.map +1 -1
- package/dist/index.cjs +838 -484
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -14
- package/dist/index.d.ts +17 -14
- package/dist/index.js +833 -483
- package/dist/index.js.map +1 -1
- package/dist/{select-Bkv8g8u_.d.cts → select-BKZrMRCQ.d.cts} +363 -28
- package/dist/{select-Bkv8g8u_.d.ts → select-BKZrMRCQ.d.ts} +363 -28
- package/package.json +1 -1
- package/src/codegen/naming-strategy.ts +64 -0
- package/src/codegen/typescript.ts +48 -53
- package/src/core/ddl/schema-generator.ts +3 -2
- package/src/core/ddl/schema-introspect.ts +1 -1
- package/src/core/dialect/abstract.ts +28 -0
- package/src/decorators/column.ts +13 -4
- package/src/index.ts +8 -1
- package/src/orm/entity-context.ts +30 -0
- package/src/orm/entity-meta.ts +2 -2
- package/src/orm/entity-metadata.ts +1 -6
- package/src/orm/entity.ts +88 -88
- package/src/orm/execute.ts +42 -25
- package/src/orm/execution-context.ts +12 -0
- package/src/orm/hydration-context.ts +14 -0
- package/src/orm/identity-map.ts +4 -0
- package/src/orm/interceptor-pipeline.ts +29 -0
- package/src/orm/lazy-batch.ts +6 -6
- package/src/orm/orm-session.ts +234 -0
- package/src/orm/orm.ts +58 -0
- package/src/orm/relation-change-processor.ts +5 -1
- package/src/orm/relations/belongs-to.ts +45 -44
- package/src/orm/relations/has-many.ts +44 -43
- package/src/orm/relations/has-one.ts +140 -139
- package/src/orm/relations/many-to-many.ts +46 -45
- package/src/orm/unit-of-work.ts +6 -1
- package/src/query-builder/select.ts +509 -3
- package/src/orm/orm-context.ts +0 -159
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { TableNode, FunctionTableNode } from '../core/ast/query.js';
|
|
2
|
+
import type { ColumnNode } from '../core/ast/expression.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Strategy interface for converting database names to TypeScript identifiers
|
|
6
|
+
*/
|
|
7
|
+
export interface NamingStrategy {
|
|
8
|
+
/**
|
|
9
|
+
* Converts a table name to a TypeScript symbol name
|
|
10
|
+
* @param table - Table node, function table node, or name
|
|
11
|
+
* @returns Valid TypeScript identifier
|
|
12
|
+
*/
|
|
13
|
+
tableToSymbol(table: TableNode | FunctionTableNode | string): string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Converts a column reference to a property name
|
|
17
|
+
* @param column - Column node
|
|
18
|
+
* @returns Valid TypeScript property name
|
|
19
|
+
*/
|
|
20
|
+
columnToProperty(column: ColumnNode): string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Default naming strategy that maintains backward compatibility
|
|
25
|
+
* with the original capitalize() behavior
|
|
26
|
+
*/
|
|
27
|
+
export class DefaultNamingStrategy implements NamingStrategy {
|
|
28
|
+
/**
|
|
29
|
+
* Converts table names to TypeScript symbols
|
|
30
|
+
* @param table - Table node, function table node, or string name
|
|
31
|
+
* @returns Capitalized table name (handles schema-qualified names)
|
|
32
|
+
*/
|
|
33
|
+
tableToSymbol(table: TableNode | FunctionTableNode | string): string {
|
|
34
|
+
const tableName = typeof table === 'string' ? table : table.name;
|
|
35
|
+
|
|
36
|
+
// Handle schema-qualified names (e.g., "auth.user" → "AuthUser")
|
|
37
|
+
if (tableName.includes('.')) {
|
|
38
|
+
return tableName.split('.')
|
|
39
|
+
.map(part => this.capitalize(part))
|
|
40
|
+
.join('');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return this.capitalize(tableName);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Converts column references to property names
|
|
48
|
+
* @param column - Column node
|
|
49
|
+
* @returns Column name as-is (for backward compatibility)
|
|
50
|
+
*/
|
|
51
|
+
columnToProperty(column: ColumnNode): string {
|
|
52
|
+
return column.name;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Capitalizes the first letter of a string
|
|
57
|
+
* @param s - String to capitalize
|
|
58
|
+
* @returns Capitalized string
|
|
59
|
+
*/
|
|
60
|
+
private capitalize(s: string): string {
|
|
61
|
+
if (!s) return s;
|
|
62
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { SelectQueryNode } from '../core/ast/query.js';
|
|
2
|
-
import {
|
|
3
|
-
ExpressionNode,
|
|
4
|
-
OperandNode,
|
|
1
|
+
import { SelectQueryNode } from '../core/ast/query.js';
|
|
2
|
+
import {
|
|
3
|
+
ExpressionNode,
|
|
4
|
+
OperandNode,
|
|
5
5
|
BinaryExpressionNode,
|
|
6
6
|
LogicalExpressionNode,
|
|
7
7
|
InExpressionNode,
|
|
@@ -20,18 +20,12 @@ import {
|
|
|
20
20
|
visitExpression,
|
|
21
21
|
visitOperand
|
|
22
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
|
-
import { HydrationMetadata } from '../core/hydration/types.js';
|
|
27
|
-
import { getJoinRelationName } from '../core/ast/join-metadata.js';
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Capitalizes the first letter of a string
|
|
31
|
-
* @param s - String to capitalize
|
|
32
|
-
* @returns Capitalized string
|
|
33
|
-
*/
|
|
34
|
-
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
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
|
+
import { HydrationMetadata } from '../core/hydration/types.js';
|
|
27
|
+
import { getJoinRelationName } from '../core/ast/join-metadata.js';
|
|
28
|
+
import { NamingStrategy, DefaultNamingStrategy } from './naming-strategy.js';
|
|
35
29
|
|
|
36
30
|
const assertNever = (value: never): never => {
|
|
37
31
|
throw new Error(`Unhandled SQL operator: ${value}`);
|
|
@@ -41,6 +35,7 @@ const assertNever = (value: never): never => {
|
|
|
41
35
|
* Generates TypeScript code from query AST nodes
|
|
42
36
|
*/
|
|
43
37
|
export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVisitor<string> {
|
|
38
|
+
constructor(private namingStrategy: NamingStrategy = new DefaultNamingStrategy()) {}
|
|
44
39
|
|
|
45
40
|
/**
|
|
46
41
|
* Generates TypeScript code from a query AST
|
|
@@ -54,14 +49,14 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
|
|
|
54
49
|
return lines.join('\n');
|
|
55
50
|
}
|
|
56
51
|
|
|
57
|
-
/**
|
|
58
|
-
* Builds TypeScript method chain lines from query AST
|
|
59
|
-
* @param ast - Query AST
|
|
60
|
-
* @returns Array of TypeScript method chain lines
|
|
61
|
-
*/
|
|
62
|
-
private buildSelectLines(ast: SelectQueryNode): string[] {
|
|
63
|
-
const lines: string[] = [];
|
|
64
|
-
const hydration = (ast.meta as HydrationMetadata | undefined)?.hydration;
|
|
52
|
+
/**
|
|
53
|
+
* Builds TypeScript method chain lines from query AST
|
|
54
|
+
* @param ast - Query AST
|
|
55
|
+
* @returns Array of TypeScript method chain lines
|
|
56
|
+
*/
|
|
57
|
+
private buildSelectLines(ast: SelectQueryNode): string[] {
|
|
58
|
+
const lines: string[] = [];
|
|
59
|
+
const hydration = (ast.meta as HydrationMetadata | undefined)?.hydration;
|
|
65
60
|
const hydratedRelations = new Set(hydration?.relations?.map(r => r.name) ?? []);
|
|
66
61
|
|
|
67
62
|
const selections = ast.columns
|
|
@@ -77,29 +72,29 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
|
|
|
77
72
|
lines.push(` ${sel}${index < selections.length - 1 ? ',' : ''}`);
|
|
78
73
|
});
|
|
79
74
|
lines.push(`})`);
|
|
80
|
-
lines.push(`.from(${
|
|
81
|
-
|
|
82
|
-
if (ast.distinct && ast.distinct.length) {
|
|
83
|
-
const cols = ast.distinct.map(c => `${
|
|
84
|
-
lines.push(`.distinct(${cols})`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
ast.joins.forEach(join => {
|
|
88
|
-
const relationName = getJoinRelationName(join);
|
|
89
|
-
if (relationName && hydratedRelations.has(relationName)) {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (relationName) {
|
|
94
|
-
if (join.kind === 'INNER') {
|
|
95
|
-
lines.push(`.joinRelation('${relationName}')`);
|
|
96
|
-
} else {
|
|
97
|
-
lines.push(`.joinRelation('${relationName}', '${join.kind}')`);
|
|
98
|
-
}
|
|
99
|
-
} else {
|
|
100
|
-
const table =
|
|
101
|
-
const cond = this.printExpression(join.condition);
|
|
102
|
-
let method = 'innerJoin';
|
|
75
|
+
lines.push(`.from(${this.namingStrategy.tableToSymbol(ast.from)})`);
|
|
76
|
+
|
|
77
|
+
if (ast.distinct && ast.distinct.length) {
|
|
78
|
+
const cols = ast.distinct.map(c => `${this.namingStrategy.tableToSymbol(c.table)}.${c.name}`).join(', ');
|
|
79
|
+
lines.push(`.distinct(${cols})`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
ast.joins.forEach(join => {
|
|
83
|
+
const relationName = getJoinRelationName(join);
|
|
84
|
+
if (relationName && hydratedRelations.has(relationName)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (relationName) {
|
|
89
|
+
if (join.kind === 'INNER') {
|
|
90
|
+
lines.push(`.joinRelation('${relationName}')`);
|
|
91
|
+
} else {
|
|
92
|
+
lines.push(`.joinRelation('${relationName}', '${join.kind}')`);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
const table = this.namingStrategy.tableToSymbol(join.table);
|
|
96
|
+
const cond = this.printExpression(join.condition);
|
|
97
|
+
let method = 'innerJoin';
|
|
103
98
|
if (join.kind === 'LEFT') method = 'leftJoin';
|
|
104
99
|
if (join.kind === 'RIGHT') method = 'rightJoin';
|
|
105
100
|
lines.push(`.${method}(${table}, ${cond})`);
|
|
@@ -121,7 +116,7 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
|
|
|
121
116
|
}
|
|
122
117
|
|
|
123
118
|
if (ast.groupBy && ast.groupBy.length) {
|
|
124
|
-
const cols = ast.groupBy.map(c => `${
|
|
119
|
+
const cols = ast.groupBy.map(c => `${this.namingStrategy.tableToSymbol(c.table)}.${c.name}`).join(', ');
|
|
125
120
|
lines.push(`.groupBy(${cols})`);
|
|
126
121
|
}
|
|
127
122
|
|
|
@@ -131,7 +126,7 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
|
|
|
131
126
|
|
|
132
127
|
if (ast.orderBy && ast.orderBy.length) {
|
|
133
128
|
ast.orderBy.forEach(o => {
|
|
134
|
-
lines.push(`.orderBy(${
|
|
129
|
+
lines.push(`.orderBy(${this.namingStrategy.tableToSymbol(o.column.table)}.${o.column.name}, '${o.direction}')`);
|
|
135
130
|
});
|
|
136
131
|
}
|
|
137
132
|
|
|
@@ -292,7 +287,7 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
|
|
|
292
287
|
* @returns TypeScript code representation
|
|
293
288
|
*/
|
|
294
289
|
private printColumnOperand(column: ColumnNode): string {
|
|
295
|
-
return `${
|
|
290
|
+
return `${this.namingStrategy.tableToSymbol(column.table)}.${column.name}`;
|
|
296
291
|
}
|
|
297
292
|
|
|
298
293
|
/**
|
|
@@ -321,7 +316,7 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
|
|
|
321
316
|
* @returns TypeScript code representation
|
|
322
317
|
*/
|
|
323
318
|
private printJsonPathOperand(json: JsonPathNode): string {
|
|
324
|
-
return `jsonPath(${
|
|
319
|
+
return `jsonPath(${this.namingStrategy.tableToSymbol(json.column.table)}.${json.column.name}, '${json.path}')`;
|
|
325
320
|
}
|
|
326
321
|
|
|
327
322
|
/**
|
|
@@ -364,14 +359,14 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
|
|
|
364
359
|
|
|
365
360
|
if (node.partitionBy && node.partitionBy.length > 0) {
|
|
366
361
|
const partitionClause =
|
|
367
|
-
'PARTITION BY ' + node.partitionBy.map(col => `${
|
|
362
|
+
'PARTITION BY ' + node.partitionBy.map(col => `${this.namingStrategy.tableToSymbol(col.table)}.${col.name}`).join(', ');
|
|
368
363
|
parts.push(partitionClause);
|
|
369
364
|
}
|
|
370
365
|
|
|
371
366
|
if (node.orderBy && node.orderBy.length > 0) {
|
|
372
367
|
const orderClause =
|
|
373
368
|
'ORDER BY ' +
|
|
374
|
-
node.orderBy.map(o => `${
|
|
369
|
+
node.orderBy.map(o => `${this.namingStrategy.tableToSymbol(o.column.table)}.${o.column.name} ${o.direction}`).join(', ');
|
|
375
370
|
parts.push(orderClause);
|
|
376
371
|
}
|
|
377
372
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TableDef, IndexDef, IndexColumn } from '../../schema/table.js';
|
|
2
2
|
import type { ColumnDef, ForeignKeyReference } from '../../schema/column.js';
|
|
3
|
-
import type { SchemaDialect
|
|
3
|
+
import type { SchemaDialect } from './schema-dialect.js';
|
|
4
4
|
import { deriveIndexName } from './naming-strategy.js';
|
|
5
5
|
import {
|
|
6
6
|
formatLiteral,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
Quoter
|
|
11
11
|
} from './sql-writing.js';
|
|
12
12
|
import { DatabaseTable, DatabaseColumn, ColumnDiff } from './schema-types.js';
|
|
13
|
+
import { DialectName } from './schema-dialect.js';
|
|
13
14
|
|
|
14
15
|
export interface SchemaGenerateResult {
|
|
15
16
|
tableSql: string;
|
|
@@ -152,4 +153,4 @@ const orderTablesByDependencies = (tables: TableDef[]): TableDef[] => {
|
|
|
152
153
|
};
|
|
153
154
|
|
|
154
155
|
// Re-export DialectName for backward compatibility
|
|
155
|
-
export { DialectName };
|
|
156
|
+
export type { DialectName };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DialectName } from './schema-generator.js';
|
|
1
|
+
import type { DialectName } from './schema-generator.js';
|
|
2
2
|
import { DatabaseSchema } from './schema-types.js';
|
|
3
3
|
import { DbExecutor } from '../execution/db-executor.js';
|
|
4
4
|
import type { IntrospectOptions, SchemaIntrospector, IntrospectContext } from './introspect/types.js';
|
|
@@ -291,6 +291,34 @@ export abstract class Dialect
|
|
|
291
291
|
this.registerDefaultExpressionCompilers();
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Creates a new Dialect instance (for testing purposes)
|
|
296
|
+
* @param functionStrategy - Optional function strategy
|
|
297
|
+
* @returns New Dialect instance
|
|
298
|
+
*/
|
|
299
|
+
static create(functionStrategy?: FunctionStrategy): Dialect {
|
|
300
|
+
// Create a minimal concrete implementation for testing
|
|
301
|
+
class TestDialect extends Dialect {
|
|
302
|
+
protected readonly dialect: DialectName = 'sqlite';
|
|
303
|
+
quoteIdentifier(id: string): string {
|
|
304
|
+
return `"${id}"`;
|
|
305
|
+
}
|
|
306
|
+
protected compileSelectAst(): never {
|
|
307
|
+
throw new Error('Not implemented');
|
|
308
|
+
}
|
|
309
|
+
protected compileInsertAst(): never {
|
|
310
|
+
throw new Error('Not implemented');
|
|
311
|
+
}
|
|
312
|
+
protected compileUpdateAst(): never {
|
|
313
|
+
throw new Error('Not implemented');
|
|
314
|
+
}
|
|
315
|
+
protected compileDeleteAst(): never {
|
|
316
|
+
throw new Error('Not implemented');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return new TestDialect(functionStrategy);
|
|
320
|
+
}
|
|
321
|
+
|
|
294
322
|
/**
|
|
295
323
|
* Registers an expression compiler for a specific node type
|
|
296
324
|
* @param type - Expression node type
|
package/src/decorators/column.ts
CHANGED
|
@@ -16,11 +16,20 @@ export interface ColumnOptions {
|
|
|
16
16
|
export type ColumnInput = ColumnOptions | ColumnDef;
|
|
17
17
|
|
|
18
18
|
const normalizeColumnInput = (input: ColumnInput): ColumnDefLike => {
|
|
19
|
+
const asOptions = input as ColumnOptions;
|
|
20
|
+
const asDefinition = input as ColumnDef;
|
|
19
21
|
const column: ColumnDefLike = {
|
|
20
|
-
type:
|
|
21
|
-
args:
|
|
22
|
-
notNull:
|
|
23
|
-
primary:
|
|
22
|
+
type: asOptions.type ?? asDefinition.type,
|
|
23
|
+
args: asOptions.args ?? asDefinition.args,
|
|
24
|
+
notNull: asOptions.notNull ?? asDefinition.notNull,
|
|
25
|
+
primary: asOptions.primary ?? asDefinition.primary,
|
|
26
|
+
unique: asDefinition.unique,
|
|
27
|
+
default: asDefinition.default,
|
|
28
|
+
autoIncrement: asDefinition.autoIncrement,
|
|
29
|
+
generated: asDefinition.generated,
|
|
30
|
+
check: asDefinition.check,
|
|
31
|
+
references: asDefinition.references,
|
|
32
|
+
comment: asDefinition.comment
|
|
24
33
|
};
|
|
25
34
|
|
|
26
35
|
if (!column.type) {
|
package/src/index.ts
CHANGED
|
@@ -23,13 +23,20 @@ export * from './core/functions/datetime.js';
|
|
|
23
23
|
export * from './orm/als.js';
|
|
24
24
|
export * from './orm/hydration.js';
|
|
25
25
|
export * from './codegen/typescript.js';
|
|
26
|
-
export * from './orm/orm-
|
|
26
|
+
export * from './orm/orm-session.js';
|
|
27
|
+
export * from './orm/orm.js';
|
|
27
28
|
export * from './orm/entity.js';
|
|
28
29
|
export * from './orm/lazy-batch.js';
|
|
29
30
|
export * from './orm/relations/has-many.js';
|
|
30
31
|
export * from './orm/relations/belongs-to.js';
|
|
31
32
|
export * from './orm/relations/many-to-many.js';
|
|
32
33
|
export * from './orm/execute.js';
|
|
34
|
+
export * from './orm/entity-context.js';
|
|
35
|
+
export * from './orm/execution-context.js';
|
|
36
|
+
export * from './orm/hydration-context.js';
|
|
37
|
+
export * from './orm/domain-event-bus.js';
|
|
38
|
+
export * from './orm/runtime-types.js';
|
|
39
|
+
export * from './orm/query-logger.js';
|
|
33
40
|
|
|
34
41
|
// NEW: execution abstraction + helpers
|
|
35
42
|
export * from './core/execution/db-executor.js';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Dialect } from '../core/dialect/abstract.js';
|
|
2
|
+
import type { DbExecutor } from '../core/execution/db-executor.js';
|
|
3
|
+
import { TableDef } from '../schema/table.js';
|
|
4
|
+
import { RelationDef } from '../schema/relation.js';
|
|
5
|
+
import { RelationChange, RelationKey, TrackedEntity } from './runtime-types.js';
|
|
6
|
+
|
|
7
|
+
export interface EntityContext {
|
|
8
|
+
dialect: Dialect;
|
|
9
|
+
executor: DbExecutor;
|
|
10
|
+
|
|
11
|
+
getEntity(table: TableDef, pk: any): any;
|
|
12
|
+
setEntity(table: TableDef, pk: any, entity: any): void;
|
|
13
|
+
|
|
14
|
+
trackNew(table: TableDef, entity: any, pk?: any): void;
|
|
15
|
+
trackManaged(table: TableDef, pk: any, entity: any): void;
|
|
16
|
+
|
|
17
|
+
markDirty(entity: any): void;
|
|
18
|
+
markRemoved(entity: any): void;
|
|
19
|
+
|
|
20
|
+
getEntitiesForTable(table: TableDef): TrackedEntity[];
|
|
21
|
+
|
|
22
|
+
registerRelationChange(
|
|
23
|
+
root: any,
|
|
24
|
+
relationKey: RelationKey,
|
|
25
|
+
rootTable: TableDef,
|
|
26
|
+
relationName: string,
|
|
27
|
+
relation: RelationDef,
|
|
28
|
+
change: RelationChange<any>
|
|
29
|
+
): void;
|
|
30
|
+
}
|
package/src/orm/entity-meta.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TableDef } from '../schema/table.js';
|
|
2
|
-
import {
|
|
2
|
+
import { EntityContext } from './entity-context.js';
|
|
3
3
|
import { RelationMap } from '../schema/types.js';
|
|
4
4
|
|
|
5
5
|
export const ENTITY_META = Symbol('EntityMeta');
|
|
@@ -7,7 +7,7 @@ export const ENTITY_META = Symbol('EntityMeta');
|
|
|
7
7
|
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
8
8
|
|
|
9
9
|
export interface EntityMeta<TTable extends TableDef> {
|
|
10
|
-
ctx:
|
|
10
|
+
ctx: EntityContext;
|
|
11
11
|
table: TTable;
|
|
12
12
|
lazyRelations: (keyof RelationMap<TTable>)[];
|
|
13
13
|
relationCache: Map<string, Promise<any>>;
|
|
@@ -6,12 +6,7 @@ export type EntityConstructor = new (...args: any[]) => any;
|
|
|
6
6
|
export type EntityOrTableTarget = EntityConstructor | TableDef;
|
|
7
7
|
export type EntityOrTableTargetResolver = EntityOrTableTarget | (() => EntityOrTableTarget);
|
|
8
8
|
|
|
9
|
-
export
|
|
10
|
-
type: ColumnType;
|
|
11
|
-
args?: ColumnDef['args'];
|
|
12
|
-
primary?: boolean;
|
|
13
|
-
notNull?: boolean;
|
|
14
|
-
}
|
|
9
|
+
export type ColumnDefLike = Omit<ColumnDef, 'name' | 'table'>;
|
|
15
10
|
|
|
16
11
|
interface BaseRelationMetadata {
|
|
17
12
|
propertyKey: string;
|
package/src/orm/entity.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { TableDef } from '../schema/table.js';
|
|
2
|
-
import { Entity, RelationMap, HasManyCollection, HasOneReference, BelongsToReference, ManyToManyCollection } from '../schema/types.js';
|
|
3
|
-
import {
|
|
4
|
-
import { ENTITY_META, EntityMeta, getEntityMeta } from './entity-meta.js';
|
|
5
|
-
import { DefaultHasManyCollection } from './relations/has-many.js';
|
|
6
|
-
import { DefaultHasOneReference } from './relations/has-one.js';
|
|
7
|
-
import { DefaultBelongsToReference } from './relations/belongs-to.js';
|
|
8
|
-
import { DefaultManyToManyCollection } from './relations/many-to-many.js';
|
|
9
|
-
import { HasManyRelation, HasOneRelation, BelongsToRelation, BelongsToManyRelation, RelationKinds } from '../schema/relation.js';
|
|
10
|
-
import { loadHasManyRelation, loadHasOneRelation, loadBelongsToRelation, loadBelongsToManyRelation } from './lazy-batch.js';
|
|
2
|
+
import { Entity, RelationMap, HasManyCollection, HasOneReference, BelongsToReference, ManyToManyCollection } from '../schema/types.js';
|
|
3
|
+
import { EntityContext } from './entity-context.js';
|
|
4
|
+
import { ENTITY_META, EntityMeta, getEntityMeta } from './entity-meta.js';
|
|
5
|
+
import { DefaultHasManyCollection } from './relations/has-many.js';
|
|
6
|
+
import { DefaultHasOneReference } from './relations/has-one.js';
|
|
7
|
+
import { DefaultBelongsToReference } from './relations/belongs-to.js';
|
|
8
|
+
import { DefaultManyToManyCollection } from './relations/many-to-many.js';
|
|
9
|
+
import { HasManyRelation, HasOneRelation, BelongsToRelation, BelongsToManyRelation, RelationKinds } from '../schema/relation.js';
|
|
10
|
+
import { loadHasManyRelation, loadHasOneRelation, loadBelongsToRelation, loadBelongsToManyRelation } from './lazy-batch.js';
|
|
11
11
|
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
12
12
|
|
|
13
13
|
type Rows = Record<string, any>[];
|
|
@@ -45,7 +45,7 @@ export const createEntityProxy = <
|
|
|
45
45
|
TTable extends TableDef,
|
|
46
46
|
TLazy extends keyof RelationMap<TTable> = keyof RelationMap<TTable>
|
|
47
47
|
>(
|
|
48
|
-
ctx:
|
|
48
|
+
ctx: EntityContext,
|
|
49
49
|
table: TTable,
|
|
50
50
|
row: Record<string, any>,
|
|
51
51
|
lazyRelations: TLazy[] = [] as TLazy[]
|
|
@@ -105,7 +105,7 @@ export const createEntityProxy = <
|
|
|
105
105
|
};
|
|
106
106
|
|
|
107
107
|
export const createEntityFromRow = <TTable extends TableDef>(
|
|
108
|
-
ctx:
|
|
108
|
+
ctx: EntityContext,
|
|
109
109
|
table: TTable,
|
|
110
110
|
row: Record<string, any>,
|
|
111
111
|
lazyRelations: (keyof RelationMap<TTable>)[] = []
|
|
@@ -129,60 +129,60 @@ export const createEntityFromRow = <TTable extends TableDef>(
|
|
|
129
129
|
|
|
130
130
|
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
132
|
+
const populateHydrationCache = <TTable extends TableDef>(
|
|
133
|
+
entity: any,
|
|
134
|
+
row: Record<string, any>,
|
|
135
|
+
meta: EntityMeta<TTable>
|
|
136
|
+
): void => {
|
|
137
|
+
for (const relationName of Object.keys(meta.table.relations)) {
|
|
138
|
+
const relation = meta.table.relations[relationName];
|
|
139
|
+
const data = row[relationName];
|
|
140
|
+
if (relation.type === RelationKinds.HasOne) {
|
|
141
|
+
const localKey = relation.localKey || findPrimaryKey(meta.table);
|
|
142
|
+
const rootValue = entity[localKey];
|
|
143
|
+
if (rootValue === undefined || rootValue === null) continue;
|
|
144
|
+
if (!data || typeof data !== 'object') continue;
|
|
145
|
+
const cache = new Map<string, Record<string, any>>();
|
|
146
|
+
cache.set(toKey(rootValue), data as Record<string, any>);
|
|
147
|
+
meta.relationHydration.set(relationName, cache);
|
|
148
|
+
meta.relationCache.set(relationName, Promise.resolve(cache));
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!Array.isArray(data)) continue;
|
|
153
|
+
|
|
154
|
+
if (relation.type === RelationKinds.HasMany || relation.type === RelationKinds.BelongsToMany) {
|
|
155
|
+
const localKey = relation.localKey || findPrimaryKey(meta.table);
|
|
156
|
+
const rootValue = entity[localKey];
|
|
157
|
+
if (rootValue === undefined || rootValue === null) continue;
|
|
158
|
+
const cache = new Map<string, Rows>();
|
|
159
|
+
cache.set(toKey(rootValue), data as Rows);
|
|
160
|
+
meta.relationHydration.set(relationName, cache);
|
|
161
|
+
meta.relationCache.set(relationName, Promise.resolve(cache));
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (relation.type === RelationKinds.BelongsTo) {
|
|
166
|
+
const targetKey = relation.localKey || findPrimaryKey(relation.target);
|
|
167
|
+
const cache = new Map<string, Record<string, any>>();
|
|
168
|
+
for (const item of data) {
|
|
169
|
+
const pkValue = item[targetKey];
|
|
170
|
+
if (pkValue === undefined || pkValue === null) continue;
|
|
171
|
+
cache.set(toKey(pkValue), item);
|
|
172
|
+
}
|
|
173
|
+
if (cache.size) {
|
|
160
174
|
meta.relationHydration.set(relationName, cache);
|
|
161
175
|
meta.relationCache.set(relationName, Promise.resolve(cache));
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (relation.type === RelationKinds.BelongsTo) {
|
|
166
|
-
const targetKey = relation.localKey || findPrimaryKey(relation.target);
|
|
167
|
-
const cache = new Map<string, Record<string, any>>();
|
|
168
|
-
for (const item of data) {
|
|
169
|
-
const pkValue = item[targetKey];
|
|
170
|
-
if (pkValue === undefined || pkValue === null) continue;
|
|
171
|
-
cache.set(toKey(pkValue), item);
|
|
172
|
-
}
|
|
173
|
-
if (cache.size) {
|
|
174
|
-
meta.relationHydration.set(relationName, cache);
|
|
175
|
-
meta.relationCache.set(relationName, Promise.resolve(cache));
|
|
176
|
-
}
|
|
177
176
|
}
|
|
178
177
|
}
|
|
179
|
-
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
180
|
|
|
181
|
-
const getRelationWrapper = (
|
|
182
|
-
meta: EntityMeta<any>,
|
|
183
|
-
relationName: string,
|
|
184
|
-
owner: any
|
|
185
|
-
): HasManyCollection<any> | HasOneReference<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
|
|
181
|
+
const getRelationWrapper = (
|
|
182
|
+
meta: EntityMeta<any>,
|
|
183
|
+
relationName: string,
|
|
184
|
+
owner: any
|
|
185
|
+
): HasManyCollection<any> | HasOneReference<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
|
|
186
186
|
if (meta.relationWrappers.has(relationName)) {
|
|
187
187
|
return meta.relationWrappers.get(relationName) as HasManyCollection<any>;
|
|
188
188
|
}
|
|
@@ -198,34 +198,34 @@ const getRelationWrapper = (
|
|
|
198
198
|
return wrapper;
|
|
199
199
|
};
|
|
200
200
|
|
|
201
|
-
const instantiateWrapper = (
|
|
202
|
-
meta: EntityMeta<any>,
|
|
203
|
-
relationName: string,
|
|
204
|
-
relation: HasManyRelation | HasOneRelation | BelongsToRelation | BelongsToManyRelation,
|
|
205
|
-
owner: any
|
|
206
|
-
): HasManyCollection<any> | HasOneReference<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
|
|
207
|
-
switch (relation.type) {
|
|
208
|
-
case RelationKinds.HasOne: {
|
|
209
|
-
const hasOne = relation as HasOneRelation;
|
|
210
|
-
const localKey = hasOne.localKey || findPrimaryKey(meta.table);
|
|
211
|
-
const loader = () => relationLoaderCache(meta, relationName, () =>
|
|
212
|
-
loadHasOneRelation(meta.ctx, meta.table, relationName, hasOne)
|
|
213
|
-
);
|
|
214
|
-
return new DefaultHasOneReference(
|
|
215
|
-
meta.ctx,
|
|
216
|
-
meta,
|
|
217
|
-
owner,
|
|
218
|
-
relationName,
|
|
219
|
-
hasOne,
|
|
220
|
-
meta.table,
|
|
221
|
-
loader,
|
|
222
|
-
(row: Record<string, any>) => createEntityFromRow(meta.ctx, hasOne.target, row),
|
|
223
|
-
localKey
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
case RelationKinds.HasMany: {
|
|
227
|
-
const hasMany = relation as HasManyRelation;
|
|
228
|
-
const localKey = hasMany.localKey || findPrimaryKey(meta.table);
|
|
201
|
+
const instantiateWrapper = (
|
|
202
|
+
meta: EntityMeta<any>,
|
|
203
|
+
relationName: string,
|
|
204
|
+
relation: HasManyRelation | HasOneRelation | BelongsToRelation | BelongsToManyRelation,
|
|
205
|
+
owner: any
|
|
206
|
+
): HasManyCollection<any> | HasOneReference<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
|
|
207
|
+
switch (relation.type) {
|
|
208
|
+
case RelationKinds.HasOne: {
|
|
209
|
+
const hasOne = relation as HasOneRelation;
|
|
210
|
+
const localKey = hasOne.localKey || findPrimaryKey(meta.table);
|
|
211
|
+
const loader = () => relationLoaderCache(meta, relationName, () =>
|
|
212
|
+
loadHasOneRelation(meta.ctx, meta.table, relationName, hasOne)
|
|
213
|
+
);
|
|
214
|
+
return new DefaultHasOneReference(
|
|
215
|
+
meta.ctx,
|
|
216
|
+
meta,
|
|
217
|
+
owner,
|
|
218
|
+
relationName,
|
|
219
|
+
hasOne,
|
|
220
|
+
meta.table,
|
|
221
|
+
loader,
|
|
222
|
+
(row: Record<string, any>) => createEntityFromRow(meta.ctx, hasOne.target, row),
|
|
223
|
+
localKey
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
case RelationKinds.HasMany: {
|
|
227
|
+
const hasMany = relation as HasManyRelation;
|
|
228
|
+
const localKey = hasMany.localKey || findPrimaryKey(meta.table);
|
|
229
229
|
const loader = () => relationLoaderCache(meta, relationName, () =>
|
|
230
230
|
loadHasManyRelation(meta.ctx, meta.table, relationName, hasMany)
|
|
231
231
|
);
|