@danceroutine/tango-orm 1.7.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/InternalDialect-ClSaUNso.js +10 -0
- package/dist/InternalDialect-ClSaUNso.js.map +1 -0
- package/dist/PostgresAdapter-CXKdKBG-.js +4 -0
- package/dist/PostgresAdapter-DySFW6vy.js +128 -0
- package/dist/PostgresAdapter-DySFW6vy.js.map +1 -0
- package/dist/{SqliteClient-CjOK9-ki.js → SqliteAdapter-CDdOjRmW.js} +57 -3
- package/dist/SqliteAdapter-CDdOjRmW.js.map +1 -0
- package/dist/SqliteAdapter-mjtXuVTg.js +4 -0
- package/dist/connection/adapters/Adapter.d.ts +32 -1
- package/dist/connection/adapters/dialects/PostgresAdapter.d.ts +5 -6
- package/dist/connection/adapters/dialects/SqliteAdapter.d.ts +4 -6
- package/dist/connection/adapters/index.d.ts +1 -1
- package/dist/connection/index.d.ts +1 -1
- package/dist/connection/index.js +4 -5
- package/dist/{connection-B_K2ZAf7.js → connection-Dmhgx31M.js} +5 -7
- package/dist/{connection-B_K2ZAf7.js.map → connection-Dmhgx31M.js.map} +1 -1
- package/dist/{defaultRuntime-BPK9kWEW.js → defaultRuntime-DzqBQ9Hb.js} +63 -16
- package/dist/defaultRuntime-DzqBQ9Hb.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +11 -12
- package/dist/manager/ModelManager.d.ts +25 -5
- package/dist/manager/index.d.ts +6 -0
- package/dist/manager/index.js +8 -7
- package/dist/manager/internal/MutationCompiler.d.ts +14 -6
- package/dist/manager/relations/ManyToManyRelatedManager.d.ts +147 -0
- package/dist/manager/relations/ManyToManyRelatedQuerySet.d.ts +62 -0
- package/dist/manager/relations/MaterializedModelRecord.d.ts +28 -0
- package/dist/manager/relations/index.d.ts +9 -0
- package/dist/manager/relations/internal/ThroughTableManager.d.ts +79 -0
- package/dist/manager-DrDTiCAz.js +24 -0
- package/dist/manager-DrDTiCAz.js.map +1 -0
- package/dist/query/ModelQuerySet.d.ts +20 -0
- package/dist/query/QBuilder.d.ts +3 -3
- package/dist/query/QuerySet.d.ts +49 -21
- package/dist/query/compiler/QueryCompiler.d.ts +13 -4
- package/dist/query/domain/CompiledQuery.d.ts +169 -2
- package/dist/query/domain/FilterInput.d.ts +1 -1
- package/dist/query/domain/FilterKey.d.ts +4 -2
- package/dist/query/domain/QNode.d.ts +4 -4
- package/dist/query/domain/QuerySetState.d.ts +3 -3
- package/dist/query/domain/RelationMeta.d.ts +9 -0
- package/dist/query/domain/RelationTyping.d.ts +47 -0
- package/dist/query/domain/TableMetaFactory.d.ts +1 -14
- package/dist/query/domain/index.d.ts +1 -1
- package/dist/query/domain/internal/InternalPrefetchQueryKind.d.ts +20 -0
- package/dist/query/index.d.ts +1 -0
- package/dist/query/index.js +3 -2
- package/dist/query/planning/QueryPlanner.d.ts +1 -1
- package/dist/{query-FZJoSCg4.js → query-DUZnBFhf.js} +425 -166
- package/dist/query-DUZnBFhf.js.map +1 -0
- package/dist/registerModelObjects-DxlBfuUN.js +797 -0
- package/dist/registerModelObjects-DxlBfuUN.js.map +1 -0
- package/dist/runtime/TangoRuntime.d.ts +9 -0
- package/dist/runtime/index.d.ts +3 -2
- package/dist/runtime/index.js +7 -6
- package/dist/runtime/internal/SqliteDBClientProvider.d.ts +3 -0
- package/dist/{runtime-ByXbpVBS.js → runtime-1H88J3nN.js} +3 -3
- package/dist/runtime-1H88J3nN.js.map +1 -0
- package/dist/transaction/index.js +5 -4
- package/dist/{transaction-Cs0Z9tbW.js → transaction-ZhfDf-f8.js} +2 -2
- package/dist/{transaction-Cs0Z9tbW.js.map → transaction-ZhfDf-f8.js.map} +1 -1
- package/dist/validation/SQLValidationEngine.d.ts +22 -5
- package/dist/validation/SqlValidationPlan.d.ts +5 -4
- package/dist/validation/internal/InternalSqlValidationPlanKind.d.ts +25 -0
- package/dist/validation/internal/InternalValidatedFilterDescriptorKind.d.ts +4 -0
- package/package.json +6 -6
- package/dist/PostgresAdapter-BFdo_nIt.js +0 -4
- package/dist/PostgresAdapter-CMiEpHya.js +0 -49
- package/dist/PostgresAdapter-CMiEpHya.js.map +0 -1
- package/dist/PostgresClient-BQJZfEOT.js +0 -68
- package/dist/PostgresClient-BQJZfEOT.js.map +0 -1
- package/dist/SqliteAdapter-A-P9zUhP.js +0 -4
- package/dist/SqliteAdapter-CeqhyrPC.js +0 -44
- package/dist/SqliteAdapter-CeqhyrPC.js.map +0 -1
- package/dist/SqliteClient-CjOK9-ki.js.map +0 -1
- package/dist/defaultRuntime-BPK9kWEW.js.map +0 -1
- package/dist/manager-C6oJ2tAF.js +0 -13
- package/dist/manager-C6oJ2tAF.js.map +0 -1
- package/dist/query-FZJoSCg4.js.map +0 -1
- package/dist/registerModelObjects-C-1RbUHS.js +0 -385
- package/dist/registerModelObjects-C-1RbUHS.js.map +0 -1
- package/dist/runtime-ByXbpVBS.js.map +0 -1
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
import { InternalQNodeType, InternalRelationKind, InternalSqlValidationPlanKind, ModelQuerySet, OrmSqlSafetyAdapter, QBuilder, QueryResult, QuerySet, TableMetaFactory, isQNodeLike } from "./query-DUZnBFhf.js";
|
|
2
|
+
import { RuntimeBoundClient, TransactionEngine, getTangoRuntime } from "./defaultRuntime-DzqBQ9Hb.js";
|
|
3
|
+
import { NotFoundError } from "@danceroutine/tango-core";
|
|
4
|
+
import { ModelRegistry, registerModelAugmentor } from "@danceroutine/tango-schema";
|
|
5
|
+
|
|
6
|
+
//#region src/manager/relations/ManyToManyRelatedQuerySet.ts
|
|
7
|
+
function applyShape(rows, shape) {
|
|
8
|
+
return typeof shape === "function" ? rows.map(shape) : rows.map((row) => shape.parse(row));
|
|
9
|
+
}
|
|
10
|
+
var ManyToManyRelatedQuerySet = class ManyToManyRelatedQuerySet extends QuerySet {
|
|
11
|
+
constructor(executor, bridge, state = {}) {
|
|
12
|
+
super(executor, state);
|
|
13
|
+
this.bridge = bridge;
|
|
14
|
+
}
|
|
15
|
+
async fetch(shape) {
|
|
16
|
+
if (this.isStateTrivial()) {
|
|
17
|
+
const cache = this.bridge.getCache();
|
|
18
|
+
if (cache !== null) {
|
|
19
|
+
const results = shape ? applyShape(cache, shape) : [...cache];
|
|
20
|
+
return new QueryResult(results);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const ids = await this.bridge.fetchTargetIds();
|
|
24
|
+
if (ids.length === 0) return new QueryResult([]);
|
|
25
|
+
const scopedQs = new ModelQuerySet(this.executor, this.scopedState(ids));
|
|
26
|
+
return shape ? scopedQs.fetch(shape) : scopedQs.fetch();
|
|
27
|
+
}
|
|
28
|
+
async fetchOne(shape) {
|
|
29
|
+
const result = shape ? await this.fetch(shape) : await this.fetch();
|
|
30
|
+
return result.items[0] ?? null;
|
|
31
|
+
}
|
|
32
|
+
async count() {
|
|
33
|
+
if (this.isStateTrivial()) {
|
|
34
|
+
const cache = this.bridge.getCache();
|
|
35
|
+
if (cache !== null) return cache.length;
|
|
36
|
+
}
|
|
37
|
+
const ids = await this.bridge.fetchTargetIds();
|
|
38
|
+
if (ids.length === 0) return 0;
|
|
39
|
+
if (this.isStateTrivial()) return ids.length;
|
|
40
|
+
const scopedQs = new ModelQuerySet(this.executor, this.scopedState(ids));
|
|
41
|
+
return scopedQs.count();
|
|
42
|
+
}
|
|
43
|
+
spawn(state) {
|
|
44
|
+
return new ManyToManyRelatedQuerySet(this.executor, this.bridge, state);
|
|
45
|
+
}
|
|
46
|
+
isStateTrivial() {
|
|
47
|
+
return Object.keys(this.state).length === 0;
|
|
48
|
+
}
|
|
49
|
+
scopedState(ids) {
|
|
50
|
+
const inFilter = { [`${this.bridge.targetPrimaryKeyField}__in`]: [...ids] };
|
|
51
|
+
const inAtom = {
|
|
52
|
+
kind: InternalQNodeType.ATOM,
|
|
53
|
+
where: inFilter
|
|
54
|
+
};
|
|
55
|
+
const merged = this.state.q ? QBuilder.and(inAtom, this.state.q) : inAtom;
|
|
56
|
+
return {
|
|
57
|
+
...this.state,
|
|
58
|
+
q: merged
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/manager/internal/MutationCompiler.ts
|
|
65
|
+
const InternalDuplicateInsertPolicy = {
|
|
66
|
+
ERROR: "error",
|
|
67
|
+
IGNORE: "ignore"
|
|
68
|
+
};
|
|
69
|
+
var MutationCompiler = class {
|
|
70
|
+
adapter;
|
|
71
|
+
placeholders;
|
|
72
|
+
constructor(adapter) {
|
|
73
|
+
this.adapter = adapter;
|
|
74
|
+
this.placeholders = adapter.placeholders;
|
|
75
|
+
}
|
|
76
|
+
compileInsert(plan, values) {
|
|
77
|
+
return {
|
|
78
|
+
sql: `INSERT INTO ${plan.meta.table} (${plan.writeKeys.join(", ")}) VALUES (${this.placeholders.list(plan.writeKeys.length)}) RETURNING *`,
|
|
79
|
+
params: values
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
compileUpdate(plan, values, id) {
|
|
83
|
+
const sets = plan.writeKeys.map((key, index) => `${key} = ${this.placeholders.at(index + 1)}`).join(", ");
|
|
84
|
+
const whereParam = this.placeholders.at(plan.writeKeys.length + 1);
|
|
85
|
+
return {
|
|
86
|
+
sql: `UPDATE ${plan.meta.table} SET ${sets} WHERE ${plan.meta.pk} = ${whereParam} RETURNING *`,
|
|
87
|
+
params: [...values, id]
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
compileDelete(plan, id) {
|
|
91
|
+
return {
|
|
92
|
+
sql: `DELETE FROM ${plan.meta.table} WHERE ${plan.meta.pk} = ${this.placeholders.at(1)}`,
|
|
93
|
+
params: [id]
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
compileDeleteByJoinKeys(plan, leftFilterKey, rightFilterKey, leftValue, rightValue) {
|
|
97
|
+
const leftDescriptor = plan.filterKeys[leftFilterKey];
|
|
98
|
+
const rightDescriptor = plan.filterKeys[rightFilterKey];
|
|
99
|
+
if (!leftDescriptor || !rightDescriptor) throw new Error(`MutationCompiler.compileDeleteByJoinKeys: filter keys '${leftFilterKey}' and '${rightFilterKey}' must be present on the validated plan.`);
|
|
100
|
+
return {
|
|
101
|
+
sql: `DELETE FROM ${plan.meta.table} WHERE ${leftDescriptor.field} = ${this.placeholders.at(1)} AND ${rightDescriptor.field} = ${this.placeholders.at(2)}`,
|
|
102
|
+
params: [leftValue, rightValue]
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
compileBulkInsert(plan, valueRows) {
|
|
106
|
+
const columnCount = plan.writeKeys.length;
|
|
107
|
+
const placeholders = valueRows.map((_row, rowIndex) => `(${this.placeholders.listFromOffset(columnCount, rowIndex * columnCount)})`).join(", ");
|
|
108
|
+
return {
|
|
109
|
+
sql: `INSERT INTO ${plan.meta.table} (${plan.writeKeys.join(", ")}) VALUES ${placeholders} RETURNING *`,
|
|
110
|
+
params: valueRows.flat()
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
compileInsertJoinLinks(plan, sourceKey, targetKey, ownerValue, targetValues, duplicatePolicy) {
|
|
114
|
+
const valueRows = targetValues.map((targetValue) => [ownerValue, targetValue]);
|
|
115
|
+
const placeholders = valueRows.map((_row, rowIndex) => `(${this.placeholders.listFromOffset(2, rowIndex * 2)})`).join(", ");
|
|
116
|
+
const params = valueRows.flat();
|
|
117
|
+
if (duplicatePolicy === InternalDuplicateInsertPolicy.IGNORE) switch (this.adapter.dialect) {
|
|
118
|
+
case "postgres": return {
|
|
119
|
+
sql: `INSERT INTO ${plan.meta.table} (${plan.writeKeys.join(", ")}) VALUES ${placeholders} ON CONFLICT (${sourceKey}, ${targetKey}) DO NOTHING`,
|
|
120
|
+
params
|
|
121
|
+
};
|
|
122
|
+
case "sqlite": return {
|
|
123
|
+
sql: `INSERT OR IGNORE INTO ${plan.meta.table} (${plan.writeKeys.join(", ")}) VALUES ${placeholders}`,
|
|
124
|
+
params
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
sql: `INSERT INTO ${plan.meta.table} (${plan.writeKeys.join(", ")}) VALUES ${placeholders}`,
|
|
129
|
+
params
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
compileDeleteJoinLinks(plan, leftFilterKey, rightFilterKey, leftValue, rightValues) {
|
|
133
|
+
const leftDescriptor = plan.filterKeys[leftFilterKey];
|
|
134
|
+
const rightDescriptor = plan.filterKeys[rightFilterKey];
|
|
135
|
+
if (!leftDescriptor || !rightDescriptor) throw new Error(`MutationCompiler.compileDeleteJoinLinks: filter keys '${leftFilterKey}' and '${rightFilterKey}' must be present on the validated plan.`);
|
|
136
|
+
if (rightValues.length === 0) throw new Error("MutationCompiler.compileDeleteJoinLinks requires at least one target value.");
|
|
137
|
+
if (rightValues.length === 1) return {
|
|
138
|
+
sql: `DELETE FROM ${plan.meta.table} WHERE ${leftDescriptor.field} = ${this.placeholders.at(1)} AND ${rightDescriptor.field} = ${this.placeholders.at(2)}`,
|
|
139
|
+
params: [leftValue, rightValues[0]]
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
sql: `DELETE FROM ${plan.meta.table} WHERE ${leftDescriptor.field} = ${this.placeholders.at(1)} AND ${rightDescriptor.field} IN (${this.placeholders.listFromOffset(rightValues.length, 1)})`,
|
|
143
|
+
params: [leftValue, ...rightValues]
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/manager/relations/internal/ThroughTableManager.ts
|
|
150
|
+
var ThroughTableManager = class ThroughTableManager {
|
|
151
|
+
constructor(client, mutationCompiler, descriptor, adapter, sqlSafetyAdapter$1 = new OrmSqlSafetyAdapter()) {
|
|
152
|
+
this.client = client;
|
|
153
|
+
this.mutationCompiler = mutationCompiler;
|
|
154
|
+
this.descriptor = descriptor;
|
|
155
|
+
this.adapter = adapter;
|
|
156
|
+
this.sqlSafetyAdapter = sqlSafetyAdapter$1;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Derive a {@link ThroughTableLinkDescriptor} from the through-model
|
|
160
|
+
* metadata exposed by a many-to-many relation edge.
|
|
161
|
+
*/
|
|
162
|
+
static buildLinkDescriptor(relation, throughModelFields) {
|
|
163
|
+
if (!relation.throughTable || !relation.throughSourceKey || !relation.throughTargetKey) throw new Error("Cannot derive a through-table descriptor from a relation that is not a persisted many-to-many edge.");
|
|
164
|
+
const primaryKeyField = throughModelFields.find((field) => field.primaryKey);
|
|
165
|
+
if (!primaryKeyField) throw new Error("Through-model metadata is missing a primary-key field.");
|
|
166
|
+
return {
|
|
167
|
+
table: relation.throughTable,
|
|
168
|
+
primaryKey: primaryKeyField.name,
|
|
169
|
+
columns: Object.fromEntries(throughModelFields.map((field) => [field.name, field.type])),
|
|
170
|
+
sourceColumn: relation.throughSourceKey,
|
|
171
|
+
targetColumn: relation.throughTargetKey
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Convenience factory that derives the join-table descriptor from the
|
|
176
|
+
* relation edge and instantiates a {@link ThroughTableManager} in one
|
|
177
|
+
* step.
|
|
178
|
+
*/
|
|
179
|
+
static fromRelation(inputs) {
|
|
180
|
+
return new ThroughTableManager(inputs.client, inputs.mutationCompiler, ThroughTableManager.buildLinkDescriptor(inputs.relation, inputs.throughModelFields), inputs.adapter, inputs.sqlSafetyAdapter);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Read every target primary-key value linked to the supplied owner via
|
|
184
|
+
* the join table. Used by ManyToManyRelatedManager.all to scope
|
|
185
|
+
* follow-up target queries to the current owner.
|
|
186
|
+
*/
|
|
187
|
+
async selectTargetIdsForOwner(ownerPrimaryKey) {
|
|
188
|
+
const validated = this.sqlSafetyAdapter.validate({
|
|
189
|
+
kind: InternalSqlValidationPlanKind.SELECT,
|
|
190
|
+
meta: {
|
|
191
|
+
table: this.descriptor.table,
|
|
192
|
+
pk: this.descriptor.primaryKey,
|
|
193
|
+
columns: this.descriptor.columns
|
|
194
|
+
},
|
|
195
|
+
filterKeys: [this.descriptor.sourceColumn, this.descriptor.targetColumn]
|
|
196
|
+
});
|
|
197
|
+
const sourceColumn = validated.filterKeys[this.descriptor.sourceColumn].field;
|
|
198
|
+
const targetColumn = validated.filterKeys[this.descriptor.targetColumn].field;
|
|
199
|
+
const placeholder = this.adapter.placeholders.at(1);
|
|
200
|
+
const sql = `SELECT ${targetColumn} AS target_id FROM ${validated.meta.table} WHERE ${sourceColumn} = ${placeholder}`;
|
|
201
|
+
const result = await this.client.query(sql, [ownerPrimaryKey]);
|
|
202
|
+
return result.rows.map((row) => row.target_id).filter((value) => typeof value === "string" || typeof value === "number");
|
|
203
|
+
}
|
|
204
|
+
async insertLink(ownerPrimaryKey, targetPrimaryKey, options = {}) {
|
|
205
|
+
await this.insertLinks(ownerPrimaryKey, [targetPrimaryKey], options);
|
|
206
|
+
}
|
|
207
|
+
async insertLinks(ownerPrimaryKey, targetPrimaryKeys, options = {}) {
|
|
208
|
+
if (targetPrimaryKeys.length === 0) return;
|
|
209
|
+
const validatedPlan = this.sqlSafetyAdapter.validate({
|
|
210
|
+
kind: InternalSqlValidationPlanKind.INSERT,
|
|
211
|
+
meta: {
|
|
212
|
+
table: this.descriptor.table,
|
|
213
|
+
pk: this.descriptor.primaryKey,
|
|
214
|
+
columns: this.descriptor.columns
|
|
215
|
+
},
|
|
216
|
+
writeKeys: [this.descriptor.sourceColumn, this.descriptor.targetColumn]
|
|
217
|
+
});
|
|
218
|
+
const duplicatePolicy = options.onDuplicate ?? InternalDuplicateInsertPolicy.ERROR;
|
|
219
|
+
if (duplicatePolicy === InternalDuplicateInsertPolicy.IGNORE && !this.adapter.features.ignoreDuplicateInsert) throw new Error(`Adapter '${this.adapter.name}' does not support duplicate-safe link insertion for many-to-many writes.`);
|
|
220
|
+
const compiled = this.mutationCompiler.compileInsertJoinLinks(validatedPlan, this.descriptor.sourceColumn, this.descriptor.targetColumn, ownerPrimaryKey, targetPrimaryKeys, duplicatePolicy);
|
|
221
|
+
await this.client.query(compiled.sql, compiled.params);
|
|
222
|
+
}
|
|
223
|
+
async deleteLink(ownerPrimaryKey, targetPrimaryKey) {
|
|
224
|
+
await this.deleteLinks(ownerPrimaryKey, [targetPrimaryKey]);
|
|
225
|
+
}
|
|
226
|
+
async deleteLinks(ownerPrimaryKey, targetPrimaryKeys) {
|
|
227
|
+
if (targetPrimaryKeys.length === 0) return;
|
|
228
|
+
const validated = this.sqlSafetyAdapter.validate({
|
|
229
|
+
kind: InternalSqlValidationPlanKind.SELECT,
|
|
230
|
+
meta: {
|
|
231
|
+
table: this.descriptor.table,
|
|
232
|
+
pk: this.descriptor.primaryKey,
|
|
233
|
+
columns: this.descriptor.columns
|
|
234
|
+
},
|
|
235
|
+
filterKeys: [this.descriptor.sourceColumn, this.descriptor.targetColumn]
|
|
236
|
+
});
|
|
237
|
+
const compiled = this.mutationCompiler.compileDeleteJoinLinks(validated, this.descriptor.sourceColumn, this.descriptor.targetColumn, ownerPrimaryKey, targetPrimaryKeys);
|
|
238
|
+
await this.client.query(compiled.sql, compiled.params);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region src/manager/relations/ManyToManyRelatedManager.ts
|
|
244
|
+
var ManyToManyRelatedManager = class ManyToManyRelatedManager {
|
|
245
|
+
static BRAND = "tango.orm.m2m_related_manager";
|
|
246
|
+
__tangoBrand = ManyToManyRelatedManager.BRAND;
|
|
247
|
+
prefetchCache = null;
|
|
248
|
+
/**
|
|
249
|
+
* Constructor is internal. Application and ORM code must build instances
|
|
250
|
+
* through {@link ManyToManyRelatedManager.create}, which owns the wiring
|
|
251
|
+
* between relation metadata and the backing through-table mutator. The
|
|
252
|
+
* testing package exposes a fixture for unit tests that need a custom
|
|
253
|
+
* mutator.
|
|
254
|
+
*/
|
|
255
|
+
constructor(inputs) {
|
|
256
|
+
this.inputs = inputs;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Narrow an unknown value to {@link ManyToManyRelatedManager}.
|
|
260
|
+
*/
|
|
261
|
+
static isManyToManyRelatedManager(value) {
|
|
262
|
+
return typeof value === "object" && value !== null && value.__tangoBrand === ManyToManyRelatedManager.BRAND;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Build a {@link ManyToManyRelatedManager} bound to a single owner record.
|
|
266
|
+
* The factory derives the join-table descriptor from the relation edge and
|
|
267
|
+
* through-model fields, wires a {@link ThroughTableManager} against the
|
|
268
|
+
* supplied runtime-bound client, and returns a manager whose `add`,
|
|
269
|
+
* `remove`, and `all` methods enroll in any active
|
|
270
|
+
* `transaction.atomic(...)` boundary.
|
|
271
|
+
*/
|
|
272
|
+
static create(inputs) {
|
|
273
|
+
const throughTableManager = ThroughTableManager.fromRelation({
|
|
274
|
+
relation: inputs.relation,
|
|
275
|
+
throughModelFields: inputs.throughModelFields,
|
|
276
|
+
client: inputs.client,
|
|
277
|
+
mutationCompiler: inputs.mutationCompiler,
|
|
278
|
+
adapter: inputs.adapter,
|
|
279
|
+
sqlSafetyAdapter: inputs.sqlSafetyAdapter
|
|
280
|
+
});
|
|
281
|
+
return new ManyToManyRelatedManager({
|
|
282
|
+
ownerPrimaryKey: inputs.ownerPrimaryKey,
|
|
283
|
+
relationName: inputs.relationName,
|
|
284
|
+
ownerModelLabel: inputs.ownerModelLabel,
|
|
285
|
+
targetPrimaryKeyField: inputs.relation.targetPrimaryKey,
|
|
286
|
+
throughTableManager,
|
|
287
|
+
targetExecutorProvider: inputs.targetExecutorProvider,
|
|
288
|
+
runAtomic: inputs.runAtomic
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Insert join-table rows linking the owning record to the supplied
|
|
293
|
+
* targets. Duplicate links are ignored so repeated `add(...)` calls are
|
|
294
|
+
* idempotent. When multiple targets are supplied, Tango performs the
|
|
295
|
+
* membership write inside one `transaction.atomic(...)` boundary.
|
|
296
|
+
*/
|
|
297
|
+
async add(...targets) {
|
|
298
|
+
const targetPrimaryKeys = this.resolveTargetPrimaryKeys(targets);
|
|
299
|
+
if (targetPrimaryKeys.length === 0) return;
|
|
300
|
+
if (targetPrimaryKeys.length === 1) await this.inputs.throughTableManager.insertLink(this.inputs.ownerPrimaryKey, targetPrimaryKeys[0], { onDuplicate: InternalDuplicateInsertPolicy.IGNORE });
|
|
301
|
+
else await this.inputs.runAtomic(() => this.inputs.throughTableManager.insertLinks(this.inputs.ownerPrimaryKey, targetPrimaryKeys, { onDuplicate: InternalDuplicateInsertPolicy.IGNORE }));
|
|
302
|
+
this.invalidateCache();
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Delete join-table rows linking the owning record to the supplied
|
|
306
|
+
* targets. When multiple targets are supplied, Tango performs the
|
|
307
|
+
* membership write inside one `transaction.atomic(...)` boundary.
|
|
308
|
+
*/
|
|
309
|
+
async remove(...targets) {
|
|
310
|
+
const targetPrimaryKeys = this.resolveTargetPrimaryKeys(targets);
|
|
311
|
+
if (targetPrimaryKeys.length === 0) return;
|
|
312
|
+
if (targetPrimaryKeys.length === 1) await this.inputs.throughTableManager.deleteLink(this.inputs.ownerPrimaryKey, targetPrimaryKeys[0]);
|
|
313
|
+
else await this.inputs.runAtomic(() => this.inputs.throughTableManager.deleteLinks(this.inputs.ownerPrimaryKey, targetPrimaryKeys));
|
|
314
|
+
this.invalidateCache();
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Return a {@link QuerySet} for the related target rows of this many-to-many
|
|
318
|
+
* relation. When the relation was already loaded by `prefetchRelated(...)`,
|
|
319
|
+
* the first `fetch()` resolves with the cached materialization without
|
|
320
|
+
* re-querying. Mutating the membership through `add`/`remove` invalidates
|
|
321
|
+
* that cache.
|
|
322
|
+
*/
|
|
323
|
+
all() {
|
|
324
|
+
const executor = this.inputs.targetExecutorProvider();
|
|
325
|
+
if (!executor) throw new Error(`Cannot resolve a target query executor for relation '${this.inputs.relationName}' on '${this.inputs.ownerModelLabel}'.`);
|
|
326
|
+
return new ManyToManyRelatedQuerySet(executor, {
|
|
327
|
+
getCache: () => this.prefetchCache,
|
|
328
|
+
fetchTargetIds: () => this.inputs.throughTableManager.selectTargetIdsForOwner(this.inputs.ownerPrimaryKey),
|
|
329
|
+
targetPrimaryKeyField: this.inputs.targetPrimaryKeyField
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Replace the prefetch cache with the supplied target rows. Called by the
|
|
334
|
+
* many-to-many prefetch path so a follow-up `all()` resolves without an
|
|
335
|
+
* extra database round-trip.
|
|
336
|
+
*/
|
|
337
|
+
primePrefetchCache(targets) {
|
|
338
|
+
this.prefetchCache = [...targets];
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Drop any cached prefetch results. Mutating helpers call this so reads
|
|
342
|
+
* after an `add`/`remove` go back to the database.
|
|
343
|
+
*/
|
|
344
|
+
invalidateCache() {
|
|
345
|
+
this.prefetchCache = null;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Snapshot of the current prefetch cache, exposed for diagnostics and
|
|
349
|
+
* focused unit testing. Returns a fresh array copy so callers cannot
|
|
350
|
+
* mutate the manager's internal state.
|
|
351
|
+
*/
|
|
352
|
+
snapshotCache() {
|
|
353
|
+
return this.prefetchCache ? [...this.prefetchCache] : null;
|
|
354
|
+
}
|
|
355
|
+
resolveTargetPrimaryKeys(targets) {
|
|
356
|
+
const resolved = [];
|
|
357
|
+
const seen = new Set();
|
|
358
|
+
for (const target of targets) {
|
|
359
|
+
const primaryKey = this.resolveTargetPrimaryKey(target);
|
|
360
|
+
const canonical = this.canonicalizePrimaryKey(primaryKey);
|
|
361
|
+
if (seen.has(canonical)) continue;
|
|
362
|
+
seen.add(canonical);
|
|
363
|
+
resolved.push(primaryKey);
|
|
364
|
+
}
|
|
365
|
+
return resolved;
|
|
366
|
+
}
|
|
367
|
+
resolveTargetPrimaryKey(target) {
|
|
368
|
+
if (typeof target === "string" || typeof target === "number") return target;
|
|
369
|
+
if (typeof target === "object" && target !== null) {
|
|
370
|
+
const targetPrimaryKey = target[this.inputs.targetPrimaryKeyField];
|
|
371
|
+
if (targetPrimaryKey === undefined || targetPrimaryKey === null) throw new Error(`Cannot resolve target primary key '${this.inputs.targetPrimaryKeyField}' for relation '${this.inputs.relationName}' on '${this.inputs.ownerModelLabel}'.`);
|
|
372
|
+
return targetPrimaryKey;
|
|
373
|
+
}
|
|
374
|
+
throw new Error(`Unsupported target reference for relation '${this.inputs.relationName}' on '${this.inputs.ownerModelLabel}'. Expected a record, a primary-key carrier, or a primary-key value.`);
|
|
375
|
+
}
|
|
376
|
+
canonicalizePrimaryKey(primaryKey) {
|
|
377
|
+
switch (typeof primaryKey) {
|
|
378
|
+
case "string": return `string:${primaryKey}`;
|
|
379
|
+
case "number": return `number:${primaryKey}`;
|
|
380
|
+
case "bigint": return `bigint:${String(primaryKey)}`;
|
|
381
|
+
case "boolean": return `boolean:${primaryKey}`;
|
|
382
|
+
default: return `json:${JSON.stringify(primaryKey)}`;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
//#endregion
|
|
388
|
+
//#region src/manager/ModelManager.ts
|
|
389
|
+
const sqlSafetyAdapter = new OrmSqlSafetyAdapter();
|
|
390
|
+
var ModelManager = class ModelManager {
|
|
391
|
+
static BRAND = "tango.orm.model_manager";
|
|
392
|
+
__tangoBrand = ModelManager.BRAND;
|
|
393
|
+
queryExecutor;
|
|
394
|
+
mutationCompiler;
|
|
395
|
+
model;
|
|
396
|
+
client;
|
|
397
|
+
adapter;
|
|
398
|
+
runtime;
|
|
399
|
+
constructor(model, runtime) {
|
|
400
|
+
this.model = model;
|
|
401
|
+
this.runtime = runtime;
|
|
402
|
+
this.client = new RuntimeBoundClient(runtime);
|
|
403
|
+
this.adapter = runtime.getAdapter();
|
|
404
|
+
this.mutationCompiler = new MutationCompiler(this.adapter);
|
|
405
|
+
this.queryExecutor = {
|
|
406
|
+
get meta() {
|
|
407
|
+
return ModelManager.createTableMeta(model);
|
|
408
|
+
},
|
|
409
|
+
client: this.client,
|
|
410
|
+
adapter: this.adapter,
|
|
411
|
+
run: async (compiled) => {
|
|
412
|
+
const result = await this.client.query(compiled.sql, compiled.params);
|
|
413
|
+
return result.rows;
|
|
414
|
+
},
|
|
415
|
+
attachPersistedRecordAccessors: (record, modelKey) => {
|
|
416
|
+
this.attachManyToManyRelatedManagers(record, modelKey ?? this.model.metadata.key);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
get meta() {
|
|
421
|
+
return ModelManager.createTableMeta(this.model);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Narrow an unknown value to `ModelManager`.
|
|
425
|
+
*/
|
|
426
|
+
static isModelManager(value) {
|
|
427
|
+
return typeof value === "object" && value !== null && value.__tangoBrand === ModelManager.BRAND;
|
|
428
|
+
}
|
|
429
|
+
static createTableMeta(model) {
|
|
430
|
+
const rawMeta = TableMetaFactory.create(model);
|
|
431
|
+
const validatedMeta = sqlSafetyAdapter.validate({
|
|
432
|
+
kind: InternalSqlValidationPlanKind.INSERT,
|
|
433
|
+
meta: rawMeta,
|
|
434
|
+
writeKeys: Object.keys(rawMeta.columns)
|
|
435
|
+
}).meta;
|
|
436
|
+
if (rawMeta.relations) validatedMeta.relations = rawMeta.relations;
|
|
437
|
+
return validatedMeta;
|
|
438
|
+
}
|
|
439
|
+
static mergeCreatePayloadFromWhere(modelName, pkColumn, where, defaults) {
|
|
440
|
+
const hasDefaultsArg = defaults !== undefined;
|
|
441
|
+
const providedDefaults = defaults !== undefined ? { ...defaults } : undefined;
|
|
442
|
+
if (isQNodeLike(where)) {
|
|
443
|
+
if (!hasDefaultsArg || !providedDefaults || Object.keys(providedDefaults).length === 0) throw new Error(`Cannot create ${modelName} from Q filters without defaults.`);
|
|
444
|
+
const fromQ = ModelManager.collectPlainFieldsFromQNode(modelName, where);
|
|
445
|
+
const merged$1 = {
|
|
446
|
+
...fromQ,
|
|
447
|
+
...providedDefaults
|
|
448
|
+
};
|
|
449
|
+
const keys$1 = Object.keys(merged$1).filter((key) => merged$1[key] !== undefined);
|
|
450
|
+
const nonPkKeys$1 = keys$1.filter((key) => key !== pkColumn);
|
|
451
|
+
if (nonPkKeys$1.length === 0) throw new Error(`Cannot create ${modelName} without any values.`);
|
|
452
|
+
return merged$1;
|
|
453
|
+
}
|
|
454
|
+
const atom = where;
|
|
455
|
+
const entries = Object.entries(atom);
|
|
456
|
+
const plainEntries = entries.filter(([key]) => !String(key).includes("__"));
|
|
457
|
+
const lookupOnly = entries.length > 0 && plainEntries.length === 0;
|
|
458
|
+
const fromAtom = plainEntries.length > 0 ? Object.fromEntries(plainEntries) : undefined;
|
|
459
|
+
const mergeBase = {
|
|
460
|
+
...fromAtom,
|
|
461
|
+
...providedDefaults
|
|
462
|
+
};
|
|
463
|
+
if (lookupOnly && ModelManager.countDefinedValues(mergeBase, pkColumn) === 0) throw new Error(`Cannot create ${modelName} from lookup-only filters without defaults.`);
|
|
464
|
+
const cleanedAtom = plainEntries.length > 0 ? Object.fromEntries(plainEntries) : {};
|
|
465
|
+
const merged = {
|
|
466
|
+
...cleanedAtom,
|
|
467
|
+
...mergeBase
|
|
468
|
+
};
|
|
469
|
+
const keys = Object.keys(merged).filter((key) => merged[key] !== undefined);
|
|
470
|
+
const nonPkKeys = keys.filter((key) => key !== pkColumn);
|
|
471
|
+
if (nonPkKeys.length === 0) throw new Error(`Cannot create ${modelName} without any values.`);
|
|
472
|
+
return merged;
|
|
473
|
+
}
|
|
474
|
+
static countDefinedValues(payload, pkColumn) {
|
|
475
|
+
return Object.entries(payload).filter(([key, value]) => key !== pkColumn && value !== undefined).length;
|
|
476
|
+
}
|
|
477
|
+
static collectPlainFieldsFromQNode(modelName, node) {
|
|
478
|
+
switch (node.kind) {
|
|
479
|
+
case InternalQNodeType.ATOM: return ModelManager.omitLookupKeysFromAtom(node.where);
|
|
480
|
+
case InternalQNodeType.AND: {
|
|
481
|
+
const partials = (node.nodes ?? []).map((child) => ModelManager.collectPlainFieldsFromQNode(modelName, child));
|
|
482
|
+
return ModelManager.mergeCompatiblePartials(partials);
|
|
483
|
+
}
|
|
484
|
+
case InternalQNodeType.OR: {
|
|
485
|
+
const partials = (node.nodes ?? []).map((child) => ModelManager.collectPlainFieldsFromQNode(modelName, child));
|
|
486
|
+
const nonEmpty = partials.filter((partial) => Object.keys(partial).length > 0);
|
|
487
|
+
if (nonEmpty.length > 1) throw new Error(`Cannot derive a create payload from ${modelName} OR filters with multiple predicates. Supply defaults that fully describe the insert.`);
|
|
488
|
+
return nonEmpty.length === 1 ? nonEmpty[0] : {};
|
|
489
|
+
}
|
|
490
|
+
case InternalQNodeType.NOT: return {};
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
static omitLookupKeysFromAtom(atom) {
|
|
494
|
+
const entries = Object.entries(atom).filter(([key]) => !String(key).includes("__"));
|
|
495
|
+
return Object.fromEntries(entries);
|
|
496
|
+
}
|
|
497
|
+
static mergeCompatiblePartials(partials) {
|
|
498
|
+
const merged = {};
|
|
499
|
+
for (const partial of partials) for (const [key, value] of Object.entries(partial)) {
|
|
500
|
+
const existing = merged[key];
|
|
501
|
+
if (existing !== undefined && existing !== value) throw new Error(`Conflicting values for '${key}' while deriving create payload from Q filters.`);
|
|
502
|
+
merged[key] = value;
|
|
503
|
+
}
|
|
504
|
+
return merged;
|
|
505
|
+
}
|
|
506
|
+
query() {
|
|
507
|
+
return new ModelQuerySet(this.queryExecutor, {});
|
|
508
|
+
}
|
|
509
|
+
all() {
|
|
510
|
+
return this.query();
|
|
511
|
+
}
|
|
512
|
+
async findById(id) {
|
|
513
|
+
const filter = { [this.meta.pk]: id };
|
|
514
|
+
return this.query().filter(filter).fetchOne();
|
|
515
|
+
}
|
|
516
|
+
async getOrThrow(id) {
|
|
517
|
+
const result = await this.findById(id);
|
|
518
|
+
if (!result) throw new NotFoundError(`${this.model.metadata.name} with ${this.meta.pk}=${String(id)} not found`);
|
|
519
|
+
return result;
|
|
520
|
+
}
|
|
521
|
+
async getOrCreate(args) {
|
|
522
|
+
try {
|
|
523
|
+
const record$1 = await this.query().get(args.where);
|
|
524
|
+
return {
|
|
525
|
+
record: record$1,
|
|
526
|
+
created: false
|
|
527
|
+
};
|
|
528
|
+
} catch (error) {
|
|
529
|
+
if (!NotFoundError.isNotFoundError(error)) throw error;
|
|
530
|
+
}
|
|
531
|
+
const merged = ModelManager.mergeCreatePayloadFromWhere(this.model.metadata.name, this.meta.pk, args.where, args.defaults);
|
|
532
|
+
const record = await this.create(merged);
|
|
533
|
+
return {
|
|
534
|
+
record,
|
|
535
|
+
created: true
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
async updateOrCreate(args) {
|
|
539
|
+
let existing = null;
|
|
540
|
+
try {
|
|
541
|
+
existing = await this.query().get(args.where);
|
|
542
|
+
} catch (error) {
|
|
543
|
+
if (!NotFoundError.isNotFoundError(error)) throw error;
|
|
544
|
+
}
|
|
545
|
+
if (!existing) {
|
|
546
|
+
const merged = ModelManager.mergeCreatePayloadFromWhere(this.model.metadata.name, this.meta.pk, args.where, args.defaults);
|
|
547
|
+
const record$1 = await this.create(merged);
|
|
548
|
+
return {
|
|
549
|
+
record: record$1,
|
|
550
|
+
created: true,
|
|
551
|
+
updated: false
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
const patch = args.update ?? args.defaults ?? {};
|
|
555
|
+
if (Object.keys(patch).length === 0) return {
|
|
556
|
+
record: existing,
|
|
557
|
+
created: false,
|
|
558
|
+
updated: false
|
|
559
|
+
};
|
|
560
|
+
const id = existing[this.meta.pk];
|
|
561
|
+
const record = await this.update(id, patch);
|
|
562
|
+
return {
|
|
563
|
+
record,
|
|
564
|
+
created: false,
|
|
565
|
+
updated: true
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
async create(input) {
|
|
569
|
+
const prepared = await this.runBeforeCreate(input);
|
|
570
|
+
const preparedKeys = Object.keys(prepared);
|
|
571
|
+
if (preparedKeys.length === 0) throw new Error(`Cannot create ${this.model.metadata.name} without any values.`);
|
|
572
|
+
const validatedPlan = sqlSafetyAdapter.validate({
|
|
573
|
+
kind: InternalSqlValidationPlanKind.INSERT,
|
|
574
|
+
meta: this.meta,
|
|
575
|
+
writeKeys: preparedKeys
|
|
576
|
+
});
|
|
577
|
+
const compiled = this.mutationCompiler.compileInsert(validatedPlan, preparedKeys.map((key) => prepared[key]));
|
|
578
|
+
const result = await this.queryExecutor.client.query(compiled.sql, compiled.params);
|
|
579
|
+
const created = result.rows[0];
|
|
580
|
+
this.attachOwnRelatedManagers(created);
|
|
581
|
+
await this.model.hooks?.afterCreate?.({
|
|
582
|
+
record: created,
|
|
583
|
+
model: this.model,
|
|
584
|
+
manager: this,
|
|
585
|
+
transaction: this.getHookTransaction()
|
|
586
|
+
});
|
|
587
|
+
return created;
|
|
588
|
+
}
|
|
589
|
+
async update(id, patch) {
|
|
590
|
+
const current = await this.getOrThrow(id);
|
|
591
|
+
const prepared = await this.runBeforeUpdate(id, patch, current);
|
|
592
|
+
const preparedKeys = Object.keys(prepared);
|
|
593
|
+
if (preparedKeys.length === 0) throw new Error(`Cannot update ${this.model.metadata.name} without any values.`);
|
|
594
|
+
const validatedPlan = sqlSafetyAdapter.validate({
|
|
595
|
+
kind: InternalSqlValidationPlanKind.UPDATE,
|
|
596
|
+
meta: this.meta,
|
|
597
|
+
writeKeys: preparedKeys
|
|
598
|
+
});
|
|
599
|
+
const compiled = this.mutationCompiler.compileUpdate(validatedPlan, preparedKeys.map((key) => prepared[key]), id);
|
|
600
|
+
const result = await this.queryExecutor.client.query(compiled.sql, compiled.params);
|
|
601
|
+
const updated = result.rows[0];
|
|
602
|
+
this.attachOwnRelatedManagers(updated);
|
|
603
|
+
await this.model.hooks?.afterUpdate?.({
|
|
604
|
+
id,
|
|
605
|
+
patch: prepared,
|
|
606
|
+
previous: current,
|
|
607
|
+
record: updated,
|
|
608
|
+
model: this.model,
|
|
609
|
+
manager: this,
|
|
610
|
+
transaction: this.getHookTransaction()
|
|
611
|
+
});
|
|
612
|
+
return updated;
|
|
613
|
+
}
|
|
614
|
+
async delete(id) {
|
|
615
|
+
const current = await this.getOrThrow(id);
|
|
616
|
+
this.attachOwnRelatedManagers(current);
|
|
617
|
+
await this.model.hooks?.beforeDelete?.({
|
|
618
|
+
id,
|
|
619
|
+
current,
|
|
620
|
+
model: this.model,
|
|
621
|
+
manager: this,
|
|
622
|
+
transaction: this.getHookTransaction()
|
|
623
|
+
});
|
|
624
|
+
const validatedPlan = sqlSafetyAdapter.validate({
|
|
625
|
+
kind: InternalSqlValidationPlanKind.DELETE,
|
|
626
|
+
meta: this.meta
|
|
627
|
+
});
|
|
628
|
+
const compiled = this.mutationCompiler.compileDelete(validatedPlan, id);
|
|
629
|
+
await this.queryExecutor.client.query(compiled.sql, compiled.params);
|
|
630
|
+
await this.model.hooks?.afterDelete?.({
|
|
631
|
+
id,
|
|
632
|
+
previous: current,
|
|
633
|
+
model: this.model,
|
|
634
|
+
manager: this,
|
|
635
|
+
transaction: this.getHookTransaction()
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Build a {@link ManyToManyRelatedManager} bound to a single owner record
|
|
640
|
+
* for the supplied many-to-many relation. The returned manager performs
|
|
641
|
+
* its INSERT/DELETE writes through the shared runtime-bound client, so
|
|
642
|
+
* mutations enroll in any active `transaction.atomic(...)` boundary.
|
|
643
|
+
*/
|
|
644
|
+
createManyToManyRelatedManager(relationName, ownerPrimaryKey) {
|
|
645
|
+
const relation = this.requireManyToManyEdge(relationName);
|
|
646
|
+
const registry = ModelRegistry.getOwner(this.model);
|
|
647
|
+
const throughModel = registry.getByKey(relation.throughModelKey);
|
|
648
|
+
return ManyToManyRelatedManager.create({
|
|
649
|
+
ownerPrimaryKey,
|
|
650
|
+
relationName,
|
|
651
|
+
ownerModelLabel: this.model.metadata.name,
|
|
652
|
+
relation,
|
|
653
|
+
throughModelFields: throughModel.metadata.fields,
|
|
654
|
+
client: this.client,
|
|
655
|
+
mutationCompiler: this.mutationCompiler,
|
|
656
|
+
adapter: this.adapter,
|
|
657
|
+
sqlSafetyAdapter,
|
|
658
|
+
targetExecutorProvider: () => this.resolveTargetExecutor(relation.targetModelKey),
|
|
659
|
+
runAtomic: (work) => TransactionEngine.forRuntime(this.runtime).atomic(() => work())
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
async bulkCreate(inputs) {
|
|
663
|
+
if (inputs.length === 0) return [];
|
|
664
|
+
const perRowPrepared = await Promise.all(inputs.map((input) => this.runBeforeCreate(input)));
|
|
665
|
+
const batchPrepared = await this.model.hooks?.beforeBulkCreate?.({
|
|
666
|
+
rows: perRowPrepared,
|
|
667
|
+
model: this.model,
|
|
668
|
+
manager: this,
|
|
669
|
+
transaction: this.getHookTransaction()
|
|
670
|
+
}) ?? perRowPrepared;
|
|
671
|
+
const preparedKeys = Object.keys(batchPrepared[0] ?? {});
|
|
672
|
+
if (preparedKeys.length === 0) throw new Error(`Cannot create ${this.model.metadata.name} without any values.`);
|
|
673
|
+
const validatedPlan = sqlSafetyAdapter.validate({
|
|
674
|
+
kind: InternalSqlValidationPlanKind.INSERT,
|
|
675
|
+
meta: this.meta,
|
|
676
|
+
writeKeys: preparedKeys
|
|
677
|
+
});
|
|
678
|
+
const valueRows = batchPrepared.map((input) => preparedKeys.map((key) => input[key]));
|
|
679
|
+
const compiled = this.mutationCompiler.compileBulkInsert(validatedPlan, valueRows);
|
|
680
|
+
const result = await this.queryExecutor.client.query(compiled.sql, compiled.params);
|
|
681
|
+
for (const record of result.rows) this.attachOwnRelatedManagers(record);
|
|
682
|
+
await Promise.all(result.rows.map((record) => this.model.hooks?.afterCreate?.({
|
|
683
|
+
record,
|
|
684
|
+
model: this.model,
|
|
685
|
+
manager: this,
|
|
686
|
+
transaction: this.getHookTransaction()
|
|
687
|
+
})));
|
|
688
|
+
await this.model.hooks?.afterBulkCreate?.({
|
|
689
|
+
records: result.rows,
|
|
690
|
+
model: this.model,
|
|
691
|
+
manager: this,
|
|
692
|
+
transaction: this.getHookTransaction()
|
|
693
|
+
});
|
|
694
|
+
return result.rows;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Attach a {@link ManyToManyRelatedManager} as a non-enumerable property
|
|
698
|
+
* for every persisted many-to-many relation declared on the supplied
|
|
699
|
+
* record's model. Existing properties are left untouched so that prior
|
|
700
|
+
* hydration writes (such as prefetched arrays) survive the attach pass
|
|
701
|
+
* during the incremental rollout of related-manager hydration.
|
|
702
|
+
*/
|
|
703
|
+
attachOwnRelatedManagers(record) {
|
|
704
|
+
this.attachManyToManyRelatedManagers(record, this.model.metadata.key);
|
|
705
|
+
}
|
|
706
|
+
attachManyToManyRelatedManagers(record, modelKey) {
|
|
707
|
+
if (!modelKey) return;
|
|
708
|
+
const targetManager = this.resolveManagerForModelKey(modelKey);
|
|
709
|
+
if (!targetManager) return;
|
|
710
|
+
const meta = targetManager.meta;
|
|
711
|
+
const relations = meta.relations;
|
|
712
|
+
if (!relations) return;
|
|
713
|
+
const ownerPrimaryKey = record[meta.pk];
|
|
714
|
+
if (ownerPrimaryKey === undefined || ownerPrimaryKey === null) return;
|
|
715
|
+
for (const [relationName, relation] of Object.entries(relations)) {
|
|
716
|
+
if (relation.kind !== InternalRelationKind.MANY_TO_MANY) continue;
|
|
717
|
+
if (Object.prototype.hasOwnProperty.call(record, relationName)) continue;
|
|
718
|
+
const relatedManager = targetManager.createManyToManyRelatedManager(relationName, ownerPrimaryKey);
|
|
719
|
+
Object.defineProperty(record, relationName, {
|
|
720
|
+
value: relatedManager,
|
|
721
|
+
writable: true,
|
|
722
|
+
configurable: true,
|
|
723
|
+
enumerable: false
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
resolveManagerForModelKey(modelKey) {
|
|
728
|
+
if (modelKey === this.model.metadata.key) return this;
|
|
729
|
+
const registry = ModelRegistry.getOwner(this.model);
|
|
730
|
+
const otherModel = registry.getByKey(modelKey);
|
|
731
|
+
if (!otherModel) return null;
|
|
732
|
+
const candidate = otherModel.objects;
|
|
733
|
+
return ModelManager.isModelManager(candidate) ? candidate : null;
|
|
734
|
+
}
|
|
735
|
+
resolveTargetExecutor(targetModelKey) {
|
|
736
|
+
const targetManager = this.resolveManagerForModelKey(targetModelKey);
|
|
737
|
+
if (!targetManager) return null;
|
|
738
|
+
return targetManager.queryExecutor;
|
|
739
|
+
}
|
|
740
|
+
requireManyToManyEdge(relationName) {
|
|
741
|
+
const rel = this.meta.relations?.[relationName];
|
|
742
|
+
if (!rel || rel.kind !== InternalRelationKind.MANY_TO_MANY || !rel.throughTable || !rel.throughSourceKey || !rel.throughTargetKey || !rel.throughModelKey) throw new Error(`Relation '${relationName}' on '${this.model.metadata.name}' is not a persisted many-to-many edge.`);
|
|
743
|
+
return rel;
|
|
744
|
+
}
|
|
745
|
+
async runBeforeCreate(data) {
|
|
746
|
+
return await this.model.hooks?.beforeCreate?.({
|
|
747
|
+
data,
|
|
748
|
+
model: this.model,
|
|
749
|
+
manager: this,
|
|
750
|
+
transaction: this.getHookTransaction()
|
|
751
|
+
}) ?? data;
|
|
752
|
+
}
|
|
753
|
+
async runBeforeUpdate(id, patch, current) {
|
|
754
|
+
return await this.model.hooks?.beforeUpdate?.({
|
|
755
|
+
id,
|
|
756
|
+
patch,
|
|
757
|
+
current,
|
|
758
|
+
model: this.model,
|
|
759
|
+
manager: this,
|
|
760
|
+
transaction: this.getHookTransaction()
|
|
761
|
+
}) ?? patch;
|
|
762
|
+
}
|
|
763
|
+
getHookTransaction() {
|
|
764
|
+
return TransactionEngine.forRuntime(this.runtime).getActiveTransaction();
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
//#endregion
|
|
769
|
+
//#region src/manager/registerModelObjects.ts
|
|
770
|
+
const managerCache = new WeakMap();
|
|
771
|
+
let hasRegisteredModelObjects = false;
|
|
772
|
+
function defineObjectsProperty(model) {
|
|
773
|
+
Object.defineProperty(model, "objects", {
|
|
774
|
+
configurable: true,
|
|
775
|
+
enumerable: true,
|
|
776
|
+
get() {
|
|
777
|
+
const runtime = getTangoRuntime();
|
|
778
|
+
const cached = managerCache.get(model);
|
|
779
|
+
if (cached && cached.runtime === runtime) return cached.manager;
|
|
780
|
+
const manager = new ModelManager(model, runtime);
|
|
781
|
+
managerCache.set(model, {
|
|
782
|
+
runtime,
|
|
783
|
+
manager
|
|
784
|
+
});
|
|
785
|
+
return manager;
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
function registerModelObjects() {
|
|
790
|
+
if (hasRegisteredModelObjects) return;
|
|
791
|
+
registerModelAugmentor(defineObjectsProperty);
|
|
792
|
+
hasRegisteredModelObjects = true;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
//#endregion
|
|
796
|
+
export { ManyToManyRelatedManager, ManyToManyRelatedQuerySet, ModelManager, registerModelObjects };
|
|
797
|
+
//# sourceMappingURL=registerModelObjects-DxlBfuUN.js.map
|