metal-orm 1.0.60 → 1.0.63
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 +15 -11
- package/dist/index.cjs +266 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +163 -16
- package/dist/index.d.ts +163 -16
- package/dist/index.js +259 -73
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ast/expression.ts +9 -0
- package/src/decorators/bootstrap.ts +149 -137
- package/src/decorators/index.ts +4 -0
- package/src/orm/entity-materializer.ts +159 -0
- package/src/orm/entity-registry.ts +39 -0
- package/src/orm/execute.ts +62 -18
- package/src/query-builder/hydration-planner.ts +14 -16
- package/src/query-builder/select/select-operations.ts +1 -2
- package/src/query-builder/select.ts +173 -67
package/package.json
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Expression AST nodes and builders.
|
|
3
3
|
* Re-exports components for building and visiting SQL expression trees.
|
|
4
4
|
*/
|
|
5
|
+
import type { CaseExpressionNode, FunctionNode, WindowFunctionNode } from './expression-nodes.js';
|
|
6
|
+
|
|
5
7
|
export * from './expression-nodes.js';
|
|
6
8
|
export * from './expression-builders.js';
|
|
7
9
|
export * from './window-functions.js';
|
|
@@ -9,3 +11,10 @@ export * from './aggregate-functions.js';
|
|
|
9
11
|
export * from './expression-visitor.js';
|
|
10
12
|
export type { ColumnRef, TableRef as AstTableRef } from './types.js';
|
|
11
13
|
export * from './adapters.js';
|
|
14
|
+
|
|
15
|
+
export type TypedExpression<T> =
|
|
16
|
+
(FunctionNode | CaseExpressionNode | WindowFunctionNode) & { __tsType: T };
|
|
17
|
+
|
|
18
|
+
export const asType = <T>(
|
|
19
|
+
expr: FunctionNode | CaseExpressionNode | WindowFunctionNode
|
|
20
|
+
): TypedExpression<T> => expr as TypedExpression<T>;
|
|
@@ -3,35 +3,36 @@ import {
|
|
|
3
3
|
hasMany,
|
|
4
4
|
hasOne,
|
|
5
5
|
belongsTo,
|
|
6
|
-
belongsToMany,
|
|
7
|
-
RelationKinds,
|
|
8
|
-
type HasManyRelation,
|
|
9
|
-
type HasOneRelation,
|
|
10
|
-
type BelongsToRelation,
|
|
6
|
+
belongsToMany,
|
|
7
|
+
RelationKinds,
|
|
8
|
+
type HasManyRelation,
|
|
9
|
+
type HasOneRelation,
|
|
10
|
+
type BelongsToRelation,
|
|
11
11
|
type BelongsToManyRelation,
|
|
12
12
|
type RelationDef
|
|
13
13
|
} from '../schema/relation.js';
|
|
14
|
-
import { TableDef } from '../schema/table.js';
|
|
15
|
-
import { isTableDef } from '../schema/table-guards.js';
|
|
16
|
-
import {
|
|
17
|
-
buildTableDef,
|
|
18
|
-
EntityConstructor,
|
|
19
|
-
EntityMetadata,
|
|
20
|
-
EntityOrTableTarget,
|
|
21
|
-
EntityOrTableTargetResolver,
|
|
22
|
-
getAllEntityMetadata,
|
|
23
|
-
getEntityMetadata
|
|
24
|
-
} from '../orm/entity-metadata.js';
|
|
14
|
+
import { TableDef } from '../schema/table.js';
|
|
15
|
+
import { isTableDef } from '../schema/table-guards.js';
|
|
16
|
+
import {
|
|
17
|
+
buildTableDef,
|
|
18
|
+
EntityConstructor,
|
|
19
|
+
EntityMetadata,
|
|
20
|
+
EntityOrTableTarget,
|
|
21
|
+
EntityOrTableTargetResolver,
|
|
22
|
+
getAllEntityMetadata,
|
|
23
|
+
getEntityMetadata
|
|
24
|
+
} from '../orm/entity-metadata.js';
|
|
25
25
|
|
|
26
26
|
import { tableRef, type TableRef } from '../schema/table.js';
|
|
27
27
|
import {
|
|
28
|
-
SelectableKeys,
|
|
29
|
-
ColumnDef,
|
|
30
|
-
HasManyCollection,
|
|
31
|
-
HasOneReference,
|
|
32
|
-
BelongsToReference,
|
|
33
|
-
ManyToManyCollection
|
|
34
|
-
|
|
28
|
+
SelectableKeys,
|
|
29
|
+
ColumnDef,
|
|
30
|
+
HasManyCollection,
|
|
31
|
+
HasOneReference,
|
|
32
|
+
BelongsToReference,
|
|
33
|
+
ManyToManyCollection,
|
|
34
|
+
EntityInstance
|
|
35
|
+
} from '../schema/types.js';
|
|
35
36
|
|
|
36
37
|
const unwrapTarget = (target: EntityOrTableTargetResolver): EntityOrTableTarget => {
|
|
37
38
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
@@ -41,10 +42,10 @@ const unwrapTarget = (target: EntityOrTableTargetResolver): EntityOrTableTarget
|
|
|
41
42
|
return target as EntityOrTableTarget;
|
|
42
43
|
};
|
|
43
44
|
|
|
44
|
-
const resolveTableTarget = (
|
|
45
|
-
target: EntityOrTableTargetResolver,
|
|
46
|
-
tableMap: Map<EntityConstructor, TableDef>
|
|
47
|
-
): TableDef => {
|
|
45
|
+
const resolveTableTarget = (
|
|
46
|
+
target: EntityOrTableTargetResolver,
|
|
47
|
+
tableMap: Map<EntityConstructor, TableDef>
|
|
48
|
+
): TableDef => {
|
|
48
49
|
const resolved = unwrapTarget(target);
|
|
49
50
|
if (isTableDef(resolved)) {
|
|
50
51
|
return resolved;
|
|
@@ -53,65 +54,65 @@ const resolveTableTarget = (
|
|
|
53
54
|
if (!table) {
|
|
54
55
|
throw new Error(`Entity '${(resolved as EntityConstructor).name}' is not registered with decorators`);
|
|
55
56
|
}
|
|
56
|
-
return table;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const toSnakeCase = (value: string): string => {
|
|
60
|
-
return value
|
|
61
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
62
|
-
.replace(/[^a-z0-9_]+/gi, '_')
|
|
63
|
-
.replace(/__+/g, '_')
|
|
64
|
-
.replace(/^_|_$/g, '')
|
|
65
|
-
.toLowerCase();
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const normalizeEntityName = (value: string): string => {
|
|
69
|
-
const stripped = value.replace(/Entity$/i, '');
|
|
70
|
-
const normalized = toSnakeCase(stripped || value);
|
|
71
|
-
return normalized || 'unknown';
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const getPivotKeyBaseFromTarget = (target: EntityOrTableTargetResolver): string => {
|
|
75
|
-
const resolved = unwrapTarget(target);
|
|
76
|
-
if (isTableDef(resolved)) {
|
|
77
|
-
return toSnakeCase(resolved.name || 'unknown');
|
|
78
|
-
}
|
|
79
|
-
const ctor = resolved as EntityConstructor;
|
|
80
|
-
return normalizeEntityName(ctor.name || 'unknown');
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const getPivotKeyBaseFromRoot = (meta: EntityMetadata): string => {
|
|
84
|
-
return normalizeEntityName(meta.target.name || meta.tableName || 'unknown');
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const buildRelationDefinitions = (
|
|
88
|
-
meta: EntityMetadata,
|
|
89
|
-
tableMap: Map<EntityConstructor, TableDef>
|
|
90
|
-
): Record<string, RelationDef> => {
|
|
91
|
-
const relations: Record<string, RelationDef> = {};
|
|
92
|
-
|
|
93
|
-
for (const [name, relation] of Object.entries(meta.relations)) {
|
|
57
|
+
return table;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const toSnakeCase = (value: string): string => {
|
|
61
|
+
return value
|
|
62
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
63
|
+
.replace(/[^a-z0-9_]+/gi, '_')
|
|
64
|
+
.replace(/__+/g, '_')
|
|
65
|
+
.replace(/^_|_$/g, '')
|
|
66
|
+
.toLowerCase();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const normalizeEntityName = (value: string): string => {
|
|
70
|
+
const stripped = value.replace(/Entity$/i, '');
|
|
71
|
+
const normalized = toSnakeCase(stripped || value);
|
|
72
|
+
return normalized || 'unknown';
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const getPivotKeyBaseFromTarget = (target: EntityOrTableTargetResolver): string => {
|
|
76
|
+
const resolved = unwrapTarget(target);
|
|
77
|
+
if (isTableDef(resolved)) {
|
|
78
|
+
return toSnakeCase(resolved.name || 'unknown');
|
|
79
|
+
}
|
|
80
|
+
const ctor = resolved as EntityConstructor;
|
|
81
|
+
return normalizeEntityName(ctor.name || 'unknown');
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const getPivotKeyBaseFromRoot = (meta: EntityMetadata): string => {
|
|
85
|
+
return normalizeEntityName(meta.target.name || meta.tableName || 'unknown');
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const buildRelationDefinitions = (
|
|
89
|
+
meta: EntityMetadata,
|
|
90
|
+
tableMap: Map<EntityConstructor, TableDef>
|
|
91
|
+
): Record<string, RelationDef> => {
|
|
92
|
+
const relations: Record<string, RelationDef> = {};
|
|
93
|
+
|
|
94
|
+
for (const [name, relation] of Object.entries(meta.relations)) {
|
|
94
95
|
switch (relation.kind) {
|
|
95
|
-
case RelationKinds.HasOne: {
|
|
96
|
-
const foreignKey = relation.foreignKey ?? `${getPivotKeyBaseFromRoot(meta)}_id`;
|
|
97
|
-
relations[name] = hasOne(
|
|
98
|
-
resolveTableTarget(relation.target, tableMap),
|
|
99
|
-
foreignKey,
|
|
100
|
-
relation.localKey,
|
|
101
|
-
relation.cascade
|
|
102
|
-
);
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
case RelationKinds.HasMany: {
|
|
106
|
-
const foreignKey = relation.foreignKey ?? `${getPivotKeyBaseFromRoot(meta)}_id`;
|
|
107
|
-
relations[name] = hasMany(
|
|
108
|
-
resolveTableTarget(relation.target, tableMap),
|
|
109
|
-
foreignKey,
|
|
110
|
-
relation.localKey,
|
|
111
|
-
relation.cascade
|
|
112
|
-
);
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
96
|
+
case RelationKinds.HasOne: {
|
|
97
|
+
const foreignKey = relation.foreignKey ?? `${getPivotKeyBaseFromRoot(meta)}_id`;
|
|
98
|
+
relations[name] = hasOne(
|
|
99
|
+
resolveTableTarget(relation.target, tableMap),
|
|
100
|
+
foreignKey,
|
|
101
|
+
relation.localKey,
|
|
102
|
+
relation.cascade
|
|
103
|
+
);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case RelationKinds.HasMany: {
|
|
107
|
+
const foreignKey = relation.foreignKey ?? `${getPivotKeyBaseFromRoot(meta)}_id`;
|
|
108
|
+
relations[name] = hasMany(
|
|
109
|
+
resolveTableTarget(relation.target, tableMap),
|
|
110
|
+
foreignKey,
|
|
111
|
+
relation.localKey,
|
|
112
|
+
relation.cascade
|
|
113
|
+
);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
115
116
|
case RelationKinds.BelongsTo: {
|
|
116
117
|
relations[name] = belongsTo(
|
|
117
118
|
resolveTableTarget(relation.target, tableMap),
|
|
@@ -121,22 +122,22 @@ const buildRelationDefinitions = (
|
|
|
121
122
|
);
|
|
122
123
|
break;
|
|
123
124
|
}
|
|
124
|
-
case RelationKinds.BelongsToMany: {
|
|
125
|
-
const pivotForeignKeyToRoot =
|
|
126
|
-
relation.pivotForeignKeyToRoot ?? `${getPivotKeyBaseFromRoot(meta)}_id`;
|
|
127
|
-
const pivotForeignKeyToTarget =
|
|
128
|
-
relation.pivotForeignKeyToTarget ?? `${getPivotKeyBaseFromTarget(relation.target)}_id`;
|
|
129
|
-
relations[name] = belongsToMany(
|
|
130
|
-
resolveTableTarget(relation.target, tableMap),
|
|
131
|
-
resolveTableTarget(relation.pivotTable, tableMap),
|
|
132
|
-
{
|
|
133
|
-
pivotForeignKeyToRoot,
|
|
134
|
-
pivotForeignKeyToTarget,
|
|
135
|
-
localKey: relation.localKey,
|
|
136
|
-
targetKey: relation.targetKey,
|
|
137
|
-
pivotPrimaryKey: relation.pivotPrimaryKey,
|
|
138
|
-
defaultPivotColumns: relation.defaultPivotColumns,
|
|
139
|
-
cascade: relation.cascade
|
|
125
|
+
case RelationKinds.BelongsToMany: {
|
|
126
|
+
const pivotForeignKeyToRoot =
|
|
127
|
+
relation.pivotForeignKeyToRoot ?? `${getPivotKeyBaseFromRoot(meta)}_id`;
|
|
128
|
+
const pivotForeignKeyToTarget =
|
|
129
|
+
relation.pivotForeignKeyToTarget ?? `${getPivotKeyBaseFromTarget(relation.target)}_id`;
|
|
130
|
+
relations[name] = belongsToMany(
|
|
131
|
+
resolveTableTarget(relation.target, tableMap),
|
|
132
|
+
resolveTableTarget(relation.pivotTable, tableMap),
|
|
133
|
+
{
|
|
134
|
+
pivotForeignKeyToRoot,
|
|
135
|
+
pivotForeignKeyToTarget,
|
|
136
|
+
localKey: relation.localKey,
|
|
137
|
+
targetKey: relation.targetKey,
|
|
138
|
+
pivotPrimaryKey: relation.pivotPrimaryKey,
|
|
139
|
+
defaultPivotColumns: relation.defaultPivotColumns,
|
|
140
|
+
cascade: relation.cascade
|
|
140
141
|
}
|
|
141
142
|
);
|
|
142
143
|
break;
|
|
@@ -196,35 +197,46 @@ type NonFunctionKeys<T> = {
|
|
|
196
197
|
type RelationKeys<TEntity extends object> =
|
|
197
198
|
Exclude<NonFunctionKeys<TEntity>, SelectableKeys<TEntity>> & string;
|
|
198
199
|
|
|
199
|
-
type EntityTable<TEntity extends object> =
|
|
200
|
-
Omit<TableDef<{ [K in SelectableKeys<TEntity>]: ColumnDef }>, 'relations'> & {
|
|
201
|
-
relations: {
|
|
200
|
+
type EntityTable<TEntity extends object> =
|
|
201
|
+
Omit<TableDef<{ [K in SelectableKeys<TEntity>]: ColumnDef }>, 'relations'> & {
|
|
202
|
+
relations: {
|
|
202
203
|
[K in RelationKeys<TEntity>]:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
};
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
export
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
204
|
+
NonNullable<TEntity[K]> extends HasManyCollection<infer TChild>
|
|
205
|
+
? HasManyRelation<EntityTable<NonNullable<TChild> & object>>
|
|
206
|
+
: NonNullable<TEntity[K]> extends ManyToManyCollection<infer TTarget, infer TPivot>
|
|
207
|
+
? BelongsToManyRelation<
|
|
208
|
+
EntityTable<NonNullable<TTarget> & object>,
|
|
209
|
+
TPivot extends object ? EntityTable<NonNullable<TPivot> & object> : TableDef
|
|
210
|
+
>
|
|
211
|
+
: NonNullable<TEntity[K]> extends HasOneReference<infer TChild>
|
|
212
|
+
? HasOneRelation<EntityTable<NonNullable<TChild> & object>>
|
|
213
|
+
: NonNullable<TEntity[K]> extends BelongsToReference<infer TParent>
|
|
214
|
+
? BelongsToRelation<EntityTable<NonNullable<TParent> & object>>
|
|
215
|
+
: NonNullable<TEntity[K]> extends object
|
|
216
|
+
? BelongsToRelation<EntityTable<NonNullable<TEntity[K]> & object>>
|
|
217
|
+
: never;
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export type DecoratedEntityInstance<TEntity extends object> =
|
|
222
|
+
TEntity & EntityInstance<EntityTable<TEntity>>;
|
|
223
|
+
|
|
224
|
+
export const selectFromEntity = <TEntity extends object>(
|
|
225
|
+
ctor: EntityConstructor<TEntity>
|
|
226
|
+
): SelectQueryBuilder<DecoratedEntityInstance<TEntity>, EntityTable<TEntity>> => {
|
|
227
|
+
const table = getTableDefFromEntity(ctor);
|
|
228
|
+
if (!table) {
|
|
229
|
+
throw new Error(`Entity '${ctor.name}' is not registered with decorators or has not been bootstrapped`);
|
|
226
230
|
}
|
|
227
|
-
return new SelectQueryBuilder(
|
|
231
|
+
return new SelectQueryBuilder(
|
|
232
|
+
table as unknown as EntityTable<TEntity>,
|
|
233
|
+
undefined,
|
|
234
|
+
undefined,
|
|
235
|
+
undefined,
|
|
236
|
+
undefined,
|
|
237
|
+
undefined,
|
|
238
|
+
ctor
|
|
239
|
+
);
|
|
228
240
|
};
|
|
229
241
|
|
|
230
242
|
/**
|
|
@@ -233,12 +245,12 @@ export const selectFromEntity = <TEntity extends object>(
|
|
|
233
245
|
* Lazily bootstraps entity metadata (via getTableDefFromEntity) and returns a
|
|
234
246
|
* `tableRef(...)`-style proxy so users can write `u.id` instead of `u.columns.id`.
|
|
235
247
|
*/
|
|
236
|
-
export const entityRef = <TEntity extends object>(
|
|
237
|
-
ctor: EntityConstructor<TEntity>
|
|
238
|
-
): TableRef<EntityTable<TEntity>> => {
|
|
239
|
-
const table = getTableDefFromEntity(ctor);
|
|
240
|
-
if (!table) {
|
|
241
|
-
throw new Error(`Entity '${ctor.name}' is not registered with decorators or has not been bootstrapped`);
|
|
242
|
-
}
|
|
243
|
-
return tableRef(table as EntityTable<TEntity>);
|
|
244
|
-
};
|
|
248
|
+
export const entityRef = <TEntity extends object>(
|
|
249
|
+
ctor: EntityConstructor<TEntity>
|
|
250
|
+
): TableRef<EntityTable<TEntity>> => {
|
|
251
|
+
const table = getTableDefFromEntity(ctor);
|
|
252
|
+
if (!table) {
|
|
253
|
+
throw new Error(`Entity '${ctor.name}' is not registered with decorators or has not been bootstrapped`);
|
|
254
|
+
}
|
|
255
|
+
return tableRef(table as EntityTable<TEntity>);
|
|
256
|
+
};
|
package/src/decorators/index.ts
CHANGED
|
@@ -6,3 +6,7 @@ export * from './column-decorator.js';
|
|
|
6
6
|
export * from './relations.js';
|
|
7
7
|
export * from './bootstrap.js';
|
|
8
8
|
export { getDecoratorMetadata } from './decorator-metadata.js';
|
|
9
|
+
|
|
10
|
+
// Entity Materialization - convert query results to real class instances
|
|
11
|
+
export { materializeAs, DefaultEntityMaterializer, PrototypeMaterializationStrategy, ConstructorMaterializationStrategy } from '../orm/entity-materializer.js';
|
|
12
|
+
export type { EntityMaterializer, EntityMaterializationStrategy } from '../orm/entity-materializer.js';
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { EntityConstructor } from './entity-metadata.js';
|
|
2
|
+
import { rebuildRegistry } from './entity-registry.js';
|
|
3
|
+
import { hasEntityMeta } from './entity-meta.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Strategy interface for materializing entity instances (Open/Closed Principle).
|
|
7
|
+
*/
|
|
8
|
+
export interface EntityMaterializationStrategy {
|
|
9
|
+
/**
|
|
10
|
+
* Creates an instance of the entity class and populates it with data.
|
|
11
|
+
* @param ctor - The entity constructor
|
|
12
|
+
* @param data - The raw data to populate
|
|
13
|
+
* @returns The materialized entity instance
|
|
14
|
+
*/
|
|
15
|
+
materialize<T>(ctor: EntityConstructor<T>, data: Record<string, unknown>): T;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default strategy: Uses Object.create() to create instance without calling constructor.
|
|
20
|
+
* Safe for classes with required constructor parameters.
|
|
21
|
+
*/
|
|
22
|
+
export class PrototypeMaterializationStrategy implements EntityMaterializationStrategy {
|
|
23
|
+
materialize<T>(ctor: EntityConstructor<T>, data: Record<string, unknown>): T {
|
|
24
|
+
const instance = Object.create(ctor.prototype) as T;
|
|
25
|
+
Object.assign(instance, data);
|
|
26
|
+
return instance;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Alternative strategy: Calls default constructor then assigns properties.
|
|
32
|
+
* Use when class constructor initializes important state.
|
|
33
|
+
*/
|
|
34
|
+
export class ConstructorMaterializationStrategy implements EntityMaterializationStrategy {
|
|
35
|
+
materialize<T>(ctor: EntityConstructor<T>, data: Record<string, unknown>): T {
|
|
36
|
+
const instance = Reflect.construct(ctor, []) as T & Record<string, unknown>;
|
|
37
|
+
Object.assign(instance, data);
|
|
38
|
+
return instance;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Interface for materializing query results into real entity class instances.
|
|
44
|
+
*/
|
|
45
|
+
export interface EntityMaterializer {
|
|
46
|
+
/**
|
|
47
|
+
* Materializes a single row into a real entity instance.
|
|
48
|
+
* @param entityClass - The entity constructor
|
|
49
|
+
* @param row - The raw data row
|
|
50
|
+
* @returns The materialized entity instance
|
|
51
|
+
*/
|
|
52
|
+
materialize<T>(entityClass: EntityConstructor<T>, row: Record<string, unknown>): T;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Materializes multiple rows into real entity instances.
|
|
56
|
+
* @param entityClass - The entity constructor
|
|
57
|
+
* @param rows - The raw data rows
|
|
58
|
+
* @returns Array of materialized entity instances
|
|
59
|
+
*/
|
|
60
|
+
materializeMany<T>(entityClass: EntityConstructor<T>, rows: Record<string, unknown>[]): T[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Default implementation of EntityMaterializer.
|
|
65
|
+
* Converts query results into actual class instances with working methods.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* const materializer = new DefaultEntityMaterializer();
|
|
69
|
+
* const users = materializer.materializeMany(User, queryResults);
|
|
70
|
+
* users[0] instanceof User; // true
|
|
71
|
+
* users[0].getFullName(); // works!
|
|
72
|
+
*/
|
|
73
|
+
export class DefaultEntityMaterializer implements EntityMaterializer {
|
|
74
|
+
constructor(
|
|
75
|
+
private readonly strategy: EntityMaterializationStrategy = new ConstructorMaterializationStrategy()
|
|
76
|
+
) { }
|
|
77
|
+
|
|
78
|
+
materialize<T>(ctor: EntityConstructor<T>, row: Record<string, unknown>): T {
|
|
79
|
+
if (hasEntityMeta(row)) {
|
|
80
|
+
return this.materializeEntityProxy(ctor, row);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const instance = this.strategy.materialize(ctor, row);
|
|
84
|
+
this.materializeRelations(instance as Record<string, unknown>);
|
|
85
|
+
return instance;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
materializeMany<T>(ctor: EntityConstructor<T>, rows: Record<string, unknown>[]): T[] {
|
|
89
|
+
return rows.map(row => this.materialize(ctor, row));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Recursively materializes nested relation data.
|
|
94
|
+
*/
|
|
95
|
+
private materializeRelations(instance: Record<string, unknown>): void {
|
|
96
|
+
// Rebuild registry to ensure we have latest metadata
|
|
97
|
+
rebuildRegistry();
|
|
98
|
+
|
|
99
|
+
for (const value of Object.values(instance)) {
|
|
100
|
+
if (value === null || value === undefined) continue;
|
|
101
|
+
|
|
102
|
+
// Handle has-one / belongs-to (single object)
|
|
103
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
104
|
+
const nested = value as Record<string, unknown>;
|
|
105
|
+
// Check if this looks like an entity (has common entity patterns)
|
|
106
|
+
if (this.isEntityLike(nested)) {
|
|
107
|
+
// For now, keep as-is since we don't have relation metadata here
|
|
108
|
+
// Future: use relation metadata to get target constructor
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Handle has-many / belongs-to-many (array)
|
|
113
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
114
|
+
const first = value[0];
|
|
115
|
+
if (typeof first === 'object' && first !== null && this.isEntityLike(first)) {
|
|
116
|
+
// For now, keep array as-is
|
|
117
|
+
// Future: materialize each item if we can resolve the target class
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Simple heuristic to check if an object looks like an entity.
|
|
125
|
+
*/
|
|
126
|
+
private isEntityLike(obj: Record<string, unknown>): boolean {
|
|
127
|
+
return 'id' in obj || Object.keys(obj).some(k =>
|
|
128
|
+
k.endsWith('Id') || k === 'createdAt' || k === 'updatedAt'
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private materializeEntityProxy<T>(ctor: EntityConstructor<T>, row: Record<string, unknown>): T {
|
|
133
|
+
const proxy = row as Record<string, unknown>;
|
|
134
|
+
const baseline = this.strategy.materialize(ctor, {}) as Record<string, unknown>;
|
|
135
|
+
for (const key of Object.keys(baseline)) {
|
|
136
|
+
if (!Object.prototype.hasOwnProperty.call(proxy, key)) {
|
|
137
|
+
proxy[key] = baseline[key];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
Object.setPrototypeOf(proxy, ctor.prototype);
|
|
141
|
+
return proxy as T;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Convenience function to materialize query results as real class instances.
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* const results = await selectFromEntity(User).execute(session);
|
|
150
|
+
* const users = materializeAs(User, results);
|
|
151
|
+
* users[0] instanceof User; // true!
|
|
152
|
+
*/
|
|
153
|
+
export const materializeAs = <TEntity extends object>(
|
|
154
|
+
ctor: EntityConstructor<TEntity>,
|
|
155
|
+
results: Record<string, unknown>[]
|
|
156
|
+
): TEntity[] => {
|
|
157
|
+
const materializer = new DefaultEntityMaterializer();
|
|
158
|
+
return materializer.materializeMany(ctor, results);
|
|
159
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { TableDef } from '../schema/table.js';
|
|
2
|
+
import { EntityConstructor, getAllEntityMetadata } from './entity-metadata.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Reverse lookup registry: TableDef → EntityConstructor
|
|
6
|
+
*/
|
|
7
|
+
const tableToConstructor = new Map<TableDef, EntityConstructor>();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Gets the entity constructor for a given table definition.
|
|
11
|
+
* @param table - The table definition
|
|
12
|
+
* @returns The entity constructor or undefined if not found
|
|
13
|
+
*/
|
|
14
|
+
export const getConstructorForTable = (table: TableDef): EntityConstructor | undefined => {
|
|
15
|
+
if (!tableToConstructor.size) {
|
|
16
|
+
rebuildRegistry();
|
|
17
|
+
}
|
|
18
|
+
return tableToConstructor.get(table);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Rebuilds the registry from entity metadata.
|
|
23
|
+
* Called automatically on first lookup or when metadata changes.
|
|
24
|
+
*/
|
|
25
|
+
export const rebuildRegistry = (): void => {
|
|
26
|
+
tableToConstructor.clear();
|
|
27
|
+
for (const meta of getAllEntityMetadata()) {
|
|
28
|
+
if (meta.table) {
|
|
29
|
+
tableToConstructor.set(meta.table, meta.target);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Clears the entity registry.
|
|
36
|
+
*/
|
|
37
|
+
export const clearEntityRegistry = (): void => {
|
|
38
|
+
tableToConstructor.clear();
|
|
39
|
+
};
|
package/src/orm/execute.ts
CHANGED
|
@@ -61,26 +61,56 @@ const executeWithContexts = async <TTable extends TableDef>(
|
|
|
61
61
|
await loadLazyRelationsForTable(entityCtx, qb.getTable(), lazyRelations, lazyRelationOptions);
|
|
62
62
|
return entities;
|
|
63
63
|
};
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
|
|
65
|
+
const executePlainWithContexts = async <TTable extends TableDef>(
|
|
66
|
+
execCtx: ExecutionContext,
|
|
67
|
+
qb: SelectQueryBuilder<unknown, TTable>
|
|
68
|
+
): Promise<Record<string, unknown>[]> => {
|
|
69
|
+
const ast = qb.getAST();
|
|
70
|
+
const compiled = execCtx.dialect.compileSelect(ast);
|
|
71
|
+
const executed = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
|
|
72
|
+
const rows = flattenResults(executed);
|
|
73
|
+
|
|
74
|
+
if (ast.setOps && ast.setOps.length > 0) {
|
|
75
|
+
return rows;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return hydrateRows(rows, qb.getHydrationPlan());
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Executes a hydrated query using the ORM session.
|
|
83
|
+
* @template TTable - The table type
|
|
84
|
+
* @param session - The ORM session
|
|
85
|
+
* @param qb - The select query builder
|
|
70
86
|
* @returns Promise resolving to array of entity instances
|
|
71
87
|
*/
|
|
72
|
-
export async function executeHydrated<TTable extends TableDef>(
|
|
73
|
-
session: OrmSession,
|
|
74
|
-
qb: SelectQueryBuilder<unknown, TTable>
|
|
75
|
-
): Promise<EntityInstance<TTable>[]> {
|
|
76
|
-
return executeWithContexts(session.getExecutionContext(), session, qb);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Executes a hydrated query
|
|
81
|
-
* @template TTable - The table type
|
|
82
|
-
* @param
|
|
83
|
-
* @param
|
|
88
|
+
export async function executeHydrated<TTable extends TableDef>(
|
|
89
|
+
session: OrmSession,
|
|
90
|
+
qb: SelectQueryBuilder<unknown, TTable>
|
|
91
|
+
): Promise<EntityInstance<TTable>[]> {
|
|
92
|
+
return executeWithContexts(session.getExecutionContext(), session, qb);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Executes a hydrated query and returns plain row objects (no entity proxies).
|
|
97
|
+
* @template TTable - The table type
|
|
98
|
+
* @param session - The ORM session
|
|
99
|
+
* @param qb - The select query builder
|
|
100
|
+
* @returns Promise resolving to array of plain row objects
|
|
101
|
+
*/
|
|
102
|
+
export async function executeHydratedPlain<TTable extends TableDef>(
|
|
103
|
+
session: OrmSession,
|
|
104
|
+
qb: SelectQueryBuilder<unknown, TTable>
|
|
105
|
+
): Promise<Record<string, unknown>[]> {
|
|
106
|
+
return executePlainWithContexts(session.getExecutionContext(), qb);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Executes a hydrated query using execution and hydration contexts.
|
|
111
|
+
* @template TTable - The table type
|
|
112
|
+
* @param _execCtx - The execution context (unused)
|
|
113
|
+
* @param hydCtx - The hydration context
|
|
84
114
|
* @param qb - The select query builder
|
|
85
115
|
* @returns Promise resolving to array of entity instances
|
|
86
116
|
*/
|
|
@@ -96,6 +126,20 @@ export async function executeHydratedWithContexts<TTable extends TableDef>(
|
|
|
96
126
|
return executeWithContexts(execCtx, entityCtx, qb);
|
|
97
127
|
}
|
|
98
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Executes a hydrated query using execution context and returns plain row objects.
|
|
131
|
+
* @template TTable - The table type
|
|
132
|
+
* @param execCtx - The execution context
|
|
133
|
+
* @param qb - The select query builder
|
|
134
|
+
* @returns Promise resolving to array of plain row objects
|
|
135
|
+
*/
|
|
136
|
+
export async function executeHydratedPlainWithContexts<TTable extends TableDef>(
|
|
137
|
+
execCtx: ExecutionContext,
|
|
138
|
+
qb: SelectQueryBuilder<unknown, TTable>
|
|
139
|
+
): Promise<Record<string, unknown>[]> {
|
|
140
|
+
return executePlainWithContexts(execCtx, qb);
|
|
141
|
+
}
|
|
142
|
+
|
|
99
143
|
const loadLazyRelationsForTable = async <TTable extends TableDef>(
|
|
100
144
|
ctx: EntityContext,
|
|
101
145
|
table: TTable,
|