metal-orm 1.1.10 → 1.1.16
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 +758 -769
- package/dist/index.cjs +106 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +106 -43
- package/dist/index.js.map +1 -1
- package/package.json +100 -96
- package/scripts/generate-entities/render.mjs +21 -12
- package/scripts/generate-entities/schema.mjs +209 -195
- package/scripts/generate-entities/tree-detection.mjs +159 -153
- package/scripts/inflection/pt-br.mjs +468 -468
- package/scripts/run-eslint.mjs +33 -34
- package/scripts/show-sql-seed.sql +102 -0
- package/scripts/show-sql.mjs +321 -325
- package/src/bulk/bulk-delete-executor.ts +3 -5
- package/src/bulk/bulk-insert-executor.ts +7 -7
- package/src/bulk/bulk-update-executor.ts +2 -2
- package/src/bulk/bulk-upsert-executor.ts +5 -7
- package/src/codegen/naming-strategy.ts +54 -54
- package/src/codegen/typescript.ts +482 -482
- package/src/core/ast/adapters.ts +1 -1
- package/src/core/ast/builders.ts +123 -123
- package/src/core/ast/expression-builders.ts +745 -745
- package/src/core/ast/expression-nodes.ts +304 -304
- package/src/core/ast/expression-visitor.ts +212 -212
- package/src/core/ast/expression.ts +20 -20
- package/src/core/ast/helpers.ts +14 -14
- package/src/core/ast/join.ts +18 -18
- package/src/core/ast/query.ts +200 -200
- package/src/core/ddl/dialects/base-schema-dialect.ts +1 -1
- package/src/core/ddl/dialects/mssql-schema-dialect.ts +1 -1
- package/src/core/ddl/dialects/mysql-schema-dialect.ts +1 -1
- package/src/core/ddl/dialects/postgres-schema-dialect.ts +1 -1
- package/src/core/ddl/dialects/render-reference.test.ts +70 -70
- package/src/core/ddl/dialects/sqlite-schema-dialect.ts +1 -1
- package/src/core/ddl/introspect/catalogs/index.ts +5 -5
- package/src/core/ddl/introspect/catalogs/postgres.ts +146 -146
- package/src/core/ddl/introspect/context.ts +15 -15
- package/src/core/ddl/introspect/functions/postgres.ts +35 -35
- package/src/core/ddl/introspect/mysql.ts +113 -36
- package/src/core/ddl/introspect/registry.ts +40 -40
- package/src/core/ddl/introspect/run-select.ts +35 -35
- package/src/core/ddl/introspect/utils.ts +56 -56
- package/src/core/ddl/naming-strategy.ts +16 -16
- package/src/core/ddl/schema-dialect.ts +56 -56
- package/src/core/ddl/schema-diff.ts +234 -234
- package/src/core/ddl/schema-generator.ts +1 -1
- package/src/core/ddl/schema-introspect.ts +25 -25
- package/src/core/ddl/sql-writing.ts +171 -171
- package/src/core/dialect/abstract.ts +541 -541
- package/src/core/dialect/base/cte-compiler.ts +33 -33
- package/src/core/dialect/base/function-table-formatter.ts +103 -103
- package/src/core/dialect/base/groupby-compiler.ts +21 -21
- package/src/core/dialect/base/join-compiler.ts +25 -25
- package/src/core/dialect/base/orderby-compiler.ts +35 -35
- package/src/core/dialect/base/pagination-strategy.ts +32 -32
- package/src/core/dialect/base/returning-strategy.ts +57 -57
- package/src/core/dialect/base/sql-dialect.ts +294 -294
- package/src/core/dialect/dialect-factory.ts +91 -91
- package/src/core/dialect/mssql/functions.ts +120 -120
- package/src/core/dialect/mssql/index.ts +317 -317
- package/src/core/dialect/mysql/functions.ts +103 -103
- package/src/core/dialect/mysql/index.ts +105 -105
- package/src/core/dialect/postgres/functions.ts +108 -108
- package/src/core/dialect/postgres/index.ts +91 -91
- package/src/core/dialect/sqlite/functions.ts +129 -129
- package/src/core/dialect/sqlite/index.ts +67 -67
- package/src/core/driver/database-driver.ts +19 -19
- package/src/core/driver/mssql-driver.ts +23 -23
- package/src/core/driver/mysql-driver.ts +23 -23
- package/src/core/driver/postgres-driver.ts +23 -23
- package/src/core/driver/sqlite-driver.ts +23 -23
- package/src/core/execution/db-executor.ts +92 -92
- package/src/core/execution/executors/better-sqlite3-executor.ts +94 -94
- package/src/core/execution/executors/mssql-executor.ts +184 -184
- package/src/core/execution/executors/mysql-executor.ts +88 -88
- package/src/core/execution/executors/postgres-executor.ts +26 -26
- package/src/core/execution/executors/sqlite-executor.ts +42 -42
- package/src/core/execution/pooling/pool-types.ts +30 -30
- package/src/core/execution/pooling/pool.ts +275 -275
- package/src/core/functions/array.ts +35 -35
- package/src/core/functions/control-flow.ts +79 -79
- package/src/core/functions/datetime.ts +237 -237
- package/src/core/functions/json.ts +70 -70
- package/src/core/functions/numeric.ts +297 -297
- package/src/core/functions/text.ts +334 -334
- package/src/core/functions/types.ts +33 -33
- package/src/core/hydration/types.ts +51 -51
- package/src/core/sql/sql-operator-config.ts +39 -39
- package/src/core/sql/sql.ts +152 -152
- package/src/decorators/column-decorator.ts +87 -87
- package/src/decorators/decorator-metadata.ts +143 -143
- package/src/decorators/entity.ts +93 -93
- package/src/decorators/index.ts +39 -39
- package/src/decorators/relations.ts +231 -231
- package/src/decorators/transformers/built-in/string-transformers.ts +231 -231
- package/src/decorators/transformers/transformer-decorators.ts +134 -134
- package/src/decorators/transformers/transformer-executor.ts +171 -171
- package/src/decorators/transformers/transformer-metadata.ts +73 -73
- package/src/decorators/validators/built-in/br-cep-validator.ts +55 -55
- package/src/decorators/validators/built-in/br-cnpj-validator.ts +105 -105
- package/src/decorators/validators/built-in/br-cpf-validator.ts +103 -103
- package/src/decorators/validators/country-validator-registry.ts +64 -64
- package/src/decorators/validators/country-validators-decorators.ts +203 -203
- package/src/decorators/validators/country-validators.ts +105 -105
- package/src/decorators/validators/validator-metadata.ts +59 -59
- package/src/dto/dto-types.ts +226 -226
- package/src/dto/filter-types.ts +141 -141
- package/src/dto/index.ts +95 -95
- package/src/dto/openapi/generators/base.ts +29 -29
- package/src/dto/openapi/generators/column.ts +37 -37
- package/src/dto/openapi/generators/dto.ts +91 -91
- package/src/dto/openapi/generators/filter.ts +75 -75
- package/src/dto/openapi/generators/nested-dto.ts +618 -618
- package/src/dto/openapi/generators/pagination.ts +111 -111
- package/src/dto/openapi/generators/relation-filter.ts +228 -228
- package/src/dto/openapi/index.ts +12 -12
- package/src/dto/openapi/types.ts +101 -101
- package/src/dto/openapi/utilities.ts +95 -95
- package/src/dto/pagination-utils.ts +150 -150
- package/src/dto/transform.ts +197 -197
- package/src/index.ts +86 -86
- package/src/orm/als.ts +58 -58
- package/src/orm/domain-event-bus.ts +119 -119
- package/src/orm/entity-context.ts +86 -86
- package/src/orm/entity-materializer.ts +159 -159
- package/src/orm/entity-meta.ts +97 -97
- package/src/orm/entity-registry.ts +39 -39
- package/src/orm/entity-relation-cache.ts +39 -39
- package/src/orm/execute.ts +194 -194
- package/src/orm/execution-context.ts +18 -18
- package/src/orm/hydration-context.ts +26 -26
- package/src/orm/hydration.ts +112 -112
- package/src/orm/identity-map.ts +61 -61
- package/src/orm/interceptor-pipeline.ts +16 -16
- package/src/orm/lazy-batch/belongs-to-many.ts +119 -119
- package/src/orm/lazy-batch/belongs-to.ts +108 -108
- package/src/orm/lazy-batch/has-many.ts +69 -69
- package/src/orm/lazy-batch/has-one.ts +68 -68
- package/src/orm/lazy-batch/shared.ts +125 -125
- package/src/orm/orm-session.ts +620 -620
- package/src/orm/orm.ts +34 -34
- package/src/orm/pooled-executor-factory.ts +121 -121
- package/src/orm/query-logger.ts +40 -40
- package/src/orm/relation-change-processor.ts +420 -420
- package/src/orm/relations/belongs-to.ts +143 -143
- package/src/orm/relations/has-many.ts +204 -204
- package/src/orm/relations/has-one.ts +174 -174
- package/src/orm/relations/many-to-many.ts +288 -288
- package/src/orm/relations/morph-many.ts +156 -156
- package/src/orm/relations/morph-one.ts +151 -151
- package/src/orm/relations/morph-to.ts +162 -162
- package/src/orm/runtime-types.ts +92 -92
- package/src/orm/save-graph.ts +561 -561
- package/src/orm/transaction-runner.ts +24 -24
- package/src/orm/unit-of-work.ts +426 -426
- package/src/query/index.ts +69 -69
- package/src/query/target.ts +46 -46
- package/src/query-builder/column-selector.ts +81 -81
- package/src/query-builder/delete.ts +141 -141
- package/src/query-builder/insert-query-state.ts +149 -149
- package/src/query-builder/insert.ts +117 -117
- package/src/query-builder/query-ast-service.ts +287 -287
- package/src/query-builder/query-resolution.ts +78 -78
- package/src/query-builder/raw-column-parser.ts +38 -38
- package/src/query-builder/relation-filter-utils.ts +153 -153
- package/src/query-builder/relation-join-planner.ts +10 -10
- package/src/query-builder/relation-manager.ts +87 -87
- package/src/query-builder/relation-projection-helper.ts +102 -102
- package/src/query-builder/relation-types.ts +9 -9
- package/src/query-builder/relation-utils.ts +15 -15
- package/src/query-builder/select/cte-facet.ts +40 -40
- package/src/query-builder/select/from-facet.ts +80 -80
- package/src/query-builder/select/join-facet.ts +62 -62
- package/src/query-builder/select/predicate-facet.ts +104 -104
- package/src/query-builder/select/projection-facet.ts +70 -70
- package/src/query-builder/select/relation-facet.ts +81 -81
- package/src/query-builder/select/setop-facet.ts +36 -36
- package/src/query-builder/select-helpers.ts +1 -1
- package/src/query-builder/select-query-builder-deps.ts +130 -130
- package/src/query-builder/select-query-state.ts +207 -207
- package/src/query-builder/update.ts +151 -151
- package/src/schema/column-types.ts +322 -322
- package/src/schema/table-guards.ts +37 -37
- package/src/schema/table.ts +281 -281
- package/src/tree/index.ts +13 -13
- package/src/tree/nested-set-strategy.ts +545 -545
- package/src/tree/tree-decorator.ts +397 -397
- package/src/tree/tree-manager.ts +754 -754
- package/src/tree/tree-query.ts +427 -427
- package/src/tree/tree-types.ts +299 -299
package/src/orm/unit-of-work.ts
CHANGED
|
@@ -1,426 +1,426 @@
|
|
|
1
|
-
import { ColumnNode, eq } from '../core/ast/expression.js';
|
|
2
|
-
import type { ValueOperandInput } from '../core/ast/expression.js';
|
|
3
|
-
import type { Dialect, CompiledQuery } from '../core/dialect/abstract.js';
|
|
4
|
-
import { InsertQueryBuilder } from '../query-builder/insert.js';
|
|
5
|
-
import { UpdateQueryBuilder } from '../query-builder/update.js';
|
|
6
|
-
import { DeleteQueryBuilder } from '../query-builder/delete.js';
|
|
7
|
-
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
8
|
-
import type { TableDef, TableHooks } from '../schema/table.js';
|
|
9
|
-
import { payloadResultSets } from '../core/execution/db-executor.js';
|
|
10
|
-
import type { DbExecutor, QueryResult } from '../core/execution/db-executor.js';
|
|
11
|
-
import { IdentityMap } from './identity-map.js';
|
|
12
|
-
import { EntityStatus } from './runtime-types.js';
|
|
13
|
-
import type { TrackedEntity } from './runtime-types.js';
|
|
14
|
-
import type { PrimaryKey } from './entity-context.js';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Unit of Work pattern implementation for tracking entity changes.
|
|
18
|
-
*/
|
|
19
|
-
export class UnitOfWork {
|
|
20
|
-
private readonly trackedEntities = new Map<object, TrackedEntity>();
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Creates a new UnitOfWork instance.
|
|
24
|
-
* @param dialect - The database dialect
|
|
25
|
-
* @param executor - The database executor
|
|
26
|
-
* @param identityMap - The identity map
|
|
27
|
-
* @param hookContext - Function to get the hook context
|
|
28
|
-
*/
|
|
29
|
-
constructor(
|
|
30
|
-
private readonly dialect: Dialect,
|
|
31
|
-
private readonly executor: DbExecutor,
|
|
32
|
-
private readonly identityMap: IdentityMap,
|
|
33
|
-
private readonly hookContext: () => unknown
|
|
34
|
-
) { }
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Gets the identity buckets map.
|
|
38
|
-
*/
|
|
39
|
-
get identityBuckets(): Map<string, Map<string, TrackedEntity>> {
|
|
40
|
-
return this.identityMap.bucketsMap;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Gets all tracked entities.
|
|
45
|
-
* @returns Array of tracked entities
|
|
46
|
-
*/
|
|
47
|
-
getTracked(): TrackedEntity[] {
|
|
48
|
-
return Array.from(this.trackedEntities.values());
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Gets an entity by table and primary key.
|
|
53
|
-
* @param table - The table definition
|
|
54
|
-
* @param pk - The primary key value
|
|
55
|
-
* @returns The entity or undefined if not found
|
|
56
|
-
*/
|
|
57
|
-
getEntity(table: TableDef, pk: PrimaryKey): object | undefined {
|
|
58
|
-
return this.identityMap.getEntity(table, pk);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Gets all tracked entities for a specific table.
|
|
63
|
-
* @param table - The table definition
|
|
64
|
-
* @returns Array of tracked entities
|
|
65
|
-
*/
|
|
66
|
-
getEntitiesForTable(table: TableDef): TrackedEntity[] {
|
|
67
|
-
return this.identityMap.getEntitiesForTable(table);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Finds a tracked entity.
|
|
72
|
-
* @param entity - The entity to find
|
|
73
|
-
* @returns The tracked entity or undefined if not found
|
|
74
|
-
*/
|
|
75
|
-
findTracked(entity: object): TrackedEntity | undefined {
|
|
76
|
-
return this.trackedEntities.get(entity);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Sets an entity in the identity map.
|
|
81
|
-
* @param table - The table definition
|
|
82
|
-
* @param pk - The primary key value
|
|
83
|
-
* @param entity - The entity instance
|
|
84
|
-
*/
|
|
85
|
-
setEntity(table: TableDef, pk: PrimaryKey, entity: object): void {
|
|
86
|
-
if (pk === null || pk === undefined) return;
|
|
87
|
-
let tracked = this.trackedEntities.get(entity);
|
|
88
|
-
if (!tracked) {
|
|
89
|
-
tracked = {
|
|
90
|
-
table,
|
|
91
|
-
entity,
|
|
92
|
-
pk,
|
|
93
|
-
status: EntityStatus.Managed,
|
|
94
|
-
original: this.createSnapshot(table, entity as Record<string, unknown>)
|
|
95
|
-
};
|
|
96
|
-
this.trackedEntities.set(entity, tracked);
|
|
97
|
-
} else {
|
|
98
|
-
tracked.pk = pk;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
this.registerIdentity(tracked);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Tracks a new entity.
|
|
106
|
-
* @param table - The table definition
|
|
107
|
-
* @param entity - The entity instance
|
|
108
|
-
* @param pk - Optional primary key value
|
|
109
|
-
*/
|
|
110
|
-
trackNew(table: TableDef, entity: object, pk?: PrimaryKey): void {
|
|
111
|
-
const tracked: TrackedEntity = {
|
|
112
|
-
table,
|
|
113
|
-
entity,
|
|
114
|
-
pk: pk ?? null,
|
|
115
|
-
status: EntityStatus.New,
|
|
116
|
-
original: null
|
|
117
|
-
};
|
|
118
|
-
this.trackedEntities.set(entity, tracked);
|
|
119
|
-
if (pk != null) {
|
|
120
|
-
this.registerIdentity(tracked);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Tracks a managed entity.
|
|
126
|
-
* @param table - The table definition
|
|
127
|
-
* @param pk - The primary key value
|
|
128
|
-
* @param entity - The entity instance
|
|
129
|
-
*/
|
|
130
|
-
trackManaged(table: TableDef, pk: PrimaryKey, entity: object): void {
|
|
131
|
-
const tracked: TrackedEntity = {
|
|
132
|
-
table,
|
|
133
|
-
entity,
|
|
134
|
-
pk,
|
|
135
|
-
status: EntityStatus.Managed,
|
|
136
|
-
original: this.createSnapshot(table, entity as Record<string, unknown>)
|
|
137
|
-
};
|
|
138
|
-
this.trackedEntities.set(entity, tracked);
|
|
139
|
-
this.registerIdentity(tracked);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Marks an entity as dirty (modified).
|
|
144
|
-
* @param entity - The entity to mark as dirty
|
|
145
|
-
*/
|
|
146
|
-
markDirty(entity: object): void {
|
|
147
|
-
const tracked = this.trackedEntities.get(entity);
|
|
148
|
-
if (!tracked) return;
|
|
149
|
-
if (tracked.status === EntityStatus.New || tracked.status === EntityStatus.Removed) return;
|
|
150
|
-
tracked.status = EntityStatus.Dirty;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Marks an entity as removed.
|
|
155
|
-
* @param entity - The entity to mark as removed
|
|
156
|
-
*/
|
|
157
|
-
markRemoved(entity: object): void {
|
|
158
|
-
const tracked = this.trackedEntities.get(entity);
|
|
159
|
-
if (!tracked) return;
|
|
160
|
-
tracked.status = EntityStatus.Removed;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Flushes pending changes to the database.
|
|
165
|
-
*/
|
|
166
|
-
async flush(): Promise<void> {
|
|
167
|
-
const toFlush = Array.from(this.trackedEntities.values());
|
|
168
|
-
for (const tracked of toFlush) {
|
|
169
|
-
switch (tracked.status) {
|
|
170
|
-
case EntityStatus.New:
|
|
171
|
-
await this.flushInsert(tracked);
|
|
172
|
-
break;
|
|
173
|
-
case EntityStatus.Dirty:
|
|
174
|
-
await this.flushUpdate(tracked);
|
|
175
|
-
break;
|
|
176
|
-
case EntityStatus.Removed:
|
|
177
|
-
await this.flushDelete(tracked);
|
|
178
|
-
break;
|
|
179
|
-
default:
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Resets the unit of work by clearing all tracked entities and identity map.
|
|
187
|
-
*/
|
|
188
|
-
reset(): void {
|
|
189
|
-
this.trackedEntities.clear();
|
|
190
|
-
this.identityMap.clear();
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Flushes an insert operation for a new entity.
|
|
195
|
-
* @param tracked - The tracked entity to insert
|
|
196
|
-
*/
|
|
197
|
-
private async flushInsert(tracked: TrackedEntity): Promise<void> {
|
|
198
|
-
await this.runHook(tracked.table.hooks?.beforeInsert, tracked);
|
|
199
|
-
|
|
200
|
-
const payload = this.extractColumns(tracked.table, tracked.entity as Record<string, unknown>);
|
|
201
|
-
let builder = new InsertQueryBuilder(tracked.table).values(payload as Record<string, ValueOperandInput>);
|
|
202
|
-
if (this.dialect.supportsDmlReturningClause()) {
|
|
203
|
-
builder = builder.returning(...this.getReturningColumns(tracked.table));
|
|
204
|
-
}
|
|
205
|
-
const compiled = builder.compile(this.dialect);
|
|
206
|
-
const results = await this.executeCompiled(compiled);
|
|
207
|
-
this.applyReturningResults(tracked, results);
|
|
208
|
-
this.applyInsertedIdIfAbsent(tracked, results);
|
|
209
|
-
|
|
210
|
-
tracked.status = EntityStatus.Managed;
|
|
211
|
-
tracked.original = this.createSnapshot(tracked.table, tracked.entity as Record<string, unknown>);
|
|
212
|
-
tracked.pk = this.getPrimaryKeyValue(tracked);
|
|
213
|
-
this.registerIdentity(tracked);
|
|
214
|
-
|
|
215
|
-
await this.runHook(tracked.table.hooks?.afterInsert, tracked);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Flushes an update operation for a modified entity.
|
|
220
|
-
* @param tracked - The tracked entity to update
|
|
221
|
-
*/
|
|
222
|
-
private async flushUpdate(tracked: TrackedEntity): Promise<void> {
|
|
223
|
-
if (tracked.pk == null) return;
|
|
224
|
-
const changes = this.computeChanges(tracked);
|
|
225
|
-
if (!Object.keys(changes).length) {
|
|
226
|
-
tracked.status = EntityStatus.Managed;
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
await this.runHook(tracked.table.hooks?.beforeUpdate, tracked);
|
|
231
|
-
|
|
232
|
-
const pkColumn = tracked.table.columns[findPrimaryKey(tracked.table)];
|
|
233
|
-
if (!pkColumn) return;
|
|
234
|
-
|
|
235
|
-
let builder = new UpdateQueryBuilder(tracked.table)
|
|
236
|
-
.set(changes)
|
|
237
|
-
.where(eq(pkColumn, tracked.pk));
|
|
238
|
-
|
|
239
|
-
if (this.dialect.supportsDmlReturningClause()) {
|
|
240
|
-
builder = builder.returning(...this.getReturningColumns(tracked.table));
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const compiled = builder.compile(this.dialect);
|
|
244
|
-
const results = await this.executeCompiled(compiled);
|
|
245
|
-
this.applyReturningResults(tracked, results);
|
|
246
|
-
|
|
247
|
-
tracked.status = EntityStatus.Managed;
|
|
248
|
-
tracked.original = this.createSnapshot(tracked.table, tracked.entity as Record<string, unknown>);
|
|
249
|
-
this.registerIdentity(tracked);
|
|
250
|
-
|
|
251
|
-
await this.runHook(tracked.table.hooks?.afterUpdate, tracked);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Flushes a delete operation for a removed entity.
|
|
256
|
-
* @param tracked - The tracked entity to delete
|
|
257
|
-
*/
|
|
258
|
-
private async flushDelete(tracked: TrackedEntity): Promise<void> {
|
|
259
|
-
if (tracked.pk == null) return;
|
|
260
|
-
await this.runHook(tracked.table.hooks?.beforeDelete, tracked);
|
|
261
|
-
|
|
262
|
-
const pkColumn = tracked.table.columns[findPrimaryKey(tracked.table)];
|
|
263
|
-
if (!pkColumn) return;
|
|
264
|
-
|
|
265
|
-
const builder = new DeleteQueryBuilder(tracked.table).where(eq(pkColumn, tracked.pk));
|
|
266
|
-
const compiled = builder.compile(this.dialect);
|
|
267
|
-
await this.executeCompiled(compiled);
|
|
268
|
-
|
|
269
|
-
tracked.status = EntityStatus.Detached;
|
|
270
|
-
this.trackedEntities.delete(tracked.entity);
|
|
271
|
-
this.identityMap.remove(tracked);
|
|
272
|
-
|
|
273
|
-
await this.runHook(tracked.table.hooks?.afterDelete, tracked);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Runs a table hook if defined.
|
|
278
|
-
* @param hook - The hook function
|
|
279
|
-
* @param tracked - The tracked entity
|
|
280
|
-
*/
|
|
281
|
-
private async runHook(
|
|
282
|
-
hook: TableHooks[keyof TableHooks] | undefined,
|
|
283
|
-
tracked: TrackedEntity
|
|
284
|
-
): Promise<void> {
|
|
285
|
-
if (!hook) return;
|
|
286
|
-
await hook(this.hookContext(), tracked.entity);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Computes changes between current entity state and original snapshot.
|
|
291
|
-
* @param tracked - The tracked entity
|
|
292
|
-
* @returns Object with changed column values
|
|
293
|
-
*/
|
|
294
|
-
private computeChanges(tracked: TrackedEntity): Record<string, unknown> {
|
|
295
|
-
const snapshot = tracked.original ?? {};
|
|
296
|
-
const changes: Record<string, unknown> = {};
|
|
297
|
-
for (const column of Object.keys(tracked.table.columns)) {
|
|
298
|
-
const current = (tracked.entity as Record<string, unknown>)[column];
|
|
299
|
-
if (snapshot[column] !== current) {
|
|
300
|
-
changes[column] = current;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
return changes;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Extracts column values from an entity.
|
|
308
|
-
* @param table - The table definition
|
|
309
|
-
* @param entity - The entity instance
|
|
310
|
-
* @returns Object with column values
|
|
311
|
-
*/
|
|
312
|
-
private extractColumns(table: TableDef, entity: Record<string, unknown>): Record<string, unknown> {
|
|
313
|
-
const payload: Record<string, unknown> = {};
|
|
314
|
-
for (const column of Object.keys(table.columns)) {
|
|
315
|
-
if (entity[column] === undefined) continue;
|
|
316
|
-
payload[column] = entity[column];
|
|
317
|
-
}
|
|
318
|
-
return payload;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Executes a compiled query.
|
|
323
|
-
* @param compiled - The compiled query
|
|
324
|
-
* @returns Query results
|
|
325
|
-
*/
|
|
326
|
-
private async executeCompiled(compiled: CompiledQuery): Promise<QueryResult[]> {
|
|
327
|
-
const payload = await this.executor.executeSql(compiled.sql, compiled.params);
|
|
328
|
-
return payloadResultSets(payload);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Gets columns for RETURNING clause.
|
|
333
|
-
* @param table - The table definition
|
|
334
|
-
* @returns Array of column nodes
|
|
335
|
-
*/
|
|
336
|
-
private getReturningColumns(table: TableDef): ColumnNode[] {
|
|
337
|
-
return Object.values(table.columns).map(column => ({
|
|
338
|
-
type: 'Column',
|
|
339
|
-
table: table.name,
|
|
340
|
-
name: column.name,
|
|
341
|
-
alias: column.name
|
|
342
|
-
}));
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Applies RETURNING clause results to the tracked entity.
|
|
347
|
-
* @param tracked - The tracked entity
|
|
348
|
-
* @param results - Query results
|
|
349
|
-
*/
|
|
350
|
-
private applyReturningResults(tracked: TrackedEntity, results: QueryResult[]): void {
|
|
351
|
-
const first = results[0];
|
|
352
|
-
if (!first || first.columns.length === 0 || first.values.length === 0) return;
|
|
353
|
-
|
|
354
|
-
const row = first.values[0];
|
|
355
|
-
for (let i = 0; i < first.columns.length; i++) {
|
|
356
|
-
const columnName = this.normalizeColumnName(first.columns[i]);
|
|
357
|
-
if (!(columnName in tracked.table.columns)) continue;
|
|
358
|
-
(tracked.entity as Record<string, unknown>)[columnName] = row[i];
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Applies the driver-provided insertId when no RETURNING clause was used.
|
|
364
|
-
* Only sets the PK if it is currently absent on the entity.
|
|
365
|
-
* @param tracked - The tracked entity
|
|
366
|
-
* @param results - Query results (may contain meta.insertId)
|
|
367
|
-
*/
|
|
368
|
-
private applyInsertedIdIfAbsent(tracked: TrackedEntity, results: QueryResult[]): void {
|
|
369
|
-
const pkName = findPrimaryKey(tracked.table);
|
|
370
|
-
const current = (tracked.entity as Record<string, unknown>)[pkName];
|
|
371
|
-
if (current != null) return;
|
|
372
|
-
|
|
373
|
-
const first = results[0];
|
|
374
|
-
const insertId = first?.meta?.insertId;
|
|
375
|
-
if (insertId == null) return;
|
|
376
|
-
|
|
377
|
-
(tracked.entity as Record<string, unknown>)[pkName] = insertId;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* Normalizes a column name by removing quotes and table prefixes.
|
|
382
|
-
* @param column - The column name to normalize
|
|
383
|
-
* @returns Normalized column name
|
|
384
|
-
*/
|
|
385
|
-
private normalizeColumnName(column: string): string {
|
|
386
|
-
const parts = column.split('.');
|
|
387
|
-
const candidate = parts[parts.length - 1];
|
|
388
|
-
return candidate.replace(/^["`[\]]+|["`[\]]+$/g, '');
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Registers an entity in the identity map.
|
|
393
|
-
* @param tracked - The tracked entity to register
|
|
394
|
-
*/
|
|
395
|
-
private registerIdentity(tracked: TrackedEntity): void {
|
|
396
|
-
if (tracked.pk == null) return;
|
|
397
|
-
this.identityMap.register(tracked);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Creates a snapshot of an entity's current state.
|
|
402
|
-
* @param table - The table definition
|
|
403
|
-
* @param entity - The entity instance
|
|
404
|
-
* @returns Object with entity state
|
|
405
|
-
*/
|
|
406
|
-
private createSnapshot(table: TableDef, entity: Record<string, unknown>): Record<string, unknown> {
|
|
407
|
-
const snapshot: Record<string, unknown> = {};
|
|
408
|
-
for (const column of Object.keys(table.columns)) {
|
|
409
|
-
snapshot[column] = entity[column];
|
|
410
|
-
}
|
|
411
|
-
return snapshot;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Gets the primary key value from a tracked entity.
|
|
416
|
-
* @param tracked - The tracked entity
|
|
417
|
-
* @returns Primary key value or null
|
|
418
|
-
*/
|
|
419
|
-
private getPrimaryKeyValue(tracked: TrackedEntity): PrimaryKey | null {
|
|
420
|
-
const key = findPrimaryKey(tracked.table);
|
|
421
|
-
const val = (tracked.entity as Record<string, unknown>)[key];
|
|
422
|
-
if (val === undefined || val === null) return null;
|
|
423
|
-
if (typeof val !== 'string' && typeof val !== 'number') return null;
|
|
424
|
-
return val as PrimaryKey;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
1
|
+
import { ColumnNode, eq } from '../core/ast/expression.js';
|
|
2
|
+
import type { ValueOperandInput } from '../core/ast/expression.js';
|
|
3
|
+
import type { Dialect, CompiledQuery } from '../core/dialect/abstract.js';
|
|
4
|
+
import { InsertQueryBuilder } from '../query-builder/insert.js';
|
|
5
|
+
import { UpdateQueryBuilder } from '../query-builder/update.js';
|
|
6
|
+
import { DeleteQueryBuilder } from '../query-builder/delete.js';
|
|
7
|
+
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
8
|
+
import type { TableDef, TableHooks } from '../schema/table.js';
|
|
9
|
+
import { payloadResultSets } from '../core/execution/db-executor.js';
|
|
10
|
+
import type { DbExecutor, QueryResult } from '../core/execution/db-executor.js';
|
|
11
|
+
import { IdentityMap } from './identity-map.js';
|
|
12
|
+
import { EntityStatus } from './runtime-types.js';
|
|
13
|
+
import type { TrackedEntity } from './runtime-types.js';
|
|
14
|
+
import type { PrimaryKey } from './entity-context.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Unit of Work pattern implementation for tracking entity changes.
|
|
18
|
+
*/
|
|
19
|
+
export class UnitOfWork {
|
|
20
|
+
private readonly trackedEntities = new Map<object, TrackedEntity>();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a new UnitOfWork instance.
|
|
24
|
+
* @param dialect - The database dialect
|
|
25
|
+
* @param executor - The database executor
|
|
26
|
+
* @param identityMap - The identity map
|
|
27
|
+
* @param hookContext - Function to get the hook context
|
|
28
|
+
*/
|
|
29
|
+
constructor(
|
|
30
|
+
private readonly dialect: Dialect,
|
|
31
|
+
private readonly executor: DbExecutor,
|
|
32
|
+
private readonly identityMap: IdentityMap,
|
|
33
|
+
private readonly hookContext: () => unknown
|
|
34
|
+
) { }
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Gets the identity buckets map.
|
|
38
|
+
*/
|
|
39
|
+
get identityBuckets(): Map<string, Map<string, TrackedEntity>> {
|
|
40
|
+
return this.identityMap.bucketsMap;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Gets all tracked entities.
|
|
45
|
+
* @returns Array of tracked entities
|
|
46
|
+
*/
|
|
47
|
+
getTracked(): TrackedEntity[] {
|
|
48
|
+
return Array.from(this.trackedEntities.values());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Gets an entity by table and primary key.
|
|
53
|
+
* @param table - The table definition
|
|
54
|
+
* @param pk - The primary key value
|
|
55
|
+
* @returns The entity or undefined if not found
|
|
56
|
+
*/
|
|
57
|
+
getEntity(table: TableDef, pk: PrimaryKey): object | undefined {
|
|
58
|
+
return this.identityMap.getEntity(table, pk);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Gets all tracked entities for a specific table.
|
|
63
|
+
* @param table - The table definition
|
|
64
|
+
* @returns Array of tracked entities
|
|
65
|
+
*/
|
|
66
|
+
getEntitiesForTable(table: TableDef): TrackedEntity[] {
|
|
67
|
+
return this.identityMap.getEntitiesForTable(table);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Finds a tracked entity.
|
|
72
|
+
* @param entity - The entity to find
|
|
73
|
+
* @returns The tracked entity or undefined if not found
|
|
74
|
+
*/
|
|
75
|
+
findTracked(entity: object): TrackedEntity | undefined {
|
|
76
|
+
return this.trackedEntities.get(entity);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Sets an entity in the identity map.
|
|
81
|
+
* @param table - The table definition
|
|
82
|
+
* @param pk - The primary key value
|
|
83
|
+
* @param entity - The entity instance
|
|
84
|
+
*/
|
|
85
|
+
setEntity(table: TableDef, pk: PrimaryKey, entity: object): void {
|
|
86
|
+
if (pk === null || pk === undefined) return;
|
|
87
|
+
let tracked = this.trackedEntities.get(entity);
|
|
88
|
+
if (!tracked) {
|
|
89
|
+
tracked = {
|
|
90
|
+
table,
|
|
91
|
+
entity,
|
|
92
|
+
pk,
|
|
93
|
+
status: EntityStatus.Managed,
|
|
94
|
+
original: this.createSnapshot(table, entity as Record<string, unknown>)
|
|
95
|
+
};
|
|
96
|
+
this.trackedEntities.set(entity, tracked);
|
|
97
|
+
} else {
|
|
98
|
+
tracked.pk = pk;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.registerIdentity(tracked);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Tracks a new entity.
|
|
106
|
+
* @param table - The table definition
|
|
107
|
+
* @param entity - The entity instance
|
|
108
|
+
* @param pk - Optional primary key value
|
|
109
|
+
*/
|
|
110
|
+
trackNew(table: TableDef, entity: object, pk?: PrimaryKey): void {
|
|
111
|
+
const tracked: TrackedEntity = {
|
|
112
|
+
table,
|
|
113
|
+
entity,
|
|
114
|
+
pk: pk ?? null,
|
|
115
|
+
status: EntityStatus.New,
|
|
116
|
+
original: null
|
|
117
|
+
};
|
|
118
|
+
this.trackedEntities.set(entity, tracked);
|
|
119
|
+
if (pk != null) {
|
|
120
|
+
this.registerIdentity(tracked);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Tracks a managed entity.
|
|
126
|
+
* @param table - The table definition
|
|
127
|
+
* @param pk - The primary key value
|
|
128
|
+
* @param entity - The entity instance
|
|
129
|
+
*/
|
|
130
|
+
trackManaged(table: TableDef, pk: PrimaryKey, entity: object): void {
|
|
131
|
+
const tracked: TrackedEntity = {
|
|
132
|
+
table,
|
|
133
|
+
entity,
|
|
134
|
+
pk,
|
|
135
|
+
status: EntityStatus.Managed,
|
|
136
|
+
original: this.createSnapshot(table, entity as Record<string, unknown>)
|
|
137
|
+
};
|
|
138
|
+
this.trackedEntities.set(entity, tracked);
|
|
139
|
+
this.registerIdentity(tracked);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Marks an entity as dirty (modified).
|
|
144
|
+
* @param entity - The entity to mark as dirty
|
|
145
|
+
*/
|
|
146
|
+
markDirty(entity: object): void {
|
|
147
|
+
const tracked = this.trackedEntities.get(entity);
|
|
148
|
+
if (!tracked) return;
|
|
149
|
+
if (tracked.status === EntityStatus.New || tracked.status === EntityStatus.Removed) return;
|
|
150
|
+
tracked.status = EntityStatus.Dirty;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Marks an entity as removed.
|
|
155
|
+
* @param entity - The entity to mark as removed
|
|
156
|
+
*/
|
|
157
|
+
markRemoved(entity: object): void {
|
|
158
|
+
const tracked = this.trackedEntities.get(entity);
|
|
159
|
+
if (!tracked) return;
|
|
160
|
+
tracked.status = EntityStatus.Removed;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Flushes pending changes to the database.
|
|
165
|
+
*/
|
|
166
|
+
async flush(): Promise<void> {
|
|
167
|
+
const toFlush = Array.from(this.trackedEntities.values());
|
|
168
|
+
for (const tracked of toFlush) {
|
|
169
|
+
switch (tracked.status) {
|
|
170
|
+
case EntityStatus.New:
|
|
171
|
+
await this.flushInsert(tracked);
|
|
172
|
+
break;
|
|
173
|
+
case EntityStatus.Dirty:
|
|
174
|
+
await this.flushUpdate(tracked);
|
|
175
|
+
break;
|
|
176
|
+
case EntityStatus.Removed:
|
|
177
|
+
await this.flushDelete(tracked);
|
|
178
|
+
break;
|
|
179
|
+
default:
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Resets the unit of work by clearing all tracked entities and identity map.
|
|
187
|
+
*/
|
|
188
|
+
reset(): void {
|
|
189
|
+
this.trackedEntities.clear();
|
|
190
|
+
this.identityMap.clear();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Flushes an insert operation for a new entity.
|
|
195
|
+
* @param tracked - The tracked entity to insert
|
|
196
|
+
*/
|
|
197
|
+
private async flushInsert(tracked: TrackedEntity): Promise<void> {
|
|
198
|
+
await this.runHook(tracked.table.hooks?.beforeInsert, tracked);
|
|
199
|
+
|
|
200
|
+
const payload = this.extractColumns(tracked.table, tracked.entity as Record<string, unknown>);
|
|
201
|
+
let builder = new InsertQueryBuilder(tracked.table).values(payload as Record<string, ValueOperandInput>);
|
|
202
|
+
if (this.dialect.supportsDmlReturningClause()) {
|
|
203
|
+
builder = builder.returning(...this.getReturningColumns(tracked.table));
|
|
204
|
+
}
|
|
205
|
+
const compiled = builder.compile(this.dialect);
|
|
206
|
+
const results = await this.executeCompiled(compiled);
|
|
207
|
+
this.applyReturningResults(tracked, results);
|
|
208
|
+
this.applyInsertedIdIfAbsent(tracked, results);
|
|
209
|
+
|
|
210
|
+
tracked.status = EntityStatus.Managed;
|
|
211
|
+
tracked.original = this.createSnapshot(tracked.table, tracked.entity as Record<string, unknown>);
|
|
212
|
+
tracked.pk = this.getPrimaryKeyValue(tracked);
|
|
213
|
+
this.registerIdentity(tracked);
|
|
214
|
+
|
|
215
|
+
await this.runHook(tracked.table.hooks?.afterInsert, tracked);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Flushes an update operation for a modified entity.
|
|
220
|
+
* @param tracked - The tracked entity to update
|
|
221
|
+
*/
|
|
222
|
+
private async flushUpdate(tracked: TrackedEntity): Promise<void> {
|
|
223
|
+
if (tracked.pk == null) return;
|
|
224
|
+
const changes = this.computeChanges(tracked);
|
|
225
|
+
if (!Object.keys(changes).length) {
|
|
226
|
+
tracked.status = EntityStatus.Managed;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await this.runHook(tracked.table.hooks?.beforeUpdate, tracked);
|
|
231
|
+
|
|
232
|
+
const pkColumn = tracked.table.columns[findPrimaryKey(tracked.table)];
|
|
233
|
+
if (!pkColumn) return;
|
|
234
|
+
|
|
235
|
+
let builder = new UpdateQueryBuilder(tracked.table)
|
|
236
|
+
.set(changes)
|
|
237
|
+
.where(eq(pkColumn, tracked.pk));
|
|
238
|
+
|
|
239
|
+
if (this.dialect.supportsDmlReturningClause()) {
|
|
240
|
+
builder = builder.returning(...this.getReturningColumns(tracked.table));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const compiled = builder.compile(this.dialect);
|
|
244
|
+
const results = await this.executeCompiled(compiled);
|
|
245
|
+
this.applyReturningResults(tracked, results);
|
|
246
|
+
|
|
247
|
+
tracked.status = EntityStatus.Managed;
|
|
248
|
+
tracked.original = this.createSnapshot(tracked.table, tracked.entity as Record<string, unknown>);
|
|
249
|
+
this.registerIdentity(tracked);
|
|
250
|
+
|
|
251
|
+
await this.runHook(tracked.table.hooks?.afterUpdate, tracked);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Flushes a delete operation for a removed entity.
|
|
256
|
+
* @param tracked - The tracked entity to delete
|
|
257
|
+
*/
|
|
258
|
+
private async flushDelete(tracked: TrackedEntity): Promise<void> {
|
|
259
|
+
if (tracked.pk == null) return;
|
|
260
|
+
await this.runHook(tracked.table.hooks?.beforeDelete, tracked);
|
|
261
|
+
|
|
262
|
+
const pkColumn = tracked.table.columns[findPrimaryKey(tracked.table)];
|
|
263
|
+
if (!pkColumn) return;
|
|
264
|
+
|
|
265
|
+
const builder = new DeleteQueryBuilder(tracked.table).where(eq(pkColumn, tracked.pk));
|
|
266
|
+
const compiled = builder.compile(this.dialect);
|
|
267
|
+
await this.executeCompiled(compiled);
|
|
268
|
+
|
|
269
|
+
tracked.status = EntityStatus.Detached;
|
|
270
|
+
this.trackedEntities.delete(tracked.entity);
|
|
271
|
+
this.identityMap.remove(tracked);
|
|
272
|
+
|
|
273
|
+
await this.runHook(tracked.table.hooks?.afterDelete, tracked);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Runs a table hook if defined.
|
|
278
|
+
* @param hook - The hook function
|
|
279
|
+
* @param tracked - The tracked entity
|
|
280
|
+
*/
|
|
281
|
+
private async runHook(
|
|
282
|
+
hook: TableHooks[keyof TableHooks] | undefined,
|
|
283
|
+
tracked: TrackedEntity
|
|
284
|
+
): Promise<void> {
|
|
285
|
+
if (!hook) return;
|
|
286
|
+
await hook(this.hookContext(), tracked.entity);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Computes changes between current entity state and original snapshot.
|
|
291
|
+
* @param tracked - The tracked entity
|
|
292
|
+
* @returns Object with changed column values
|
|
293
|
+
*/
|
|
294
|
+
private computeChanges(tracked: TrackedEntity): Record<string, unknown> {
|
|
295
|
+
const snapshot = tracked.original ?? {};
|
|
296
|
+
const changes: Record<string, unknown> = {};
|
|
297
|
+
for (const column of Object.keys(tracked.table.columns)) {
|
|
298
|
+
const current = (tracked.entity as Record<string, unknown>)[column];
|
|
299
|
+
if (snapshot[column] !== current) {
|
|
300
|
+
changes[column] = current;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return changes;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Extracts column values from an entity.
|
|
308
|
+
* @param table - The table definition
|
|
309
|
+
* @param entity - The entity instance
|
|
310
|
+
* @returns Object with column values
|
|
311
|
+
*/
|
|
312
|
+
private extractColumns(table: TableDef, entity: Record<string, unknown>): Record<string, unknown> {
|
|
313
|
+
const payload: Record<string, unknown> = {};
|
|
314
|
+
for (const column of Object.keys(table.columns)) {
|
|
315
|
+
if (entity[column] === undefined) continue;
|
|
316
|
+
payload[column] = entity[column];
|
|
317
|
+
}
|
|
318
|
+
return payload;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Executes a compiled query.
|
|
323
|
+
* @param compiled - The compiled query
|
|
324
|
+
* @returns Query results
|
|
325
|
+
*/
|
|
326
|
+
private async executeCompiled(compiled: CompiledQuery): Promise<QueryResult[]> {
|
|
327
|
+
const payload = await this.executor.executeSql(compiled.sql, compiled.params);
|
|
328
|
+
return payloadResultSets(payload);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Gets columns for RETURNING clause.
|
|
333
|
+
* @param table - The table definition
|
|
334
|
+
* @returns Array of column nodes
|
|
335
|
+
*/
|
|
336
|
+
private getReturningColumns(table: TableDef): ColumnNode[] {
|
|
337
|
+
return Object.values(table.columns).map(column => ({
|
|
338
|
+
type: 'Column',
|
|
339
|
+
table: table.name,
|
|
340
|
+
name: column.name,
|
|
341
|
+
alias: column.name
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Applies RETURNING clause results to the tracked entity.
|
|
347
|
+
* @param tracked - The tracked entity
|
|
348
|
+
* @param results - Query results
|
|
349
|
+
*/
|
|
350
|
+
private applyReturningResults(tracked: TrackedEntity, results: QueryResult[]): void {
|
|
351
|
+
const first = results[0];
|
|
352
|
+
if (!first || first.columns.length === 0 || first.values.length === 0) return;
|
|
353
|
+
|
|
354
|
+
const row = first.values[0];
|
|
355
|
+
for (let i = 0; i < first.columns.length; i++) {
|
|
356
|
+
const columnName = this.normalizeColumnName(first.columns[i]);
|
|
357
|
+
if (!(columnName in tracked.table.columns)) continue;
|
|
358
|
+
(tracked.entity as Record<string, unknown>)[columnName] = row[i];
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Applies the driver-provided insertId when no RETURNING clause was used.
|
|
364
|
+
* Only sets the PK if it is currently absent on the entity.
|
|
365
|
+
* @param tracked - The tracked entity
|
|
366
|
+
* @param results - Query results (may contain meta.insertId)
|
|
367
|
+
*/
|
|
368
|
+
private applyInsertedIdIfAbsent(tracked: TrackedEntity, results: QueryResult[]): void {
|
|
369
|
+
const pkName = findPrimaryKey(tracked.table);
|
|
370
|
+
const current = (tracked.entity as Record<string, unknown>)[pkName];
|
|
371
|
+
if (current != null) return;
|
|
372
|
+
|
|
373
|
+
const first = results[0];
|
|
374
|
+
const insertId = first?.meta?.insertId;
|
|
375
|
+
if (insertId == null) return;
|
|
376
|
+
|
|
377
|
+
(tracked.entity as Record<string, unknown>)[pkName] = insertId;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Normalizes a column name by removing quotes and table prefixes.
|
|
382
|
+
* @param column - The column name to normalize
|
|
383
|
+
* @returns Normalized column name
|
|
384
|
+
*/
|
|
385
|
+
private normalizeColumnName(column: string): string {
|
|
386
|
+
const parts = column.split('.');
|
|
387
|
+
const candidate = parts[parts.length - 1];
|
|
388
|
+
return candidate.replace(/^["`[\]]+|["`[\]]+$/g, '');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Registers an entity in the identity map.
|
|
393
|
+
* @param tracked - The tracked entity to register
|
|
394
|
+
*/
|
|
395
|
+
private registerIdentity(tracked: TrackedEntity): void {
|
|
396
|
+
if (tracked.pk == null) return;
|
|
397
|
+
this.identityMap.register(tracked);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Creates a snapshot of an entity's current state.
|
|
402
|
+
* @param table - The table definition
|
|
403
|
+
* @param entity - The entity instance
|
|
404
|
+
* @returns Object with entity state
|
|
405
|
+
*/
|
|
406
|
+
private createSnapshot(table: TableDef, entity: Record<string, unknown>): Record<string, unknown> {
|
|
407
|
+
const snapshot: Record<string, unknown> = {};
|
|
408
|
+
for (const column of Object.keys(table.columns)) {
|
|
409
|
+
snapshot[column] = entity[column];
|
|
410
|
+
}
|
|
411
|
+
return snapshot;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Gets the primary key value from a tracked entity.
|
|
416
|
+
* @param tracked - The tracked entity
|
|
417
|
+
* @returns Primary key value or null
|
|
418
|
+
*/
|
|
419
|
+
private getPrimaryKeyValue(tracked: TrackedEntity): PrimaryKey | null {
|
|
420
|
+
const key = findPrimaryKey(tracked.table);
|
|
421
|
+
const val = (tracked.entity as Record<string, unknown>)[key];
|
|
422
|
+
if (val === undefined || val === null) return null;
|
|
423
|
+
if (typeof val !== 'string' && typeof val !== 'number') return null;
|
|
424
|
+
return val as PrimaryKey;
|
|
425
|
+
}
|
|
426
|
+
}
|