@prisma-idb/client-idb 0.11.0 → 0.13.0
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 +6 -0
- package/dist/client-auto.d.mts +2 -2
- package/dist/client-auto.d.mts.map +1 -1
- package/dist/client-auto.mjs +44 -3
- package/dist/client-auto.mjs.map +1 -1
- package/dist/client.d.mts +3 -2
- package/dist/client.mjs +3 -2
- package/dist/decimal-identity-D_1J9lPN.d.mts +21 -0
- package/dist/decimal-identity-D_1J9lPN.d.mts.map +1 -0
- package/dist/internal.d.mts +5 -4
- package/dist/internal.d.mts.map +1 -1
- package/dist/internal.mjs +2 -2
- package/dist/{managed-client-CxlNmERd.mjs → managed-client-DMk8ERfX.mjs} +12 -5
- package/dist/managed-client-DMk8ERfX.mjs.map +1 -0
- package/dist/{managed-client-CS78EjPu.d.mts → managed-client-eksFK4UF.d.mts} +32 -3
- package/dist/managed-client-eksFK4UF.d.mts.map +1 -0
- package/dist/mutation-executor-IuJmyWEG.mjs +2132 -0
- package/dist/mutation-executor-IuJmyWEG.mjs.map +1 -0
- package/dist/{executor-Bp-hdRg2.d.mts → mutation-scope-CE3GMR4X.d.mts} +79 -28
- package/dist/mutation-scope-CE3GMR4X.d.mts.map +1 -0
- package/dist/{idb-orm-BitJ-EMq.mjs → orm-transaction-BGx1eukw.mjs} +366 -558
- package/dist/orm-transaction-BGx1eukw.mjs.map +1 -0
- package/dist/{idb-orm-9XN3c9ly.d.mts → orm-transaction-B_ZJ92uY.d.mts} +40 -7
- package/dist/orm-transaction-B_ZJ92uY.d.mts.map +1 -0
- package/dist/orm.d.mts +13 -4
- package/dist/orm.d.mts.map +1 -1
- package/dist/orm.mjs +3 -3
- package/package.json +9 -9
- package/dist/executor-Bp-hdRg2.d.mts.map +0 -1
- package/dist/idb-orm-9XN3c9ly.d.mts.map +0 -1
- package/dist/idb-orm-BitJ-EMq.mjs.map +0 -1
- package/dist/managed-client-CS78EjPu.d.mts.map +0 -1
- package/dist/managed-client-CxlNmERd.mjs.map +0 -1
- package/dist/mutation-executor-SJqTuSbb.mjs +0 -1196
- package/dist/mutation-executor-SJqTuSbb.mjs.map +0 -1
- package/dist/mutation-scope-1hS0ogGa.d.mts +0 -33
- package/dist/mutation-scope-1hS0ogGa.d.mts.map +0 -1
|
@@ -1,1196 +0,0 @@
|
|
|
1
|
-
import { evaluateFilter, shorthandToFilterExpr } from "@prisma-idb/adapter-idb/runtime";
|
|
2
|
-
import { domainModelsAtDefaultNamespace } from "@prisma/orm-framework/contract/types";
|
|
3
|
-
import { fieldValueToken, fieldValuesEqual, keyEquals as keyEquals$1, keyToken } from "@prisma-idb/target-idb/runtime";
|
|
4
|
-
import { generateId } from "@prisma/orm-framework/ids/runtime";
|
|
5
|
-
import { keyPathFields } from "@prisma-idb/target-idb/pack";
|
|
6
|
-
//#region src/core/types.ts
|
|
7
|
-
/**
|
|
8
|
-
* Extract the `storeName` from a model's storage metadata at runtime.
|
|
9
|
-
* Falls back to the model name if `storeName` is absent.
|
|
10
|
-
*/
|
|
11
|
-
function getStoreName(contract, modelName) {
|
|
12
|
-
return (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.storeName ?? modelName;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Extract the `keyPath` from a model's storage metadata at runtime.
|
|
16
|
-
* A single field name for the common case; an ordered array of field names
|
|
17
|
-
* for a compound primary key.
|
|
18
|
-
*
|
|
19
|
-
* @throws if the model is unknown or its storage has no `keyPath` — a
|
|
20
|
-
* validated contract always has one, so this only fires on a malformed or
|
|
21
|
-
* hand-built contract, where guessing a key name would silently read/write
|
|
22
|
-
* the wrong key.
|
|
23
|
-
*/
|
|
24
|
-
function getKeyPath(contract, modelName) {
|
|
25
|
-
const keyPath = (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.keyPath;
|
|
26
|
-
if (keyPath === void 0) throw new Error(`Model "${modelName}" has no storage.keyPath in the contract.`);
|
|
27
|
-
return keyPath;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Extracts a row's primary key as an {@link IDBValidKey} — the scalar value
|
|
31
|
-
* at `row[keyPath]` for a single-field key, or an ordered array of the
|
|
32
|
-
* compound key's member field values (matching the array form IDB itself
|
|
33
|
-
* expects for a compound `keyPath`). The one construction site every
|
|
34
|
-
* key-get/put/delete/update call should route through, so compound-key field
|
|
35
|
-
* order stays consistent everywhere.
|
|
36
|
-
*/
|
|
37
|
-
function extractKeyFromRow(row, keyPath) {
|
|
38
|
-
if (typeof keyPath === "string") return row[keyPath];
|
|
39
|
-
return keyPath.map((field) => row[field]);
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Resolve a model's named relation to a {@link ContractReferenceRelation} at
|
|
43
|
-
* runtime, or `undefined` when the relation is absent or an embedded relation
|
|
44
|
-
* (no `on` join block). Used by `include()` to find the related model name and
|
|
45
|
-
* cardinality before building the child-accessor refinement.
|
|
46
|
-
*/
|
|
47
|
-
function getRelation(contract, modelName, relName) {
|
|
48
|
-
const relation = domainModelsAtDefaultNamespace(contract.domain)[modelName]?.relations?.[relName];
|
|
49
|
-
if (relation === void 0 || !("on" in relation)) return void 0;
|
|
50
|
-
return relation;
|
|
51
|
-
}
|
|
52
|
-
//#endregion
|
|
53
|
-
//#region src/core/enum-validation.ts
|
|
54
|
-
const rulesByContract = /* @__PURE__ */ new WeakMap();
|
|
55
|
-
function resolveEnumRule(contract, field) {
|
|
56
|
-
const ref = field.valueSet;
|
|
57
|
-
if (ref === void 0 || ref.plane !== "domain" || ref.entityKind !== "enum") return void 0;
|
|
58
|
-
const contractEnum = contract.domain.namespaces[ref.namespaceId]?.enum?.[ref.entityName];
|
|
59
|
-
if (contractEnum === void 0) throw new Error(`Enum "${ref.entityName}" referenced by the contract is not declared in its domain.`);
|
|
60
|
-
return {
|
|
61
|
-
enumName: ref.entityName,
|
|
62
|
-
values: new Set(contractEnum.members.map((member) => member.value)),
|
|
63
|
-
nullable: field.nullable,
|
|
64
|
-
many: field.many === true
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
function getModelEnumRules(contract, modelName) {
|
|
68
|
-
let byModel = rulesByContract.get(contract);
|
|
69
|
-
if (byModel === void 0) {
|
|
70
|
-
byModel = /* @__PURE__ */ new Map();
|
|
71
|
-
rulesByContract.set(contract, byModel);
|
|
72
|
-
}
|
|
73
|
-
const cached = byModel.get(modelName);
|
|
74
|
-
if (cached !== void 0) return cached;
|
|
75
|
-
const rules = /* @__PURE__ */ new Map();
|
|
76
|
-
const fields = domainModelsAtDefaultNamespace(contract.domain)[modelName]?.fields ?? {};
|
|
77
|
-
for (const [fieldName, field] of Object.entries(fields)) {
|
|
78
|
-
const rule = resolveEnumRule(contract, field);
|
|
79
|
-
if (rule !== void 0) rules.set(fieldName, rule);
|
|
80
|
-
}
|
|
81
|
-
byModel.set(modelName, rules);
|
|
82
|
-
return rules;
|
|
83
|
-
}
|
|
84
|
-
function describe(value) {
|
|
85
|
-
return typeof value === "string" ? JSON.stringify(value) : `${String(value)} (${typeof value})`;
|
|
86
|
-
}
|
|
87
|
-
function invalidValueError(modelName, fieldName, rule, value) {
|
|
88
|
-
const expected = [...rule.values].map((v) => JSON.stringify(v)).join(", ");
|
|
89
|
-
return /* @__PURE__ */ new Error(`Invalid value ${describe(value)} for enum field "${modelName}.${fieldName}". Expected one of enum "${rule.enumName}": ${expected}.`);
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Throws if `data` sets an enum field to a value its enum doesn't declare.
|
|
93
|
-
* Fields left `undefined` are not checked. `null` is accepted only on an
|
|
94
|
-
* optional (`Role?`) field; a list (`Role[]`) field must be an array whose
|
|
95
|
-
* every element is a declared value.
|
|
96
|
-
*/
|
|
97
|
-
function assertEnumValues(contract, modelName, data) {
|
|
98
|
-
const rules = getModelEnumRules(contract, modelName);
|
|
99
|
-
if (rules.size === 0) return;
|
|
100
|
-
for (const [fieldName, rule] of rules) {
|
|
101
|
-
const value = data[fieldName];
|
|
102
|
-
if (value === void 0) continue;
|
|
103
|
-
if (value === null) {
|
|
104
|
-
if (rule.nullable) continue;
|
|
105
|
-
throw new Error(`Enum field "${modelName}.${fieldName}" is required and cannot be null.`);
|
|
106
|
-
}
|
|
107
|
-
if (rule.many) {
|
|
108
|
-
if (!Array.isArray(value)) throw new Error(`Enum list field "${modelName}.${fieldName}" expects an array of "${rule.enumName}" values, got ${describe(value)}.`);
|
|
109
|
-
for (const element of value) if (!rule.values.has(element)) throw invalidValueError(modelName, fieldName, rule, element);
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
if (!rule.values.has(value)) throw invalidValueError(modelName, fieldName, rule, value);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
//#endregion
|
|
116
|
-
//#region src/core/mutation-defaults.ts
|
|
117
|
-
function createMutationDefaultsCache() {
|
|
118
|
-
return /* @__PURE__ */ new Map();
|
|
119
|
-
}
|
|
120
|
-
/** Generator ids whose value is shared across an entire top-level call. Every other id always regenerates. */
|
|
121
|
-
const CACHEABLE_GENERATOR_IDS = /* @__PURE__ */ new Set(["timestampNow"]);
|
|
122
|
-
function computeGeneratedValue(spec) {
|
|
123
|
-
const id = spec.id;
|
|
124
|
-
switch (id) {
|
|
125
|
-
case "timestampNow": return /* @__PURE__ */ new Date();
|
|
126
|
-
case "literal": return spec.params?.["value"];
|
|
127
|
-
case "uuidv4":
|
|
128
|
-
case "uuidv7":
|
|
129
|
-
case "cuid2": return generateId(spec.params !== void 0 ? {
|
|
130
|
-
id,
|
|
131
|
-
params: spec.params
|
|
132
|
-
} : { id });
|
|
133
|
-
default: throw new Error(`Unknown mutation-default generator id "${String(id)}"`);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
function resolveGeneratedValue(spec, cache) {
|
|
137
|
-
const id = spec.id;
|
|
138
|
-
if (!CACHEABLE_GENERATOR_IDS.has(id)) return computeGeneratedValue(spec);
|
|
139
|
-
const cached = cache.get(id);
|
|
140
|
-
if (cached !== void 0) return cached;
|
|
141
|
-
const value = computeGeneratedValue(spec);
|
|
142
|
-
cache.set(id, value);
|
|
143
|
-
return value;
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Fills in every column with an `onCreate` default that `data` doesn't
|
|
147
|
-
* already set. Returns `data` unchanged (same reference) if there's nothing
|
|
148
|
-
* to apply.
|
|
149
|
-
*/
|
|
150
|
-
function applyCreateDefaults(defaults, storeName, data, cache) {
|
|
151
|
-
if (!defaults || defaults.length === 0) return data;
|
|
152
|
-
let result = data;
|
|
153
|
-
for (const d of defaults) {
|
|
154
|
-
if (d.ref.table !== storeName || !d.onCreate) continue;
|
|
155
|
-
if (result[d.ref.column] !== void 0) continue;
|
|
156
|
-
if (result === data) result = { ...data };
|
|
157
|
-
result[d.ref.column] = resolveGeneratedValue(d.onCreate, cache);
|
|
158
|
-
}
|
|
159
|
-
return result;
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Fills in every column with an `onUpdate` default that `patch` doesn't
|
|
163
|
-
* already set. A patch with no defined values (empty, or every field
|
|
164
|
-
* explicitly `undefined`) is returned unchanged — no write means no default
|
|
165
|
-
* should fire either (matches SQL's "empty update payloads skip onUpdate
|
|
166
|
-
* defaults" rule).
|
|
167
|
-
*/
|
|
168
|
-
function applyUpdateDefaults(defaults, storeName, patch, cache) {
|
|
169
|
-
if (Object.values(patch).every((v) => v === void 0)) return patch;
|
|
170
|
-
if (!defaults || defaults.length === 0) return patch;
|
|
171
|
-
let result = patch;
|
|
172
|
-
for (const d of defaults) {
|
|
173
|
-
if (d.ref.table !== storeName || !d.onUpdate) continue;
|
|
174
|
-
if (result[d.ref.column] !== void 0) continue;
|
|
175
|
-
if (result === patch) result = { ...patch };
|
|
176
|
-
result[d.ref.column] = resolveGeneratedValue(d.onUpdate, cache);
|
|
177
|
-
}
|
|
178
|
-
return result;
|
|
179
|
-
}
|
|
180
|
-
//#endregion
|
|
181
|
-
//#region src/core/mutation-scope.ts
|
|
182
|
-
/**
|
|
183
|
-
* Run a callback inside a single multi-store IDB readwrite transaction.
|
|
184
|
-
*
|
|
185
|
-
* Opens the transaction, passes the `IdbTransactionScope` to `run`, then:
|
|
186
|
-
* - On success: awaits `scope.commit()` (waits for `tx.oncomplete`).
|
|
187
|
-
* - On error: calls `scope.rollback()` and rethrows.
|
|
188
|
-
*
|
|
189
|
-
* The callback receives the low-level scope, not the ORM accessor.
|
|
190
|
-
* Use this for multi-store atomic writes that span several object stores.
|
|
191
|
-
*/
|
|
192
|
-
async function withMutationScope(executor, storeNames, run) {
|
|
193
|
-
const tx = await executor.transaction(storeNames, "readwrite");
|
|
194
|
-
try {
|
|
195
|
-
const result = await run(tx);
|
|
196
|
-
await tx.commit();
|
|
197
|
-
return result;
|
|
198
|
-
} catch (err) {
|
|
199
|
-
tx.rollback();
|
|
200
|
-
throw err;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
//#endregion
|
|
204
|
-
//#region src/core/relation-mutator.ts
|
|
205
|
-
function createRelationMutator() {
|
|
206
|
-
return {
|
|
207
|
-
create(data) {
|
|
208
|
-
return {
|
|
209
|
-
kind: "create",
|
|
210
|
-
data: Array.isArray(data) ? [...data] : [data]
|
|
211
|
-
};
|
|
212
|
-
},
|
|
213
|
-
connect(criteria) {
|
|
214
|
-
return {
|
|
215
|
-
kind: "connect",
|
|
216
|
-
criteria: Array.isArray(criteria) ? [...criteria] : [criteria]
|
|
217
|
-
};
|
|
218
|
-
},
|
|
219
|
-
disconnect(criteria) {
|
|
220
|
-
if (!criteria) return { kind: "disconnect" };
|
|
221
|
-
return {
|
|
222
|
-
kind: "disconnect",
|
|
223
|
-
criteria: [...criteria]
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
function isRelationMutationDescriptor(value) {
|
|
229
|
-
if (!value || typeof value !== "object") return false;
|
|
230
|
-
const candidate = value;
|
|
231
|
-
return candidate.kind === "create" || candidate.kind === "connect" || candidate.kind === "disconnect";
|
|
232
|
-
}
|
|
233
|
-
function isRelationMutationCallback(value) {
|
|
234
|
-
return typeof value === "function";
|
|
235
|
-
}
|
|
236
|
-
//#endregion
|
|
237
|
-
//#region src/core/mutation-executor.ts
|
|
238
|
-
function makePlanMeta(contract) {
|
|
239
|
-
return {
|
|
240
|
-
target: "idb",
|
|
241
|
-
storageHash: contract.storage.storageHash,
|
|
242
|
-
lane: "idb-mutation-executor",
|
|
243
|
-
annotations: { groupingKey: "nested" }
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
const relationDefsCache = /* @__PURE__ */ new WeakMap();
|
|
247
|
-
function getRelationDefinitions(contract, modelName) {
|
|
248
|
-
let perContract = relationDefsCache.get(contract);
|
|
249
|
-
if (!perContract) {
|
|
250
|
-
perContract = /* @__PURE__ */ new Map();
|
|
251
|
-
relationDefsCache.set(contract, perContract);
|
|
252
|
-
}
|
|
253
|
-
const cached = perContract.get(modelName);
|
|
254
|
-
if (cached) return cached;
|
|
255
|
-
const model = domainModelsAtDefaultNamespace(contract.domain)[modelName];
|
|
256
|
-
if (!model) {
|
|
257
|
-
perContract.set(modelName, []);
|
|
258
|
-
return [];
|
|
259
|
-
}
|
|
260
|
-
const defs = [];
|
|
261
|
-
for (const [relationName, rawRelation] of Object.entries(model.relations)) {
|
|
262
|
-
if (!rawRelation || typeof rawRelation !== "object" || !("on" in rawRelation)) continue;
|
|
263
|
-
const relation = rawRelation;
|
|
264
|
-
const relatedModelName = relation.to.model;
|
|
265
|
-
const relatedStoreName = getStoreName(contract, relatedModelName);
|
|
266
|
-
defs.push({
|
|
267
|
-
relationName,
|
|
268
|
-
relatedModelName,
|
|
269
|
-
relatedStoreName,
|
|
270
|
-
cardinality: relation.cardinality,
|
|
271
|
-
localFields: relation.on.localFields,
|
|
272
|
-
targetFields: relation.on.targetFields
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
perContract.set(modelName, defs);
|
|
276
|
-
return defs;
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* Returns true if `data` contains at least one field that is both a known
|
|
280
|
-
* relation name for `modelName` and a function (a mutation callback).
|
|
281
|
-
*/
|
|
282
|
-
function hasNestedMutationCallbacks(contract, modelName, data) {
|
|
283
|
-
const relationNames = new Set(getRelationDefinitions(contract, modelName).map((r) => r.relationName));
|
|
284
|
-
for (const [fieldName, value] of Object.entries(data)) if (relationNames.has(fieldName) && isRelationMutationCallback(value)) return true;
|
|
285
|
-
return false;
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Guards that the executor supports multi-store transactions.
|
|
289
|
-
* Throws a clear error if `transaction()` is not available — the user must
|
|
290
|
-
* use IdbRuntime (createIdbRuntime / createAutoMigratingIdbClient) rather than
|
|
291
|
-
* a plain IdbQueryExecutor stub.
|
|
292
|
-
*/
|
|
293
|
-
function requireTransactionExecutor(executor) {
|
|
294
|
-
if (typeof executor.transaction !== "function") throw new Error("This operation requires an executor with transaction support (nested relation writes, FK-validated create/update, and referential-action delete all need it). Use IdbRuntime (createIdbRuntime or createAutoMigratingIdbClient) instead of a plain IdbQueryExecutor.");
|
|
295
|
-
return executor;
|
|
296
|
-
}
|
|
297
|
-
async function executeNestedCreateMutation(options) {
|
|
298
|
-
const { executor, contract, modelName, data } = options;
|
|
299
|
-
const record = data;
|
|
300
|
-
const storeNames = collectStoreNames(contract, modelName, record, "create");
|
|
301
|
-
const defaultsCache = createMutationDefaultsCache();
|
|
302
|
-
return withMutationScope(executor, storeNames, (scope) => createGraph(scope, contract, modelName, record, defaultsCache));
|
|
303
|
-
}
|
|
304
|
-
async function executeNestedUpdateMutation(options) {
|
|
305
|
-
const { executor, contract, modelName, filters, data } = options;
|
|
306
|
-
const record = data;
|
|
307
|
-
const storeNames = collectStoreNames(contract, modelName, record, "update");
|
|
308
|
-
const defaultsCache = createMutationDefaultsCache();
|
|
309
|
-
return withMutationScope(executor, storeNames, (scope) => updateFirstGraph(scope, contract, modelName, filters, record, defaultsCache));
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Every store a nested write may touch: the model's own store, each related
|
|
313
|
-
* store a relation callback writes to, and the parent stores that foreign-key
|
|
314
|
-
* checks read, for the model and for each related model. An update also
|
|
315
|
-
* declares the stores its `onUpdate` referential actions may touch.
|
|
316
|
-
*
|
|
317
|
-
* Parent stores are declared for every foreign key, not only the ones `data`
|
|
318
|
-
* sets, because a nested write can set foreign keys through `connect()` or
|
|
319
|
-
* through a default.
|
|
320
|
-
*/
|
|
321
|
-
function collectStoreNames(contract, modelName, data, kind) {
|
|
322
|
-
const stores = /* @__PURE__ */ new Set([getStoreName(contract, modelName), ...fkParentStoreNames(contract, modelName)]);
|
|
323
|
-
const scalarData = {};
|
|
324
|
-
const relationNames = /* @__PURE__ */ new Set();
|
|
325
|
-
for (const def of getRelationDefinitions(contract, modelName)) {
|
|
326
|
-
relationNames.add(def.relationName);
|
|
327
|
-
if (def.relationName in data && isRelationMutationCallback(data[def.relationName])) {
|
|
328
|
-
stores.add(def.relatedStoreName);
|
|
329
|
-
for (const store of fkParentStoreNames(contract, def.relatedModelName)) stores.add(store);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
if (kind === "update") {
|
|
333
|
-
for (const [field, value] of Object.entries(data)) if (!relationNames.has(field)) scalarData[field] = value;
|
|
334
|
-
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, getStoreName(contract, modelName), scalarData, createMutationDefaultsCache());
|
|
335
|
-
for (const store of collectOnUpdateEnforcementStoreNames(contract, modelName, patch).storeNames) stores.add(store);
|
|
336
|
-
}
|
|
337
|
-
return [...stores];
|
|
338
|
-
}
|
|
339
|
-
/** The stores of every parent `modelName` has a foreign key to. */
|
|
340
|
-
function fkParentStoreNames(contract, modelName) {
|
|
341
|
-
return getRelationDefinitions(contract, modelName).filter((def) => def.cardinality === "N:1").map((def) => def.relatedStoreName);
|
|
342
|
-
}
|
|
343
|
-
async function createGraph(scope, contract, modelName, input, defaultsCache) {
|
|
344
|
-
const parsed = parseMutationInput(contract, modelName, input);
|
|
345
|
-
const { parentOwned, childOwned } = partitionByOwnership(parsed.relationMutations);
|
|
346
|
-
const scalarData = { ...parsed.scalarData };
|
|
347
|
-
for (const item of parentOwned) {
|
|
348
|
-
if (item.mutation.kind === "disconnect") throw new Error("disconnect() is only supported in update() nested mutations");
|
|
349
|
-
await applyParentOwnedMutation(scope, contract, scalarData, item.relation, item.mutation, defaultsCache);
|
|
350
|
-
}
|
|
351
|
-
const parentRow = await insertSingleRow(scope, contract, modelName, scalarData, defaultsCache);
|
|
352
|
-
for (const item of childOwned) {
|
|
353
|
-
if (item.mutation.kind === "disconnect") throw new Error("disconnect() is only supported in update() nested mutations");
|
|
354
|
-
await applyChildOwnedMutation(scope, contract, modelName, parentRow, item.relation, item.mutation, defaultsCache);
|
|
355
|
-
}
|
|
356
|
-
return parentRow;
|
|
357
|
-
}
|
|
358
|
-
async function updateFirstGraph(scope, contract, modelName, filters, input, defaultsCache) {
|
|
359
|
-
const existingRow = await findFirstByFilters(scope, contract, modelName, filters);
|
|
360
|
-
if (!existingRow) return null;
|
|
361
|
-
const parsed = parseMutationInput(contract, modelName, input);
|
|
362
|
-
const { parentOwned, childOwned } = partitionByOwnership(parsed.relationMutations);
|
|
363
|
-
const scalarData = { ...parsed.scalarData };
|
|
364
|
-
for (const item of parentOwned) await applyParentOwnedMutation(scope, contract, scalarData, item.relation, item.mutation, defaultsCache);
|
|
365
|
-
let parentRow = existingRow;
|
|
366
|
-
if (Object.keys(scalarData).length > 0) {
|
|
367
|
-
const storeName = getStoreName(contract, modelName);
|
|
368
|
-
const key = extractKeyFromRow(existingRow, getKeyPath(contract, modelName));
|
|
369
|
-
const meta = makePlanMeta(contract);
|
|
370
|
-
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, storeName, scalarData, defaultsCache);
|
|
371
|
-
assertEnumValues(contract, modelName, patch);
|
|
372
|
-
await validateScalarFks(scope, contract, modelName, patch, existingRow);
|
|
373
|
-
await applyReferentialActionsForRowOnUpdate(scope, contract, modelName, existingRow, patch);
|
|
374
|
-
const updated = (await scope.execute({
|
|
375
|
-
meta,
|
|
376
|
-
kind: "update",
|
|
377
|
-
storeName,
|
|
378
|
-
key,
|
|
379
|
-
patch
|
|
380
|
-
}))[0];
|
|
381
|
-
if (updated) parentRow = updated;
|
|
382
|
-
}
|
|
383
|
-
for (const item of childOwned) await applyChildOwnedMutation(scope, contract, modelName, parentRow, item.relation, item.mutation, defaultsCache);
|
|
384
|
-
return parentRow;
|
|
385
|
-
}
|
|
386
|
-
function parseMutationInput(contract, modelName, input) {
|
|
387
|
-
const scalarData = {};
|
|
388
|
-
const relationDefs = new Map(getRelationDefinitions(contract, modelName).map((r) => [r.relationName, r]));
|
|
389
|
-
const relationMutations = [];
|
|
390
|
-
for (const [fieldName, value] of Object.entries(input)) {
|
|
391
|
-
const relation = relationDefs.get(fieldName);
|
|
392
|
-
if (!relation) {
|
|
393
|
-
scalarData[fieldName] = value;
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
if (!isRelationMutationCallback(value)) throw new Error(`Relation field "${fieldName}" on model "${modelName}" expects a mutator callback`);
|
|
397
|
-
const mutation = value(createRelationMutator());
|
|
398
|
-
if (!isRelationMutationDescriptor(mutation)) throw new Error(`Relation field "${fieldName}" on model "${modelName}" returned an invalid mutation descriptor`);
|
|
399
|
-
relationMutations.push({
|
|
400
|
-
relation,
|
|
401
|
-
mutation
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
return {
|
|
405
|
-
scalarData,
|
|
406
|
-
relationMutations
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
function partitionByOwnership(mutations) {
|
|
410
|
-
const parentOwned = [];
|
|
411
|
-
const childOwned = [];
|
|
412
|
-
for (const item of mutations) {
|
|
413
|
-
if (item.relation.cardinality === "N:1") {
|
|
414
|
-
parentOwned.push(item);
|
|
415
|
-
continue;
|
|
416
|
-
}
|
|
417
|
-
if (item.relation.cardinality === "M:N") throw new Error("M:N nested mutations are not supported");
|
|
418
|
-
childOwned.push(item);
|
|
419
|
-
}
|
|
420
|
-
return {
|
|
421
|
-
parentOwned,
|
|
422
|
-
childOwned
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
async function applyParentOwnedMutation(scope, contract, scalarData, relation, mutation, defaultsCache) {
|
|
426
|
-
if (mutation.kind === "disconnect") {
|
|
427
|
-
for (const localField of relation.localFields) scalarData[localField] = null;
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
if (mutation.kind === "create") {
|
|
431
|
-
const row = mutation.data[0];
|
|
432
|
-
if (!row) throw new Error(`create() nested mutation for relation "${relation.relationName}" requires data`);
|
|
433
|
-
copyRelatedValuesToParent(relation, scalarData, await insertSingleRow(scope, contract, relation.relatedModelName, row, defaultsCache));
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
const criterion = mutation.criteria[0];
|
|
437
|
-
if (!criterion) throw new Error(`connect() nested mutation for relation "${relation.relationName}" requires a criterion`);
|
|
438
|
-
const relatedRow = await findRowByCriterion(scope, contract, relation.relatedModelName, criterion);
|
|
439
|
-
if (!relatedRow) throw new Error(`connect() nested mutation for relation "${relation.relationName}" did not find a matching row`);
|
|
440
|
-
copyRelatedValuesToParent(relation, scalarData, relatedRow);
|
|
441
|
-
}
|
|
442
|
-
function copyRelatedValuesToParent(relation, scalarData, relatedRow) {
|
|
443
|
-
for (let i = 0; i < relation.localFields.length; i++) {
|
|
444
|
-
const localField = relation.localFields[i];
|
|
445
|
-
const targetField = relation.targetFields[i];
|
|
446
|
-
if (!localField || !targetField) continue;
|
|
447
|
-
scalarData[localField] = relatedRow[targetField];
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
async function applyChildOwnedMutation(scope, contract, parentModelName, parentRow, relation, mutation, defaultsCache) {
|
|
451
|
-
const parentValues = readParentColumnValues(parentModelName, relation, parentRow);
|
|
452
|
-
if (mutation.kind === "create") {
|
|
453
|
-
for (const childInput of mutation.data) {
|
|
454
|
-
const payload = { ...childInput };
|
|
455
|
-
for (const [childField, parentValue] of parentValues.entries()) payload[childField] = parentValue;
|
|
456
|
-
await insertSingleRow(scope, contract, relation.relatedModelName, payload, defaultsCache);
|
|
457
|
-
}
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
if (mutation.kind === "connect") {
|
|
461
|
-
for (const criterion of mutation.criteria) {
|
|
462
|
-
const setValues = {};
|
|
463
|
-
for (const [childField, parentValue] of parentValues.entries()) setValues[childField] = parentValue;
|
|
464
|
-
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, relation.relatedStoreName, setValues, defaultsCache);
|
|
465
|
-
const filter = buildCriterionFilter(criterion);
|
|
466
|
-
const meta = makePlanMeta(contract);
|
|
467
|
-
await scope.execute({
|
|
468
|
-
meta,
|
|
469
|
-
kind: "scan-write",
|
|
470
|
-
storeName: relation.relatedStoreName,
|
|
471
|
-
write: "put-merged",
|
|
472
|
-
patch,
|
|
473
|
-
filter
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
const setValues = {};
|
|
479
|
-
for (const childField of parentValues.keys()) setValues[childField] = null;
|
|
480
|
-
const meta = makePlanMeta(contract);
|
|
481
|
-
if (!mutation.criteria || mutation.criteria.length === 0) {
|
|
482
|
-
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, relation.relatedStoreName, setValues, defaultsCache);
|
|
483
|
-
const parentJoinFilter = buildParentJoinFilter(parentValues);
|
|
484
|
-
await scope.execute({
|
|
485
|
-
meta,
|
|
486
|
-
kind: "scan-write",
|
|
487
|
-
storeName: relation.relatedStoreName,
|
|
488
|
-
write: "put-merged",
|
|
489
|
-
patch,
|
|
490
|
-
filter: parentJoinFilter
|
|
491
|
-
});
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
for (const criterion of mutation.criteria) {
|
|
495
|
-
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, relation.relatedStoreName, setValues, defaultsCache);
|
|
496
|
-
const criterionFilter = buildCriterionFilter(criterion);
|
|
497
|
-
const parentJoinFilter = buildParentJoinFilter(parentValues);
|
|
498
|
-
const combinedFilter = (row) => parentJoinFilter(row) && criterionFilter(row);
|
|
499
|
-
await scope.execute({
|
|
500
|
-
meta,
|
|
501
|
-
kind: "scan-write",
|
|
502
|
-
storeName: relation.relatedStoreName,
|
|
503
|
-
write: "put-merged",
|
|
504
|
-
patch,
|
|
505
|
-
filter: combinedFilter
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
function readParentColumnValues(parentModelName, relation, parentRow) {
|
|
510
|
-
const values = /* @__PURE__ */ new Map();
|
|
511
|
-
for (let i = 0; i < relation.localFields.length; i++) {
|
|
512
|
-
const localField = relation.localFields[i];
|
|
513
|
-
const targetField = relation.targetFields[i];
|
|
514
|
-
if (!localField || !targetField) continue;
|
|
515
|
-
const parentValue = parentRow[localField];
|
|
516
|
-
if (parentValue === void 0) throw new Error(`Nested mutation requires parent field "${localField}" to be present in "${parentModelName}" row`);
|
|
517
|
-
values.set(targetField, parentValue);
|
|
518
|
-
}
|
|
519
|
-
return values;
|
|
520
|
-
}
|
|
521
|
-
async function insertSingleRow(scope, contract, modelName, data, defaultsCache) {
|
|
522
|
-
assertNoNestedCallbacks(modelName, data);
|
|
523
|
-
const storeName = getStoreName(contract, modelName);
|
|
524
|
-
const meta = makePlanMeta(contract);
|
|
525
|
-
const record = applyCreateDefaults(contract.execution?.mutations.defaults, storeName, data, defaultsCache);
|
|
526
|
-
assertEnumValues(contract, modelName, record);
|
|
527
|
-
await validateScalarFks(scope, contract, modelName, record);
|
|
528
|
-
return (await scope.execute({
|
|
529
|
-
meta,
|
|
530
|
-
kind: "add",
|
|
531
|
-
storeName,
|
|
532
|
-
record
|
|
533
|
-
}))[0] ?? record;
|
|
534
|
-
}
|
|
535
|
-
/**
|
|
536
|
-
* Recursive nesting (a relation callback inside an already-nested create) is
|
|
537
|
-
* not supported in Phase 6.4. Without this guard the callback function would be
|
|
538
|
-
* handed to `store.put(...)`, where IDB's structured-clone throws an opaque
|
|
539
|
-
* `DataCloneError` ("could not be cloned") that gives the developer no hint
|
|
540
|
-
* about the real cause. Surface a precise error instead. (PLAN Issue #22.)
|
|
541
|
-
*/
|
|
542
|
-
function assertNoNestedCallbacks(modelName, data) {
|
|
543
|
-
for (const [field, value] of Object.entries(data)) if (typeof value === "function") throw new Error(`Recursive nested writes are not supported: field "${field}" on a nested "${modelName}" record is a relation callback. Only one level of relation nesting is supported — flatten the inner relation into a separate create/connect call.`);
|
|
544
|
-
}
|
|
545
|
-
async function findRowByCriterion(scope, contract, modelName, criterion) {
|
|
546
|
-
const expr = shorthandToFilterExpr(criterion);
|
|
547
|
-
if (!expr) throw new Error(`Nested connect for model "${modelName}" requires a non-empty criterion`);
|
|
548
|
-
const filter = (row) => evaluateFilter(expr, row);
|
|
549
|
-
return scanOneRow(scope, contract, modelName, filter);
|
|
550
|
-
}
|
|
551
|
-
async function findFirstByFilters(scope, contract, modelName, filters) {
|
|
552
|
-
if (filters.length === 0) return null;
|
|
553
|
-
const combined = filters.length === 1 ? filters[0] : {
|
|
554
|
-
kind: "and",
|
|
555
|
-
exprs: filters
|
|
556
|
-
};
|
|
557
|
-
const filter = (row) => evaluateFilter(combined, row);
|
|
558
|
-
return scanOneRow(scope, contract, modelName, filter);
|
|
559
|
-
}
|
|
560
|
-
async function scanOneRow(scope, contract, modelName, filter) {
|
|
561
|
-
const storeName = getStoreName(contract, modelName);
|
|
562
|
-
const plan = {
|
|
563
|
-
meta: makePlanMeta(contract),
|
|
564
|
-
kind: "cursor-scan",
|
|
565
|
-
storeName,
|
|
566
|
-
filter,
|
|
567
|
-
take: 1
|
|
568
|
-
};
|
|
569
|
-
return (await scope.execute(plan))[0] ?? null;
|
|
570
|
-
}
|
|
571
|
-
function buildCriterionFilter(criterion) {
|
|
572
|
-
const expr = shorthandToFilterExpr(criterion);
|
|
573
|
-
if (!expr) return () => true;
|
|
574
|
-
return (row) => evaluateFilter(expr, row);
|
|
575
|
-
}
|
|
576
|
-
function buildParentJoinFilter(parentValues) {
|
|
577
|
-
const pairs = [...parentValues.entries()];
|
|
578
|
-
return (row) => pairs.every(([childField, parentValue]) => fieldValuesEqual(row[childField], parentValue));
|
|
579
|
-
}
|
|
580
|
-
function getStoredAction(contract, modelName, relationName, kind) {
|
|
581
|
-
return (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.relations?.[relationName]?.[kind];
|
|
582
|
-
}
|
|
583
|
-
function sameFields(a, b) {
|
|
584
|
-
return a.length === b.length && a.every((field, index) => field === b[index]);
|
|
585
|
-
}
|
|
586
|
-
/**
|
|
587
|
-
* Resolves the referential action for a child-enforcement relation: prefers
|
|
588
|
-
* the action stored directly on this side, falls back to the same action
|
|
589
|
-
* stored on the inverse (FK-owning) side of the relation, and finally to
|
|
590
|
-
* `defaultAction` when neither side declares one.
|
|
591
|
-
*
|
|
592
|
-
* Both branches are live, for different authoring paths. PSL only ever
|
|
593
|
-
* stores the action on the `N:1` (FK-owning) side — enforcement always walks
|
|
594
|
-
* from the parent (`1:N`/non-owning-`1:1`) side, which PSL leaves bare, so
|
|
595
|
-
* the fallback is what actually finds it. TS-DSL relation entries are
|
|
596
|
-
* self-contained (no PSL-style pairing requirement) and typically declare
|
|
597
|
-
* the action directly on the side this function is called for, so the direct
|
|
598
|
-
* check is what fires there — see `contract-builder.ts`'s
|
|
599
|
-
* `validateNoConflictingRelationActions`, which rejects declaring the same
|
|
600
|
-
* action kind on both sides so this fallback never has to arbitrate a real
|
|
601
|
-
* conflict.
|
|
602
|
-
*/
|
|
603
|
-
function getReferentialActionForRelation(contract, modelName, def, kind) {
|
|
604
|
-
return enforcedAction(findDeclaredAction(contract, modelName, def, kind));
|
|
605
|
-
}
|
|
606
|
-
function findDeclaredAction(contract, modelName, def, kind) {
|
|
607
|
-
const direct = getStoredAction(contract, modelName, def.relationName, kind);
|
|
608
|
-
if (direct !== void 0) return direct;
|
|
609
|
-
for (const inverse of getRelationDefinitions(contract, def.relatedModelName)) {
|
|
610
|
-
if (inverse.relatedModelName !== modelName) continue;
|
|
611
|
-
if (!sameFields(inverse.localFields, def.targetFields)) continue;
|
|
612
|
-
if (!sameFields(inverse.targetFields, def.localFields)) continue;
|
|
613
|
-
const inverseAction = getStoredAction(contract, def.relatedModelName, inverse.relationName, kind);
|
|
614
|
-
if (inverseAction !== void 0) return inverseAction;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
/**
|
|
618
|
-
* Applies the default and resolves `noAction`, matching what Postgres does
|
|
619
|
-
* with the same schema, so a synced app behaves the same on both sides:
|
|
620
|
-
*
|
|
621
|
-
* - An undeclared action defaults to `restrict`, for `onDelete` and
|
|
622
|
-
* `onUpdate` alike. Prisma 8 emits no `ON DELETE`/`ON UPDATE` clause for an
|
|
623
|
-
* undeclared action, and the database default is `NO ACTION`.
|
|
624
|
-
* - `noAction` behaves like `restrict`. In SQL, `NO ACTION` also rejects the
|
|
625
|
-
* change; it only defers the check to the end of the statement, which makes
|
|
626
|
-
* no difference here.
|
|
627
|
-
*/
|
|
628
|
-
function enforcedAction(declared) {
|
|
629
|
-
return declared === void 0 || declared === "noAction" ? "restrict" : declared;
|
|
630
|
-
}
|
|
631
|
-
function isChildEnforcementRelation(contract, modelName, def) {
|
|
632
|
-
if (def.cardinality === "1:N") return true;
|
|
633
|
-
if (def.cardinality === "1:1") {
|
|
634
|
-
const keyPath = getKeyPath(contract, modelName);
|
|
635
|
-
return sameFields(def.localFields, keyPathFields(keyPath));
|
|
636
|
-
}
|
|
637
|
-
return false;
|
|
638
|
-
}
|
|
639
|
-
/**
|
|
640
|
-
* `true` if any child row matches the relation against `parentRow`'s values —
|
|
641
|
-
* the existence check behind `restrict`.
|
|
642
|
-
*/
|
|
643
|
-
async function childExists(scope, meta, def, parentRow) {
|
|
644
|
-
return (await scope.execute({
|
|
645
|
-
meta,
|
|
646
|
-
kind: "cursor-scan",
|
|
647
|
-
storeName: def.relatedStoreName,
|
|
648
|
-
filter: buildChildFilterFromRow(def, parentRow),
|
|
649
|
-
take: 1
|
|
650
|
-
})).length > 0;
|
|
651
|
-
}
|
|
652
|
-
/**
|
|
653
|
-
* Builds a filter matching a relation's children against one specific parent
|
|
654
|
-
* row's values. Shared by `onDelete` cascade (`applyReferentialActionsForRow`)
|
|
655
|
-
* and `onUpdate` cascade (`applyReferentialActionsForRowOnUpdate`).
|
|
656
|
-
*/
|
|
657
|
-
function buildChildFilterFromRow(def, row) {
|
|
658
|
-
const pairs = def.localFields.map((lf, i) => ({
|
|
659
|
-
childField: def.targetFields[i],
|
|
660
|
-
parentValue: row[lf]
|
|
661
|
-
}));
|
|
662
|
-
return (child) => pairs.every(({ childField, parentValue }) => fieldValuesEqual(child[childField], parentValue));
|
|
663
|
-
}
|
|
664
|
-
/** Reads a field's literal `@default(...)` value from `IdbModelStorage.fieldDefaults`, if declared. */
|
|
665
|
-
function getFieldDefault(contract, modelName, fieldName) {
|
|
666
|
-
return (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.fieldDefaults?.[fieldName];
|
|
667
|
-
}
|
|
668
|
-
/**
|
|
669
|
-
* Builds the patch for a `setDefault` referential action: each of the
|
|
670
|
-
* relation's `targetFields` (the child's FK fields) reset to its own
|
|
671
|
-
* declared literal default — not the parent's. Throws when any target field
|
|
672
|
-
* has no declared default; `setDefault` is only meaningful when every FK
|
|
673
|
-
* field it resets has somewhere to reset to.
|
|
674
|
-
*/
|
|
675
|
-
function buildSetDefaultPatch(contract, def) {
|
|
676
|
-
const patch = {};
|
|
677
|
-
for (const targetField of def.targetFields) {
|
|
678
|
-
const value = getFieldDefault(contract, def.relatedModelName, targetField);
|
|
679
|
-
if (value === void 0) throw new Error(`setDefault referential action on relation '${def.relationName}' requires field '${def.relatedModelName}.${targetField}' to declare a literal @default(...) value — no default is registered for that field.`);
|
|
680
|
-
patch[targetField] = value;
|
|
681
|
-
}
|
|
682
|
-
return patch;
|
|
683
|
-
}
|
|
684
|
-
/**
|
|
685
|
-
* Validates that a `setDefault` patch actually references a real row before
|
|
686
|
-
* it's written — without this, `setDefault` would silently reintroduce the
|
|
687
|
-
* exact dangling-FK problem the rest of this file exists to prevent. A real
|
|
688
|
-
* SQL database only makes `SET DEFAULT` safe because its FK constraint
|
|
689
|
-
* re-checks the new value transactionally at write time; IDB has no such
|
|
690
|
-
* engine to delegate to, so this check is IDB's equivalent.
|
|
691
|
-
*
|
|
692
|
-
* Checks against `modelName`'s own store (the parent being deleted/updated)
|
|
693
|
-
* using `def.localFields` — the relation's own semantics ("child.targetField
|
|
694
|
-
* references parent.localField") mean that's exactly what a default value
|
|
695
|
-
* written to the child's FK field must match. That store is always already
|
|
696
|
-
* part of the transaction (it's the model literally being written), so no
|
|
697
|
-
* store-list changes are needed to call this.
|
|
698
|
-
*
|
|
699
|
-
* A compound relation is checked as one tuple: a single parent row must
|
|
700
|
-
* match every field's default.
|
|
701
|
-
*
|
|
702
|
-
* `excludeKey`, when given, excludes the parent row currently being
|
|
703
|
-
* deleted/updated from the existence scan. That row is still physically
|
|
704
|
-
* present in the store at this point (its own write/delete hasn't happened
|
|
705
|
-
* yet — this check runs first, before the enforcement loop's caller applies
|
|
706
|
-
* it), so without the exclusion, a default value equal to that row's *old*
|
|
707
|
-
* `localField` value would false-positive: the scan would find the row about
|
|
708
|
-
* to disappear (or change away from that value) and wrongly conclude the
|
|
709
|
-
* default references a real row, when after the transaction nothing will.
|
|
710
|
-
*/
|
|
711
|
-
async function validateSetDefaultPatch(scope, contract, modelName, def, patch, excludeKey) {
|
|
712
|
-
const values = def.targetFields.map((f) => patch[f]);
|
|
713
|
-
if (values.some((v) => v === null || v === void 0)) return;
|
|
714
|
-
if (!await parentExists(scope, contract, makePlanMeta(contract), modelName, def.localFields, values, excludeKey)) throw new Error(`setDefault referential action on relation '${def.relationName}' would set ${def.relatedModelName} to ${describeTuple(def.targetFields, values)}, but no ${modelName} with ${describeTuple(def.localFields, values)} exists — the declared default does not reference a real row.`);
|
|
715
|
-
}
|
|
716
|
-
/**
|
|
717
|
-
* Collects every store an `onUpdate`-enforced write to `modelName` might
|
|
718
|
-
* touch: the model's own store, plus the related store of any 1:N/1:1-
|
|
719
|
-
* parent-side relation whose `localFields` appear in the raw patch —
|
|
720
|
-
* pessimistic (checks field *presence*, not whether the resolved value will
|
|
721
|
-
* actually change, since the current row isn't read yet when this runs).
|
|
722
|
-
* Once a `cascade` edge is entered, the walk continues transitively (mirrors
|
|
723
|
-
* `collectDeleteStoreNames`'s cascade-only recursion), since a propagated
|
|
724
|
-
* value change may itself need to cascade further.
|
|
725
|
-
*
|
|
726
|
-
* Returns `enforces: false` when no `onUpdate` enforcement can apply to this
|
|
727
|
-
* write — callers use that to keep the existing fast blind-write path
|
|
728
|
-
* instead of paying for a read-before-write. This is tracked as an explicit
|
|
729
|
-
* flag rather than inferred from `storeNames.length > 1`: for a
|
|
730
|
-
* self-referential relation, `def.relatedStoreName` equals `modelName`'s own
|
|
731
|
-
* store (already the seed of the `Set`), so the store count alone can't tell
|
|
732
|
-
* "no enforcement" apart from "enforces, but only against this same store".
|
|
733
|
-
*/
|
|
734
|
-
function collectOnUpdateEnforcementStoreNames(contract, modelName, data) {
|
|
735
|
-
const stores = /* @__PURE__ */ new Set([getStoreName(contract, modelName)]);
|
|
736
|
-
const visitedModels = /* @__PURE__ */ new Set();
|
|
737
|
-
let enforces = false;
|
|
738
|
-
function walkCascadeChain(mName) {
|
|
739
|
-
if (visitedModels.has(mName)) return;
|
|
740
|
-
visitedModels.add(mName);
|
|
741
|
-
for (const def of getRelationDefinitions(contract, mName)) {
|
|
742
|
-
if (!isChildEnforcementRelation(contract, mName, def)) continue;
|
|
743
|
-
const action = getReferentialActionForRelation(contract, mName, def, "onUpdate");
|
|
744
|
-
stores.add(def.relatedStoreName);
|
|
745
|
-
if (action === "cascade") walkCascadeChain(def.relatedModelName);
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
visitedModels.add(modelName);
|
|
749
|
-
for (const def of getRelationDefinitions(contract, modelName)) {
|
|
750
|
-
if (!isChildEnforcementRelation(contract, modelName, def)) continue;
|
|
751
|
-
if (!def.localFields.some((f) => f in data)) continue;
|
|
752
|
-
const action = getReferentialActionForRelation(contract, modelName, def, "onUpdate");
|
|
753
|
-
enforces = true;
|
|
754
|
-
stores.add(def.relatedStoreName);
|
|
755
|
-
if (action === "cascade") walkCascadeChain(def.relatedModelName);
|
|
756
|
-
}
|
|
757
|
-
return {
|
|
758
|
-
storeNames: [...stores],
|
|
759
|
-
enforces
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
/**
|
|
763
|
-
* Enforces every `onUpdate` action declared on `modelName`'s child relations
|
|
764
|
-
* against one specific `oldRow` about to be patched with `patch`. Only
|
|
765
|
-
* relations whose `localFields` are both present in `patch` and differ from
|
|
766
|
-
* `oldRow`'s current value are enforced — a patch that sets a field to its
|
|
767
|
-
* existing value, or never touches a locally-referenced field, is a no-op
|
|
768
|
-
* here. Children are matched against `oldRow`'s pre-change values.
|
|
769
|
-
*
|
|
770
|
-
* `cascade` propagates the new value(s) onto the matched children's FK
|
|
771
|
-
* fields and recurses into each child's own `onUpdate` relations before
|
|
772
|
-
* writing (the propagated change may itself need to cascade further) —
|
|
773
|
-
* mirrors `applyReferentialActionsForRow`'s recursive shape, including the
|
|
774
|
-
* `visited` row-level cycle guard (see that function's doc comment for the
|
|
775
|
-
* fresh-per-top-level-row / shared-across-one-row's-descent contract).
|
|
776
|
-
* `setNull`/`setDefault` are leaf actions: the child's FK field changes but
|
|
777
|
-
* its own key doesn't, so nothing below it needs re-enforcement.
|
|
778
|
-
*
|
|
779
|
-
* Compound relations use values from the same `oldRow`/`patch`, so every
|
|
780
|
-
* child is matched against the full pre-change tuple.
|
|
781
|
-
*/
|
|
782
|
-
async function applyReferentialActionsForRowOnUpdate(scope, contract, modelName, oldRow, patch, visited = /* @__PURE__ */ new Set()) {
|
|
783
|
-
const keyPath = getKeyPath(contract, modelName);
|
|
784
|
-
const rowKey = `${getStoreName(contract, modelName)}::${keyToken(extractKeyFromRow(oldRow, keyPath))}`;
|
|
785
|
-
if (visited.has(rowKey)) return;
|
|
786
|
-
visited.add(rowKey);
|
|
787
|
-
const meta = makePlanMeta(contract);
|
|
788
|
-
for (const def of getRelationDefinitions(contract, modelName)) {
|
|
789
|
-
if (!isChildEnforcementRelation(contract, modelName, def)) continue;
|
|
790
|
-
const changedFields = def.localFields.filter((f) => f in patch && !fieldValuesEqual(patch[f], oldRow[f]));
|
|
791
|
-
if (changedFields.length === 0) continue;
|
|
792
|
-
const action = getReferentialActionForRelation(contract, modelName, def, "onUpdate");
|
|
793
|
-
const childFilter = buildChildFilterFromRow(def, oldRow);
|
|
794
|
-
if (action === "restrict") {
|
|
795
|
-
if (await childExists(scope, meta, def, oldRow)) throw new Error(`Cannot update ${modelName} '${keyToken(extractKeyFromRow(oldRow, keyPath))}': changing field(s) ${changedFields.join(", ")} would orphan child records on relation '${def.relationName}'. Update or remove those children first, or declare onUpdate: Cascade, SetNull or SetDefault on the relation.`);
|
|
796
|
-
continue;
|
|
797
|
-
}
|
|
798
|
-
if (action === "cascade") {
|
|
799
|
-
const childPatch = {};
|
|
800
|
-
for (let i = 0; i < def.localFields.length; i++) {
|
|
801
|
-
const lf = def.localFields[i];
|
|
802
|
-
const tf = def.targetFields[i];
|
|
803
|
-
if (changedFields.includes(lf)) childPatch[tf] = patch[lf];
|
|
804
|
-
}
|
|
805
|
-
const children = await scope.execute({
|
|
806
|
-
meta,
|
|
807
|
-
kind: "cursor-scan",
|
|
808
|
-
storeName: def.relatedStoreName,
|
|
809
|
-
filter: childFilter
|
|
810
|
-
});
|
|
811
|
-
for (const child of children) await applyReferentialActionsForRowOnUpdate(scope, contract, def.relatedModelName, child, childPatch, visited);
|
|
812
|
-
await scope.execute({
|
|
813
|
-
meta,
|
|
814
|
-
kind: "scan-write",
|
|
815
|
-
storeName: def.relatedStoreName,
|
|
816
|
-
write: "put-merged",
|
|
817
|
-
patch: childPatch,
|
|
818
|
-
filter: childFilter
|
|
819
|
-
});
|
|
820
|
-
continue;
|
|
821
|
-
}
|
|
822
|
-
if (action === "setNull") {
|
|
823
|
-
const childPatch = {};
|
|
824
|
-
for (const targetField of def.targetFields) childPatch[targetField] = null;
|
|
825
|
-
await scope.execute({
|
|
826
|
-
meta,
|
|
827
|
-
kind: "scan-write",
|
|
828
|
-
storeName: def.relatedStoreName,
|
|
829
|
-
write: "put-merged",
|
|
830
|
-
patch: childPatch,
|
|
831
|
-
filter: childFilter
|
|
832
|
-
});
|
|
833
|
-
continue;
|
|
834
|
-
}
|
|
835
|
-
if (action === "setDefault") {
|
|
836
|
-
const childPatch = buildSetDefaultPatch(contract, def);
|
|
837
|
-
await validateSetDefaultPatch(scope, contract, modelName, def, childPatch, extractKeyFromRow(oldRow, keyPath));
|
|
838
|
-
await scope.execute({
|
|
839
|
-
meta,
|
|
840
|
-
kind: "scan-write",
|
|
841
|
-
storeName: def.relatedStoreName,
|
|
842
|
-
write: "put-merged",
|
|
843
|
-
patch: childPatch,
|
|
844
|
-
filter: childFilter
|
|
845
|
-
});
|
|
846
|
-
continue;
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
/**
|
|
851
|
-
* Returns true if `data` contains at least one non-null value for a localField
|
|
852
|
-
* of a N:1 relation — indicating scalar FK fields that need existence validation.
|
|
853
|
-
*/
|
|
854
|
-
function hasScalarFkFields(contract, modelName, data) {
|
|
855
|
-
for (const def of getRelationDefinitions(contract, modelName)) {
|
|
856
|
-
if (def.cardinality !== "N:1") continue;
|
|
857
|
-
for (const localField of def.localFields) if (localField in data && data[localField] !== null && data[localField] !== void 0) return true;
|
|
858
|
-
}
|
|
859
|
-
return false;
|
|
860
|
-
}
|
|
861
|
-
function collectScalarFkStoreNames(contract, modelName, data) {
|
|
862
|
-
const stores = /* @__PURE__ */ new Set([getStoreName(contract, modelName)]);
|
|
863
|
-
for (const def of getRelationDefinitions(contract, modelName)) {
|
|
864
|
-
if (def.cardinality !== "N:1") continue;
|
|
865
|
-
if (def.localFields.some((f) => f in data && data[f] !== null && data[f] !== void 0)) stores.add(def.relatedStoreName);
|
|
866
|
-
}
|
|
867
|
-
return [...stores];
|
|
868
|
-
}
|
|
869
|
-
/**
|
|
870
|
-
* Checks that every foreign key `data` sets points at an existing parent row,
|
|
871
|
-
* inside the write's transaction.
|
|
872
|
-
*
|
|
873
|
-
* A compound foreign key is checked as one tuple: a single parent row must
|
|
874
|
-
* match every field. Checking the fields one at a time could pass with each
|
|
875
|
-
* value taken from a different parent. When `data` sets only some fields of a
|
|
876
|
-
* compound key, the rest come from `existingRow`, the row being updated.
|
|
877
|
-
* Callers must pass it in that case; see {@link fkCheckNeedsExistingRow}.
|
|
878
|
-
*
|
|
879
|
-
* A key with any `null` field isn't checked, like SQL's default `MATCH SIMPLE`.
|
|
880
|
-
*/
|
|
881
|
-
async function validateScalarFks(scope, contract, modelName, data, existingRow) {
|
|
882
|
-
const meta = makePlanMeta(contract);
|
|
883
|
-
for (const def of getRelationDefinitions(contract, modelName)) {
|
|
884
|
-
if (def.cardinality !== "N:1") continue;
|
|
885
|
-
if (!def.localFields.some((f) => f in data)) continue;
|
|
886
|
-
const values = def.localFields.map((f) => f in data ? data[f] : existingRow?.[f]);
|
|
887
|
-
if (values.some((v) => v === null || v === void 0)) continue;
|
|
888
|
-
if (!await parentExists(scope, contract, meta, def.relatedModelName, def.targetFields, values)) throw new Error(`FK violation on relation '${def.relationName}': no ${def.relatedModelName} with ${describeTuple(def.targetFields, values)}`);
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
/**
|
|
892
|
-
* `true` when `data` sets some, but not all, fields of a compound foreign
|
|
893
|
-
* key. Checking it then needs the rest of the key from the row being
|
|
894
|
-
* updated, so the update has to read each row before writing it.
|
|
895
|
-
*/
|
|
896
|
-
function fkCheckNeedsExistingRow(contract, modelName, data) {
|
|
897
|
-
return getRelationDefinitions(contract, modelName).some((def) => {
|
|
898
|
-
if (def.cardinality !== "N:1") return false;
|
|
899
|
-
const set = def.localFields.filter((f) => f in data).length;
|
|
900
|
-
return set > 0 && set < def.localFields.length;
|
|
901
|
-
});
|
|
902
|
-
}
|
|
903
|
-
/**
|
|
904
|
-
* Does `parentModel` have a row whose `fields` equal `values`, pairwise?
|
|
905
|
-
*
|
|
906
|
-
* Scans the parent store, comparing values the way IndexedDB compares keys.
|
|
907
|
-
* `excludeKey` leaves out one parent row, for checks that run while that row
|
|
908
|
-
* is being deleted or changed.
|
|
909
|
-
*/
|
|
910
|
-
async function parentExists(scope, contract, meta, parentModel, fields, values, excludeKey) {
|
|
911
|
-
const storeName = getStoreName(contract, parentModel);
|
|
912
|
-
const keyPath = getKeyPath(contract, parentModel);
|
|
913
|
-
const filter = (row) => fields.every((f, i) => fieldValuesEqual(row[f], values[i])) && (excludeKey === void 0 || !keyEquals$1(extractKeyFromRow(row, keyPath), excludeKey));
|
|
914
|
-
return (await scope.execute({
|
|
915
|
-
meta,
|
|
916
|
-
kind: "cursor-scan",
|
|
917
|
-
storeName,
|
|
918
|
-
filter,
|
|
919
|
-
take: 1
|
|
920
|
-
})).length > 0;
|
|
921
|
-
}
|
|
922
|
-
/** `id='u1'`, or `orgId='a', id='u1'` for a compound key. */
|
|
923
|
-
function describeTuple(fields, values) {
|
|
924
|
-
return fields.map((f, i) => `${f}='${String(values[i])}'`).join(", ");
|
|
925
|
-
}
|
|
926
|
-
async function executeScalarCreateWithFkValidation(options) {
|
|
927
|
-
const { executor, contract, modelName, data } = options;
|
|
928
|
-
const defaultsCache = createMutationDefaultsCache();
|
|
929
|
-
const record = applyCreateDefaults(contract.execution?.mutations.defaults, getStoreName(contract, modelName), data, defaultsCache);
|
|
930
|
-
return withMutationScope(executor, collectScalarFkStoreNames(contract, modelName, record), (scope) => insertSingleRow(scope, contract, modelName, record, defaultsCache));
|
|
931
|
-
}
|
|
932
|
-
/**
|
|
933
|
-
* `createAll()` for rows that set foreign keys: checks every row's foreign
|
|
934
|
-
* keys and inserts it, all in one transaction, so one bad row writes nothing.
|
|
935
|
-
* The batch shares one defaults cache, like the plain `createAll()` path.
|
|
936
|
-
*/
|
|
937
|
-
async function executeScalarCreateAllWithFkValidation(options) {
|
|
938
|
-
const { executor, contract, modelName, data } = options;
|
|
939
|
-
const defaultsCache = createMutationDefaultsCache();
|
|
940
|
-
const storeName = getStoreName(contract, modelName);
|
|
941
|
-
const records = data.map((row) => applyCreateDefaults(contract.execution?.mutations.defaults, storeName, row, defaultsCache));
|
|
942
|
-
return withMutationScope(executor, [...new Set(records.flatMap((row) => collectScalarFkStoreNames(contract, modelName, row)))], async (scope) => {
|
|
943
|
-
const inserted = [];
|
|
944
|
-
for (const record of records) inserted.push(await insertSingleRow(scope, contract, modelName, record, defaultsCache));
|
|
945
|
-
return inserted;
|
|
946
|
-
});
|
|
947
|
-
}
|
|
948
|
-
/**
|
|
949
|
-
* Combines FK-existence store scope (this model's own N:1 relations) with
|
|
950
|
-
* `onUpdate` enforcement store scope (this model's child relations) for a
|
|
951
|
-
* single write. Returns the union alongside whether `onUpdate` enforcement
|
|
952
|
-
* actually applies, including self-relations that touch only this model's
|
|
953
|
-
* own store. Callers use that flag to choose between the existing
|
|
954
|
-
* fast blind-write path and the read-before-write enforcement path.
|
|
955
|
-
*/
|
|
956
|
-
function collectUpdateStoreNames(contract, modelName, data) {
|
|
957
|
-
const fkStoreNames = collectScalarFkStoreNames(contract, modelName, data);
|
|
958
|
-
const { storeNames: onUpdateStoreNames, enforces } = collectOnUpdateEnforcementStoreNames(contract, modelName, data);
|
|
959
|
-
return {
|
|
960
|
-
storeNames: [.../* @__PURE__ */ new Set([...fkStoreNames, ...onUpdateStoreNames])],
|
|
961
|
-
enforcesOnUpdate: enforces
|
|
962
|
-
};
|
|
963
|
-
}
|
|
964
|
-
/** Updates the first matching row with FK validation and referential actions. */
|
|
965
|
-
async function executeScalarUpdateWithFkValidation(options) {
|
|
966
|
-
return (await executeScalarUpdates(options, 1))[0] ?? null;
|
|
967
|
-
}
|
|
968
|
-
/**
|
|
969
|
-
* Bulk counterpart to {@link executeScalarUpdateWithFkValidation}: same FK
|
|
970
|
-
* validation and `onUpdate` enforcement, but applies the patch to every row
|
|
971
|
-
* the filter matches (no `take: 1`) and returns all of them.
|
|
972
|
-
*
|
|
973
|
-
* The transaction scope lets the sync interceptor observe the affected rows
|
|
974
|
-
* and write one outbox event per row in the same transaction. A bulk write's
|
|
975
|
-
* affected keys are only known after it runs, so tracking cannot happen by
|
|
976
|
-
* extending the plan before execution.
|
|
977
|
-
*/
|
|
978
|
-
async function executeBulkUpdateWithFkValidation(options) {
|
|
979
|
-
return executeScalarUpdates(options);
|
|
980
|
-
}
|
|
981
|
-
/** Shares validation and atomic writes; only single-row updates limit the scan. */
|
|
982
|
-
async function executeScalarUpdates(options, take) {
|
|
983
|
-
const { executor, contract, modelName, filters, data } = options;
|
|
984
|
-
const storeName = getStoreName(contract, modelName);
|
|
985
|
-
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, storeName, data, createMutationDefaultsCache());
|
|
986
|
-
assertEnumValues(contract, modelName, patch);
|
|
987
|
-
const { storeNames, enforcesOnUpdate } = collectUpdateStoreNames(contract, modelName, patch);
|
|
988
|
-
const needsRowForFks = fkCheckNeedsExistingRow(contract, modelName, patch);
|
|
989
|
-
return withMutationScope(executor, storeNames, async (scope) => {
|
|
990
|
-
if (!needsRowForFks) await validateScalarFks(scope, contract, modelName, patch);
|
|
991
|
-
const meta = makePlanMeta(contract);
|
|
992
|
-
const combined = filters.length === 0 ? void 0 : filters.length === 1 ? filters[0] : {
|
|
993
|
-
kind: "and",
|
|
994
|
-
exprs: filters
|
|
995
|
-
};
|
|
996
|
-
const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
|
|
997
|
-
if (!enforcesOnUpdate && !needsRowForFks) return scope.execute({
|
|
998
|
-
meta,
|
|
999
|
-
kind: "scan-write",
|
|
1000
|
-
storeName,
|
|
1001
|
-
write: "put-merged",
|
|
1002
|
-
patch,
|
|
1003
|
-
...take !== void 0 ? { take } : {},
|
|
1004
|
-
...filter !== void 0 ? { filter } : {}
|
|
1005
|
-
});
|
|
1006
|
-
const oldRows = await scope.execute({
|
|
1007
|
-
meta,
|
|
1008
|
-
kind: "cursor-scan",
|
|
1009
|
-
storeName,
|
|
1010
|
-
...take !== void 0 ? { take } : {},
|
|
1011
|
-
...filter !== void 0 ? { filter } : {}
|
|
1012
|
-
});
|
|
1013
|
-
const keyPath = getKeyPath(contract, modelName);
|
|
1014
|
-
const results = [];
|
|
1015
|
-
for (const oldRow of oldRows) {
|
|
1016
|
-
if (needsRowForFks) await validateScalarFks(scope, contract, modelName, patch, oldRow);
|
|
1017
|
-
if (enforcesOnUpdate) await applyReferentialActionsForRowOnUpdate(scope, contract, modelName, oldRow, patch);
|
|
1018
|
-
const key = extractKeyFromRow(oldRow, keyPath);
|
|
1019
|
-
const updated = (await scope.execute({
|
|
1020
|
-
meta,
|
|
1021
|
-
kind: "update",
|
|
1022
|
-
storeName,
|
|
1023
|
-
key,
|
|
1024
|
-
patch
|
|
1025
|
-
}))[0];
|
|
1026
|
-
if (updated) results.push(updated);
|
|
1027
|
-
}
|
|
1028
|
-
return results;
|
|
1029
|
-
});
|
|
1030
|
-
}
|
|
1031
|
-
/**
|
|
1032
|
-
* Returns true if the model has at least one child relation (1:N or parent-side
|
|
1033
|
-
* 1:1). Every such relation has an `onDelete` action to enforce: `noAction`
|
|
1034
|
-
* behaves like `restrict`, which is also the default.
|
|
1035
|
-
*/
|
|
1036
|
-
function hasEnforceableChildRelations(contract, modelName) {
|
|
1037
|
-
return getRelationDefinitions(contract, modelName).some((def) => isChildEnforcementRelation(contract, modelName, def));
|
|
1038
|
-
}
|
|
1039
|
-
/**
|
|
1040
|
-
* Transitively walks the `onDelete` cascade graph from `modelName`, collecting
|
|
1041
|
-
* every store a recursive delete might touch. `cascade` edges are walked
|
|
1042
|
-
* further (a cascaded child's own children may themselves cascade);
|
|
1043
|
-
* `restrict`/`setNull`/`setDefault` edges still need their store added (each
|
|
1044
|
-
* is read or written once) but don't recurse further, since none of them
|
|
1045
|
-
* delete the child row. Guarded by a model-level visited set — required
|
|
1046
|
-
* because IDB must declare every store a transaction might touch before it
|
|
1047
|
-
* opens, so this static walk must terminate even on a self-referential model
|
|
1048
|
-
* or a cycle of mutually-cascading models.
|
|
1049
|
-
*/
|
|
1050
|
-
function collectDeleteStoreNames(contract, modelName) {
|
|
1051
|
-
const stores = /* @__PURE__ */ new Set();
|
|
1052
|
-
const visitedModels = /* @__PURE__ */ new Set();
|
|
1053
|
-
function walk(mName) {
|
|
1054
|
-
if (visitedModels.has(mName)) return;
|
|
1055
|
-
visitedModels.add(mName);
|
|
1056
|
-
stores.add(getStoreName(contract, mName));
|
|
1057
|
-
for (const def of getRelationDefinitions(contract, mName)) {
|
|
1058
|
-
if (!isChildEnforcementRelation(contract, mName, def)) continue;
|
|
1059
|
-
const action = getReferentialActionForRelation(contract, mName, def, "onDelete");
|
|
1060
|
-
stores.add(def.relatedStoreName);
|
|
1061
|
-
if (action === "cascade") walk(def.relatedModelName);
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
walk(modelName);
|
|
1065
|
-
return [...stores];
|
|
1066
|
-
}
|
|
1067
|
-
/**
|
|
1068
|
-
* Enforces every `onDelete` action declared on `modelName`'s child relations
|
|
1069
|
-
* against one specific `row` about to be deleted. `cascade` recurses into
|
|
1070
|
-
* each matched child's own `onDelete` relations *before* deleting it — so a
|
|
1071
|
-
* multi-hop chain (`User --cascade--> Post --cascade--> Comment`) is fully
|
|
1072
|
-
* torn down, and a `restrict` several hops deep still aborts the whole
|
|
1073
|
-
* transaction (recursing before deleting means the delete never happens if a
|
|
1074
|
-
* deeper hop throws). `setNull`/`setDefault` are leaf actions: the child row
|
|
1075
|
-
* survives, so recursion never continues past them.
|
|
1076
|
-
*
|
|
1077
|
-
* `visited` (keyed by `storeName::key`) guards against row-level cycles — two
|
|
1078
|
-
* specific rows whose FKs point at each other through a self-referential or
|
|
1079
|
-
* mutually-cascading relation graph. Callers should leave it at its default
|
|
1080
|
-
* (a fresh `Set` per top-level call) so independent rows deleted in the same
|
|
1081
|
-
* `deleteAll()` batch don't cross-suppress each other's cascades; it's only
|
|
1082
|
-
* ever passed explicitly by this function's own recursive calls, to keep one
|
|
1083
|
-
* shared guard across a single row's full recursive descent.
|
|
1084
|
-
*/
|
|
1085
|
-
async function applyReferentialActionsForRow(scope, contract, modelName, row, visited = /* @__PURE__ */ new Set()) {
|
|
1086
|
-
const keyPath = getKeyPath(contract, modelName);
|
|
1087
|
-
const rowKey = `${getStoreName(contract, modelName)}::${keyToken(extractKeyFromRow(row, keyPath))}`;
|
|
1088
|
-
if (visited.has(rowKey)) return;
|
|
1089
|
-
visited.add(rowKey);
|
|
1090
|
-
const meta = makePlanMeta(contract);
|
|
1091
|
-
for (const def of getRelationDefinitions(contract, modelName)) {
|
|
1092
|
-
if (!isChildEnforcementRelation(contract, modelName, def)) continue;
|
|
1093
|
-
const action = getReferentialActionForRelation(contract, modelName, def, "onDelete");
|
|
1094
|
-
const childFilter = buildChildFilterFromRow(def, row);
|
|
1095
|
-
if (action === "restrict") {
|
|
1096
|
-
if (await childExists(scope, meta, def, row)) throw new Error(`Cannot delete ${modelName} '${keyToken(extractKeyFromRow(row, keyPath))}': child records exist on relation '${def.relationName}'. Delete those children first, or declare onDelete: Cascade, SetNull or SetDefault on the relation.`);
|
|
1097
|
-
continue;
|
|
1098
|
-
}
|
|
1099
|
-
if (action === "cascade") {
|
|
1100
|
-
const childKeyPath = getKeyPath(contract, def.relatedModelName);
|
|
1101
|
-
const children = await scope.execute({
|
|
1102
|
-
meta,
|
|
1103
|
-
kind: "cursor-scan",
|
|
1104
|
-
storeName: def.relatedStoreName,
|
|
1105
|
-
filter: childFilter
|
|
1106
|
-
});
|
|
1107
|
-
for (const child of children) {
|
|
1108
|
-
await applyReferentialActionsForRow(scope, contract, def.relatedModelName, child, visited);
|
|
1109
|
-
await scope.execute({
|
|
1110
|
-
meta,
|
|
1111
|
-
kind: "delete",
|
|
1112
|
-
storeName: def.relatedStoreName,
|
|
1113
|
-
key: extractKeyFromRow(child, childKeyPath)
|
|
1114
|
-
});
|
|
1115
|
-
}
|
|
1116
|
-
continue;
|
|
1117
|
-
}
|
|
1118
|
-
if (action === "setNull") {
|
|
1119
|
-
const patch = {};
|
|
1120
|
-
for (const targetField of def.targetFields) patch[targetField] = null;
|
|
1121
|
-
await scope.execute({
|
|
1122
|
-
meta,
|
|
1123
|
-
kind: "scan-write",
|
|
1124
|
-
storeName: def.relatedStoreName,
|
|
1125
|
-
write: "put-merged",
|
|
1126
|
-
patch,
|
|
1127
|
-
filter: childFilter
|
|
1128
|
-
});
|
|
1129
|
-
continue;
|
|
1130
|
-
}
|
|
1131
|
-
if (action === "setDefault") {
|
|
1132
|
-
const patch = buildSetDefaultPatch(contract, def);
|
|
1133
|
-
await validateSetDefaultPatch(scope, contract, modelName, def, patch, extractKeyFromRow(row, keyPath));
|
|
1134
|
-
await scope.execute({
|
|
1135
|
-
meta,
|
|
1136
|
-
kind: "scan-write",
|
|
1137
|
-
storeName: def.relatedStoreName,
|
|
1138
|
-
write: "put-merged",
|
|
1139
|
-
patch,
|
|
1140
|
-
filter: childFilter
|
|
1141
|
-
});
|
|
1142
|
-
continue;
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
async function executeDeleteWithReferentialActions(options) {
|
|
1147
|
-
const { executor, contract, modelName, key } = options;
|
|
1148
|
-
await withMutationScope(executor, collectDeleteStoreNames(contract, modelName), async (scope) => {
|
|
1149
|
-
const storeName = getStoreName(contract, modelName);
|
|
1150
|
-
const meta = makePlanMeta(contract);
|
|
1151
|
-
const row = (await scope.execute({
|
|
1152
|
-
meta,
|
|
1153
|
-
kind: "key-get",
|
|
1154
|
-
storeName,
|
|
1155
|
-
key
|
|
1156
|
-
}))[0];
|
|
1157
|
-
if (!row) return [];
|
|
1158
|
-
await applyReferentialActionsForRow(scope, contract, modelName, row);
|
|
1159
|
-
await scope.execute({
|
|
1160
|
-
meta,
|
|
1161
|
-
kind: "delete",
|
|
1162
|
-
storeName,
|
|
1163
|
-
key
|
|
1164
|
-
});
|
|
1165
|
-
return [];
|
|
1166
|
-
});
|
|
1167
|
-
}
|
|
1168
|
-
async function executeDeleteAllWithReferentialActions(options) {
|
|
1169
|
-
const { executor, contract, modelName, filter } = options;
|
|
1170
|
-
return withMutationScope(executor, collectDeleteStoreNames(contract, modelName), async (scope) => {
|
|
1171
|
-
const storeName = getStoreName(contract, modelName);
|
|
1172
|
-
const meta = makePlanMeta(contract);
|
|
1173
|
-
const keyPath = getKeyPath(contract, modelName);
|
|
1174
|
-
const rows = await scope.execute({
|
|
1175
|
-
meta,
|
|
1176
|
-
kind: "cursor-scan",
|
|
1177
|
-
storeName,
|
|
1178
|
-
...filter !== void 0 ? { filter } : {}
|
|
1179
|
-
});
|
|
1180
|
-
for (const row of rows) {
|
|
1181
|
-
await applyReferentialActionsForRow(scope, contract, modelName, row);
|
|
1182
|
-
const key = extractKeyFromRow(row, keyPath);
|
|
1183
|
-
await scope.execute({
|
|
1184
|
-
meta,
|
|
1185
|
-
kind: "delete",
|
|
1186
|
-
storeName,
|
|
1187
|
-
key
|
|
1188
|
-
});
|
|
1189
|
-
}
|
|
1190
|
-
return rows;
|
|
1191
|
-
});
|
|
1192
|
-
}
|
|
1193
|
-
//#endregion
|
|
1194
|
-
export { keyToken as A, assertEnumValues as C, getRelation as D, getKeyPath as E, getStoreName as O, createMutationDefaultsCache as S, fieldValueToken as T, requireTransactionExecutor as _, collectScalarFkStoreNames as a, applyCreateDefaults as b, executeDeleteWithReferentialActions as c, executeScalarCreateAllWithFkValidation as d, executeScalarCreateWithFkValidation as f, hasScalarFkFields as g, hasNestedMutationCallbacks as h, collectOnUpdateEnforcementStoreNames as i, keyEquals$1 as k, executeNestedCreateMutation as l, hasEnforceableChildRelations as m, applyReferentialActionsForRowOnUpdate as n, executeBulkUpdateWithFkValidation as o, executeScalarUpdateWithFkValidation as p, collectDeleteStoreNames as r, executeDeleteAllWithReferentialActions as s, applyReferentialActionsForRow as t, executeNestedUpdateMutation as u, validateScalarFks as v, extractKeyFromRow as w, applyUpdateDefaults as x, withMutationScope as y };
|
|
1195
|
-
|
|
1196
|
-
//# sourceMappingURL=mutation-executor-SJqTuSbb.mjs.map
|