metal-orm 1.0.63 → 1.0.65
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 +10 -8
- package/dist/index.cjs +130 -107
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +848 -457
- package/dist/index.d.ts +848 -457
- package/dist/index.js +130 -107
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ast/aggregate-functions.ts +14 -5
- package/src/core/ast/expression-builders.ts +325 -111
- package/src/core/ast/window-functions.ts +62 -52
- package/src/core/functions/array.ts +12 -3
- package/src/core/functions/control-flow.ts +28 -18
- package/src/core/functions/datetime.ts +113 -84
- package/src/core/functions/json.ts +40 -8
- package/src/core/functions/numeric.ts +116 -79
- package/src/core/functions/text.ts +181 -114
- package/src/index.ts +2 -1
- package/src/orm/entity-context.ts +9 -7
- package/src/orm/entity-relations.ts +207 -207
- package/src/orm/entity.ts +124 -124
- package/src/orm/execute.ts +166 -166
- package/src/orm/identity-map.ts +1 -1
- package/src/orm/lazy-batch/shared.ts +1 -1
- package/src/orm/orm-session.ts +54 -54
- package/src/orm/relation-change-processor.ts +3 -3
- package/src/orm/relations/has-many.ts +1 -1
- package/src/orm/runtime-types.ts +5 -5
- package/src/orm/save-graph.ts +161 -161
- package/src/orm/unit-of-work.ts +58 -56
- package/src/query-builder/insert-query-state.ts +156 -155
- package/src/query-builder/insert.ts +5 -2
- package/src/query-builder/select.ts +112 -111
- package/src/schema/column-types.ts +14 -14
- package/src/schema/table.ts +39 -31
- package/src/schema/types.ts +54 -54
package/src/orm/save-graph.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
EntityInstance,
|
|
3
|
-
HasManyCollection,
|
|
4
|
-
HasOneReference,
|
|
5
|
-
BelongsToReference,
|
|
6
|
-
ManyToManyCollection
|
|
7
|
-
} from '../schema/types.js';
|
|
8
|
-
import { normalizeColumnType, type ColumnDef } from '../schema/column-types.js';
|
|
9
|
-
import {
|
|
10
|
-
RelationKinds,
|
|
11
|
-
type BelongsToManyRelation,
|
|
12
|
-
type BelongsToRelation,
|
|
13
|
-
type HasManyRelation,
|
|
1
|
+
import type {
|
|
2
|
+
EntityInstance,
|
|
3
|
+
HasManyCollection,
|
|
4
|
+
HasOneReference,
|
|
5
|
+
BelongsToReference,
|
|
6
|
+
ManyToManyCollection
|
|
7
|
+
} from '../schema/types.js';
|
|
8
|
+
import { normalizeColumnType, type ColumnDef } from '../schema/column-types.js';
|
|
9
|
+
import {
|
|
10
|
+
RelationKinds,
|
|
11
|
+
type BelongsToManyRelation,
|
|
12
|
+
type BelongsToRelation,
|
|
13
|
+
type HasManyRelation,
|
|
14
14
|
type HasOneRelation,
|
|
15
15
|
type RelationDef
|
|
16
16
|
} from '../schema/relation.js';
|
|
@@ -24,16 +24,16 @@ import type { OrmSession } from './orm-session.js';
|
|
|
24
24
|
/**
|
|
25
25
|
* Options for controlling the behavior of save graph operations.
|
|
26
26
|
*/
|
|
27
|
-
export interface SaveGraphOptions {
|
|
28
|
-
/** Remove existing collection members that are not present in the payload */
|
|
29
|
-
pruneMissing?: boolean;
|
|
30
|
-
/**
|
|
31
|
-
* Coerce JSON-friendly input values into DB-friendly primitives.
|
|
32
|
-
* Currently:
|
|
33
|
-
* - Date -> ISO string (for DATE/DATETIME/TIMESTAMP/TIMESTAMPTZ columns)
|
|
34
|
-
*/
|
|
35
|
-
coerce?: 'json';
|
|
36
|
-
}
|
|
27
|
+
export interface SaveGraphOptions {
|
|
28
|
+
/** Remove existing collection members that are not present in the payload */
|
|
29
|
+
pruneMissing?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Coerce JSON-friendly input values into DB-friendly primitives.
|
|
32
|
+
* Currently:
|
|
33
|
+
* - Date -> ISO string (for DATE/DATETIME/TIMESTAMP/TIMESTAMPTZ columns)
|
|
34
|
+
*/
|
|
35
|
+
coerce?: 'json';
|
|
36
|
+
}
|
|
37
37
|
|
|
38
38
|
/** Represents an entity object with arbitrary properties. */
|
|
39
39
|
|
|
@@ -51,58 +51,58 @@ type AnyEntity = Record<string, unknown>;
|
|
|
51
51
|
|
|
52
52
|
*/
|
|
53
53
|
|
|
54
|
-
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
55
|
-
|
|
56
|
-
const coerceColumnValue = (
|
|
57
|
-
table: TableDef,
|
|
58
|
-
columnName: string,
|
|
59
|
-
value: unknown,
|
|
60
|
-
options: SaveGraphOptions
|
|
61
|
-
): unknown => {
|
|
62
|
-
if (options.coerce !== 'json') return value;
|
|
63
|
-
if (value === null || value === undefined) return value;
|
|
64
|
-
|
|
65
|
-
const column = table.columns[columnName] as unknown as ColumnDef | undefined;
|
|
66
|
-
if (!column) return value;
|
|
67
|
-
|
|
68
|
-
const normalized = normalizeColumnType(column.type);
|
|
69
|
-
|
|
70
|
-
const isDateLikeColumn =
|
|
71
|
-
normalized === 'date' ||
|
|
72
|
-
normalized === 'datetime' ||
|
|
73
|
-
normalized === 'timestamp' ||
|
|
74
|
-
normalized === 'timestamptz';
|
|
75
|
-
|
|
76
|
-
if (isDateLikeColumn && value instanceof Date) {
|
|
77
|
-
return value.toISOString();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Future coercions can be added here based on `normalized`.
|
|
81
|
-
return value;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const pickColumns = (table: TableDef, payload: AnyEntity, options: SaveGraphOptions): Record<string, unknown> => {
|
|
85
|
-
const columns: Record<string, unknown> = {};
|
|
86
|
-
for (const key of Object.keys(table.columns)) {
|
|
87
|
-
if (payload[key] !== undefined) {
|
|
88
|
-
columns[key] = coerceColumnValue(table, key, payload[key], options);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return columns;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const ensureEntity = <TTable extends TableDef>(
|
|
95
|
-
session: OrmSession,
|
|
96
|
-
table: TTable,
|
|
97
|
-
payload: AnyEntity,
|
|
98
|
-
options: SaveGraphOptions
|
|
99
|
-
): EntityInstance<TTable> => {
|
|
100
|
-
const pk = findPrimaryKey(table);
|
|
101
|
-
const row = pickColumns(table, payload, options);
|
|
102
|
-
const pkValue = payload[pk];
|
|
54
|
+
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
55
|
+
|
|
56
|
+
const coerceColumnValue = (
|
|
57
|
+
table: TableDef,
|
|
58
|
+
columnName: string,
|
|
59
|
+
value: unknown,
|
|
60
|
+
options: SaveGraphOptions
|
|
61
|
+
): unknown => {
|
|
62
|
+
if (options.coerce !== 'json') return value;
|
|
63
|
+
if (value === null || value === undefined) return value;
|
|
64
|
+
|
|
65
|
+
const column = table.columns[columnName] as unknown as ColumnDef | undefined;
|
|
66
|
+
if (!column) return value;
|
|
67
|
+
|
|
68
|
+
const normalized = normalizeColumnType(column.type);
|
|
69
|
+
|
|
70
|
+
const isDateLikeColumn =
|
|
71
|
+
normalized === 'date' ||
|
|
72
|
+
normalized === 'datetime' ||
|
|
73
|
+
normalized === 'timestamp' ||
|
|
74
|
+
normalized === 'timestamptz';
|
|
75
|
+
|
|
76
|
+
if (isDateLikeColumn && value instanceof Date) {
|
|
77
|
+
return value.toISOString();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Future coercions can be added here based on `normalized`.
|
|
81
|
+
return value;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const pickColumns = (table: TableDef, payload: AnyEntity, options: SaveGraphOptions): Record<string, unknown> => {
|
|
85
|
+
const columns: Record<string, unknown> = {};
|
|
86
|
+
for (const key of Object.keys(table.columns)) {
|
|
87
|
+
if (payload[key] !== undefined) {
|
|
88
|
+
columns[key] = coerceColumnValue(table, key, payload[key], options);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return columns;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const ensureEntity = <TTable extends TableDef>(
|
|
95
|
+
session: OrmSession,
|
|
96
|
+
table: TTable,
|
|
97
|
+
payload: AnyEntity,
|
|
98
|
+
options: SaveGraphOptions
|
|
99
|
+
): EntityInstance<TTable> => {
|
|
100
|
+
const pk = findPrimaryKey(table);
|
|
101
|
+
const row = pickColumns(table, payload, options);
|
|
102
|
+
const pkValue = payload[pk];
|
|
103
103
|
|
|
104
104
|
if (pkValue !== undefined && pkValue !== null) {
|
|
105
|
-
const tracked = session.getEntity(table, pkValue);
|
|
105
|
+
const tracked = session.getEntity(table, pkValue as string | number);
|
|
106
106
|
if (tracked) {
|
|
107
107
|
return tracked as EntityInstance<TTable>;
|
|
108
108
|
}
|
|
@@ -112,16 +112,16 @@ const ensureEntity = <TTable extends TableDef>(
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
return createEntityFromRow(session, table, row) as EntityInstance<TTable>;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const assignColumns = (table: TableDef, entity: AnyEntity, payload: AnyEntity, options: SaveGraphOptions): void => {
|
|
119
|
-
for (const key of Object.keys(table.columns)) {
|
|
120
|
-
if (payload[key] !== undefined) {
|
|
121
|
-
entity[key] = coerceColumnValue(table, key, payload[key], options);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
};
|
|
115
|
+
return createEntityFromRow(session, table, row) as EntityInstance<TTable>;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const assignColumns = (table: TableDef, entity: AnyEntity, payload: AnyEntity, options: SaveGraphOptions): void => {
|
|
119
|
+
for (const key of Object.keys(table.columns)) {
|
|
120
|
+
if (payload[key] !== undefined) {
|
|
121
|
+
entity[key] = coerceColumnValue(table, key, payload[key], options);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
125
|
|
|
126
126
|
const isEntityInCollection = (items: AnyEntity[], pkName: string, entity: AnyEntity): boolean => {
|
|
127
127
|
if (items.includes(entity)) return true;
|
|
@@ -157,13 +157,13 @@ const handleHasMany = async (
|
|
|
157
157
|
const asObj = typeof item === 'object' ? (item as AnyEntity) : { [targetPk]: item };
|
|
158
158
|
const pkValue = asObj[targetPk];
|
|
159
159
|
|
|
160
|
-
const current =
|
|
161
|
-
findInCollectionByPk(existing, targetPk, pkValue) ??
|
|
162
|
-
(pkValue !== undefined && pkValue !== null ? session.getEntity(targetTable, pkValue) : undefined);
|
|
163
|
-
|
|
164
|
-
const entity = current ?? ensureEntity(session, targetTable, asObj, options);
|
|
165
|
-
assignColumns(targetTable, entity as AnyEntity, asObj, options);
|
|
166
|
-
await applyGraphToEntity(session, targetTable, entity as AnyEntity, asObj, options);
|
|
160
|
+
const current =
|
|
161
|
+
findInCollectionByPk(existing, targetPk, pkValue) ??
|
|
162
|
+
(pkValue !== undefined && pkValue !== null ? session.getEntity(targetTable, pkValue as string | number) : undefined);
|
|
163
|
+
|
|
164
|
+
const entity = current ?? ensureEntity(session, targetTable, asObj, options);
|
|
165
|
+
assignColumns(targetTable, entity as AnyEntity, asObj, options);
|
|
166
|
+
await applyGraphToEntity(session, targetTable, entity as AnyEntity, asObj, options);
|
|
167
167
|
|
|
168
168
|
if (!isEntityInCollection(collection.getItems() as unknown as AnyEntity[], targetPk, entity as unknown as AnyEntity)) {
|
|
169
169
|
collection.attach(entity);
|
|
@@ -184,15 +184,15 @@ const handleHasMany = async (
|
|
|
184
184
|
}
|
|
185
185
|
};
|
|
186
186
|
|
|
187
|
-
const handleHasOne = async (
|
|
188
|
-
session: OrmSession,
|
|
189
|
-
root: AnyEntity,
|
|
190
|
-
relationName: string,
|
|
191
|
-
relation: HasOneRelation,
|
|
192
|
-
payload: unknown,
|
|
193
|
-
options: SaveGraphOptions
|
|
194
|
-
): Promise<void> => {
|
|
195
|
-
const ref = root[relationName] as unknown as HasOneReference<object>;
|
|
187
|
+
const handleHasOne = async (
|
|
188
|
+
session: OrmSession,
|
|
189
|
+
root: AnyEntity,
|
|
190
|
+
relationName: string,
|
|
191
|
+
relation: HasOneRelation,
|
|
192
|
+
payload: unknown,
|
|
193
|
+
options: SaveGraphOptions
|
|
194
|
+
): Promise<void> => {
|
|
195
|
+
const ref = root[relationName] as unknown as HasOneReference<object>;
|
|
196
196
|
if (payload === undefined) return;
|
|
197
197
|
if (payload === null) {
|
|
198
198
|
ref.set(null);
|
|
@@ -206,21 +206,21 @@ const handleHasOne = async (
|
|
|
206
206
|
}
|
|
207
207
|
return;
|
|
208
208
|
}
|
|
209
|
-
const attached = ref.set(payload as AnyEntity);
|
|
210
|
-
if (attached) {
|
|
211
|
-
await applyGraphToEntity(session, relation.target, attached as AnyEntity, payload as AnyEntity, options);
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
const handleBelongsTo = async (
|
|
216
|
-
session: OrmSession,
|
|
217
|
-
root: AnyEntity,
|
|
218
|
-
relationName: string,
|
|
219
|
-
relation: BelongsToRelation,
|
|
220
|
-
payload: unknown,
|
|
221
|
-
options: SaveGraphOptions
|
|
222
|
-
): Promise<void> => {
|
|
223
|
-
const ref = root[relationName] as unknown as BelongsToReference<object>;
|
|
209
|
+
const attached = ref.set(payload as AnyEntity);
|
|
210
|
+
if (attached) {
|
|
211
|
+
await applyGraphToEntity(session, relation.target, attached as AnyEntity, payload as AnyEntity, options);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const handleBelongsTo = async (
|
|
216
|
+
session: OrmSession,
|
|
217
|
+
root: AnyEntity,
|
|
218
|
+
relationName: string,
|
|
219
|
+
relation: BelongsToRelation,
|
|
220
|
+
payload: unknown,
|
|
221
|
+
options: SaveGraphOptions
|
|
222
|
+
): Promise<void> => {
|
|
223
|
+
const ref = root[relationName] as unknown as BelongsToReference<object>;
|
|
224
224
|
if (payload === undefined) return;
|
|
225
225
|
if (payload === null) {
|
|
226
226
|
ref.set(null);
|
|
@@ -234,11 +234,11 @@ const handleBelongsTo = async (
|
|
|
234
234
|
}
|
|
235
235
|
return;
|
|
236
236
|
}
|
|
237
|
-
const attached = ref.set(payload as AnyEntity);
|
|
238
|
-
if (attached) {
|
|
239
|
-
await applyGraphToEntity(session, relation.target, attached as AnyEntity, payload as AnyEntity, options);
|
|
240
|
-
}
|
|
241
|
-
};
|
|
237
|
+
const attached = ref.set(payload as AnyEntity);
|
|
238
|
+
if (attached) {
|
|
239
|
+
await applyGraphToEntity(session, relation.target, attached as AnyEntity, payload as AnyEntity, options);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
242
|
|
|
243
243
|
const handleBelongsToMany = async (
|
|
244
244
|
session: OrmSession,
|
|
@@ -265,14 +265,14 @@ const handleBelongsToMany = async (
|
|
|
265
265
|
continue;
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
const asObj = item as AnyEntity;
|
|
269
|
-
const pkValue = asObj[targetPk];
|
|
270
|
-
const entity = pkValue !== undefined && pkValue !== null
|
|
271
|
-
? session.getEntity(targetTable, pkValue) ?? ensureEntity(session, targetTable, asObj, options)
|
|
272
|
-
: ensureEntity(session, targetTable, asObj, options);
|
|
273
|
-
|
|
274
|
-
assignColumns(targetTable, entity as AnyEntity, asObj, options);
|
|
275
|
-
await applyGraphToEntity(session, targetTable, entity as AnyEntity, asObj, options);
|
|
268
|
+
const asObj = item as AnyEntity;
|
|
269
|
+
const pkValue = asObj[targetPk];
|
|
270
|
+
const entity = pkValue !== undefined && pkValue !== null
|
|
271
|
+
? session.getEntity(targetTable, pkValue as string | number) ?? ensureEntity(session, targetTable, asObj, options)
|
|
272
|
+
: ensureEntity(session, targetTable, asObj, options);
|
|
273
|
+
|
|
274
|
+
assignColumns(targetTable, entity as AnyEntity, asObj, options);
|
|
275
|
+
await applyGraphToEntity(session, targetTable, entity as AnyEntity, asObj, options);
|
|
276
276
|
|
|
277
277
|
if (!isEntityInCollection(collection.getItems() as unknown as AnyEntity[], targetPk, entity as unknown as AnyEntity)) {
|
|
278
278
|
collection.attach(entity);
|
|
@@ -314,36 +314,36 @@ const applyRelation = async (
|
|
|
314
314
|
}
|
|
315
315
|
};
|
|
316
316
|
|
|
317
|
-
const applyGraphToEntity = async (
|
|
318
|
-
session: OrmSession,
|
|
319
|
-
table: TableDef,
|
|
320
|
-
entity: AnyEntity,
|
|
321
|
-
payload: AnyEntity,
|
|
322
|
-
options: SaveGraphOptions
|
|
323
|
-
): Promise<void> => {
|
|
324
|
-
assignColumns(table, entity, payload, options);
|
|
325
|
-
|
|
326
|
-
for (const [relationName, relation] of Object.entries(table.relations)) {
|
|
327
|
-
if (!(relationName in payload)) continue;
|
|
328
|
-
await applyRelation(session, table, entity, relationName, relation as RelationDef, payload[relationName], options);
|
|
329
|
-
}
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
export const saveGraph = async <TTable extends TableDef>(
|
|
333
|
-
session: OrmSession,
|
|
334
|
-
entityClass: EntityConstructor,
|
|
335
|
-
payload: AnyEntity,
|
|
336
|
-
options: SaveGraphOptions = {}
|
|
337
|
-
): Promise<EntityInstance<TTable>> => {
|
|
317
|
+
const applyGraphToEntity = async (
|
|
318
|
+
session: OrmSession,
|
|
319
|
+
table: TableDef,
|
|
320
|
+
entity: AnyEntity,
|
|
321
|
+
payload: AnyEntity,
|
|
322
|
+
options: SaveGraphOptions
|
|
323
|
+
): Promise<void> => {
|
|
324
|
+
assignColumns(table, entity, payload, options);
|
|
325
|
+
|
|
326
|
+
for (const [relationName, relation] of Object.entries(table.relations)) {
|
|
327
|
+
if (!(relationName in payload)) continue;
|
|
328
|
+
await applyRelation(session, table, entity, relationName, relation as RelationDef, payload[relationName], options);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
export const saveGraph = async <TTable extends TableDef>(
|
|
333
|
+
session: OrmSession,
|
|
334
|
+
entityClass: EntityConstructor,
|
|
335
|
+
payload: AnyEntity,
|
|
336
|
+
options: SaveGraphOptions = {}
|
|
337
|
+
): Promise<EntityInstance<TTable>> => {
|
|
338
338
|
const table = getTableDefFromEntity(entityClass);
|
|
339
339
|
if (!table) {
|
|
340
340
|
throw new Error('Entity metadata has not been bootstrapped');
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
-
const root = ensureEntity<TTable>(session, table as TTable, payload, options);
|
|
344
|
-
await applyGraphToEntity(session, table, root as AnyEntity, payload, options);
|
|
345
|
-
return root;
|
|
346
|
-
};
|
|
343
|
+
const root = ensureEntity<TTable>(session, table as TTable, payload, options);
|
|
344
|
+
await applyGraphToEntity(session, table, root as AnyEntity, payload, options);
|
|
345
|
+
return root;
|
|
346
|
+
};
|
|
347
347
|
|
|
348
348
|
/**
|
|
349
349
|
|
|
@@ -361,7 +361,7 @@ export const saveGraph = async <TTable extends TableDef>(
|
|
|
361
361
|
|
|
362
362
|
*/
|
|
363
363
|
|
|
364
|
-
export const saveGraphInternal = async <TCtor extends EntityConstructor>(
|
|
364
|
+
export const saveGraphInternal = async <TCtor extends EntityConstructor>(
|
|
365
365
|
|
|
366
366
|
session: OrmSession,
|
|
367
367
|
|
|
@@ -373,7 +373,7 @@ export const saveGraphInternal = async <TCtor extends EntityConstructor>(
|
|
|
373
373
|
|
|
374
374
|
): Promise<InstanceType<TCtor>> => {
|
|
375
375
|
|
|
376
|
-
const table = getTableDefFromEntity(entityClass);
|
|
376
|
+
const table = getTableDefFromEntity(entityClass);
|
|
377
377
|
|
|
378
378
|
if (!table) {
|
|
379
379
|
|
|
@@ -381,10 +381,10 @@ export const saveGraphInternal = async <TCtor extends EntityConstructor>(
|
|
|
381
381
|
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
-
const root = ensureEntity(session, table, payload, options);
|
|
385
|
-
|
|
386
|
-
await applyGraphToEntity(session, table, root as AnyEntity, payload, options);
|
|
387
|
-
|
|
388
|
-
return root as unknown as InstanceType<TCtor>;
|
|
389
|
-
|
|
390
|
-
};
|
|
384
|
+
const root = ensureEntity(session, table, payload, options);
|
|
385
|
+
|
|
386
|
+
await applyGraphToEntity(session, table, root as AnyEntity, payload, options);
|
|
387
|
+
|
|
388
|
+
return root as unknown as InstanceType<TCtor>;
|
|
389
|
+
|
|
390
|
+
};
|
package/src/orm/unit-of-work.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ColumnNode, eq } from '../core/ast/expression.js';
|
|
1
|
+
import { ColumnNode, eq } from '../core/ast/expression.js';
|
|
2
|
+
import type { ValueOperandInput } from '../core/ast/expression.js';
|
|
2
3
|
import type { Dialect, CompiledQuery } from '../core/dialect/abstract.js';
|
|
3
4
|
import { InsertQueryBuilder } from '../query-builder/insert.js';
|
|
4
5
|
import { UpdateQueryBuilder } from '../query-builder/update.js';
|
|
@@ -13,8 +14,8 @@ import type { TrackedEntity } from './runtime-types.js';
|
|
|
13
14
|
/**
|
|
14
15
|
* Unit of Work pattern implementation for tracking entity changes.
|
|
15
16
|
*/
|
|
16
|
-
export class UnitOfWork {
|
|
17
|
-
private readonly trackedEntities = new Map<
|
|
17
|
+
export class UnitOfWork {
|
|
18
|
+
private readonly trackedEntities = new Map<object, TrackedEntity>();
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Creates a new UnitOfWork instance.
|
|
@@ -51,9 +52,9 @@ export class UnitOfWork {
|
|
|
51
52
|
* @param pk - The primary key value
|
|
52
53
|
* @returns The entity or undefined if not found
|
|
53
54
|
*/
|
|
54
|
-
getEntity(table: TableDef, pk: string | number):
|
|
55
|
-
return this.identityMap.getEntity(table, pk);
|
|
56
|
-
}
|
|
55
|
+
getEntity(table: TableDef, pk: string | number): object | undefined {
|
|
56
|
+
return this.identityMap.getEntity(table, pk);
|
|
57
|
+
}
|
|
57
58
|
|
|
58
59
|
/**
|
|
59
60
|
* Gets all tracked entities for a specific table.
|
|
@@ -69,9 +70,9 @@ export class UnitOfWork {
|
|
|
69
70
|
* @param entity - The entity to find
|
|
70
71
|
* @returns The tracked entity or undefined if not found
|
|
71
72
|
*/
|
|
72
|
-
findTracked(entity:
|
|
73
|
-
return this.trackedEntities.get(entity);
|
|
74
|
-
}
|
|
73
|
+
findTracked(entity: object): TrackedEntity | undefined {
|
|
74
|
+
return this.trackedEntities.get(entity);
|
|
75
|
+
}
|
|
75
76
|
|
|
76
77
|
/**
|
|
77
78
|
* Sets an entity in the identity map.
|
|
@@ -79,9 +80,9 @@ export class UnitOfWork {
|
|
|
79
80
|
* @param pk - The primary key value
|
|
80
81
|
* @param entity - The entity instance
|
|
81
82
|
*/
|
|
82
|
-
setEntity(table: TableDef, pk: string | number, entity:
|
|
83
|
-
if (pk === null || pk === undefined) return;
|
|
84
|
-
let tracked = this.trackedEntities.get(entity);
|
|
83
|
+
setEntity(table: TableDef, pk: string | number, entity: object): void {
|
|
84
|
+
if (pk === null || pk === undefined) return;
|
|
85
|
+
let tracked = this.trackedEntities.get(entity);
|
|
85
86
|
if (!tracked) {
|
|
86
87
|
tracked = {
|
|
87
88
|
table,
|
|
@@ -104,7 +105,7 @@ export class UnitOfWork {
|
|
|
104
105
|
* @param entity - The entity instance
|
|
105
106
|
* @param pk - Optional primary key value
|
|
106
107
|
*/
|
|
107
|
-
trackNew(table: TableDef, entity:
|
|
108
|
+
trackNew(table: TableDef, entity: object, pk?: string | number): void {
|
|
108
109
|
const tracked: TrackedEntity = {
|
|
109
110
|
table,
|
|
110
111
|
entity,
|
|
@@ -124,7 +125,7 @@ export class UnitOfWork {
|
|
|
124
125
|
* @param pk - The primary key value
|
|
125
126
|
* @param entity - The entity instance
|
|
126
127
|
*/
|
|
127
|
-
trackManaged(table: TableDef, pk: string | number, entity:
|
|
128
|
+
trackManaged(table: TableDef, pk: string | number, entity: object): void {
|
|
128
129
|
const tracked: TrackedEntity = {
|
|
129
130
|
table,
|
|
130
131
|
entity,
|
|
@@ -140,22 +141,22 @@ export class UnitOfWork {
|
|
|
140
141
|
* Marks an entity as dirty (modified).
|
|
141
142
|
* @param entity - The entity to mark as dirty
|
|
142
143
|
*/
|
|
143
|
-
markDirty(entity:
|
|
144
|
-
const tracked = this.trackedEntities.get(entity);
|
|
145
|
-
if (!tracked) return;
|
|
146
|
-
if (tracked.status === EntityStatus.New || tracked.status === EntityStatus.Removed) return;
|
|
147
|
-
tracked.status = EntityStatus.Dirty;
|
|
148
|
-
}
|
|
144
|
+
markDirty(entity: object): void {
|
|
145
|
+
const tracked = this.trackedEntities.get(entity);
|
|
146
|
+
if (!tracked) return;
|
|
147
|
+
if (tracked.status === EntityStatus.New || tracked.status === EntityStatus.Removed) return;
|
|
148
|
+
tracked.status = EntityStatus.Dirty;
|
|
149
|
+
}
|
|
149
150
|
|
|
150
151
|
/**
|
|
151
152
|
* Marks an entity as removed.
|
|
152
153
|
* @param entity - The entity to mark as removed
|
|
153
154
|
*/
|
|
154
|
-
markRemoved(entity:
|
|
155
|
-
const tracked = this.trackedEntities.get(entity);
|
|
156
|
-
if (!tracked) return;
|
|
157
|
-
tracked.status = EntityStatus.Removed;
|
|
158
|
-
}
|
|
155
|
+
markRemoved(entity: object): void {
|
|
156
|
+
const tracked = this.trackedEntities.get(entity);
|
|
157
|
+
if (!tracked) return;
|
|
158
|
+
tracked.status = EntityStatus.Removed;
|
|
159
|
+
}
|
|
159
160
|
|
|
160
161
|
/**
|
|
161
162
|
* Flushes pending changes to the database.
|
|
@@ -194,8 +195,8 @@ export class UnitOfWork {
|
|
|
194
195
|
private async flushInsert(tracked: TrackedEntity): Promise<void> {
|
|
195
196
|
await this.runHook(tracked.table.hooks?.beforeInsert, tracked);
|
|
196
197
|
|
|
197
|
-
const payload = this.extractColumns(tracked.table, tracked.entity as Record<string, unknown>);
|
|
198
|
-
let builder = new InsertQueryBuilder(tracked.table).values(payload);
|
|
198
|
+
const payload = this.extractColumns(tracked.table, tracked.entity as Record<string, unknown>);
|
|
199
|
+
let builder = new InsertQueryBuilder(tracked.table).values(payload as Record<string, ValueOperandInput>);
|
|
199
200
|
if (this.dialect.supportsReturning()) {
|
|
200
201
|
builder = builder.returning(...this.getReturningColumns(tracked.table));
|
|
201
202
|
}
|
|
@@ -287,16 +288,16 @@ export class UnitOfWork {
|
|
|
287
288
|
* @param tracked - The tracked entity
|
|
288
289
|
* @returns Object with changed column values
|
|
289
290
|
*/
|
|
290
|
-
private computeChanges(tracked: TrackedEntity): Record<string, unknown> {
|
|
291
|
-
const snapshot = tracked.original ?? {};
|
|
292
|
-
const changes: Record<string, unknown> = {};
|
|
293
|
-
for (const column of Object.keys(tracked.table.columns)) {
|
|
294
|
-
const current = tracked.entity[column];
|
|
295
|
-
if (snapshot[column] !== current) {
|
|
296
|
-
changes[column] = current;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
return changes;
|
|
291
|
+
private computeChanges(tracked: TrackedEntity): Record<string, unknown> {
|
|
292
|
+
const snapshot = tracked.original ?? {};
|
|
293
|
+
const changes: Record<string, unknown> = {};
|
|
294
|
+
for (const column of Object.keys(tracked.table.columns)) {
|
|
295
|
+
const current = (tracked.entity as Record<string, unknown>)[column];
|
|
296
|
+
if (snapshot[column] !== current) {
|
|
297
|
+
changes[column] = current;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return changes;
|
|
300
301
|
}
|
|
301
302
|
|
|
302
303
|
/**
|
|
@@ -342,18 +343,18 @@ export class UnitOfWork {
|
|
|
342
343
|
* @param tracked - The tracked entity
|
|
343
344
|
* @param results - Query results
|
|
344
345
|
*/
|
|
345
|
-
private applyReturningResults(tracked: TrackedEntity, results: QueryResult[]): void {
|
|
346
|
-
if (!this.dialect.supportsReturning()) return;
|
|
347
|
-
const first = results[0];
|
|
348
|
-
if (!first || first.values.length === 0) return;
|
|
349
|
-
|
|
350
|
-
const row = first.values[0];
|
|
351
|
-
for (let i = 0; i < first.columns.length; i++) {
|
|
352
|
-
const columnName = this.normalizeColumnName(first.columns[i]);
|
|
353
|
-
if (!(columnName in tracked.table.columns)) continue;
|
|
354
|
-
tracked.entity[columnName] = row[i];
|
|
355
|
-
}
|
|
356
|
-
}
|
|
346
|
+
private applyReturningResults(tracked: TrackedEntity, results: QueryResult[]): void {
|
|
347
|
+
if (!this.dialect.supportsReturning()) return;
|
|
348
|
+
const first = results[0];
|
|
349
|
+
if (!first || first.values.length === 0) return;
|
|
350
|
+
|
|
351
|
+
const row = first.values[0];
|
|
352
|
+
for (let i = 0; i < first.columns.length; i++) {
|
|
353
|
+
const columnName = this.normalizeColumnName(first.columns[i]);
|
|
354
|
+
if (!(columnName in tracked.table.columns)) continue;
|
|
355
|
+
(tracked.entity as Record<string, unknown>)[columnName] = row[i];
|
|
356
|
+
}
|
|
357
|
+
}
|
|
357
358
|
|
|
358
359
|
/**
|
|
359
360
|
* Normalizes a column name by removing quotes and table prefixes.
|
|
@@ -394,10 +395,11 @@ export class UnitOfWork {
|
|
|
394
395
|
* @param tracked - The tracked entity
|
|
395
396
|
* @returns Primary key value or null
|
|
396
397
|
*/
|
|
397
|
-
private getPrimaryKeyValue(tracked: TrackedEntity): string | number | null {
|
|
398
|
-
const key = findPrimaryKey(tracked.table);
|
|
399
|
-
const val = tracked.entity[key];
|
|
400
|
-
if (val === undefined || val === null) return null;
|
|
401
|
-
return
|
|
402
|
-
|
|
403
|
-
}
|
|
398
|
+
private getPrimaryKeyValue(tracked: TrackedEntity): string | number | null {
|
|
399
|
+
const key = findPrimaryKey(tracked.table);
|
|
400
|
+
const val = (tracked.entity as Record<string, unknown>)[key];
|
|
401
|
+
if (val === undefined || val === null) return null;
|
|
402
|
+
if (typeof val !== 'string' && typeof val !== 'number') return null;
|
|
403
|
+
return val;
|
|
404
|
+
}
|
|
405
|
+
}
|