metal-orm 1.0.57 → 1.0.59
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 +23 -13
- package/dist/index.cjs +1750 -733
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +244 -157
- package/dist/index.d.ts +244 -157
- package/dist/index.js +1745 -733
- package/dist/index.js.map +1 -1
- package/package.json +69 -69
- package/src/core/ddl/schema-generator.ts +44 -1
- package/src/decorators/bootstrap.ts +186 -113
- package/src/decorators/column-decorator.ts +8 -49
- package/src/decorators/decorator-metadata.ts +10 -46
- package/src/decorators/entity.ts +30 -40
- package/src/decorators/relations.ts +30 -56
- package/src/orm/entity-hydration.ts +72 -0
- package/src/orm/entity-meta.ts +18 -13
- package/src/orm/entity-metadata.ts +240 -238
- package/src/orm/entity-relation-cache.ts +39 -0
- package/src/orm/entity-relations.ts +207 -0
- package/src/orm/entity.ts +124 -343
- package/src/orm/execute.ts +87 -20
- package/src/orm/lazy-batch/belongs-to-many.ts +134 -0
- package/src/orm/lazy-batch/belongs-to.ts +108 -0
- package/src/orm/lazy-batch/has-many.ts +69 -0
- package/src/orm/lazy-batch/has-one.ts +68 -0
- package/src/orm/lazy-batch/shared.ts +125 -0
- package/src/orm/lazy-batch.ts +4 -309
- package/src/orm/relations/belongs-to.ts +2 -2
- package/src/orm/relations/has-many.ts +23 -9
- package/src/orm/relations/has-one.ts +2 -2
- package/src/orm/relations/many-to-many.ts +29 -14
- package/src/orm/save-graph-types.ts +2 -2
- package/src/orm/save-graph.ts +18 -18
- package/src/query-builder/relation-conditions.ts +80 -59
- package/src/query-builder/relation-cte-builder.ts +63 -0
- package/src/query-builder/relation-filter-utils.ts +159 -0
- package/src/query-builder/relation-include-strategies.ts +177 -0
- package/src/query-builder/relation-join-planner.ts +80 -0
- package/src/query-builder/relation-service.ts +103 -159
- package/src/query-builder/relation-types.ts +43 -12
- package/src/query-builder/select/projection-facet.ts +23 -23
- package/src/query-builder/select/select-operations.ts +145 -0
- package/src/query-builder/select.ts +373 -426
- package/src/schema/relation.ts +22 -18
- package/src/schema/table.ts +22 -9
- package/src/schema/types.ts +103 -84
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { TableDef } from '../schema/table.js';
|
|
2
|
+
import { EntityInstance, HasManyCollection, HasOneReference, BelongsToReference, ManyToManyCollection } from '../schema/types.js';
|
|
3
|
+
import { EntityMeta, RelationKey } from './entity-meta.js';
|
|
4
|
+
import { DefaultHasManyCollection } from './relations/has-many.js';
|
|
5
|
+
import { DefaultHasOneReference } from './relations/has-one.js';
|
|
6
|
+
import { DefaultBelongsToReference } from './relations/belongs-to.js';
|
|
7
|
+
import { DefaultManyToManyCollection } from './relations/many-to-many.js';
|
|
8
|
+
import { HasManyRelation, HasOneRelation, BelongsToRelation, BelongsToManyRelation, RelationKinds } from '../schema/relation.js';
|
|
9
|
+
import { loadHasManyRelation, loadHasOneRelation, loadBelongsToRelation, loadBelongsToManyRelation } from './lazy-batch.js';
|
|
10
|
+
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
11
|
+
import { relationLoaderCache } from './entity-relation-cache.js';
|
|
12
|
+
|
|
13
|
+
export type RelationEntityFactory = (
|
|
14
|
+
table: TableDef,
|
|
15
|
+
row: Record<string, unknown>
|
|
16
|
+
) => EntityInstance<TableDef>;
|
|
17
|
+
|
|
18
|
+
const proxifyRelationWrapper = <T extends object>(wrapper: T): T => {
|
|
19
|
+
return new Proxy(wrapper, {
|
|
20
|
+
get(target, prop, receiver) {
|
|
21
|
+
if (typeof prop === 'symbol') {
|
|
22
|
+
return Reflect.get(target, prop, receiver);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (prop in target) {
|
|
26
|
+
return Reflect.get(target, prop, receiver);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const getItems = (target as { getItems?: () => unknown }).getItems;
|
|
30
|
+
if (typeof getItems === 'function') {
|
|
31
|
+
const items = getItems.call(target);
|
|
32
|
+
if (items && prop in (items as object)) {
|
|
33
|
+
const propName = prop as string;
|
|
34
|
+
const value = (items as Record<string, unknown>)[propName];
|
|
35
|
+
return typeof value === 'function' ? value.bind(items) : value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const getRef = (target as { get?: () => unknown }).get;
|
|
40
|
+
if (typeof getRef === 'function') {
|
|
41
|
+
const current = getRef.call(target);
|
|
42
|
+
if (current && prop in (current as object)) {
|
|
43
|
+
const propName = prop as string;
|
|
44
|
+
const value = (current as Record<string, unknown>)[propName];
|
|
45
|
+
return typeof value === 'function' ? value.bind(current) : value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return undefined;
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
set(target, prop, value, receiver) {
|
|
53
|
+
if (typeof prop === 'symbol') {
|
|
54
|
+
return Reflect.set(target, prop, value, receiver);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (prop in target) {
|
|
58
|
+
return Reflect.set(target, prop, value, receiver);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const getRef = (target as { get?: () => unknown }).get;
|
|
62
|
+
if (typeof getRef === 'function') {
|
|
63
|
+
const current = getRef.call(target);
|
|
64
|
+
if (current && typeof current === 'object') {
|
|
65
|
+
return Reflect.set(current as object, prop, value);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const getItems = (target as { getItems?: () => unknown }).getItems;
|
|
70
|
+
if (typeof getItems === 'function') {
|
|
71
|
+
const items = getItems.call(target);
|
|
72
|
+
return Reflect.set(items as object, prop, value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return Reflect.set(target, prop, value, receiver);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Gets a relation wrapper for an entity.
|
|
82
|
+
* @param meta - The entity metadata
|
|
83
|
+
* @param relationName - The relation name
|
|
84
|
+
* @param owner - The owner entity
|
|
85
|
+
* @param createEntity - The entity factory for relation rows
|
|
86
|
+
* @returns The relation wrapper or undefined
|
|
87
|
+
*/
|
|
88
|
+
export const getRelationWrapper = <TTable extends TableDef>(
|
|
89
|
+
meta: EntityMeta<TTable>,
|
|
90
|
+
relationName: RelationKey<TTable> | string,
|
|
91
|
+
owner: unknown,
|
|
92
|
+
createEntity: RelationEntityFactory
|
|
93
|
+
): HasManyCollection<unknown> | HasOneReference<object> | BelongsToReference<object> | ManyToManyCollection<unknown> | undefined => {
|
|
94
|
+
const relationKey = relationName as string;
|
|
95
|
+
|
|
96
|
+
if (meta.relationWrappers.has(relationKey)) {
|
|
97
|
+
return meta.relationWrappers.get(relationKey) as HasManyCollection<unknown>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const relation = meta.table.relations[relationKey];
|
|
101
|
+
if (!relation) return undefined;
|
|
102
|
+
|
|
103
|
+
const wrapper = instantiateWrapper(meta, relationKey, relation, owner, createEntity);
|
|
104
|
+
if (!wrapper) return undefined;
|
|
105
|
+
|
|
106
|
+
const proxied = proxifyRelationWrapper(wrapper as object);
|
|
107
|
+
meta.relationWrappers.set(relationKey, proxied);
|
|
108
|
+
return proxied as HasManyCollection<unknown>;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Instantiates the appropriate relation wrapper based on relation type.
|
|
113
|
+
* @param meta - The entity metadata
|
|
114
|
+
* @param relationName - The relation name
|
|
115
|
+
* @param relation - The relation definition
|
|
116
|
+
* @param owner - The owner entity
|
|
117
|
+
* @param createEntity - The entity factory for relation rows
|
|
118
|
+
* @returns The relation wrapper or undefined
|
|
119
|
+
*/
|
|
120
|
+
const instantiateWrapper = <TTable extends TableDef>(
|
|
121
|
+
meta: EntityMeta<TTable>,
|
|
122
|
+
relationName: string,
|
|
123
|
+
relation: HasManyRelation | HasOneRelation | BelongsToRelation | BelongsToManyRelation,
|
|
124
|
+
owner: unknown,
|
|
125
|
+
createEntity: RelationEntityFactory
|
|
126
|
+
): HasManyCollection<unknown> | HasOneReference<object> | BelongsToReference<object> | ManyToManyCollection<unknown> | undefined => {
|
|
127
|
+
const metaBase = meta as unknown as EntityMeta<TableDef>;
|
|
128
|
+
const lazyOptions = meta.lazyRelationOptions.get(relationName);
|
|
129
|
+
const loadCached = <T extends Map<string, unknown>>(factory: () => Promise<T>) =>
|
|
130
|
+
relationLoaderCache(metaBase, relationName, factory);
|
|
131
|
+
switch (relation.type) {
|
|
132
|
+
case RelationKinds.HasOne: {
|
|
133
|
+
const hasOne = relation as HasOneRelation;
|
|
134
|
+
const localKey = hasOne.localKey || findPrimaryKey(meta.table);
|
|
135
|
+
const loader = () => loadCached(() =>
|
|
136
|
+
loadHasOneRelation(meta.ctx, meta.table, relationName, hasOne, lazyOptions)
|
|
137
|
+
);
|
|
138
|
+
return new DefaultHasOneReference(
|
|
139
|
+
meta.ctx,
|
|
140
|
+
metaBase,
|
|
141
|
+
owner,
|
|
142
|
+
relationName,
|
|
143
|
+
hasOne,
|
|
144
|
+
meta.table,
|
|
145
|
+
loader,
|
|
146
|
+
(row: Record<string, unknown>) => createEntity(hasOne.target, row),
|
|
147
|
+
localKey
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
case RelationKinds.HasMany: {
|
|
151
|
+
const hasMany = relation as HasManyRelation;
|
|
152
|
+
const localKey = hasMany.localKey || findPrimaryKey(meta.table);
|
|
153
|
+
const loader = () => loadCached(() =>
|
|
154
|
+
loadHasManyRelation(meta.ctx, meta.table, relationName, hasMany, lazyOptions)
|
|
155
|
+
);
|
|
156
|
+
return new DefaultHasManyCollection(
|
|
157
|
+
meta.ctx,
|
|
158
|
+
metaBase,
|
|
159
|
+
owner,
|
|
160
|
+
relationName,
|
|
161
|
+
hasMany,
|
|
162
|
+
meta.table,
|
|
163
|
+
loader,
|
|
164
|
+
(row: Record<string, unknown>) => createEntity(relation.target, row),
|
|
165
|
+
localKey
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
case RelationKinds.BelongsTo: {
|
|
169
|
+
const belongsTo = relation as BelongsToRelation;
|
|
170
|
+
const targetKey = belongsTo.localKey || findPrimaryKey(belongsTo.target);
|
|
171
|
+
const loader = () => loadCached(() =>
|
|
172
|
+
loadBelongsToRelation(meta.ctx, meta.table, relationName, belongsTo, lazyOptions)
|
|
173
|
+
);
|
|
174
|
+
return new DefaultBelongsToReference(
|
|
175
|
+
meta.ctx,
|
|
176
|
+
metaBase,
|
|
177
|
+
owner,
|
|
178
|
+
relationName,
|
|
179
|
+
belongsTo,
|
|
180
|
+
meta.table,
|
|
181
|
+
loader,
|
|
182
|
+
(row: Record<string, unknown>) => createEntity(relation.target, row),
|
|
183
|
+
targetKey
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
case RelationKinds.BelongsToMany: {
|
|
187
|
+
const many = relation as BelongsToManyRelation;
|
|
188
|
+
const localKey = many.localKey || findPrimaryKey(meta.table);
|
|
189
|
+
const loader = () => loadCached(() =>
|
|
190
|
+
loadBelongsToManyRelation(meta.ctx, meta.table, relationName, many, lazyOptions)
|
|
191
|
+
);
|
|
192
|
+
return new DefaultManyToManyCollection(
|
|
193
|
+
meta.ctx,
|
|
194
|
+
metaBase,
|
|
195
|
+
owner,
|
|
196
|
+
relationName,
|
|
197
|
+
many,
|
|
198
|
+
meta.table,
|
|
199
|
+
loader,
|
|
200
|
+
(row: Record<string, unknown>) => createEntity(relation.target, row),
|
|
201
|
+
localKey
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
default:
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
};
|
package/src/orm/entity.ts
CHANGED
|
@@ -1,343 +1,124 @@
|
|
|
1
|
-
import { TableDef } from '../schema/table.js';
|
|
2
|
-
import { EntityInstance
|
|
3
|
-
import { EntityContext } from './entity-context.js';
|
|
4
|
-
import { ENTITY_META, EntityMeta,
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
* @
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Creates an entity instance from a database row.
|
|
129
|
-
* @template TTable - The table type
|
|
130
|
-
* @template TResult - The result type
|
|
131
|
-
* @param ctx - The entity context
|
|
132
|
-
* @param table - The table definition
|
|
133
|
-
* @param row - The database row
|
|
134
|
-
* @param lazyRelations - Optional lazy relations
|
|
135
|
-
* @returns The entity instance
|
|
136
|
-
*/
|
|
137
|
-
export const createEntityFromRow = <
|
|
138
|
-
TTable extends TableDef,
|
|
139
|
-
TResult extends EntityInstance<TTable> = EntityInstance<TTable>
|
|
140
|
-
>(
|
|
141
|
-
ctx: EntityContext,
|
|
142
|
-
table: TTable,
|
|
143
|
-
row: Record<string, unknown>,
|
|
144
|
-
lazyRelations: (keyof RelationMap<TTable>)[] = []
|
|
145
|
-
): TResult => {
|
|
146
|
-
const pkName = findPrimaryKey(table);
|
|
147
|
-
const pkValue = row[pkName];
|
|
148
|
-
if (pkValue !== undefined && pkValue !== null) {
|
|
149
|
-
const tracked = ctx.getEntity(table, pkValue);
|
|
150
|
-
if (tracked) return tracked as TResult;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const entity = createEntityProxy(ctx, table, row, lazyRelations);
|
|
154
|
-
if (pkValue !== undefined && pkValue !== null) {
|
|
155
|
-
ctx.trackManaged(table, pkValue, entity);
|
|
156
|
-
} else {
|
|
157
|
-
ctx.trackNew(table, entity);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return entity as TResult;
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Converts a value to a string key.
|
|
165
|
-
* @param value - The value to convert
|
|
166
|
-
* @returns String representation of the value
|
|
167
|
-
*/
|
|
168
|
-
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Populates the hydration cache with relation data from the database row.
|
|
172
|
-
* @template TTable - The table type
|
|
173
|
-
* @param entity - The entity instance
|
|
174
|
-
* @param row - The database row
|
|
175
|
-
* @param meta - The entity metadata
|
|
176
|
-
*/
|
|
177
|
-
const populateHydrationCache = <TTable extends TableDef>(
|
|
178
|
-
entity: Record<string, unknown>,
|
|
179
|
-
row: Record<string, unknown>,
|
|
180
|
-
meta: EntityMeta<TTable>
|
|
181
|
-
): void => {
|
|
182
|
-
for (const relationName of Object.keys(meta.table.relations)) {
|
|
183
|
-
const relation = meta.table.relations[relationName];
|
|
184
|
-
const data = row[relationName];
|
|
185
|
-
if (relation.type === RelationKinds.HasOne) {
|
|
186
|
-
const localKey = relation.localKey || findPrimaryKey(meta.table);
|
|
187
|
-
const rootValue = entity[localKey];
|
|
188
|
-
if (rootValue === undefined || rootValue === null) continue;
|
|
189
|
-
if (!data || typeof data !== 'object') continue;
|
|
190
|
-
const cache = new Map<string, Record<string, unknown>>();
|
|
191
|
-
cache.set(toKey(rootValue), data as Record<string, unknown>);
|
|
192
|
-
meta.relationHydration.set(relationName, cache);
|
|
193
|
-
meta.relationCache.set(relationName, Promise.resolve(cache));
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (!Array.isArray(data)) continue;
|
|
198
|
-
|
|
199
|
-
if (relation.type === RelationKinds.HasMany || relation.type === RelationKinds.BelongsToMany) {
|
|
200
|
-
const localKey = relation.localKey || findPrimaryKey(meta.table);
|
|
201
|
-
const rootValue = entity[localKey];
|
|
202
|
-
if (rootValue === undefined || rootValue === null) continue;
|
|
203
|
-
const cache = new Map<string, Rows>();
|
|
204
|
-
cache.set(toKey(rootValue), data as Rows);
|
|
205
|
-
meta.relationHydration.set(relationName, cache);
|
|
206
|
-
meta.relationCache.set(relationName, Promise.resolve(cache));
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (relation.type === RelationKinds.BelongsTo) {
|
|
211
|
-
const targetKey = relation.localKey || findPrimaryKey(relation.target);
|
|
212
|
-
const cache = new Map<string, Record<string, unknown>>();
|
|
213
|
-
for (const item of data) {
|
|
214
|
-
const pkValue = item[targetKey];
|
|
215
|
-
if (pkValue === undefined || pkValue === null) continue;
|
|
216
|
-
cache.set(toKey(pkValue), item);
|
|
217
|
-
}
|
|
218
|
-
if (cache.size) {
|
|
219
|
-
meta.relationHydration.set(relationName, cache);
|
|
220
|
-
meta.relationCache.set(relationName, Promise.resolve(cache));
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Gets a relation wrapper for an entity.
|
|
228
|
-
* @param meta - The entity metadata
|
|
229
|
-
* @param relationName - The relation name
|
|
230
|
-
* @param owner - The owner entity
|
|
231
|
-
* @returns The relation wrapper or undefined
|
|
232
|
-
*/
|
|
233
|
-
const getRelationWrapper = (
|
|
234
|
-
meta: EntityMeta<TableDef>,
|
|
235
|
-
relationName: string,
|
|
236
|
-
owner: unknown
|
|
237
|
-
): HasManyCollection<unknown> | HasOneReference<unknown> | BelongsToReference<unknown> | ManyToManyCollection<unknown> | undefined => {
|
|
238
|
-
if (meta.relationWrappers.has(relationName)) {
|
|
239
|
-
return meta.relationWrappers.get(relationName) as HasManyCollection<unknown>;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const relation = meta.table.relations[relationName];
|
|
243
|
-
if (!relation) return undefined;
|
|
244
|
-
|
|
245
|
-
const wrapper = instantiateWrapper(meta, relationName, relation, owner);
|
|
246
|
-
if (wrapper) {
|
|
247
|
-
meta.relationWrappers.set(relationName, wrapper);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return wrapper;
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Instantiates the appropriate relation wrapper based on relation type.
|
|
255
|
-
* @param meta - The entity metadata
|
|
256
|
-
* @param relationName - The relation name
|
|
257
|
-
* @param relation - The relation definition
|
|
258
|
-
* @param owner - The owner entity
|
|
259
|
-
* @returns The relation wrapper or undefined
|
|
260
|
-
*/
|
|
261
|
-
const instantiateWrapper = (
|
|
262
|
-
meta: EntityMeta<TableDef>,
|
|
263
|
-
relationName: string,
|
|
264
|
-
relation: HasManyRelation | HasOneRelation | BelongsToRelation | BelongsToManyRelation,
|
|
265
|
-
owner: unknown
|
|
266
|
-
): HasManyCollection<unknown> | HasOneReference<unknown> | BelongsToReference<unknown> | ManyToManyCollection<unknown> | undefined => {
|
|
267
|
-
switch (relation.type) {
|
|
268
|
-
case RelationKinds.HasOne: {
|
|
269
|
-
const hasOne = relation as HasOneRelation;
|
|
270
|
-
const localKey = hasOne.localKey || findPrimaryKey(meta.table);
|
|
271
|
-
const loader = () => relationLoaderCache(meta, relationName, () =>
|
|
272
|
-
loadHasOneRelation(meta.ctx, meta.table, relationName, hasOne)
|
|
273
|
-
);
|
|
274
|
-
return new DefaultHasOneReference(
|
|
275
|
-
meta.ctx,
|
|
276
|
-
meta,
|
|
277
|
-
owner,
|
|
278
|
-
relationName,
|
|
279
|
-
hasOne,
|
|
280
|
-
meta.table,
|
|
281
|
-
loader,
|
|
282
|
-
(row: Record<string, unknown>) => createEntityFromRow(meta.ctx, hasOne.target, row),
|
|
283
|
-
localKey
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
|
-
case RelationKinds.HasMany: {
|
|
287
|
-
const hasMany = relation as HasManyRelation;
|
|
288
|
-
const localKey = hasMany.localKey || findPrimaryKey(meta.table);
|
|
289
|
-
const loader = () => relationLoaderCache(meta, relationName, () =>
|
|
290
|
-
loadHasManyRelation(meta.ctx, meta.table, relationName, hasMany)
|
|
291
|
-
);
|
|
292
|
-
return new DefaultHasManyCollection(
|
|
293
|
-
meta.ctx,
|
|
294
|
-
meta,
|
|
295
|
-
owner,
|
|
296
|
-
relationName,
|
|
297
|
-
hasMany,
|
|
298
|
-
meta.table,
|
|
299
|
-
loader,
|
|
300
|
-
(row: Record<string, unknown>) => createEntityFromRow(meta.ctx, relation.target, row),
|
|
301
|
-
localKey
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
case RelationKinds.BelongsTo: {
|
|
305
|
-
const belongsTo = relation as BelongsToRelation;
|
|
306
|
-
const targetKey = belongsTo.localKey || findPrimaryKey(belongsTo.target);
|
|
307
|
-
const loader = () => relationLoaderCache(meta, relationName, () =>
|
|
308
|
-
loadBelongsToRelation(meta.ctx, meta.table, relationName, belongsTo)
|
|
309
|
-
);
|
|
310
|
-
return new DefaultBelongsToReference(
|
|
311
|
-
meta.ctx,
|
|
312
|
-
meta,
|
|
313
|
-
owner,
|
|
314
|
-
relationName,
|
|
315
|
-
belongsTo,
|
|
316
|
-
meta.table,
|
|
317
|
-
loader,
|
|
318
|
-
(row: Record<string, unknown>) => createEntityFromRow(meta.ctx, relation.target, row),
|
|
319
|
-
targetKey
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
case RelationKinds.BelongsToMany: {
|
|
323
|
-
const many = relation as BelongsToManyRelation;
|
|
324
|
-
const localKey = many.localKey || findPrimaryKey(meta.table);
|
|
325
|
-
const loader = () => relationLoaderCache(meta, relationName, () =>
|
|
326
|
-
loadBelongsToManyRelation(meta.ctx, meta.table, relationName, many)
|
|
327
|
-
);
|
|
328
|
-
return new DefaultManyToManyCollection(
|
|
329
|
-
meta.ctx,
|
|
330
|
-
meta,
|
|
331
|
-
owner,
|
|
332
|
-
relationName,
|
|
333
|
-
many,
|
|
334
|
-
meta.table,
|
|
335
|
-
loader,
|
|
336
|
-
(row: Record<string, unknown>) => createEntityFromRow(meta.ctx, relation.target, row),
|
|
337
|
-
localKey
|
|
338
|
-
);
|
|
339
|
-
}
|
|
340
|
-
default:
|
|
341
|
-
return undefined;
|
|
342
|
-
}
|
|
343
|
-
};
|
|
1
|
+
import { TableDef } from '../schema/table.js';
|
|
2
|
+
import { EntityInstance } from '../schema/types.js';
|
|
3
|
+
import { EntityContext } from './entity-context.js';
|
|
4
|
+
import { ENTITY_META, EntityMeta, RelationKey } from './entity-meta.js';
|
|
5
|
+
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
6
|
+
import { RelationIncludeOptions } from '../query-builder/relation-types.js';
|
|
7
|
+
import { populateHydrationCache } from './entity-hydration.js';
|
|
8
|
+
import { getRelationWrapper, RelationEntityFactory } from './entity-relations.js';
|
|
9
|
+
|
|
10
|
+
export { relationLoaderCache } from './entity-relation-cache.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates an entity proxy with lazy loading capabilities.
|
|
14
|
+
* @template TTable - The table type
|
|
15
|
+
* @template TLazy - The lazy relation keys
|
|
16
|
+
* @param ctx - The entity context
|
|
17
|
+
* @param table - The table definition
|
|
18
|
+
* @param row - The database row
|
|
19
|
+
* @param lazyRelations - Optional lazy relations
|
|
20
|
+
* @returns The entity instance
|
|
21
|
+
*/
|
|
22
|
+
export const createEntityProxy = <
|
|
23
|
+
TTable extends TableDef,
|
|
24
|
+
TLazy extends RelationKey<TTable> = RelationKey<TTable>
|
|
25
|
+
>(
|
|
26
|
+
ctx: EntityContext,
|
|
27
|
+
table: TTable,
|
|
28
|
+
row: Record<string, unknown>,
|
|
29
|
+
lazyRelations: TLazy[] = [] as TLazy[],
|
|
30
|
+
lazyRelationOptions: Map<string, RelationIncludeOptions> = new Map()
|
|
31
|
+
): EntityInstance<TTable> => {
|
|
32
|
+
const target: Record<string, unknown> = { ...row };
|
|
33
|
+
const meta: EntityMeta<TTable> = {
|
|
34
|
+
ctx,
|
|
35
|
+
table,
|
|
36
|
+
lazyRelations: [...lazyRelations],
|
|
37
|
+
lazyRelationOptions: new Map(lazyRelationOptions),
|
|
38
|
+
relationCache: new Map(),
|
|
39
|
+
relationHydration: new Map(),
|
|
40
|
+
relationWrappers: new Map()
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const createRelationEntity: RelationEntityFactory = (relationTable, relationRow) =>
|
|
44
|
+
createEntityFromRow(meta.ctx, relationTable, relationRow);
|
|
45
|
+
|
|
46
|
+
Object.defineProperty(target, ENTITY_META, {
|
|
47
|
+
value: meta,
|
|
48
|
+
enumerable: false,
|
|
49
|
+
writable: false
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const handler: ProxyHandler<object> = {
|
|
53
|
+
get(targetObj, prop, receiver) {
|
|
54
|
+
if (prop === ENTITY_META) {
|
|
55
|
+
return meta;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (prop === '$load') {
|
|
59
|
+
return async (relationName: RelationKey<TTable>) => {
|
|
60
|
+
const wrapper = getRelationWrapper(meta, relationName, receiver, createRelationEntity);
|
|
61
|
+
if (wrapper && typeof wrapper.load === 'function') {
|
|
62
|
+
return wrapper.load();
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (typeof prop === 'string' && table.relations[prop]) {
|
|
69
|
+
return getRelationWrapper(meta, prop as RelationKey<TTable>, receiver, createRelationEntity);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return Reflect.get(targetObj, prop, receiver);
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
set(targetObj, prop, value, receiver) {
|
|
76
|
+
const result = Reflect.set(targetObj, prop, value, receiver);
|
|
77
|
+
if (typeof prop === 'string' && table.columns[prop]) {
|
|
78
|
+
ctx.markDirty(receiver);
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const proxy = new Proxy(target, handler) as EntityInstance<TTable>;
|
|
85
|
+
populateHydrationCache(proxy, row, meta);
|
|
86
|
+
return proxy;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Creates an entity instance from a database row.
|
|
91
|
+
* @template TTable - The table type
|
|
92
|
+
* @template TResult - The result type
|
|
93
|
+
* @param ctx - The entity context
|
|
94
|
+
* @param table - The table definition
|
|
95
|
+
* @param row - The database row
|
|
96
|
+
* @param lazyRelations - Optional lazy relations
|
|
97
|
+
* @returns The entity instance
|
|
98
|
+
*/
|
|
99
|
+
export const createEntityFromRow = <
|
|
100
|
+
TTable extends TableDef,
|
|
101
|
+
TResult extends EntityInstance<TTable> = EntityInstance<TTable>
|
|
102
|
+
>(
|
|
103
|
+
ctx: EntityContext,
|
|
104
|
+
table: TTable,
|
|
105
|
+
row: Record<string, unknown>,
|
|
106
|
+
lazyRelations: RelationKey<TTable>[] = [],
|
|
107
|
+
lazyRelationOptions: Map<string, RelationIncludeOptions> = new Map()
|
|
108
|
+
): TResult => {
|
|
109
|
+
const pkName = findPrimaryKey(table);
|
|
110
|
+
const pkValue = row[pkName];
|
|
111
|
+
if (pkValue !== undefined && pkValue !== null) {
|
|
112
|
+
const tracked = ctx.getEntity(table, pkValue);
|
|
113
|
+
if (tracked) return tracked as TResult;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const entity = createEntityProxy(ctx, table, row, lazyRelations, lazyRelationOptions);
|
|
117
|
+
if (pkValue !== undefined && pkValue !== null) {
|
|
118
|
+
ctx.trackManaged(table, pkValue, entity);
|
|
119
|
+
} else {
|
|
120
|
+
ctx.trackNew(table, entity);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return entity as TResult;
|
|
124
|
+
};
|