@prisma-idb/client-idb 0.12.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 (35) hide show
  1. package/dist/client-auto.d.mts +2 -2
  2. package/dist/client-auto.d.mts.map +1 -1
  3. package/dist/client-auto.mjs +44 -3
  4. package/dist/client-auto.mjs.map +1 -1
  5. package/dist/client.d.mts +3 -2
  6. package/dist/client.mjs +3 -2
  7. package/dist/decimal-identity-D_1J9lPN.d.mts +21 -0
  8. package/dist/decimal-identity-D_1J9lPN.d.mts.map +1 -0
  9. package/dist/internal.d.mts +5 -4
  10. package/dist/internal.d.mts.map +1 -1
  11. package/dist/internal.mjs +2 -2
  12. package/dist/{managed-client-M3xMUk0n.mjs → managed-client-DMk8ERfX.mjs} +12 -5
  13. package/dist/managed-client-DMk8ERfX.mjs.map +1 -0
  14. package/dist/{managed-client-DQlsxc4Z.d.mts → managed-client-eksFK4UF.d.mts} +32 -3
  15. package/dist/managed-client-eksFK4UF.d.mts.map +1 -0
  16. package/dist/{mutation-executor-inLFb0TE.mjs → mutation-executor-IuJmyWEG.mjs} +486 -105
  17. package/dist/mutation-executor-IuJmyWEG.mjs.map +1 -0
  18. package/dist/{executor-CIEHpa1j.d.mts → mutation-scope-CE3GMR4X.d.mts} +66 -22
  19. package/dist/mutation-scope-CE3GMR4X.d.mts.map +1 -0
  20. package/dist/{idb-orm-B1Br1C0F.mjs → orm-transaction-BGx1eukw.mjs} +357 -61
  21. package/dist/orm-transaction-BGx1eukw.mjs.map +1 -0
  22. package/dist/{idb-orm-6OuwkeDM.d.mts → orm-transaction-B_ZJ92uY.d.mts} +40 -7
  23. package/dist/orm-transaction-B_ZJ92uY.d.mts.map +1 -0
  24. package/dist/orm.d.mts +13 -4
  25. package/dist/orm.d.mts.map +1 -1
  26. package/dist/orm.mjs +3 -3
  27. package/package.json +9 -9
  28. package/dist/executor-CIEHpa1j.d.mts.map +0 -1
  29. package/dist/idb-orm-6OuwkeDM.d.mts.map +0 -1
  30. package/dist/idb-orm-B1Br1C0F.mjs.map +0 -1
  31. package/dist/managed-client-DQlsxc4Z.d.mts.map +0 -1
  32. package/dist/managed-client-M3xMUk0n.mjs.map +0 -1
  33. package/dist/mutation-executor-inLFb0TE.mjs.map +0 -1
  34. package/dist/mutation-scope-DrxFhek3.d.mts +0 -33
  35. package/dist/mutation-scope-DrxFhek3.d.mts.map +0 -1
@@ -1,7 +1,9 @@
1
- import { A as combineFilterExprs, C as planQuery, D as createMutationDefaultsCache, E as applyUpdateDefaults, F as getStoreName, M as fieldValueToken, N as getKeyPath, O as assertEnumValues, P as getRelation, S as lowerRows, T as applyCreateDefaults, _ as readMutationRows, a as collectScalarFkStoreNames, b as withMutationScope, c as executeDeleteWithReferentialActions, d as executeScalarCreateAllWithFkValidation, f as executeScalarCreateWithFkValidation, g as hasScalarFkFields, h as hasNestedMutationCallbacks, i as collectOnUpdateEnforcementStoreNames, j as extractKeyFromRow, k as buildRowComparator, l as executeNestedCreateMutation, m as hasEnforceableChildRelations, n as applyReferentialActionsForRowOnUpdate, o as executeBulkUpdateWithFkValidation, p as executeScalarUpdateWithFkValidation, s as executeDeleteAllWithReferentialActions, u as executeNestedUpdateMutation, v as requireTransactionExecutor, w as buildCatalog, x as lowerCount, y as validateScalarFks } from "./mutation-executor-inLFb0TE.mjs";
1
+ import { A as createMutationDefaultsCache, B as resolveDecimalKey, C as validateScalarFks, D as planQuery, E as lowerRows, F as combineFilterExprs, G as fieldValueToken, J as getStoreName, K as getKeyPath, M as assertValidRecord, N as assertEnumValues, O as applyUpdateDefaults, P as buildRowComparator, S as requireTransactionExecutor, T as lowerCount, U as buildCatalog, V as withDecimalIdentityExecutor, W as extractKeyFromRow, _ as hasEnforceableChildRelations, a as collectOnUpdateEnforcementStoreNames, b as makePlanMeta, c as executeBulkUpdateWithFkValidation, d as executeMutationPlan, f as executeNestedCreateMutation, g as executeScalarUpdateWithFkValidation, h as executeScalarCreateWithFkValidation, i as collectOnUpdateClosureStoreNames, k as completeCreateRecord, l as executeDeleteAllWithReferentialActions, m as executeScalarCreateAllWithFkValidation, n as applyReferentialActionsForRowOnUpdate, o as collectRelatedStoreNames, p as executeNestedUpdateMutation, q as getRelation, r as collectDeleteStoreNames, s as collectScalarFkStoreNames, u as executeDeleteWithReferentialActions, v as hasNestedMutationCallbacks, w as withMutationScope, x as readMutationRows, y as hasScalarFkFields } from "./mutation-executor-IuJmyWEG.mjs";
2
2
  import { AsyncIterableResult } from "@prisma/orm-framework/components/runtime";
