metal-orm 1.0.103 → 1.0.104

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/src/orm/entity.ts CHANGED
@@ -1,151 +1,160 @@
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
+ json[key] = self[key];
65
+ }
66
+ for (const [relationName, wrapper] of meta.relationWrappers) {
67
+ if (Object.prototype.hasOwnProperty.call(json, relationName)) continue;
68
+ if (isRelationWrapperLoaded(wrapper)) {
69
+ json[relationName] = unwrapRelationForJson(wrapper);
70
+ }
71
+ }
72
+ return json;
73
+ };
74
+
75
+ Object.defineProperty(target, ENTITY_META, {
76
+ value: meta,
77
+ enumerable: false,
78
+ writable: false
79
+ });
80
+
81
+ const handler: ProxyHandler<object> = {
82
+ get(targetObj, prop, receiver) {
83
+ if (prop === ENTITY_META) {
84
+ return meta;
85
+ }
86
+
87
+ if (prop === '$load') {
88
+ return async (relationName: RelationKey<TTable>) => {
89
+ const wrapper = getRelationWrapper(meta, relationName, receiver, createRelationEntity);
90
+ if (wrapper && typeof wrapper.load === 'function') {
91
+ return wrapper.load();
92
+ }
93
+ return undefined;
94
+ };
95
+ }
96
+
97
+ if (prop === 'toJSON') {
98
+ if (prop in targetObj) {
99
+ return Reflect.get(targetObj, prop, receiver);
100
+ }
101
+ return () => buildJson(receiver as JsonSource<TTable>);
102
+ }
103
+
104
+ if (typeof prop === 'string' && table.relations[prop]) {
105
+ return getRelationWrapper(meta, prop as RelationKey<TTable>, receiver, createRelationEntity);
106
+ }
107
+
108
+ return Reflect.get(targetObj, prop, receiver);
109
+ },
110
+
111
+ set(targetObj, prop, value, receiver) {
112
+ const result = Reflect.set(targetObj, prop, value, receiver);
113
+ if (typeof prop === 'string' && table.columns[prop]) {
114
+ ctx.markDirty(receiver);
115
+ }
116
+ return result;
117
+ }
118
+ };
119
+
120
+ const proxy = new Proxy(target, handler) as EntityInstance<TTable>;
121
+ populateHydrationCache(proxy, row, meta);
122
+ return proxy;
123
+ };
124
+
125
+ /**
126
+ * Creates an entity instance from a database row.
127
+ * @template TTable - The table type
128
+ * @template TResult - The result type
129
+ * @param ctx - The entity context
130
+ * @param table - The table definition
131
+ * @param row - The database row
132
+ * @param lazyRelations - Optional lazy relations
133
+ * @returns The entity instance
134
+ */
135
+ export const createEntityFromRow = <
136
+ TTable extends TableDef,
137
+ TResult extends EntityInstance<TTable> = EntityInstance<TTable>
138
+ >(
139
+ ctx: EntityContext,
140
+ table: TTable,
141
+ row: Record<string, unknown>,
142
+ lazyRelations: RelationKey<TTable>[] = [],
143
+ lazyRelationOptions: Map<string, RelationIncludeOptions> = new Map()
144
+ ): TResult => {
145
+ const pkName = findPrimaryKey(table);
146
+ const pkValue = row[pkName];
147
+ if (pkValue !== undefined && pkValue !== null) {
148
+ const tracked = ctx.getEntity(table, pkValue as PrimaryKey);
149
+ if (tracked) return tracked as TResult;
150
+ }
151
+
152
+ const entity = createEntityProxy(ctx, table, row, lazyRelations, lazyRelationOptions);
153
+ if (pkValue !== undefined && pkValue !== null) {
154
+ ctx.trackManaged(table, pkValue as PrimaryKey, entity);
155
+ } else {
156
+ ctx.trackNew(table, entity);
157
+ }
158
+
159
+ return entity as TResult;
160
+ };
@@ -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: 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
+ #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
+ }