metal-orm 1.1.11 → 1.1.18
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.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +100 -96
- package/scripts/generate-entities/schema.mjs +122 -122
- package/scripts/generate-entities/tree-detection.mjs +92 -92
- 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/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/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 +90 -90
- 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
|
@@ -1,420 +1,420 @@
|
|
|
1
|
-
import { and, eq } from '../core/ast/expression.js';
|
|
2
|
-
import type { ValueOperandInput } from '../core/ast/expression.js';
|
|
3
|
-
import type { Dialect } from '../core/dialect/abstract.js';
|
|
4
|
-
import { DeleteQueryBuilder } from '../query-builder/delete.js';
|
|
5
|
-
import { InsertQueryBuilder } from '../query-builder/insert.js';
|
|
6
|
-
import { UpdateQueryBuilder } from '../query-builder/update.js';
|
|
7
|
-
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
8
|
-
import type { BelongsToManyRelation, HasManyRelation, HasOneRelation, MorphOneRelation, MorphManyRelation, MorphToRelation } from '../schema/relation.js';
|
|
9
|
-
import { RelationKinds } from '../schema/relation.js';
|
|
10
|
-
import type { TableDef } from '../schema/table.js';
|
|
11
|
-
import type { DbExecutor } from '../core/execution/db-executor.js';
|
|
12
|
-
import type { RelationChangeEntry } from './runtime-types.js';
|
|
13
|
-
import { UnitOfWork } from './unit-of-work.js';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Processes relation changes for entity relationships.
|
|
17
|
-
*/
|
|
18
|
-
export class RelationChangeProcessor {
|
|
19
|
-
private readonly relationChanges: RelationChangeEntry[] = [];
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Creates a new RelationChangeProcessor instance.
|
|
23
|
-
* @param unitOfWork - The unit of work instance
|
|
24
|
-
* @param dialect - The database dialect
|
|
25
|
-
* @param executor - The database executor
|
|
26
|
-
*/
|
|
27
|
-
constructor(
|
|
28
|
-
private readonly unitOfWork: UnitOfWork,
|
|
29
|
-
private readonly dialect: Dialect,
|
|
30
|
-
private readonly executor: DbExecutor
|
|
31
|
-
) { }
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Registers a relation change for processing.
|
|
35
|
-
* @param entry - The relation change entry
|
|
36
|
-
*/
|
|
37
|
-
registerChange(entry: RelationChangeEntry): void {
|
|
38
|
-
this.relationChanges.push(entry);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Resets the relation change processor by clearing all pending changes.
|
|
43
|
-
*/
|
|
44
|
-
reset(): void {
|
|
45
|
-
this.relationChanges.length = 0;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Processes all pending relation changes.
|
|
50
|
-
*/
|
|
51
|
-
async process(): Promise<void> {
|
|
52
|
-
if (!this.relationChanges.length) return;
|
|
53
|
-
const entries = [...this.relationChanges];
|
|
54
|
-
this.relationChanges.length = 0;
|
|
55
|
-
|
|
56
|
-
for (const entry of entries) {
|
|
57
|
-
switch (entry.relation.type) {
|
|
58
|
-
case RelationKinds.HasMany:
|
|
59
|
-
await this.handleHasManyChange(entry);
|
|
60
|
-
break;
|
|
61
|
-
case RelationKinds.HasOne:
|
|
62
|
-
await this.handleHasOneChange(entry);
|
|
63
|
-
break;
|
|
64
|
-
case RelationKinds.BelongsToMany:
|
|
65
|
-
await this.handleBelongsToManyChange(entry);
|
|
66
|
-
break;
|
|
67
|
-
case RelationKinds.BelongsTo:
|
|
68
|
-
await this.handleBelongsToChange(entry);
|
|
69
|
-
break;
|
|
70
|
-
case RelationKinds.MorphOne:
|
|
71
|
-
await this.handleMorphOneChange(entry);
|
|
72
|
-
break;
|
|
73
|
-
case RelationKinds.MorphMany:
|
|
74
|
-
await this.handleMorphManyChange(entry);
|
|
75
|
-
break;
|
|
76
|
-
case RelationKinds.MorphTo:
|
|
77
|
-
await this.handleMorphToChange(entry);
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Handles changes for has-many relations.
|
|
85
|
-
* @param entry - The relation change entry
|
|
86
|
-
*/
|
|
87
|
-
private async handleHasManyChange(entry: RelationChangeEntry): Promise<void> {
|
|
88
|
-
const relation = entry.relation as HasManyRelation;
|
|
89
|
-
const target = entry.change.entity;
|
|
90
|
-
if (!target) return;
|
|
91
|
-
|
|
92
|
-
const tracked = this.unitOfWork.findTracked(target as object);
|
|
93
|
-
if (!tracked) return;
|
|
94
|
-
|
|
95
|
-
const localKey = relation.localKey || findPrimaryKey(entry.rootTable);
|
|
96
|
-
const rootValue = entry.root[localKey];
|
|
97
|
-
if (rootValue === undefined || rootValue === null) return;
|
|
98
|
-
|
|
99
|
-
if (entry.change.kind === 'add' || entry.change.kind === 'attach') {
|
|
100
|
-
this.assignHasManyForeignKey(tracked.entity as Record<string, unknown>, relation, rootValue);
|
|
101
|
-
this.unitOfWork.markDirty(tracked.entity);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (entry.change.kind === 'remove') {
|
|
106
|
-
this.detachHasManyChild(tracked.entity as Record<string, unknown>, relation);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Handles changes for has-one relations.
|
|
112
|
-
* @param entry - The relation change entry
|
|
113
|
-
*/
|
|
114
|
-
private async handleHasOneChange(entry: RelationChangeEntry): Promise<void> {
|
|
115
|
-
const relation = entry.relation as HasOneRelation;
|
|
116
|
-
const target = entry.change.entity;
|
|
117
|
-
if (!target) return;
|
|
118
|
-
|
|
119
|
-
const tracked = this.unitOfWork.findTracked(target as object);
|
|
120
|
-
if (!tracked) return;
|
|
121
|
-
|
|
122
|
-
const localKey = relation.localKey || findPrimaryKey(entry.rootTable);
|
|
123
|
-
const rootValue = entry.root[localKey];
|
|
124
|
-
if (rootValue === undefined || rootValue === null) return;
|
|
125
|
-
|
|
126
|
-
if (entry.change.kind === 'attach' || entry.change.kind === 'add') {
|
|
127
|
-
this.assignHasOneForeignKey(tracked.entity as Record<string, unknown>, relation, rootValue);
|
|
128
|
-
this.unitOfWork.markDirty(tracked.entity);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (entry.change.kind === 'remove') {
|
|
133
|
-
this.detachHasOneChild(tracked.entity as Record<string, unknown>, relation);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Handles changes for belongs-to relations.
|
|
139
|
-
* @param _entry - The relation change entry (reserved for future use)
|
|
140
|
-
*/
|
|
141
|
-
private async handleBelongsToChange(_entry: RelationChangeEntry): Promise<void> {
|
|
142
|
-
void _entry;
|
|
143
|
-
// Reserved for future cascade/persist behaviors for belongs-to relations.
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Handles changes for belongs-to-many relations.
|
|
148
|
-
* @param entry - The relation change entry
|
|
149
|
-
*/
|
|
150
|
-
private async handleBelongsToManyChange(entry: RelationChangeEntry): Promise<void> {
|
|
151
|
-
const relation = entry.relation as BelongsToManyRelation;
|
|
152
|
-
const rootKey = relation.localKey || findPrimaryKey(entry.rootTable);
|
|
153
|
-
const rootId = entry.root[rootKey];
|
|
154
|
-
if (rootId === undefined || rootId === null) return;
|
|
155
|
-
|
|
156
|
-
const targetId = this.resolvePrimaryKeyValue(entry.change.entity as Record<string, unknown>, relation.target);
|
|
157
|
-
if (targetId === null) return;
|
|
158
|
-
|
|
159
|
-
const pivotPayload = this.buildPivotPayload(relation, entry.change.pivot);
|
|
160
|
-
|
|
161
|
-
if (entry.change.kind === 'update') {
|
|
162
|
-
if (pivotPayload) {
|
|
163
|
-
await this.updatePivotRow(relation, rootId, targetId, pivotPayload);
|
|
164
|
-
}
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (entry.change.kind === 'attach' || entry.change.kind === 'add') {
|
|
169
|
-
await this.insertPivotRow(relation, rootId, targetId, pivotPayload);
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (entry.change.kind === 'detach' || entry.change.kind === 'remove') {
|
|
174
|
-
await this.deletePivotRow(relation, rootId, targetId);
|
|
175
|
-
|
|
176
|
-
if (relation.cascade === 'all' || relation.cascade === 'remove') {
|
|
177
|
-
this.unitOfWork.markRemoved(entry.change.entity as object);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Assigns a foreign key for has-many relations.
|
|
184
|
-
* @param child - The child entity
|
|
185
|
-
* @param relation - The has-many relation
|
|
186
|
-
* @param rootValue - The root entity's primary key value
|
|
187
|
-
*/
|
|
188
|
-
private assignHasManyForeignKey(child: Record<string, unknown>, relation: HasManyRelation, rootValue: unknown): void {
|
|
189
|
-
const current = child[relation.foreignKey];
|
|
190
|
-
if (current === rootValue) return;
|
|
191
|
-
child[relation.foreignKey] = rootValue;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Detaches a child entity from has-many relations.
|
|
196
|
-
* @param child - The child entity
|
|
197
|
-
* @param relation - The has-many relation
|
|
198
|
-
*/
|
|
199
|
-
private detachHasManyChild(child: Record<string, unknown>, relation: HasManyRelation): void {
|
|
200
|
-
if (relation.cascade === 'all' || relation.cascade === 'remove') {
|
|
201
|
-
this.unitOfWork.markRemoved(child);
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
child[relation.foreignKey] = null;
|
|
205
|
-
this.unitOfWork.markDirty(child);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Assigns a foreign key for has-one relations.
|
|
210
|
-
* @param child - The child entity
|
|
211
|
-
* @param relation - The has-one relation
|
|
212
|
-
* @param rootValue - The root entity's primary key value
|
|
213
|
-
*/
|
|
214
|
-
private assignHasOneForeignKey(child: Record<string, unknown>, relation: HasOneRelation, rootValue: unknown): void {
|
|
215
|
-
const current = child[relation.foreignKey];
|
|
216
|
-
if (current === rootValue) return;
|
|
217
|
-
child[relation.foreignKey] = rootValue;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Detaches a child entity from has-one relations.
|
|
222
|
-
* @param child - The child entity
|
|
223
|
-
* @param relation - The has-one relation
|
|
224
|
-
*/
|
|
225
|
-
private detachHasOneChild(child: Record<string, unknown>, relation: HasOneRelation): void {
|
|
226
|
-
if (relation.cascade === 'all' || relation.cascade === 'remove') {
|
|
227
|
-
this.unitOfWork.markRemoved(child);
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
child[relation.foreignKey] = null;
|
|
231
|
-
this.unitOfWork.markDirty(child);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Inserts a pivot row for belongs-to-many relations.
|
|
236
|
-
* @param relation - The belongs-to-many relation
|
|
237
|
-
* @param rootId - The root entity's primary key value
|
|
238
|
-
* @param targetId - The target entity's primary key value
|
|
239
|
-
*/
|
|
240
|
-
private async insertPivotRow(
|
|
241
|
-
relation: BelongsToManyRelation,
|
|
242
|
-
rootId: string | number,
|
|
243
|
-
targetId: string | number,
|
|
244
|
-
pivotPayload?: Record<string, unknown>
|
|
245
|
-
): Promise<void> {
|
|
246
|
-
const payload = {
|
|
247
|
-
[relation.pivotForeignKeyToRoot]: rootId,
|
|
248
|
-
[relation.pivotForeignKeyToTarget]: targetId,
|
|
249
|
-
...(pivotPayload ?? {})
|
|
250
|
-
};
|
|
251
|
-
const builder = new InsertQueryBuilder(relation.pivotTable)
|
|
252
|
-
.values(payload as Record<string, ValueOperandInput>);
|
|
253
|
-
const compiled = builder.compile(this.dialect);
|
|
254
|
-
await this.executor.executeSql(compiled.sql, compiled.params);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Updates a pivot row for belongs-to-many relations.
|
|
259
|
-
* @param relation - The belongs-to-many relation
|
|
260
|
-
* @param rootId - The root entity's primary key value
|
|
261
|
-
* @param targetId - The target entity's primary key value
|
|
262
|
-
* @param pivotPayload - The pivot columns to update
|
|
263
|
-
*/
|
|
264
|
-
private async updatePivotRow(
|
|
265
|
-
relation: BelongsToManyRelation,
|
|
266
|
-
rootId: string | number,
|
|
267
|
-
targetId: string | number,
|
|
268
|
-
pivotPayload: Record<string, unknown>
|
|
269
|
-
): Promise<void> {
|
|
270
|
-
const rootCol = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
|
|
271
|
-
const targetCol = relation.pivotTable.columns[relation.pivotForeignKeyToTarget];
|
|
272
|
-
if (!rootCol || !targetCol) return;
|
|
273
|
-
|
|
274
|
-
const builder = new UpdateQueryBuilder(relation.pivotTable)
|
|
275
|
-
.set(pivotPayload)
|
|
276
|
-
.where(and(eq(rootCol, rootId), eq(targetCol, targetId)));
|
|
277
|
-
const compiled = builder.compile(this.dialect);
|
|
278
|
-
await this.executor.executeSql(compiled.sql, compiled.params);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Deletes a pivot row for belongs-to-many relations.
|
|
283
|
-
* @param relation - The belongs-to-many relation
|
|
284
|
-
* @param rootId - The root entity's primary key value
|
|
285
|
-
* @param targetId - The target entity's primary key value
|
|
286
|
-
*/
|
|
287
|
-
private async deletePivotRow(relation: BelongsToManyRelation, rootId: string | number, targetId: string | number): Promise<void> {
|
|
288
|
-
const rootCol = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
|
|
289
|
-
const targetCol = relation.pivotTable.columns[relation.pivotForeignKeyToTarget];
|
|
290
|
-
if (!rootCol || !targetCol) return;
|
|
291
|
-
|
|
292
|
-
const builder = new DeleteQueryBuilder(relation.pivotTable).where(
|
|
293
|
-
and(eq(rootCol, rootId), eq(targetCol, targetId))
|
|
294
|
-
);
|
|
295
|
-
const compiled = builder.compile(this.dialect);
|
|
296
|
-
await this.executor.executeSql(compiled.sql, compiled.params);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Resolves the primary key value from an entity.
|
|
301
|
-
* @param entity - The entity
|
|
302
|
-
* @param table - The table definition
|
|
303
|
-
* @returns The primary key value or null
|
|
304
|
-
*/
|
|
305
|
-
private resolvePrimaryKeyValue(entity: Record<string, unknown>, table: TableDef): string | number | null {
|
|
306
|
-
if (!entity) return null;
|
|
307
|
-
const key = findPrimaryKey(table);
|
|
308
|
-
const value = entity[key];
|
|
309
|
-
if (value === undefined || value === null) return null;
|
|
310
|
-
return (value as string | number | null | undefined) ?? null;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
private buildPivotPayload(
|
|
314
|
-
relation: BelongsToManyRelation,
|
|
315
|
-
pivot: Record<string, unknown> | undefined
|
|
316
|
-
): Record<string, unknown> | undefined {
|
|
317
|
-
if (!pivot) return undefined;
|
|
318
|
-
const payload: Record<string, unknown> = {};
|
|
319
|
-
for (const [key, value] of Object.entries(pivot)) {
|
|
320
|
-
if (value === undefined) continue;
|
|
321
|
-
if (!relation.pivotTable.columns[key]) continue;
|
|
322
|
-
if (key === relation.pivotForeignKeyToRoot || key === relation.pivotForeignKeyToTarget) continue;
|
|
323
|
-
payload[key] = value;
|
|
324
|
-
}
|
|
325
|
-
return Object.keys(payload).length ? payload : undefined;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
private async handleMorphOneChange(entry: RelationChangeEntry): Promise<void> {
|
|
329
|
-
const relation = entry.relation as MorphOneRelation;
|
|
330
|
-
const target = entry.change.entity;
|
|
331
|
-
if (!target) return;
|
|
332
|
-
|
|
333
|
-
const tracked = this.unitOfWork.findTracked(target as object);
|
|
334
|
-
if (!tracked) return;
|
|
335
|
-
|
|
336
|
-
const localKey = relation.localKey || findPrimaryKey(entry.rootTable);
|
|
337
|
-
const rootValue = entry.root[localKey];
|
|
338
|
-
if (rootValue === undefined || rootValue === null) return;
|
|
339
|
-
|
|
340
|
-
if (entry.change.kind === 'add' || entry.change.kind === 'attach') {
|
|
341
|
-
const child = tracked.entity as Record<string, unknown>;
|
|
342
|
-
child[relation.idField] = rootValue;
|
|
343
|
-
child[relation.typeField] = relation.typeValue;
|
|
344
|
-
this.unitOfWork.markDirty(tracked.entity);
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (entry.change.kind === 'remove') {
|
|
349
|
-
const child = tracked.entity as Record<string, unknown>;
|
|
350
|
-
if (relation.cascade === 'all' || relation.cascade === 'remove') {
|
|
351
|
-
this.unitOfWork.markRemoved(child);
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
child[relation.idField] = null;
|
|
355
|
-
child[relation.typeField] = null;
|
|
356
|
-
this.unitOfWork.markDirty(child);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
private async handleMorphManyChange(entry: RelationChangeEntry): Promise<void> {
|
|
361
|
-
const relation = entry.relation as MorphManyRelation;
|
|
362
|
-
const target = entry.change.entity;
|
|
363
|
-
if (!target) return;
|
|
364
|
-
|
|
365
|
-
const tracked = this.unitOfWork.findTracked(target as object);
|
|
366
|
-
if (!tracked) return;
|
|
367
|
-
|
|
368
|
-
const localKey = relation.localKey || findPrimaryKey(entry.rootTable);
|
|
369
|
-
const rootValue = entry.root[localKey];
|
|
370
|
-
if (rootValue === undefined || rootValue === null) return;
|
|
371
|
-
|
|
372
|
-
if (entry.change.kind === 'add' || entry.change.kind === 'attach') {
|
|
373
|
-
const child = tracked.entity as Record<string, unknown>;
|
|
374
|
-
child[relation.idField] = rootValue;
|
|
375
|
-
child[relation.typeField] = relation.typeValue;
|
|
376
|
-
this.unitOfWork.markDirty(tracked.entity);
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (entry.change.kind === 'remove') {
|
|
381
|
-
const child = tracked.entity as Record<string, unknown>;
|
|
382
|
-
if (relation.cascade === 'all' || relation.cascade === 'remove') {
|
|
383
|
-
this.unitOfWork.markRemoved(child);
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
child[relation.idField] = null;
|
|
387
|
-
child[relation.typeField] = null;
|
|
388
|
-
this.unitOfWork.markDirty(child);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
private async handleMorphToChange(entry: RelationChangeEntry): Promise<void> {
|
|
393
|
-
const relation = entry.relation as MorphToRelation;
|
|
394
|
-
const target = entry.change.entity;
|
|
395
|
-
if (!target) return;
|
|
396
|
-
|
|
397
|
-
if (entry.change.kind === 'attach' || entry.change.kind === 'add') {
|
|
398
|
-
const targetEntity = target as Record<string, unknown>;
|
|
399
|
-
for (const [typeValue, targetTable] of Object.entries(relation.targets)) {
|
|
400
|
-
const targetPk = relation.targetKey || findPrimaryKey(targetTable);
|
|
401
|
-
const pkValue = targetEntity[targetPk];
|
|
402
|
-
if (pkValue !== undefined && pkValue !== null) {
|
|
403
|
-
const rootEntity = entry.root as Record<string, unknown>;
|
|
404
|
-
rootEntity[relation.typeField] = typeValue;
|
|
405
|
-
rootEntity[relation.idField] = pkValue;
|
|
406
|
-
this.unitOfWork.markDirty(entry.root as object);
|
|
407
|
-
break;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (entry.change.kind === 'remove') {
|
|
414
|
-
const rootEntity = entry.root as Record<string, unknown>;
|
|
415
|
-
rootEntity[relation.typeField] = null;
|
|
416
|
-
rootEntity[relation.idField] = null;
|
|
417
|
-
this.unitOfWork.markDirty(entry.root as object);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
1
|
+
import { and, eq } from '../core/ast/expression.js';
|
|
2
|
+
import type { ValueOperandInput } from '../core/ast/expression.js';
|
|
3
|
+
import type { Dialect } from '../core/dialect/abstract.js';
|
|
4
|
+
import { DeleteQueryBuilder } from '../query-builder/delete.js';
|
|
5
|
+
import { InsertQueryBuilder } from '../query-builder/insert.js';
|
|
6
|
+
import { UpdateQueryBuilder } from '../query-builder/update.js';
|
|
7
|
+
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
8
|
+
import type { BelongsToManyRelation, HasManyRelation, HasOneRelation, MorphOneRelation, MorphManyRelation, MorphToRelation } from '../schema/relation.js';
|
|
9
|
+
import { RelationKinds } from '../schema/relation.js';
|
|
10
|
+
import type { TableDef } from '../schema/table.js';
|
|
11
|
+
import type { DbExecutor } from '../core/execution/db-executor.js';
|
|
12
|
+
import type { RelationChangeEntry } from './runtime-types.js';
|
|
13
|
+
import { UnitOfWork } from './unit-of-work.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Processes relation changes for entity relationships.
|
|
17
|
+
*/
|
|
18
|
+
export class RelationChangeProcessor {
|
|
19
|
+
private readonly relationChanges: RelationChangeEntry[] = [];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates a new RelationChangeProcessor instance.
|
|
23
|
+
* @param unitOfWork - The unit of work instance
|
|
24
|
+
* @param dialect - The database dialect
|
|
25
|
+
* @param executor - The database executor
|
|
26
|
+
*/
|
|
27
|
+
constructor(
|
|
28
|
+
private readonly unitOfWork: UnitOfWork,
|
|
29
|
+
private readonly dialect: Dialect,
|
|
30
|
+
private readonly executor: DbExecutor
|
|
31
|
+
) { }
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Registers a relation change for processing.
|
|
35
|
+
* @param entry - The relation change entry
|
|
36
|
+
*/
|
|
37
|
+
registerChange(entry: RelationChangeEntry): void {
|
|
38
|
+
this.relationChanges.push(entry);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resets the relation change processor by clearing all pending changes.
|
|
43
|
+
*/
|
|
44
|
+
reset(): void {
|
|
45
|
+
this.relationChanges.length = 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Processes all pending relation changes.
|
|
50
|
+
*/
|
|
51
|
+
async process(): Promise<void> {
|
|
52
|
+
if (!this.relationChanges.length) return;
|
|
53
|
+
const entries = [...this.relationChanges];
|
|
54
|
+
this.relationChanges.length = 0;
|
|
55
|
+
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
switch (entry.relation.type) {
|
|
58
|
+
case RelationKinds.HasMany:
|
|
59
|
+
await this.handleHasManyChange(entry);
|
|
60
|
+
break;
|
|
61
|
+
case RelationKinds.HasOne:
|
|
62
|
+
await this.handleHasOneChange(entry);
|
|
63
|
+
break;
|
|
64
|
+
case RelationKinds.BelongsToMany:
|
|
65
|
+
await this.handleBelongsToManyChange(entry);
|
|
66
|
+
break;
|
|
67
|
+
case RelationKinds.BelongsTo:
|
|
68
|
+
await this.handleBelongsToChange(entry);
|
|
69
|
+
break;
|
|
70
|
+
case RelationKinds.MorphOne:
|
|
71
|
+
await this.handleMorphOneChange(entry);
|
|
72
|
+
break;
|
|
73
|
+
case RelationKinds.MorphMany:
|
|
74
|
+
await this.handleMorphManyChange(entry);
|
|
75
|
+
break;
|
|
76
|
+
case RelationKinds.MorphTo:
|
|
77
|
+
await this.handleMorphToChange(entry);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Handles changes for has-many relations.
|
|
85
|
+
* @param entry - The relation change entry
|
|
86
|
+
*/
|
|
87
|
+
private async handleHasManyChange(entry: RelationChangeEntry): Promise<void> {
|
|
88
|
+
const relation = entry.relation as HasManyRelation;
|
|
89
|
+
const target = entry.change.entity;
|
|
90
|
+
if (!target) return;
|
|
91
|
+
|
|
92
|
+
const tracked = this.unitOfWork.findTracked(target as object);
|
|
93
|
+
if (!tracked) return;
|
|
94
|
+
|
|
95
|
+
const localKey = relation.localKey || findPrimaryKey(entry.rootTable);
|
|
96
|
+
const rootValue = entry.root[localKey];
|
|
97
|
+
if (rootValue === undefined || rootValue === null) return;
|
|
98
|
+
|
|
99
|
+
if (entry.change.kind === 'add' || entry.change.kind === 'attach') {
|
|
100
|
+
this.assignHasManyForeignKey(tracked.entity as Record<string, unknown>, relation, rootValue);
|
|
101
|
+
this.unitOfWork.markDirty(tracked.entity);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (entry.change.kind === 'remove') {
|
|
106
|
+
this.detachHasManyChild(tracked.entity as Record<string, unknown>, relation);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Handles changes for has-one relations.
|
|
112
|
+
* @param entry - The relation change entry
|
|
113
|
+
*/
|
|
114
|
+
private async handleHasOneChange(entry: RelationChangeEntry): Promise<void> {
|
|
115
|
+
const relation = entry.relation as HasOneRelation;
|
|
116
|
+
const target = entry.change.entity;
|
|
117
|
+
if (!target) return;
|
|
118
|
+
|
|
119
|
+
const tracked = this.unitOfWork.findTracked(target as object);
|
|
120
|
+
if (!tracked) return;
|
|
121
|
+
|
|
122
|
+
const localKey = relation.localKey || findPrimaryKey(entry.rootTable);
|
|
123
|
+
const rootValue = entry.root[localKey];
|
|
124
|
+
if (rootValue === undefined || rootValue === null) return;
|
|
125
|
+
|
|
126
|
+
if (entry.change.kind === 'attach' || entry.change.kind === 'add') {
|
|
127
|
+
this.assignHasOneForeignKey(tracked.entity as Record<string, unknown>, relation, rootValue);
|
|
128
|
+
this.unitOfWork.markDirty(tracked.entity);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (entry.change.kind === 'remove') {
|
|
133
|
+
this.detachHasOneChild(tracked.entity as Record<string, unknown>, relation);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Handles changes for belongs-to relations.
|
|
139
|
+
* @param _entry - The relation change entry (reserved for future use)
|
|
140
|
+
*/
|
|
141
|
+
private async handleBelongsToChange(_entry: RelationChangeEntry): Promise<void> {
|
|
142
|
+
void _entry;
|
|
143
|
+
// Reserved for future cascade/persist behaviors for belongs-to relations.
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Handles changes for belongs-to-many relations.
|
|
148
|
+
* @param entry - The relation change entry
|
|
149
|
+
*/
|
|
150
|
+
private async handleBelongsToManyChange(entry: RelationChangeEntry): Promise<void> {
|
|
151
|
+
const relation = entry.relation as BelongsToManyRelation;
|
|
152
|
+
const rootKey = relation.localKey || findPrimaryKey(entry.rootTable);
|
|
153
|
+
const rootId = entry.root[rootKey];
|
|
154
|
+
if (rootId === undefined || rootId === null) return;
|
|
155
|
+
|
|
156
|
+
const targetId = this.resolvePrimaryKeyValue(entry.change.entity as Record<string, unknown>, relation.target);
|
|
157
|
+
if (targetId === null) return;
|
|
158
|
+
|
|
159
|
+
const pivotPayload = this.buildPivotPayload(relation, entry.change.pivot);
|
|
160
|
+
|
|
161
|
+
if (entry.change.kind === 'update') {
|
|
162
|
+
if (pivotPayload) {
|
|
163
|
+
await this.updatePivotRow(relation, rootId, targetId, pivotPayload);
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (entry.change.kind === 'attach' || entry.change.kind === 'add') {
|
|
169
|
+
await this.insertPivotRow(relation, rootId, targetId, pivotPayload);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (entry.change.kind === 'detach' || entry.change.kind === 'remove') {
|
|
174
|
+
await this.deletePivotRow(relation, rootId, targetId);
|
|
175
|
+
|
|
176
|
+
if (relation.cascade === 'all' || relation.cascade === 'remove') {
|
|
177
|
+
this.unitOfWork.markRemoved(entry.change.entity as object);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Assigns a foreign key for has-many relations.
|
|
184
|
+
* @param child - The child entity
|
|
185
|
+
* @param relation - The has-many relation
|
|
186
|
+
* @param rootValue - The root entity's primary key value
|
|
187
|
+
*/
|
|
188
|
+
private assignHasManyForeignKey(child: Record<string, unknown>, relation: HasManyRelation, rootValue: unknown): void {
|
|
189
|
+
const current = child[relation.foreignKey];
|
|
190
|
+
if (current === rootValue) return;
|
|
191
|
+
child[relation.foreignKey] = rootValue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Detaches a child entity from has-many relations.
|
|
196
|
+
* @param child - The child entity
|
|
197
|
+
* @param relation - The has-many relation
|
|
198
|
+
*/
|
|
199
|
+
private detachHasManyChild(child: Record<string, unknown>, relation: HasManyRelation): void {
|
|
200
|
+
if (relation.cascade === 'all' || relation.cascade === 'remove') {
|
|
201
|
+
this.unitOfWork.markRemoved(child);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
child[relation.foreignKey] = null;
|
|
205
|
+
this.unitOfWork.markDirty(child);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Assigns a foreign key for has-one relations.
|
|
210
|
+
* @param child - The child entity
|
|
211
|
+
* @param relation - The has-one relation
|
|
212
|
+
* @param rootValue - The root entity's primary key value
|
|
213
|
+
*/
|
|
214
|
+
private assignHasOneForeignKey(child: Record<string, unknown>, relation: HasOneRelation, rootValue: unknown): void {
|
|
215
|
+
const current = child[relation.foreignKey];
|
|
216
|
+
if (current === rootValue) return;
|
|
217
|
+
child[relation.foreignKey] = rootValue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Detaches a child entity from has-one relations.
|
|
222
|
+
* @param child - The child entity
|
|
223
|
+
* @param relation - The has-one relation
|
|
224
|
+
*/
|
|
225
|
+
private detachHasOneChild(child: Record<string, unknown>, relation: HasOneRelation): void {
|
|
226
|
+
if (relation.cascade === 'all' || relation.cascade === 'remove') {
|
|
227
|
+
this.unitOfWork.markRemoved(child);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
child[relation.foreignKey] = null;
|
|
231
|
+
this.unitOfWork.markDirty(child);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Inserts a pivot row for belongs-to-many relations.
|
|
236
|
+
* @param relation - The belongs-to-many relation
|
|
237
|
+
* @param rootId - The root entity's primary key value
|
|
238
|
+
* @param targetId - The target entity's primary key value
|
|
239
|
+
*/
|
|
240
|
+
private async insertPivotRow(
|
|
241
|
+
relation: BelongsToManyRelation,
|
|
242
|
+
rootId: string | number,
|
|
243
|
+
targetId: string | number,
|
|
244
|
+
pivotPayload?: Record<string, unknown>
|
|
245
|
+
): Promise<void> {
|
|
246
|
+
const payload = {
|
|
247
|
+
[relation.pivotForeignKeyToRoot]: rootId,
|
|
248
|
+
[relation.pivotForeignKeyToTarget]: targetId,
|
|
249
|
+
...(pivotPayload ?? {})
|
|
250
|
+
};
|
|
251
|
+
const builder = new InsertQueryBuilder(relation.pivotTable)
|
|
252
|
+
.values(payload as Record<string, ValueOperandInput>);
|
|
253
|
+
const compiled = builder.compile(this.dialect);
|
|
254
|
+
await this.executor.executeSql(compiled.sql, compiled.params);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Updates a pivot row for belongs-to-many relations.
|
|
259
|
+
* @param relation - The belongs-to-many relation
|
|
260
|
+
* @param rootId - The root entity's primary key value
|
|
261
|
+
* @param targetId - The target entity's primary key value
|
|
262
|
+
* @param pivotPayload - The pivot columns to update
|
|
263
|
+
*/
|
|
264
|
+
private async updatePivotRow(
|
|
265
|
+
relation: BelongsToManyRelation,
|
|
266
|
+
rootId: string | number,
|
|
267
|
+
targetId: string | number,
|
|
268
|
+
pivotPayload: Record<string, unknown>
|
|
269
|
+
): Promise<void> {
|
|
270
|
+
const rootCol = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
|
|
271
|
+
const targetCol = relation.pivotTable.columns[relation.pivotForeignKeyToTarget];
|
|
272
|
+
if (!rootCol || !targetCol) return;
|
|
273
|
+
|
|
274
|
+
const builder = new UpdateQueryBuilder(relation.pivotTable)
|
|
275
|
+
.set(pivotPayload)
|
|
276
|
+
.where(and(eq(rootCol, rootId), eq(targetCol, targetId)));
|
|
277
|
+
const compiled = builder.compile(this.dialect);
|
|
278
|
+
await this.executor.executeSql(compiled.sql, compiled.params);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Deletes a pivot row for belongs-to-many relations.
|
|
283
|
+
* @param relation - The belongs-to-many relation
|
|
284
|
+
* @param rootId - The root entity's primary key value
|
|
285
|
+
* @param targetId - The target entity's primary key value
|
|
286
|
+
*/
|
|
287
|
+
private async deletePivotRow(relation: BelongsToManyRelation, rootId: string | number, targetId: string | number): Promise<void> {
|
|
288
|
+
const rootCol = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
|
|
289
|
+
const targetCol = relation.pivotTable.columns[relation.pivotForeignKeyToTarget];
|
|
290
|
+
if (!rootCol || !targetCol) return;
|
|
291
|
+
|
|
292
|
+
const builder = new DeleteQueryBuilder(relation.pivotTable).where(
|
|
293
|
+
and(eq(rootCol, rootId), eq(targetCol, targetId))
|
|
294
|
+
);
|
|
295
|
+
const compiled = builder.compile(this.dialect);
|
|
296
|
+
await this.executor.executeSql(compiled.sql, compiled.params);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Resolves the primary key value from an entity.
|
|
301
|
+
* @param entity - The entity
|
|
302
|
+
* @param table - The table definition
|
|
303
|
+
* @returns The primary key value or null
|
|
304
|
+
*/
|
|
305
|
+
private resolvePrimaryKeyValue(entity: Record<string, unknown>, table: TableDef): string | number | null {
|
|
306
|
+
if (!entity) return null;
|
|
307
|
+
const key = findPrimaryKey(table);
|
|
308
|
+
const value = entity[key];
|
|
309
|
+
if (value === undefined || value === null) return null;
|
|
310
|
+
return (value as string | number | null | undefined) ?? null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private buildPivotPayload(
|
|
314
|
+
relation: BelongsToManyRelation,
|
|
315
|
+
pivot: Record<string, unknown> | undefined
|
|
316
|
+
): Record<string, unknown> | undefined {
|
|
317
|
+
if (!pivot) return undefined;
|
|
318
|
+
const payload: Record<string, unknown> = {};
|
|
319
|
+
for (const [key, value] of Object.entries(pivot)) {
|
|
320
|
+
if (value === undefined) continue;
|
|
321
|
+
if (!relation.pivotTable.columns[key]) continue;
|
|
322
|
+
if (key === relation.pivotForeignKeyToRoot || key === relation.pivotForeignKeyToTarget) continue;
|
|
323
|
+
payload[key] = value;
|
|
324
|
+
}
|
|
325
|
+
return Object.keys(payload).length ? payload : undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private async handleMorphOneChange(entry: RelationChangeEntry): Promise<void> {
|
|
329
|
+
const relation = entry.relation as MorphOneRelation;
|
|
330
|
+
const target = entry.change.entity;
|
|
331
|
+
if (!target) return;
|
|
332
|
+
|
|
333
|
+
const tracked = this.unitOfWork.findTracked(target as object);
|
|
334
|
+
if (!tracked) return;
|
|
335
|
+
|
|
336
|
+
const localKey = relation.localKey || findPrimaryKey(entry.rootTable);
|
|
337
|
+
const rootValue = entry.root[localKey];
|
|
338
|
+
if (rootValue === undefined || rootValue === null) return;
|
|
339
|
+
|
|
340
|
+
if (entry.change.kind === 'add' || entry.change.kind === 'attach') {
|
|
341
|
+
const child = tracked.entity as Record<string, unknown>;
|
|
342
|
+
child[relation.idField] = rootValue;
|
|
343
|
+
child[relation.typeField] = relation.typeValue;
|
|
344
|
+
this.unitOfWork.markDirty(tracked.entity);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (entry.change.kind === 'remove') {
|
|
349
|
+
const child = tracked.entity as Record<string, unknown>;
|
|
350
|
+
if (relation.cascade === 'all' || relation.cascade === 'remove') {
|
|
351
|
+
this.unitOfWork.markRemoved(child);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
child[relation.idField] = null;
|
|
355
|
+
child[relation.typeField] = null;
|
|
356
|
+
this.unitOfWork.markDirty(child);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private async handleMorphManyChange(entry: RelationChangeEntry): Promise<void> {
|
|
361
|
+
const relation = entry.relation as MorphManyRelation;
|
|
362
|
+
const target = entry.change.entity;
|
|
363
|
+
if (!target) return;
|
|
364
|
+
|
|
365
|
+
const tracked = this.unitOfWork.findTracked(target as object);
|
|
366
|
+
if (!tracked) return;
|
|
367
|
+
|
|
368
|
+
const localKey = relation.localKey || findPrimaryKey(entry.rootTable);
|
|
369
|
+
const rootValue = entry.root[localKey];
|
|
370
|
+
if (rootValue === undefined || rootValue === null) return;
|
|
371
|
+
|
|
372
|
+
if (entry.change.kind === 'add' || entry.change.kind === 'attach') {
|
|
373
|
+
const child = tracked.entity as Record<string, unknown>;
|
|
374
|
+
child[relation.idField] = rootValue;
|
|
375
|
+
child[relation.typeField] = relation.typeValue;
|
|
376
|
+
this.unitOfWork.markDirty(tracked.entity);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (entry.change.kind === 'remove') {
|
|
381
|
+
const child = tracked.entity as Record<string, unknown>;
|
|
382
|
+
if (relation.cascade === 'all' || relation.cascade === 'remove') {
|
|
383
|
+
this.unitOfWork.markRemoved(child);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
child[relation.idField] = null;
|
|
387
|
+
child[relation.typeField] = null;
|
|
388
|
+
this.unitOfWork.markDirty(child);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private async handleMorphToChange(entry: RelationChangeEntry): Promise<void> {
|
|
393
|
+
const relation = entry.relation as MorphToRelation;
|
|
394
|
+
const target = entry.change.entity;
|
|
395
|
+
if (!target) return;
|
|
396
|
+
|
|
397
|
+
if (entry.change.kind === 'attach' || entry.change.kind === 'add') {
|
|
398
|
+
const targetEntity = target as Record<string, unknown>;
|
|
399
|
+
for (const [typeValue, targetTable] of Object.entries(relation.targets)) {
|
|
400
|
+
const targetPk = relation.targetKey || findPrimaryKey(targetTable);
|
|
401
|
+
const pkValue = targetEntity[targetPk];
|
|
402
|
+
if (pkValue !== undefined && pkValue !== null) {
|
|
403
|
+
const rootEntity = entry.root as Record<string, unknown>;
|
|
404
|
+
rootEntity[relation.typeField] = typeValue;
|
|
405
|
+
rootEntity[relation.idField] = pkValue;
|
|
406
|
+
this.unitOfWork.markDirty(entry.root as object);
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (entry.change.kind === 'remove') {
|
|
414
|
+
const rootEntity = entry.root as Record<string, unknown>;
|
|
415
|
+
rootEntity[relation.typeField] = null;
|
|
416
|
+
rootEntity[relation.idField] = null;
|
|
417
|
+
this.unitOfWork.markDirty(entry.root as object);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|