3
- import { andExpr, fieldFilter, nullCheckExpr, shorthandToFilterExpr } from "@prisma-idb/adapter-idb/runtime";
4
3
  import { domainModelsAtDefaultNamespace } from "@prisma/orm-framework/contract/types";
4
+ import { IdbExecuteError } from "@prisma-idb/driver-idb/runtime";
5
+ import { absentAsNull, isValidIdbKey } from "@prisma-idb/target-idb/runtime";
6
+ import { andExpr, fieldFilter, nullCheckExpr, shorthandToFilterExpr } from "@prisma-idb/adapter-idb/runtime";
5
7
  //#region src/core/model-accessor.ts
6
8
  /**
7
9
  * Proxy-based typed accessor handed to `where(fn)` callbacks.
@@ -52,7 +54,9 @@ function createFieldAccessor(field) {
52
54
  */
53
55
  function createModelAccessor() {
54
56
  const cache = /* @__PURE__ */ new Map();
55
- return new Proxy({}, { get(_target, prop) {
57
+ return new Proxy({}, {
58
+ /** Reuse the field accessor within one filter callback so repeated reads share its operator surface. */
59
+ get(_target, prop) {
56
60
  if (typeof prop !== "string") return void 0;
57
61
  let acc = cache.get(prop);
58
62
  if (acc === void 0) {
@@ -145,33 +149,43 @@ function isAggregateSelector(value) {
145
149
  * - `sum`/`avg`/`min`/`max` → computed over the non-null numeric values of
146
150
  * `field`; `null` when no row has a non-null value (matching Prisma's
147
151
  * "aggregate of an empty set is null" and the vendor `coerceAggregateValue`
148
- * null handling). String-encoded numbers are coerced via `Number()`.
152
+ * null handling). Values must match the declared number codec.
149
153
  */
150
- function reduceAggregate(fn, field, rows) {
154
+ function reduceAggregate(fn, field, rows, codecId) {
151
155
  if (fn === "count") return rows.length;
152
156
  if (field === void 0) return null;
153
157
  const values = [];
154
158
  for (const row of rows) {
155
159
  const raw = row[field];
156
160
  if (raw === null || raw === void 0) continue;
157
- const n = typeof raw === "bigint" ? Number(raw) : Number(raw);
158
- if (!Number.isNaN(n)) values.push(n);
161
+ if (typeof raw !== "number" || !Number.isFinite(raw)) throw new TypeError(`${fn}("${field}") requires finite number values`);
162
+ if (codecId === "idb/int32@1" && (!Number.isInteger(raw) || raw < -2147483648 || raw > 2147483647)) throw new TypeError(`${fn}("${field}") requires Int values in the signed 32-bit range`);
163
+ values.push(raw);
159
164
  }
160
165
  if (values.length === 0) return null;
161
166
  switch (fn) {
162
- case "sum": return values.reduce((acc, n) => acc + n, 0);
163
- case "avg": return values.reduce((acc, n) => acc + n, 0) / values.length;
164
- case "min": return Math.min(...values);
165
- case "max": return Math.max(...values);
167
+ case "sum": return sumValues(values, field, codecId === "idb/int32@1");
168
+ case "avg": return sumValues(values, field, codecId === "idb/int32@1") / values.length;
169
+ case "min": return values.reduce((acc, n) => Math.min(acc, n));
170
+ case "max": return values.reduce((acc, n) => Math.max(acc, n));
171
+ }
172
+ }
173
+ /** Reject overflow before a rounded or nonfinite intermediate total can reach the result. */
174
+ function sumValues(values, field, integer) {
175
+ let total = 0;
176
+ for (const value of values) {
177
+ total += value;
178
+ if (!Number.isFinite(total) || integer && !Number.isSafeInteger(total)) throw new RangeError(`Aggregate total for "${field}" exceeds the ${integer ? "safe integer" : "finite number"} range`);
166
179
  }
180
+ return total;
167
181
  }
168
182
  /**
169
183
  * Run every selector in an aggregate spec against `rows`, producing the
170
184
  * result object keyed by the spec's aliases.
171
185
  */
172
- function computeAggregateSpec(spec, rows) {
186
+ function computeAggregateSpec(spec, rows, fields = {}) {
173
187
  const result = {};
174
- for (const [alias, selector] of Object.entries(spec)) result[alias] = reduceAggregate(selector.fn, selector.field, rows);
188
+ for (const [alias, selector] of Object.entries(spec)) result[alias] = reduceAggregate(selector.fn, selector.field, rows, fields[selector.field ?? ""]?.codecId);
175
189
  return result;
176
190
  }
177
191
  /**
@@ -189,14 +203,20 @@ function toAggregateRequests(spec) {
189
203
  }
190
204
  /**
191
205
  * Validate a user-supplied aggregate spec: it must be non-empty and every
192
- * value must be a real selector. Throws a descriptive error otherwise, so a
206
+ * value must be a real selector over a declared scalar number field (except
207
+ * count, which has no field). Throws a descriptive error otherwise, so a
193
208
  * typo (e.g. returning a plain object) fails loudly rather than silently
194
209
  * producing `NaN`/`undefined` results.
195
210
  */
196
- function assertValidAggregateSpec(spec, context) {
211
+ function assertValidAggregateSpec(spec, context, fields) {
197
212
  const entries = Object.entries(spec);
198
213
  if (entries.length === 0) throw new Error(`${context} requires at least one aggregation selector`);
199
- for (const [alias, selector] of entries) if (!isAggregateSelector(selector)) throw new Error(`${context} selector "${alias}" is invalid`);
214
+ for (const [alias, selector] of entries) {
215
+ if (!isAggregateSelector(selector)) throw new Error(`${context} selector "${alias}" is invalid`);
216
+ if (selector.fn === "count") continue;
217
+ const field = typeof selector.field === "string" ? fields[selector.field] : void 0;
218
+ if (!field || field.collection || field.codecId !== "idb/int32@1" && field.codecId !== "idb/double@1") throw new TypeError(`${context} selector "${alias}" requires a declared number field (Int or Float)`);
219
+ }
200
220
  }
201
221
  //#endregion
202
222
  //#region src/core/grouped-accessor.ts
@@ -215,9 +235,11 @@ function groupToken(value) {
215
235
  }
216
236
  /** Build a grouped accessor that reduces materialized rows from the source accessor. */
217
237
  function createGroupedAccessor(init) {
218
- return { async aggregate(fn) {
238
+ return {
239
+ /** Materialize matching rows, partition them by group key, and reduce each group. */
240
+ async aggregate(fn) {
219
241
  const spec = fn(createAggregateBuilder());
220
- assertValidAggregateSpec(spec, "groupBy().aggregate()");
242
+ assertValidAggregateSpec(spec, "groupBy().aggregate()", init.fields);
221
243
  const ast = {
222
244
  kind: "groupBy",
223
245
  modelName: init.modelName,
@@ -243,11 +265,30 @@ function createGroupedAccessor(init) {
243
265
  }
244
266
  return Array.from(groups.values()).map((group) => ({
245
267
  ...group.key,
246
- ...computeAggregateSpec(spec, group.rows)
268
+ ...computeAggregateSpec(spec, group.rows, init.fields)
247
269
  }));
248
270
  } };
249
271
  }
250
272
  //#endregion
273
+ //#region src/core/unique-lookup.ts
274
+ /**
275
+ * Copy the own enumerable properties of an upsert `where` once. Validation and
276
+ * filter parsing then see the same values even when a property is a getter.
277
+ * Non-object inputs pass through so `assertUniqueLookup` can reject them.
278
+ */
279
+ function snapshotLookup(where) {
280
+ return typeof where === "object" && where !== null && !Array.isArray(where) ? Object.fromEntries(Object.entries(where)) : where;
281
+ }
282
+ /**
283
+ * Require equality on a complete native unique key before upsert can mutate.
284
+ * Null and other non-key values are absent from IndexedDB indexes, so even a
285
+ * declared unique index cannot make those filters identify at most one row.
286
+ */
287
+ function assertUniqueLookup(catalog, where) {
288
+ const sources = [catalog.primaryKey, ...catalog.indexes];
289
+ if (!(typeof where === "object" && where !== null && !Array.isArray(where) && sources.some((source) => source.unique && !source.multiEntry && source.fields.length > 0 && source.fields.every((field) => Object.prototype.propertyIsEnumerable.call(where, field) && isValidIdbKey(where[field]))))) throw new Error(`Invalid upsert where for "${catalog.storeName}": provide a complete primary key or unique index with non-null IndexedDB key values`);
290
+ }
291
+ //#endregion
251
292
  //#region src/core/relation-loader.ts
252
293
  /**
253
294
  * Batch-load a single named relation for all rows in `rows` and attach the
@@ -308,7 +349,7 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
308
349
  const joinWhere = andExpr(targetFields.map((field, i) => fieldFilter(field, "in", [...localTuples.values()].map((tuple) => tuple[i]))));
309
350
  const where = refinedWhere === void 0 ? joinWhere : andExpr([joinWhere, refinedWhere]);
310
351
  const request = { ...refinedWhere === void 0 ? {} : { where: refinedWhere } };
311
- const catalog = buildCatalog(contract, relatedStoreName);
352
+ const catalog = buildCatalog(contract, relatedStoreName, relatedModelName);
312
353
  const lowered = lowerRows(catalog, planQuery(catalog, { where }), planMeta, request, (row) => {
313
354
  const token = tupleToken(row, targetFields);
314
355
  return token !== null && localTuples.has(token);
@@ -317,10 +358,10 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
317
358
  meta: planMeta,
318
359
  idbPlan: lowered.idbPlan
319
360
  };
320
- return attachRelatedRows(relName, entry, rows, lowered.finish(await executor.query(plan).toArray()), relation);
361
+ return attachRelatedRows(relName, entry, rows, lowered.finish(await executor.query(plan).toArray()), relation, catalog.fields);
321
362
  }
322
363
  /** Attach related rows by their join tuple, shaping each parent's collection independently. */
323
- function attachRelatedRows(relName, entry, rows, relatedRows, relation) {
364
+ function attachRelatedRows(relName, entry, rows, relatedRows, relation, fields) {
324
365
  const { cardinality, on: { localFields, targetFields } } = relation;
325
366
  if (cardinality === "1:N") {
326
367
  const grouped = /* @__PURE__ */ new Map();
@@ -339,7 +380,7 @@ function attachRelatedRows(relName, entry, rows, relatedRows, relation) {
339
380
  ...row,
340
381
  [relName]: groupFor(row).length
341
382
  }));
342
- const comparator = buildRowComparator(entry.state.orderBy);
383
+ const comparator = buildRowComparator(entry.state.orderBy, fields);
343
384
  const skip = entry.state.skip ?? 0;
344
385
  const take = entry.state.take;
345
386
  return rows.map((row) => {
@@ -411,6 +452,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
411
452
  this.#newGroupingKey = newGroupingKey ?? (() => `idb-op-${++_key}`);
412
453
  this.#includeRefinementMode = includeRefinementMode;
413
454
  }
455
+ /** Add a callback or shorthand predicate to a new accessor without mutating this one. */
414
456
  where(filter) {
415
457
  const expr = typeof filter === "function" ? filter(createModelAccessor()) : shorthandToFilterExpr(filter);
416
458
  if (expr === void 0) return this.#clone({});
@@ -425,6 +467,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
425
467
  skip(n) {
426
468
  return this.#clone({ skip: n });
427
469
  }
470
+ /** Add a collection or count refinement while preserving this accessor state. */
428
471
  include(relation, refineFn) {
429
472
  const entry = this.#resolveIncludeEntry(relation, refineFn);
430
473
  const newState = mergeAccessorState(this.#state, { includes: {
@@ -433,9 +476,11 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
433
476
  } });
434
477
  return new IdbStoreAccessorImpl(this.#contract, this.#modelName, this.#executor, newState, this.#newGroupingKey, this.#includeRefinementMode);
435
478
  }
479
+ /** Narrow scalar output fields while retaining included relations. */
436
480
  select(...fields) {
437
481
  return this.#clone({ selectedFields: fields });
438
482
  }
483
+ /** Read matching rows, load includes, and project fields before yielding. */
439
484
  all() {
440
485
  const groupingKey = this.#newGroupingKey();
441
486
  const buildScanPlan = this.#buildScanPlan.bind(this);
@@ -453,9 +498,11 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
453
498
  async first() {
454
499
  return this.take(1).all().first();
455
500
  }
501
+ /** Reduce all filtered rows; query pagination does not constrain aggregates. */
456
502
  async aggregate(fn) {
457
503
  const spec = fn(createAggregateBuilder());
458
- assertValidAggregateSpec(spec, "aggregate()");
504
+ const { fields } = buildCatalog(this.#contract, this.#storeName, this.#modelName);
505
+ assertValidAggregateSpec(spec, "aggregate()", fields);
459
506
  const combined = this.#combinedFilterExpr();
460
507
  const ast = {
461
508
  kind: "aggregate",
@@ -463,18 +510,21 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
463
510
  aggregates: toAggregateRequests(spec),
464
511
  ...combined !== void 0 ? { where: combined } : {}
465
512
  };
466
- return computeAggregateSpec(spec, await this.#materialize(this.#newGroupingKey(), ast));
513
+ return computeAggregateSpec(spec, await this.#materialize(this.#newGroupingKey(), ast), fields);
467
514
  }
515
+ /** Carry this filter into a grouped accessor whose materialization is owned here. */
468
516
  groupBy(...fields) {
469
517
  const combined = this.#combinedFilterExpr();
470
518
  const materialize = (ast) => this.#materialize(this.#newGroupingKey(), ast);
471
519
  return createGroupedAccessor({
472
520
  modelName: this.#modelName,
473
521
  by: fields,
522
+ fields: buildCatalog(this.#contract, this.#storeName, this.#modelName).fields,
474
523
  where: combined,
475
524
  materialize
476
525
  });
477
526
  }
527
+ /** Complete and validate one record, using a transaction when relations need checks. */
478
528
  async create(data) {
479
529
  const record = data;
480
530
  if (hasNestedMutationCallbacks(this.#contract, this.#modelName, record)) return await executeNestedCreateMutation({
@@ -483,8 +533,9 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
483
533
  modelName: this.#modelName,
484
534
  data: record
485
535
  });
486
- const withDefaults = applyCreateDefaults(this.#contract.execution?.mutations.defaults, this.#storeName, record, createMutationDefaultsCache());
536
+ const withDefaults = completeCreateRecord(this.#contract, this.#modelName, record, createMutationDefaultsCache());
487
537
  assertEnumValues(this.#contract, this.#modelName, withDefaults);
538
+ assertValidRecord(this.#contract, this.#modelName, "create", withDefaults);
488
539
  if (hasScalarFkFields(this.#contract, this.#modelName, withDefaults)) return await executeScalarCreateWithFkValidation({
489
540
  executor: requireTransactionExecutor(this.#executor),
490
541
  contract: this.#contract,
@@ -507,38 +558,29 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
507
558
  record: withDefaults
508
559
  }
509
560
  };
510
- for await (const row of this.#executor.query(plan)) return row;
511
- return withDefaults;
561
+ const [row] = await this.#executor.query(plan).toArray();
562
+ return row ?? withDefaults;
512
563
  }
564
+ /** Resolve a primary key numerically for Decimal fields while retaining its stored spelling. */
513
565
  async findUnique(key) {
514
- const groupingKey = this.#newGroupingKey();
515
- const meta = this.#planMeta(groupingKey);
566
+ if (!isValidIdbKey(key)) return null;
567
+ const meta = this.#planMeta(this.#newGroupingKey());
516
568
  const ast = {
517
569
  kind: "findUnique",
518
570
  modelName: this.#modelName,
519
571
  key
520
572
  };
521
- const keyPath = getKeyPath(this.#contract, this.#modelName);
522
- const fields = typeof keyPath === "string" ? [keyPath] : keyPath;
523
- const values = typeof keyPath === "string" ? [key] : key;
524
- const where = combineFilterExprs(fields.map((field, i) => ({
525
- kind: "field",
526
- field,
527
- op: "eq",
528
- value: values[i]
529
- })));
530
- const request = {
531
- ...where === void 0 ? {} : { where },
532
- take: 1
533
- };
534
- const lowered = this.#lowerRows(meta, request);
535
- const rows = await this.#executor.query({
573
+ const row = await resolveDecimalKey({ execute: (body) => this.#executor.query({
536
574
  meta,
537
575
  ast,
538
- idbPlan: lowered.idbPlan
539
- }).toArray();
540
- return lowered.finish(rows)[0] ?? null;
576
+ idbPlan: {
577
+ ...body,
578
+ meta
579
+ }
580
+ }).toArray() }, this.#contract, this.#modelName, key);
581
+ return row === void 0 ? null : absentAsNull(this.#contract, this.#modelName)(row);
541
582
  }
583
+ /** Delete a keyed row with child actions when its relations require them. */
542
584
  async delete(key) {
543
585
  if (hasEnforceableChildRelations(this.#contract, this.#modelName)) {
544
586
  await executeDeleteWithReferentialActions({
@@ -567,6 +609,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
567
609
  };
568
610
  await this.#executor.query(plan).toArray();
569
611
  }
612
+ /** Update the first matching row with scalar or nested relation validation. */
570
613
  async update(patch) {
571
614
  const patchRecord = patch;
572
615
  if (hasNestedMutationCallbacks(this.#contract, this.#modelName, patchRecord)) return await executeNestedUpdateMutation({
@@ -614,9 +657,12 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
614
657
  async updateCount(patch) {
615
658
  return (await this.updateAll(patch).toArray()).length;
616
659
  }
660
+ /** Select and apply the create or update arm inside one transaction. */
617
661
  async upsert(args) {
662
+ const where = snapshotLookup(args.where);
663
+ assertUniqueLookup(buildCatalog(this.#contract, this.#storeName, this.#modelName), where);
618
664
  const keyPath = getKeyPath(this.#contract, this.#modelName);
619
- const whereExpr = shorthandToFilterExpr(args.where);
665
+ const whereExpr = shorthandToFilterExpr(where);
620
666
  const meta = this.#planMeta(this.#newGroupingKey());
621
667
  const createRecord = args.create;
622
668
  const patchRecord = args.update;
@@ -624,9 +670,11 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
624
670
  const executionDefaults = this.#contract.execution?.mutations.defaults;
625
671
  const exec = requireTransactionExecutor(this.#executor);
626
672
  const effectivePatch = applyUpdateDefaults(executionDefaults, storeName, patchRecord, createMutationDefaultsCache());
627
- const createWithDefaults = applyCreateDefaults(executionDefaults, storeName, createRecord, createMutationDefaultsCache());
673
+ const createWithDefaults = completeCreateRecord(this.#contract, this.#modelName, createRecord, createMutationDefaultsCache());
628
674
  assertEnumValues(this.#contract, this.#modelName, effectivePatch);
629
675
  assertEnumValues(this.#contract, this.#modelName, createWithDefaults);
676
+ assertValidRecord(this.#contract, this.#modelName, "update", effectivePatch);
677
+ assertValidRecord(this.#contract, this.#modelName, "create", createWithDefaults);
630
678
  const { storeNames: onUpdateStoreNames } = collectOnUpdateEnforcementStoreNames(this.#contract, this.#modelName, effectivePatch);
631
679
  const storeNames = [.../* @__PURE__ */ new Set([
632
680
  storeName,
@@ -648,19 +696,20 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
648
696
  await validateScalarFks(scope, this.#contract, this.#modelName, effectivePatch, existing);
649
697
  const key = extractKeyFromRow(existing, keyPath);
650
698
  await applyReferentialActionsForRowOnUpdate(scope, this.#contract, this.#modelName, existing, effectivePatch);
651
- return (await scope.execute({
699
+ const rows = await scope.execute({
652
700
  meta,
653
701
  kind: "update",
654
702
  storeName,
655
703
  key,
656
704
  patch: effectivePatch
657
- }))[0] ?? existing;
705
+ });
706
+ return absentAsNull(this.#contract, this.#modelName)(rows[0] ?? existing);
658
707
  });
659
708
  }
709
+ /** Complete a batch with shared query defaults and insert it atomically. */
660
710
  createAll(data) {
661
711
  const defaultsCache = createMutationDefaultsCache();
662
- const executionDefaults = this.#contract.execution?.mutations.defaults;
663
- const records = data.map((d) => applyCreateDefaults(executionDefaults, this.#storeName, d, defaultsCache));
712
+ const records = data.map((d) => completeCreateRecord(this.#contract, this.#modelName, d, defaultsCache));
664
713
  if (records.some((row) => hasScalarFkFields(this.#contract, this.#modelName, row))) {
665
714
  const executor = requireTransactionExecutor(this.#executor);
666
715
  const contract = this.#contract;
@@ -700,7 +749,10 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
700
749
  const contract = this.#contract;
701
750
  const modelName = this.#modelName;
702
751
  return new AsyncIterableResult((async function* () {
703
- for (const record of records) assertEnumValues(contract, modelName, record);
752
+ for (const record of records) {
753
+ assertEnumValues(contract, modelName, record);
754
+ assertValidRecord(contract, modelName, "create", record);
755
+ }
704
756
  for await (const row of executorQuery(plan)) yield row;
705
757
  })());
706
758
  }
@@ -735,10 +787,12 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
735
787
  async deleteCount() {
736
788
  return (await this.deleteAll().toArray()).length;
737
789
  }
790
+ /** Return an include-count descriptor during refinement, or execute a top-level count. */
738
791
  count() {
739
792
  if (this.#includeRefinementMode) return createIncludeScalar(this.#state);
740
793
  return this.#countTerminal();
741
794
  }
795
+ /** Count this filtered page through the planner, without applying select or include. */
742
796
  async #countTerminal() {
743
797
  const combined = this.#combinedFilterExpr();
744
798
  const { skip, take } = this.#state;
@@ -748,7 +802,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
748
802
  ...take === void 0 ? {} : { take }
749
803
  };
750
804
  const meta = this.#planMeta(this.#newGroupingKey());
751
- const catalog = buildCatalog(this.#contract, this.#storeName);
805
+ const catalog = buildCatalog(this.#contract, this.#storeName, this.#modelName);
752
806
  const lowered = lowerCount(catalog, planQuery(catalog, request), meta, request);
753
807
  const ast = {
754
808
  kind: "count",
@@ -806,9 +860,10 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
806
860
  return lowered.finish(rows);
807
861
  }
808
862
  #lowerRows(meta, request) {
809
- const catalog = buildCatalog(this.#contract, this.#storeName);
863
+ const catalog = buildCatalog(this.#contract, this.#storeName, this.#modelName);
810
864
  return lowerRows(catalog, planQuery(catalog, request), meta, request);
811
865
  }
866
+ /** Pair the query intent with its planned driver operation and result shaping. */
812
867
  #buildScanPlan(groupingKey) {
813
868
  const combined = this.#combinedFilterExpr();
814
869
  const meta = this.#planMeta(groupingKey);
@@ -839,6 +894,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
839
894
  #combinedFilterExpr() {
840
895
  return combineFilterExprs(this.#state.filters);
841
896
  }
897
+ /** Batch-load each requested relation using the unprojected parent rows. */
842
898
  async #applyIncludes(rows, groupingKey) {
843
899
  const relNames = Object.keys(this.#state.includes);
844
900
  if (relNames.length === 0) return rows;
@@ -864,6 +920,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
864
920
  return out;
865
921
  });
866
922
  }
923
+ /** Attach the contract hash and grouping key shared by this operation. */
867
924
  #planMeta(groupingKey) {
868
925
  return {
869
926
  target: "idb",
@@ -872,6 +929,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
872
929
  annotations: { groupingKey }
873
930
  };
874
931
  }
932
+ /** Preserve shared runtime wiring while replacing only the accessor state. */
875
933
  #clone(overrides) {
876
934
  return new IdbStoreAccessorImpl(this.#contract, this.#modelName, this.#executor, mergeAccessorState(this.#state, overrides), this.#newGroupingKey, this.#includeRefinementMode);
877
935
  }
@@ -891,7 +949,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
891
949
  * import { createIdbRuntime } from "@prisma-idb/runtime-idb/runtime";
892
950
  * import contract from "./prisma/idb-contract";
893
951
  *
894
- * const runtime = createIdbRuntime({ adapter, driver });
952
+ * const runtime = createIdbRuntime({ adapter, driver, contract });
895
953
  * const db = idbOrm({ contract, executor: runtime });
896
954
  *
897
955
  * // Typed query builder:
@@ -900,7 +958,8 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
900
958
  * ```
901
959
  */
902
960
  function idbOrm(options) {
903
- const { contract, executor } = options;
961
+ const { contract } = options;
962
+ const executor = withDecimalIdentityExecutor(contract, options.executor);
904
963
  let _key = 0;
905
964
  const newGroupingKey = () => `idb-op-${++_key}`;
906
965
  const client = {};
@@ -908,6 +967,243 @@ function idbOrm(options) {
908
967
  return client;
909
968
  }
910
969
  //#endregion
911
- export { idbOrm as t };
970
+ //#region src/core/open-transaction.ts
971
+ /**
972
+ * An executor bound to one already-open IDB transaction.
973
+ *
974
+ * `db.transaction()` hands its callback ORM accessors built on this executor.
975
+ * Every query the accessors issue runs on the open transaction instead of
976
+ * opening a new one. Operations that normally open their own mutation scope
977
+ * (FK-checked writes, cascade deletes) get a view of the same transaction:
978
+ * `commit()` on the view does nothing, so an inner operation cannot commit the
979
+ * outer transaction, and `rollback()` aborts it.
980
+ *
981
+ * The executor also records how the transaction ended, so the runner can tell
982
+ * a rolled-back transaction from one that IndexedDB auto-committed early
983
+ * (ADR 005).
984
+ */
985
+ function isTransactionInactive(error) {
986
+ return error instanceof IdbExecuteError && error.code === "TRANSACTION_INACTIVE";
987
+ }
988
+ /** Bind ORM operations to one scope and record aborts or early commits. */
989
+ var OpenTransaction = class {
990
+ #scope;
991
+ #storeNames;
992
+ #meta;
993
+ #state = "open";
994
+ #autoCommitCause;
995
+ #ended = false;
996
+ constructor(scope, storeNames, meta) {
997
+ this.#scope = scope;
998
+ this.#storeNames = new Set(storeNames);
999
+ this.#meta = meta;
1000
+ }
1001
+ /** Stop accepting operations. Called once the callback has settled. */
1002
+ end() {
1003
+ this.#ended = true;
1004
+ }
1005
+ /** The error of the first operation that found the transaction already finished, if any. */
1006
+ get autoCommitCause() {
1007
+ return this.#autoCommitCause;
1008
+ }
1009
+ /**
1010
+ * Whether IndexedDB has already finished this transaction on its own, so a
1011
+ * rollback can no longer undo its writes. Probes the transaction when no
1012
+ * operation has failed yet, and records the probe error as the cause, so call
1013
+ * it before rolling back or committing.
1014
+ */
1015
+ async hasAutoCommitted() {
1016
+ if (this.#state !== "open") return this.#state === "auto-committed";
1017
+ const [storeName] = this.#storeNames;
1018
+ try {
1019
+ await this.#scope.execute({
1020
+ meta: this.#meta,
1021
+ kind: "key-get",
1022
+ storeName,
1023
+ key: ""
1024
+ });
1025
+ return false;
1026
+ } catch (error) {
1027
+ if (!isTransactionInactive(error)) return false;
1028
+ this.#state = "auto-committed";
1029
+ this.#autoCommitCause = error;
1030
+ return true;
1031
+ }
1032
+ }
1033
+ /** Expose rows from the existing scope through the ORM query-result interface. */
1034
+ query(plan) {
1035
+ const run = executeMutationPlan.bind(null, this);
1036
+ return new AsyncIterableResult((async function* () {
1037
+ for (const row of await run(plan.idbPlan)) yield row;
1038
+ })());
1039
+ }
1040
+ /** Reuse the outer scope after verifying that the requested stores were declared. */
1041
+ async transaction(storeNames) {
1042
+ this.#assertNotEnded();
1043
+ try {
1044
+ this.#assertStoresListed(storeNames);
1045
+ } catch (error) {
1046
+ this.#recordFailure(error);
1047
+ throw error;
1048
+ }
1049
+ return this;
1050
+ }
1051
+ /** Run one operation in the scope and abort the transaction when it fails. */
1052
+ async execute(plan) {
1053
+ this.#assertNotEnded();
1054
+ try {
1055
+ this.#assertStoresListed([plan.storeName]);
1056
+ return await this.#scope.execute(plan);
1057
+ } catch (error) {
1058
+ this.#recordFailure(error);
1059
+ throw error;
1060
+ }
1061
+ }
1062
+ async commit() {}
1063
+ rollback() {
1064
+ this.#recordFailure(void 0);
1065
+ }
1066
+ /**
1067
+ * Wrap an ORM accessor so that any call that fails aborts the transaction.
1068
+ *
1069
+ * Some failures, such as enum validation, happen before an operation reaches
1070
+ * this executor. Without this wrapper the callback could catch them and
1071
+ * commit the writes made before. The wrapper covers errors thrown
1072
+ * synchronously, rejected promises, and rejections of a result that is
1073
+ * awaited directly (`AsyncIterableResult`). Objects the accessor returns,
1074
+ * such as `where()` builders, are wrapped too.
1075
+ */
1076
+ abortOnFailure(target) {
1077
+ return new Proxy(target, { get: (object, property) => {
1078
+ const value = Reflect.get(object, property, object);
1079
+ if (typeof value !== "function") return value;
1080
+ if (property === "then") return this.#observeThen(object, value);
1081
+ return (...args) => {
1082
+ const result = this.#recordThrown(() => value.apply(object, args));
1083
+ if (result instanceof Promise) {
1084
+ result.catch((error) => this.#recordFailure(error));
1085
+ return result;
1086
+ }
1087
+ return typeof result === "object" && result !== null ? this.abortOnFailure(result) : result;
1088
+ };
1089
+ } });
1090
+ }
1091
+ /**
1092
+ * `await result` passes its own rejection handler to `then()`, which turns the
1093
+ * rejection into a fulfilled promise. Record the failure before that handler runs.
1094
+ */
1095
+ #observeThen(object, then) {
1096
+ return (onFulfilled, onRejected) => then.call(object, onFulfilled, (error) => {
1097
+ this.#recordFailure(error);
1098
+ if (typeof onRejected !== "function") throw error;
1099
+ return onRejected(error);
1100
+ });
1101
+ }
1102
+ /** Record synchronous accessor failures before propagating them to the callback. */
1103
+ #recordThrown(call) {
1104
+ try {
1105
+ return call();
1106
+ } catch (error) {
1107
+ this.#recordFailure(error);
1108
+ throw error;
1109
+ }
1110
+ }
1111
+ #assertNotEnded() {
1112
+ if (this.#ended) throw new Error("This transaction has ended. Do not use `tx` after the db.transaction() callback finishes.");
1113
+ }
1114
+ /** Reject undeclared stores before an operation can escape the transaction scope. */
1115
+ #assertStoresListed(storeNames) {
1116
+ const missing = storeNames.filter((name) => !this.#storeNames.has(name));
1117
+ if (missing.length > 0) throw new Error(`Stores ${missing.map((name) => `"${name}"`).join(", ")} are not part of this transaction. List the models that own them in the first argument of db.transaction().`);
1118
+ }
1119
+ /** Any failed operation aborts the whole transaction, even if the callback catches the error. */
1120
+ #recordFailure(error) {
1121
+ if (this.#state !== "open") return;
1122
+ if (isTransactionInactive(error)) {
1123
+ this.#state = "auto-committed";
1124
+ this.#autoCommitCause = error;
1125
+ return;
1126
+ }
1127
+ this.#state = "aborted";
1128
+ this.#scope.rollback();
1129
+ }
1130
+ };
1131
+ //#endregion
1132
+ //#region src/core/orm-transaction.ts
1133
+ /**
1134
+ * Thrown when IndexedDB finished the transaction before the callback did, so
1135
+ * writes made before that point were committed and could not be rolled back.
1136
+ *
1137
+ * IndexedDB commits a transaction as soon as it has no pending request. That
1138
+ * happens when the callback awaits something other than a `tx` operation, such
1139
+ * as a timer or a network call (ADR 005). `cause` holds the original error.
1140
+ */
1141
+ var IdbTransactionCommittedEarlyError = class extends Error {
1142
+ constructor(cause) {
1143
+ super("The transaction was committed before the db.transaction() callback finished, so writes made earlier in the callback were saved and not rolled back. Await only `tx` operations inside the callback; do not await timers or network calls.", { cause });
1144
+ this.name = "IdbTransactionCommittedEarlyError";
1145
+ }
1146
+ };
1147
+ /**
1148
+ * Run `fn` inside one readwrite transaction over the models behind `rootKeys`.
1149
+ *
1150
+ * Besides the listed models, the transaction opens the stores their operations
1151
+ * need: parents for foreign-key checks, children for referential actions, and
1152
+ * related models for `include`. An operation that needs any other store fails
1153
+ * with an error that names it.
1154
+ */
1155
+ async function runOrmTransaction(options, rootKeys, fn) {
1156
+ const { contract, executor } = options;
1157
+ if (rootKeys.length === 0) throw new Error("db.transaction() needs at least one model to open the transaction on.");
1158
+ const modelNames = rootKeys.map((key) => {
1159
+ const root = contract.roots[key];
1160
+ if (root === void 0) throw new Error(`db.transaction(): "${key}" is not a root of the contract.`);
1161
+ return root.model;
1162
+ });
1163
+ const storeNames = [...new Set(modelNames.flatMap((model) => storeNamesNeededBy(contract, model)))];
1164
+ const scope = await executor.transaction(storeNames, "readwrite");
1165
+ const open = new OpenTransaction(scope, storeNames, makePlanMeta(contract));
1166
+ let result;
1167
+ try {
1168
+ result = await fn(pickRoots(idbOrm({
1169
+ contract,
1170
+ executor: open
1171
+ }), rootKeys, open));
1172
+ } catch (error) {
1173
+ open.end();
1174
+ if (await open.hasAutoCommitted()) await failCommittedEarly(scope, error);
1175
+ scope.rollback();
1176
+ throw error;
1177
+ }
1178
+ open.end();
1179
+ if (await open.hasAutoCommitted()) await failCommittedEarly(scope, open.autoCommitCause);
1180
+ await scope.commit();
1181
+ return result;
1182
+ }
1183
+ /**
1184
+ * Reports a transaction that IndexedDB committed before the callback ended. The writes
1185
+ * persisted, so the scope is committed rather than rolled back: a tracked scope only
1186
+ * announces its writes (for example, sync outbox notifications) when it commits.
1187
+ */
1188
+ async function failCommittedEarly(scope, cause) {
1189
+ await scope.commit().catch(() => {});
1190
+ throw new IdbTransactionCommittedEarlyError(cause);
1191
+ }
1192
+ /** Collect stores used by relation reads, child actions, and cascading updates. */
1193
+ function storeNamesNeededBy(contract, modelName) {
1194
+ return [
1195
+ ...collectRelatedStoreNames(contract, modelName),
1196
+ ...collectDeleteStoreNames(contract, modelName),
1197
+ ...collectOnUpdateClosureStoreNames(contract, modelName)
1198
+ ];
1199
+ }
1200
+ /** Expose only the requested roots and abort the scope on any accessor failure. */
1201
+ function pickRoots(orm, rootKeys, open) {
1202
+ const picked = {};
1203
+ for (const key of rootKeys) picked[key] = open.abortOnFailure(orm[key]);
1204
+ return picked;
1205
+ }
1206
+ //#endregion
1207
+ export { runOrmTransaction as n, idbOrm as r, IdbTransactionCommittedEarlyError as t };
912
1208
 
913
- //# sourceMappingURL=idb-orm-B1Br1C0F.mjs.map
1209
+ //# sourceMappingURL=orm-transaction-BGx1eukw.mjs.map