metal-orm 1.0.64 → 1.0.66

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.64",
3
+ "version": "1.0.66",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "engines": {
@@ -1,4 +1,4 @@
1
- import { ColumnNode, FunctionNode } from './expression-nodes.js';
1
+ import { ColumnNode } from './expression-nodes.js';
2
2
  import { columnOperand, valueToOperand, ValueOperandInput } from './expression-builders.js';
3
3
  import { ColumnRef } from './types.js';
4
4
  import { OrderByNode } from './query.js';
@@ -5,7 +5,7 @@ import { OrderByNode } from './query.js';
5
5
  import { ColumnRef } from './types.js';
6
6
  import { TypedExpression, asType } from './expression.js';
7
7
 
8
- const buildWindowFunction = <T = any>(
8
+ const buildWindowFunction = <T = unknown>(
9
9
  name: string,
10
10
  args: (ColumnNode | LiteralNode | JsonPathNode)[] = [],
11
11
  partitionBy?: ColumnNode[],
@@ -66,7 +66,7 @@ export const ntile = (n: number): TypedExpression<number> =>
66
66
  * @param defaultValue - Optional default value.
67
67
  * @returns A `TypedExpression<T>` representing the `LAG` window function.
68
68
  */
69
- export const lag = <T = any>(
69
+ export const lag = <T = unknown>(
70
70
  col: ColumnRef | ColumnNode,
71
71
  offset: number = 1,
72
72
  defaultValue?: LiteralNode['value']
@@ -89,7 +89,7 @@ export const lag = <T = any>(
89
89
  * @param defaultValue - Optional default value.
90
90
  * @returns A `TypedExpression<T>` representing the `LEAD` window function.
91
91
  */
92
- export const lead = <T = any>(
92
+ export const lead = <T = unknown>(
93
93
  col: ColumnRef | ColumnNode,
94
94
  offset: number = 1,
95
95
  defaultValue?: LiteralNode['value']
@@ -110,7 +110,7 @@ export const lead = <T = any>(
110
110
  * @param col - Column or expression to get first value from.
111
111
  * @returns A `TypedExpression<T>` representing the `FIRST_VALUE` window function.
112
112
  */
113
- export const firstValue = <T = any>(col: ColumnRef | ColumnNode): TypedExpression<T> =>
113
+ export const firstValue = <T = unknown>(col: ColumnRef | ColumnNode): TypedExpression<T> =>
114
114
  buildWindowFunction<T>('FIRST_VALUE', [columnOperand(col)]);
115
115
 
116
116
  /**
@@ -119,7 +119,7 @@ export const firstValue = <T = any>(col: ColumnRef | ColumnNode): TypedExpressio
119
119
  * @param col - Column or expression to get last value from.
120
120
  * @returns A `TypedExpression<T>` representing the `LAST_VALUE` window function.
121
121
  */
122
- export const lastValue = <T = any>(col: ColumnRef | ColumnNode): TypedExpression<T> =>
122
+ export const lastValue = <T = unknown>(col: ColumnRef | ColumnNode): TypedExpression<T> =>
123
123
  buildWindowFunction<T>('LAST_VALUE', [columnOperand(col)]);
124
124
 
125
125
  /**
@@ -131,7 +131,7 @@ export const lastValue = <T = any>(col: ColumnRef | ColumnNode): TypedExpression
131
131
  * @param orderBy - Optional ORDER BY clauses.
132
132
  * @returns A `TypedExpression<T>` representing the window function.
133
133
  */
134
- export const windowFunction = <T = any>(
134
+ export const windowFunction = <T = unknown>(
135
135
  name: string,
136
136
  args: (ColumnRef | ColumnNode | LiteralNode | JsonPathNode)[] = [],
137
137
  partitionBy?: (ColumnRef | ColumnNode)[],
@@ -22,14 +22,14 @@ const fn = (key: string, args: OperandInput[]): FunctionNode => ({
22
22
  args: args.map(toOperand)
23
23
  });
24
24
 
25
- const afn = <T = any[]>(key: string, args: OperandInput[]): TypedExpression<T> => asType<T>(fn(key, args));
25
+ const afn = <T = unknown[]>(key: string, args: OperandInput[]): TypedExpression<T> => asType<T>(fn(key, args));
26
26
 
27
27
  /**
28
28
  * Appends a value to the end of an array.
29
29
  *
30
30
  * @param array - Array column or value.
31
31
  * @param value - Value to append.
32
- * @returns A `TypedExpression<any[]>` representing the `ARRAY_APPEND` SQL function.
32
+ * @returns A `TypedExpression<unknown[]>` representing the `ARRAY_APPEND` SQL function.
33
33
  */
34
- export const arrayAppend = (array: OperandInput, value: OperandInput): TypedExpression<any[]> =>
35
- afn('ARRAY_APPEND', [array, value]);
34
+ export const arrayAppend = (array: OperandInput, value: OperandInput): TypedExpression<unknown[]> =>
35
+ afn<unknown[]>('ARRAY_APPEND', [array, value]);
@@ -22,7 +22,7 @@ const fn = (key: string, args: OperandInput[]): FunctionNode => ({
22
22
  args: args.map(toOperand)
23
23
  });
24
24
 
25
- const afn = <T = any>(key: string, args: OperandInput[]): TypedExpression<T> => asType<T>(fn(key, args));
25
+ const afn = <T = unknown>(key: string, args: OperandInput[]): TypedExpression<T> => asType<T>(fn(key, args));
26
26
 
27
27
  /**
28
28
  * Returns the first non-null value in a list.
@@ -33,7 +33,7 @@ const afn = <T = any>(key: string, args: OperandInput[]): TypedExpression<T> =>
33
33
  * @example
34
34
  * coalesce(users.nickname, users.firstName, 'Guest');
35
35
  */
36
- export const coalesce = <T = any>(...args: OperandInput[]): TypedExpression<T> => {
36
+ export const coalesce = <T = unknown>(...args: OperandInput[]): TypedExpression<T> => {
37
37
  if (args.length < 2) throw new Error('coalesce() expects at least 2 arguments');
38
38
  return afn<T>('COALESCE', args);
39
39
  };
@@ -45,7 +45,7 @@ export const coalesce = <T = any>(...args: OperandInput[]): TypedExpression<T> =
45
45
  * @param val2 - The second value to compare against.
46
46
  * @returns A `TypedExpression<T>` representing the `NULLIF` SQL function.
47
47
  */
48
- export const nullif = <T = any>(val1: OperandInput, val2: OperandInput): TypedExpression<T> => afn<T>('NULLIF', [val1, val2]);
48
+ export const nullif = <T = unknown>(val1: OperandInput, val2: OperandInput): TypedExpression<T> => afn<T>('NULLIF', [val1, val2]);
49
49
 
50
50
  /**
51
51
  * Returns the largest value in a list.
@@ -53,7 +53,7 @@ export const nullif = <T = any>(val1: OperandInput, val2: OperandInput): TypedEx
53
53
  * @param args - The list of values or columns to compare.
54
54
  * @returns A `TypedExpression<T>` representing the `GREATEST` SQL function.
55
55
  */
56
- export const greatest = <T = any>(...args: OperandInput[]): TypedExpression<T> => {
56
+ export const greatest = <T = unknown>(...args: OperandInput[]): TypedExpression<T> => {
57
57
  if (args.length < 2) throw new Error('greatest() expects at least 2 arguments');
58
58
  return afn<T>('GREATEST', args);
59
59
  };
@@ -64,7 +64,7 @@ export const greatest = <T = any>(...args: OperandInput[]): TypedExpression<T> =
64
64
  * @param args - The list of values or columns to compare.
65
65
  * @returns A `TypedExpression<T>` representing the `LEAST` SQL function.
66
66
  */
67
- export const least = <T = any>(...args: OperandInput[]): TypedExpression<T> => {
67
+ export const least = <T = unknown>(...args: OperandInput[]): TypedExpression<T> => {
68
68
  if (args.length < 2) throw new Error('least() expects at least 2 arguments');
69
69
  return afn<T>('LEAST', args);
70
70
  };
@@ -76,4 +76,4 @@ export const least = <T = any>(...args: OperandInput[]): TypedExpression<T> => {
76
76
  * @param defaultValue - The default value to return if val is null.
77
77
  * @returns A `TypedExpression<T>` representing the `COALESCE` SQL function.
78
78
  */
79
- export const ifNull = <T = any>(val: OperandInput, defaultValue: OperandInput): TypedExpression<T> => coalesce<T>(val, defaultValue);
79
+ export const ifNull = <T = unknown>(val: OperandInput, defaultValue: OperandInput): TypedExpression<T> => coalesce<T>(val, defaultValue);
@@ -23,7 +23,7 @@ const fn = (key: string, args: OperandInput[]): FunctionNode => ({
23
23
  });
24
24
 
25
25
  const nfn = (key: string, args: OperandInput[]): TypedExpression<number> => asType<number>(fn(key, args));
26
- const afn = <T = any>(key: string, args: OperandInput[]): TypedExpression<T> => asType<T>(fn(key, args));
26
+ const afn = <T = unknown>(key: string, args: OperandInput[]): TypedExpression<T> => asType<T>(fn(key, args));
27
27
 
28
28
  /**
29
29
  * Returns the number of elements in a JSON array or object.
@@ -41,18 +41,18 @@ export const jsonLength = (target: OperandInput, path?: OperandInput): TypedExpr
41
41
  * @param target - JSON column or value.
42
42
  * @param path - JSON path to set.
43
43
  * @param value - Value to set.
44
- * @returns A `TypedExpression<any>` representing the `JSON_SET` SQL function.
44
+ * @returns A `TypedExpression<T>` representing the `JSON_SET` SQL function.
45
45
  */
46
- export const jsonSet = (target: OperandInput, path: OperandInput, value: OperandInput): TypedExpression<any> =>
47
- afn('JSON_SET', [target, path, value]);
46
+ export const jsonSet = <T = unknown>(target: OperandInput, path: OperandInput, value: OperandInput): TypedExpression<T> =>
47
+ afn<T>('JSON_SET', [target, path, value]);
48
48
 
49
49
  /**
50
50
  * Aggregates values into a JSON array.
51
51
  *
52
52
  * @param value - Column or expression to aggregate.
53
- * @returns A `TypedExpression<any[]>` representing the `JSON_ARRAYAGG` SQL function.
53
+ * @returns A `TypedExpression<unknown[]>` representing the `JSON_ARRAYAGG` SQL function.
54
54
  */
55
- export const jsonArrayAgg = (value: OperandInput): TypedExpression<any[]> => afn<any[]>('JSON_ARRAYAGG', [value]);
55
+ export const jsonArrayAgg = (value: OperandInput): TypedExpression<unknown[]> => afn<unknown[]>('JSON_ARRAYAGG', [value]);
56
56
 
57
57
  /**
58
58
  * Checks if a JSON document contains a specific piece of data.
package/src/index.ts CHANGED
@@ -41,7 +41,8 @@ export * from './orm/relations/has-many.js';
41
41
  export * from './orm/relations/belongs-to.js';
42
42
  export * from './orm/relations/many-to-many.js';
43
43
  export * from './orm/execute.js';
44
- export * from './orm/entity-context.js';
44
+ export type { EntityContext } from './orm/entity-context.js';
45
+ export type { PrimaryKey as EntityPrimaryKey } from './orm/entity-context.js';
45
46
  export * from './orm/execution-context.js';
46
47
  export * from './orm/hydration-context.js';
47
48
  export * from './orm/domain-event-bus.js';
@@ -2,7 +2,9 @@ import { Dialect } from '../core/dialect/abstract.js';
2
2
  import type { DbExecutor } from '../core/execution/db-executor.js';
3
3
  import { TableDef } from '../schema/table.js';
4
4
  import { RelationDef } from '../schema/relation.js';
5
- import { RelationChange, RelationKey, TrackedEntity } from './runtime-types.js';
5
+ import { RelationChange, RelationKey, TrackedEntity } from './runtime-types.js';
6
+
7
+ export type PrimaryKey = string | number;
6
8
 
7
9
  /**
8
10
  * Interface for entity context providing entity tracking and management.
@@ -19,7 +21,7 @@ export interface EntityContext {
19
21
  * @param pk - The primary key value
20
22
  * @returns The entity or undefined
21
23
  */
22
- getEntity(table: TableDef, pk: unknown): unknown;
24
+ getEntity(table: TableDef, pk: PrimaryKey): object | undefined;
23
25
 
24
26
  /**
25
27
  * Sets an entity in the context.
@@ -27,7 +29,7 @@ export interface EntityContext {
27
29
  * @param pk - The primary key value
28
30
  * @param entity - The entity to set
29
31
  */
30
- setEntity(table: TableDef, pk: unknown, entity: unknown): void;
32
+ setEntity(table: TableDef, pk: PrimaryKey, entity: object): void;
31
33
 
32
34
  /**
33
35
  * Tracks a new entity.
@@ -35,7 +37,7 @@ export interface EntityContext {
35
37
  * @param entity - The new entity
36
38
  * @param pk - Optional primary key
37
39
  */
38
- trackNew(table: TableDef, entity: unknown, pk?: unknown): void;
40
+ trackNew(table: TableDef, entity: object, pk?: PrimaryKey): void;
39
41
 
40
42
  /**
41
43
  * Tracks a managed entity.
@@ -43,19 +45,19 @@ export interface EntityContext {
43
45
  * @param pk - The primary key
44
46
  * @param entity - The managed entity
45
47
  */
46
- trackManaged(table: TableDef, pk: unknown, entity: unknown): void;
48
+ trackManaged(table: TableDef, pk: PrimaryKey, entity: object): void;
47
49
 
48
50
  /**
49
51
  * Marks an entity as dirty.
50
52
  * @param entity - The entity to mark
51
53
  */
52
- markDirty(entity: unknown): void;
54
+ markDirty(entity: object): void;
53
55
 
54
56
  /**
55
57
  * Marks an entity as removed.
56
58
  * @param entity - The entity to mark
57
59
  */
58
- markRemoved(entity: unknown): void;
60
+ markRemoved(entity: object): void;
59
61
 
60
62
  /**
61
63
  * Gets all tracked entities for a table.
@@ -1,207 +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
- };
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
+ };