metal-orm 1.0.106 → 1.0.107

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