metal-orm 1.0.71 → 1.0.73

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metal-orm",
3
- "version": "1.0.71",
3
+ "version": "1.0.73",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "engines": {
@@ -125,16 +125,16 @@ const instantiateWrapper = <TTable extends TableDef>(
125
125
  createEntity: RelationEntityFactory
126
126
  ): HasManyCollection<unknown> | HasOneReference<object> | BelongsToReference<object> | ManyToManyCollection<unknown> | undefined => {
127
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);
128
+ const loadCached = <T extends Map<string, unknown>>(factory: () => Promise<T>) =>
129
+ relationLoaderCache(metaBase, relationName, factory);
130
+ const resolveOptions = () => meta.lazyRelationOptions.get(relationName);
131
131
  switch (relation.type) {
132
132
  case RelationKinds.HasOne: {
133
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
- );
134
+ const localKey = hasOne.localKey || findPrimaryKey(meta.table);
135
+ const loader = () => loadCached(() =>
136
+ loadHasOneRelation(meta.ctx, meta.table, relationName, hasOne, resolveOptions())
137
+ );
138
138
  return new DefaultHasOneReference(
139
139
  meta.ctx,
140
140
  metaBase,
@@ -149,10 +149,10 @@ const instantiateWrapper = <TTable extends TableDef>(
149
149
  }
150
150
  case RelationKinds.HasMany: {
151
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
- );
152
+ const localKey = hasMany.localKey || findPrimaryKey(meta.table);
153
+ const loader = () => loadCached(() =>
154
+ loadHasManyRelation(meta.ctx, meta.table, relationName, hasMany, resolveOptions())
155
+ );
156
156
  return new DefaultHasManyCollection(
157
157
  meta.ctx,
158
158
  metaBase,
@@ -167,10 +167,10 @@ const instantiateWrapper = <TTable extends TableDef>(
167
167
  }
168
168
  case RelationKinds.BelongsTo: {
169
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
- );
170
+ const targetKey = belongsTo.localKey || findPrimaryKey(belongsTo.target);
171
+ const loader = () => loadCached(() =>
172
+ loadBelongsToRelation(meta.ctx, meta.table, relationName, belongsTo, resolveOptions())
173
+ );
174
174
  return new DefaultBelongsToReference(
175
175
  meta.ctx,
176
176
  metaBase,
@@ -185,10 +185,10 @@ const instantiateWrapper = <TTable extends TableDef>(
185
185
  }
186
186
  case RelationKinds.BelongsToMany: {
187
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
- );
188
+ const localKey = many.localKey || findPrimaryKey(meta.table);
189
+ const loader = () => loadCached(() =>
190
+ loadBelongsToManyRelation(meta.ctx, meta.table, relationName, many, resolveOptions())
191
+ );
192
192
  return new DefaultManyToManyCollection(
193
193
  meta.ctx,
194
194
  metaBase,
package/src/orm/entity.ts CHANGED
@@ -7,13 +7,20 @@ import { RelationIncludeOptions } from '../query-builder/relation-types.js';
7
7
  import { populateHydrationCache } from './entity-hydration.js';
8
8
  import { getRelationWrapper, RelationEntityFactory } from './entity-relations.js';
9
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
10
+ export { relationLoaderCache } from './entity-relation-cache.js';
11
+
12
+ const isRelationWrapperLoaded = (value: unknown): boolean => {
13
+ if (!value || typeof value !== 'object') return false;
14
+ return Boolean((value as { loaded?: boolean }).loaded);
15
+ };
16
+
17
+ type JsonSource<TTable extends TableDef> = EntityInstance<TTable> & Record<string, unknown>;
18
+
19
+ /**
20
+ * Creates an entity proxy with lazy loading capabilities.
21
+ * @template TTable - The table type
22
+ * @template TLazy - The lazy relation keys
23
+ * @param ctx - The entity context
17
24
  * @param table - The table definition
18
25
  * @param row - The database row
19
26
  * @param lazyRelations - Optional lazy relations
@@ -28,26 +35,39 @@ export const createEntityProxy = <
28
35
  row: Record<string, unknown>,
29
36
  lazyRelations: TLazy[] = [] as TLazy[],
30
37
  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,
38
+ ): EntityInstance<TTable> => {
39
+ const target: Record<string, unknown> = { ...row };
40
+ const meta: EntityMeta<TTable> = {
41
+ ctx,
42
+ table,
36
43
  lazyRelations: [...lazyRelations],
37
44
  lazyRelationOptions: new Map(lazyRelationOptions),
38
45
  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
- });
46
+ relationHydration: new Map(),
47
+ relationWrappers: new Map()
48
+ };
49
+ const createRelationEntity: RelationEntityFactory = (relationTable, relationRow) =>
50
+ createEntityFromRow(meta.ctx, relationTable, relationRow);
51
+
52
+ const buildJson = (self: JsonSource<TTable>): Record<string, unknown> => {
53
+ const json: Record<string, unknown> = {};
54
+ for (const key of Object.keys(target)) {
55
+ json[key] = self[key];
56
+ }
57
+ for (const [relationName, wrapper] of meta.relationWrappers) {
58
+ if (Object.prototype.hasOwnProperty.call(json, relationName)) continue;
59
+ if (isRelationWrapperLoaded(wrapper)) {
60
+ json[relationName] = wrapper;
61
+ }
62
+ }
63
+ return json;
64
+ };
65
+
66
+ Object.defineProperty(target, ENTITY_META, {
67
+ value: meta,
68
+ enumerable: false,
69
+ writable: false
70
+ });
51
71
 
52
72
  const handler: ProxyHandler<object> = {
53
73
  get(targetObj, prop, receiver) {
@@ -55,21 +75,28 @@ export const createEntityProxy = <
55
75
  return meta;
56
76
  }
57
77
 
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);
78
+ if (prop === '$load') {
79
+ return async (relationName: RelationKey<TTable>) => {
80
+ const wrapper = getRelationWrapper(meta, relationName, receiver, createRelationEntity);
81
+ if (wrapper && typeof wrapper.load === 'function') {
82
+ return wrapper.load();
83
+ }
84
+ return undefined;
85
+ };
86
+ }
87
+
88
+ if (prop === 'toJSON') {
89
+ if (prop in targetObj) {
90
+ return Reflect.get(targetObj, prop, receiver);
91
+ }
92
+ return () => buildJson(receiver as JsonSource<TTable>);
93
+ }
94
+
95
+ if (typeof prop === 'string' && table.relations[prop]) {
96
+ return getRelationWrapper(meta, prop as RelationKey<TTable>, receiver, createRelationEntity);
97
+ }
98
+
99
+ return Reflect.get(targetObj, prop, receiver);
73
100
  },
74
101
 
75
102
  set(targetObj, prop, value, receiver) {
@@ -77,14 +104,14 @@ export const createEntityProxy = <
77
104
  if (typeof prop === 'string' && table.columns[prop]) {
78
105
  ctx.markDirty(receiver);
79
106
  }
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
- };
107
+ return result;
108
+ }
109
+ };
110
+
111
+ const proxy = new Proxy(target, handler) as EntityInstance<TTable>;
112
+ populateHydrationCache(proxy, row, meta);
113
+ return proxy;
114
+ };
88
115
 
89
116
  /**
90
117
  * Creates an entity instance from a database row.
@@ -12,8 +12,9 @@ import {
12
12
  import { EntityContext } from './entity-context.js';
13
13
  import { ExecutionContext } from './execution-context.js';
14
14
  import { HydrationContext } from './hydration-context.js';
15
- import { RelationIncludeOptions } from '../query-builder/relation-types.js';
16
- import { getEntityMeta, RelationKey } from './entity-meta.js';
15
+ import { RelationIncludeOptions } from '../query-builder/relation-types.js';
16
+ import { getEntityMeta, RelationKey } from './entity-meta.js';
17
+ import { preloadRelationIncludes } from './relation-preload.js';
17
18
  import {
18
19
  loadHasManyRelation,
19
20
  loadHasOneRelation,
@@ -44,23 +45,26 @@ const executeWithContexts = async <TTable extends TableDef>(
44
45
  qb: SelectQueryBuilder<unknown, TTable>
45
46
  ): Promise<EntityInstance<TTable>[]> => {
46
47
  const ast = qb.getAST();
47
- const compiled = execCtx.dialect.compileSelect(ast);
48
- const executed = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
49
- const rows = flattenResults(executed);
50
- const lazyRelations = qb.getLazyRelations() as RelationKey<TTable>[];
51
- const lazyRelationOptions = qb.getLazyRelationOptions();
52
-
53
- if (ast.setOps && ast.setOps.length > 0) {
54
- const proxies = rows.map(row => createEntityProxy(entityCtx, qb.getTable(), row, lazyRelations, lazyRelationOptions));
55
- await loadLazyRelationsForTable(entityCtx, qb.getTable(), lazyRelations, lazyRelationOptions);
56
- return proxies;
57
- }
58
-
59
- const hydrated = hydrateRows(rows, qb.getHydrationPlan());
60
- const entities = hydrated.map(row => createEntityFromRow(entityCtx, qb.getTable(), row, lazyRelations, lazyRelationOptions));
61
- await loadLazyRelationsForTable(entityCtx, qb.getTable(), lazyRelations, lazyRelationOptions);
62
- return entities;
63
- };
48
+ const compiled = execCtx.dialect.compileSelect(ast);
49
+ const executed = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
50
+ const rows = flattenResults(executed);
51
+ const lazyRelations = qb.getLazyRelations() as RelationKey<TTable>[];
52
+ const lazyRelationOptions = qb.getLazyRelationOptions();
53
+ const includeTree = qb.getIncludeTree();
54
+
55
+ if (ast.setOps && ast.setOps.length > 0) {
56
+ const proxies = rows.map(row => createEntityProxy(entityCtx, qb.getTable(), row, lazyRelations, lazyRelationOptions));
57
+ await loadLazyRelationsForTable(entityCtx, qb.getTable(), lazyRelations, lazyRelationOptions);
58
+ await preloadRelationIncludes(proxies as Record<string, unknown>[], includeTree);
59
+ return proxies;
60
+ }
61
+
62
+ const hydrated = hydrateRows(rows, qb.getHydrationPlan());
63
+ const entities = hydrated.map(row => createEntityFromRow(entityCtx, qb.getTable(), row, lazyRelations, lazyRelationOptions));
64
+ await loadLazyRelationsForTable(entityCtx, qb.getTable(), lazyRelations, lazyRelationOptions);
65
+ await preloadRelationIncludes(entities as Record<string, unknown>[], includeTree);
66
+ return entities;
67
+ };
64
68
 
65
69
  const executePlainWithContexts = async <TTable extends TableDef>(
66
70
  execCtx: ExecutionContext,
@@ -0,0 +1,82 @@
1
+ import type { NormalizedRelationIncludeTree } from '../query-builder/relation-include-tree.js';
2
+ import type { RelationIncludeOptions } from '../query-builder/relation-types.js';
3
+ import { getEntityMeta } from './entity-meta.js';
4
+
5
+ type LoadableRelation = {
6
+ load?: () => Promise<unknown>;
7
+ getItems?: () => unknown;
8
+ get?: () => unknown;
9
+ };
10
+
11
+ const collectEntities = (value: unknown): Record<string, unknown>[] => {
12
+ if (!value) return [];
13
+ if (Array.isArray(value)) {
14
+ return value.filter(item => item && typeof item === 'object') as Record<string, unknown>[];
15
+ }
16
+ if (typeof value === 'object') {
17
+ return [value as Record<string, unknown>];
18
+ }
19
+ return [];
20
+ };
21
+
22
+ const loadRelation = async (
23
+ entity: Record<string, unknown>,
24
+ relationName: string
25
+ ): Promise<Record<string, unknown>[]> => {
26
+ const wrapper = entity[relationName] as LoadableRelation | undefined;
27
+ if (!wrapper) return [];
28
+
29
+ if (typeof wrapper.load === 'function') {
30
+ const loaded = await wrapper.load();
31
+ return collectEntities(loaded);
32
+ }
33
+
34
+ if (typeof wrapper.getItems === 'function') {
35
+ return collectEntities(wrapper.getItems());
36
+ }
37
+
38
+ if (typeof wrapper.get === 'function') {
39
+ return collectEntities(wrapper.get());
40
+ }
41
+
42
+ return collectEntities(wrapper);
43
+ };
44
+
45
+ const setLazyOptionsIfEmpty = (
46
+ entity: Record<string, unknown>,
47
+ relationName: string,
48
+ options?: RelationIncludeOptions
49
+ ): void => {
50
+ if (!options) return;
51
+ const meta = getEntityMeta(entity);
52
+ if (!meta || meta.lazyRelationOptions.has(relationName)) return;
53
+ meta.lazyRelationOptions.set(relationName, options);
54
+ };
55
+
56
+ export const preloadRelationIncludes = async (
57
+ entities: Record<string, unknown>[],
58
+ includeTree: NormalizedRelationIncludeTree,
59
+ depth = 0
60
+ ): Promise<void> => {
61
+ if (!entities.length) return;
62
+ const entries = Object.entries(includeTree);
63
+ if (!entries.length) return;
64
+
65
+ for (const [relationName, node] of entries) {
66
+ const shouldLoad = depth > 0 || Boolean(node.include);
67
+ if (!shouldLoad) continue;
68
+
69
+ for (const entity of entities) {
70
+ setLazyOptionsIfEmpty(entity, relationName, node.options);
71
+ }
72
+
73
+ const loaded = await Promise.all(
74
+ entities.map(entity => loadRelation(entity, relationName))
75
+ );
76
+ const relatedEntities = loaded.flat();
77
+
78
+ if (node.include && relatedEntities.length) {
79
+ await preloadRelationIncludes(relatedEntities, node.include, depth + 1);
80
+ }
81
+ }
82
+ };
@@ -0,0 +1,98 @@
1
+ import type { TableDef } from '../schema/table.js';
2
+ import type { RelationDef } from '../schema/relation.js';
3
+ import type { RelationMap, RelationTargetTable } from '../schema/types.js';
4
+ import type { RelationIncludeOptions, TypedRelationIncludeOptions } from './relation-types.js';
5
+
6
+ export type RelationIncludeInput<TTable extends TableDef> = {
7
+ [K in keyof RelationMap<TTable> & string]?: true | RelationIncludeNodeInput<TTable['relations'][K]>;
8
+ };
9
+
10
+ export type RelationIncludeNodeInput<TRel extends RelationDef> =
11
+ TypedRelationIncludeOptions<TRel> & {
12
+ include?: RelationIncludeInput<RelationTargetTable<TRel>>;
13
+ };
14
+
15
+ export type NormalizedRelationIncludeNode = {
16
+ options?: RelationIncludeOptions;
17
+ include?: NormalizedRelationIncludeTree;
18
+ };
19
+
20
+ export type NormalizedRelationIncludeTree = Record<string, NormalizedRelationIncludeNode>;
21
+
22
+ const isObject = (value: unknown): value is Record<string, unknown> =>
23
+ Boolean(value && typeof value === 'object');
24
+
25
+ export const normalizeRelationIncludeNode = <TRel extends RelationDef>(
26
+ value?: true | RelationIncludeNodeInput<TRel>
27
+ ): NormalizedRelationIncludeNode => {
28
+ if (!value || value === true) {
29
+ return {};
30
+ }
31
+
32
+ if (!isObject(value)) {
33
+ return {};
34
+ }
35
+
36
+ const { include, ...rest } = value as Record<string, unknown>;
37
+ const options = Object.keys(rest).length ? (rest as RelationIncludeOptions) : undefined;
38
+ const normalizedInclude = isObject(include)
39
+ ? normalizeRelationInclude(include as RelationIncludeInput<TableDef>)
40
+ : undefined;
41
+
42
+ if (normalizedInclude && Object.keys(normalizedInclude).length > 0) {
43
+ return { options, include: normalizedInclude };
44
+ }
45
+
46
+ return { options };
47
+ };
48
+
49
+ export const normalizeRelationInclude = (
50
+ input?: RelationIncludeInput<TableDef>
51
+ ): NormalizedRelationIncludeTree => {
52
+ if (!input) return {};
53
+
54
+ const tree: NormalizedRelationIncludeTree = {};
55
+ for (const [key, value] of Object.entries(input)) {
56
+ tree[key] = normalizeRelationIncludeNode(value as RelationIncludeNodeInput<RelationDef> | true);
57
+ }
58
+ return tree;
59
+ };
60
+
61
+ export const mergeRelationIncludeTrees = (
62
+ base: NormalizedRelationIncludeTree,
63
+ next: NormalizedRelationIncludeTree
64
+ ): NormalizedRelationIncludeTree => {
65
+ const merged: NormalizedRelationIncludeTree = { ...base };
66
+
67
+ for (const [key, node] of Object.entries(next)) {
68
+ const existing = merged[key];
69
+ if (!existing) {
70
+ merged[key] = node;
71
+ continue;
72
+ }
73
+
74
+ const include = existing.include && node.include
75
+ ? mergeRelationIncludeTrees(existing.include, node.include)
76
+ : (node.include ?? existing.include);
77
+
78
+ merged[key] = {
79
+ options: node.options ?? existing.options,
80
+ ...(include ? { include } : {})
81
+ };
82
+ }
83
+
84
+ return merged;
85
+ };
86
+
87
+ export const cloneRelationIncludeTree = (
88
+ tree: NormalizedRelationIncludeTree
89
+ ): NormalizedRelationIncludeTree => {
90
+ const cloned: NormalizedRelationIncludeTree = {};
91
+ for (const [key, node] of Object.entries(tree)) {
92
+ cloned[key] = {
93
+ options: node.options,
94
+ ...(node.include ? { include: cloneRelationIncludeTree(node.include) } : {})
95
+ };
96
+ }
97
+ return cloned;
98
+ };