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/README.md +1 -1
- package/dist/index.cjs +481 -203
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -3
- package/dist/index.d.ts +58 -3
- package/dist/index.js +480 -203
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/scripts/generate-entities/schema.mjs +196 -195
- package/src/decorators/decorator-metadata.ts +52 -46
- package/src/decorators/entity.ts +73 -68
- package/src/orm/entity-metadata.ts +301 -301
- package/src/orm/entity.ts +199 -199
- package/src/orm/save-graph.ts +446 -446
- package/src/orm/unit-of-work.ts +6 -6
- package/src/query-builder/select/cursor-pagination.ts +323 -0
- package/src/query-builder/select.ts +42 -1
- package/src/tree/tree-decorator.ts +137 -54
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
|
+
};
|