metal-orm 1.0.103 → 1.0.105

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.103",
3
+ "version": "1.0.105",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "engines": {
@@ -17,13 +17,36 @@ export type RelationEntityFactory = (
17
17
 
18
18
  const proxifyRelationWrapper = <T extends object>(wrapper: T): T => {
19
19
  return new Proxy(wrapper, {
20
- get(target, prop, receiver) {
20
+ get(target, prop, _receiver) {
21
+ // Intercept toJSON to ensure proper serialization
22
+ if (prop === 'toJSON') {
23
+ return () => {
24
+ // Call the wrapper's toJSON method if it exists
25
+ const wrapperToJSON = (target as { toJSON?: () => unknown }).toJSON;
26
+ if (typeof wrapperToJSON === 'function') {
27
+ return wrapperToJSON.call(target);
28
+ }
29
+ // Fallback: return the underlying data
30
+ const getRef = (target as { get?: () => unknown }).get;
31
+ if (typeof getRef === 'function') {
32
+ return getRef.call(target);
33
+ }
34
+ const getItems = (target as { getItems?: () => unknown }).getItems;
35
+ if (typeof getItems === 'function') {
36
+ return getItems.call(target);
37
+ }
38
+ return target;
39
+ };
40
+ }
41
+
21
42
  if (typeof prop === 'symbol') {
22
- return Reflect.get(target, prop, receiver);
43
+ const value = Reflect.get(target, prop, target);
44
+ return typeof value === 'function' ? value.bind(target) : value;
23
45
  }
24
46
 
25
47
  if (prop in target) {
26
- return Reflect.get(target, prop, receiver);
48
+ const value = Reflect.get(target, prop, target);
49
+ return typeof value === 'function' ? value.bind(target) : value;
27
50
  }
28
51
 
29
52
  const getItems = (target as { getItems?: () => unknown }).getItems;
@@ -49,13 +72,13 @@ const proxifyRelationWrapper = <T extends object>(wrapper: T): T => {
49
72
  return undefined;
50
73
  },
51
74
 
52
- set(target, prop, value, receiver) {
75
+ set(target, prop, value, _receiver) {
53
76
  if (typeof prop === 'symbol') {
54
- return Reflect.set(target, prop, value, receiver);
77
+ return Reflect.set(target, prop, value, target);
55
78
  }
56
79
 
57
80
  if (prop in target) {
58
- return Reflect.set(target, prop, value, receiver);
81
+ return Reflect.set(target, prop, value, target);
59
82
  }
60
83
 
61
84
  const getRef = (target as { get?: () => unknown }).get;
@@ -72,7 +95,7 @@ const proxifyRelationWrapper = <T extends object>(wrapper: T): T => {
72
95
  return Reflect.set(items as object, prop, value);
73
96
  }
74
97
 
75
- return Reflect.set(target, prop, value, receiver);
98
+ return Reflect.set(target, prop, value, target);
76
99
  }
77
100
  });
78
101
  };
package/src/orm/entity.ts CHANGED
@@ -1,151 +1,162 @@
1
- import { TableDef } from '../schema/table.js';
2
- import { EntityInstance } from '../schema/types.js';
3
- import type { EntityContext, PrimaryKey } 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
- 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
24
- * @param table - The table definition
25
- * @param row - The database row
26
- * @param lazyRelations - Optional lazy relations
27
- * @returns The entity instance
28
- */
29
- export const createEntityProxy = <
30
- TTable extends TableDef,
31
- TLazy extends RelationKey<TTable> = RelationKey<TTable>
32
- >(
33
- ctx: EntityContext,
34
- table: TTable,
35
- row: Record<string, unknown>,
36
- lazyRelations: TLazy[] = [] as TLazy[],
37
- lazyRelationOptions: Map<string, RelationIncludeOptions> = new Map()
38
- ): EntityInstance<TTable> => {
39
- const target: Record<string, unknown> = { ...row };
40
- const meta: EntityMeta<TTable> = {
41
- ctx,
42
- table,
43
- lazyRelations: [...lazyRelations],
44
- lazyRelationOptions: new Map(lazyRelationOptions),
45
- relationCache: new Map(),
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
- });
71
-
72
- const handler: ProxyHandler<object> = {
73
- get(targetObj, prop, receiver) {
74
- if (prop === ENTITY_META) {
75
- return meta;
76
- }
77
-
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);
100
- },
101
-
102
- set(targetObj, prop, value, receiver) {
103
- const result = Reflect.set(targetObj, prop, value, receiver);
104
- if (typeof prop === 'string' && table.columns[prop]) {
105
- ctx.markDirty(receiver);
106
- }
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
- };
115
-
116
- /**
117
- * Creates an entity instance from a database row.
118
- * @template TTable - The table type
119
- * @template TResult - The result type
120
- * @param ctx - The entity context
121
- * @param table - The table definition
122
- * @param row - The database row
123
- * @param lazyRelations - Optional lazy relations
124
- * @returns The entity instance
125
- */
126
- export const createEntityFromRow = <
127
- TTable extends TableDef,
128
- TResult extends EntityInstance<TTable> = EntityInstance<TTable>
129
- >(
130
- ctx: EntityContext,
131
- table: TTable,
132
- row: Record<string, unknown>,
133
- lazyRelations: RelationKey<TTable>[] = [],
134
- lazyRelationOptions: Map<string, RelationIncludeOptions> = new Map()
135
- ): TResult => {
136
- const pkName = findPrimaryKey(table);
137
- const pkValue = row[pkName];
138
- if (pkValue !== undefined && pkValue !== null) {
139
- const tracked = ctx.getEntity(table, pkValue as PrimaryKey);
140
- if (tracked) return tracked as TResult;
141
- }
142
-
143
- const entity = createEntityProxy(ctx, table, row, lazyRelations, lazyRelationOptions);
144
- if (pkValue !== undefined && pkValue !== null) {
145
- ctx.trackManaged(table, pkValue as PrimaryKey, entity);
146
- } else {
147
- ctx.trackNew(table, entity);
148
- }
149
-
150
- return entity as TResult;
151
- };
1
+ import { TableDef } from '../schema/table.js';
2
+ import { EntityInstance } from '../schema/types.js';
3
+ import type { EntityContext, PrimaryKey } 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
+ const isRelationWrapperLoaded = (value: unknown): boolean => {
13
+ if (!value || typeof value !== 'object') return false;
14
+ const wrapper = value as { isLoaded?: () => boolean };
15
+ if (typeof wrapper.isLoaded === 'function') return wrapper.isLoaded();
16
+ return false;
17
+ };
18
+
19
+ const unwrapRelationForJson = (value: unknown): unknown => {
20
+ if (value && typeof value === 'object' && typeof (value as { toJSON?: () => unknown }).toJSON === 'function') {
21
+ return (value as { toJSON: () => unknown }).toJSON();
22
+ }
23
+ return value;
24
+ };
25
+
26
+ type JsonSource<TTable extends TableDef> = EntityInstance<TTable> & Record<string, unknown>;
27
+
28
+ /**
29
+ * Creates an entity proxy with lazy loading capabilities.
30
+ * @template TTable - The table type
31
+ * @template TLazy - The lazy relation keys
32
+ * @param ctx - The entity context
33
+ * @param table - The table definition
34
+ * @param row - The database row
35
+ * @param lazyRelations - Optional lazy relations
36
+ * @returns The entity instance
37
+ */
38
+ export const createEntityProxy = <
39
+ TTable extends TableDef,
40
+ TLazy extends RelationKey<TTable> = RelationKey<TTable>
41
+ >(
42
+ ctx: EntityContext,
43
+ table: TTable,
44
+ row: Record<string, unknown>,
45
+ lazyRelations: TLazy[] = [] as TLazy[],
46
+ lazyRelationOptions: Map<string, RelationIncludeOptions> = new Map()
47
+ ): EntityInstance<TTable> => {
48
+ const target: Record<string, unknown> = { ...row };
49
+ const meta: EntityMeta<TTable> = {
50
+ ctx,
51
+ table,
52
+ lazyRelations: [...lazyRelations],
53
+ lazyRelationOptions: new Map(lazyRelationOptions),
54
+ relationCache: new Map(),
55
+ relationHydration: new Map(),
56
+ relationWrappers: new Map()
57
+ };
58
+ const createRelationEntity: RelationEntityFactory = (relationTable, relationRow) =>
59
+ createEntityFromRow(meta.ctx, relationTable, relationRow);
60
+
61
+ const buildJson = (self: JsonSource<TTable>): Record<string, unknown> => {
62
+ const json: Record<string, unknown> = {};
63
+ for (const key of Object.keys(target)) {
64
+ const value = self[key];
65
+ // Unwrap relation wrappers to get the actual data
66
+ json[key] = unwrapRelationForJson(value);
67
+ }
68
+ for (const [relationName, wrapper] of meta.relationWrappers) {
69
+ if (Object.prototype.hasOwnProperty.call(json, relationName)) continue;
70
+ if (isRelationWrapperLoaded(wrapper)) {
71
+ json[relationName] = unwrapRelationForJson(wrapper);
72
+ }
73
+ }
74
+ return json;
75
+ };
76
+
77
+ Object.defineProperty(target, ENTITY_META, {
78
+ value: meta,
79
+ enumerable: false,
80
+ writable: false
81
+ });
82
+
83
+ const handler: ProxyHandler<object> = {
84
+ get(targetObj, prop, receiver) {
85
+ if (prop === ENTITY_META) {
86
+ return meta;
87
+ }
88
+
89
+ if (prop === '$load') {
90
+ return async (relationName: RelationKey<TTable>) => {
91
+ const wrapper = getRelationWrapper(meta, relationName, receiver, createRelationEntity);
92
+ if (wrapper && typeof wrapper.load === 'function') {
93
+ return wrapper.load();
94
+ }
95
+ return undefined;
96
+ };
97
+ }
98
+
99
+ if (prop === 'toJSON') {
100
+ if (prop in targetObj) {
101
+ return Reflect.get(targetObj, prop, receiver);
102
+ }
103
+ return () => buildJson(receiver as JsonSource<TTable>);
104
+ }
105
+
106
+ if (typeof prop === 'string' && table.relations[prop]) {
107
+ return getRelationWrapper(meta, prop as RelationKey<TTable>, receiver, createRelationEntity);
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: RelationKey<TTable>[] = [],
145
+ lazyRelationOptions: Map<string, RelationIncludeOptions> = new Map()
146
+ ): TResult => {
147
+ const pkName = findPrimaryKey(table);
148
+ const pkValue = row[pkName];
149
+ if (pkValue !== undefined && pkValue !== null) {
150
+ const tracked = ctx.getEntity(table, pkValue as PrimaryKey);
151
+ if (tracked) return tracked as TResult;
152
+ }
153
+
154
+ const entity = createEntityProxy(ctx, table, row, lazyRelations, lazyRelationOptions);
155
+ if (pkValue !== undefined && pkValue !== null) {
156
+ ctx.trackManaged(table, pkValue as PrimaryKey, entity);
157
+ } else {
158
+ ctx.trackNew(table, entity);
159
+ }
160
+
161
+ return entity as TResult;
162
+ };
@@ -1,126 +1,130 @@
1
- import { BelongsToReferenceApi } from '../../schema/types.js';
2
- import { EntityContext } from '../entity-context.js';
3
- import { RelationKey } from '../runtime-types.js';
4
- import { BelongsToRelation } from '../../schema/relation.js';
5
- import { TableDef } from '../../schema/table.js';
6
- import { EntityMeta, getHydrationRecord, hasEntityMeta } from '../entity-meta.js';
7
-
8
- type Rows = Record<string, unknown>;
9
-
10
- const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
11
-
12
- const hideInternal = (obj: object, keys: string[]): void => {
13
- for (const key of keys) {
14
- Object.defineProperty(obj, key, {
15
- value: obj[key],
16
- writable: false,
17
- configurable: false,
18
- enumerable: false
19
- });
20
- }
21
- };
22
-
23
- /**
24
- * Default implementation of a belongs-to reference.
25
- * Manages a reference to a parent entity from a child entity through a foreign key.
26
- *
27
- * @template TParent The type of the parent entity.
28
- */
29
- export class DefaultBelongsToReference<TParent extends object> implements BelongsToReferenceApi<TParent> {
30
- private loaded = false;
31
- private current: TParent | null = null;
32
-
33
- /**
34
- * @param ctx The entity context for tracking changes.
35
- * @param meta Metadata for the child entity.
36
- * @param root The child entity instance (carrying the foreign key).
37
- * @param relationName The name of the relation.
38
- * @param relation Relation definition.
39
- * @param rootTable Table definition of the child entity.
40
- * @param loader Function to load the parent entity.
41
- * @param createEntity Function to create entity instances from rows.
42
- * @param targetKey The primary key of the target (parent) table.
43
- */
44
- constructor(
45
- private readonly ctx: EntityContext,
46
- private readonly meta: EntityMeta<TableDef>,
47
- private readonly root: unknown,
48
- private readonly relationName: string,
49
- private readonly relation: BelongsToRelation,
50
- private readonly rootTable: TableDef,
51
- private readonly loader: () => Promise<Map<string, Rows>>,
52
- private readonly createEntity: (row: Record<string, unknown>) => TParent,
53
- private readonly targetKey: string
54
- ) {
55
- hideInternal(this, ['ctx', 'meta', 'root', 'relationName', 'relation', 'rootTable', 'loader', 'createEntity', 'targetKey']);
56
- this.populateFromHydrationCache();
57
- }
58
-
59
- async load(): Promise<TParent | null> {
60
- if (this.loaded) return this.current;
61
- const map = await this.loader();
62
- const fkValue = (this.root as Record<string, unknown>)[this.relation.foreignKey];
63
- if (fkValue === null || fkValue === undefined) {
64
- this.current = null;
65
- } else {
66
- const row = map.get(toKey(fkValue));
67
- this.current = row ? this.createEntity(row) : null;
68
- }
69
- this.loaded = true;
70
- return this.current;
71
- }
72
-
73
- get(): TParent | null {
74
- return this.current;
75
- }
76
-
77
- set(data: Partial<TParent> | TParent | null): TParent | null {
78
- if (data === null) {
79
- const previous = this.current;
80
- (this.root as Record<string, unknown>)[this.relation.foreignKey] = null;
81
- this.current = null;
82
- this.ctx.registerRelationChange(
83
- this.root,
84
- this.relationKey,
85
- this.rootTable,
86
- this.relationName,
87
- this.relation,
88
- { kind: 'remove', entity: previous }
89
- );
90
- return null;
91
- }
92
-
93
- const entity = hasEntityMeta(data) ? (data as TParent) : this.createEntity(data as Record<string, unknown>);
94
- const pkValue = (entity as Record<string, unknown>)[this.targetKey];
95
- if (pkValue !== undefined) {
96
- (this.root as Record<string, unknown>)[this.relation.foreignKey] = pkValue;
97
- }
98
- this.current = entity;
99
- this.ctx.registerRelationChange(
100
- this.root,
101
- this.relationKey,
102
- this.rootTable,
103
- this.relationName,
104
- this.relation,
105
- { kind: 'attach', entity }
106
- );
107
- return entity;
108
- }
109
-
110
- private get relationKey(): RelationKey {
111
- return `${this.rootTable.name}.${this.relationName}`;
112
- }
113
-
114
- private populateFromHydrationCache(): void {
115
- const fkValue = (this.root as Record<string, unknown>)[this.relation.foreignKey];
116
- if (fkValue === undefined || fkValue === null) return;
117
- const row = getHydrationRecord(this.meta, this.relationName, fkValue);
118
- if (!row) return;
119
- this.current = this.createEntity(row);
120
- this.loaded = true;
121
- }
122
-
123
- toJSON(): TParent | null {
124
- return this.current;
125
- }
126
- }
1
+ import { BelongsToReferenceApi } from '../../schema/types.js';
2
+ import { EntityContext } from '../entity-context.js';
3
+ import { RelationKey } from '../runtime-types.js';
4
+ import { BelongsToRelation } from '../../schema/relation.js';
5
+ import { TableDef } from '../../schema/table.js';
6
+ import { EntityMeta, getHydrationRecord, hasEntityMeta } from '../entity-meta.js';
7
+
8
+ type Rows = Record<string, unknown>;
9
+
10
+ const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
11
+
12
+ const hideInternal = (obj: object, keys: string[]): void => {
13
+ for (const key of keys) {
14
+ Object.defineProperty(obj, key, {
15
+ value: obj[key],
16
+ writable: false,
17
+ configurable: true, // Must be configurable for Proxy get trap to work properly
18
+ enumerable: false
19
+ });
20
+ }
21
+ };
22
+
23
+ /**
24
+ * Default implementation of a belongs-to reference.
25
+ * Manages a reference to a parent entity from a child entity through a foreign key.
26
+ *
27
+ * @template TParent The type of the parent entity.
28
+ */
29
+ export class DefaultBelongsToReference<TParent extends object> implements BelongsToReferenceApi<TParent> {
30
+ #loaded = false;
31
+ #current: TParent | null = null;
32
+
33
+ /**
34
+ * @param ctx The entity context for tracking changes.
35
+ * @param meta Metadata for the child entity.
36
+ * @param root The child entity instance (carrying the foreign key).
37
+ * @param relationName The name of the relation.
38
+ * @param relation Relation definition.
39
+ * @param rootTable Table definition of the child entity.
40
+ * @param loader Function to load the parent entity.
41
+ * @param createEntity Function to create entity instances from rows.
42
+ * @param targetKey The primary key of the target (parent) table.
43
+ */
44
+ constructor(
45
+ private readonly ctx: EntityContext,
46
+ private readonly meta: EntityMeta<TableDef>,
47
+ private readonly root: unknown,
48
+ private readonly relationName: string,
49
+ private readonly relation: BelongsToRelation,
50
+ private readonly rootTable: TableDef,
51
+ private readonly loader: () => Promise<Map<string, Rows>>,
52
+ private readonly createEntity: (row: Record<string, unknown>) => TParent,
53
+ private readonly targetKey: string
54
+ ) {
55
+ hideInternal(this, ['ctx', 'meta', 'root', 'relationName', 'relation', 'rootTable', 'loader', 'createEntity', 'targetKey']);
56
+ this.populateFromHydrationCache();
57
+ }
58
+
59
+ async load(): Promise<TParent | null> {
60
+ if (this.#loaded) return this.#current;
61
+ const map = await this.loader();
62
+ const fkValue = (this.root as Record<string, unknown>)[this.relation.foreignKey];
63
+ if (fkValue === null || fkValue === undefined) {
64
+ this.#current = null;
65
+ } else {
66
+ const row = map.get(toKey(fkValue));
67
+ this.#current = row ? this.createEntity(row) : null;
68
+ }
69
+ this.#loaded = true;
70
+ return this.#current;
71
+ }
72
+
73
+ get(): TParent | null {
74
+ return this.#current;
75
+ }
76
+
77
+ set(data: Partial<TParent> | TParent | null): TParent | null {
78
+ if (data === null) {
79
+ const previous = this.#current;
80
+ (this.root as Record<string, unknown>)[this.relation.foreignKey] = null;
81
+ this.#current = null;
82
+ this.ctx.registerRelationChange(
83
+ this.root,
84
+ this.relationKey,
85
+ this.rootTable,
86
+ this.relationName,
87
+ this.relation,
88
+ { kind: 'remove', entity: previous }
89
+ );
90
+ return null;
91
+ }
92
+
93
+ const entity = hasEntityMeta(data) ? (data as TParent) : this.createEntity(data as Record<string, unknown>);
94
+ const pkValue = (entity as Record<string, unknown>)[this.targetKey];
95
+ if (pkValue !== undefined) {
96
+ (this.root as Record<string, unknown>)[this.relation.foreignKey] = pkValue;
97
+ }
98
+ this.#current = entity;
99
+ this.ctx.registerRelationChange(
100
+ this.root,
101
+ this.relationKey,
102
+ this.rootTable,
103
+ this.relationName,
104
+ this.relation,
105
+ { kind: 'attach', entity }
106
+ );
107
+ return entity;
108
+ }
109
+
110
+ isLoaded(): boolean {
111
+ return this.#loaded;
112
+ }
113
+
114
+ private get relationKey(): RelationKey {
115
+ return `${this.rootTable.name}.${this.relationName}`;
116
+ }
117
+
118
+ private populateFromHydrationCache(): void {
119
+ const fkValue = (this.root as Record<string, unknown>)[this.relation.foreignKey];
120
+ if (fkValue === undefined || fkValue === null) return;
121
+ const row = getHydrationRecord(this.meta, this.relationName, fkValue);
122
+ if (!row) return;
123
+ this.#current = this.createEntity(row);
124
+ this.#loaded = true;
125
+ }
126
+
127
+ toJSON(): TParent | null {
128
+ return this.#current;
129
+ }
130
+ }