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.
Files changed (46) hide show
  1. package/README.md +23 -13
  2. package/dist/index.cjs +1750 -733
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +244 -157
  5. package/dist/index.d.ts +244 -157
  6. package/dist/index.js +1745 -733
  7. package/dist/index.js.map +1 -1
  8. package/package.json +69 -69
  9. package/src/core/ddl/schema-generator.ts +44 -1
  10. package/src/decorators/bootstrap.ts +186 -113
  11. package/src/decorators/column-decorator.ts +8 -49
  12. package/src/decorators/decorator-metadata.ts +10 -46
  13. package/src/decorators/entity.ts +30 -40
  14. package/src/decorators/relations.ts +30 -56
  15. package/src/orm/entity-hydration.ts +72 -0
  16. package/src/orm/entity-meta.ts +18 -13
  17. package/src/orm/entity-metadata.ts +240 -238
  18. package/src/orm/entity-relation-cache.ts +39 -0
  19. package/src/orm/entity-relations.ts +207 -0
  20. package/src/orm/entity.ts +124 -343
  21. package/src/orm/execute.ts +87 -20
  22. package/src/orm/lazy-batch/belongs-to-many.ts +134 -0
  23. package/src/orm/lazy-batch/belongs-to.ts +108 -0
  24. package/src/orm/lazy-batch/has-many.ts +69 -0
  25. package/src/orm/lazy-batch/has-one.ts +68 -0
  26. package/src/orm/lazy-batch/shared.ts +125 -0
  27. package/src/orm/lazy-batch.ts +4 -309
  28. package/src/orm/relations/belongs-to.ts +2 -2
  29. package/src/orm/relations/has-many.ts +23 -9
  30. package/src/orm/relations/has-one.ts +2 -2
  31. package/src/orm/relations/many-to-many.ts +29 -14
  32. package/src/orm/save-graph-types.ts +2 -2
  33. package/src/orm/save-graph.ts +18 -18
  34. package/src/query-builder/relation-conditions.ts +80 -59
  35. package/src/query-builder/relation-cte-builder.ts +63 -0
  36. package/src/query-builder/relation-filter-utils.ts +159 -0
  37. package/src/query-builder/relation-include-strategies.ts +177 -0
  38. package/src/query-builder/relation-join-planner.ts +80 -0
  39. package/src/query-builder/relation-service.ts +103 -159
  40. package/src/query-builder/relation-types.ts +43 -12
  41. package/src/query-builder/select/projection-facet.ts +23 -23
  42. package/src/query-builder/select/select-operations.ts +145 -0
  43. package/src/query-builder/select.ts +373 -426
  44. package/src/schema/relation.ts +22 -18
  45. package/src/schema/table.ts +22 -9
  46. 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, 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
- import { findPrimaryKey } from '../query-builder/hydration-planner.js';
12
-
13
- /**
14
- * Type representing an array of database rows.
15
- */
16
- type Rows = Record<string, unknown>[];
17
-
18
- /**
19
- * Caches relation loader results across entities of the same type.
20
- * @template T - The cache type
21
- * @param meta - The entity metadata
22
- * @param relationName - The relation name
23
- * @param factory - The factory function to create the cache
24
- * @returns Promise with the cached relation data
25
- */
26
- const relationLoaderCache = <T extends Map<string, unknown>>(
27
- meta: EntityMeta<TableDef>,
28
- relationName: string,
29
- factory: () => Promise<T>
30
- ): Promise<T> => {
31
- if (meta.relationCache.has(relationName)) {
32
- return meta.relationCache.get(relationName)! as Promise<T>;
33
- }
34
-
35
- const promise = factory().then(value => {
36
- for (const tracked of meta.ctx.getEntitiesForTable(meta.table)) {
37
- const otherMeta = getEntityMeta(tracked.entity);
38
- if (!otherMeta) continue;
39
- otherMeta.relationHydration.set(relationName, value);
40
- }
41
- return value;
42
- });
43
-
44
- meta.relationCache.set(relationName, promise);
45
-
46
- for (const tracked of meta.ctx.getEntitiesForTable(meta.table)) {
47
- const otherMeta = getEntityMeta(tracked.entity);
48
- if (!otherMeta) continue;
49
- otherMeta.relationCache.set(relationName, promise);
50
- }
51
-
52
- return promise;
53
- };
54
-
55
- /**
56
- * Creates an entity proxy with lazy loading capabilities.
57
- * @template TTable - The table type
58
- * @template TLazy - The lazy relation keys
59
- * @param ctx - The entity context
60
- * @param table - The table definition
61
- * @param row - The database row
62
- * @param lazyRelations - Optional lazy relations
63
- * @returns The entity instance
64
- */
65
- export const createEntityProxy = <
66
- TTable extends TableDef,
67
- TLazy extends keyof RelationMap<TTable> = keyof RelationMap<TTable>
68
- >(
69
- ctx: EntityContext,
70
- table: TTable,
71
- row: Record<string, unknown>,
72
- lazyRelations: TLazy[] = [] as TLazy[]
73
- ): EntityInstance<TTable> => {
74
- const target: Record<string, unknown> = { ...row };
75
- const meta: EntityMeta<TTable> = {
76
- ctx,
77
- table,
78
- lazyRelations: [...lazyRelations],
79
- relationCache: new Map(),
80
- relationHydration: new Map(),
81
- relationWrappers: new Map()
82
- };
83
-
84
- Object.defineProperty(target, ENTITY_META, {
85
- value: meta,
86
- enumerable: false,
87
- writable: false
88
- });
89
-
90
- const handler: ProxyHandler<object> = {
91
- get(targetObj, prop, receiver) {
92
- if (prop === ENTITY_META) {
93
- return meta;
94
- }
95
-
96
- if (prop === '$load') {
97
- return async (relationName: keyof RelationMap<TTable>) => {
98
- const wrapper = getRelationWrapper(meta as unknown as EntityMeta<TableDef>, relationName as string, receiver);
99
- if (wrapper && typeof wrapper.load === 'function') {
100
- return wrapper.load();
101
- }
102
- return undefined;
103
- };
104
- }
105
-
106
- if (typeof prop === 'string' && table.relations[prop]) {
107
- return getRelationWrapper(meta as unknown as EntityMeta<TableDef>, prop, receiver);
108
- }
109
-
110
- return Reflect.get(targetObj, prop, receiver);
111
- },
112
-
113
- set(targetObj, prop, value, receiver) {
114
- const result = Reflect.set(targetObj, prop, value, receiver);
115
- if (typeof prop === 'string' && table.columns[prop]) {
116
- ctx.markDirty(receiver);
117
- }
118
- return result;
119
- }
120
- };
121
-
122
- const proxy = new Proxy(target, handler) as EntityInstance<TTable>;
123
- populateHydrationCache(proxy, row, meta);
124
- return proxy;
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
+ };