metal-orm 1.1.20 → 1.1.22
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/dist/index.cjs +124 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -31
- package/dist/index.d.ts +58 -31
- package/dist/index.js +124 -84
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ddl/introspect/catalogs/mssql.ts +0 -8
- package/src/core/ddl/introspect/catalogs/mysql.ts +0 -5
- package/src/core/ddl/introspect/catalogs/postgres.ts +0 -9
- package/src/core/ddl/introspect/catalogs/sqlite.ts +0 -3
- package/src/decorators/entity.ts +2 -3
- package/src/index.ts +2 -1
- package/src/orm/entity-metadata.ts +6 -11
- package/src/orm/lifecycle.ts +20 -0
- package/src/orm/orm-session.ts +37 -3
- package/src/orm/relation-change-processor.ts +15 -11
- package/src/orm/unit-of-work.ts +15 -9
- package/src/schema/table.ts +10 -25
- package/src/tree/tree-manager.ts +111 -43
package/package.json
CHANGED
|
@@ -17,7 +17,6 @@ export const SysColumns = defineTable(
|
|
|
17
17
|
user_type_id: col.int()
|
|
18
18
|
},
|
|
19
19
|
{},
|
|
20
|
-
undefined,
|
|
21
20
|
{ schema: 'sys' }
|
|
22
21
|
);
|
|
23
22
|
|
|
@@ -30,7 +29,6 @@ export const SysTables = defineTable(
|
|
|
30
29
|
is_ms_shipped: col.boolean()
|
|
31
30
|
},
|
|
32
31
|
{},
|
|
33
|
-
undefined,
|
|
34
32
|
{ schema: 'sys' }
|
|
35
33
|
);
|
|
36
34
|
|
|
@@ -41,7 +39,6 @@ export const SysSchemas = defineTable(
|
|
|
41
39
|
name: col.varchar(255)
|
|
42
40
|
},
|
|
43
41
|
{},
|
|
44
|
-
undefined,
|
|
45
42
|
{ schema: 'sys' }
|
|
46
43
|
);
|
|
47
44
|
|
|
@@ -52,7 +49,6 @@ export const SysTypes = defineTable(
|
|
|
52
49
|
name: col.varchar(255)
|
|
53
50
|
},
|
|
54
51
|
{},
|
|
55
|
-
undefined,
|
|
56
52
|
{ schema: 'sys' }
|
|
57
53
|
);
|
|
58
54
|
|
|
@@ -69,7 +65,6 @@ export const SysIndexes = defineTable(
|
|
|
69
65
|
is_hypothetical: col.boolean()
|
|
70
66
|
},
|
|
71
67
|
{},
|
|
72
|
-
undefined,
|
|
73
68
|
{ schema: 'sys' }
|
|
74
69
|
);
|
|
75
70
|
|
|
@@ -82,7 +77,6 @@ export const SysIndexColumns = defineTable(
|
|
|
82
77
|
key_ordinal: col.int()
|
|
83
78
|
},
|
|
84
79
|
{},
|
|
85
|
-
undefined,
|
|
86
80
|
{ schema: 'sys' }
|
|
87
81
|
);
|
|
88
82
|
|
|
@@ -95,7 +89,6 @@ export const SysForeignKeys = defineTable(
|
|
|
95
89
|
update_referential_action_desc: col.varchar(64)
|
|
96
90
|
},
|
|
97
91
|
{},
|
|
98
|
-
undefined,
|
|
99
92
|
{ schema: 'sys' }
|
|
100
93
|
);
|
|
101
94
|
|
|
@@ -110,7 +103,6 @@ export const SysForeignKeyColumns = defineTable(
|
|
|
110
103
|
constraint_column_id: col.int()
|
|
111
104
|
},
|
|
112
105
|
{},
|
|
113
|
-
undefined,
|
|
114
106
|
{ schema: 'sys' }
|
|
115
107
|
);
|
|
116
108
|
|
|
@@ -12,7 +12,6 @@ export const InformationSchemaTables = defineTable(
|
|
|
12
12
|
table_comment: col.varchar(1024)
|
|
13
13
|
},
|
|
14
14
|
{},
|
|
15
|
-
undefined,
|
|
16
15
|
{ schema: INFORMATION_SCHEMA }
|
|
17
16
|
);
|
|
18
17
|
|
|
@@ -32,7 +31,6 @@ export const InformationSchemaColumns = defineTable(
|
|
|
32
31
|
ordinal_position: col.int()
|
|
33
32
|
},
|
|
34
33
|
{},
|
|
35
|
-
undefined,
|
|
36
34
|
{ schema: INFORMATION_SCHEMA }
|
|
37
35
|
);
|
|
38
36
|
|
|
@@ -51,7 +49,6 @@ export const InformationSchemaKeyColumnUsage = defineTable(
|
|
|
51
49
|
referenced_column_name: col.varchar(255)
|
|
52
50
|
},
|
|
53
51
|
{},
|
|
54
|
-
undefined,
|
|
55
52
|
{ schema: INFORMATION_SCHEMA }
|
|
56
53
|
);
|
|
57
54
|
|
|
@@ -65,7 +62,6 @@ export const InformationSchemaReferentialConstraints = defineTable(
|
|
|
65
62
|
update_rule: col.varchar(255)
|
|
66
63
|
},
|
|
67
64
|
{},
|
|
68
|
-
undefined,
|
|
69
65
|
{ schema: INFORMATION_SCHEMA }
|
|
70
66
|
);
|
|
71
67
|
|
|
@@ -81,7 +77,6 @@ export const InformationSchemaStatistics = defineTable(
|
|
|
81
77
|
seq_in_index: col.int()
|
|
82
78
|
},
|
|
83
79
|
{},
|
|
84
|
-
undefined,
|
|
85
80
|
{ schema: INFORMATION_SCHEMA }
|
|
86
81
|
);
|
|
87
82
|
|
|
@@ -14,7 +14,6 @@ export const PgInformationSchemaColumns = defineTable(
|
|
|
14
14
|
ordinal_position: col.int()
|
|
15
15
|
},
|
|
16
16
|
{},
|
|
17
|
-
undefined,
|
|
18
17
|
{ schema: 'information_schema' }
|
|
19
18
|
);
|
|
20
19
|
|
|
@@ -27,7 +26,6 @@ export const PgClass = defineTable(
|
|
|
27
26
|
relkind: col.varchar(1)
|
|
28
27
|
},
|
|
29
28
|
{},
|
|
30
|
-
undefined,
|
|
31
29
|
{ schema: 'pg_catalog' }
|
|
32
30
|
);
|
|
33
31
|
|
|
@@ -38,7 +36,6 @@ export const PgNamespace = defineTable(
|
|
|
38
36
|
nspname: col.varchar(255)
|
|
39
37
|
},
|
|
40
38
|
{},
|
|
41
|
-
undefined,
|
|
42
39
|
{ schema: 'pg_catalog' }
|
|
43
40
|
);
|
|
44
41
|
|
|
@@ -52,7 +49,6 @@ export const PgIndex = defineTable(
|
|
|
52
49
|
indpred: col.varchar(1024)
|
|
53
50
|
},
|
|
54
51
|
{},
|
|
55
|
-
undefined,
|
|
56
52
|
{ schema: 'pg_catalog' }
|
|
57
53
|
);
|
|
58
54
|
|
|
@@ -64,7 +60,6 @@ export const PgAttribute = defineTable(
|
|
|
64
60
|
attnum: col.int()
|
|
65
61
|
},
|
|
66
62
|
{},
|
|
67
|
-
undefined,
|
|
68
63
|
{ schema: 'pg_catalog' }
|
|
69
64
|
);
|
|
70
65
|
|
|
@@ -80,7 +75,6 @@ export const PgTableConstraints = defineTable(
|
|
|
80
75
|
constraint_type: col.varchar(255)
|
|
81
76
|
},
|
|
82
77
|
{},
|
|
83
|
-
undefined,
|
|
84
78
|
{ schema: 'information_schema' }
|
|
85
79
|
);
|
|
86
80
|
|
|
@@ -97,7 +91,6 @@ export const PgKeyColumnUsage = defineTable(
|
|
|
97
91
|
ordinal_position: col.int()
|
|
98
92
|
},
|
|
99
93
|
{},
|
|
100
|
-
undefined,
|
|
101
94
|
{ schema: 'information_schema' }
|
|
102
95
|
);
|
|
103
96
|
|
|
@@ -113,7 +106,6 @@ export const PgConstraintColumnUsage = defineTable(
|
|
|
113
106
|
column_name: col.varchar(255)
|
|
114
107
|
},
|
|
115
108
|
{},
|
|
116
|
-
undefined,
|
|
117
109
|
{ schema: 'information_schema' }
|
|
118
110
|
);
|
|
119
111
|
|
|
@@ -131,7 +123,6 @@ export const PgReferentialConstraints = defineTable(
|
|
|
131
123
|
delete_rule: col.varchar(64)
|
|
132
124
|
},
|
|
133
125
|
{},
|
|
134
|
-
undefined,
|
|
135
126
|
{ schema: 'information_schema' }
|
|
136
127
|
);
|
|
137
128
|
|
|
@@ -14,7 +14,6 @@ export const SqliteMaster = defineTable(
|
|
|
14
14
|
sql: col.varchar(4096) // Original DDL
|
|
15
15
|
},
|
|
16
16
|
{},
|
|
17
|
-
undefined,
|
|
18
17
|
{ schema: undefined }
|
|
19
18
|
);
|
|
20
19
|
|
|
@@ -26,7 +25,6 @@ export const SqliteSequence = defineTable(
|
|
|
26
25
|
seq: col.int() // Last autoincrement value
|
|
27
26
|
},
|
|
28
27
|
{},
|
|
29
|
-
undefined,
|
|
30
28
|
{ schema: undefined }
|
|
31
29
|
);
|
|
32
30
|
|
|
@@ -39,7 +37,6 @@ export const SqliteStat1 = defineTable(
|
|
|
39
37
|
stat: col.varchar(255) // Statistics string
|
|
40
38
|
},
|
|
41
39
|
{},
|
|
42
|
-
undefined,
|
|
43
40
|
{ schema: undefined }
|
|
44
41
|
);
|
|
45
42
|
|
package/src/decorators/entity.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { TableHooks } from '../schema/table.js';
|
|
2
1
|
import { RelationKinds } from '../schema/relation.js';
|
|
3
2
|
import {
|
|
4
3
|
addColumnMetadata,
|
|
@@ -15,7 +14,6 @@ import { syncTreeEntityMetadata } from '../tree/tree-decorator.js';
|
|
|
15
14
|
*/
|
|
16
15
|
export interface EntityOptions {
|
|
17
16
|
tableName?: string;
|
|
18
|
-
hooks?: TableHooks;
|
|
19
17
|
/** Entity type: 'table' (default) or 'view'. Views are read-only. */
|
|
20
18
|
type?: 'table' | 'view';
|
|
21
19
|
}
|
|
@@ -42,6 +40,7 @@ const deriveTableNameFromConstructor = (ctor: EntityConstructor<unknown>): strin
|
|
|
42
40
|
|
|
43
41
|
/**
|
|
44
42
|
* Class decorator to mark a class as an entity and configure its table mapping.
|
|
43
|
+
* Runtime lifecycle hooks are registered on OrmSession, not entity metadata.
|
|
45
44
|
* @param options - Configuration options for the entity.
|
|
46
45
|
* @returns A class decorator that registers the entity metadata.
|
|
47
46
|
*/
|
|
@@ -52,7 +51,7 @@ export function Entity(options: EntityOptions = {}) {
|
|
|
52
51
|
): T {
|
|
53
52
|
const ctor = value;
|
|
54
53
|
const tableName = options.tableName ?? deriveTableNameFromConstructor(ctor);
|
|
55
|
-
setEntityTableName(ctor, tableName, options.
|
|
54
|
+
setEntityTableName(ctor, tableName, options.type);
|
|
56
55
|
|
|
57
56
|
const bag = context ? readMetadataBag(context) : readMetadataBagFromConstructor(ctor);
|
|
58
57
|
if (bag) {
|
package/src/index.ts
CHANGED
|
@@ -38,6 +38,7 @@ export * from './orm/hydration.js';
|
|
|
38
38
|
export * from './codegen/typescript.js';
|
|
39
39
|
export * from './orm/orm-session.js';
|
|
40
40
|
export * from './orm/orm.js';
|
|
41
|
+
export * from './orm/lifecycle.js';
|
|
41
42
|
export * from './orm/column-introspection.js';
|
|
42
43
|
export * from './orm/entity.js';
|
|
43
44
|
export * from './orm/lazy-batch.js';
|
|
@@ -84,4 +85,4 @@ export * from './tree/index.js';
|
|
|
84
85
|
export * from './cache/index.js';
|
|
85
86
|
|
|
86
87
|
// Bulk operations module
|
|
87
|
-
export * from './bulk/index.js';
|
|
88
|
+
export * from './bulk/index.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ColumnDef } from '../schema/column-types.js';
|
|
2
|
-
import { defineTable, TableDef
|
|
2
|
+
import { defineTable, TableDef } from '../schema/table.js';
|
|
3
3
|
import { CascadeMode, RelationKinds } from '../schema/relation.js';
|
|
4
4
|
import type { TransformerMetadata } from '../decorators/transformers/transformer-metadata.js';
|
|
5
5
|
|
|
@@ -153,7 +153,7 @@ export interface MorphManyRelationMetadata {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
/**
|
|
156
|
-
* Union type for all relation metadata.
|
|
156
|
+
* Union type for all relation metadata types.
|
|
157
157
|
*/
|
|
158
158
|
export type RelationMetadata =
|
|
159
159
|
| HasManyRelationMetadata
|
|
@@ -169,6 +169,7 @@ export type EntityType = 'table' | 'view';
|
|
|
169
169
|
|
|
170
170
|
/**
|
|
171
171
|
* Metadata for entities.
|
|
172
|
+
* Runtime lifecycle hooks intentionally live on OrmSession rather than here.
|
|
172
173
|
* @template TColumns - The columns type
|
|
173
174
|
*/
|
|
174
175
|
export interface EntityMetadata<TColumns extends Record<string, ColumnDefLike> = Record<string, ColumnDefLike>> {
|
|
@@ -184,8 +185,6 @@ export interface EntityMetadata<TColumns extends Record<string, ColumnDefLike> =
|
|
|
184
185
|
relations: Record<string, RelationMetadata>;
|
|
185
186
|
/** The transformers */
|
|
186
187
|
transformers: Record<string, TransformerMetadata>;
|
|
187
|
-
/** Optional hooks */
|
|
188
|
-
hooks?: TableHooks;
|
|
189
188
|
/** Optional table definition */
|
|
190
189
|
table?: TableDef<MaterializeColumns<TColumns>>;
|
|
191
190
|
}
|
|
@@ -295,25 +294,21 @@ export const addTransformerMetadata = (
|
|
|
295
294
|
};
|
|
296
295
|
|
|
297
296
|
/**
|
|
298
|
-
* Sets the table name
|
|
297
|
+
* Sets the table name and type for an entity.
|
|
298
|
+
* Lifecycle policy is session-bound and is not persisted in metadata.
|
|
299
299
|
* @param target - The entity constructor
|
|
300
300
|
* @param tableName - The table name
|
|
301
|
-
* @param hooks - Optional table hooks
|
|
302
301
|
* @param type - Entity type: 'table' or 'view'
|
|
303
302
|
*/
|
|
304
303
|
export const setEntityTableName = (
|
|
305
304
|
target: EntityConstructor,
|
|
306
305
|
tableName: string,
|
|
307
|
-
hooks?: TableHooks,
|
|
308
306
|
type?: EntityType
|
|
309
307
|
): void => {
|
|
310
308
|
const meta = ensureEntityMetadata(target);
|
|
311
309
|
if (tableName && tableName.length > 0) {
|
|
312
310
|
meta.tableName = tableName;
|
|
313
311
|
}
|
|
314
|
-
if (hooks) {
|
|
315
|
-
meta.hooks = hooks;
|
|
316
|
-
}
|
|
317
312
|
if (type) {
|
|
318
313
|
meta.type = type;
|
|
319
314
|
}
|
|
@@ -340,7 +335,7 @@ export const buildTableDef = <TColumns extends Record<string, ColumnDefLike>>(me
|
|
|
340
335
|
} as ColumnDef;
|
|
341
336
|
}
|
|
342
337
|
|
|
343
|
-
const table = defineTable(meta.tableName, columns as MaterializeColumns<TColumns
|
|
338
|
+
const table = defineTable(meta.tableName, columns as MaterializeColumns<TColumns>);
|
|
344
339
|
meta.table = table;
|
|
345
340
|
return table;
|
|
346
341
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TableDef } from '../schema/table.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Entity lifecycle hooks executed by the Unit of Work.
|
|
5
|
+
*
|
|
6
|
+
* Hooks are runtime/session configuration. They are intentionally not stored on
|
|
7
|
+
* TableDef so the same mapping can be used by independent sessions with
|
|
8
|
+
* different lifecycle policies.
|
|
9
|
+
*/
|
|
10
|
+
export interface TableHooks<TEntity = unknown, TContext = unknown> {
|
|
11
|
+
beforeInsert?(ctx: TContext, entity: TEntity): Promise<void> | void;
|
|
12
|
+
afterInsert?(ctx: TContext, entity: TEntity): Promise<void> | void;
|
|
13
|
+
beforeUpdate?(ctx: TContext, entity: TEntity): Promise<void> | void;
|
|
14
|
+
afterUpdate?(ctx: TContext, entity: TEntity): Promise<void> | void;
|
|
15
|
+
beforeDelete?(ctx: TContext, entity: TEntity): Promise<void> | void;
|
|
16
|
+
afterDelete?(ctx: TContext, entity: TEntity): Promise<void> | void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @internal */
|
|
20
|
+
export type TableHookResolver = (table: TableDef) => TableHooks | undefined;
|
package/src/orm/orm-session.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { EntityConstructor } from './entity-metadata.js';
|
|
|
13
13
|
import { Orm } from './orm.js';
|
|
14
14
|
import { IdentityMap } from './identity-map.js';
|
|
15
15
|
import { UnitOfWork } from './unit-of-work.js';
|
|
16
|
+
import type { TableHooks } from './lifecycle.js';
|
|
16
17
|
import { DomainEventBus, DomainEventHandler, InitialHandlers } from './domain-event-bus.js';
|
|
17
18
|
import { RelationChangeProcessor } from './relation-change-processor.js';
|
|
18
19
|
import { createQueryLoggingExecutor, QueryLogger } from './query-logger.js';
|
|
@@ -38,6 +39,12 @@ const NESTED_TRANSACTIONS_REQUIRE_SAVEPOINTS =
|
|
|
38
39
|
const ROLLBACK_ONLY_TRANSACTION =
|
|
39
40
|
'Cannot commit transaction because an inner transaction failed';
|
|
40
41
|
|
|
42
|
+
type LifecycleHookTarget = TableDef | EntityConstructor<object>;
|
|
43
|
+
type LifecycleHookEntity<TTarget extends LifecycleHookTarget> =
|
|
44
|
+
TTarget extends TableDef ? EntityInstance<TTarget> :
|
|
45
|
+
TTarget extends EntityConstructor<object> ? InstanceType<TTarget> :
|
|
46
|
+
never;
|
|
47
|
+
|
|
41
48
|
/**
|
|
42
49
|
* Interface for ORM interceptors that allow hooking into the flush lifecycle.
|
|
43
50
|
*/
|
|
@@ -106,6 +113,7 @@ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements Entit
|
|
|
106
113
|
readonly tenantId?: string | number;
|
|
107
114
|
|
|
108
115
|
private readonly interceptors: OrmInterceptor[];
|
|
116
|
+
private readonly tableHooks = new WeakMap<TableDef, TableHooks>();
|
|
109
117
|
private saveGraphDefaults?: SaveGraphSessionOptions;
|
|
110
118
|
private transactionDepth = 0;
|
|
111
119
|
private savepointCounter = 0;
|
|
@@ -121,7 +129,13 @@ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements Entit
|
|
|
121
129
|
this.interceptors = [...(opts.interceptors ?? [])];
|
|
122
130
|
|
|
123
131
|
this.identityMap = new IdentityMap();
|
|
124
|
-
this.unitOfWork = new UnitOfWork(
|
|
132
|
+
this.unitOfWork = new UnitOfWork(
|
|
133
|
+
this.orm.dialect,
|
|
134
|
+
this.executor,
|
|
135
|
+
this.identityMap,
|
|
136
|
+
() => this,
|
|
137
|
+
table => this.tableHooks.get(table)
|
|
138
|
+
);
|
|
125
139
|
this.relationChanges = new RelationChangeProcessor(this.unitOfWork, this.orm.dialect, this.executor);
|
|
126
140
|
this.domainEvents = new DomainEventBus<E, OrmSession<E>>(opts.domainEventHandlers);
|
|
127
141
|
this.cacheManager = opts.cacheManager;
|
|
@@ -250,6 +264,24 @@ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements Entit
|
|
|
250
264
|
return this.unitOfWork.getEntitiesForTable(table);
|
|
251
265
|
}
|
|
252
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Registers INSERT/UPDATE/DELETE lifecycle hooks for this Session only.
|
|
269
|
+
* The target can be a TableDef or a decorated entity constructor.
|
|
270
|
+
* Registering again for the same table replaces the previous hook set.
|
|
271
|
+
*/
|
|
272
|
+
registerTableHooks<TTarget extends LifecycleHookTarget>(
|
|
273
|
+
target: TTarget,
|
|
274
|
+
hooks: TableHooks<LifecycleHookEntity<TTarget>, OrmSession<E>>
|
|
275
|
+
): void {
|
|
276
|
+
const table = typeof target === 'function'
|
|
277
|
+
? getTableDefFromEntity(target as EntityConstructor<object>)
|
|
278
|
+
: target;
|
|
279
|
+
if (!table) {
|
|
280
|
+
throw new Error('Entity metadata has not been bootstrapped');
|
|
281
|
+
}
|
|
282
|
+
this.tableHooks.set(table as TableDef, hooks as unknown as TableHooks);
|
|
283
|
+
}
|
|
284
|
+
|
|
253
285
|
/**
|
|
254
286
|
* Registers an interceptor for flush lifecycle hooks.
|
|
255
287
|
* @param interceptor - The interceptor to register
|
|
@@ -261,7 +293,7 @@ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements Entit
|
|
|
261
293
|
/**
|
|
262
294
|
* Registers a domain event handler.
|
|
263
295
|
* @param type - The event type
|
|
264
|
-
* @param handler - The event handler
|
|
296
|
+
* @param handler - The domain event handler
|
|
265
297
|
*/
|
|
266
298
|
registerDomainEventHandler<TType extends E['type']>(
|
|
267
299
|
type: TType,
|
|
@@ -500,7 +532,9 @@ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements Entit
|
|
|
500
532
|
}
|
|
501
533
|
|
|
502
534
|
/**
|
|
503
|
-
* Flushes pending changes to the database without session
|
|
535
|
+
* Flushes pending changes to the database without session interceptors,
|
|
536
|
+
* relation processing, or domain events. Table lifecycle hooks still run
|
|
537
|
+
* because they are part of the Unit of Work.
|
|
504
538
|
*/
|
|
505
539
|
async flush(): Promise<void> {
|
|
506
540
|
await this.unitOfWork.flush();
|
|
@@ -7,7 +7,6 @@ import { UpdateQueryBuilder } from '../query-builder/update.js';
|
|
|
7
7
|
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
8
8
|
import type { BelongsToManyRelation, HasManyRelation, HasOneRelation, MorphOneRelation, MorphManyRelation, MorphToRelation } from '../schema/relation.js';
|
|
9
9
|
import { RelationKinds } from '../schema/relation.js';
|
|
10
|
-
import type { TableDef } from '../schema/table.js';
|
|
11
10
|
import type { DbExecutor } from '../core/execution/db-executor.js';
|
|
12
11
|
import type { RelationChangeEntry } from './runtime-types.js';
|
|
13
12
|
import { UnitOfWork } from './unit-of-work.js';
|
|
@@ -153,7 +152,10 @@ export class RelationChangeProcessor {
|
|
|
153
152
|
const rootId = entry.root[rootKey];
|
|
154
153
|
if (rootId === undefined || rootId === null) return;
|
|
155
154
|
|
|
156
|
-
const targetId = this.
|
|
155
|
+
const targetId = this.resolveBelongsToManyTargetValue(
|
|
156
|
+
entry.change.entity as Record<string, unknown>,
|
|
157
|
+
relation
|
|
158
|
+
);
|
|
157
159
|
if (targetId === null) return;
|
|
158
160
|
|
|
159
161
|
const pivotPayload = this.buildPivotPayload(relation, entry.change.pivot);
|
|
@@ -235,7 +237,7 @@ export class RelationChangeProcessor {
|
|
|
235
237
|
* Inserts a pivot row for belongs-to-many relations.
|
|
236
238
|
* @param relation - The belongs-to-many relation
|
|
237
239
|
* @param rootId - The root entity's primary key value
|
|
238
|
-
* @param targetId - The target entity's
|
|
240
|
+
* @param targetId - The target entity's relation key value
|
|
239
241
|
*/
|
|
240
242
|
private async insertPivotRow(
|
|
241
243
|
relation: BelongsToManyRelation,
|
|
@@ -258,7 +260,7 @@ export class RelationChangeProcessor {
|
|
|
258
260
|
* Updates a pivot row for belongs-to-many relations.
|
|
259
261
|
* @param relation - The belongs-to-many relation
|
|
260
262
|
* @param rootId - The root entity's primary key value
|
|
261
|
-
* @param targetId - The target entity's
|
|
263
|
+
* @param targetId - The target entity's relation key value
|
|
262
264
|
* @param pivotPayload - The pivot columns to update
|
|
263
265
|
*/
|
|
264
266
|
private async updatePivotRow(
|
|
@@ -282,7 +284,7 @@ export class RelationChangeProcessor {
|
|
|
282
284
|
* Deletes a pivot row for belongs-to-many relations.
|
|
283
285
|
* @param relation - The belongs-to-many relation
|
|
284
286
|
* @param rootId - The root entity's primary key value
|
|
285
|
-
* @param targetId - The target entity's
|
|
287
|
+
* @param targetId - The target entity's relation key value
|
|
286
288
|
*/
|
|
287
289
|
private async deletePivotRow(relation: BelongsToManyRelation, rootId: string | number, targetId: string | number): Promise<void> {
|
|
288
290
|
const rootCol = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
|
|
@@ -297,14 +299,16 @@ export class RelationChangeProcessor {
|
|
|
297
299
|
}
|
|
298
300
|
|
|
299
301
|
/**
|
|
300
|
-
* Resolves the
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
* @returns The primary key value or null
|
|
302
|
+
* Resolves the target-side key value used by a belongs-to-many relation.
|
|
303
|
+
* The declared targetKey is part of the relation contract and takes
|
|
304
|
+
* precedence over the target table primary key.
|
|
304
305
|
*/
|
|
305
|
-
private
|
|
306
|
+
private resolveBelongsToManyTargetValue(
|
|
307
|
+
entity: Record<string, unknown>,
|
|
308
|
+
relation: BelongsToManyRelation
|
|
309
|
+
): string | number | null {
|
|
306
310
|
if (!entity) return null;
|
|
307
|
-
const key = findPrimaryKey(
|
|
311
|
+
const key = relation.targetKey || findPrimaryKey(relation.target);
|
|
308
312
|
const value = entity[key];
|
|
309
313
|
if (value === undefined || value === null) return null;
|
|
310
314
|
return (value as string | number | null | undefined) ?? null;
|
package/src/orm/unit-of-work.ts
CHANGED
|
@@ -5,13 +5,14 @@ import { InsertQueryBuilder } from '../query-builder/insert.js';
|
|
|
5
5
|
import { UpdateQueryBuilder } from '../query-builder/update.js';
|
|
6
6
|
import { DeleteQueryBuilder } from '../query-builder/delete.js';
|
|
7
7
|
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
8
|
-
import type { TableDef
|
|
8
|
+
import type { TableDef } from '../schema/table.js';
|
|
9
9
|
import { payloadResultSets } from '../core/execution/db-executor.js';
|
|
10
10
|
import type { DbExecutor, QueryResult } from '../core/execution/db-executor.js';
|
|
11
11
|
import { IdentityMap } from './identity-map.js';
|
|
12
12
|
import { EntityStatus } from './runtime-types.js';
|
|
13
13
|
import type { TrackedEntity } from './runtime-types.js';
|
|
14
14
|
import type { PrimaryKey } from './entity-context.js';
|
|
15
|
+
import type { TableHookResolver, TableHooks } from './lifecycle.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Unit of Work pattern implementation for tracking entity changes.
|
|
@@ -25,12 +26,14 @@ export class UnitOfWork {
|
|
|
25
26
|
* @param executor - The database executor
|
|
26
27
|
* @param identityMap - The identity map
|
|
27
28
|
* @param hookContext - Function to get the hook context
|
|
29
|
+
* @param resolveTableHooks - Session/runtime lifecycle hook resolver
|
|
28
30
|
*/
|
|
29
31
|
constructor(
|
|
30
32
|
private readonly dialect: Dialect,
|
|
31
33
|
private readonly executor: DbExecutor,
|
|
32
34
|
private readonly identityMap: IdentityMap,
|
|
33
|
-
private readonly hookContext: () => unknown
|
|
35
|
+
private readonly hookContext: () => unknown,
|
|
36
|
+
private readonly resolveTableHooks: TableHookResolver = () => undefined
|
|
34
37
|
) { }
|
|
35
38
|
|
|
36
39
|
/**
|
|
@@ -195,7 +198,8 @@ export class UnitOfWork {
|
|
|
195
198
|
* @param tracked - The tracked entity to insert
|
|
196
199
|
*/
|
|
197
200
|
private async flushInsert(tracked: TrackedEntity): Promise<void> {
|
|
198
|
-
|
|
201
|
+
const hooks = this.resolveTableHooks(tracked.table);
|
|
202
|
+
await this.runHook(hooks?.beforeInsert, tracked);
|
|
199
203
|
|
|
200
204
|
const payload = this.extractColumns(tracked.table, tracked.entity as Record<string, unknown>);
|
|
201
205
|
let builder = new InsertQueryBuilder(tracked.table).values(payload as Record<string, ValueOperandInput>);
|
|
@@ -212,7 +216,7 @@ export class UnitOfWork {
|
|
|
212
216
|
tracked.pk = this.getPrimaryKeyValue(tracked);
|
|
213
217
|
this.registerIdentity(tracked);
|
|
214
218
|
|
|
215
|
-
await this.runHook(
|
|
219
|
+
await this.runHook(hooks?.afterInsert, tracked);
|
|
216
220
|
}
|
|
217
221
|
|
|
218
222
|
/**
|
|
@@ -227,7 +231,8 @@ export class UnitOfWork {
|
|
|
227
231
|
return;
|
|
228
232
|
}
|
|
229
233
|
|
|
230
|
-
|
|
234
|
+
const hooks = this.resolveTableHooks(tracked.table);
|
|
235
|
+
await this.runHook(hooks?.beforeUpdate, tracked);
|
|
231
236
|
|
|
232
237
|
const pkColumn = tracked.table.columns[findPrimaryKey(tracked.table)];
|
|
233
238
|
if (!pkColumn) return;
|
|
@@ -248,7 +253,7 @@ export class UnitOfWork {
|
|
|
248
253
|
tracked.original = this.createSnapshot(tracked.table, tracked.entity as Record<string, unknown>);
|
|
249
254
|
this.registerIdentity(tracked);
|
|
250
255
|
|
|
251
|
-
await this.runHook(
|
|
256
|
+
await this.runHook(hooks?.afterUpdate, tracked);
|
|
252
257
|
}
|
|
253
258
|
|
|
254
259
|
/**
|
|
@@ -257,7 +262,8 @@ export class UnitOfWork {
|
|
|
257
262
|
*/
|
|
258
263
|
private async flushDelete(tracked: TrackedEntity): Promise<void> {
|
|
259
264
|
if (tracked.pk == null) return;
|
|
260
|
-
|
|
265
|
+
const hooks = this.resolveTableHooks(tracked.table);
|
|
266
|
+
await this.runHook(hooks?.beforeDelete, tracked);
|
|
261
267
|
|
|
262
268
|
const pkColumn = tracked.table.columns[findPrimaryKey(tracked.table)];
|
|
263
269
|
if (!pkColumn) return;
|
|
@@ -270,11 +276,11 @@ export class UnitOfWork {
|
|
|
270
276
|
this.trackedEntities.delete(tracked.entity);
|
|
271
277
|
this.identityMap.remove(tracked);
|
|
272
278
|
|
|
273
|
-
await this.runHook(
|
|
279
|
+
await this.runHook(hooks?.afterDelete, tracked);
|
|
274
280
|
}
|
|
275
281
|
|
|
276
282
|
/**
|
|
277
|
-
* Runs a
|
|
283
|
+
* Runs a lifecycle hook if defined.
|
|
278
284
|
* @param hook - The hook function
|
|
279
285
|
* @param tracked - The tracked entity
|
|
280
286
|
*/
|
package/src/schema/table.ts
CHANGED
|
@@ -36,23 +36,15 @@ export interface TableOptions {
|
|
|
36
36
|
collation?: string;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export interface TableHooks<TEntity = unknown, TContext = unknown> {
|
|
40
|
-
beforeInsert?(ctx: TContext, entity: TEntity): Promise<void> | void;
|
|
41
|
-
afterInsert?(ctx: TContext, entity: TEntity): Promise<void> | void;
|
|
42
|
-
beforeUpdate?(ctx: TContext, entity: TEntity): Promise<void> | void;
|
|
43
|
-
afterUpdate?(ctx: TContext, entity: TEntity): Promise<void> | void;
|
|
44
|
-
beforeDelete?(ctx: TContext, entity: TEntity): Promise<void> | void;
|
|
45
|
-
afterDelete?(ctx: TContext, entity: TEntity): Promise<void> | void;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
39
|
/**
|
|
49
|
-
* Definition of a database table with its columns and relationships
|
|
40
|
+
* Definition of a database table with its columns and relationships.
|
|
41
|
+
*
|
|
42
|
+
* Runtime lifecycle policy intentionally does not live here. TableDef is
|
|
43
|
+
* mapping/schema metadata and can be shared by multiple OrmSession instances.
|
|
50
44
|
* @typeParam T - Type of the columns record
|
|
51
45
|
*/
|
|
52
46
|
export interface TableDef<
|
|
53
|
-
T extends Record<string, ColumnDef> = Record<string, ColumnDef
|
|
54
|
-
TEntity = unknown,
|
|
55
|
-
TContext = unknown
|
|
47
|
+
T extends Record<string, ColumnDef> = Record<string, ColumnDef>
|
|
56
48
|
> {
|
|
57
49
|
/** Name of the table */
|
|
58
50
|
name: string;
|
|
@@ -62,8 +54,6 @@ export interface TableDef<
|
|
|
62
54
|
columns: T;
|
|
63
55
|
/** Record of relationship definitions keyed by relation name */
|
|
64
56
|
relations: Record<string, RelationDef>;
|
|
65
|
-
/** Optional lifecycle hooks */
|
|
66
|
-
hooks?: TableHooks<TEntity, TContext>;
|
|
67
57
|
/** Composite primary key definition (falls back to column.primary flags) */
|
|
68
58
|
primaryKey?: string[];
|
|
69
59
|
/** Secondary indexes */
|
|
@@ -79,11 +69,12 @@ export interface TableDef<
|
|
|
79
69
|
}
|
|
80
70
|
|
|
81
71
|
/**
|
|
82
|
-
* Creates a table definition with columns and relationships
|
|
72
|
+
* Creates a table definition with columns and relationships.
|
|
83
73
|
* @typeParam T - Type of the columns record
|
|
84
74
|
* @param name - Name of the table
|
|
85
|
-
* @param columns - Record of column definitions
|
|
75
|
+
* @param columns - Record of column definitions keyed by property name
|
|
86
76
|
* @param relations - Record of relationship definitions (optional)
|
|
77
|
+
* @param options - Schema/table options (optional)
|
|
87
78
|
* @returns Complete table definition with runtime-filled column metadata
|
|
88
79
|
*
|
|
89
80
|
* @example
|
|
@@ -95,17 +86,12 @@ export interface TableDef<
|
|
|
95
86
|
* });
|
|
96
87
|
* ```
|
|
97
88
|
*/
|
|
98
|
-
export const defineTable = <
|
|
99
|
-
T extends Record<string, ColumnDef>,
|
|
100
|
-
TEntity = unknown,
|
|
101
|
-
TContext = unknown
|
|
102
|
-
>(
|
|
89
|
+
export const defineTable = <T extends Record<string, ColumnDef>>(
|
|
103
90
|
name: string,
|
|
104
91
|
columns: T,
|
|
105
92
|
relations: Record<string, RelationDef> = {},
|
|
106
|
-
hooks?: TableHooks<TEntity, TContext>,
|
|
107
93
|
options: TableOptions = {}
|
|
108
|
-
): TableDef<T
|
|
94
|
+
): TableDef<T> => {
|
|
109
95
|
// Runtime mutability to assign names to column definitions for convenience
|
|
110
96
|
const colsWithNames = Object.entries(columns).reduce((acc, [key, def]) => {
|
|
111
97
|
const colDef = { ...def, name: key, table: name };
|
|
@@ -118,7 +104,6 @@ export const defineTable = <
|
|
|
118
104
|
schema: options.schema,
|
|
119
105
|
columns: colsWithNames,
|
|
120
106
|
relations,
|
|
121
|
-
hooks,
|
|
122
107
|
primaryKey: options.primaryKey,
|
|
123
108
|
indexes: options.indexes,
|
|
124
109
|
checks: options.checks,
|