@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
@@ -0,0 +1,2132 @@
1
+ import { AsyncIterableResult } from "@prisma/orm-framework/components/runtime";
2
+ import { domainModelsAtDefaultNamespace } from "@prisma/orm-framework/contract/types";
3
+ import { IdbExecuteError } from "@prisma-idb/driver-idb/runtime";
4
+ import { absentAsNull, compareDecimalValues, compareFieldValues, fieldValueToken, fieldValuesEqual, isValidIdbKey, keyEquals, keyEquals as keyEquals$1, keyToken, keyToken as keyToken$1, validateRecord } from "@prisma-idb/target-idb/runtime";
5
+ import { andExpr, assertValidDecimalFilter, evaluateFilter, fieldFilter, shorthandToFilterExpr } from "@prisma-idb/adapter-idb/runtime";
6
+ import { generateId } from "@prisma/orm-framework/ids/runtime";
7
+ import { keyPathFields } from "@prisma-idb/target-idb/pack";
8
+ //#region src/core/types.ts
9
+ /**
10
+ * Extract the `storeName` from a model's storage metadata at runtime.
11
+ * Falls back to the model name if `storeName` is absent.
12
+ */
13
+ function getStoreName(contract, modelName) {
14
+ return (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.storeName ?? modelName;
15
+ }
16
+ /**
17
+ * Extract the `keyPath` from a model's storage metadata at runtime.
18
+ * A single field name for the common case; an ordered array of field names
19
+ * for a compound primary key.
20
+ *
21
+ * @throws if the model is unknown or its storage has no `keyPath` — a
22
+ * validated contract always has one, so this only fires on a malformed or
23
+ * hand-built contract, where guessing a key name would silently read/write
24
+ * the wrong key.
25
+ */
26
+ function getKeyPath(contract, modelName) {
27
+ const keyPath = (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.keyPath;
28
+ if (keyPath === void 0) throw new Error(`Model "${modelName}" has no storage.keyPath in the contract.`);
29
+ return keyPath;
30
+ }
31
+ /**
32
+ * Extracts a row's primary key as an {@link IDBValidKey} — the scalar value
33
+ * at `row[keyPath]` for a single-field key, or an ordered array of the
34
+ * compound key's member field values (matching the array form IDB itself
35
+ * expects for a compound `keyPath`). The one construction site every
36
+ * key-get/put/delete/update call should route through, so compound-key field
37
+ * order stays consistent everywhere.
38
+ */
39
+ function extractKeyFromRow(row, keyPath) {
40
+ if (typeof keyPath === "string") return row[keyPath];
41
+ return keyPath.map((field) => row[field]);
42
+ }
43
+ /**
44
+ * Resolve a model's named relation to a {@link ContractReferenceRelation} at
45
+ * runtime, or `undefined` when the relation is absent or an embedded relation
46
+ * (no `on` join block). Used by `include()` to find the related model name and
47
+ * cardinality before building the child-accessor refinement.
48
+ */
49
+ function getRelation(contract, modelName, relName) {
50
+ const relation = domainModelsAtDefaultNamespace(contract.domain)[modelName]?.relations?.[relName];
51
+ if (relation === void 0 || !("on" in relation)) return void 0;
52
+ return relation;
53
+ }
54
+ //#endregion
55
+ //#region src/core/planner/catalog.ts
56
+ const catalogs = /* @__PURE__ */ new WeakMap();
57
+ /**
58
+ * Build a catalog once per contract identity, store name and model name.
59
+ * Contracts are immutable.
60
+ *
61
+ * Several models can share one store, and each reads the rows differently.
62
+ * Pass the model that owns the request. Omit `modelName` for a store that no
63
+ * app model owns, such as an extension's own store: it has no fields to fill.
64
+ */
65
+ function buildCatalog(contract, storeName, modelName) {
66
+ const cacheKey = `${storeName}\0${modelName ?? ""}`;
67
+ const cached = catalogs.get(contract)?.get(cacheKey);
68
+ if (cached) return cached;
69
+ const store = contract.storage.stores[storeName];
70
+ if (!store) throw new Error(`Store "${storeName}" is not in the contract`);
71
+ const fields = {};
72
+ let normalize = (row) => row;
73
+ if (modelName !== void 0) {
74
+ const model = domainModelsAtDefaultNamespace(contract.domain)[modelName];
75
+ if (model === void 0) throw new Error(`Model "${modelName}" is not in the contract`);
76
+ if (getStoreName(contract, modelName) !== storeName) throw new Error(`Model "${modelName}" is not stored in "${storeName}"`);
77
+ normalize = absentAsNull(contract, modelName);
78
+ for (const [name, field] of Object.entries(model.fields)) fields[name] = Object.freeze({
79
+ codecId: field.type.kind === "scalar" ? field.type.codecId : void 0,
80
+ nullable: field.nullable,
81
+ collection: Boolean(field.many) || field.dict === true
82
+ });
83
+ }
84
+ const catalog = Object.freeze({
85
+ storeName,
86
+ primaryKey: source(store.keyPath, true, false),
87
+ indexes: Object.freeze(Object.entries(store.indexes ?? {}).map(([name, index]) => source(index.keyPath, index.unique, index.multiEntry ?? false, name))),
88
+ fields: Object.freeze(fields),
89
+ absentAsNull: normalize
90
+ });
91
+ let byStore = catalogs.get(contract);
92
+ if (!byStore) {
93
+ byStore = /* @__PURE__ */ new Map();
94
+ catalogs.set(contract, byStore);
95
+ }
96
+ byStore.set(cacheKey, catalog);
97
+ return catalog;
98
+ }
99
+ /** Freeze a key source and its field order for deterministic access selection. */
100
+ function source(keyPath, unique, multiEntry, indexName) {
101
+ const path = typeof keyPath === "string" ? keyPath : Object.freeze([...keyPath]);
102
+ return Object.freeze({
103
+ keyPath: path,
104
+ fields: Object.freeze(typeof path === "string" ? [path] : [...path]),
105
+ unique,
106
+ multiEntry,
107
+ ...indexName === void 0 ? {} : { indexName }
108
+ });
109
+ }
110
+ //#endregion
111
+ //#region src/core/decimal-identity.ts
112
+ /** A legacy store or sync metadata set contains several spellings of one Decimal key. */
113
+ var IdbDecimalIdentityConflictError = class extends Error {
114
+ code = "DECIMAL_IDENTITY_CONFLICT";
115
+ constructor(modelName) {
116
+ super(`Several stored keys identify the same Decimal value in model "${modelName}". Resolve the conflicting records before retrying.`);
117
+ this.name = "IdbDecimalIdentityConflictError";
118
+ }
119
+ };
120
+ /** Whether a primary key contains a contract-declared Decimal field. */
121
+ function hasDecimalPrimaryKey(contract, modelName) {
122
+ const catalog = buildCatalog(contract, getStoreName(contract, modelName), modelName);
123
+ return hasDecimalFields(catalog, catalog.primaryKey);
124
+ }
125
+ /** Compare scalar or compound primary keys using Decimal codecs and native IDB equality elsewhere. */
126
+ function decimalKeysEqual(contract, modelName, left, right) {
127
+ const catalog = buildCatalog(contract, getStoreName(contract, modelName), modelName);
128
+ const leftParts = typeof catalog.primaryKey.keyPath === "string" ? [left] : left;
129
+ const rightParts = typeof catalog.primaryKey.keyPath === "string" ? [right] : right;
130
+ if (!Array.isArray(leftParts) || !Array.isArray(rightParts) || leftParts.length !== catalog.primaryKey.fields.length || rightParts.length !== leftParts.length) return false;
131
+ return catalog.primaryKey.fields.every((field, i) => valuesEqual(catalog, field, leftParts[i], rightParts[i]));
132
+ }
133
+ /** Keep an existing Decimal primary-key spelling when the supplied value identifies the same row. */
134
+ function preserveDecimalKeySpelling(contract, modelName, record, storedKey) {
135
+ const catalog = buildCatalog(contract, getStoreName(contract, modelName), modelName);
136
+ const parts = typeof catalog.primaryKey.keyPath === "string" ? [storedKey] : storedKey;
137
+ if (!Array.isArray(parts) || parts.length !== catalog.primaryKey.fields.length) return record;
138
+ const result = { ...record };
139
+ catalog.primaryKey.fields.forEach((field, i) => {
140
+ if (catalog.fields[field]?.codecId === "idb/decimal@1" && result[field] !== void 0 && valuesEqual(catalog, field, result[field], parts[i])) result[field] = parts[i];
141
+ });
142
+ return result;
143
+ }
144
+ /** Read the physical key first, then check Decimal aliases without choosing a legacy duplicate. */
145
+ async function resolveDecimalKey(scope, contract, modelName, key, snapshot) {
146
+ if (!isValidIdbKey(key)) return void 0;
147
+ const storeName = getStoreName(contract, modelName);
148
+ const decimal = hasDecimalPrimaryKey(contract, modelName);
149
+ if (decimal && !decimalKeysEqual(contract, modelName, key, key)) return void 0;
150
+ if (!snapshot) {
151
+ const [exact] = await scope.execute({
152
+ kind: "key-get",
153
+ storeName,
154
+ key
155
+ });
156
+ if (!decimal) return exact;
157
+ }
158
+ const matches = (snapshot ?? await scope.execute({
159
+ kind: "cursor-scan",
160
+ storeName
161
+ })).filter((row) => decimalKeysEqual(contract, modelName, extractKeyFromRow(row, buildCatalog(contract, storeName, modelName).primaryKey.keyPath), key));
162
+ if (matches.length > 1) throw new IdbDecimalIdentityConflictError(modelName);
163
+ return matches[0];
164
+ }
165
+ /** Whether this key source needs numeric equality instead of the native string index. */
166
+ function hasDecimalFields(catalog, source) {
167
+ return source.fields.some((field) => catalog.fields[field]?.codecId === "idb/decimal@1");
168
+ }
169
+ /** Compare one declared scalar without converting Decimal text through Number. */
170
+ function valuesEqual(catalog, field, left, right) {
171
+ return catalog.fields[field]?.codecId === "idb/decimal@1" ? typeof left === "string" && typeof right === "string" && compareDecimalValues(left, right) === 0 : isValidIdbKey(left) && isValidIdbKey(right) && keyEquals(left, right);
172
+ }
173
+ /** Numeric unique sources only; ordinary native indexes continue to enforce their own constraints. */
174
+ function decimalUniqueSources(catalog) {
175
+ return [catalog.primaryKey, ...catalog.indexes].filter((source) => source.unique && hasDecimalFields(catalog, source));
176
+ }
177
+ /** Match one index entry, excluding absent/null keys just as IndexedDB does. */
178
+ function sourceCollides(catalog, source, left, right) {
179
+ if (source.multiEntry && source.fields.length === 1) {
180
+ const field = source.fields[0];
181
+ const a = Array.isArray(left[field]) ? left[field] : [left[field]];
182
+ const b = Array.isArray(right[field]) ? right[field] : [right[field]];
183
+ return a.some((x) => b.some((y) => valuesEqual(catalog, field, x, y)));
184
+ }
185
+ return source.fields.every((field) => valuesEqual(catalog, field, left[field], right[field]));
186
+ }
187
+ /** Check new values against every other physical row inside the write transaction. */
188
+ async function assertUnique(scope, catalog, record, previousKey, kind, modelName, snapshot) {
189
+ const rows = snapshot ?? await scope.execute({
190
+ kind: "cursor-scan",
191
+ storeName: catalog.storeName
192
+ });
193
+ const previous = rows.find((row) => previousKey !== void 0 && keyEquals(extractKeyFromRow(row, catalog.primaryKey.keyPath), previousKey));
194
+ for (const source of decimalUniqueSources(catalog)) {
195
+ const matches = rows.filter((row) => sourceCollides(catalog, source, row, record));
196
+ if (matches.length > 1 || previous && rows.filter((row) => sourceCollides(catalog, source, row, previous)).length > 1) throw new IdbDecimalIdentityConflictError(modelName);
197
+ if (matches.some((row) => previousKey === void 0 || !keyEquals(extractKeyFromRow(row, catalog.primaryKey.keyPath), previousKey))) throw new IdbExecuteError({
198
+ code: kind === "add" ? "ADD_FAILED" : "PUT_FAILED",
199
+ planKind: kind,
200
+ storeName: catalog.storeName,
201
+ cause: new DOMException("Numerically equal Decimal key already exists", "ConstraintError")
202
+ }, "Unique constraint failed for a Decimal key");
203
+ }
204
+ }
205
+ const decimalModelCache = /* @__PURE__ */ new WeakMap();
206
+ /** Cache the contract models whose uniqueness needs more than native string indexes. */
207
+ function decimalModels(contract) {
208
+ const cached = decimalModelCache.get(contract);
209
+ if (cached) return cached;
210
+ const models = /* @__PURE__ */ new Map();
211
+ for (const modelName of Object.keys(domainModelsAtDefaultNamespace(contract.domain))) {
212
+ const store = getStoreName(contract, modelName);
213
+ if (decimalUniqueSources(buildCatalog(contract, store, modelName)).length > 0) models.set(store, modelName);
214
+ }
215
+ decimalModelCache.set(contract, models);
216
+ return models;
217
+ }
218
+ /** Wrap the transaction seam so nested, bulk and raw sync writes share the same identity rules. */
219
+ function withDecimalIdentityScope(scope, contract) {
220
+ const models = decimalModels(contract);
221
+ if (models.size === 0) return scope;
222
+ return {
223
+ commit: () => scope.commit(),
224
+ rollback: () => scope.rollback(),
225
+ async execute(plan) {
226
+ const modelName = models.get(plan.storeName);
227
+ if (modelName === void 0) return scope.execute(plan);
228
+ try {
229
+ return await executeDecimalWrite(scope, contract, modelName, plan);
230
+ } catch (error) {
231
+ scope.rollback();
232
+ throw error;
233
+ }
234
+ }
235
+ };
236
+ }
237
+ /** Resolve a keyed write or materialize a scan before enforcing its numeric constraints. */
238
+ async function executeDecimalWrite(scope, contract, modelName, plan, snapshot) {
239
+ const catalog = buildCatalog(contract, plan.storeName, modelName);
240
+ if (plan.kind === "scan-write") return executeDecimalScanWrite(scope, contract, modelName, plan);
241
+ if (plan.kind === "delete") {
242
+ if (!isValidIdbKey(plan.key)) return scope.execute(plan);
243
+ const row = await resolveDecimalKey(scope, contract, modelName, plan.key, snapshot);
244
+ return scope.execute({
245
+ ...plan,
246
+ key: row ? extractKeyFromRow(row, catalog.primaryKey.keyPath) : plan.key
247
+ });
248
+ }
249
+ if (plan.kind !== "add" && plan.kind !== "put" && plan.kind !== "update") return scope.execute(plan);
250
+ const key = plan.kind === "update" ? plan.key : extractKeyFromRow(plan.record, catalog.primaryKey.keyPath);
251
+ const previous = plan.kind === "add" ? void 0 : await resolveDecimalKey(scope, contract, modelName, key, snapshot);
252
+ const previousKey = previous && extractKeyFromRow(previous, catalog.primaryKey.keyPath);
253
+ const record = preserveDecimalKeySpelling(contract, modelName, plan.kind === "update" ? {
254
+ ...previous,
255
+ ...plan.patch
256
+ } : plan.record, previousKey);
257
+ await assertUnique(scope, catalog, record, previousKey, plan.kind, modelName, snapshot);
258
+ return scope.execute(plan.kind === "update" ? {
259
+ ...plan,
260
+ key: previousKey ?? plan.key,
261
+ patch: preserveDecimalKeySpelling(contract, modelName, plan.patch, previousKey)
262
+ } : {
263
+ ...plan,
264
+ record
265
+ });
266
+ }
267
+ /** Materialize preimages once so moving indexed fields cannot revisit a row. */
268
+ async function executeDecimalScanWrite(scope, contract, modelName, plan) {
269
+ const catalog = buildCatalog(contract, plan.storeName, modelName);
270
+ const { take, ...scan } = plan;
271
+ const rows = await scope.execute({
272
+ ...scan,
273
+ kind: "cursor-scan"
274
+ });
275
+ const seen = /* @__PURE__ */ new Set();
276
+ const matches = rows.filter((row) => {
277
+ const token = keyToken(extractKeyFromRow(row, catalog.primaryKey.keyPath));
278
+ if (seen.has(token)) return false;
279
+ seen.add(token);
280
+ return true;
281
+ }).slice(0, take);
282
+ if (matches.length === 0) return [];
283
+ const snapshot = !plan.indexName && !plan.range && !plan.filter ? [...rows] : await scope.execute({
284
+ kind: "cursor-scan",
285
+ storeName: plan.storeName
286
+ });
287
+ const result = [];
288
+ for (const row of matches) {
289
+ const key = extractKeyFromRow(row, catalog.primaryKey.keyPath);
290
+ const written = await executeDecimalWrite(scope, contract, modelName, plan.write === "delete" ? {
291
+ ...plan,
292
+ kind: "delete",
293
+ key
294
+ } : {
295
+ ...plan,
296
+ kind: "update",
297
+ key,
298
+ patch: plan.patch ?? {}
299
+ }, snapshot);
300
+ result.push(...written);
301
+ const index = snapshot.findIndex((stored) => keyEquals(extractKeyFromRow(stored, catalog.primaryKey.keyPath), key));
302
+ if (index >= 0) snapshot.splice(index, 1, ...plan.write === "delete" ? [] : written);
303
+ else if (plan.write !== "delete") snapshot.push(...written);
304
+ }
305
+ return result;
306
+ }
307
+ /** Decorate only Decimal-key mutations; all other queries retain their existing execution path. */
308
+ function withDecimalIdentityExecutor(contract, executor) {
309
+ const models = decimalModels(contract);
310
+ if (models.size === 0) return executor;
311
+ const inner = executor;
312
+ /** Only mutations of declared Decimal key sources need a transaction wrapper. */
313
+ const needsCheck = (plan) => {
314
+ if (![
315
+ "add",
316
+ "put",
317
+ "update",
318
+ "delete",
319
+ "scan-write"
320
+ ].includes(plan.kind)) return false;
321
+ return models.has(plan.storeName);
322
+ };
323
+ /** Keep validation and the write under the caller's transaction and sync interceptor. */
324
+ const transaction = async (stores, mode) => {
325
+ if (typeof inner.transaction !== "function") throw new Error("Decimal key mutations require a transaction-capable executor");
326
+ return withDecimalIdentityScope(await inner.transaction(stores, mode), contract);
327
+ };
328
+ return {
329
+ ...typeof inner.transaction === "function" ? { transaction } : {},
330
+ query(plan) {
331
+ const body = plan.idbPlan;
332
+ const ops = body.kind === "batch" ? body.ops : [body];
333
+ if (!ops.some(needsCheck)) return executor.query(plan);
334
+ return new AsyncIterableResult((async function* () {
335
+ const scope = await transaction(body.kind === "batch" ? [...body.storeNames] : [body.storeName], "readwrite");
336
+ try {
337
+ const rows = [];
338
+ for (const op of ops) rows.push(...await scope.execute(op));
339
+ await scope.commit();
340
+ for (const row of rows) yield row;
341
+ } catch (error) {
342
+ scope.rollback();
343
+ throw error;
344
+ }
345
+ })());
346
+ }
347
+ };
348
+ }
349
+ //#endregion
350
+ //#region src/core/query-shaping.ts
351
+ /**
352
+ * Combine accumulated filter expressions with AND.
353
+ *
354
+ * Returns `undefined` when no filter is present so callers can skip building a
355
+ * row-filter closure. Shared by {@link IdbStoreAccessorImpl} (top-level scans)
356
+ * and the relation loader (refined `include()` child scans).
357
+ */
358
+ function combineFilterExprs(filters) {
359
+ if (filters.length === 0) return void 0;
360
+ if (filters.length === 1) return filters[0];
361
+ return andExpr(filters);
362
+ }
363
+ /**
364
+ * Build an in-memory comparator from an `orderBy` spec (field → direction).
365
+ *
366
+ * Returns `undefined` when there is nothing to sort by. Compares fields in
367
+ * declaration order. Decimal fields use exact numeric order; other fields
368
+ * use {@link compareFieldValues}: the same order an
369
+ * IndexedDB index uses, so equal `Date`s tie and fall through to the next
370
+ * field, and `null`s sort last (first when descending).
371
+ */
372
+ function buildRowComparator(orderBy, fields = {}) {
373
+ if (orderBy === void 0) return void 0;
374
+ return (a, b) => {
375
+ for (const [field, dir] of Object.entries(orderBy)) {
376
+ const metadata = fields[field];
377
+ const decimal = metadata?.codecId === "idb/decimal@1" && !metadata.collection;
378
+ const present = a[field] != null && b[field] != null;
379
+ const cmp = decimal && present ? compareDecimalValues(a[field], b[field]) : compareFieldValues(a[field], b[field]);
380
+ if (cmp === 0) continue;
381
+ return dir === "desc" ? -cmp : cmp;
382
+ }
383
+ return 0;
384
+ };
385
+ }
386
+ //#endregion
387
+ //#region src/core/enum-validation.ts
388
+ const rulesByContract = /* @__PURE__ */ new WeakMap();
389
+ /** Resolve an enum reference against its declared namespace before validating writes. */
390
+ function resolveEnumRule(contract, field) {
391
+ const ref = field.valueSet;
392
+ if (ref === void 0 || ref.plane !== "domain" || ref.entityKind !== "enum") return void 0;
393
+ const contractEnum = contract.domain.namespaces[ref.namespaceId]?.enum?.[ref.entityName];
394
+ if (contractEnum === void 0) throw new Error(`Enum "${ref.entityName}" referenced by the contract is not declared in its domain.`);
395
+ return {
396
+ enumName: ref.entityName,
397
+ values: new Set(contractEnum.members.map((member) => member.value)),
398
+ nullable: field.nullable,
399
+ many: Boolean(field.many),
400
+ elementNullable: typeof field.many === "object" && field.many.elementNullable === true
401
+ };
402
+ }
403
+ /** Cache enum rules by contract identity and model to avoid rebuilding them for each write. */
404
+ function getModelEnumRules(contract, modelName) {
405
+ let byModel = rulesByContract.get(contract);
406
+ if (byModel === void 0) {
407
+ byModel = /* @__PURE__ */ new Map();
408
+ rulesByContract.set(contract, byModel);
409
+ }
410
+ const cached = byModel.get(modelName);
411
+ if (cached !== void 0) return cached;
412
+ const rules = /* @__PURE__ */ new Map();
413
+ const fields = domainModelsAtDefaultNamespace(contract.domain)[modelName]?.fields ?? {};
414
+ for (const [fieldName, field] of Object.entries(fields)) {
415
+ const rule = resolveEnumRule(contract, field);
416
+ if (rule !== void 0) rules.set(fieldName, rule);
417
+ }
418
+ byModel.set(modelName, rules);
419
+ return rules;
420
+ }
421
+ function describe(value) {
422
+ return typeof value === "string" ? JSON.stringify(value) : `${String(value)} (${typeof value})`;
423
+ }
424
+ /** Describe an invalid enum value together with the declared alternatives. */
425
+ function invalidValueError(modelName, fieldName, rule, value) {
426
+ const expected = [...rule.values].map((v) => JSON.stringify(v)).join(", ");
427
+ return /* @__PURE__ */ new Error(`Invalid value ${describe(value)} for enum field "${modelName}.${fieldName}". Expected one of enum "${rule.enumName}": ${expected}.`);
428
+ }
429
+ /**
430
+ * Throws if `data` sets an enum field to a value its enum doesn't declare.
431
+ * Fields left `undefined` are not checked. `null` is accepted only on an
432
+ * optional (`Role?`) field; a list (`Role[]`) field must be an array whose
433
+ * every element is a declared value, or `null` when the list allows nullable elements.
434
+ */
435
+ function assertEnumValues(contract, modelName, data) {
436
+ const rules = getModelEnumRules(contract, modelName);
437
+ if (rules.size === 0) return;
438
+ for (const [fieldName, rule] of rules) {
439
+ const value = data[fieldName];
440
+ if (value === void 0) continue;
441
+ if (value === null) {
442
+ if (rule.nullable) continue;
443
+ throw new Error(`Enum field "${modelName}.${fieldName}" is required and cannot be null.`);
444
+ }
445
+ if (rule.many) {
446
+ if (!Array.isArray(value)) throw new Error(`Enum list field "${modelName}.${fieldName}" expects an array of "${rule.enumName}" values, got ${describe(value)}.`);
447
+ for (const element of value) {
448
+ if (element === null && rule.elementNullable) continue;
449
+ if (!rule.values.has(element)) throw invalidValueError(modelName, fieldName, rule, element);
450
+ }
451
+ continue;
452
+ }
453
+ if (!rule.values.has(value)) throw invalidValueError(modelName, fieldName, rule, value);
454
+ }
455
+ }
456
+ //#endregion
457
+ //#region src/core/record-validation.ts
458
+ /** A local ORM create or update failed the contract's native record validation. */
459
+ var IdbRecordValidationError = class extends Error {
460
+ modelName;
461
+ operation;
462
+ issues;
463
+ constructor(modelName, operation, issues) {
464
+ super(`Invalid ${operation} record for "${modelName}": ${issues.join("; ")}`);
465
+ this.modelName = modelName;
466
+ this.operation = operation;
467
+ this.issues = issues;
468
+ this.name = "IdbRecordValidationError";
469
+ }
470
+ };
471
+ /** Validate final write data, after defaults and relation scalar values are resolved. */
472
+ function assertValidRecord(contract, modelName, operation, data) {
473
+ const keyPath = getKeyPath(contract, modelName);
474
+ const store = contract.storage.stores[getStoreName(contract, modelName)];
475
+ const optionalFields = operation === "create" && store?.autoIncrement && typeof keyPath === "string" && !Object.hasOwn(data, keyPath) ? [keyPath] : [];
476
+ const result = validateRecord(contract, modelName, data, {
477
+ partial: operation === "update",
478
+ optionalFields
479
+ });
480
+ if (!result.ok) throw new IdbRecordValidationError(modelName, operation, result.issues);
481
+ }
482
+ //#endregion
483
+ //#region src/core/mutation-defaults.ts
484
+ function createMutationDefaultsCache() {
485
+ return /* @__PURE__ */ new Map();
486
+ }
487
+ /** Generator ids whose value is shared across an entire top-level call. Every other id always regenerates. */
488
+ const CACHEABLE_GENERATOR_IDS = /* @__PURE__ */ new Set(["timestampNow"]);
489
+ /** Resolve one declared default with the same generator IDs as the SQL execution layer. */
490
+ function computeGeneratedValue(spec) {
491
+ const id = spec.id;
492
+ switch (id) {
493
+ case "timestampNow": return /* @__PURE__ */ new Date();
494
+ case "literal": return spec.params?.["value"];
495
+ case "uuidv4":
496
+ case "uuidv7":
497
+ case "cuid2": return generateId(spec.params !== void 0 ? {
498
+ id,
499
+ params: spec.params
500
+ } : { id });
501
+ default: throw new Error(`Unknown mutation-default generator id "${String(id)}"`);
502
+ }
503
+ }
504
+ /** Share query-stable defaults across one mutation while generating fresh IDs for each field. */
505
+ function resolveGeneratedValue(spec, cache) {
506
+ const id = spec.id;
507
+ if (!CACHEABLE_GENERATOR_IDS.has(id)) return computeGeneratedValue(spec);
508
+ const cached = cache.get(id);
509
+ if (cached !== void 0) return cached;
510
+ const value = computeGeneratedValue(spec);
511
+ cache.set(id, value);
512
+ return value;
513
+ }
514
+ /**
515
+ * Fills in every column with an `onCreate` default that `data` doesn't
516
+ * already set. Returns `data` unchanged (same reference) if there's nothing
517
+ * to apply.
518
+ */
519
+ function applyCreateDefaults(defaults, storeName, data, cache) {
520
+ if (!defaults || defaults.length === 0) return data;
521
+ let result = data;
522
+ for (const d of defaults) {
523
+ if (d.ref.entry !== storeName || !d.onCreate) continue;
524
+ if (result[d.ref.field] !== void 0) continue;
525
+ if (result === data) result = { ...data };
526
+ result[d.ref.field] = resolveGeneratedValue(d.onCreate, cache);
527
+ }
528
+ return result;
529
+ }
530
+ /**
531
+ * Completes a create record: applies the `onCreate` defaults, then stores
532
+ * `null` for every nullable field the record still lacks. IndexedDB has no
533
+ * column defaults, so this is what keeps stored rows complete while the create
534
+ * input leaves nullable fields optional, as Prisma's does. Call it after any
535
+ * relation values are resolved into `data`, and validate the result.
536
+ */
537
+ function completeCreateRecord(contract, modelName, data, cache) {
538
+ const withDefaults = applyCreateDefaults(contract.execution?.mutations.defaults, getStoreName(contract, modelName), data, cache);
539
+ return absentAsNull(contract, modelName)(withDefaults);
540
+ }
541
+ /**
542
+ * Fills in every column with an `onUpdate` default that `patch` doesn't
543
+ * already set. A patch with no defined values (empty, or every field
544
+ * explicitly `undefined`) is returned unchanged — no write means no default
545
+ * should fire either (matches SQL's "empty update payloads skip onUpdate
546
+ * defaults" rule).
547
+ */
548
+ function applyUpdateDefaults(defaults, storeName, patch, cache) {
549
+ if (Object.values(patch).every((v) => v === void 0)) return patch;
550
+ if (!defaults || defaults.length === 0) return patch;
551
+ let result = patch;
552
+ for (const d of defaults) {
553
+ if (d.ref.entry !== storeName || !d.onUpdate) continue;
554
+ if (result[d.ref.field] !== void 0) continue;
555
+ if (result === patch) result = { ...patch };
556
+ result[d.ref.field] = resolveGeneratedValue(d.onUpdate, cache);
557
+ }
558
+ return result;
559
+ }
560
+ //#endregion
561
+ //#region src/core/planner/plan.ts
562
+ const COST = {
563
+ UNIQUE_POINT: 0,
564
+ PRIMARY_POINT: 1,
565
+ INDEX_POINT: 2,
566
+ PREFIX_POINT: 3,
567
+ TWO_SIDED_RANGE: 4,
568
+ ONE_SIDED_RANGE: 5,
569
+ ORDER_ONLY_SCAN: 6
570
+ };
571
+ const KEY_PARTIAL_CODECS = /* @__PURE__ */ new Set(["idb/double@1", "idb/date@1"]);
572
+ const KEY_CODECS = /* @__PURE__ */ new Set([
573
+ "idb/string@1",
574
+ "idb/int32@1",
575
+ "idb/double@1",
576
+ "idb/date@1",
577
+ "idb/bytes@1"
578
+ ]);
579
+ /** Choose the cheapest safe access without opening IndexedDB or probing cardinality. */
580
+ function planQuery(catalog, request) {
581
+ if (request.where !== void 0) assertValidDecimalFilter(request.where, catalog.fields);
582
+ const normalized = normalize(request.where);
583
+ if (normalized.empty) return {
584
+ access: { kind: "empty" },
585
+ exact: true
586
+ };
587
+ const orders = Object.entries(request.orderBy ?? {});
588
+ if (normalized.excluded || request.hasRelationFilter || orders.length > 1) return full(false);
589
+ const constraints = /* @__PURE__ */ new Map();
590
+ for (const atom of normalized.atoms) {
591
+ let constraint = constraints.get(atom.field);
592
+ if (!constraint) {
593
+ constraint = { consumed: 0 };
594
+ constraints.set(atom.field, constraint);
595
+ }
596
+ addAtom(constraint, atom, catalog.fields[atom.field]);
597
+ }
598
+ for (const [name, constraint] of constraints) {
599
+ if (keyPartial(catalog.fields[name]) && !constraint.rejectsNonKeys) {
600
+ delete constraint.lower;
601
+ delete constraint.upper;
602
+ constraint.consumed = 0;
603
+ }
604
+ if (contradictory(constraint)) return {
605
+ access: { kind: "empty" },
606
+ exact: true
607
+ };
608
+ }
609
+ const sources = [catalog.primaryKey, ...catalog.indexes];
610
+ const candidates = sources.flatMap((source, declaration) => {
611
+ const candidate = filteringCandidate(catalog, source, constraints, declaration);
612
+ return candidate ? [candidate] : [];
613
+ });
614
+ candidates.sort((a, b) => a.cost - b.cost || a.ranges.length - b.ranges.length || Number(a.source.indexName !== void 0) - Number(b.source.indexName !== void 0) || a.declaration - b.declaration);
615
+ let selected = candidates[0];
616
+ let direction;
617
+ if (orders.length === 1 && request.take !== void 0) {
618
+ const [field, order] = orders[0];
619
+ if (selected) {
620
+ if (catalog.fields[field]?.codecId !== "idb/decimal@1" && sourceOrdersField(selected.source, field, constraints)) direction = order === "asc" ? "next" : "prev";
621
+ } else {
622
+ const ordering = sources.find((source) => !source.multiEntry && singleFieldSource(source, field) && completeKeyField(catalog.fields[field]));
623
+ if (ordering) {
624
+ selected = {
625
+ source: ordering,
626
+ ranges: [void 0],
627
+ cost: COST.ORDER_ONLY_SCAN,
628
+ consumed: 0,
629
+ declaration: sources.indexOf(ordering)
630
+ };
631
+ direction = order === "asc" ? "next" : "prev";
632
+ }
633
+ }
634
+ }
635
+ if (!selected) {
636
+ const partialOrder = orders.length === 1 && request.take !== void 0 && keyPartial(catalog.fields[orders[0][0]]);
637
+ return full(normalized.atoms.length + normalized.residual === 0 && !partialOrder);
638
+ }
639
+ return {
640
+ access: {
641
+ kind: "ranges",
642
+ source: selected.source,
643
+ ranges: selected.ranges
644
+ },
645
+ exact: selected.consumed === normalized.atoms.length && normalized.residual === 0,
646
+ ...direction === void 0 ? {} : { direction }
647
+ };
648
+ }
649
+ function full(exact) {
650
+ return {
651
+ access: { kind: "full" },
652
+ exact
653
+ };
654
+ }
655
+ /** Flatten supported conjunctions and same-field unions while retaining unsupported predicates. */
656
+ function normalize(where) {
657
+ const result = {
658
+ atoms: [],
659
+ residual: 0,
660
+ excluded: false,
661
+ empty: false
662
+ };
663
+ /** Collect usable atoms and mark filters that require the full-row path. */
664
+ function visit(expr) {
665
+ switch (expr.kind) {
666
+ case "field":
667
+ if (expr.op === "in" && (!Array.isArray(expr.value) || expr.value.length === 0)) result.empty = true;
668
+ result.atoms.push(expr);
669
+ break;
670
+ case "and":
671
+ expr.exprs.forEach(visit);
672
+ break;
673
+ case "or": {
674
+ if (expr.exprs.length === 0) {
675
+ result.empty = true;
676
+ break;
677
+ }
678
+ const branches = expr.exprs;
679
+ const first = branches[0];
680
+ if (first.kind !== "field" || !branches.every((branch) => branch.kind === "field" && branch.field === first.field && (branch.op === "eq" || branch.op === "in"))) {
681
+ result.excluded = true;
682
+ break;
683
+ }
684
+ const values = branches.flatMap((branch) => {
685
+ const atom = branch;
686
+ return atom.op === "eq" ? [atom.value] : Array.isArray(atom.value) ? atom.value : [];
687
+ });
688
+ visit({
689
+ kind: "field",
690
+ field: first.field,
691
+ op: "in",
692
+ value: values
693
+ });
694
+ break;
695
+ }
696
+ case "not":
697
+ result.excluded = true;
698
+ break;
699
+ case "null-check": result.residual++;
700
+ }
701
+ }
702
+ if (where) visit(where);
703
+ return result;
704
+ }
705
+ function keyTyped(field) {
706
+ return field !== void 0 && !field.collection && KEY_CODECS.has(field.codecId ?? "");
707
+ }
708
+ function keyPartial(field) {
709
+ return KEY_PARTIAL_CODECS.has(field?.codecId ?? "");
710
+ }
711
+ function completeKeyField(field, constraint) {
712
+ return keyTyped(field) && field?.nullable === false && (!keyPartial(field) || constraint?.rejectsNonKeys === true);
713
+ }
714
+ /** A compound cursor orders a suffix only within one fixed preceding key. */
715
+ function sourceOrdersField(source, field, constraints) {
716
+ const position = source.fields.indexOf(field);
717
+ return position >= 0 && source.fields.slice(0, position).every((name) => constraints.get(name)?.points?.length === 1);
718
+ }
719
+ function singleFieldSource(source, field) {
720
+ return source.fields.length === 1 && source.fields[0] === field;
721
+ }
722
+ /** Intersect one supported predicate with a field constraint without narrowing unsafe values. */
723
+ function addAtom(constraint, atom, field) {
724
+ if (field?.codecId === "idb/decimal@1") return;
725
+ if (atom.op === "eq" || atom.op === "in") {
726
+ const values = atom.op === "eq" ? [atom.value] : Array.isArray(atom.value) ? atom.value : [];
727
+ if (!values.every(isValidIdbKey)) return;
728
+ const points = uniqueKeys(values);
729
+ constraint.points = constraint.points === void 0 ? points : constraint.points.filter((point) => points.some((value) => keyEquals(point, value)));
730
+ constraint.rejectsNonKeys = true;
731
+ constraint.consumed++;
732
+ return;
733
+ }
734
+ if (!keyTyped(field)) return;
735
+ if (atom.op === "startsWith") {
736
+ if (field?.codecId !== "idb/string@1" || typeof atom.value !== "string") return;
737
+ intersectBound(constraint, "lower", {
738
+ value: atom.value,
739
+ open: false
740
+ });
741
+ const upper = stringSuccessor(atom.value);
742
+ if (upper !== void 0) intersectBound(constraint, "upper", {
743
+ value: upper,
744
+ open: true
745
+ });
746
+ constraint.startsWith = true;
747
+ constraint.consumed++;
748
+ } else if ([
749
+ "gt",
750
+ "gte",
751
+ "lt",
752
+ "lte"
753
+ ].includes(atom.op) && isValidIdbKey(atom.value)) {
754
+ const side = atom.op === "gt" || atom.op === "gte" ? "lower" : "upper";
755
+ const strict = atom.op === "gt" || atom.op === "lt";
756
+ intersectBound(constraint, side, {
757
+ value: atom.value,
758
+ open: strict
759
+ });
760
+ if (strict) constraint.rejectsNonKeys = true;
761
+ constraint.consumed++;
762
+ }
763
+ }
764
+ function uniqueKeys(values) {
765
+ const sorted = [...values].sort(compareFieldValues);
766
+ return sorted.filter((value, i) => i === 0 || compareFieldValues(sorted[i - 1], value) !== 0);
767
+ }
768
+ /** Keep the tighter endpoint when several comparisons constrain the same field. */
769
+ function intersectBound(constraint, side, bound) {
770
+ const previous = constraint[side];
771
+ const comparison = previous === void 0 ? 0 : compareFieldValues(bound.value, previous.value);
772
+ if (!previous || (side === "lower" ? comparison > 0 : comparison < 0) || comparison === 0 && bound.open) constraint[side] = bound;
773
+ }
774
+ /** Check a point against both endpoints, including open-bound equality. */
775
+ function insideBounds(value, constraint) {
776
+ const lower = constraint.lower;
777
+ const upper = constraint.upper;
778
+ if (lower && (compareFieldValues(value, lower.value) < 0 || lower.open && keyEquals(value, lower.value))) return false;
779
+ if (upper && (compareFieldValues(value, upper.value) > 0 || upper.open && keyEquals(value, upper.value))) return false;
780
+ return true;
781
+ }
782
+ /** Discard points outside the bounds and detect ranges that cannot match. */
783
+ function contradictory(constraint) {
784
+ if (constraint.points) {
785
+ constraint.points = constraint.points.filter((value) => insideBounds(value, constraint));
786
+ return constraint.points.length === 0;
787
+ }
788
+ if (!constraint.lower || !constraint.upper) return false;
789
+ const comparison = compareFieldValues(constraint.lower.value, constraint.upper.value);
790
+ return comparison > 0 || comparison === 0 && (constraint.lower.open || constraint.upper.open);
791
+ }
792
+ /** Build a safe source range only when its index contains every possible match. */
793
+ function filteringCandidate(catalog, source, constraints, declaration) {
794
+ if (source.multiEntry || source.fields.length === 0) return void 0;
795
+ if (source.fields.some((name) => keyPartial(catalog.fields[name]) && !constraints.get(name)?.rejectsNonKeys)) return void 0;
796
+ let prefixes = [[]];
797
+ let consumed = 0;
798
+ let position = 0;
799
+ while (position < source.fields.length) {
800
+ const constraint = constraints.get(source.fields[position]);
801
+ if (constraint?.points === void 0) break;
802
+ if (prefixes.length * constraint.points.length > 1024) return void 0;
803
+ prefixes = prefixes.flatMap((prefix) => constraint.points.map((point) => [...prefix, point]));
804
+ consumed += constraint.consumed;
805
+ position++;
806
+ }
807
+ const constraint = constraints.get(source.fields[position] ?? "");
808
+ const hasRange = constraint !== void 0 && (constraint.lower !== void 0 || constraint.upper !== void 0);
809
+ if (position === 0 && !hasRange) return void 0;
810
+ const used = position + Number(hasRange);
811
+ if (!source.fields.slice(used).every((field) => completeKeyField(catalog.fields[field], constraints.get(field)))) return void 0;
812
+ const compound = typeof source.keyPath !== "string";
813
+ if (position === source.fields.length) return {
814
+ source,
815
+ ranges: prefixes.map((prefix) => ({
816
+ kind: "only",
817
+ key: compound ? prefix : prefix[0]
818
+ })),
819
+ cost: source.indexName !== void 0 && source.unique ? COST.UNIQUE_POINT : source.indexName === void 0 ? COST.PRIMARY_POINT : COST.INDEX_POINT,
820
+ consumed,
821
+ declaration
822
+ };
823
+ if (hasRange) consumed += constraint.consumed;
824
+ const ranges = prefixes.map((prefix) => compound ? compoundRange(prefix, constraint, source.fields.length - used) : scalarRange(constraint));
825
+ if (ranges.some((range) => range.kind === "bound" && (compareFieldValues(range.lower, range.upper) > 0 || keyEquals(range.lower, range.upper) && (range.lowerOpen || range.upperOpen)))) return void 0;
826
+ return {
827
+ source,
828
+ ranges,
829
+ cost: hasRange ? constraint.lower && constraint.upper && !constraint.startsWith ? COST.TWO_SIDED_RANGE : COST.ONE_SIDED_RANGE : COST.PREFIX_POINT,
830
+ consumed,
831
+ declaration
832
+ };
833
+ }
834
+ /** Convert scalar endpoints into the matching driver range descriptor. */
835
+ function scalarRange(constraint) {
836
+ if (constraint.lower && constraint.upper) return {
837
+ kind: "bound",
838
+ lower: constraint.lower.value,
839
+ upper: constraint.upper.value,
840
+ lowerOpen: constraint.lower.open,
841
+ upperOpen: constraint.upper.open
842
+ };
843
+ if (constraint.lower) return {
844
+ kind: "lower",
845
+ key: constraint.lower.value,
846
+ open: constraint.lower.open
847
+ };
848
+ return {
849
+ kind: "upper",
850
+ key: constraint.upper.value,
851
+ open: constraint.upper.open
852
+ };
853
+ }
854
+ /** Bound a compound prefix and suffix using IndexedDB key ordering. */
855
+ function compoundRange(prefix, constraint, trailing) {
856
+ const lower = constraint?.lower;
857
+ const upper = constraint?.upper;
858
+ return {
859
+ kind: "bound",
860
+ lower: lower ? [
861
+ ...prefix,
862
+ lower.value,
863
+ ...lower.open && trailing > 0 ? [[]] : []
864
+ ] : prefix.length ? prefix : [-Infinity],
865
+ upper: upper ? [
866
+ ...prefix,
867
+ upper.value,
868
+ ...!upper.open && trailing > 0 ? [[]] : []
869
+ ] : [...prefix, []],
870
+ lowerOpen: lower?.open ?? false,
871
+ upperOpen: upper ? upper.open || trailing > 0 : true
872
+ };
873
+ }
874
+ /** Find the exclusive upper bound of a UTF-16 prefix, if one exists. */
875
+ function stringSuccessor(prefix) {
876
+ for (let i = prefix.length - 1; i >= 0; i--) {
877
+ const code = prefix.charCodeAt(i);
878
+ if (code < 65535) return prefix.slice(0, i) + String.fromCharCode(code + 1);
879
+ }
880
+ }
881
+ //#endregion
882
+ //#region src/core/planner/lower.ts
883
+ /**
884
+ * The row filter shared by every lowered read and write. The driver runs it on stored rows, so it
885
+ * reads each row as the contract describes it first: a filter on `null` matches a row that
886
+ * lacks the field.
887
+ */
888
+ function rowFilter(catalog, request, additionalFilter) {
889
+ return (stored) => {
890
+ const row = catalog.absentAsNull(stored);
891
+ return (request.where === void 0 || evaluateFilter(request.where, row, catalog.fields)) && (additionalFilter === void 0 || additionalFilter(row));
892
+ };
893
+ }
894
+ /**
895
+ * Lower row access without creating IDBKeyRange objects or awaiting work.
896
+ * Run `finish` after the executor has materialized all returned rows.
897
+ * Without `orderBy`, row order and pagination selection are unspecified.
898
+ * Rows tied on every ordering field may appear in any order, including at
899
+ * pagination boundaries. Add a unique ordering field for deterministic pages.
900
+ */
901
+ function lowerRows(catalog, logical, meta, request, additionalFilter) {
902
+ const filter = request.where === void 0 && additionalFilter === void 0 ? void 0 : rowFilter(catalog, request, additionalFilter);
903
+ const comparator = logical.direction === void 0 ? buildRowComparator(request.orderBy, catalog.fields) : void 0;
904
+ const base = {
905
+ meta,
906
+ storeName: catalog.storeName
907
+ };
908
+ const pagination = {
909
+ ...request.skip === void 0 ? {} : { skip: request.skip },
910
+ ...request.take === void 0 ? {} : { take: request.take }
911
+ };
912
+ let shapedByDriver = false;
913
+ let idbPlan;
914
+ switch (logical.access.kind) {
915
+ case "empty":
916
+ idbPlan = {
917
+ meta,
918
+ kind: "batch",
919
+ storeNames: [catalog.storeName],
920
+ ops: []
921
+ };
922
+ break;
923
+ case "full":
924
+ idbPlan = {
925
+ ...base,
926
+ kind: "cursor-scan",
927
+ ...filter === void 0 ? {} : { filter },
928
+ ...comparator === void 0 ? {} : { comparator },
929
+ ...pagination
930
+ };
931
+ shapedByDriver = true;
932
+ break;
933
+ case "ranges": {
934
+ const { source } = logical.access;
935
+ const ranges = [...logical.access.ranges];
936
+ if (logical.direction === "prev") ranges.reverse();
937
+ const single = ranges.length === 1;
938
+ const ops = ranges.map((range) => {
939
+ if (range?.kind === "only" && source.indexName === void 0) return {
940
+ ...base,
941
+ kind: "key-get",
942
+ key: range.key
943
+ };
944
+ const access = {
945
+ ...base,
946
+ ...source.indexName === void 0 ? {} : { indexName: source.indexName },
947
+ ...range === void 0 ? {} : { range }
948
+ };
949
+ if (!single && logical.direction !== void 0 && request.take !== void 0) return {
950
+ ...access,
951
+ kind: "cursor-scan",
952
+ direction: logical.direction,
953
+ ...filter === void 0 ? {} : { filter },
954
+ take: (request.skip ?? 0) + request.take
955
+ };
956
+ if (single && request.take !== void 0) {
957
+ shapedByDriver = true;
958
+ return {
959
+ ...access,
960
+ kind: "cursor-scan",
961
+ ...filter === void 0 ? {} : { filter },
962
+ ...logical.direction === void 0 ? {} : { direction: logical.direction },
963
+ ...comparator === void 0 ? {} : { comparator },
964
+ ...pagination
965
+ };
966
+ }
967
+ if (logical.direction === "prev") return {
968
+ ...access,
969
+ kind: "cursor-scan",
970
+ direction: "prev",
971
+ ...filter === void 0 ? {} : { filter }
972
+ };
973
+ return {
974
+ ...access,
975
+ kind: "get-all"
976
+ };
977
+ });
978
+ idbPlan = single ? ops[0] : {
979
+ meta,
980
+ kind: "batch",
981
+ storeNames: [catalog.storeName],
982
+ ops
983
+ };
984
+ break;
985
+ }
986
+ }
987
+ return {
988
+ idbPlan,
989
+ /** Normalize returned rows and apply filtering and shaping not already handled by the driver. */
990
+ finish(rows) {
991
+ let result = rows.map(catalog.absentAsNull);
992
+ if (filter !== void 0) result = result.filter(filter);
993
+ if (!shapedByDriver) {
994
+ if (comparator !== void 0) result.sort(comparator);
995
+ const skip = request.skip ?? 0;
996
+ result = result.slice(skip, request.take === void 0 ? void 0 : skip + request.take);
997
+ }
998
+ return result;
999
+ }
1000
+ };
1001
+ }
1002
+ /**
1003
+ * Lower a row count. An `exact` plan counts index entries and loads no rows.
1004
+ * The planner emits disjoint ranges on a non-multi-entry source, so each row
1005
+ * is counted once. Anything else counts the rows that pass the filter.
1006
+ * Pagination and `additionalFilter` always take the row path.
1007
+ */
1008
+ function lowerCount(catalog, logical, meta, request, additionalFilter) {
1009
+ const paginated = request.skip !== void 0 || request.take !== void 0;
1010
+ if (!logical.exact || paginated || additionalFilter !== void 0) {
1011
+ const lowered = lowerRows(catalog, logical, meta, request, additionalFilter);
1012
+ return {
1013
+ idbPlan: lowered.idbPlan,
1014
+ finish: (rows) => lowered.finish(rows).length
1015
+ };
1016
+ }
1017
+ return {
1018
+ idbPlan: combine(catalog, meta, accessOps(catalog, meta, logical, (target) => ({
1019
+ ...target,
1020
+ kind: "count"
1021
+ }))),
1022
+ finish: (rows) => rows.reduce((total, row) => total + row["count"], 0)
1023
+ };
1024
+ }
1025
+ /**
1026
+ * Lower an existence check. An `exact` plan reads one primary key per range
1027
+ * and loads no rows. Anything else stops each range at its first row that
1028
+ * passes the filter.
1029
+ */
1030
+ function lowerExists(catalog, logical, meta, request, additionalFilter) {
1031
+ if (logical.exact && additionalFilter === void 0) return {
1032
+ idbPlan: combine(catalog, meta, accessOps(catalog, meta, logical, (target) => ({
1033
+ ...target,
1034
+ kind: "keys",
1035
+ take: 1
1036
+ }))),
1037
+ finish: (rows) => rows.length > 0
1038
+ };
1039
+ const filter = rowFilter(catalog, request, additionalFilter);
1040
+ return {
1041
+ idbPlan: combine(catalog, meta, accessOps(catalog, meta, logical, (target) => ({
1042
+ ...target,
1043
+ kind: "cursor-scan",
1044
+ filter,
1045
+ take: 1
1046
+ }))),
1047
+ finish: (rows) => rows.some(filter)
1048
+ };
1049
+ }
1050
+ /**
1051
+ * Lower writes through the same access paths as reads. If the patch touches
1052
+ * the walked key, collect every range first so moved rows cannot enter a
1053
+ * later range or change the cursor's position. Multiple ranges with a limit
1054
+ * also collect first, because the limit applies to their combined matches.
1055
+ */
1056
+ function lowerWrite(catalog, logical, meta, request, additionalFilter) {
1057
+ const { access } = logical;
1058
+ const walkedFields = access.kind === "ranges" ? access.source.fields : catalog.primaryKey.fields;
1059
+ const movesKey = request.write === "put-merged" && walkedFields.some((field) => field in (request.patch ?? {}));
1060
+ const limitedRanges = access.kind === "ranges" && access.ranges.length > 1 && request.take !== void 0;
1061
+ if (movesKey || limitedRanges) return {
1062
+ kind: "collect",
1063
+ rows: lowerRows(catalog, logical, meta, request, additionalFilter)
1064
+ };
1065
+ const filter = rowFilter(catalog, request, additionalFilter);
1066
+ return {
1067
+ kind: "scan",
1068
+ idbPlan: combine(catalog, meta, accessOps(catalog, meta, logical, (target) => ({
1069
+ ...target,
1070
+ kind: "scan-write",
1071
+ write: request.write,
1072
+ filter,
1073
+ ...request.patch === void 0 ? {} : { patch: request.patch },
1074
+ ...request.take === void 0 ? {} : { take: request.take }
1075
+ })))
1076
+ };
1077
+ }
1078
+ /** One operation per planned range, in ascending key order. */
1079
+ function accessOps(catalog, meta, logical, build) {
1080
+ const { access } = logical;
1081
+ const base = {
1082
+ meta,
1083
+ storeName: catalog.storeName
1084
+ };
1085
+ switch (access.kind) {
1086
+ case "empty": return [];
1087
+ case "full": return [build(base)];
1088
+ case "ranges": {
1089
+ const { indexName } = access.source;
1090
+ return access.ranges.map((range) => build({
1091
+ ...base,
1092
+ ...indexName === void 0 ? {} : { indexName },
1093
+ ...range === void 0 ? {} : { range }
1094
+ }));
1095
+ }
1096
+ }
1097
+ }
1098
+ function combine(catalog, meta, ops) {
1099
+ return ops.length === 1 ? ops[0] : {
1100
+ meta,
1101
+ kind: "batch",
1102
+ storeNames: [catalog.storeName],
1103
+ ops
1104
+ };
1105
+ }
1106
+ //#endregion
1107
+ //#region src/core/mutation-scope.ts
1108
+ /**
1109
+ * Run a callback inside a single multi-store IDB readwrite transaction.
1110
+ *
1111
+ * Opens the transaction, passes the `IdbTransactionScope` to `run`, then:
1112
+ * - On success: awaits `scope.commit()` (waits for `tx.oncomplete`).
1113
+ * - On error: calls `scope.rollback()` and rethrows.
1114
+ *
1115
+ * The callback receives the low-level scope, not the ORM accessor.
1116
+ * Use this for multi-store atomic writes that span several object stores.
1117
+ */
1118
+ async function withMutationScope(executor, storeNames, run) {
1119
+ const tx = await executor.transaction(storeNames, "readwrite");
1120
+ try {
1121
+ const result = await run(tx);
1122
+ await tx.commit();
1123
+ return result;
1124
+ } catch (err) {
1125
+ tx.rollback();
1126
+ throw err;
1127
+ }
1128
+ }
1129
+ //#endregion
1130
+ //#region src/core/relation-mutator.ts
1131
+ /** Build the relation descriptors consumed by the nested-write executor. */
1132
+ function createRelationMutator() {
1133
+ return {
1134
+ /** Normalize a single nested record or a list into one create descriptor. */
1135
+ create(data) {
1136
+ return {
1137
+ kind: "create",
1138
+ data: Array.isArray(data) ? [...data] : [data]
1139
+ };
1140
+ },
1141
+ connect(criteria) {
1142
+ return {
1143
+ kind: "connect",
1144
+ criteria: Array.isArray(criteria) ? [...criteria] : [criteria]
1145
+ };
1146
+ },
1147
+ disconnect(criteria) {
1148
+ if (!criteria) return { kind: "disconnect" };
1149
+ return {
1150
+ kind: "disconnect",
1151
+ criteria: [...criteria]
1152
+ };
1153
+ }
1154
+ };
1155
+ }
1156
+ function isRelationMutationDescriptor(value) {
1157
+ if (!value || typeof value !== "object") return false;
1158
+ const candidate = value;
1159
+ return candidate.kind === "create" || candidate.kind === "connect" || candidate.kind === "disconnect";
1160
+ }
1161
+ function isRelationMutationCallback(value) {
1162
+ return typeof value === "function";
1163
+ }
1164
+ //#endregion
1165
+ //#region src/core/mutation-executor.ts
1166
+ /** Plan metadata for the raw plans the mutation executor issues itself, outside any accessor's grouping key. */
1167
+ function makePlanMeta(contract) {
1168
+ return {
1169
+ target: "idb",
1170
+ storageHash: contract.storage.storageHash,
1171
+ lane: "idb-mutation-executor",
1172
+ annotations: { groupingKey: "nested" }
1173
+ };
1174
+ }
1175
+ const relationDefsCache = /* @__PURE__ */ new WeakMap();
1176
+ /** Cache reference-relation metadata by contract identity and model. */
1177
+ function getRelationDefinitions(contract, modelName) {
1178
+ let perContract = relationDefsCache.get(contract);
1179
+ if (!perContract) {
1180
+ perContract = /* @__PURE__ */ new Map();
1181
+ relationDefsCache.set(contract, perContract);
1182
+ }
1183
+ const cached = perContract.get(modelName);
1184
+ if (cached) return cached;
1185
+ const model = domainModelsAtDefaultNamespace(contract.domain)[modelName];
1186
+ if (!model) {
1187
+ perContract.set(modelName, []);
1188
+ return [];
1189
+ }
1190
+ const defs = [];
1191
+ for (const [relationName, rawRelation] of Object.entries(model.relations)) {
1192
+ if (!rawRelation || typeof rawRelation !== "object" || !("on" in rawRelation)) continue;
1193
+ const relation = rawRelation;
1194
+ const relatedModelName = relation.to.model;
1195
+ const relatedStoreName = getStoreName(contract, relatedModelName);
1196
+ defs.push({
1197
+ relationName,
1198
+ relatedModelName,
1199
+ relatedStoreName,
1200
+ cardinality: relation.cardinality,
1201
+ localFields: relation.on.localFields,
1202
+ targetFields: relation.on.targetFields
1203
+ });
1204
+ }
1205
+ perContract.set(modelName, defs);
1206
+ return defs;
1207
+ }
1208
+ /**
1209
+ * Returns true if `data` contains at least one field that is both a known
1210
+ * relation name for `modelName` and a function (a mutation callback).
1211
+ */
1212
+ function hasNestedMutationCallbacks(contract, modelName, data) {
1213
+ const relationNames = new Set(getRelationDefinitions(contract, modelName).map((r) => r.relationName));
1214
+ for (const [fieldName, value] of Object.entries(data)) if (relationNames.has(fieldName) && isRelationMutationCallback(value)) return true;
1215
+ return false;
1216
+ }
1217
+ /**
1218
+ * Guards that the executor supports multi-store transactions.
1219
+ * Throws a clear error if `transaction()` is not available — the user must
1220
+ * use IdbRuntime (createIdbRuntime / createAutoMigratingIdbClient) rather than
1221
+ * a plain IdbQueryExecutor stub.
1222
+ */
1223
+ function requireTransactionExecutor(executor) {
1224
+ 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.");
1225
+ return executor;
1226
+ }
1227
+ /** Create a parent and its nested related rows in one transaction. */
1228
+ async function executeNestedCreateMutation(options) {
1229
+ const { executor, contract, modelName, data } = options;
1230
+ const record = data;
1231
+ const storeNames = collectStoreNames(contract, modelName, record, "create");
1232
+ const defaultsCache = createMutationDefaultsCache();
1233
+ return withMutationScope(executor, storeNames, (scope) => createGraph(scope, contract, modelName, record, defaultsCache));
1234
+ }
1235
+ /** Update the first filtered parent and its nested relations in one transaction. */
1236
+ async function executeNestedUpdateMutation(options) {
1237
+ const { executor, contract, modelName, filters, data } = options;
1238
+ const record = data;
1239
+ const storeNames = collectStoreNames(contract, modelName, record, "update");
1240
+ const defaultsCache = createMutationDefaultsCache();
1241
+ return withMutationScope(executor, storeNames, (scope) => updateFirstGraph(scope, contract, modelName, filters, record, defaultsCache));
1242
+ }
1243
+ /**
1244
+ * Every store a nested write may touch: the model's own store, each related
1245
+ * store a relation callback writes to, and the parent stores that foreign-key
1246
+ * checks read, for the model and for each related model. An update also
1247
+ * declares the stores its `onUpdate` referential actions may touch.
1248
+ *
1249
+ * Parent stores are declared for every foreign key, not only the ones `data`
1250
+ * sets, because a nested write can set foreign keys through `connect()` or
1251
+ * through a default.
1252
+ */
1253
+ function collectStoreNames(contract, modelName, data, kind) {
1254
+ const stores = /* @__PURE__ */ new Set([getStoreName(contract, modelName), ...fkParentStoreNames(contract, modelName)]);
1255
+ const scalarData = {};
1256
+ const relationNames = /* @__PURE__ */ new Set();
1257
+ for (const def of getRelationDefinitions(contract, modelName)) {
1258
+ relationNames.add(def.relationName);
1259
+ if (def.relationName in data && isRelationMutationCallback(data[def.relationName])) {
1260
+ stores.add(def.relatedStoreName);
1261
+ for (const store of fkParentStoreNames(contract, def.relatedModelName)) stores.add(store);
1262
+ }
1263
+ }
1264
+ if (kind === "update") {
1265
+ for (const [field, value] of Object.entries(data)) if (!relationNames.has(field)) scalarData[field] = value;
1266
+ const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, getStoreName(contract, modelName), scalarData, createMutationDefaultsCache());
1267
+ for (const store of collectOnUpdateEnforcementStoreNames(contract, modelName, patch).storeNames) stores.add(store);
1268
+ }
1269
+ return [...stores];
1270
+ }
1271
+ /** The stores of every parent `modelName` has a foreign key to. */
1272
+ function fkParentStoreNames(contract, modelName) {
1273
+ return getRelationDefinitions(contract, modelName).filter((def) => def.cardinality === "N:1").map((def) => def.relatedStoreName);
1274
+ }
1275
+ /** Resolve parent-owned references before inserting the parent, then write its children. */
1276
+ async function createGraph(scope, contract, modelName, input, defaultsCache) {
1277
+ const parsed = parseMutationInput(contract, modelName, input);
1278
+ const { parentOwned, childOwned } = partitionByOwnership(parsed.relationMutations);
1279
+ const scalarData = { ...parsed.scalarData };
1280
+ for (const item of parentOwned) {
1281
+ if (item.mutation.kind === "disconnect") throw new Error("disconnect() is only supported in update() nested mutations");
1282
+ await applyParentOwnedMutation(scope, contract, scalarData, item.relation, item.mutation, defaultsCache);
1283
+ }
1284
+ const parentRow = await insertSingleRow(scope, contract, modelName, scalarData, defaultsCache);
1285
+ for (const item of childOwned) {
1286
+ if (item.mutation.kind === "disconnect") throw new Error("disconnect() is only supported in update() nested mutations");
1287
+ await applyChildOwnedMutation(scope, contract, modelName, parentRow, item.relation, item.mutation, defaultsCache);
1288
+ }
1289
+ return parentRow;
1290
+ }
1291
+ /** Read the parent before resolving relation mutations and validating its scalar patch. */
1292
+ async function updateFirstGraph(scope, contract, modelName, filters, input, defaultsCache) {
1293
+ const existingRow = await findFirstByFilters(scope, contract, modelName, filters);
1294
+ if (!existingRow) return null;
1295
+ const parsed = parseMutationInput(contract, modelName, input);
1296
+ const { parentOwned, childOwned } = partitionByOwnership(parsed.relationMutations);
1297
+ const scalarData = { ...parsed.scalarData };
1298
+ for (const item of parentOwned) await applyParentOwnedMutation(scope, contract, scalarData, item.relation, item.mutation, defaultsCache);
1299
+ let parentRow = existingRow;
1300
+ if (Object.keys(scalarData).length > 0) {
1301
+ const storeName = getStoreName(contract, modelName);
1302
+ const key = extractKeyFromRow(existingRow, getKeyPath(contract, modelName));
1303
+ const meta = makePlanMeta(contract);
1304
+ const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, storeName, scalarData, defaultsCache);
1305
+ assertEnumValues(contract, modelName, patch);
1306
+ assertValidRecord(contract, modelName, "update", patch);
1307
+ await validateScalarFks(scope, contract, modelName, patch, existingRow);
1308
+ await applyReferentialActionsForRowOnUpdate(scope, contract, modelName, existingRow, patch);
1309
+ const updated = (await scope.execute({
1310
+ meta,
1311
+ kind: "update",
1312
+ storeName,
1313
+ key,
1314
+ patch
1315
+ }))[0];
1316
+ if (updated) parentRow = absentAsNull(contract, modelName)(updated);
1317
+ }
1318
+ for (const item of childOwned) await applyChildOwnedMutation(scope, contract, modelName, parentRow, item.relation, item.mutation, defaultsCache);
1319
+ return parentRow;
1320
+ }
1321
+ /** Separate scalar values from validated relation callback results. */
1322
+ function parseMutationInput(contract, modelName, input) {
1323
+ const scalarData = {};
1324
+ const relationDefs = new Map(getRelationDefinitions(contract, modelName).map((r) => [r.relationName, r]));
1325
+ const relationMutations = [];
1326
+ for (const [fieldName, value] of Object.entries(input)) {
1327
+ const relation = relationDefs.get(fieldName);
1328
+ if (!relation) {
1329
+ scalarData[fieldName] = value;
1330
+ continue;
1331
+ }
1332
+ if (!isRelationMutationCallback(value)) throw new Error(`Relation field "${fieldName}" on model "${modelName}" expects a mutator callback`);
1333
+ const mutation = value(createRelationMutator());
1334
+ if (!isRelationMutationDescriptor(mutation)) throw new Error(`Relation field "${fieldName}" on model "${modelName}" returned an invalid mutation descriptor`);
1335
+ relationMutations.push({
1336
+ relation,
1337
+ mutation
1338
+ });
1339
+ }
1340
+ return {
1341
+ scalarData,
1342
+ relationMutations
1343
+ };
1344
+ }
1345
+ /** Separate relations whose foreign keys belong to the parent from those owned by children. */
1346
+ function partitionByOwnership(mutations) {
1347
+ const parentOwned = [];
1348
+ const childOwned = [];
1349
+ for (const item of mutations) {
1350
+ if (item.relation.cardinality === "N:1") {
1351
+ parentOwned.push(item);
1352
+ continue;
1353
+ }
1354
+ if (item.relation.cardinality === "M:N") throw new Error("M:N nested mutations are not supported");
1355
+ childOwned.push(item);
1356
+ }
1357
+ return {
1358
+ parentOwned,
1359
+ childOwned
1360
+ };
1361
+ }
1362
+ /** Resolve a to-one mutation into scalar foreign-key values on the parent. */
1363
+ async function applyParentOwnedMutation(scope, contract, scalarData, relation, mutation, defaultsCache) {
1364
+ if (mutation.kind === "disconnect") {
1365
+ for (const localField of relation.localFields) scalarData[localField] = null;
1366
+ return;
1367
+ }
1368
+ if (mutation.kind === "create") {
1369
+ const row = mutation.data[0];
1370
+ if (!row) throw new Error(`create() nested mutation for relation "${relation.relationName}" requires data`);
1371
+ copyRelatedValuesToParent(relation, scalarData, await insertSingleRow(scope, contract, relation.relatedModelName, row, defaultsCache));
1372
+ return;
1373
+ }
1374
+ const criterion = mutation.criteria[0];
1375
+ if (!criterion) throw new Error(`connect() nested mutation for relation "${relation.relationName}" requires a criterion`);
1376
+ const relatedRow = await findRowByCriterion(scope, contract, relation.relatedModelName, criterion);
1377
+ if (!relatedRow) throw new Error(`connect() nested mutation for relation "${relation.relationName}" did not find a matching row`);
1378
+ copyRelatedValuesToParent(relation, scalarData, relatedRow);
1379
+ }
1380
+ /** Copy each referenced value into the matching local foreign-key field. */
1381
+ function copyRelatedValuesToParent(relation, scalarData, relatedRow) {
1382
+ for (let i = 0; i < relation.localFields.length; i++) {
1383
+ const localField = relation.localFields[i];
1384
+ const targetField = relation.targetFields[i];
1385
+ if (!localField || !targetField) continue;
1386
+ scalarData[localField] = relatedRow[targetField];
1387
+ }
1388
+ }
1389
+ /** Create, connect, or disconnect children using the persisted parent join values. */
1390
+ async function applyChildOwnedMutation(scope, contract, parentModelName, parentRow, relation, mutation, defaultsCache) {
1391
+ const parentValues = readParentColumnValues(parentModelName, relation, parentRow);
1392
+ if (mutation.kind === "create") {
1393
+ for (const childInput of mutation.data) {
1394
+ const payload = { ...childInput };
1395
+ for (const [childField, parentValue] of parentValues.entries()) payload[childField] = parentValue;
1396
+ await insertSingleRow(scope, contract, relation.relatedModelName, payload, defaultsCache);
1397
+ }
1398
+ return;
1399
+ }
1400
+ if (mutation.kind === "connect") {
1401
+ for (const criterion of mutation.criteria) {
1402
+ const setValues = {};
1403
+ for (const [childField, parentValue] of parentValues.entries()) setValues[childField] = parentValue;
1404
+ const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, relation.relatedStoreName, setValues, defaultsCache);
1405
+ const where = shorthandToFilterExpr(criterion);
1406
+ await writeMutationRows(scope, contract, relation.relatedModelName, {
1407
+ write: "put-merged",
1408
+ patch,
1409
+ ...where === void 0 ? {} : { where }
1410
+ });
1411
+ }
1412
+ return;
1413
+ }
1414
+ const setValues = {};
1415
+ for (const childField of parentValues.keys()) setValues[childField] = null;
1416
+ if (!mutation.criteria || mutation.criteria.length === 0) {
1417
+ const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, relation.relatedStoreName, setValues, defaultsCache);
1418
+ const parentJoinWhere = buildParentJoinWhere(parentValues);
1419
+ await writeMutationRows(scope, contract, relation.relatedModelName, {
1420
+ write: "put-merged",
1421
+ patch,
1422
+ where: parentJoinWhere
1423
+ });
1424
+ return;
1425
+ }
1426
+ for (const criterion of mutation.criteria) {
1427
+ const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, relation.relatedStoreName, setValues, defaultsCache);
1428
+ const criterionWhere = shorthandToFilterExpr(criterion);
1429
+ const parentJoinWhere = buildParentJoinWhere(parentValues);
1430
+ const where = criterionWhere === void 0 ? parentJoinWhere : andExpr([parentJoinWhere, criterionWhere]);
1431
+ await writeMutationRows(scope, contract, relation.relatedModelName, {
1432
+ write: "put-merged",
1433
+ patch,
1434
+ where
1435
+ });
1436
+ }
1437
+ }
1438
+ /** Resolve parent join values before writing children; missing values cannot form a relation. */
1439
+ function readParentColumnValues(parentModelName, relation, parentRow) {
1440
+ const values = /* @__PURE__ */ new Map();
1441
+ for (let i = 0; i < relation.localFields.length; i++) {
1442
+ const localField = relation.localFields[i];
1443
+ const targetField = relation.targetFields[i];
1444
+ if (!localField || !targetField) continue;
1445
+ const parentValue = parentRow[localField];
1446
+ if (parentValue === void 0) throw new Error(`Nested mutation requires parent field "${localField}" to be present in "${parentModelName}" row`);
1447
+ values.set(targetField, parentValue);
1448
+ }
1449
+ return values;
1450
+ }
1451
+ /** Complete and validate a nested record, then insert it with foreign-key checks. */
1452
+ async function insertSingleRow(scope, contract, modelName, data, defaultsCache) {
1453
+ assertNoNestedCallbacks(modelName, data);
1454
+ const storeName = getStoreName(contract, modelName);
1455
+ const meta = makePlanMeta(contract);
1456
+ const record = completeCreateRecord(contract, modelName, data, defaultsCache);
1457
+ assertEnumValues(contract, modelName, record);
1458
+ assertValidRecord(contract, modelName, "create", record);
1459
+ await validateScalarFks(scope, contract, modelName, record);
1460
+ return (await scope.execute({
1461
+ meta,
1462
+ kind: "add",
1463
+ storeName,
1464
+ record
1465
+ }))[0] ?? record;
1466
+ }
1467
+ /**
1468
+ * Recursive nesting (a relation callback inside an already-nested create) is
1469
+ * not supported. Without this guard the callback function would be
1470
+ * handed to `store.put(...)`, where IDB's structured-clone throws an opaque
1471
+ * `DataCloneError` ("could not be cloned") that gives the developer no hint
1472
+ * about the real cause. Surface a precise error instead.
1473
+ */
1474
+ function assertNoNestedCallbacks(modelName, data) {
1475
+ 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.`);
1476
+ }
1477
+ /** Require a non-empty connect criterion and return its first matching row. */
1478
+ async function findRowByCriterion(scope, contract, modelName, criterion) {
1479
+ const expr = shorthandToFilterExpr(criterion);
1480
+ if (!expr) throw new Error(`Nested connect for model "${modelName}" requires a non-empty criterion`);
1481
+ return (await readMutationRows(scope, contract, modelName, expr, 1))[0] ?? null;
1482
+ }
1483
+ /** Find the first parent selected by the nested-update filters. */
1484
+ async function findFirstByFilters(scope, contract, modelName, filters) {
1485
+ if (filters.length === 0) return null;
1486
+ return (await readMutationRows(scope, contract, modelName, andExpr(filters), 1))[0] ?? null;
1487
+ }
1488
+ /** Execute lowered atomic operations without leaving the transaction's microtask chain. */
1489
+ async function executeMutationPlan(scope, plan) {
1490
+ const plans = plan.kind === "batch" ? plan.ops : [plan];
1491
+ const rows = [];
1492
+ for (const op of plans) rows.push(...await scope.execute(op));
1493
+ return rows;
1494
+ }
1495
+ /** Read mutation preimages through the planner, then reapply the original filter. */
1496
+ async function readMutationRows(scope, contract, modelName, where, take) {
1497
+ const catalog = buildCatalog(contract, getStoreName(contract, modelName), modelName);
1498
+ const request = {
1499
+ ...where === void 0 ? {} : { where },
1500
+ ...take === void 0 ? {} : { take }
1501
+ };
1502
+ const lowered = lowerRows(catalog, planQuery(catalog, request), makePlanMeta(contract), request);
1503
+ return lowered.finish(await executeMutationPlan(scope, lowered.idbPlan));
1504
+ }
1505
+ /** Write planned matches, collecting preimages first when changing the walked index key. */
1506
+ async function writeMutationRows(scope, contract, modelName, request) {
1507
+ if (request.write !== "delete") assertValidRecord(contract, modelName, "update", request.patch ?? {});
1508
+ const catalog = buildCatalog(contract, getStoreName(contract, modelName), modelName);
1509
+ const meta = makePlanMeta(contract);
1510
+ const lowered = lowerWrite(catalog, planQuery(catalog, request), meta, request);
1511
+ if (lowered.kind === "scan") return executeMutationPlan(scope, lowered.idbPlan);
1512
+ const oldRows = lowered.rows.finish(await executeMutationPlan(scope, lowered.rows.idbPlan));
1513
+ const results = [];
1514
+ for (const row of oldRows) {
1515
+ const key = extractKeyFromRow(row, catalog.primaryKey.keyPath);
1516
+ const rows = await scope.execute({
1517
+ meta,
1518
+ storeName: catalog.storeName,
1519
+ ...request.write === "delete" ? {
1520
+ kind: "delete",
1521
+ key
1522
+ } : {
1523
+ kind: "update",
1524
+ key,
1525
+ patch: request.patch ?? {}
1526
+ }
1527
+ });
1528
+ results.push(...rows);
1529
+ }
1530
+ return results;
1531
+ }
1532
+ function buildParentJoinWhere(parentValues) {
1533
+ return andExpr([...parentValues].map(([field, value]) => fieldFilter(field, "eq", value)));
1534
+ }
1535
+ /** Read an explicitly declared referential action from model storage metadata. */
1536
+ function getStoredAction(contract, modelName, relationName, kind) {
1537
+ return (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.relations?.[relationName]?.[kind];
1538
+ }
1539
+ function sameFields(a, b) {
1540
+ return a.length === b.length && a.every((field, index) => field === b[index]);
1541
+ }
1542
+ /**
1543
+ * Resolves the referential action for a child-enforcement relation: prefers
1544
+ * the action stored directly on this side, falls back to the same action
1545
+ * stored on the inverse (FK-owning) side of the relation, and finally to
1546
+ * `defaultAction` when neither side declares one.
1547
+ *
1548
+ * Both branches are live, for different authoring paths. PSL only ever
1549
+ * stores the action on the `N:1` (FK-owning) side — enforcement always walks
1550
+ * from the parent (`1:N`/non-owning-`1:1`) side, which PSL leaves bare, so
1551
+ * the fallback is what actually finds it. TS-DSL relation entries are
1552
+ * self-contained (no PSL-style pairing requirement) and typically declare
1553
+ * the action directly on the side this function is called for, so the direct
1554
+ * check is what fires there — see `contract-builder.ts`'s
1555
+ * `validateNoConflictingRelationActions`, which rejects declaring the same
1556
+ * action kind on both sides so this fallback never has to arbitrate a real
1557
+ * conflict.
1558
+ */
1559
+ function getReferentialActionForRelation(contract, modelName, def, kind) {
1560
+ return enforcedAction(findDeclaredAction(contract, modelName, def, kind));
1561
+ }
1562
+ /** Find an action on the enforcement side or on its matching inverse relation. */
1563
+ function findDeclaredAction(contract, modelName, def, kind) {
1564
+ const direct = getStoredAction(contract, modelName, def.relationName, kind);
1565
+ if (direct !== void 0) return direct;
1566
+ for (const inverse of getRelationDefinitions(contract, def.relatedModelName)) {
1567
+ if (inverse.relatedModelName !== modelName) continue;
1568
+ if (!sameFields(inverse.localFields, def.targetFields)) continue;
1569
+ if (!sameFields(inverse.targetFields, def.localFields)) continue;
1570
+ const inverseAction = getStoredAction(contract, def.relatedModelName, inverse.relationName, kind);
1571
+ if (inverseAction !== void 0) return inverseAction;
1572
+ }
1573
+ }
1574
+ /**
1575
+ * Applies the default and resolves `noAction`, matching what Postgres does
1576
+ * with the same schema, so a synced app behaves the same on both sides:
1577
+ *
1578
+ * - An undeclared action defaults to `restrict`, for `onDelete` and
1579
+ * `onUpdate` alike. Prisma 8 emits no `ON DELETE`/`ON UPDATE` clause for an
1580
+ * undeclared action, and the database default is `NO ACTION`.
1581
+ * - `noAction` behaves like `restrict`. In SQL, `NO ACTION` also rejects the
1582
+ * change; it only defers the check to the end of the statement, which makes
1583
+ * no difference here.
1584
+ */
1585
+ function enforcedAction(declared) {
1586
+ return declared === void 0 || declared === "noAction" ? "restrict" : declared;
1587
+ }
1588
+ /** Identify relations that need child checks when this model changes. */
1589
+ function isChildEnforcementRelation(contract, modelName, def) {
1590
+ if (def.cardinality === "1:N") return true;
1591
+ if (def.cardinality === "1:1") {
1592
+ const keyPath = getKeyPath(contract, modelName);
1593
+ return sameFields(def.localFields, keyPathFields(keyPath));
1594
+ }
1595
+ return false;
1596
+ }
1597
+ /**
1598
+ * `true` if any child row matches the relation against `parentRow`'s values —
1599
+ * the existence check behind `restrict`. A null value references no child.
1600
+ */
1601
+ async function childExists(scope, contract, meta, def, parentRow) {
1602
+ const values = def.localFields.map((field) => parentRow[field]);
1603
+ if (values.some((value) => value === null || value === void 0)) return false;
1604
+ return rowExists(scope, contract, meta, def.relatedModelName, def.targetFields, values);
1605
+ }
1606
+ /**
1607
+ * Does `modelName` have a row whose `fields` equal `values`, pairwise?
1608
+ * Compares values the way IndexedDB compares keys. `additionalFilter` narrows
1609
+ * the match in memory; it makes the check read rows instead of keys.
1610
+ */
1611
+ async function rowExists(scope, contract, meta, modelName, fields, values, additionalFilter) {
1612
+ const where = andExpr(fields.map((field, i) => fieldFilter(field, "eq", values[i])));
1613
+ const catalog = buildCatalog(contract, getStoreName(contract, modelName), modelName);
1614
+ const lowered = lowerExists(catalog, planQuery(catalog, { where }), meta, { where }, additionalFilter);
1615
+ return lowered.finish(await executeMutationPlan(scope, lowered.idbPlan));
1616
+ }
1617
+ /**
1618
+ * Builds a filter matching a relation's children against one specific parent
1619
+ * row's values. Shared by `onDelete` cascade (`applyReferentialActionsForRow`)
1620
+ * and `onUpdate` cascade (`applyReferentialActionsForRowOnUpdate`).
1621
+ */
1622
+ function buildChildWhereFromRow(def, row) {
1623
+ return andExpr(def.targetFields.map((field, i) => fieldFilter(field, "eq", row[def.localFields[i]])));
1624
+ }
1625
+ /** Reads a field's literal `@default(...)` value from `IdbModelStorage.fieldDefaults`, if declared. */
1626
+ function getFieldDefault(contract, modelName, fieldName) {
1627
+ return (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.fieldDefaults?.[fieldName];
1628
+ }
1629
+ /**
1630
+ * Builds the patch for a `setDefault` referential action: each of the
1631
+ * relation's `targetFields` (the child's FK fields) reset to its own
1632
+ * declared literal default — not the parent's. Throws when any target field
1633
+ * has no declared default; `setDefault` is only meaningful when every FK
1634
+ * field it resets has somewhere to reset to.
1635
+ */
1636
+ function buildSetDefaultPatch(contract, def) {
1637
+ const patch = {};
1638
+ for (const targetField of def.targetFields) {
1639
+ const value = getFieldDefault(contract, def.relatedModelName, targetField);
1640
+ 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.`);
1641
+ patch[targetField] = value;
1642
+ }
1643
+ return patch;
1644
+ }
1645
+ /**
1646
+ * Validates that a `setDefault` patch actually references a real row before
1647
+ * it's written — without this, `setDefault` would silently reintroduce the
1648
+ * exact dangling-FK problem the rest of this file exists to prevent. A real
1649
+ * SQL database only makes `SET DEFAULT` safe because its FK constraint
1650
+ * re-checks the new value transactionally at write time; IDB has no such
1651
+ * engine to delegate to, so this check is IDB's equivalent.
1652
+ *
1653
+ * Checks against `modelName`'s own store (the parent being deleted/updated)
1654
+ * using `def.localFields` — the relation's own semantics ("child.targetField
1655
+ * references parent.localField") mean that's exactly what a default value
1656
+ * written to the child's FK field must match. That store is always already
1657
+ * part of the transaction (it's the model literally being written), so no
1658
+ * store-list changes are needed to call this.
1659
+ *
1660
+ * A compound relation is checked as one tuple: a single parent row must
1661
+ * match every field's default.
1662
+ *
1663
+ * `excludeKey`, when given, excludes the parent row currently being
1664
+ * deleted/updated from the existence scan. That row is still physically
1665
+ * present in the store at this point (its own write/delete hasn't happened
1666
+ * yet — this check runs first, before the enforcement loop's caller applies
1667
+ * it), so without the exclusion, a default value equal to that row's *old*
1668
+ * `localField` value would false-positive: the scan would find the row about
1669
+ * to disappear (or change away from that value) and wrongly conclude the
1670
+ * default references a real row, when after the transaction nothing will.
1671
+ */
1672
+ async function validateSetDefaultPatch(scope, contract, modelName, def, patch, excludeKey) {
1673
+ const values = def.targetFields.map((f) => patch[f]);
1674
+ if (values.some((v) => v === null || v === void 0)) return;
1675
+ 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.`);
1676
+ }
1677
+ /**
1678
+ * Collects every store an `onUpdate`-enforced write to `modelName` might
1679
+ * touch: the model's own store, plus the related store of any 1:N/1:1-
1680
+ * parent-side relation whose `localFields` appear in the raw patch —
1681
+ * pessimistic (checks field *presence*, not whether the resolved value will
1682
+ * actually change, since the current row isn't read yet when this runs).
1683
+ * Once a `cascade` edge is entered, the walk continues transitively (mirrors
1684
+ * `collectDeleteStoreNames`'s cascade-only recursion), since a propagated
1685
+ * value change may itself need to cascade further.
1686
+ *
1687
+ * Returns `enforces: false` when no `onUpdate` enforcement can apply to this
1688
+ * write — callers use that to keep the existing fast blind-write path
1689
+ * instead of paying for a read-before-write. This is tracked as an explicit
1690
+ * flag rather than inferred from `storeNames.length > 1`: for a
1691
+ * self-referential relation, `def.relatedStoreName` equals `modelName`'s own
1692
+ * store (already the seed of the `Set`), so the store count alone can't tell
1693
+ * "no enforcement" apart from "enforces, but only against this same store".
1694
+ */
1695
+ function collectOnUpdateEnforcementStoreNames(contract, modelName, data) {
1696
+ const stores = /* @__PURE__ */ new Set([getStoreName(contract, modelName)]);
1697
+ const visitedModels = /* @__PURE__ */ new Set([modelName]);
1698
+ let enforces = false;
1699
+ for (const def of getRelationDefinitions(contract, modelName)) {
1700
+ if (!isChildEnforcementRelation(contract, modelName, def)) continue;
1701
+ if (!def.localFields.some((f) => f in data)) continue;
1702
+ const action = getReferentialActionForRelation(contract, modelName, def, "onUpdate");
1703
+ enforces = true;
1704
+ stores.add(def.relatedStoreName);
1705
+ if (action === "cascade") addOnUpdateCascadeStores(contract, def.relatedModelName, stores, visitedModels);
1706
+ }
1707
+ return {
1708
+ storeNames: [...stores],
1709
+ enforces
1710
+ };
1711
+ }
1712
+ /**
1713
+ * Enforces every `onUpdate` action declared on `modelName`'s child relations
1714
+ * against one specific `oldRow` about to be patched with `patch`. Only
1715
+ * relations whose `localFields` are both present in `patch` and differ from
1716
+ * `oldRow`'s current value are enforced — a patch that sets a field to its
1717
+ * existing value, or never touches a locally-referenced field, is a no-op
1718
+ * here. Children are matched against `oldRow`'s pre-change values.
1719
+ *
1720
+ * `cascade` propagates the new value(s) onto the matched children's FK
1721
+ * fields and recurses into each child's own `onUpdate` relations before
1722
+ * writing (the propagated change may itself need to cascade further) —
1723
+ * mirrors `applyReferentialActionsForRow`'s recursive shape, including the
1724
+ * `visited` row-level cycle guard (see that function's doc comment for the
1725
+ * fresh-per-top-level-row / shared-across-one-row's-descent contract).
1726
+ * `setNull`/`setDefault` are leaf actions: the child's FK field changes but
1727
+ * its own key doesn't, so nothing below it needs re-enforcement.
1728
+ *
1729
+ * Compound relations use values from the same `oldRow`/`patch`, so every
1730
+ * child is matched against the full pre-change tuple.
1731
+ */
1732
+ async function applyReferentialActionsForRowOnUpdate(scope, contract, modelName, oldRow, patch, visited = /* @__PURE__ */ new Set()) {
1733
+ const keyPath = getKeyPath(contract, modelName);
1734
+ patch = preserveDecimalKeySpelling(contract, modelName, patch, extractKeyFromRow(oldRow, keyPath));
1735
+ const rowKey = `${getStoreName(contract, modelName)}::${keyToken$1(extractKeyFromRow(oldRow, keyPath))}`;
1736
+ if (visited.has(rowKey)) return;
1737
+ visited.add(rowKey);
1738
+ const meta = makePlanMeta(contract);
1739
+ for (const def of getRelationDefinitions(contract, modelName)) {
1740
+ if (!isChildEnforcementRelation(contract, modelName, def)) continue;
1741
+ const changedFields = def.localFields.filter((f) => f in patch && !fieldValuesEqual(patch[f], oldRow[f]));
1742
+ if (changedFields.length === 0) continue;
1743
+ const action = getReferentialActionForRelation(contract, modelName, def, "onUpdate");
1744
+ const childWhere = buildChildWhereFromRow(def, oldRow);
1745
+ if (action === "restrict") {
1746
+ if (await childExists(scope, contract, meta, def, oldRow)) throw new Error(`Cannot update ${modelName} '${keyToken$1(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.`);
1747
+ continue;
1748
+ }
1749
+ if (action === "cascade") {
1750
+ const childPatch = {};
1751
+ for (let i = 0; i < def.localFields.length; i++) {
1752
+ const lf = def.localFields[i];
1753
+ const tf = def.targetFields[i];
1754
+ if (changedFields.includes(lf)) childPatch[tf] = patch[lf];
1755
+ }
1756
+ const children = await readMutationRows(scope, contract, def.relatedModelName, childWhere);
1757
+ for (const child of children) await applyReferentialActionsForRowOnUpdate(scope, contract, def.relatedModelName, child, childPatch, visited);
1758
+ await writeMutationRows(scope, contract, def.relatedModelName, {
1759
+ write: "put-merged",
1760
+ patch: childPatch,
1761
+ where: childWhere
1762
+ });
1763
+ continue;
1764
+ }
1765
+ if (action === "setNull") {
1766
+ const childPatch = {};
1767
+ for (const targetField of def.targetFields) childPatch[targetField] = null;
1768
+ await writeMutationRows(scope, contract, def.relatedModelName, {
1769
+ write: "put-merged",
1770
+ patch: childPatch,
1771
+ where: childWhere
1772
+ });
1773
+ continue;
1774
+ }
1775
+ if (action === "setDefault") {
1776
+ const childPatch = buildSetDefaultPatch(contract, def);
1777
+ await validateSetDefaultPatch(scope, contract, modelName, def, childPatch, extractKeyFromRow(oldRow, keyPath));
1778
+ await writeMutationRows(scope, contract, def.relatedModelName, {
1779
+ write: "put-merged",
1780
+ patch: childPatch,
1781
+ where: childWhere
1782
+ });
1783
+ continue;
1784
+ }
1785
+ }
1786
+ }
1787
+ /**
1788
+ * Returns true if `data` contains at least one non-null value for a localField
1789
+ * of a N:1 relation — indicating scalar FK fields that need existence validation.
1790
+ */
1791
+ function hasScalarFkFields(contract, modelName, data) {
1792
+ for (const def of getRelationDefinitions(contract, modelName)) {
1793
+ if (def.cardinality !== "N:1") continue;
1794
+ for (const localField of def.localFields) if (localField in data && data[localField] !== null && data[localField] !== void 0) return true;
1795
+ }
1796
+ return false;
1797
+ }
1798
+ /** Include parent stores for the non-null foreign keys supplied by this write. */
1799
+ function collectScalarFkStoreNames(contract, modelName, data) {
1800
+ const stores = /* @__PURE__ */ new Set([getStoreName(contract, modelName)]);
1801
+ for (const def of getRelationDefinitions(contract, modelName)) {
1802
+ if (def.cardinality !== "N:1") continue;
1803
+ if (def.localFields.some((f) => f in data && data[f] !== null && data[f] !== void 0)) stores.add(def.relatedStoreName);
1804
+ }
1805
+ return [...stores];
1806
+ }
1807
+ /**
1808
+ * Checks that every foreign key `data` sets points at an existing parent row,
1809
+ * inside the write's transaction.
1810
+ *
1811
+ * A compound foreign key is checked as one tuple: a single parent row must
1812
+ * match every field. Checking the fields one at a time could pass with each
1813
+ * value taken from a different parent. When `data` sets only some fields of a
1814
+ * compound key, the rest come from `existingRow`, the row being updated.
1815
+ * Callers must pass it in that case; see {@link fkCheckNeedsExistingRow}.
1816
+ *
1817
+ * A key with any `null` field isn't checked, like SQL's default `MATCH SIMPLE`.
1818
+ */
1819
+ async function validateScalarFks(scope, contract, modelName, data, existingRow) {
1820
+ const meta = makePlanMeta(contract);
1821
+ for (const def of getRelationDefinitions(contract, modelName)) {
1822
+ if (def.cardinality !== "N:1") continue;
1823
+ if (!def.localFields.some((f) => f in data)) continue;
1824
+ const values = def.localFields.map((f) => f in data ? data[f] : existingRow?.[f]);
1825
+ if (values.some((v) => v === null || v === void 0)) continue;
1826
+ 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)}`);
1827
+ }
1828
+ }
1829
+ /**
1830
+ * `true` when `data` sets some, but not all, fields of a compound foreign
1831
+ * key. Checking it then needs the rest of the key from the row being
1832
+ * updated, so the update has to read each row before writing it.
1833
+ */
1834
+ function fkCheckNeedsExistingRow(contract, modelName, data) {
1835
+ return getRelationDefinitions(contract, modelName).some((def) => {
1836
+ if (def.cardinality !== "N:1") return false;
1837
+ const set = def.localFields.filter((f) => f in data).length;
1838
+ return set > 0 && set < def.localFields.length;
1839
+ });
1840
+ }
1841
+ /**
1842
+ * Does `parentModel` have a row whose `fields` equal `values`, pairwise?
1843
+ *
1844
+ * `excludeKey` leaves out one parent row, for checks that run while that row
1845
+ * is being deleted or changed.
1846
+ */
1847
+ async function parentExists(scope, contract, meta, parentModel, fields, values, excludeKey) {
1848
+ const keyPath = getKeyPath(contract, parentModel);
1849
+ return rowExists(scope, contract, meta, parentModel, fields, values, excludeKey === void 0 ? void 0 : (row) => !keyEquals$1(extractKeyFromRow(row, keyPath), excludeKey));
1850
+ }
1851
+ /** `id='u1'`, or `orgId='a', id='u1'` for a compound key. */
1852
+ function describeTuple(fields, values) {
1853
+ return fields.map((f, i) => `${f}='${String(values[i])}'`).join(", ");
1854
+ }
1855
+ /** Create one completed record and check its foreign keys in the same transaction. */
1856
+ async function executeScalarCreateWithFkValidation(options) {
1857
+ const { executor, contract, modelName, data } = options;
1858
+ const defaultsCache = createMutationDefaultsCache();
1859
+ const record = completeCreateRecord(contract, modelName, data, defaultsCache);
1860
+ return withMutationScope(executor, collectScalarFkStoreNames(contract, modelName, record), (scope) => insertSingleRow(scope, contract, modelName, record, defaultsCache));
1861
+ }
1862
+ /**
1863
+ * `createAll()` for rows that set foreign keys: checks every row's foreign
1864
+ * keys and inserts it, all in one transaction, so one bad row writes nothing.
1865
+ * The batch shares one defaults cache, like the plain `createAll()` path.
1866
+ */
1867
+ async function executeScalarCreateAllWithFkValidation(options) {
1868
+ const { executor, contract, modelName, data } = options;
1869
+ const defaultsCache = createMutationDefaultsCache();
1870
+ const records = data.map((row) => completeCreateRecord(contract, modelName, row, defaultsCache));
1871
+ return withMutationScope(executor, [...new Set(records.flatMap((row) => collectScalarFkStoreNames(contract, modelName, row)))], async (scope) => {
1872
+ const inserted = [];
1873
+ for (const record of records) inserted.push(await insertSingleRow(scope, contract, modelName, record, defaultsCache));
1874
+ return inserted;
1875
+ });
1876
+ }
1877
+ /**
1878
+ * Adds the stores an `onUpdate` cascade reaches from `modelName`: every child
1879
+ * relation's store, walking further through `cascade` edges only.
1880
+ */
1881
+ function addOnUpdateCascadeStores(contract, modelName, stores, visitedModels) {
1882
+ if (visitedModels.has(modelName)) return;
1883
+ visitedModels.add(modelName);
1884
+ for (const def of getRelationDefinitions(contract, modelName)) {
1885
+ if (!isChildEnforcementRelation(contract, modelName, def)) continue;
1886
+ stores.add(def.relatedStoreName);
1887
+ if (getReferentialActionForRelation(contract, modelName, def, "onUpdate") === "cascade") addOnUpdateCascadeStores(contract, def.relatedModelName, stores, visitedModels);
1888
+ }
1889
+ }
1890
+ /**
1891
+ * Every store an `onUpdate` enforcement on `modelName` could touch, whichever
1892
+ * fields the write sets. Use it to open a transaction before the write's
1893
+ * fields are known.
1894
+ */
1895
+ function collectOnUpdateClosureStoreNames(contract, modelName) {
1896
+ const stores = /* @__PURE__ */ new Set([getStoreName(contract, modelName)]);
1897
+ addOnUpdateCascadeStores(contract, modelName, stores, /* @__PURE__ */ new Set());
1898
+ return [...stores];
1899
+ }
1900
+ /**
1901
+ * Combines FK-existence store scope (this model's own N:1 relations) with
1902
+ * `onUpdate` enforcement store scope (this model's child relations) for a
1903
+ * single write. Returns the union alongside whether `onUpdate` enforcement
1904
+ * actually applies, including self-relations that touch only this model's
1905
+ * own store. Callers use that flag to choose between the existing
1906
+ * fast blind-write path and the read-before-write enforcement path.
1907
+ */
1908
+ function collectUpdateStoreNames(contract, modelName, data) {
1909
+ const fkStoreNames = collectScalarFkStoreNames(contract, modelName, data);
1910
+ const { storeNames: onUpdateStoreNames, enforces } = collectOnUpdateEnforcementStoreNames(contract, modelName, data);
1911
+ return {
1912
+ storeNames: [.../* @__PURE__ */ new Set([...fkStoreNames, ...onUpdateStoreNames])],
1913
+ enforcesOnUpdate: enforces
1914
+ };
1915
+ }
1916
+ /** Updates the first matching row with FK validation and referential actions. */
1917
+ async function executeScalarUpdateWithFkValidation(options) {
1918
+ return (await executeScalarUpdates(options, 1))[0] ?? null;
1919
+ }
1920
+ /**
1921
+ * Bulk counterpart to {@link executeScalarUpdateWithFkValidation}: same FK
1922
+ * validation and `onUpdate` enforcement, but applies the patch to every row
1923
+ * the filter matches (no `take: 1`) and returns all of them.
1924
+ *
1925
+ * The transaction scope lets the sync interceptor observe the affected rows
1926
+ * and write one outbox event per row in the same transaction. A bulk write's
1927
+ * affected keys are only known after it runs, so tracking cannot happen by
1928
+ * extending the plan before execution.
1929
+ */
1930
+ async function executeBulkUpdateWithFkValidation(options) {
1931
+ return executeScalarUpdates(options);
1932
+ }
1933
+ /** Shares validation and atomic writes; only single-row updates limit the scan. */
1934
+ async function executeScalarUpdates(options, take) {
1935
+ const { executor, contract, modelName, filters, data } = options;
1936
+ const storeName = getStoreName(contract, modelName);
1937
+ const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, storeName, data, createMutationDefaultsCache());
1938
+ assertEnumValues(contract, modelName, patch);
1939
+ assertValidRecord(contract, modelName, "update", patch);
1940
+ const { storeNames, enforcesOnUpdate } = collectUpdateStoreNames(contract, modelName, patch);
1941
+ const needsRowForFks = fkCheckNeedsExistingRow(contract, modelName, patch);
1942
+ return (await withMutationScope(executor, storeNames, async (scope) => {
1943
+ if (!needsRowForFks) await validateScalarFks(scope, contract, modelName, patch);
1944
+ const meta = makePlanMeta(contract);
1945
+ const where = filters.length === 0 ? void 0 : andExpr(filters);
1946
+ if (!enforcesOnUpdate && !needsRowForFks) return writeMutationRows(scope, contract, modelName, {
1947
+ write: "put-merged",
1948
+ patch,
1949
+ ...take === void 0 ? {} : { take },
1950
+ ...where === void 0 ? {} : { where }
1951
+ });
1952
+ const oldRows = await readMutationRows(scope, contract, modelName, where, take);
1953
+ const keyPath = getKeyPath(contract, modelName);
1954
+ const results = [];
1955
+ for (const oldRow of oldRows) {
1956
+ if (needsRowForFks) await validateScalarFks(scope, contract, modelName, patch, oldRow);
1957
+ if (enforcesOnUpdate) await applyReferentialActionsForRowOnUpdate(scope, contract, modelName, oldRow, patch);
1958
+ const key = extractKeyFromRow(oldRow, keyPath);
1959
+ const updated = (await scope.execute({
1960
+ meta,
1961
+ kind: "update",
1962
+ storeName,
1963
+ key,
1964
+ patch
1965
+ }))[0];
1966
+ if (updated) results.push(updated);
1967
+ }
1968
+ return results;
1969
+ })).map(absentAsNull(contract, modelName));
1970
+ }
1971
+ /**
1972
+ * Returns true if the model has at least one child relation (1:N or parent-side
1973
+ * 1:1). Every such relation has an `onDelete` action to enforce: `noAction`
1974
+ * behaves like `restrict`, which is also the default.
1975
+ */
1976
+ function hasEnforceableChildRelations(contract, modelName) {
1977
+ return getRelationDefinitions(contract, modelName).some((def) => isChildEnforcementRelation(contract, modelName, def));
1978
+ }
1979
+ /** The store of `modelName` plus the store of every model it has a relation to, in either direction. */
1980
+ function collectRelatedStoreNames(contract, modelName) {
1981
+ const stores = /* @__PURE__ */ new Set([getStoreName(contract, modelName)]);
1982
+ for (const def of getRelationDefinitions(contract, modelName)) stores.add(def.relatedStoreName);
1983
+ return [...stores];
1984
+ }
1985
+ /**
1986
+ * Transitively walks the `onDelete` cascade graph from `modelName`, collecting
1987
+ * every store a recursive delete might touch. `cascade` edges are walked
1988
+ * further (a cascaded child's own children may themselves cascade);
1989
+ * `restrict`/`setNull`/`setDefault` edges still need their store added (each
1990
+ * is read or written once) but don't recurse further, since none of them
1991
+ * delete the child row. Guarded by a model-level visited set — required
1992
+ * because IDB must declare every store a transaction might touch before it
1993
+ * opens, so this static walk must terminate even on a self-referential model
1994
+ * or a cycle of mutually-cascading models.
1995
+ */
1996
+ function collectDeleteStoreNames(contract, modelName) {
1997
+ const stores = /* @__PURE__ */ new Set();
1998
+ const visitedModels = /* @__PURE__ */ new Set();
1999
+ /** Collect cascade stores once per model so cyclic relations cannot recurse indefinitely. */
2000
+ function walk(mName) {
2001
+ if (visitedModels.has(mName)) return;
2002
+ visitedModels.add(mName);
2003
+ stores.add(getStoreName(contract, mName));
2004
+ for (const def of getRelationDefinitions(contract, mName)) {
2005
+ if (!isChildEnforcementRelation(contract, mName, def)) continue;
2006
+ const action = getReferentialActionForRelation(contract, mName, def, "onDelete");
2007
+ stores.add(def.relatedStoreName);
2008
+ if (action === "cascade") walk(def.relatedModelName);
2009
+ }
2010
+ }
2011
+ walk(modelName);
2012
+ return [...stores];
2013
+ }
2014
+ /**
2015
+ * Enforces every `onDelete` action declared on `modelName`'s child relations
2016
+ * against one specific `row` about to be deleted. `cascade` recurses into
2017
+ * each matched child's own `onDelete` relations *before* deleting it — so a
2018
+ * multi-hop chain (`User --cascade--> Post --cascade--> Comment`) is fully
2019
+ * torn down, and a `restrict` several hops deep still aborts the whole
2020
+ * transaction (recursing before deleting means the delete never happens if a
2021
+ * deeper hop throws). `setNull`/`setDefault` are leaf actions: the child row
2022
+ * survives, so recursion never continues past them.
2023
+ *
2024
+ * `visited` (keyed by `storeName::key`) guards against row-level cycles — two
2025
+ * specific rows whose FKs point at each other through a self-referential or
2026
+ * mutually-cascading relation graph. Callers should leave it at its default
2027
+ * (a fresh `Set` per top-level call) so independent rows deleted in the same
2028
+ * `deleteAll()` batch don't cross-suppress each other's cascades; it's only
2029
+ * ever passed explicitly by this function's own recursive calls, to keep one
2030
+ * shared guard across a single row's full recursive descent.
2031
+ */
2032
+ async function applyReferentialActionsForRow(scope, contract, modelName, row, visited = /* @__PURE__ */ new Set()) {
2033
+ const keyPath = getKeyPath(contract, modelName);
2034
+ const rowKey = `${getStoreName(contract, modelName)}::${keyToken$1(extractKeyFromRow(row, keyPath))}`;
2035
+ if (visited.has(rowKey)) return;
2036
+ visited.add(rowKey);
2037
+ const meta = makePlanMeta(contract);
2038
+ for (const def of getRelationDefinitions(contract, modelName)) {
2039
+ if (!isChildEnforcementRelation(contract, modelName, def)) continue;
2040
+ const action = getReferentialActionForRelation(contract, modelName, def, "onDelete");
2041
+ const childWhere = buildChildWhereFromRow(def, row);
2042
+ if (action === "restrict") {
2043
+ if (await childExists(scope, contract, meta, def, row)) throw new Error(`Cannot delete ${modelName} '${keyToken$1(extractKeyFromRow(row, keyPath))}': child records exist on relation '${def.relationName}'. Delete those children first, or declare onDelete: Cascade, SetNull or SetDefault on the relation.`);
2044
+ continue;
2045
+ }
2046
+ if (action === "cascade") {
2047
+ const childKeyPath = getKeyPath(contract, def.relatedModelName);
2048
+ const children = await readMutationRows(scope, contract, def.relatedModelName, childWhere);
2049
+ for (const child of children) {
2050
+ await applyReferentialActionsForRow(scope, contract, def.relatedModelName, child, visited);
2051
+ await scope.execute({
2052
+ meta,
2053
+ kind: "delete",
2054
+ storeName: def.relatedStoreName,
2055
+ key: extractKeyFromRow(child, childKeyPath)
2056
+ });
2057
+ }
2058
+ continue;
2059
+ }
2060
+ if (action === "setNull") {
2061
+ const patch = {};
2062
+ for (const targetField of def.targetFields) patch[targetField] = null;
2063
+ await writeMutationRows(scope, contract, def.relatedModelName, {
2064
+ write: "put-merged",
2065
+ patch,
2066
+ where: childWhere
2067
+ });
2068
+ continue;
2069
+ }
2070
+ if (action === "setDefault") {
2071
+ const patch = buildSetDefaultPatch(contract, def);
2072
+ await validateSetDefaultPatch(scope, contract, modelName, def, patch, extractKeyFromRow(row, keyPath));
2073
+ await writeMutationRows(scope, contract, def.relatedModelName, {
2074
+ write: "put-merged",
2075
+ patch,
2076
+ where: childWhere
2077
+ });
2078
+ continue;
2079
+ }
2080
+ }
2081
+ }
2082
+ /** Load a keyed row, apply its child actions, and delete it in one transaction. */
2083
+ async function executeDeleteWithReferentialActions(options) {
2084
+ const { executor, contract, modelName, key } = options;
2085
+ await withMutationScope(executor, collectDeleteStoreNames(contract, modelName), async (scope) => {
2086
+ const storeName = getStoreName(contract, modelName);
2087
+ const meta = makePlanMeta(contract);
2088
+ const keyPath = getKeyPath(contract, modelName);
2089
+ const fields = keyPathFields(keyPath);
2090
+ const values = typeof keyPath === "string" ? [key] : key;
2091
+ const where = andExpr(fields.map((field, i) => fieldFilter(field, "eq", values[i])));
2092
+ const row = (await readMutationRows(scope, contract, modelName, where, 1))[0];
2093
+ if (!row) return [];
2094
+ await applyReferentialActionsForRow(scope, contract, modelName, row);
2095
+ await scope.execute({
2096
+ meta,
2097
+ kind: "delete",
2098
+ storeName,
2099
+ key
2100
+ });
2101
+ return [];
2102
+ });
2103
+ }
2104
+ /** Delete filtered rows with all child actions inside the same transaction. */
2105
+ async function executeDeleteAllWithReferentialActions(options) {
2106
+ const { executor, contract, modelName, where } = options;
2107
+ return withMutationScope(executor, collectDeleteStoreNames(contract, modelName), async (scope) => {
2108
+ const storeName = getStoreName(contract, modelName);
2109
+ const meta = makePlanMeta(contract);
2110
+ const keyPath = getKeyPath(contract, modelName);
2111
+ if (!hasEnforceableChildRelations(contract, modelName)) return (await writeMutationRows(scope, contract, modelName, {
2112
+ write: "delete",
2113
+ ...where === void 0 ? {} : { where }
2114
+ })).map(absentAsNull(contract, modelName));
2115
+ const rows = await readMutationRows(scope, contract, modelName, where);
2116
+ for (const row of rows) {
2117
+ await applyReferentialActionsForRow(scope, contract, modelName, row);
2118
+ const key = extractKeyFromRow(row, keyPath);
2119
+ await scope.execute({
2120
+ meta,
2121
+ kind: "delete",
2122
+ storeName,
2123
+ key
2124
+ });
2125
+ }
2126
+ return rows;
2127
+ });
2128
+ }
2129
+ //#endregion
2130
+ export { createMutationDefaultsCache as A, resolveDecimalKey as B, validateScalarFks as C, planQuery as D, lowerRows as E, combineFilterExprs as F, fieldValueToken as G, withDecimalIdentityScope as H, IdbDecimalIdentityConflictError as I, getStoreName as J, getKeyPath as K, decimalKeysEqual as L, assertValidRecord as M, assertEnumValues as N, applyUpdateDefaults as O, buildRowComparator as P, hasDecimalPrimaryKey as R, requireTransactionExecutor as S, lowerCount as T, buildCatalog as U, withDecimalIdentityExecutor as V, extractKeyFromRow as W, keyToken$1 as X, keyEquals$1 as Y, hasEnforceableChildRelations as _, collectOnUpdateEnforcementStoreNames as a, makePlanMeta as b, executeBulkUpdateWithFkValidation as c, executeMutationPlan as d, executeNestedCreateMutation as f, executeScalarUpdateWithFkValidation as g, executeScalarCreateWithFkValidation as h, collectOnUpdateClosureStoreNames as i, IdbRecordValidationError as j, completeCreateRecord as k, executeDeleteAllWithReferentialActions as l, executeScalarCreateAllWithFkValidation as m, applyReferentialActionsForRowOnUpdate as n, collectRelatedStoreNames as o, executeNestedUpdateMutation as p, getRelation as q, collectDeleteStoreNames as r, collectScalarFkStoreNames as s, applyReferentialActionsForRow as t, executeDeleteWithReferentialActions as u, hasNestedMutationCallbacks as v, withMutationScope as w, readMutationRows as x, hasScalarFkFields as y, preserveDecimalKeySpelling as z };
2131
+
2132
+ //# sourceMappingURL=mutation-executor-IuJmyWEG.mjs.map