@prisma-next-idb/client-idb 0.1.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,1526 @@
1
+ import { AsyncIterableResult } from "@prisma-next/framework-components/runtime";
2
+ import { andExpr, evaluateFilter, fieldFilter, nullCheckExpr, shorthandToFilterExpr } from "@prisma-next-idb/adapter-idb/runtime";
3
+ import { domainModelsAtDefaultNamespace } from "@prisma-next/contract/types";
4
+ //#region src/core/types.ts
5
+ /**
6
+ * Extract the `storeName` from a model's storage metadata at runtime.
7
+ * Falls back to the model name if `storeName` is absent.
8
+ */
9
+ function getStoreName(contract, modelName) {
10
+ return (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.storeName ?? modelName;
11
+ }
12
+ /**
13
+ * Extract the `keyPath` from a model's storage metadata at runtime.
14
+ * Falls back to `"id"` (the invariant key name for all syncable IDB models).
15
+ */
16
+ function getKeyPath$1(contract, modelName) {
17
+ return (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.keyPath ?? "id";
18
+ }
19
+ /**
20
+ * Resolve a model's named relation to a {@link ContractReferenceRelation} at
21
+ * runtime, or `undefined` when the relation is absent or an embedded relation
22
+ * (no `on` join block). Used by `include()` to find the related model name and
23
+ * cardinality before building the child-accessor refinement.
24
+ */
25
+ function getRelation(contract, modelName, relName) {
26
+ const relation = domainModelsAtDefaultNamespace(contract.domain)[modelName]?.relations?.[relName];
27
+ if (relation === void 0 || !("on" in relation)) return void 0;
28
+ return relation;
29
+ }
30
+ //#endregion
31
+ //#region src/core/model-accessor.ts
32
+ /**
33
+ * Proxy-based typed accessor handed to `where(fn)` callbacks.
34
+ *
35
+ * Mirrors `createModelAccessor()` from
36
+ * `vendor/prisma-next/packages/3-extensions/sql-orm-client` but without
37
+ * the codec-trait gating — IDB stores native JS values, so every operator
38
+ * is available on every field. The runtime accessor object is a Proxy
39
+ * that materialises an `IdbFieldAccessor` on demand for whichever field
40
+ * name is read; the type-level surface narrows that to the model's
41
+ * actual field set so callbacks get autocomplete.
42
+ */
43
+ /**
44
+ * Build an {@link IdbFieldAccessor} for a single field. The accessor is
45
+ * shared via the proxy below, but each accessor instance is bound to its
46
+ * own field name so the produced AST nodes carry the right field.
47
+ */
48
+ function createFieldAccessor(field) {
49
+ return {
50
+ eq: (value) => fieldFilter(field, "eq", value),
51
+ neq: (value) => fieldFilter(field, "neq", value),
52
+ gt: (value) => fieldFilter(field, "gt", value),
53
+ lt: (value) => fieldFilter(field, "lt", value),
54
+ gte: (value) => fieldFilter(field, "gte", value),
55
+ lte: (value) => fieldFilter(field, "lte", value),
56
+ in: (values) => fieldFilter(field, "in", values),
57
+ notIn: (values) => fieldFilter(field, "notIn", values),
58
+ contains: (sub) => fieldFilter(field, "contains", sub),
59
+ startsWith: (sub) => fieldFilter(field, "startsWith", sub),
60
+ endsWith: (sub) => fieldFilter(field, "endsWith", sub),
61
+ isNull: () => nullCheckExpr(field, true),
62
+ isNotNull: () => nullCheckExpr(field, false)
63
+ };
64
+ }
65
+ /**
66
+ * Create the typed model accessor used by `where(fn)` callbacks.
67
+ *
68
+ * Implemented as a Proxy keyed on field names — every read returns an
69
+ * {@link IdbFieldAccessor} bound to that field. The accessor cache means
70
+ * `m.name.eq(...)` and `m.name.startsWith(...)` reuse the same per-field
71
+ * accessor object across the same `where()` invocation.
72
+ *
73
+ * The type parameter is structural — no contract is consulted at
74
+ * runtime, because TS already gates the visible field set at compile
75
+ * time. (Misspelled fields surface as accessor calls on names that
76
+ * don't exist in stored rows, which is consistent with how the
77
+ * shorthand form behaves today.)
78
+ */
79
+ function createModelAccessor() {
80
+ const cache = /* @__PURE__ */ new Map();
81
+ return new Proxy({}, { get(_target, prop) {
82
+ if (typeof prop !== "string") return void 0;
83
+ let acc = cache.get(prop);
84
+ if (acc === void 0) {
85
+ acc = createFieldAccessor(prop);
86
+ cache.set(prop, acc);
87
+ }
88
+ return acc;
89
+ } });
90
+ }
91
+ //#endregion
92
+ //#region src/core/store-state.ts
93
+ /** Build an {@link IdbIncludeScalar} marker for a `count()` reducer. */
94
+ function createIncludeScalar(state) {
95
+ return {
96
+ kind: "includeScalar",
97
+ fn: "count",
98
+ state
99
+ };
100
+ }
101
+ /** Type guard: is `value` an {@link IdbIncludeScalar} marker? */
102
+ function isIncludeScalar(value) {
103
+ if (typeof value !== "object" || value === null) return false;
104
+ const candidate = value;
105
+ return candidate.kind === "includeScalar" && candidate.fn === "count";
106
+ }
107
+ /** Create a fresh, empty accessor state (no filters, no ordering, no includes). */
108
+ function emptyAccessorState() {
109
+ return {
110
+ filters: [],
111
+ includes: {}
112
+ };
113
+ }
114
+ /** Return a shallow-merged copy of `state` with the given overrides applied. */
115
+ function mergeAccessorState(state, overrides) {
116
+ return {
117
+ ...state,
118
+ ...overrides
119
+ };
120
+ }
121
+ //#endregion
122
+ //#region src/core/query-shaping.ts
123
+ /**
124
+ * Combine accumulated filter expressions with AND.
125
+ *
126
+ * Returns `undefined` when no filter is present so callers can skip building a
127
+ * row-filter closure. Shared by {@link IdbStoreAccessorImpl} (top-level scans)
128
+ * and the relation loader (refined `include()` child scans).
129
+ */
130
+ function combineFilterExprs(filters) {
131
+ if (filters.length === 0) return void 0;
132
+ if (filters.length === 1) return filters[0];
133
+ return andExpr(filters);
134
+ }
135
+ /**
136
+ * Build an in-memory comparator from an `orderBy` spec (field → direction).
137
+ *
138
+ * Returns `undefined` when there is nothing to sort by. Compares fields in
139
+ * declaration order; values are primitives (strings, numbers, dates) in
140
+ * practice, so JS relational comparison is sufficient.
141
+ */
142
+ function buildRowComparator(orderBy) {
143
+ if (orderBy === void 0) return void 0;
144
+ return (a, b) => {
145
+ for (const [field, dir] of Object.entries(orderBy)) {
146
+ const av = a[field];
147
+ const bv = b[field];
148
+ if (av === bv) continue;
149
+ const cmp = av < bv ? -1 : 1;
150
+ return dir === "desc" ? -cmp : cmp;
151
+ }
152
+ return 0;
153
+ };
154
+ }
155
+ //#endregion
156
+ //#region src/core/aggregate-builder.ts
157
+ /**
158
+ * Build the aggregate selector factory handed to `.aggregate(agg => …)` and
159
+ * `groupBy(...).aggregate(agg => …)`.
160
+ *
161
+ * Each method returns a frozen {@link IdbAggregateSelector} marker; the actual
162
+ * reduction happens in {@link reduceAggregate} once the matching rows are
163
+ * materialised in memory. Mirrors `createAggregateBuilder` from the vendor
164
+ * `sql-orm-client/aggregate-builder.ts`, minus the field→column mapping (IDB
165
+ * stores native field names).
166
+ */
167
+ function createAggregateBuilder() {
168
+ return {
169
+ count: () => ({
170
+ kind: "aggregate",
171
+ fn: "count"
172
+ }),
173
+ sum: (field) => ({
174
+ kind: "aggregate",
175
+ fn: "sum",
176
+ field
177
+ }),
178
+ avg: (field) => ({
179
+ kind: "aggregate",
180
+ fn: "avg",
181
+ field
182
+ }),
183
+ min: (field) => ({
184
+ kind: "aggregate",
185
+ fn: "min",
186
+ field
187
+ }),
188
+ max: (field) => ({
189
+ kind: "aggregate",
190
+ fn: "max",
191
+ field
192
+ })
193
+ };
194
+ }
195
+ /** Type guard: is `value` an {@link IdbAggregateSelector}? */
196
+ function isAggregateSelector(value) {
197
+ if (typeof value !== "object" || value === null) return false;
198
+ const candidate = value;
199
+ return candidate.kind === "aggregate" && (candidate.fn === "count" || candidate.fn === "sum" || candidate.fn === "avg" || candidate.fn === "min" || candidate.fn === "max");
200
+ }
201
+ /**
202
+ * Reduce a set of materialised rows to a single aggregate value.
203
+ *
204
+ * - `count` → the number of rows (always a number, never null).
205
+ * - `sum`/`avg`/`min`/`max` → computed over the non-null numeric values of
206
+ * `field`; `null` when no row has a non-null value (matching Prisma's
207
+ * "aggregate of an empty set is null" and the vendor `coerceAggregateValue`
208
+ * null handling). String-encoded numbers are coerced via `Number()`.
209
+ */
210
+ function reduceAggregate(fn, field, rows) {
211
+ if (fn === "count") return rows.length;
212
+ if (field === void 0) return null;
213
+ const values = [];
214
+ for (const row of rows) {
215
+ const raw = row[field];
216
+ if (raw === null || raw === void 0) continue;
217
+ const n = typeof raw === "bigint" ? Number(raw) : Number(raw);
218
+ if (!Number.isNaN(n)) values.push(n);
219
+ }
220
+ if (values.length === 0) return null;
221
+ switch (fn) {
222
+ case "sum": return values.reduce((acc, n) => acc + n, 0);
223
+ case "avg": return values.reduce((acc, n) => acc + n, 0) / values.length;
224
+ case "min": return Math.min(...values);
225
+ case "max": return Math.max(...values);
226
+ }
227
+ }
228
+ /**
229
+ * Run every selector in an aggregate spec against `rows`, producing the
230
+ * result object keyed by the spec's aliases.
231
+ */
232
+ function computeAggregateSpec(spec, rows) {
233
+ const result = {};
234
+ for (const [alias, selector] of Object.entries(spec)) result[alias] = reduceAggregate(selector.fn, selector.field, rows);
235
+ return result;
236
+ }
237
+ /**
238
+ * Project an aggregate spec down to the plain `{ fn, field? }` shape carried by
239
+ * the query AST (`IdbAggregateAst.aggregates` / `IdbGroupByAst.aggregates`), so
240
+ * middleware can observe the requested aggregations.
241
+ */
242
+ function toAggregateRequests(spec) {
243
+ const out = {};
244
+ for (const [alias, selector] of Object.entries(spec)) out[alias] = selector.field !== void 0 ? {
245
+ fn: selector.fn,
246
+ field: selector.field
247
+ } : { fn: selector.fn };
248
+ return out;
249
+ }
250
+ /**
251
+ * Validate a user-supplied aggregate spec: it must be non-empty and every
252
+ * value must be a real selector. Throws a descriptive error otherwise, so a
253
+ * typo (e.g. returning a plain object) fails loudly rather than silently
254
+ * producing `NaN`/`undefined` results.
255
+ */
256
+ function assertValidAggregateSpec(spec, context) {
257
+ const entries = Object.entries(spec);
258
+ if (entries.length === 0) throw new Error(`${context} requires at least one aggregation selector`);
259
+ for (const [alias, selector] of entries) if (!isAggregateSelector(selector)) throw new Error(`${context} selector "${alias}" is invalid`);
260
+ }
261
+ //#endregion
262
+ //#region src/core/grouped-accessor.ts
263
+ /**
264
+ * Build the composite group key for a row. Uses a JSON encoding of the ordered
265
+ * key-field values so multi-field groups and primitive value types (string,
266
+ * number, boolean, null) partition correctly.
267
+ */
268
+ function groupKeyOf(by, row) {
269
+ return JSON.stringify(by.map((field) => row[field] ?? null));
270
+ }
271
+ function createGroupedAccessor(init) {
272
+ return { async aggregate(fn) {
273
+ const spec = fn(createAggregateBuilder());
274
+ assertValidAggregateSpec(spec, "groupBy().aggregate()");
275
+ const ast = {
276
+ kind: "groupBy",
277
+ modelName: init.modelName,
278
+ by: init.by,
279
+ aggregates: toAggregateRequests(spec),
280
+ ...init.where !== void 0 ? { where: init.where } : {}
281
+ };
282
+ const rows = await init.materialize(ast);
283
+ const groups = /* @__PURE__ */ new Map();
284
+ for (const row of rows) {
285
+ const gk = groupKeyOf(init.by, row);
286
+ let group = groups.get(gk);
287
+ if (group === void 0) {
288
+ const key = {};
289
+ for (const field of init.by) key[field] = row[field];
290
+ group = {
291
+ key,
292
+ rows: []
293
+ };
294
+ groups.set(gk, group);
295
+ }
296
+ group.rows.push(row);
297
+ }
298
+ return Array.from(groups.values()).map((group) => ({
299
+ ...group.key,
300
+ ...computeAggregateSpec(spec, group.rows)
301
+ }));
302
+ } };
303
+ }
304
+ //#endregion
305
+ //#region src/core/relation-loader.ts
306
+ /**
307
+ * Batch-load a single named relation for all rows in `rows` and attach the
308
+ * result to each row under the `relName` key.
309
+ *
310
+ * The join is done with one cursor scan over the related store (with an
311
+ * in-memory filter), then grouped/indexed in memory — avoiding N+1 queries.
312
+ *
313
+ * The `entry` carries any `include()` refinement:
314
+ *
315
+ * - `collection` — the refined `where` further filters the child scan;
316
+ * `orderBy` / `skip` / `take` are applied **per parent group** for `1:N`
317
+ * relations (each parent's children are independently sorted and paginated).
318
+ * - `scalar` — the relation field becomes the `count` of matching children
319
+ * (to-many only; `include()` rejects scalar refinements on to-one relations).
320
+ *
321
+ * @param relName - The relation key to load (e.g. `"posts"`, `"author"`).
322
+ * @param entry - How to materialise the relation (collection vs scalar + refinement state).
323
+ * @param rows - The parent rows to attach related data to.
324
+ * @param contract - The resolved IDB contract.
325
+ * @param modelName - The source model name (owner of the relation).
326
+ * @param executor - The query executor used to run the related-store scan.
327
+ */
328
+ async function loadRelation(relName, entry, rows, contract, modelName, executor, groupingKey) {
329
+ if (rows.length === 0) return rows;
330
+ const models = domainModelsAtDefaultNamespace(contract.domain);
331
+ const model = models[modelName];
332
+ if (model === void 0) return rows;
333
+ const rawRelation = model.relations[relName];
334
+ if (rawRelation === void 0) return rows;
335
+ if (!("on" in rawRelation)) return rows;
336
+ const relation = rawRelation;
337
+ const { cardinality, on } = relation;
338
+ const relatedModelName = relation.to.model;
339
+ const localField = on.localFields[0];
340
+ const foreignField = on.targetFields[0];
341
+ if (localField === void 0 || foreignField === void 0) return rows;
342
+ const relatedModel = models[relatedModelName];
343
+ if (relatedModel === void 0) return rows;
344
+ const relatedStoreName = typeof relatedModel.storage === "object" && relatedModel.storage !== null && "storeName" in relatedModel.storage ? String(relatedModel.storage["storeName"]) : relatedModelName;
345
+ const isScalar = entry.kind === "scalar";
346
+ const localValues = /* @__PURE__ */ new Set();
347
+ for (const row of rows) {
348
+ const v = row[localField];
349
+ if (v !== void 0 && v !== null) localValues.add(v);
350
+ }
351
+ if (localValues.size === 0) return rows.map((row) => ({
352
+ ...row,
353
+ [relName]: isScalar ? 0 : cardinality === "1:N" ? [] : null
354
+ }));
355
+ const capturedForeignField = foreignField;
356
+ const refinedWhere = combineFilterExprs(entry.state.filters);
357
+ const filter = (row) => localValues.has(row[capturedForeignField]) && (refinedWhere === void 0 || evaluateFilter(refinedWhere, row));
358
+ const planMeta = {
359
+ target: "idb",
360
+ storageHash: contract.storage.storageHash,
361
+ lane: "idb-orm",
362
+ annotations: { groupingKey }
363
+ };
364
+ const plan = {
365
+ meta: planMeta,
366
+ idbPlan: {
367
+ meta: planMeta,
368
+ kind: "cursor-scan",
369
+ storeName: relatedStoreName,
370
+ filter
371
+ }
372
+ };
373
+ const relatedRows = [];
374
+ for await (const row of executor.execute(plan)) relatedRows.push(row);
375
+ if (cardinality === "1:N") {
376
+ const grouped = /* @__PURE__ */ new Map();
377
+ for (const rrow of relatedRows) {
378
+ const gk = rrow[capturedForeignField];
379
+ const group = grouped.get(gk) ?? [];
380
+ group.push(rrow);
381
+ grouped.set(gk, group);
382
+ }
383
+ if (isScalar) return rows.map((row) => ({
384
+ ...row,
385
+ [relName]: (grouped.get(row[localField]) ?? []).length
386
+ }));
387
+ const comparator = buildRowComparator(entry.state.orderBy);
388
+ const skip = entry.state.skip ?? 0;
389
+ const take = entry.state.take;
390
+ return rows.map((row) => {
391
+ let group = grouped.get(row[localField]) ?? [];
392
+ if (comparator !== void 0) group = [...group].sort(comparator);
393
+ if (skip > 0 || take !== void 0) group = group.slice(skip, take !== void 0 ? skip + take : void 0);
394
+ return {
395
+ ...row,
396
+ [relName]: group
397
+ };
398
+ });
399
+ }
400
+ const indexed = /* @__PURE__ */ new Map();
401
+ for (const rrow of relatedRows) indexed.set(rrow[capturedForeignField], rrow);
402
+ return rows.map((row) => ({
403
+ ...row,
404
+ [relName]: indexed.get(row[localField]) ?? null
405
+ }));
406
+ }
407
+ //#endregion
408
+ //#region src/core/mutation-scope.ts
409
+ /**
410
+ * Run a callback inside a single multi-store IDB readwrite transaction.
411
+ *
412
+ * Opens the transaction, passes the `IdbTransactionScope` to `run`, then:
413
+ * - On success: awaits `scope.commit()` (waits for `tx.oncomplete`).
414
+ * - On error: calls `scope.rollback()` and rethrows.
415
+ *
416
+ * The callback receives the low-level scope, not the ORM accessor.
417
+ * Use this for multi-store atomic writes that span several object stores.
418
+ */
419
+ async function withMutationScope(executor, storeNames, run) {
420
+ const tx = await executor.transaction(storeNames, "readwrite");
421
+ try {
422
+ const result = await run(tx);
423
+ await tx.commit();
424
+ return result;
425
+ } catch (err) {
426
+ tx.rollback();
427
+ throw err;
428
+ }
429
+ }
430
+ //#endregion
431
+ //#region src/core/relation-mutator.ts
432
+ function createRelationMutator() {
433
+ return {
434
+ create(data) {
435
+ return {
436
+ kind: "create",
437
+ data: Array.isArray(data) ? [...data] : [data]
438
+ };
439
+ },
440
+ connect(criteria) {
441
+ return {
442
+ kind: "connect",
443
+ criteria: Array.isArray(criteria) ? [...criteria] : [criteria]
444
+ };
445
+ },
446
+ disconnect(criteria) {
447
+ if (!criteria) return { kind: "disconnect" };
448
+ return {
449
+ kind: "disconnect",
450
+ criteria: [...criteria]
451
+ };
452
+ }
453
+ };
454
+ }
455
+ function isRelationMutationDescriptor(value) {
456
+ if (!value || typeof value !== "object") return false;
457
+ const candidate = value;
458
+ return candidate.kind === "create" || candidate.kind === "connect" || candidate.kind === "disconnect";
459
+ }
460
+ function isRelationMutationCallback(value) {
461
+ return typeof value === "function";
462
+ }
463
+ //#endregion
464
+ //#region src/core/mutation-executor.ts
465
+ function makePlanMeta(contract) {
466
+ return {
467
+ target: "idb",
468
+ storageHash: contract.storage.storageHash,
469
+ lane: "idb-mutation-executor",
470
+ annotations: { groupingKey: "nested" }
471
+ };
472
+ }
473
+ const relationDefsCache = /* @__PURE__ */ new WeakMap();
474
+ function getRelationDefinitions(contract, modelName) {
475
+ let perContract = relationDefsCache.get(contract);
476
+ if (!perContract) {
477
+ perContract = /* @__PURE__ */ new Map();
478
+ relationDefsCache.set(contract, perContract);
479
+ }
480
+ const cached = perContract.get(modelName);
481
+ if (cached) return cached;
482
+ const model = domainModelsAtDefaultNamespace(contract.domain)[modelName];
483
+ if (!model) {
484
+ perContract.set(modelName, []);
485
+ return [];
486
+ }
487
+ const defs = [];
488
+ for (const [relationName, rawRelation] of Object.entries(model.relations)) {
489
+ if (!rawRelation || typeof rawRelation !== "object" || !("on" in rawRelation)) continue;
490
+ const relation = rawRelation;
491
+ const relatedModelName = relation.to.model;
492
+ const relatedStoreName = getStoreName(contract, relatedModelName);
493
+ defs.push({
494
+ relationName,
495
+ relatedModelName,
496
+ relatedStoreName,
497
+ cardinality: relation.cardinality,
498
+ localFields: relation.on.localFields,
499
+ targetFields: relation.on.targetFields
500
+ });
501
+ }
502
+ perContract.set(modelName, defs);
503
+ return defs;
504
+ }
505
+ /**
506
+ * Returns true if `data` contains at least one field that is both a known
507
+ * relation name for `modelName` and a function (a mutation callback).
508
+ */
509
+ function hasNestedMutationCallbacks(contract, modelName, data) {
510
+ const relationNames = new Set(getRelationDefinitions(contract, modelName).map((r) => r.relationName));
511
+ for (const [fieldName, value] of Object.entries(data)) if (relationNames.has(fieldName) && isRelationMutationCallback(value)) return true;
512
+ return false;
513
+ }
514
+ /**
515
+ * Guards that the executor supports multi-store transactions.
516
+ * Throws a clear error if `transaction()` is not available — the user must
517
+ * use IdbRuntime (createIdbRuntime / createAutoMigratingIdbClient) rather than
518
+ * a plain IdbQueryExecutor stub.
519
+ */
520
+ function requireTransactionExecutor(executor) {
521
+ if (typeof executor.transaction !== "function") throw new Error("Nested relation writes require an executor with transaction support. Use IdbRuntime (createIdbRuntime or createAutoMigratingIdbClient) instead of a plain IdbQueryExecutor.");
522
+ return executor;
523
+ }
524
+ async function executeNestedCreateMutation(options) {
525
+ const { executor, contract, modelName, data } = options;
526
+ const record = data;
527
+ return withMutationScope(executor, collectStoreNames(contract, modelName, record), (scope) => createGraph(scope, contract, modelName, record));
528
+ }
529
+ async function executeNestedUpdateMutation(options) {
530
+ const { executor, contract, modelName, filters, data } = options;
531
+ const record = data;
532
+ return withMutationScope(executor, collectStoreNames(contract, modelName, record), (scope) => updateFirstGraph(scope, contract, modelName, filters, record));
533
+ }
534
+ function collectStoreNames(contract, modelName, data) {
535
+ const stores = /* @__PURE__ */ new Set([getStoreName(contract, modelName)]);
536
+ for (const def of getRelationDefinitions(contract, modelName)) if (def.relationName in data && isRelationMutationCallback(data[def.relationName])) stores.add(def.relatedStoreName);
537
+ return [...stores];
538
+ }
539
+ async function createGraph(scope, contract, modelName, input) {
540
+ const parsed = parseMutationInput(contract, modelName, input);
541
+ const { parentOwned, childOwned } = partitionByOwnership(parsed.relationMutations);
542
+ const scalarData = { ...parsed.scalarData };
543
+ for (const item of parentOwned) {
544
+ if (item.mutation.kind === "disconnect") throw new Error("disconnect() is only supported in update() nested mutations");
545
+ await applyParentOwnedMutation(scope, contract, modelName, scalarData, item.relation, item.mutation);
546
+ }
547
+ const parentRow = await insertSingleRow(scope, contract, modelName, scalarData);
548
+ for (const item of childOwned) {
549
+ if (item.mutation.kind === "disconnect") throw new Error("disconnect() is only supported in update() nested mutations");
550
+ await applyChildOwnedMutation(scope, contract, modelName, parentRow, item.relation, item.mutation);
551
+ }
552
+ return parentRow;
553
+ }
554
+ async function updateFirstGraph(scope, contract, modelName, filters, input) {
555
+ const existingRow = await findFirstByFilters(scope, contract, modelName, filters);
556
+ if (!existingRow) return null;
557
+ const parsed = parseMutationInput(contract, modelName, input);
558
+ const { parentOwned, childOwned } = partitionByOwnership(parsed.relationMutations);
559
+ const scalarData = { ...parsed.scalarData };
560
+ for (const item of parentOwned) await applyParentOwnedMutation(scope, contract, modelName, scalarData, item.relation, item.mutation);
561
+ let parentRow = existingRow;
562
+ if (Object.keys(scalarData).length > 0) {
563
+ const storeName = getStoreName(contract, modelName);
564
+ const key = existingRow[getKeyPath(contract, modelName)];
565
+ const meta = makePlanMeta(contract);
566
+ const updated = (await scope.execute({
567
+ meta,
568
+ kind: "update",
569
+ storeName,
570
+ key,
571
+ patch: scalarData
572
+ }))[0];
573
+ if (updated) parentRow = updated;
574
+ }
575
+ for (const item of childOwned) await applyChildOwnedMutation(scope, contract, modelName, parentRow, item.relation, item.mutation);
576
+ return parentRow;
577
+ }
578
+ function parseMutationInput(contract, modelName, input) {
579
+ const scalarData = {};
580
+ const relationDefs = new Map(getRelationDefinitions(contract, modelName).map((r) => [r.relationName, r]));
581
+ const relationMutations = [];
582
+ for (const [fieldName, value] of Object.entries(input)) {
583
+ const relation = relationDefs.get(fieldName);
584
+ if (!relation) {
585
+ scalarData[fieldName] = value;
586
+ continue;
587
+ }
588
+ if (!isRelationMutationCallback(value)) throw new Error(`Relation field "${fieldName}" on model "${modelName}" expects a mutator callback`);
589
+ const mutation = value(createRelationMutator());
590
+ if (!isRelationMutationDescriptor(mutation)) throw new Error(`Relation field "${fieldName}" on model "${modelName}" returned an invalid mutation descriptor`);
591
+ relationMutations.push({
592
+ relation,
593
+ mutation
594
+ });
595
+ }
596
+ return {
597
+ scalarData,
598
+ relationMutations
599
+ };
600
+ }
601
+ function partitionByOwnership(mutations) {
602
+ const parentOwned = [];
603
+ const childOwned = [];
604
+ for (const item of mutations) {
605
+ if (item.relation.cardinality === "N:1") {
606
+ parentOwned.push(item);
607
+ continue;
608
+ }
609
+ if (item.relation.cardinality === "M:N") throw new Error("M:N nested mutations are not supported");
610
+ childOwned.push(item);
611
+ }
612
+ return {
613
+ parentOwned,
614
+ childOwned
615
+ };
616
+ }
617
+ async function applyParentOwnedMutation(scope, contract, parentModelName, scalarData, relation, mutation) {
618
+ if (mutation.kind === "disconnect") {
619
+ for (const localField of relation.localFields) scalarData[localField] = null;
620
+ return;
621
+ }
622
+ if (mutation.kind === "create") {
623
+ const row = mutation.data[0];
624
+ if (!row) throw new Error(`create() nested mutation for relation "${relation.relationName}" requires data`);
625
+ copyRelatedValuesToParent(relation, scalarData, await insertSingleRow(scope, contract, relation.relatedModelName, row), parentModelName, contract);
626
+ return;
627
+ }
628
+ const criterion = mutation.criteria[0];
629
+ if (!criterion) throw new Error(`connect() nested mutation for relation "${relation.relationName}" requires a criterion`);
630
+ const relatedRow = await findRowByCriterion(scope, contract, relation.relatedModelName, criterion);
631
+ if (!relatedRow) throw new Error(`connect() nested mutation for relation "${relation.relationName}" did not find a matching row`);
632
+ copyRelatedValuesToParent(relation, scalarData, relatedRow, parentModelName, contract);
633
+ }
634
+ function copyRelatedValuesToParent(relation, scalarData, relatedRow, _parentModelName, _contract) {
635
+ for (let i = 0; i < relation.localFields.length; i++) {
636
+ const localField = relation.localFields[i];
637
+ const targetField = relation.targetFields[i];
638
+ if (!localField || !targetField) continue;
639
+ scalarData[localField] = relatedRow[targetField];
640
+ }
641
+ }
642
+ async function applyChildOwnedMutation(scope, contract, parentModelName, parentRow, relation, mutation) {
643
+ const parentValues = readParentColumnValues(parentModelName, relation, parentRow);
644
+ if (mutation.kind === "create") {
645
+ for (const childInput of mutation.data) {
646
+ const payload = { ...childInput };
647
+ for (const [childField, parentValue] of parentValues.entries()) payload[childField] = parentValue;
648
+ await insertSingleRow(scope, contract, relation.relatedModelName, payload);
649
+ }
650
+ return;
651
+ }
652
+ if (mutation.kind === "connect") {
653
+ for (const criterion of mutation.criteria) {
654
+ const setValues = {};
655
+ for (const [childField, parentValue] of parentValues.entries()) setValues[childField] = parentValue;
656
+ const filter = buildCriterionFilter(criterion);
657
+ const meta = makePlanMeta(contract);
658
+ await scope.execute({
659
+ meta,
660
+ kind: "scan-write",
661
+ storeName: relation.relatedStoreName,
662
+ write: "put-merged",
663
+ patch: setValues,
664
+ filter
665
+ });
666
+ }
667
+ return;
668
+ }
669
+ const setValues = {};
670
+ for (const childField of parentValues.keys()) setValues[childField] = null;
671
+ const meta = makePlanMeta(contract);
672
+ if (!mutation.criteria || mutation.criteria.length === 0) {
673
+ const parentJoinFilter = buildParentJoinFilter(parentValues);
674
+ await scope.execute({
675
+ meta,
676
+ kind: "scan-write",
677
+ storeName: relation.relatedStoreName,
678
+ write: "put-merged",
679
+ patch: setValues,
680
+ filter: parentJoinFilter
681
+ });
682
+ return;
683
+ }
684
+ for (const criterion of mutation.criteria) {
685
+ const criterionFilter = buildCriterionFilter(criterion);
686
+ const parentJoinFilter = buildParentJoinFilter(parentValues);
687
+ const combinedFilter = (row) => parentJoinFilter(row) && criterionFilter(row);
688
+ await scope.execute({
689
+ meta,
690
+ kind: "scan-write",
691
+ storeName: relation.relatedStoreName,
692
+ write: "put-merged",
693
+ patch: setValues,
694
+ filter: combinedFilter
695
+ });
696
+ }
697
+ }
698
+ function readParentColumnValues(parentModelName, relation, parentRow) {
699
+ const values = /* @__PURE__ */ new Map();
700
+ for (let i = 0; i < relation.localFields.length; i++) {
701
+ const localField = relation.localFields[i];
702
+ const targetField = relation.targetFields[i];
703
+ if (!localField || !targetField) continue;
704
+ const parentValue = parentRow[localField];
705
+ if (parentValue === void 0) throw new Error(`Nested mutation requires parent field "${localField}" to be present in "${parentModelName}" row`);
706
+ values.set(targetField, parentValue);
707
+ }
708
+ return values;
709
+ }
710
+ async function insertSingleRow(scope, contract, modelName, data) {
711
+ assertNoNestedCallbacks(modelName, data);
712
+ const storeName = getStoreName(contract, modelName);
713
+ const meta = makePlanMeta(contract);
714
+ return (await scope.execute({
715
+ meta,
716
+ kind: "put",
717
+ storeName,
718
+ record: data
719
+ }))[0] ?? data;
720
+ }
721
+ /**
722
+ * Recursive nesting (a relation callback inside an already-nested create) is
723
+ * not supported in Phase 6.4. Without this guard the callback function would be
724
+ * handed to `store.put(...)`, where IDB's structured-clone throws an opaque
725
+ * `DataCloneError` ("could not be cloned") that gives the developer no hint
726
+ * about the real cause. Surface a precise error instead. (PLAN Issue #22.)
727
+ */
728
+ function assertNoNestedCallbacks(modelName, data) {
729
+ for (const [field, value] of Object.entries(data)) if (typeof value === "function") throw new Error(`Recursive nested writes are not supported: field "${field}" on a nested "${modelName}" record is a relation callback. Only one level of relation nesting is supported — flatten the inner relation into a separate create/connect call.`);
730
+ }
731
+ async function findRowByCriterion(scope, contract, modelName, criterion) {
732
+ const expr = shorthandToFilterExpr(criterion);
733
+ if (!expr) throw new Error(`Nested connect for model "${modelName}" requires a non-empty criterion`);
734
+ const filter = (row) => evaluateFilter(expr, row);
735
+ return scanOneRow(scope, contract, modelName, filter);
736
+ }
737
+ async function findFirstByFilters(scope, contract, modelName, filters) {
738
+ if (filters.length === 0) return null;
739
+ const combined = filters.length === 1 ? filters[0] : {
740
+ kind: "and",
741
+ exprs: filters
742
+ };
743
+ const filter = (row) => evaluateFilter(combined, row);
744
+ return scanOneRow(scope, contract, modelName, filter);
745
+ }
746
+ async function scanOneRow(scope, contract, modelName, filter) {
747
+ const storeName = getStoreName(contract, modelName);
748
+ const plan = {
749
+ meta: makePlanMeta(contract),
750
+ kind: "cursor-scan",
751
+ storeName,
752
+ filter,
753
+ take: 1
754
+ };
755
+ return (await scope.execute(plan))[0] ?? null;
756
+ }
757
+ function buildCriterionFilter(criterion) {
758
+ const expr = shorthandToFilterExpr(criterion);
759
+ if (!expr) return () => true;
760
+ return (row) => evaluateFilter(expr, row);
761
+ }
762
+ function buildParentJoinFilter(parentValues) {
763
+ const pairs = [...parentValues.entries()];
764
+ return (row) => pairs.every(([childField, parentValue]) => row[childField] === parentValue);
765
+ }
766
+ function getKeyPath(contract, modelName) {
767
+ return (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.keyPath ?? "id";
768
+ }
769
+ function getOnDelete(contract, modelName, relationName) {
770
+ return (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.relations?.[relationName]?.onDelete ?? "restrict";
771
+ }
772
+ function isDeleteEnforcementRelation(contract, modelName, def) {
773
+ if (def.cardinality === "1:N") return true;
774
+ if (def.cardinality === "1:1") {
775
+ const keyPath = getKeyPath(contract, modelName);
776
+ return def.localFields.length > 0 && def.localFields[0] === keyPath;
777
+ }
778
+ return false;
779
+ }
780
+ /**
781
+ * Returns true if `data` contains at least one non-null value for a localField
782
+ * of a N:1 relation — indicating scalar FK fields that need existence validation.
783
+ */
784
+ function hasScalarFkFields(contract, modelName, data) {
785
+ for (const def of getRelationDefinitions(contract, modelName)) {
786
+ if (def.cardinality !== "N:1") continue;
787
+ for (const localField of def.localFields) if (localField in data && data[localField] !== null && data[localField] !== void 0) return true;
788
+ }
789
+ return false;
790
+ }
791
+ function collectScalarFkStoreNames(contract, modelName, data) {
792
+ const stores = /* @__PURE__ */ new Set([getStoreName(contract, modelName)]);
793
+ for (const def of getRelationDefinitions(contract, modelName)) {
794
+ if (def.cardinality !== "N:1") continue;
795
+ if (def.localFields.some((f) => f in data && data[f] !== null && data[f] !== void 0)) stores.add(def.relatedStoreName);
796
+ }
797
+ return [...stores];
798
+ }
799
+ async function validateScalarFks(scope, contract, modelName, data) {
800
+ const meta = makePlanMeta(contract);
801
+ for (const def of getRelationDefinitions(contract, modelName)) {
802
+ if (def.cardinality !== "N:1") continue;
803
+ for (let i = 0; i < def.localFields.length; i++) {
804
+ const localField = def.localFields[i];
805
+ const targetField = def.targetFields[i];
806
+ const value = data[localField];
807
+ if (!(localField in data) || value === null || value === void 0) continue;
808
+ const filter = (row) => row[targetField] === value;
809
+ const plan = {
810
+ meta,
811
+ kind: "cursor-scan",
812
+ storeName: def.relatedStoreName,
813
+ filter,
814
+ take: 1
815
+ };
816
+ if ((await scope.execute(plan)).length === 0) throw new Error(`FK violation on relation '${def.relationName}': no ${def.relatedModelName} with ${targetField}='${String(value)}'`);
817
+ }
818
+ }
819
+ }
820
+ async function executeScalarCreateWithFkValidation(options) {
821
+ const { executor, contract, modelName, data } = options;
822
+ return withMutationScope(executor, collectScalarFkStoreNames(contract, modelName, data), async (scope) => {
823
+ await validateScalarFks(scope, contract, modelName, data);
824
+ return insertSingleRow(scope, contract, modelName, data);
825
+ });
826
+ }
827
+ async function executeScalarUpdateWithFkValidation(options) {
828
+ const { executor, contract, modelName, filters, data } = options;
829
+ return withMutationScope(executor, collectScalarFkStoreNames(contract, modelName, data), async (scope) => {
830
+ await validateScalarFks(scope, contract, modelName, data);
831
+ const storeName = getStoreName(contract, modelName);
832
+ const meta = makePlanMeta(contract);
833
+ const combined = filters.length === 0 ? void 0 : filters.length === 1 ? filters[0] : {
834
+ kind: "and",
835
+ exprs: filters
836
+ };
837
+ const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
838
+ return (await scope.execute({
839
+ meta,
840
+ kind: "scan-write",
841
+ storeName,
842
+ write: "put-merged",
843
+ patch: data,
844
+ take: 1,
845
+ ...filter !== void 0 ? { filter } : {}
846
+ }))[0] ?? null;
847
+ });
848
+ }
849
+ /**
850
+ * Returns true if the model has at least one child relation (1:N or parent-side
851
+ * 1:1) whose `onDelete` action requires enforcement (anything except `noAction`).
852
+ * Since the default is `restrict`, any model with 1:N/1:1 relations that do not
853
+ * explicitly set `noAction` returns true.
854
+ */
855
+ function hasEnforceableChildRelations(contract, modelName) {
856
+ for (const def of getRelationDefinitions(contract, modelName)) {
857
+ if (!isDeleteEnforcementRelation(contract, modelName, def)) continue;
858
+ if (getOnDelete(contract, modelName, def.relationName) !== "noAction") return true;
859
+ }
860
+ return false;
861
+ }
862
+ function collectDeleteStoreNames(contract, modelName) {
863
+ const stores = /* @__PURE__ */ new Set([getStoreName(contract, modelName)]);
864
+ for (const def of getRelationDefinitions(contract, modelName)) {
865
+ if (!isDeleteEnforcementRelation(contract, modelName, def)) continue;
866
+ if (getOnDelete(contract, modelName, def.relationName) !== "noAction") stores.add(def.relatedStoreName);
867
+ }
868
+ return [...stores];
869
+ }
870
+ async function applyReferentialActionsForRow(scope, contract, modelName, row) {
871
+ const meta = makePlanMeta(contract);
872
+ for (const def of getRelationDefinitions(contract, modelName)) {
873
+ if (!isDeleteEnforcementRelation(contract, modelName, def)) continue;
874
+ const action = getOnDelete(contract, modelName, def.relationName);
875
+ if (action === "noAction") continue;
876
+ const pairs = def.localFields.map((lf, i) => ({
877
+ childField: def.targetFields[i],
878
+ parentValue: row[lf]
879
+ }));
880
+ const childFilter = (child) => pairs.every(({ childField, parentValue }) => child[childField] === parentValue);
881
+ if (action === "restrict") {
882
+ if ((await scope.execute({
883
+ meta,
884
+ kind: "cursor-scan",
885
+ storeName: def.relatedStoreName,
886
+ filter: childFilter,
887
+ take: 1
888
+ })).length > 0) {
889
+ const keyPath = getKeyPath(contract, modelName);
890
+ throw new Error(`Cannot delete ${modelName} '${String(row[keyPath])}': child records exist on relation '${def.relationName}'. Use onDelete: 'cascade', 'setNull', or 'noAction'.`);
891
+ }
892
+ continue;
893
+ }
894
+ if (action === "cascade") {
895
+ await scope.execute({
896
+ meta,
897
+ kind: "scan-write",
898
+ storeName: def.relatedStoreName,
899
+ write: "delete",
900
+ filter: childFilter
901
+ });
902
+ continue;
903
+ }
904
+ if (action === "setNull") {
905
+ const patch = {};
906
+ for (const targetField of def.targetFields) patch[targetField] = null;
907
+ await scope.execute({
908
+ meta,
909
+ kind: "scan-write",
910
+ storeName: def.relatedStoreName,
911
+ write: "put-merged",
912
+ patch,
913
+ filter: childFilter
914
+ });
915
+ continue;
916
+ }
917
+ if (action === "setDefault") throw new Error(`setDefault referential action is not supported on relation '${def.relationName}': IDB contracts do not track field defaults. Use 'cascade', 'setNull', or 'noAction' instead.`);
918
+ }
919
+ }
920
+ async function executeDeleteWithReferentialActions(options) {
921
+ const { executor, contract, modelName, key } = options;
922
+ await withMutationScope(executor, collectDeleteStoreNames(contract, modelName), async (scope) => {
923
+ const storeName = getStoreName(contract, modelName);
924
+ const meta = makePlanMeta(contract);
925
+ const row = (await scope.execute({
926
+ meta,
927
+ kind: "key-get",
928
+ storeName,
929
+ key
930
+ }))[0];
931
+ if (!row) return [];
932
+ await applyReferentialActionsForRow(scope, contract, modelName, row);
933
+ await scope.execute({
934
+ meta,
935
+ kind: "delete",
936
+ storeName,
937
+ key
938
+ });
939
+ return [];
940
+ });
941
+ }
942
+ async function executeDeleteAllWithReferentialActions(options) {
943
+ const { executor, contract, modelName, filter } = options;
944
+ return withMutationScope(executor, collectDeleteStoreNames(contract, modelName), async (scope) => {
945
+ const storeName = getStoreName(contract, modelName);
946
+ const meta = makePlanMeta(contract);
947
+ const keyPath = getKeyPath(contract, modelName);
948
+ const rows = await scope.execute({
949
+ meta,
950
+ kind: "cursor-scan",
951
+ storeName,
952
+ ...filter !== void 0 ? { filter } : {}
953
+ });
954
+ for (const row of rows) {
955
+ await applyReferentialActionsForRow(scope, contract, modelName, row);
956
+ const key = row[keyPath];
957
+ await scope.execute({
958
+ meta,
959
+ kind: "delete",
960
+ storeName,
961
+ key
962
+ });
963
+ }
964
+ return rows;
965
+ });
966
+ }
967
+ //#endregion
968
+ //#region src/core/store-accessor.ts
969
+ /**
970
+ * Concrete immutable query builder.
971
+ *
972
+ * Internal details:
973
+ * - All state is in `#state` (filters, orderBy, skip, take, includes, selectedFields).
974
+ * - Builder methods clone via `#clone()` — O(1) copies since state is
975
+ * structurally shared.
976
+ * - `all()` materialises the main rows first, then batch-loads each included
977
+ * relation, then applies any `.select()` projection before yielding.
978
+ * - `#includeRefinementMode` flips `count()` from an async terminal to an
979
+ * {@link IdbIncludeScalar} marker so it can be used inside `include()`.
980
+ */
981
+ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
982
+ #contract;
983
+ #modelName;
984
+ #executor;
985
+ #storeName;
986
+ #state;
987
+ #newGroupingKey;
988
+ #includeRefinementMode;
989
+ constructor(contract, modelName, executor, state, newGroupingKey, includeRefinementMode = false) {
990
+ this.#contract = contract;
991
+ this.#modelName = modelName;
992
+ this.#executor = executor;
993
+ this.#storeName = getStoreName(contract, modelName);
994
+ this.#state = state ?? emptyAccessorState();
995
+ let _key = 0;
996
+ this.#newGroupingKey = newGroupingKey ?? (() => `idb-op-${++_key}`);
997
+ this.#includeRefinementMode = includeRefinementMode;
998
+ }
999
+ where(filter) {
1000
+ const expr = typeof filter === "function" ? filter(createModelAccessor()) : shorthandToFilterExpr(filter);
1001
+ if (expr === void 0) return this.#clone({});
1002
+ return this.#clone({ filters: [...this.#state.filters, expr] });
1003
+ }
1004
+ orderBy(spec) {
1005
+ return this.#clone({ orderBy: spec });
1006
+ }
1007
+ take(n) {
1008
+ return this.#clone({ take: n });
1009
+ }
1010
+ skip(n) {
1011
+ return this.#clone({ skip: n });
1012
+ }
1013
+ include(relation, refineFn) {
1014
+ const entry = this.#resolveIncludeEntry(relation, refineFn);
1015
+ const newState = mergeAccessorState(this.#state, { includes: {
1016
+ ...this.#state.includes,
1017
+ [relation]: entry
1018
+ } });
1019
+ return new IdbStoreAccessorImpl(this.#contract, this.#modelName, this.#executor, newState, this.#newGroupingKey, this.#includeRefinementMode);
1020
+ }
1021
+ select(...fields) {
1022
+ return this.#clone({ selectedFields: fields });
1023
+ }
1024
+ all() {
1025
+ const groupingKey = this.#newGroupingKey();
1026
+ const buildScanPlan = this.#buildScanPlan.bind(this);
1027
+ const executorExecute = this.#executor.execute.bind(this.#executor);
1028
+ const applyIncludes = this.#applyIncludes.bind(this);
1029
+ const projectRows = this.#projectRows.bind(this);
1030
+ return new AsyncIterableResult((async function* () {
1031
+ const scanPlan = buildScanPlan(groupingKey);
1032
+ const rows = [];
1033
+ for await (const row of executorExecute(scanPlan)) rows.push(row);
1034
+ const withIncludes = await applyIncludes(rows, groupingKey);
1035
+ for (const row of projectRows(withIncludes)) yield row;
1036
+ })());
1037
+ }
1038
+ async first() {
1039
+ return this.take(1).all().first();
1040
+ }
1041
+ async aggregate(fn) {
1042
+ const spec = fn(createAggregateBuilder());
1043
+ assertValidAggregateSpec(spec, "aggregate()");
1044
+ const combined = this.#combinedFilterExpr();
1045
+ const ast = {
1046
+ kind: "aggregate",
1047
+ modelName: this.#modelName,
1048
+ aggregates: toAggregateRequests(spec),
1049
+ ...combined !== void 0 ? { where: combined } : {}
1050
+ };
1051
+ return computeAggregateSpec(spec, await this.#materialize(this.#newGroupingKey(), ast));
1052
+ }
1053
+ groupBy(...fields) {
1054
+ const combined = this.#combinedFilterExpr();
1055
+ const materialize = (ast) => this.#materialize(this.#newGroupingKey(), ast);
1056
+ return createGroupedAccessor({
1057
+ modelName: this.#modelName,
1058
+ by: fields,
1059
+ where: combined,
1060
+ materialize
1061
+ });
1062
+ }
1063
+ async create(data) {
1064
+ const record = data;
1065
+ if (hasNestedMutationCallbacks(this.#contract, this.#modelName, record)) return await executeNestedCreateMutation({
1066
+ executor: requireTransactionExecutor(this.#executor),
1067
+ contract: this.#contract,
1068
+ modelName: this.#modelName,
1069
+ data: record
1070
+ });
1071
+ if (hasScalarFkFields(this.#contract, this.#modelName, record)) return await executeScalarCreateWithFkValidation({
1072
+ executor: requireTransactionExecutor(this.#executor),
1073
+ contract: this.#contract,
1074
+ modelName: this.#modelName,
1075
+ data: record
1076
+ });
1077
+ const groupingKey = this.#newGroupingKey();
1078
+ const meta = this.#planMeta(groupingKey);
1079
+ const plan = {
1080
+ meta,
1081
+ ast: {
1082
+ kind: "create",
1083
+ modelName: this.#modelName,
1084
+ data: record
1085
+ },
1086
+ idbPlan: {
1087
+ meta,
1088
+ kind: "put",
1089
+ storeName: this.#storeName,
1090
+ record
1091
+ }
1092
+ };
1093
+ for await (const row of this.#executor.execute(plan)) return row;
1094
+ return record;
1095
+ }
1096
+ async findUnique(key) {
1097
+ const groupingKey = this.#newGroupingKey();
1098
+ const meta = this.#planMeta(groupingKey);
1099
+ const plan = {
1100
+ meta,
1101
+ ast: {
1102
+ kind: "findUnique",
1103
+ modelName: this.#modelName,
1104
+ key
1105
+ },
1106
+ idbPlan: {
1107
+ meta,
1108
+ kind: "key-get",
1109
+ storeName: this.#storeName,
1110
+ key
1111
+ }
1112
+ };
1113
+ for await (const row of this.#executor.execute(plan)) return row;
1114
+ return null;
1115
+ }
1116
+ async delete(key) {
1117
+ if (hasEnforceableChildRelations(this.#contract, this.#modelName)) {
1118
+ await executeDeleteWithReferentialActions({
1119
+ executor: requireTransactionExecutor(this.#executor),
1120
+ contract: this.#contract,
1121
+ modelName: this.#modelName,
1122
+ key
1123
+ });
1124
+ return;
1125
+ }
1126
+ const groupingKey = this.#newGroupingKey();
1127
+ const meta = this.#planMeta(groupingKey);
1128
+ const plan = {
1129
+ meta,
1130
+ ast: {
1131
+ kind: "delete",
1132
+ modelName: this.#modelName,
1133
+ key
1134
+ },
1135
+ idbPlan: {
1136
+ meta,
1137
+ kind: "delete",
1138
+ storeName: this.#storeName,
1139
+ key
1140
+ }
1141
+ };
1142
+ await this.#executor.execute(plan).toArray();
1143
+ }
1144
+ async update(patch) {
1145
+ const patchRecord = patch;
1146
+ if (hasNestedMutationCallbacks(this.#contract, this.#modelName, patchRecord)) return await executeNestedUpdateMutation({
1147
+ executor: requireTransactionExecutor(this.#executor),
1148
+ contract: this.#contract,
1149
+ modelName: this.#modelName,
1150
+ filters: this.#state.filters,
1151
+ data: patchRecord
1152
+ });
1153
+ if (hasScalarFkFields(this.#contract, this.#modelName, patchRecord)) return await executeScalarUpdateWithFkValidation({
1154
+ executor: requireTransactionExecutor(this.#executor),
1155
+ contract: this.#contract,
1156
+ modelName: this.#modelName,
1157
+ filters: this.#state.filters,
1158
+ data: patchRecord
1159
+ });
1160
+ const groupingKey = this.#newGroupingKey();
1161
+ const combined = this.#combinedFilterExpr();
1162
+ const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
1163
+ const meta = this.#planMeta(groupingKey);
1164
+ const plan = {
1165
+ meta,
1166
+ ast: {
1167
+ kind: "update",
1168
+ modelName: this.#modelName,
1169
+ patch: patchRecord,
1170
+ ...combined !== void 0 ? { where: combined } : {}
1171
+ },
1172
+ idbPlan: {
1173
+ meta,
1174
+ kind: "scan-write",
1175
+ storeName: this.#storeName,
1176
+ write: "put-merged",
1177
+ patch: patchRecord,
1178
+ take: 1,
1179
+ ...filter !== void 0 ? { filter } : {}
1180
+ }
1181
+ };
1182
+ for await (const row of this.#executor.execute(plan)) return row;
1183
+ return null;
1184
+ }
1185
+ updateAll(patch) {
1186
+ const groupingKey = this.#newGroupingKey();
1187
+ const combined = this.#combinedFilterExpr();
1188
+ const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
1189
+ const meta = this.#planMeta(groupingKey);
1190
+ const storeName = this.#storeName;
1191
+ const modelName = this.#modelName;
1192
+ const patchRecord = patch;
1193
+ const executorExecute = this.#executor.execute.bind(this.#executor);
1194
+ return new AsyncIterableResult((async function* () {
1195
+ const plan = {
1196
+ meta,
1197
+ ast: {
1198
+ kind: "updateAll",
1199
+ modelName,
1200
+ patch: patchRecord,
1201
+ ...combined !== void 0 ? { where: combined } : {}
1202
+ },
1203
+ idbPlan: {
1204
+ meta,
1205
+ kind: "scan-write",
1206
+ storeName,
1207
+ write: "put-merged",
1208
+ patch: patchRecord,
1209
+ ...filter !== void 0 ? { filter } : {}
1210
+ }
1211
+ };
1212
+ for await (const row of executorExecute(plan)) yield row;
1213
+ })());
1214
+ }
1215
+ async updateCount(patch) {
1216
+ return (await this.updateAll(patch).toArray()).length;
1217
+ }
1218
+ async upsert(args) {
1219
+ const keyPath = getKeyPath$1(this.#contract, this.#modelName);
1220
+ const whereExpr = shorthandToFilterExpr(args.where);
1221
+ const matches = (row) => whereExpr === void 0 || evaluateFilter(whereExpr, row);
1222
+ const meta = this.#planMeta(this.#newGroupingKey());
1223
+ const createRecord = args.create;
1224
+ const patchRecord = args.update;
1225
+ const storeName = this.#storeName;
1226
+ const exec = this.#executor;
1227
+ if (typeof exec.transaction === "function") return withMutationScope(exec, [storeName], async (scope) => {
1228
+ const existing = (await scope.execute({
1229
+ meta,
1230
+ kind: "cursor-scan",
1231
+ storeName,
1232
+ filter: matches,
1233
+ take: 1
1234
+ }))[0];
1235
+ if (existing === void 0) return (await scope.execute({
1236
+ meta,
1237
+ kind: "put",
1238
+ storeName,
1239
+ record: createRecord
1240
+ }))[0] ?? createRecord;
1241
+ const key = existing[keyPath];
1242
+ return (await scope.execute({
1243
+ meta,
1244
+ kind: "update",
1245
+ storeName,
1246
+ key,
1247
+ patch: patchRecord
1248
+ }))[0] ?? existing;
1249
+ });
1250
+ const existing = await this.where(args.where).first();
1251
+ if (!existing) return this.create(args.create);
1252
+ const key = existing[keyPath];
1253
+ const plan = {
1254
+ meta,
1255
+ ast: {
1256
+ kind: "upsert",
1257
+ modelName: this.#modelName,
1258
+ create: createRecord,
1259
+ update: patchRecord,
1260
+ where: args.where
1261
+ },
1262
+ idbPlan: {
1263
+ meta,
1264
+ kind: "update",
1265
+ storeName,
1266
+ key,
1267
+ patch: patchRecord
1268
+ }
1269
+ };
1270
+ for await (const row of this.#executor.execute(plan)) return row;
1271
+ return existing;
1272
+ }
1273
+ createAll(data) {
1274
+ const groupingKey = this.#newGroupingKey();
1275
+ const meta = this.#planMeta(groupingKey);
1276
+ const records = data.map((d) => d);
1277
+ const plan = {
1278
+ meta,
1279
+ ast: {
1280
+ kind: "createAll",
1281
+ modelName: this.#modelName,
1282
+ data: records
1283
+ },
1284
+ idbPlan: {
1285
+ meta,
1286
+ kind: "batch",
1287
+ storeNames: [this.#storeName],
1288
+ ops: records.map((record) => ({
1289
+ meta,
1290
+ kind: "put",
1291
+ storeName: this.#storeName,
1292
+ record
1293
+ }))
1294
+ }
1295
+ };
1296
+ const executorExecute = this.#executor.execute.bind(this.#executor);
1297
+ return new AsyncIterableResult((async function* () {
1298
+ for await (const row of executorExecute(plan)) yield row;
1299
+ })());
1300
+ }
1301
+ async createCount(data) {
1302
+ return (await this.createAll(data).toArray()).length;
1303
+ }
1304
+ deleteAll() {
1305
+ const combined = this.#combinedFilterExpr();
1306
+ const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
1307
+ if (hasEnforceableChildRelations(this.#contract, this.#modelName)) {
1308
+ const contract = this.#contract;
1309
+ const modelName = this.#modelName;
1310
+ const executor = requireTransactionExecutor(this.#executor);
1311
+ return new AsyncIterableResult((async function* () {
1312
+ const rows = await executeDeleteAllWithReferentialActions({
1313
+ executor,
1314
+ contract,
1315
+ modelName,
1316
+ ...filter !== void 0 ? { filter } : {}
1317
+ });
1318
+ for (const row of rows) yield row;
1319
+ })());
1320
+ }
1321
+ const groupingKey = this.#newGroupingKey();
1322
+ const meta = this.#planMeta(groupingKey);
1323
+ const ast = {
1324
+ kind: "deleteAll",
1325
+ modelName: this.#modelName,
1326
+ ...combined !== void 0 ? { where: combined } : {}
1327
+ };
1328
+ const storeName = this.#storeName;
1329
+ const executorExecute = this.#executor.execute.bind(this.#executor);
1330
+ return new AsyncIterableResult((async function* () {
1331
+ const plan = {
1332
+ meta,
1333
+ ast,
1334
+ idbPlan: {
1335
+ meta,
1336
+ kind: "scan-write",
1337
+ storeName,
1338
+ write: "delete",
1339
+ ...filter !== void 0 ? { filter } : {}
1340
+ }
1341
+ };
1342
+ for await (const row of executorExecute(plan)) yield row;
1343
+ })());
1344
+ }
1345
+ async deleteCount() {
1346
+ return (await this.deleteAll().toArray()).length;
1347
+ }
1348
+ count() {
1349
+ if (this.#includeRefinementMode) return createIncludeScalar(this.#state);
1350
+ return this.#countTerminal();
1351
+ }
1352
+ async #countTerminal() {
1353
+ const groupingKey = this.#newGroupingKey();
1354
+ const scanPlan = this.#buildScanPlan(groupingKey);
1355
+ const scanAst = scanPlan.ast;
1356
+ const ast = {
1357
+ kind: "count",
1358
+ modelName: this.#modelName,
1359
+ ...scanAst?.kind === "findMany" && scanAst.where !== void 0 ? { where: scanAst.where } : {}
1360
+ };
1361
+ const plan = {
1362
+ ...scanPlan,
1363
+ ast
1364
+ };
1365
+ let n = 0;
1366
+ for await (const _ of this.#executor.execute(plan)) n++;
1367
+ return n;
1368
+ }
1369
+ /**
1370
+ * Resolve an `include()` argument pair into an {@link IncludeEntry}: run the
1371
+ * optional refinement against a fresh refinement-mode child accessor, then
1372
+ * classify the result as a scalar count or a refined collection.
1373
+ */
1374
+ #resolveIncludeEntry(relation, refineFn) {
1375
+ if (refineFn === void 0) return {
1376
+ kind: "collection",
1377
+ state: emptyAccessorState()
1378
+ };
1379
+ const rel = getRelation(this.#contract, this.#modelName, relation);
1380
+ const relatedModelName = rel?.to.model ?? relation;
1381
+ const refined = refineFn(new IdbStoreAccessorImpl(this.#contract, relatedModelName, this.#executor, emptyAccessorState(), this.#newGroupingKey, true));
1382
+ if (isIncludeScalar(refined)) {
1383
+ if (rel !== void 0 && rel.cardinality !== "1:N") throw new Error(`include('${relation}'): count() is only supported for to-many (1:N) relations`);
1384
+ return {
1385
+ kind: "scalar",
1386
+ fn: refined.fn,
1387
+ state: refined.state
1388
+ };
1389
+ }
1390
+ if (refined instanceof IdbStoreAccessorImpl) return {
1391
+ kind: "collection",
1392
+ state: refined.#state
1393
+ };
1394
+ throw new Error(`include('${relation}') refinement must return the collection (for where/orderBy/take/skip) or a count() selector`);
1395
+ }
1396
+ /**
1397
+ * Materialise all rows matching the accumulated filters with no pagination —
1398
+ * used by `aggregate()` / `groupBy()`. The supplied `ast` is attached to the
1399
+ * scan plan so middleware can observe the aggregate intent.
1400
+ */
1401
+ async #materialize(groupingKey, ast) {
1402
+ const combined = this.#combinedFilterExpr();
1403
+ const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
1404
+ const meta = this.#planMeta(groupingKey);
1405
+ const plan = {
1406
+ meta,
1407
+ ast,
1408
+ idbPlan: {
1409
+ meta,
1410
+ kind: "cursor-scan",
1411
+ storeName: this.#storeName,
1412
+ ...filter !== void 0 ? { filter } : {}
1413
+ }
1414
+ };
1415
+ const rows = [];
1416
+ for await (const row of this.#executor.execute(plan)) rows.push(row);
1417
+ return rows;
1418
+ }
1419
+ #buildScanPlan(groupingKey) {
1420
+ const combined = this.#combinedFilterExpr();
1421
+ const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
1422
+ const comparator = buildRowComparator(this.#state.orderBy);
1423
+ const meta = this.#planMeta(groupingKey);
1424
+ return {
1425
+ meta,
1426
+ ast: {
1427
+ kind: "findMany",
1428
+ modelName: this.#modelName,
1429
+ ...combined !== void 0 ? { where: combined } : {},
1430
+ ...this.#state.orderBy !== void 0 ? { orderBy: this.#state.orderBy } : {},
1431
+ ...this.#state.skip !== void 0 ? { skip: this.#state.skip } : {},
1432
+ ...this.#state.take !== void 0 ? { take: this.#state.take } : {}
1433
+ },
1434
+ idbPlan: {
1435
+ meta,
1436
+ kind: "cursor-scan",
1437
+ storeName: this.#storeName,
1438
+ ...filter !== void 0 ? { filter } : {},
1439
+ ...comparator !== void 0 ? { comparator } : {},
1440
+ ...this.#state.skip !== void 0 ? { skip: this.#state.skip } : {},
1441
+ ...this.#state.take !== void 0 ? { take: this.#state.take } : {}
1442
+ }
1443
+ };
1444
+ }
1445
+ /**
1446
+ * Combine all accumulated filter expressions with AND.
1447
+ *
1448
+ * Returns `undefined` when no filter has been installed so the driver can
1449
+ * skip building a row filter closure (a small perf and readability win on
1450
+ * `.all()` paths). Delegates to the shared {@link combineFilterExprs}.
1451
+ */
1452
+ #combinedFilterExpr() {
1453
+ return combineFilterExprs(this.#state.filters);
1454
+ }
1455
+ async #applyIncludes(rows, groupingKey) {
1456
+ const relNames = Object.keys(this.#state.includes);
1457
+ if (relNames.length === 0) return rows;
1458
+ let result = rows;
1459
+ for (const relName of relNames) {
1460
+ const entry = this.#state.includes[relName];
1461
+ result = await loadRelation(relName, entry, result, this.#contract, this.#modelName, this.#executor, groupingKey);
1462
+ }
1463
+ return result;
1464
+ }
1465
+ /**
1466
+ * Apply a `.select()` projection (if any) to materialised rows. Keeps the
1467
+ * selected scalar fields plus every included relation key (which `include()`
1468
+ * attached during {@link #applyIncludes}); a no-op when nothing is selected.
1469
+ */
1470
+ #projectRows(rows) {
1471
+ const selected = this.#state.selectedFields;
1472
+ if (selected === void 0) return rows;
1473
+ const keep = [...selected, ...Object.keys(this.#state.includes)];
1474
+ return rows.map((row) => {
1475
+ const out = {};
1476
+ for (const field of keep) if (field in row) out[field] = row[field];
1477
+ return out;
1478
+ });
1479
+ }
1480
+ #planMeta(groupingKey) {
1481
+ return {
1482
+ target: "idb",
1483
+ storageHash: this.#contract.storage.storageHash,
1484
+ lane: "idb-orm",
1485
+ annotations: { groupingKey }
1486
+ };
1487
+ }
1488
+ #clone(overrides) {
1489
+ return new IdbStoreAccessorImpl(this.#contract, this.#modelName, this.#executor, mergeAccessorState(this.#state, overrides), this.#newGroupingKey, this.#includeRefinementMode);
1490
+ }
1491
+ };
1492
+ //#endregion
1493
+ //#region src/core/idb-orm.ts
1494
+ /**
1495
+ * Create a typed IDB ORM client from a contract and executor.
1496
+ *
1497
+ * The client exposes one `IdbStoreAccessor` per entry in `contract.roots`.
1498
+ * Only roots-declared stores are accessible at the top level — other stores
1499
+ * can be reached via `.include()` on any accessor.
1500
+ *
1501
+ * @example
1502
+ * ```ts
1503
+ * import { idbOrm } from "@prisma-next-idb/client-idb/orm";
1504
+ * import { createIdbRuntime } from "@prisma-next-idb/runtime-idb/runtime";
1505
+ * import contract from "./prisma/idb-contract";
1506
+ *
1507
+ * const runtime = createIdbRuntime({ adapter, driver });
1508
+ * const db = idbOrm({ contract, executor: runtime });
1509
+ *
1510
+ * // Typed query builder:
1511
+ * const alice = await db.users.where({ email: "alice@example.com" }).first();
1512
+ * const postsWithAuthor = await db.posts.include("author").all();
1513
+ * ```
1514
+ */
1515
+ function idbOrm(options) {
1516
+ const { contract, executor } = options;
1517
+ let _key = 0;
1518
+ const newGroupingKey = () => `idb-op-${++_key}`;
1519
+ const client = {};
1520
+ for (const [rootKey, ref] of Object.entries(contract.roots)) client[rootKey] = new IdbStoreAccessorImpl(contract, ref.model, executor, void 0, newGroupingKey);
1521
+ return client;
1522
+ }
1523
+ //#endregion
1524
+ export { isRelationMutationDescriptor as a, isRelationMutationCallback as i, hasNestedMutationCallbacks as n, withMutationScope as o, createRelationMutator as r, idbOrm as t };
1525
+
1526
+ //# sourceMappingURL=idb-orm-NHIZ3oNt.mjs.map