@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.
Files changed (37) hide show
  1. package/README.md +6 -0
  2. package/dist/client-auto.d.mts +2 -2
  3. package/dist/client-auto.d.mts.map +1 -1
  4. package/dist/client-auto.mjs +44 -3
  5. package/dist/client-auto.mjs.map +1 -1
  6. package/dist/client.d.mts +3 -2
  7. package/dist/client.mjs +3 -2
  8. package/dist/decimal-identity-D_1J9lPN.d.mts +21 -0
  9. package/dist/decimal-identity-D_1J9lPN.d.mts.map +1 -0
  10. package/dist/internal.d.mts +5 -4
  11. package/dist/internal.d.mts.map +1 -1
  12. package/dist/internal.mjs +2 -2
  13. package/dist/{managed-client-CxlNmERd.mjs → managed-client-DMk8ERfX.mjs} +12 -5
  14. package/dist/managed-client-DMk8ERfX.mjs.map +1 -0
  15. package/dist/{managed-client-CS78EjPu.d.mts → managed-client-eksFK4UF.d.mts} +32 -3
  16. package/dist/managed-client-eksFK4UF.d.mts.map +1 -0
  17. package/dist/mutation-executor-IuJmyWEG.mjs +2132 -0
  18. package/dist/mutation-executor-IuJmyWEG.mjs.map +1 -0
  19. package/dist/{executor-Bp-hdRg2.d.mts → mutation-scope-CE3GMR4X.d.mts} +79 -28
  20. package/dist/mutation-scope-CE3GMR4X.d.mts.map +1 -0
  21. package/dist/{idb-orm-BitJ-EMq.mjs → orm-transaction-BGx1eukw.mjs} +366 -558
  22. package/dist/orm-transaction-BGx1eukw.mjs.map +1 -0
  23. package/dist/{idb-orm-9XN3c9ly.d.mts → orm-transaction-B_ZJ92uY.d.mts} +40 -7
  24. package/dist/orm-transaction-B_ZJ92uY.d.mts.map +1 -0
  25. package/dist/orm.d.mts +13 -4
  26. package/dist/orm.d.mts.map +1 -1
  27. package/dist/orm.mjs +3 -3
  28. package/package.json +9 -9
  29. package/dist/executor-Bp-hdRg2.d.mts.map +0 -1
  30. package/dist/idb-orm-9XN3c9ly.d.mts.map +0 -1
  31. package/dist/idb-orm-BitJ-EMq.mjs.map +0 -1
  32. package/dist/managed-client-CS78EjPu.d.mts.map +0 -1
  33. package/dist/managed-client-CxlNmERd.mjs.map +0 -1
  34. package/dist/mutation-executor-SJqTuSbb.mjs +0 -1196
  35. package/dist/mutation-executor-SJqTuSbb.mjs.map +0 -1
  36. package/dist/mutation-scope-1hS0ogGa.d.mts +0 -33
  37. 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