metal-orm 1.1.7 → 1.1.9

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,199 +1,199 @@
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
- import { RelationKinds } from '../schema/relation.js';
10
-
11
- export { relationLoaderCache } from './entity-relation-cache.js';
12
-
13
- /**
14
- * Options for toJSON serialization.
15
- */
16
- export interface ToJsonOptions {
17
- /**
18
- * If true (default), includes all relations defined in the schema (empty arrays/null for unloaded).
19
- * If false, only includes relations that were loaded.
20
- */
21
- includeAllRelations?: boolean;
22
- }
23
-
24
- const isRelationWrapperLoaded = (value: unknown): boolean => {
25
- if (!value || typeof value !== 'object') return false;
26
- return Boolean((value as { loaded?: boolean }).loaded);
27
- };
28
-
29
- type JsonSource<TTable extends TableDef> = EntityInstance<TTable> & Record<string, unknown>;
30
-
31
- /**
32
- * Creates an entity proxy with lazy loading capabilities.
33
- * @template TTable - The table type
34
- * @template TLazy - The lazy relation keys
35
- * @param ctx - The entity context
36
- * @param table - The table definition
37
- * @param row - The database row
38
- * @param lazyRelations - Optional lazy relations
39
- * @returns The entity instance
40
- */
41
- export const createEntityProxy = <
42
- TTable extends TableDef,
43
- TLazy extends RelationKey<TTable> = RelationKey<TTable>
44
- >(
45
- ctx: EntityContext,
46
- table: TTable,
47
- row: Record<string, unknown>,
48
- lazyRelations: TLazy[] = [] as TLazy[],
49
- lazyRelationOptions: Map<string, RelationIncludeOptions> = new Map()
50
- ): EntityInstance<TTable> => {
51
- const target: Record<string, unknown> = { ...row };
52
- const meta: EntityMeta<TTable> = {
53
- ctx,
54
- table,
55
- lazyRelations: [...lazyRelations],
56
- lazyRelationOptions: new Map(lazyRelationOptions),
57
- relationCache: new Map(),
58
- relationHydration: new Map(),
59
- relationWrappers: new Map()
60
- };
61
- const createRelationEntity: RelationEntityFactory = (relationTable, relationRow) =>
62
- createEntityFromRow(meta.ctx, relationTable, relationRow);
63
-
64
- const isCollectionRelation = (relationName: string): boolean => {
65
- const rel = table.relations[relationName];
66
- if (!rel) return false;
67
- return rel.type === RelationKinds.HasMany || rel.type === RelationKinds.BelongsToMany;
68
- };
69
-
70
- const buildJson = (self: JsonSource<TTable>, options?: ToJsonOptions): Record<string, unknown> => {
71
- const json: Record<string, unknown> = {};
72
- const includeAll = options?.includeAllRelations ?? true;
73
-
74
- // Add non-relation columns
75
- for (const key of Object.keys(target)) {
76
- if (!table.relations[key]) {
77
- json[key] = self[key];
78
- }
79
- }
80
-
81
- // Add relations
82
- if (includeAll) {
83
- // Include ALL relations from schema
84
- for (const relationName of Object.keys(table.relations)) {
85
- const wrapper = self[relationName];
86
- if (wrapper && isRelationWrapperLoaded(wrapper)) {
87
- const wrapperWithToJSON = wrapper as { toJSON?: () => unknown };
88
- json[relationName] = typeof wrapperWithToJSON.toJSON === 'function'
89
- ? wrapperWithToJSON.toJSON()
90
- : wrapper;
91
- } else {
92
- // Unloaded: use empty array for collections, null for single relations
93
- json[relationName] = isCollectionRelation(relationName) ? [] : null;
94
- }
95
- }
96
- } else {
97
- // Only include loaded relations that exist in target
98
- for (const key of Object.keys(target)) {
99
- if (table.relations[key]) {
100
- const wrapper = self[key];
101
- if (wrapper && isRelationWrapperLoaded(wrapper)) {
102
- const wrapperWithToJSON = wrapper as { toJSON?: () => unknown };
103
- json[key] = typeof wrapperWithToJSON.toJSON === 'function'
104
- ? wrapperWithToJSON.toJSON()
105
- : wrapper;
106
- }
107
- }
108
- }
109
- }
110
-
111
- return json;
112
- };
113
-
114
- Object.defineProperty(target, ENTITY_META, {
115
- value: meta,
116
- enumerable: false,
117
- writable: false
118
- });
119
-
120
- const handler: ProxyHandler<object> = {
121
- get(targetObj, prop, receiver) {
122
- if (prop === ENTITY_META) {
123
- return meta;
124
- }
125
-
126
- if (prop === '$load') {
127
- return async (relationName: RelationKey<TTable>) => {
128
- const wrapper = getRelationWrapper(meta, relationName, receiver, createRelationEntity);
129
- if (wrapper && typeof wrapper.load === 'function') {
130
- return wrapper.load();
131
- }
132
- return undefined;
133
- };
134
- }
135
-
136
- if (prop === 'toJSON') {
137
- if (prop in targetObj) {
138
- return Reflect.get(targetObj, prop, receiver);
139
- }
140
- return (options?: ToJsonOptions) => buildJson(receiver as JsonSource<TTable>, options);
141
- }
142
-
143
- if (typeof prop === 'string' && table.relations[prop]) {
144
- return getRelationWrapper(meta, prop as RelationKey<TTable>, receiver, createRelationEntity);
145
- }
146
-
147
- return Reflect.get(targetObj, prop, receiver);
148
- },
149
-
150
- set(targetObj, prop, value, receiver) {
151
- const result = Reflect.set(targetObj, prop, value, receiver);
152
- if (typeof prop === 'string' && table.columns[prop]) {
153
- ctx.markDirty(receiver);
154
- }
155
- return result;
156
- }
157
- };
158
-
159
- const proxy = new Proxy(target, handler) as EntityInstance<TTable>;
160
- populateHydrationCache(proxy, row, meta);
161
- return proxy;
162
- };
163
-
164
- /**
165
- * Creates an entity instance from a database row.
166
- * @template TTable - The table type
167
- * @template TResult - The result type
168
- * @param ctx - The entity context
169
- * @param table - The table definition
170
- * @param row - The database row
171
- * @param lazyRelations - Optional lazy relations
172
- * @returns The entity instance
173
- */
174
- export const createEntityFromRow = <
175
- TTable extends TableDef,
176
- TResult extends EntityInstance<TTable> = EntityInstance<TTable>
177
- >(
178
- ctx: EntityContext,
179
- table: TTable,
180
- row: Record<string, unknown>,
181
- lazyRelations: RelationKey<TTable>[] = [],
182
- lazyRelationOptions: Map<string, RelationIncludeOptions> = new Map()
183
- ): TResult => {
184
- const pkName = findPrimaryKey(table);
185
- const pkValue = row[pkName];
186
- if (pkValue !== undefined && pkValue !== null) {
187
- const tracked = ctx.getEntity(table, pkValue as PrimaryKey);
188
- if (tracked) return tracked as TResult;
189
- }
190
-
191
- const entity = createEntityProxy(ctx, table, row, lazyRelations, lazyRelationOptions);
192
- if (pkValue !== undefined && pkValue !== null) {
193
- ctx.trackManaged(table, pkValue as PrimaryKey, entity);
194
- } else {
195
- ctx.trackNew(table, entity);
196
- }
197
-
198
- return entity as TResult;
199
- };
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
+ import { RelationKinds } from '../schema/relation.js';
10
+
11
+ export { relationLoaderCache } from './entity-relation-cache.js';
12
+
13
+ /**
14
+ * Options for toJSON serialization.
15
+ */
16
+ export interface ToJsonOptions {
17
+ /**
18
+ * If true (default), includes all relations defined in the schema (empty arrays/null for unloaded).
19
+ * If false, only includes relations that were loaded.
20
+ */
21
+ includeAllRelations?: boolean;
22
+ }
23
+
24
+ const isRelationWrapperLoaded = (value: unknown): boolean => {
25
+ if (!value || typeof value !== 'object') return false;
26
+ return Boolean((value as { loaded?: boolean }).loaded);
27
+ };
28
+
29
+ type JsonSource<TTable extends TableDef> = EntityInstance<TTable> & Record<string, unknown>;
30
+
31
+ /**
32
+ * Creates an entity proxy with lazy loading capabilities.
33
+ * @template TTable - The table type
34
+ * @template TLazy - The lazy relation keys
35
+ * @param ctx - The entity context
36
+ * @param table - The table definition
37
+ * @param row - The database row
38
+ * @param lazyRelations - Optional lazy relations
39
+ * @returns The entity instance
40
+ */
41
+ export const createEntityProxy = <
42
+ TTable extends TableDef,
43
+ TLazy extends RelationKey<TTable> = RelationKey<TTable>
44
+ >(
45
+ ctx: EntityContext,
46
+ table: TTable,
47
+ row: Record<string, unknown>,
48
+ lazyRelations: TLazy[] = [] as TLazy[],
49
+ lazyRelationOptions: Map<string, RelationIncludeOptions> = new Map()
50
+ ): EntityInstance<TTable> => {
51
+ const target: Record<string, unknown> = { ...row };
52
+ const meta: EntityMeta<TTable> = {
53
+ ctx,
54
+ table,
55
+ lazyRelations: [...lazyRelations],
56
+ lazyRelationOptions: new Map(lazyRelationOptions),
57
+ relationCache: new Map(),
58
+ relationHydration: new Map(),
59
+ relationWrappers: new Map()
60
+ };
61
+ const createRelationEntity: RelationEntityFactory = (relationTable, relationRow) =>
62
+ createEntityFromRow(meta.ctx, relationTable, relationRow);
63
+
64
+ const isCollectionRelation = (relationName: string): boolean => {
65
+ const rel = table.relations[relationName];
66
+ if (!rel) return false;
67
+ return rel.type === RelationKinds.HasMany || rel.type === RelationKinds.BelongsToMany;
68
+ };
69
+
70
+ const buildJson = (self: JsonSource<TTable>, options?: ToJsonOptions): Record<string, unknown> => {
71
+ const json: Record<string, unknown> = {};
72
+ const includeAll = options?.includeAllRelations ?? true;
73
+
74
+ // Add non-relation columns
75
+ for (const key of Object.keys(target)) {
76
+ if (!table.relations[key]) {
77
+ json[key] = self[key];
78
+ }
79
+ }
80
+
81
+ // Add relations
82
+ if (includeAll) {
83
+ // Include ALL relations from schema
84
+ for (const relationName of Object.keys(table.relations)) {
85
+ const wrapper = self[relationName];
86
+ if (wrapper && isRelationWrapperLoaded(wrapper)) {
87
+ const wrapperWithToJSON = wrapper as { toJSON?: () => unknown };
88
+ json[relationName] = typeof wrapperWithToJSON.toJSON === 'function'
89
+ ? wrapperWithToJSON.toJSON()
90
+ : wrapper;
91
+ } else {
92
+ // Unloaded: use empty array for collections, null for single relations
93
+ json[relationName] = isCollectionRelation(relationName) ? [] : null;
94
+ }
95
+ }
96
+ } else {
97
+ // Only include loaded relations that exist in target
98
+ for (const key of Object.keys(target)) {
99
+ if (table.relations[key]) {
100
+ const wrapper = self[key];
101
+ if (wrapper && isRelationWrapperLoaded(wrapper)) {
102
+ const wrapperWithToJSON = wrapper as { toJSON?: () => unknown };
103
+ json[key] = typeof wrapperWithToJSON.toJSON === 'function'
104
+ ? wrapperWithToJSON.toJSON()
105
+ : wrapper;
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ return json;
112
+ };
113
+
114
+ Object.defineProperty(target, ENTITY_META, {
115
+ value: meta,
116
+ enumerable: false,
117
+ writable: false
118
+ });
119
+
120
+ const handler: ProxyHandler<object> = {
121
+ get(targetObj, prop, receiver) {
122
+ if (prop === ENTITY_META) {
123
+ return meta;
124
+ }
125
+
126
+ if (prop === '$load') {
127
+ return async (relationName: RelationKey<TTable>) => {
128
+ const wrapper = getRelationWrapper(meta, relationName, receiver, createRelationEntity);
129
+ if (wrapper && typeof wrapper.load === 'function') {
130
+ return wrapper.load();
131
+ }
132
+ return undefined;
133
+ };
134
+ }
135
+
136
+ if (prop === 'toJSON') {
137
+ if (prop in targetObj) {
138
+ return Reflect.get(targetObj, prop, receiver);
139
+ }
140
+ return (options?: ToJsonOptions) => buildJson(receiver as JsonSource<TTable>, options);
141
+ }
142
+
143
+ if (typeof prop === 'string' && table.relations[prop]) {
144
+ return getRelationWrapper(meta, prop as RelationKey<TTable>, receiver, createRelationEntity);
145
+ }
146
+
147
+ return Reflect.get(targetObj, prop, receiver);
148
+ },
149
+
150
+ set(targetObj, prop, value, receiver) {
151
+ const result = Reflect.set(targetObj, prop, value, receiver);
152
+ if (typeof prop === 'string' && table.columns[prop]) {
153
+ ctx.markDirty(receiver);
154
+ }
155
+ return result;
156
+ }
157
+ };
158
+
159
+ const proxy = new Proxy(target, handler) as EntityInstance<TTable>;
160
+ populateHydrationCache(proxy, row, meta);
161
+ return proxy;
162
+ };
163
+
164
+ /**
165
+ * Creates an entity instance from a database row.
166
+ * @template TTable - The table type
167
+ * @template TResult - The result type
168
+ * @param ctx - The entity context
169
+ * @param table - The table definition
170
+ * @param row - The database row
171
+ * @param lazyRelations - Optional lazy relations
172
+ * @returns The entity instance
173
+ */
174
+ export const createEntityFromRow = <
175
+ TTable extends TableDef,
176
+ TResult extends EntityInstance<TTable> = EntityInstance<TTable>
177
+ >(
178
+ ctx: EntityContext,
179
+ table: TTable,
180
+ row: Record<string, unknown>,
181
+ lazyRelations: RelationKey<TTable>[] = [],
182
+ lazyRelationOptions: Map<string, RelationIncludeOptions> = new Map()
183
+ ): TResult => {
184
+ const pkName = findPrimaryKey(table);
185
+ const pkValue = row[pkName];
186
+ if (pkValue !== undefined && pkValue !== null) {
187
+ const tracked = ctx.getEntity(table, pkValue as PrimaryKey);
188
+ if (tracked) return tracked as TResult;
189
+ }
190
+
191
+ const entity = createEntityProxy(ctx, table, row, lazyRelations, lazyRelationOptions);
192
+ if (pkValue !== undefined && pkValue !== null) {
193
+ ctx.trackManaged(table, pkValue as PrimaryKey, entity);
194
+ } else {
195
+ ctx.trackNew(table, entity);
196
+ }
197
+
198
+ return entity as TResult;
199
+ };