@prisma-idb/client-idb 0.9.1 → 0.11.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.
@@ -0,0 +1,1401 @@
1
+ import { C as assertEnumValues, D as getRelation, E as getKeyPath, O as getStoreName, S as createMutationDefaultsCache, T as fieldValueToken, _ as requireTransactionExecutor, a as collectScalarFkStoreNames, b as applyCreateDefaults, c as executeDeleteWithReferentialActions, d as executeScalarCreateAllWithFkValidation, f as executeScalarCreateWithFkValidation, g as hasScalarFkFields, h as hasNestedMutationCallbacks, i as collectOnUpdateEnforcementStoreNames, l as executeNestedCreateMutation, m as hasEnforceableChildRelations, n as applyReferentialActionsForRowOnUpdate, o as executeBulkUpdateWithFkValidation, p as executeScalarUpdateWithFkValidation, s as executeDeleteAllWithReferentialActions, u as executeNestedUpdateMutation, v as validateScalarFks, w as extractKeyFromRow, x as applyUpdateDefaults, y as withMutationScope } from "./mutation-executor-SJqTuSbb.mjs";
2
+ import { AsyncIterableResult } from "@prisma/orm-framework/components/runtime";
3
+ import { andExpr, evaluateFilter, fieldFilter, nullCheckExpr, shorthandToFilterExpr } from "@prisma-idb/adapter-idb/runtime";
4
+ import { domainModelsAtDefaultNamespace } from "@prisma/orm-framework/contract/types";
5
+ import { compareFieldValues, isValidIdbKey, keyEquals } from "@prisma-idb/target-idb/runtime";
6
+ //#region src/core/model-accessor.ts
7
+ /**
8
+ * Proxy-based typed accessor handed to `where(fn)` callbacks.
9
+ *
10
+ * Mirrors `createModelAccessor()` from
11
+ * Prisma ORM's SQL client, but without
12
+ * the codec-trait gating — IDB stores native JS values, so every operator
13
+ * is available on every field. The runtime accessor object is a Proxy
14
+ * that materialises an `IdbFieldAccessor` on demand for whichever field
15
+ * name is read; the type-level surface narrows that to the model's
16
+ * actual field set so callbacks get autocomplete.
17
+ */
18
+ /**
19
+ * Build an {@link IdbFieldAccessor} for a single field. The accessor is
20
+ * shared via the proxy below, but each accessor instance is bound to its
21
+ * own field name so the produced AST nodes carry the right field.
22
+ */
23
+ function createFieldAccessor(field) {
24
+ return {
25
+ eq: (value) => fieldFilter(field, "eq", value),
26
+ neq: (value) => fieldFilter(field, "neq", value),
27
+ gt: (value) => fieldFilter(field, "gt", value),
28
+ lt: (value) => fieldFilter(field, "lt", value),
29
+ gte: (value) => fieldFilter(field, "gte", value),
30
+ lte: (value) => fieldFilter(field, "lte", value),
31
+ in: (values) => fieldFilter(field, "in", values),
32
+ notIn: (values) => fieldFilter(field, "notIn", values),
33
+ contains: (sub) => fieldFilter(field, "contains", sub),
34
+ startsWith: (sub) => fieldFilter(field, "startsWith", sub),
35
+ endsWith: (sub) => fieldFilter(field, "endsWith", sub),
36
+ isNull: () => nullCheckExpr(field, true),
37
+ isNotNull: () => nullCheckExpr(field, false)
38
+ };
39
+ }
40
+ /**
41
+ * Create the typed model accessor used by `where(fn)` callbacks.
42
+ *
43
+ * Implemented as a Proxy keyed on field names — every read returns an
44
+ * {@link IdbFieldAccessor} bound to that field. The accessor cache means
45
+ * `m.name.eq(...)` and `m.name.startsWith(...)` reuse the same per-field
46
+ * accessor object across the same `where()` invocation.
47
+ *
48
+ * The type parameter is structural — no contract is consulted at
49
+ * runtime, because TS already gates the visible field set at compile
50
+ * time. (Misspelled fields surface as accessor calls on names that
51
+ * don't exist in stored rows, which is consistent with how the
52
+ * shorthand form behaves today.)
53
+ */
54
+ function createModelAccessor() {
55
+ const cache = /* @__PURE__ */ new Map();
56
+ return new Proxy({}, { get(_target, prop) {
57
+ if (typeof prop !== "string") return void 0;
58
+ let acc = cache.get(prop);
59
+ if (acc === void 0) {
60
+ acc = createFieldAccessor(prop);
61
+ cache.set(prop, acc);
62
+ }
63
+ return acc;
64
+ } });
65
+ }
66
+ //#endregion
67
+ //#region src/core/store-state.ts
68
+ /** Build an {@link IdbIncludeScalar} marker for a `count()` reducer. */
69
+ function createIncludeScalar(state) {
70
+ return {
71
+ kind: "includeScalar",
72
+ fn: "count",
73
+ state
74
+ };
75
+ }
76
+ /** Type guard: is `value` an {@link IdbIncludeScalar} marker? */
77
+ function isIncludeScalar(value) {
78
+ if (typeof value !== "object" || value === null) return false;
79
+ const candidate = value;
80
+ return candidate.kind === "includeScalar" && candidate.fn === "count";
81
+ }
82
+ /** Create a fresh, empty accessor state (no filters, no ordering, no includes). */
83
+ function emptyAccessorState() {
84
+ return {
85
+ filters: [],
86
+ includes: {}
87
+ };
88
+ }
89
+ /** Return a shallow-merged copy of `state` with the given overrides applied. */
90
+ function mergeAccessorState(state, overrides) {
91
+ return {
92
+ ...state,
93
+ ...overrides
94
+ };
95
+ }
96
+ //#endregion
97
+ //#region src/core/query-shaping.ts
98
+ /**
99
+ * Combine accumulated filter expressions with AND.
100
+ *
101
+ * Returns `undefined` when no filter is present so callers can skip building a
102
+ * row-filter closure. Shared by {@link IdbStoreAccessorImpl} (top-level scans)
103
+ * and the relation loader (refined `include()` child scans).
104
+ */
105
+ function combineFilterExprs(filters) {
106
+ if (filters.length === 0) return void 0;
107
+ if (filters.length === 1) return filters[0];
108
+ return andExpr(filters);
109
+ }
110
+ /**
111
+ * Build an in-memory comparator from an `orderBy` spec (field → direction).
112
+ *
113
+ * Returns `undefined` when there is nothing to sort by. Compares fields in
114
+ * declaration order, with {@link compareFieldValues}: the same order an
115
+ * IndexedDB index uses, so equal `Date`s tie and fall through to the next
116
+ * field, and `null`s sort last (first when descending).
117
+ */
118
+ function buildRowComparator(orderBy) {
119
+ if (orderBy === void 0) return void 0;
120
+ return (a, b) => {
121
+ for (const [field, dir] of Object.entries(orderBy)) {
122
+ const cmp = compareFieldValues(a[field], b[field]);
123
+ if (cmp === 0) continue;
124
+ return dir === "desc" ? -cmp : cmp;
125
+ }
126
+ return 0;
127
+ };
128
+ }
129
+ //#endregion
130
+ //#region src/core/aggregate-builder.ts
131
+ /**
132
+ * Build the aggregate selector factory handed to `.aggregate(agg => …)` and
133
+ * `groupBy(...).aggregate(agg => …)`.
134
+ *
135
+ * Each method returns an {@link IdbAggregateSelector} marker; the actual
136
+ * reduction happens in {@link reduceAggregate} once the matching rows are
137
+ * materialised in memory. Mirrors `createAggregateBuilder` from the vendor
138
+ * `sql-orm-client/aggregate-builder.ts`, minus the field→column mapping (IDB
139
+ * stores native field names).
140
+ */
141
+ function createAggregateBuilder() {
142
+ return {
143
+ count: () => ({
144
+ kind: "aggregate",
145
+ fn: "count"
146
+ }),
147
+ sum: (field) => ({
148
+ kind: "aggregate",
149
+ fn: "sum",
150
+ field
151
+ }),
152
+ avg: (field) => ({
153
+ kind: "aggregate",
154
+ fn: "avg",
155
+ field
156
+ }),
157
+ min: (field) => ({
158
+ kind: "aggregate",
159
+ fn: "min",
160
+ field
161
+ }),
162
+ max: (field) => ({
163
+ kind: "aggregate",
164
+ fn: "max",
165
+ field
166
+ })
167
+ };
168
+ }
169
+ /** Type guard: is `value` an {@link IdbAggregateSelector}? */
170
+ function isAggregateSelector(value) {
171
+ if (typeof value !== "object" || value === null) return false;
172
+ const candidate = value;
173
+ return candidate.kind === "aggregate" && (candidate.fn === "count" || candidate.fn === "sum" || candidate.fn === "avg" || candidate.fn === "min" || candidate.fn === "max");
174
+ }
175
+ /**
176
+ * Reduce a set of materialised rows to a single aggregate value.
177
+ *
178
+ * - `count` → the number of rows (always a number, never null).
179
+ * - `sum`/`avg`/`min`/`max` → computed over the non-null numeric values of
180
+ * `field`; `null` when no row has a non-null value (matching Prisma's
181
+ * "aggregate of an empty set is null" and the vendor `coerceAggregateValue`
182
+ * null handling). String-encoded numbers are coerced via `Number()`.
183
+ */
184
+ function reduceAggregate(fn, field, rows) {
185
+ if (fn === "count") return rows.length;
186
+ if (field === void 0) return null;
187
+ const values = [];
188
+ for (const row of rows) {
189
+ const raw = row[field];
190
+ if (raw === null || raw === void 0) continue;
191
+ const n = typeof raw === "bigint" ? Number(raw) : Number(raw);
192
+ if (!Number.isNaN(n)) values.push(n);
193
+ }
194
+ if (values.length === 0) return null;
195
+ switch (fn) {
196
+ case "sum": return values.reduce((acc, n) => acc + n, 0);
197
+ case "avg": return values.reduce((acc, n) => acc + n, 0) / values.length;
198
+ case "min": return Math.min(...values);
199
+ case "max": return Math.max(...values);
200
+ }
201
+ }
202
+ /**
203
+ * Run every selector in an aggregate spec against `rows`, producing the
204
+ * result object keyed by the spec's aliases.
205
+ */
206
+ function computeAggregateSpec(spec, rows) {
207
+ const result = {};
208
+ for (const [alias, selector] of Object.entries(spec)) result[alias] = reduceAggregate(selector.fn, selector.field, rows);
209
+ return result;
210
+ }
211
+ /**
212
+ * Project an aggregate spec down to the plain `{ fn, field? }` shape carried by
213
+ * the query AST (`IdbAggregateAst.aggregates` / `IdbGroupByAst.aggregates`), so
214
+ * middleware can observe the requested aggregations.
215
+ */
216
+ function toAggregateRequests(spec) {
217
+ const out = {};
218
+ for (const [alias, selector] of Object.entries(spec)) out[alias] = selector.field !== void 0 ? {
219
+ fn: selector.fn,
220
+ field: selector.field
221
+ } : { fn: selector.fn };
222
+ return out;
223
+ }
224
+ /**
225
+ * Validate a user-supplied aggregate spec: it must be non-empty and every
226
+ * value must be a real selector. Throws a descriptive error otherwise, so a
227
+ * typo (e.g. returning a plain object) fails loudly rather than silently
228
+ * producing `NaN`/`undefined` results.
229
+ */
230
+ function assertValidAggregateSpec(spec, context) {
231
+ const entries = Object.entries(spec);
232
+ if (entries.length === 0) throw new Error(`${context} requires at least one aggregation selector`);
233
+ for (const [alias, selector] of entries) if (!isAggregateSelector(selector)) throw new Error(`${context} selector "${alias}" is invalid`);
234
+ }
235
+ //#endregion
236
+ //#region src/core/grouped-accessor.ts
237
+ /**
238
+ * Build a composite group key from type-tagged field tokens. Equal Date, binary,
239
+ * array and JSON object values share a group; null and undefined also share a group.
240
+ */
241
+ function groupKeyOf(by, row) {
242
+ return JSON.stringify(by.map((field) => groupToken(row[field])));
243
+ }
244
+ /** Keep `1n`, `1` and `"1"` apart while preserving structural grouping for JSON objects. */
245
+ function groupToken(value) {
246
+ if (value instanceof Date && Number.isNaN(value.getTime())) return ["date", "invalid"];
247
+ const token = fieldValueToken(value ?? null);
248
+ return [typeof token, typeof token === "object" ? token : String(token)];
249
+ }
250
+ /** Build a grouped accessor that reduces materialized rows from the source accessor. */
251
+ function createGroupedAccessor(init) {
252
+ return { async aggregate(fn) {
253
+ const spec = fn(createAggregateBuilder());
254
+ assertValidAggregateSpec(spec, "groupBy().aggregate()");
255
+ const ast = {
256
+ kind: "groupBy",
257
+ modelName: init.modelName,
258
+ by: init.by,
259
+ aggregates: toAggregateRequests(spec),
260
+ ...init.where !== void 0 ? { where: init.where } : {}
261
+ };
262
+ const rows = await init.materialize(ast);
263
+ const groups = /* @__PURE__ */ new Map();
264
+ for (const row of rows) {
265
+ const gk = groupKeyOf(init.by, row);
266
+ let group = groups.get(gk);
267
+ if (group === void 0) {
268
+ const key = {};
269
+ for (const field of init.by) key[field] = row[field];
270
+ group = {
271
+ key,
272
+ rows: []
273
+ };
274
+ groups.set(gk, group);
275
+ }
276
+ group.rows.push(row);
277
+ }
278
+ return Array.from(groups.values()).map((group) => ({
279
+ ...group.key,
280
+ ...computeAggregateSpec(spec, group.rows)
281
+ }));
282
+ } };
283
+ }
284
+ //#endregion
285
+ //#region src/core/planner/catalog.ts
286
+ const catalogs = /* @__PURE__ */ new WeakMap();
287
+ /** Build a catalog once per contract identity and store name. Contracts are immutable. */
288
+ function buildCatalog(contract, storeName) {
289
+ const cached = catalogs.get(contract)?.get(storeName);
290
+ if (cached) return cached;
291
+ const store = contract.storage.stores[storeName];
292
+ if (!store) throw new Error(`Store "${storeName}" is not in the contract`);
293
+ const fields = {};
294
+ for (const model of Object.values(domainModelsAtDefaultNamespace(contract.domain))) {
295
+ if (model.storage.storeName !== storeName) continue;
296
+ for (const [name, field] of Object.entries(model.fields)) fields[name] = Object.freeze({
297
+ codecId: field.type.kind === "scalar" ? field.type.codecId : void 0,
298
+ nullable: field.nullable,
299
+ collection: field.many === true || field.dict === true
300
+ });
301
+ }
302
+ const catalog = Object.freeze({
303
+ storeName,
304
+ primaryKey: source(store.keyPath, true, false),
305
+ indexes: Object.freeze(Object.entries(store.indexes ?? {}).map(([name, index]) => source(index.keyPath, index.unique, index.multiEntry ?? false, name))),
306
+ fields: Object.freeze(fields)
307
+ });
308
+ let byStore = catalogs.get(contract);
309
+ if (!byStore) {
310
+ byStore = /* @__PURE__ */ new Map();
311
+ catalogs.set(contract, byStore);
312
+ }
313
+ byStore.set(storeName, catalog);
314
+ return catalog;
315
+ }
316
+ function source(keyPath, unique, multiEntry, indexName) {
317
+ const path = typeof keyPath === "string" ? keyPath : Object.freeze([...keyPath]);
318
+ return Object.freeze({
319
+ keyPath: path,
320
+ fields: Object.freeze(typeof path === "string" ? [path] : [...path]),
321
+ unique,
322
+ multiEntry,
323
+ ...indexName === void 0 ? {} : { indexName }
324
+ });
325
+ }
326
+ //#endregion
327
+ //#region src/core/planner/plan.ts
328
+ const COST = {
329
+ UNIQUE_POINT: 0,
330
+ PRIMARY_POINT: 1,
331
+ INDEX_POINT: 2,
332
+ PREFIX_POINT: 3,
333
+ TWO_SIDED_RANGE: 4,
334
+ ONE_SIDED_RANGE: 5,
335
+ ORDER_ONLY_SCAN: 6
336
+ };
337
+ const KEY_PARTIAL_CODECS = /* @__PURE__ */ new Set(["idb/double@1", "idb/date@1"]);
338
+ const KEY_CODECS = /* @__PURE__ */ new Set([
339
+ "idb/string@1",
340
+ "idb/int32@1",
341
+ "idb/double@1",
342
+ "idb/date@1",
343
+ "idb/decimal@1",
344
+ "idb/bytes@1"
345
+ ]);
346
+ /** Choose the cheapest safe access without opening IndexedDB or probing cardinality. */
347
+ function planQuery(catalog, request) {
348
+ const normalized = normalize(request.where);
349
+ if (normalized.empty) return {
350
+ access: { kind: "empty" },
351
+ exact: true
352
+ };
353
+ const orders = Object.entries(request.orderBy ?? {});
354
+ if (normalized.excluded || request.hasRelationFilter || orders.length > 1) return full(false);
355
+ const constraints = /* @__PURE__ */ new Map();
356
+ for (const atom of normalized.atoms) {
357
+ let constraint = constraints.get(atom.field);
358
+ if (!constraint) {
359
+ constraint = { consumed: 0 };
360
+ constraints.set(atom.field, constraint);
361
+ }
362
+ addAtom(constraint, atom, catalog.fields[atom.field]);
363
+ }
364
+ for (const [name, constraint] of constraints) {
365
+ if (keyPartial(catalog.fields[name]) && !constraint.rejectsNonKeys) {
366
+ delete constraint.lower;
367
+ delete constraint.upper;
368
+ constraint.consumed = 0;
369
+ }
370
+ if (contradictory(constraint)) return {
371
+ access: { kind: "empty" },
372
+ exact: true
373
+ };
374
+ }
375
+ const sources = [catalog.primaryKey, ...catalog.indexes];
376
+ const candidates = sources.flatMap((source, declaration) => {
377
+ const candidate = filteringCandidate(catalog, source, constraints, declaration);
378
+ return candidate ? [candidate] : [];
379
+ });
380
+ 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);
381
+ let selected = candidates[0];
382
+ let direction;
383
+ if (orders.length === 1 && request.take !== void 0) {
384
+ const [field, order] = orders[0];
385
+ if (selected) {
386
+ if (singleFieldSource(selected.source, field)) direction = order === "asc" ? "next" : "prev";
387
+ } else {
388
+ const ordering = sources.find((source) => !source.multiEntry && singleFieldSource(source, field) && completeKeyField(catalog.fields[field]));
389
+ if (ordering) {
390
+ selected = {
391
+ source: ordering,
392
+ ranges: [void 0],
393
+ cost: COST.ORDER_ONLY_SCAN,
394
+ consumed: 0,
395
+ declaration: sources.indexOf(ordering)
396
+ };
397
+ direction = order === "asc" ? "next" : "prev";
398
+ }
399
+ }
400
+ }
401
+ if (!selected) {
402
+ const partialOrder = orders.length === 1 && request.take !== void 0 && keyPartial(catalog.fields[orders[0][0]]);
403
+ return full(normalized.atoms.length + normalized.residual === 0 && !partialOrder);
404
+ }
405
+ return {
406
+ access: {
407
+ kind: "ranges",
408
+ source: selected.source,
409
+ ranges: selected.ranges
410
+ },
411
+ exact: selected.consumed === normalized.atoms.length && normalized.residual === 0,
412
+ ...direction === void 0 ? {} : { direction }
413
+ };
414
+ }
415
+ function full(exact) {
416
+ return {
417
+ access: { kind: "full" },
418
+ exact
419
+ };
420
+ }
421
+ function normalize(where) {
422
+ const result = {
423
+ atoms: [],
424
+ residual: 0,
425
+ excluded: false,
426
+ empty: false
427
+ };
428
+ function visit(expr) {
429
+ switch (expr.kind) {
430
+ case "field":
431
+ if (expr.op === "in" && (!Array.isArray(expr.value) || expr.value.length === 0)) result.empty = true;
432
+ result.atoms.push(expr);
433
+ break;
434
+ case "and":
435
+ expr.exprs.forEach(visit);
436
+ break;
437
+ case "or": {
438
+ if (expr.exprs.length === 0) {
439
+ result.empty = true;
440
+ break;
441
+ }
442
+ const branches = expr.exprs;
443
+ const first = branches[0];
444
+ if (first.kind !== "field" || !branches.every((branch) => branch.kind === "field" && branch.field === first.field && (branch.op === "eq" || branch.op === "in"))) {
445
+ result.excluded = true;
446
+ break;
447
+ }
448
+ const values = branches.flatMap((branch) => {
449
+ const atom = branch;
450
+ return atom.op === "eq" ? [atom.value] : Array.isArray(atom.value) ? atom.value : [];
451
+ });
452
+ visit({
453
+ kind: "field",
454
+ field: first.field,
455
+ op: "in",
456
+ value: values
457
+ });
458
+ break;
459
+ }
460
+ case "not":
461
+ result.excluded = true;
462
+ break;
463
+ case "null-check": result.residual++;
464
+ }
465
+ }
466
+ if (where) visit(where);
467
+ return result;
468
+ }
469
+ function keyTyped(field) {
470
+ return field !== void 0 && !field.collection && KEY_CODECS.has(field.codecId ?? "");
471
+ }
472
+ function keyPartial(field) {
473
+ return KEY_PARTIAL_CODECS.has(field?.codecId ?? "");
474
+ }
475
+ function completeKeyField(field, constraint) {
476
+ return keyTyped(field) && field?.nullable === false && (!keyPartial(field) || constraint?.rejectsNonKeys === true);
477
+ }
478
+ function singleFieldSource(source, field) {
479
+ return source.fields.length === 1 && source.fields[0] === field;
480
+ }
481
+ function addAtom(constraint, atom, field) {
482
+ if (atom.op === "eq" || atom.op === "in") {
483
+ const values = atom.op === "eq" ? [atom.value] : Array.isArray(atom.value) ? atom.value : [];
484
+ if (!values.every(isValidIdbKey)) return;
485
+ const points = uniqueKeys(values);
486
+ constraint.points = constraint.points === void 0 ? points : constraint.points.filter((point) => points.some((value) => keyEquals(point, value)));
487
+ constraint.rejectsNonKeys = true;
488
+ constraint.consumed++;
489
+ return;
490
+ }
491
+ if (!keyTyped(field)) return;
492
+ if (atom.op === "startsWith") {
493
+ if (field?.codecId !== "idb/string@1" || typeof atom.value !== "string") return;
494
+ intersectBound(constraint, "lower", {
495
+ value: atom.value,
496
+ open: false
497
+ });
498
+ const upper = stringSuccessor(atom.value);
499
+ if (upper !== void 0) intersectBound(constraint, "upper", {
500
+ value: upper,
501
+ open: true
502
+ });
503
+ constraint.startsWith = true;
504
+ constraint.consumed++;
505
+ } else if ([
506
+ "gt",
507
+ "gte",
508
+ "lt",
509
+ "lte"
510
+ ].includes(atom.op) && isValidIdbKey(atom.value)) {
511
+ const side = atom.op === "gt" || atom.op === "gte" ? "lower" : "upper";
512
+ const strict = atom.op === "gt" || atom.op === "lt";
513
+ intersectBound(constraint, side, {
514
+ value: atom.value,
515
+ open: strict
516
+ });
517
+ if (strict) constraint.rejectsNonKeys = true;
518
+ constraint.consumed++;
519
+ }
520
+ }
521
+ function uniqueKeys(values) {
522
+ const sorted = [...values].sort(compareFieldValues);
523
+ return sorted.filter((value, i) => i === 0 || compareFieldValues(sorted[i - 1], value) !== 0);
524
+ }
525
+ function intersectBound(constraint, side, bound) {
526
+ const previous = constraint[side];
527
+ const comparison = previous === void 0 ? 0 : compareFieldValues(bound.value, previous.value);
528
+ if (!previous || (side === "lower" ? comparison > 0 : comparison < 0) || comparison === 0 && bound.open) constraint[side] = bound;
529
+ }
530
+ function insideBounds(value, constraint) {
531
+ const lower = constraint.lower;
532
+ const upper = constraint.upper;
533
+ if (lower && (compareFieldValues(value, lower.value) < 0 || lower.open && keyEquals(value, lower.value))) return false;
534
+ if (upper && (compareFieldValues(value, upper.value) > 0 || upper.open && keyEquals(value, upper.value))) return false;
535
+ return true;
536
+ }
537
+ function contradictory(constraint) {
538
+ if (constraint.points) {
539
+ constraint.points = constraint.points.filter((value) => insideBounds(value, constraint));
540
+ return constraint.points.length === 0;
541
+ }
542
+ if (!constraint.lower || !constraint.upper) return false;
543
+ const comparison = compareFieldValues(constraint.lower.value, constraint.upper.value);
544
+ return comparison > 0 || comparison === 0 && (constraint.lower.open || constraint.upper.open);
545
+ }
546
+ function filteringCandidate(catalog, source, constraints, declaration) {
547
+ if (source.multiEntry || source.fields.length === 0) return void 0;
548
+ if (source.fields.some((name) => keyPartial(catalog.fields[name]) && !constraints.get(name)?.rejectsNonKeys)) return void 0;
549
+ let prefixes = [[]];
550
+ let consumed = 0;
551
+ let position = 0;
552
+ while (position < source.fields.length) {
553
+ const constraint = constraints.get(source.fields[position]);
554
+ if (constraint?.points === void 0) break;
555
+ if (prefixes.length * constraint.points.length > 1024) return void 0;
556
+ prefixes = prefixes.flatMap((prefix) => constraint.points.map((point) => [...prefix, point]));
557
+ consumed += constraint.consumed;
558
+ position++;
559
+ }
560
+ const constraint = constraints.get(source.fields[position] ?? "");
561
+ const hasRange = constraint !== void 0 && (constraint.lower !== void 0 || constraint.upper !== void 0);
562
+ if (position === 0 && !hasRange) return void 0;
563
+ const used = position + Number(hasRange);
564
+ if (!source.fields.slice(used).every((field) => completeKeyField(catalog.fields[field], constraints.get(field)))) return void 0;
565
+ const compound = typeof source.keyPath !== "string";
566
+ if (position === source.fields.length) return {
567
+ source,
568
+ ranges: prefixes.map((prefix) => ({
569
+ kind: "only",
570
+ key: compound ? prefix : prefix[0]
571
+ })),
572
+ cost: source.indexName !== void 0 && source.unique ? COST.UNIQUE_POINT : source.indexName === void 0 ? COST.PRIMARY_POINT : COST.INDEX_POINT,
573
+ consumed,
574
+ declaration
575
+ };
576
+ if (hasRange) consumed += constraint.consumed;
577
+ const ranges = prefixes.map((prefix) => compound ? compoundRange(prefix, constraint, source.fields.length - used) : scalarRange(constraint));
578
+ 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;
579
+ return {
580
+ source,
581
+ ranges,
582
+ cost: hasRange ? constraint.lower && constraint.upper && !constraint.startsWith ? COST.TWO_SIDED_RANGE : COST.ONE_SIDED_RANGE : COST.PREFIX_POINT,
583
+ consumed,
584
+ declaration
585
+ };
586
+ }
587
+ function scalarRange(constraint) {
588
+ if (constraint.lower && constraint.upper) return {
589
+ kind: "bound",
590
+ lower: constraint.lower.value,
591
+ upper: constraint.upper.value,
592
+ lowerOpen: constraint.lower.open,
593
+ upperOpen: constraint.upper.open
594
+ };
595
+ if (constraint.lower) return {
596
+ kind: "lower",
597
+ key: constraint.lower.value,
598
+ open: constraint.lower.open
599
+ };
600
+ return {
601
+ kind: "upper",
602
+ key: constraint.upper.value,
603
+ open: constraint.upper.open
604
+ };
605
+ }
606
+ function compoundRange(prefix, constraint, trailing) {
607
+ const lower = constraint?.lower;
608
+ const upper = constraint?.upper;
609
+ return {
610
+ kind: "bound",
611
+ lower: lower ? [
612
+ ...prefix,
613
+ lower.value,
614
+ ...lower.open && trailing > 0 ? [[]] : []
615
+ ] : prefix.length ? prefix : [-Infinity],
616
+ upper: upper ? [
617
+ ...prefix,
618
+ upper.value,
619
+ ...!upper.open && trailing > 0 ? [[]] : []
620
+ ] : [...prefix, []],
621
+ lowerOpen: lower?.open ?? false,
622
+ upperOpen: upper ? upper.open || trailing > 0 : true
623
+ };
624
+ }
625
+ function stringSuccessor(prefix) {
626
+ for (let i = prefix.length - 1; i >= 0; i--) {
627
+ const code = prefix.charCodeAt(i);
628
+ if (code < 65535) return prefix.slice(0, i) + String.fromCharCode(code + 1);
629
+ }
630
+ }
631
+ //#endregion
632
+ //#region src/core/planner/lower.ts
633
+ /**
634
+ * Lower row access without creating IDBKeyRange objects or awaiting work.
635
+ * Run `finish` after the executor has materialized all returned rows.
636
+ * Without `orderBy`, row order and pagination selection are unspecified.
637
+ * Rows tied on every ordering field may appear in any order, including at
638
+ * pagination boundaries. Add a unique ordering field for deterministic pages.
639
+ */
640
+ function lowerRows(catalog, logical, meta, request, additionalFilter) {
641
+ const filter = (row) => (request.where === void 0 || evaluateFilter(request.where, row)) && (additionalFilter === void 0 || additionalFilter(row));
642
+ const comparator = logical.direction === void 0 ? buildRowComparator(request.orderBy) : void 0;
643
+ const base = {
644
+ meta,
645
+ storeName: catalog.storeName
646
+ };
647
+ const pagination = {
648
+ ...request.skip === void 0 ? {} : { skip: request.skip },
649
+ ...request.take === void 0 ? {} : { take: request.take }
650
+ };
651
+ let shapedByDriver = false;
652
+ let idbPlan;
653
+ switch (logical.access.kind) {
654
+ case "empty":
655
+ idbPlan = {
656
+ meta,
657
+ kind: "batch",
658
+ storeNames: [catalog.storeName],
659
+ ops: []
660
+ };
661
+ break;
662
+ case "full":
663
+ idbPlan = {
664
+ ...base,
665
+ kind: "cursor-scan",
666
+ filter,
667
+ ...comparator === void 0 ? {} : { comparator },
668
+ ...pagination
669
+ };
670
+ shapedByDriver = true;
671
+ break;
672
+ case "ranges": {
673
+ const { source } = logical.access;
674
+ const ranges = [...logical.access.ranges];
675
+ if (logical.direction === "prev") ranges.reverse();
676
+ const single = ranges.length === 1;
677
+ const ops = ranges.map((range) => {
678
+ if (range?.kind === "only" && source.indexName === void 0) return {
679
+ ...base,
680
+ kind: "key-get",
681
+ key: range.key
682
+ };
683
+ const access = {
684
+ ...base,
685
+ ...source.indexName === void 0 ? {} : { indexName: source.indexName },
686
+ ...range === void 0 ? {} : { range }
687
+ };
688
+ if (!single && logical.direction !== void 0 && request.take !== void 0) return {
689
+ ...access,
690
+ kind: "cursor-scan",
691
+ direction: logical.direction,
692
+ filter,
693
+ take: (request.skip ?? 0) + request.take
694
+ };
695
+ if (single && request.take !== void 0) {
696
+ shapedByDriver = true;
697
+ return {
698
+ ...access,
699
+ kind: "cursor-scan",
700
+ filter,
701
+ ...logical.direction === void 0 ? {} : { direction: logical.direction },
702
+ ...comparator === void 0 ? {} : { comparator },
703
+ ...pagination
704
+ };
705
+ }
706
+ if (logical.direction === "prev") return {
707
+ ...access,
708
+ kind: "cursor-scan",
709
+ direction: "prev",
710
+ filter
711
+ };
712
+ return {
713
+ ...access,
714
+ kind: "get-all"
715
+ };
716
+ });
717
+ idbPlan = single ? ops[0] : {
718
+ meta,
719
+ kind: "batch",
720
+ storeNames: [catalog.storeName],
721
+ ops
722
+ };
723
+ break;
724
+ }
725
+ }
726
+ return {
727
+ idbPlan,
728
+ finish(rows) {
729
+ let result = rows.filter(filter);
730
+ if (!shapedByDriver) {
731
+ if (comparator !== void 0) result.sort(comparator);
732
+ const skip = request.skip ?? 0;
733
+ result = result.slice(skip, request.take === void 0 ? void 0 : skip + request.take);
734
+ }
735
+ return result;
736
+ }
737
+ };
738
+ }
739
+ //#endregion
740
+ //#region src/core/relation-loader.ts
741
+ /**
742
+ * Batch-load a single named relation for all rows in `rows` and attach the
743
+ * result to each row under the `relName` key.
744
+ *
745
+ * The join matches every field of the relation, so compound foreign keys join
746
+ * on the whole tuple. It plans one batch lookup for all parents, then groups
747
+ * the rows in memory, which avoids N+1 queries.
748
+ *
749
+ * The `entry` carries any `include()` refinement:
750
+ *
751
+ * - `collection` — the refined `where` further filters the child scan;
752
+ * `orderBy` / `skip` / `take` are applied **per parent group** for `1:N`
753
+ * relations (each parent's children are independently sorted and paginated).
754
+ * - `scalar` — the relation field becomes the `count` of matching children
755
+ * (to-many only; `include()` rejects scalar refinements on to-one relations).
756
+ *
757
+ * @param relName - The relation key to load (e.g. `"posts"`, `"author"`).
758
+ * @param entry - How to materialise the relation (collection vs scalar + refinement state).
759
+ * @param rows - The parent rows to attach related data to.
760
+ * @param contract - The resolved IDB contract.
761
+ * @param modelName - The source model name (owner of the relation).
762
+ * @param executor - The query executor used to run the related-store scan.
763
+ */
764
+ async function loadRelation(relName, entry, rows, contract, modelName, executor, groupingKey) {
765
+ if (rows.length === 0) return rows;
766
+ const models = domainModelsAtDefaultNamespace(contract.domain);
767
+ const model = models[modelName];
768
+ if (model === void 0) return rows;
769
+ const rawRelation = model.relations[relName];
770
+ if (rawRelation === void 0) return rows;
771
+ if (!("on" in rawRelation)) return rows;
772
+ const relation = rawRelation;
773
+ const { cardinality, on } = relation;
774
+ const relatedModelName = relation.to.model;
775
+ const localFields = on.localFields;
776
+ const targetFields = on.targetFields;
777
+ if (localFields.length === 0 || localFields.length !== targetFields.length) return rows;
778
+ if (models[relatedModelName] === void 0) return rows;
779
+ const relatedStoreName = getStoreName(contract, relatedModelName);
780
+ const isScalar = entry.kind === "scalar";
781
+ const localTuples = /* @__PURE__ */ new Map();
782
+ for (const row of rows) {
783
+ const token = tupleToken(row, localFields);
784
+ if (token !== null) localTuples.set(token, localFields.map((f) => row[f]));
785
+ }
786
+ if (localTuples.size === 0) return rows.map((row) => ({
787
+ ...row,
788
+ [relName]: isScalar ? 0 : cardinality === "1:N" ? [] : null
789
+ }));
790
+ const refinedWhere = combineFilterExprs(entry.state.filters);
791
+ const planMeta = {
792
+ target: "idb",
793
+ storageHash: contract.storage.storageHash,
794
+ lane: "idb-orm",
795
+ annotations: { groupingKey }
796
+ };
797
+ const joinWhere = andExpr(targetFields.map((field, i) => fieldFilter(field, "in", [...localTuples.values()].map((tuple) => tuple[i]))));
798
+ const where = refinedWhere === void 0 ? joinWhere : andExpr([joinWhere, refinedWhere]);
799
+ const request = { ...refinedWhere === void 0 ? {} : { where: refinedWhere } };
800
+ const catalog = buildCatalog(contract, relatedStoreName);
801
+ const lowered = lowerRows(catalog, planQuery(catalog, { where }), planMeta, request, (row) => {
802
+ const token = tupleToken(row, targetFields);
803
+ return token !== null && localTuples.has(token);
804
+ });
805
+ const plan = {
806
+ meta: planMeta,
807
+ idbPlan: lowered.idbPlan
808
+ };
809
+ return attachRelatedRows(relName, entry, rows, lowered.finish(await executor.query(plan).toArray()), relation);
810
+ }
811
+ /** Attach related rows by their join tuple, shaping each parent's collection independently. */
812
+ function attachRelatedRows(relName, entry, rows, relatedRows, relation) {
813
+ const { cardinality, on: { localFields, targetFields } } = relation;
814
+ if (cardinality === "1:N") {
815
+ const grouped = /* @__PURE__ */ new Map();
816
+ for (const rrow of relatedRows) {
817
+ const gk = tupleToken(rrow, targetFields);
818
+ if (gk === null) continue;
819
+ const group = grouped.get(gk) ?? [];
820
+ group.push(rrow);
821
+ grouped.set(gk, group);
822
+ }
823
+ const groupFor = (row) => {
824
+ const token = tupleToken(row, localFields);
825
+ return token === null ? [] : grouped.get(token) ?? [];
826
+ };
827
+ if (entry.kind === "scalar") return rows.map((row) => ({
828
+ ...row,
829
+ [relName]: groupFor(row).length
830
+ }));
831
+ const comparator = buildRowComparator(entry.state.orderBy);
832
+ const skip = entry.state.skip ?? 0;
833
+ const take = entry.state.take;
834
+ return rows.map((row) => {
835
+ let group = groupFor(row);
836
+ if (comparator !== void 0) group = [...group].sort(comparator);
837
+ if (skip > 0 || take !== void 0) group = group.slice(skip, take !== void 0 ? skip + take : void 0);
838
+ return {
839
+ ...row,
840
+ [relName]: group
841
+ };
842
+ });
843
+ }
844
+ const indexed = /* @__PURE__ */ new Map();
845
+ for (const rrow of relatedRows) {
846
+ const token = tupleToken(rrow, targetFields);
847
+ if (token !== null) indexed.set(token, rrow);
848
+ }
849
+ return rows.map((row) => {
850
+ const token = tupleToken(row, localFields);
851
+ return {
852
+ ...row,
853
+ [relName]: (token === null ? void 0 : indexed.get(token)) ?? null
854
+ };
855
+ });
856
+ }
857
+ /**
858
+ * A `Map` key for the values of `fields` in `row`, or `null` when any of them
859
+ * is null or undefined. Equal `Date`s and binary values give equal tokens.
860
+ */
861
+ function tupleToken(row, fields) {
862
+ const parts = [];
863
+ for (const field of fields) {
864
+ const value = row[field];
865
+ if (value === null || value === void 0) return null;
866
+ const token = fieldValueToken(value);
867
+ parts.push([typeof token, String(token)]);
868
+ }
869
+ return JSON.stringify(parts);
870
+ }
871
+ //#endregion
872
+ //#region src/core/store-accessor.ts
873
+ /**
874
+ * Concrete immutable query builder.
875
+ *
876
+ * Internal details:
877
+ * - All state is in `#state` (filters, orderBy, skip, take, includes, selectedFields).
878
+ * - Builder methods clone via `#clone()` — O(1) copies since state is
879
+ * structurally shared.
880
+ * - `all()` materialises the main rows first, then batch-loads each included
881
+ * relation, then applies any `.select()` projection before yielding.
882
+ * - `#includeRefinementMode` flips `count()` from an async terminal to an
883
+ * {@link IdbIncludeScalar} marker so it can be used inside `include()`.
884
+ */
885
+ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
886
+ #contract;
887
+ #modelName;
888
+ #executor;
889
+ #storeName;
890
+ #state;
891
+ #newGroupingKey;
892
+ #includeRefinementMode;
893
+ constructor(contract, modelName, executor, state, newGroupingKey, includeRefinementMode = false) {
894
+ this.#contract = contract;
895
+ this.#modelName = modelName;
896
+ this.#executor = executor;
897
+ this.#storeName = getStoreName(contract, modelName);
898
+ this.#state = state ?? emptyAccessorState();
899
+ let _key = 0;
900
+ this.#newGroupingKey = newGroupingKey ?? (() => `idb-op-${++_key}`);
901
+ this.#includeRefinementMode = includeRefinementMode;
902
+ }
903
+ where(filter) {
904
+ const expr = typeof filter === "function" ? filter(createModelAccessor()) : shorthandToFilterExpr(filter);
905
+ if (expr === void 0) return this.#clone({});
906
+ return this.#clone({ filters: [...this.#state.filters, expr] });
907
+ }
908
+ orderBy(spec) {
909
+ return this.#clone({ orderBy: spec });
910
+ }
911
+ take(n) {
912
+ return this.#clone({ take: n });
913
+ }
914
+ skip(n) {
915
+ return this.#clone({ skip: n });
916
+ }
917
+ include(relation, refineFn) {
918
+ const entry = this.#resolveIncludeEntry(relation, refineFn);
919
+ const newState = mergeAccessorState(this.#state, { includes: {
920
+ ...this.#state.includes,
921
+ [relation]: entry
922
+ } });
923
+ return new IdbStoreAccessorImpl(this.#contract, this.#modelName, this.#executor, newState, this.#newGroupingKey, this.#includeRefinementMode);
924
+ }
925
+ select(...fields) {
926
+ return this.#clone({ selectedFields: fields });
927
+ }
928
+ all() {
929
+ const groupingKey = this.#newGroupingKey();
930
+ const buildScanPlan = this.#buildScanPlan.bind(this);
931
+ const executorQuery = this.#executor.query.bind(this.#executor);
932
+ const applyIncludes = this.#applyIncludes.bind(this);
933
+ const projectRows = this.#projectRows.bind(this);
934
+ return new AsyncIterableResult((async function* () {
935
+ const scan = buildScanPlan(groupingKey);
936
+ const rows = [];
937
+ for await (const row of executorQuery(scan.plan)) rows.push(row);
938
+ const withIncludes = await applyIncludes(scan.finish(rows), groupingKey);
939
+ for (const row of projectRows(withIncludes)) yield row;
940
+ })());
941
+ }
942
+ async first() {
943
+ return this.take(1).all().first();
944
+ }
945
+ async aggregate(fn) {
946
+ const spec = fn(createAggregateBuilder());
947
+ assertValidAggregateSpec(spec, "aggregate()");
948
+ const combined = this.#combinedFilterExpr();
949
+ const ast = {
950
+ kind: "aggregate",
951
+ modelName: this.#modelName,
952
+ aggregates: toAggregateRequests(spec),
953
+ ...combined !== void 0 ? { where: combined } : {}
954
+ };
955
+ return computeAggregateSpec(spec, await this.#materialize(this.#newGroupingKey(), ast));
956
+ }
957
+ groupBy(...fields) {
958
+ const combined = this.#combinedFilterExpr();
959
+ const materialize = (ast) => this.#materialize(this.#newGroupingKey(), ast);
960
+ return createGroupedAccessor({
961
+ modelName: this.#modelName,
962
+ by: fields,
963
+ where: combined,
964
+ materialize
965
+ });
966
+ }
967
+ async create(data) {
968
+ const record = data;
969
+ if (hasNestedMutationCallbacks(this.#contract, this.#modelName, record)) return await executeNestedCreateMutation({
970
+ executor: requireTransactionExecutor(this.#executor),
971
+ contract: this.#contract,
972
+ modelName: this.#modelName,
973
+ data: record
974
+ });
975
+ const withDefaults = applyCreateDefaults(this.#contract.execution?.mutations.defaults, this.#storeName, record, createMutationDefaultsCache());
976
+ assertEnumValues(this.#contract, this.#modelName, withDefaults);
977
+ if (hasScalarFkFields(this.#contract, this.#modelName, withDefaults)) return await executeScalarCreateWithFkValidation({
978
+ executor: requireTransactionExecutor(this.#executor),
979
+ contract: this.#contract,
980
+ modelName: this.#modelName,
981
+ data: withDefaults
982
+ });
983
+ const groupingKey = this.#newGroupingKey();
984
+ const meta = this.#planMeta(groupingKey);
985
+ const plan = {
986
+ meta,
987
+ ast: {
988
+ kind: "create",
989
+ modelName: this.#modelName,
990
+ data: withDefaults
991
+ },
992
+ idbPlan: {
993
+ meta,
994
+ kind: "add",
995
+ storeName: this.#storeName,
996
+ record: withDefaults
997
+ }
998
+ };
999
+ for await (const row of this.#executor.query(plan)) return row;
1000
+ return withDefaults;
1001
+ }
1002
+ async findUnique(key) {
1003
+ const groupingKey = this.#newGroupingKey();
1004
+ const meta = this.#planMeta(groupingKey);
1005
+ const ast = {
1006
+ kind: "findUnique",
1007
+ modelName: this.#modelName,
1008
+ key
1009
+ };
1010
+ const keyPath = getKeyPath(this.#contract, this.#modelName);
1011
+ const fields = typeof keyPath === "string" ? [keyPath] : keyPath;
1012
+ const values = typeof keyPath === "string" ? [key] : key;
1013
+ const where = combineFilterExprs(fields.map((field, i) => ({
1014
+ kind: "field",
1015
+ field,
1016
+ op: "eq",
1017
+ value: values[i]
1018
+ })));
1019
+ const request = {
1020
+ ...where === void 0 ? {} : { where },
1021
+ take: 1
1022
+ };
1023
+ const lowered = this.#lowerRows(meta, request);
1024
+ const rows = await this.#executor.query({
1025
+ meta,
1026
+ ast,
1027
+ idbPlan: lowered.idbPlan
1028
+ }).toArray();
1029
+ return lowered.finish(rows)[0] ?? null;
1030
+ }
1031
+ async delete(key) {
1032
+ if (hasEnforceableChildRelations(this.#contract, this.#modelName)) {
1033
+ await executeDeleteWithReferentialActions({
1034
+ executor: requireTransactionExecutor(this.#executor),
1035
+ contract: this.#contract,
1036
+ modelName: this.#modelName,
1037
+ key
1038
+ });
1039
+ return;
1040
+ }
1041
+ const groupingKey = this.#newGroupingKey();
1042
+ const meta = this.#planMeta(groupingKey);
1043
+ const plan = {
1044
+ meta,
1045
+ ast: {
1046
+ kind: "delete",
1047
+ modelName: this.#modelName,
1048
+ key
1049
+ },
1050
+ idbPlan: {
1051
+ meta,
1052
+ kind: "delete",
1053
+ storeName: this.#storeName,
1054
+ key
1055
+ }
1056
+ };
1057
+ await this.#executor.query(plan).toArray();
1058
+ }
1059
+ async update(patch) {
1060
+ const patchRecord = patch;
1061
+ if (hasNestedMutationCallbacks(this.#contract, this.#modelName, patchRecord)) return await executeNestedUpdateMutation({
1062
+ executor: requireTransactionExecutor(this.#executor),
1063
+ contract: this.#contract,
1064
+ modelName: this.#modelName,
1065
+ filters: this.#state.filters,
1066
+ data: patchRecord
1067
+ });
1068
+ return await executeScalarUpdateWithFkValidation({
1069
+ executor: requireTransactionExecutor(this.#executor),
1070
+ contract: this.#contract,
1071
+ modelName: this.#modelName,
1072
+ filters: this.#state.filters,
1073
+ data: patchRecord
1074
+ });
1075
+ }
1076
+ /**
1077
+ * Always routes through the transaction scope (`executeBulkUpdateWithFkValidation`),
1078
+ * unlike single-row `update()`'s plan-level fallback for the no-FK-field
1079
+ * case. A bulk scan-write's affected row set isn't knowable until it
1080
+ * actually runs — going through the transaction scope is what lets the
1081
+ * sync interceptor observe the real affected rows and write one outbox
1082
+ * event per row (see `SyncInterceptingTransactionScope#maybeTrack`'s
1083
+ * `scan-write` case), atomically with the write itself, instead of one
1084
+ * event for the whole batch with no way to recover which rows it covered.
1085
+ */
1086
+ updateAll(patch) {
1087
+ const contract = this.#contract;
1088
+ const modelName = this.#modelName;
1089
+ const filters = this.#state.filters;
1090
+ const patchRecord = patch;
1091
+ const executor = requireTransactionExecutor(this.#executor);
1092
+ return new AsyncIterableResult((async function* () {
1093
+ const rows = await executeBulkUpdateWithFkValidation({
1094
+ executor,
1095
+ contract,
1096
+ modelName,
1097
+ filters,
1098
+ data: patchRecord
1099
+ });
1100
+ for (const row of rows) yield row;
1101
+ })());
1102
+ }
1103
+ async updateCount(patch) {
1104
+ return (await this.updateAll(patch).toArray()).length;
1105
+ }
1106
+ async upsert(args) {
1107
+ const keyPath = getKeyPath(this.#contract, this.#modelName);
1108
+ const whereExpr = shorthandToFilterExpr(args.where);
1109
+ const matches = (row) => whereExpr === void 0 || evaluateFilter(whereExpr, row);
1110
+ const meta = this.#planMeta(this.#newGroupingKey());
1111
+ const createRecord = args.create;
1112
+ const patchRecord = args.update;
1113
+ const storeName = this.#storeName;
1114
+ const executionDefaults = this.#contract.execution?.mutations.defaults;
1115
+ const exec = requireTransactionExecutor(this.#executor);
1116
+ const effectivePatch = applyUpdateDefaults(executionDefaults, storeName, patchRecord, createMutationDefaultsCache());
1117
+ const createWithDefaults = applyCreateDefaults(executionDefaults, storeName, createRecord, createMutationDefaultsCache());
1118
+ assertEnumValues(this.#contract, this.#modelName, effectivePatch);
1119
+ assertEnumValues(this.#contract, this.#modelName, createWithDefaults);
1120
+ const { storeNames: onUpdateStoreNames } = collectOnUpdateEnforcementStoreNames(this.#contract, this.#modelName, effectivePatch);
1121
+ const storeNames = [.../* @__PURE__ */ new Set([
1122
+ storeName,
1123
+ ...onUpdateStoreNames,
1124
+ ...collectScalarFkStoreNames(this.#contract, this.#modelName, createWithDefaults),
1125
+ ...collectScalarFkStoreNames(this.#contract, this.#modelName, effectivePatch)
1126
+ ])];
1127
+ return withMutationScope(exec, storeNames, async (scope) => {
1128
+ const existing = (await scope.execute({
1129
+ meta,
1130
+ kind: "cursor-scan",
1131
+ storeName,
1132
+ filter: matches,
1133
+ take: 1
1134
+ }))[0];
1135
+ if (existing === void 0) {
1136
+ await validateScalarFks(scope, this.#contract, this.#modelName, createWithDefaults);
1137
+ return (await scope.execute({
1138
+ meta,
1139
+ kind: "add",
1140
+ storeName,
1141
+ record: createWithDefaults
1142
+ }))[0] ?? createWithDefaults;
1143
+ }
1144
+ await validateScalarFks(scope, this.#contract, this.#modelName, effectivePatch, existing);
1145
+ const key = extractKeyFromRow(existing, keyPath);
1146
+ await applyReferentialActionsForRowOnUpdate(scope, this.#contract, this.#modelName, existing, effectivePatch);
1147
+ return (await scope.execute({
1148
+ meta,
1149
+ kind: "update",
1150
+ storeName,
1151
+ key,
1152
+ patch: effectivePatch
1153
+ }))[0] ?? existing;
1154
+ });
1155
+ }
1156
+ createAll(data) {
1157
+ const defaultsCache = createMutationDefaultsCache();
1158
+ const executionDefaults = this.#contract.execution?.mutations.defaults;
1159
+ const records = data.map((d) => applyCreateDefaults(executionDefaults, this.#storeName, d, defaultsCache));
1160
+ if (records.some((row) => hasScalarFkFields(this.#contract, this.#modelName, row))) {
1161
+ const executor = requireTransactionExecutor(this.#executor);
1162
+ const contract = this.#contract;
1163
+ const modelName = this.#modelName;
1164
+ return new AsyncIterableResult((async function* () {
1165
+ const rows = await executeScalarCreateAllWithFkValidation({
1166
+ executor,
1167
+ contract,
1168
+ modelName,
1169
+ data: records
1170
+ });
1171
+ for (const row of rows) yield row;
1172
+ })());
1173
+ }
1174
+ const groupingKey = this.#newGroupingKey();
1175
+ const meta = this.#planMeta(groupingKey);
1176
+ const plan = {
1177
+ meta,
1178
+ ast: {
1179
+ kind: "createAll",
1180
+ modelName: this.#modelName,
1181
+ data: records
1182
+ },
1183
+ idbPlan: {
1184
+ meta,
1185
+ kind: "batch",
1186
+ storeNames: [this.#storeName],
1187
+ ops: records.map((record) => ({
1188
+ meta,
1189
+ kind: "add",
1190
+ storeName: this.#storeName,
1191
+ record
1192
+ }))
1193
+ }
1194
+ };
1195
+ const executorQuery = this.#executor.query.bind(this.#executor);
1196
+ const contract = this.#contract;
1197
+ const modelName = this.#modelName;
1198
+ return new AsyncIterableResult((async function* () {
1199
+ for (const record of records) assertEnumValues(contract, modelName, record);
1200
+ for await (const row of executorQuery(plan)) yield row;
1201
+ })());
1202
+ }
1203
+ async createCount(data) {
1204
+ return (await this.createAll(data).toArray()).length;
1205
+ }
1206
+ /**
1207
+ * Always routes through the transaction scope (`executeDeleteAllWithReferentialActions`),
1208
+ * not just when there are enforceable child relations to cascade — that
1209
+ * function already handles the no-relations case correctly (an empty
1210
+ * relation list is a no-op loop), and going through it unconditionally is
1211
+ * what lets the sync interceptor's transaction-scope tracking write one
1212
+ * outbox event per actually-deleted row instead of one lump event for
1213
+ * the whole batch with no way to know which rows it covered. See
1214
+ * `updateAll()`'s doc comment for the same reasoning.
1215
+ */
1216
+ deleteAll() {
1217
+ const combined = this.#combinedFilterExpr();
1218
+ const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
1219
+ const contract = this.#contract;
1220
+ const modelName = this.#modelName;
1221
+ const executor = requireTransactionExecutor(this.#executor);
1222
+ return new AsyncIterableResult((async function* () {
1223
+ const rows = await executeDeleteAllWithReferentialActions({
1224
+ executor,
1225
+ contract,
1226
+ modelName,
1227
+ ...filter !== void 0 ? { filter } : {}
1228
+ });
1229
+ for (const row of rows) yield row;
1230
+ })());
1231
+ }
1232
+ async deleteCount() {
1233
+ return (await this.deleteAll().toArray()).length;
1234
+ }
1235
+ count() {
1236
+ if (this.#includeRefinementMode) return createIncludeScalar(this.#state);
1237
+ return this.#countTerminal();
1238
+ }
1239
+ async #countTerminal() {
1240
+ const scan = this.#buildScanPlan(this.#newGroupingKey());
1241
+ const combined = this.#combinedFilterExpr();
1242
+ const ast = {
1243
+ kind: "count",
1244
+ modelName: this.#modelName,
1245
+ ...combined !== void 0 ? { where: combined } : {}
1246
+ };
1247
+ const rows = await this.#executor.query({
1248
+ ...scan.plan,
1249
+ ast
1250
+ }).toArray();
1251
+ return scan.finish(rows).length;
1252
+ }
1253
+ /**
1254
+ * Resolve an `include()` argument pair into an {@link IncludeEntry}: run the
1255
+ * optional refinement against a fresh refinement-mode child accessor, then
1256
+ * classify the result as a scalar count or a refined collection.
1257
+ */
1258
+ #resolveIncludeEntry(relation, refineFn) {
1259
+ if (refineFn === void 0) return {
1260
+ kind: "collection",
1261
+ state: emptyAccessorState()
1262
+ };
1263
+ const rel = getRelation(this.#contract, this.#modelName, relation);
1264
+ const relatedModelName = rel?.to.model ?? relation;
1265
+ const refined = refineFn(new IdbStoreAccessorImpl(this.#contract, relatedModelName, this.#executor, emptyAccessorState(), this.#newGroupingKey, true));
1266
+ if (isIncludeScalar(refined)) {
1267
+ if (rel !== void 0 && rel.cardinality !== "1:N") throw new Error(`include('${relation}'): count() is only supported for to-many (1:N) relations`);
1268
+ return {
1269
+ kind: "scalar",
1270
+ fn: refined.fn,
1271
+ state: refined.state
1272
+ };
1273
+ }
1274
+ if (refined instanceof IdbStoreAccessorImpl) return {
1275
+ kind: "collection",
1276
+ state: refined.#state
1277
+ };
1278
+ throw new Error(`include('${relation}') refinement must return the collection (for where/orderBy/take/skip) or a count() selector`);
1279
+ }
1280
+ /**
1281
+ * Materialise all rows matching the accumulated filters with no pagination —
1282
+ * used by `aggregate()` / `groupBy()`. The supplied `ast` is attached to the
1283
+ * row plan so middleware can observe the aggregate intent.
1284
+ */
1285
+ async #materialize(groupingKey, ast) {
1286
+ const combined = this.#combinedFilterExpr();
1287
+ const meta = this.#planMeta(groupingKey);
1288
+ const lowered = this.#lowerRows(meta, { ...combined === void 0 ? {} : { where: combined } });
1289
+ const rows = await this.#executor.query({
1290
+ meta,
1291
+ ast,
1292
+ idbPlan: lowered.idbPlan
1293
+ }).toArray();
1294
+ return lowered.finish(rows);
1295
+ }
1296
+ #lowerRows(meta, request) {
1297
+ const catalog = buildCatalog(this.#contract, this.#storeName);
1298
+ return lowerRows(catalog, planQuery(catalog, request), meta, request);
1299
+ }
1300
+ #buildScanPlan(groupingKey) {
1301
+ const combined = this.#combinedFilterExpr();
1302
+ const meta = this.#planMeta(groupingKey);
1303
+ const ast = {
1304
+ kind: "findMany",
1305
+ modelName: this.#modelName,
1306
+ ...combined !== void 0 ? { where: combined } : {},
1307
+ ...this.#state.orderBy !== void 0 ? { orderBy: this.#state.orderBy } : {},
1308
+ ...this.#state.skip !== void 0 ? { skip: this.#state.skip } : {},
1309
+ ...this.#state.take !== void 0 ? { take: this.#state.take } : {}
1310
+ };
1311
+ const lowered = this.#lowerRows(meta, ast);
1312
+ return {
1313
+ plan: {
1314
+ meta,
1315
+ ast,
1316
+ idbPlan: lowered.idbPlan
1317
+ },
1318
+ finish: lowered.finish
1319
+ };
1320
+ }
1321
+ /**
1322
+ * Combine all accumulated filter expressions with AND.
1323
+ *
1324
+ * Returns `undefined` when no filter has been installed so the planner sees
1325
+ * an unfiltered request. Delegates to the shared {@link combineFilterExprs}.
1326
+ */
1327
+ #combinedFilterExpr() {
1328
+ return combineFilterExprs(this.#state.filters);
1329
+ }
1330
+ async #applyIncludes(rows, groupingKey) {
1331
+ const relNames = Object.keys(this.#state.includes);
1332
+ if (relNames.length === 0) return rows;
1333
+ let result = rows;
1334
+ for (const relName of relNames) {
1335
+ const entry = this.#state.includes[relName];
1336
+ result = await loadRelation(relName, entry, result, this.#contract, this.#modelName, this.#executor, groupingKey);
1337
+ }
1338
+ return result;
1339
+ }
1340
+ /**
1341
+ * Apply a `.select()` projection (if any) to materialised rows. Keeps the
1342
+ * selected scalar fields plus every included relation key (which `include()`
1343
+ * attached during {@link #applyIncludes}); a no-op when nothing is selected.
1344
+ */
1345
+ #projectRows(rows) {
1346
+ const selected = this.#state.selectedFields;
1347
+ if (selected === void 0) return rows;
1348
+ const keep = [...selected, ...Object.keys(this.#state.includes)];
1349
+ return rows.map((row) => {
1350
+ const out = {};
1351
+ for (const field of keep) if (field in row) out[field] = row[field];
1352
+ return out;
1353
+ });
1354
+ }
1355
+ #planMeta(groupingKey) {
1356
+ return {
1357
+ target: "idb",
1358
+ storageHash: this.#contract.storage.storageHash,
1359
+ lane: "idb-orm",
1360
+ annotations: { groupingKey }
1361
+ };
1362
+ }
1363
+ #clone(overrides) {
1364
+ return new IdbStoreAccessorImpl(this.#contract, this.#modelName, this.#executor, mergeAccessorState(this.#state, overrides), this.#newGroupingKey, this.#includeRefinementMode);
1365
+ }
1366
+ };
1367
+ //#endregion
1368
+ //#region src/core/idb-orm.ts
1369
+ /**
1370
+ * Create a typed IDB ORM client from a contract and executor.
1371
+ *
1372
+ * The client exposes one `IdbStoreAccessor` per entry in `contract.roots`.
1373
+ * Only roots-declared stores are accessible at the top level — other stores
1374
+ * can be reached via `.include()` on any accessor.
1375
+ *
1376
+ * @example
1377
+ * ```ts
1378
+ * import { idbOrm } from "@prisma-idb/client-idb/orm";
1379
+ * import { createIdbRuntime } from "@prisma-idb/runtime-idb/runtime";
1380
+ * import contract from "./prisma/idb-contract";
1381
+ *
1382
+ * const runtime = createIdbRuntime({ adapter, driver });
1383
+ * const db = idbOrm({ contract, executor: runtime });
1384
+ *
1385
+ * // Typed query builder:
1386
+ * const alice = await db.users.where({ email: "alice@example.com" }).first();
1387
+ * const postsWithAuthor = await db.posts.include("author").all();
1388
+ * ```
1389
+ */
1390
+ function idbOrm(options) {
1391
+ const { contract, executor } = options;
1392
+ let _key = 0;
1393
+ const newGroupingKey = () => `idb-op-${++_key}`;
1394
+ const client = {};
1395
+ for (const [rootKey, ref] of Object.entries(contract.roots)) client[rootKey] = new IdbStoreAccessorImpl(contract, ref.model, executor, void 0, newGroupingKey);
1396
+ return client;
1397
+ }
1398
+ //#endregion
1399
+ export { idbOrm as t };
1400
+
1401
+ //# sourceMappingURL=idb-orm-BitJ-EMq.mjs.map