@prisma-idb/client-idb 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/client-auto.d.mts +2 -2
- package/dist/client-auto.d.mts.map +1 -1
- package/dist/client-auto.mjs +44 -3
- package/dist/client-auto.mjs.map +1 -1
- package/dist/client.d.mts +3 -2
- package/dist/client.mjs +3 -2
- package/dist/decimal-identity-D_1J9lPN.d.mts +21 -0
- package/dist/decimal-identity-D_1J9lPN.d.mts.map +1 -0
- package/dist/internal.d.mts +5 -4
- package/dist/internal.d.mts.map +1 -1
- package/dist/internal.mjs +2 -2
- package/dist/{managed-client-CxlNmERd.mjs → managed-client-DMk8ERfX.mjs} +12 -5
- package/dist/managed-client-DMk8ERfX.mjs.map +1 -0
- package/dist/{managed-client-CS78EjPu.d.mts → managed-client-eksFK4UF.d.mts} +32 -3
- package/dist/managed-client-eksFK4UF.d.mts.map +1 -0
- package/dist/mutation-executor-IuJmyWEG.mjs +2132 -0
- package/dist/mutation-executor-IuJmyWEG.mjs.map +1 -0
- package/dist/{executor-Bp-hdRg2.d.mts → mutation-scope-CE3GMR4X.d.mts} +79 -28
- package/dist/mutation-scope-CE3GMR4X.d.mts.map +1 -0
- package/dist/{idb-orm-BitJ-EMq.mjs → orm-transaction-BGx1eukw.mjs} +366 -558
- package/dist/orm-transaction-BGx1eukw.mjs.map +1 -0
- package/dist/{idb-orm-9XN3c9ly.d.mts → orm-transaction-B_ZJ92uY.d.mts} +40 -7
- package/dist/orm-transaction-B_ZJ92uY.d.mts.map +1 -0
- package/dist/orm.d.mts +13 -4
- package/dist/orm.d.mts.map +1 -1
- package/dist/orm.mjs +3 -3
- package/package.json +9 -9
- package/dist/executor-Bp-hdRg2.d.mts.map +0 -1
- package/dist/idb-orm-9XN3c9ly.d.mts.map +0 -1
- package/dist/idb-orm-BitJ-EMq.mjs.map +0 -1
- package/dist/managed-client-CS78EjPu.d.mts.map +0 -1
- package/dist/managed-client-CxlNmERd.mjs.map +0 -1
- package/dist/mutation-executor-SJqTuSbb.mjs +0 -1196
- package/dist/mutation-executor-SJqTuSbb.mjs.map +0 -1
- package/dist/mutation-scope-1hS0ogGa.d.mts +0 -33
- package/dist/mutation-scope-1hS0ogGa.d.mts.map +0 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { C as
|
|
1
|
+
import { A as createMutationDefaultsCache, B as resolveDecimalKey, C as validateScalarFks, D as planQuery, E as lowerRows, F as combineFilterExprs, G as fieldValueToken, J as getStoreName, K as getKeyPath, M as assertValidRecord, N as assertEnumValues, O as applyUpdateDefaults, P as buildRowComparator, S as requireTransactionExecutor, T as lowerCount, U as buildCatalog, V as withDecimalIdentityExecutor, W as extractKeyFromRow, _ as hasEnforceableChildRelations, a as collectOnUpdateEnforcementStoreNames, b as makePlanMeta, c as executeBulkUpdateWithFkValidation, d as executeMutationPlan, f as executeNestedCreateMutation, g as executeScalarUpdateWithFkValidation, h as executeScalarCreateWithFkValidation, i as collectOnUpdateClosureStoreNames, k as completeCreateRecord, l as executeDeleteAllWithReferentialActions, m as executeScalarCreateAllWithFkValidation, n as applyReferentialActionsForRowOnUpdate, o as collectRelatedStoreNames, p as executeNestedUpdateMutation, q as getRelation, r as collectDeleteStoreNames, s as collectScalarFkStoreNames, u as executeDeleteWithReferentialActions, v as hasNestedMutationCallbacks, w as withMutationScope, x as readMutationRows, y as hasScalarFkFields } from "./mutation-executor-IuJmyWEG.mjs";
|
|
2
2
|
import { AsyncIterableResult } from "@prisma/orm-framework/components/runtime";
|
|
3
|
-
import { andExpr, evaluateFilter, fieldFilter, nullCheckExpr, shorthandToFilterExpr } from "@prisma-idb/adapter-idb/runtime";
|
|
4
3
|
import { domainModelsAtDefaultNamespace } from "@prisma/orm-framework/contract/types";
|
|
5
|
-
import {
|
|
4
|
+
import { IdbExecuteError } from "@prisma-idb/driver-idb/runtime";
|
|
5
|
+
import { absentAsNull, isValidIdbKey } from "@prisma-idb/target-idb/runtime";
|
|
6
|
+
import { andExpr, fieldFilter, nullCheckExpr, shorthandToFilterExpr } from "@prisma-idb/adapter-idb/runtime";
|
|
6
7
|
//#region src/core/model-accessor.ts
|
|
7
8
|
/**
|
|
8
9
|
* Proxy-based typed accessor handed to `where(fn)` callbacks.
|
|
@@ -53,7 +54,9 @@ function createFieldAccessor(field) {
|
|
|
53
54
|
*/
|
|
54
55
|
function createModelAccessor() {
|
|
55
56
|
const cache = /* @__PURE__ */ new Map();
|
|
56
|
-
return new Proxy({}, {
|
|
57
|
+
return new Proxy({}, {
|
|
58
|
+
/** Reuse the field accessor within one filter callback so repeated reads share its operator surface. */
|
|
59
|
+
get(_target, prop) {
|
|
57
60
|
if (typeof prop !== "string") return void 0;
|
|
58
61
|
let acc = cache.get(prop);
|
|
59
62
|
if (acc === void 0) {
|
|
@@ -94,39 +97,6 @@ function mergeAccessorState(state, overrides) {
|
|
|
94
97
|
};
|
|
95
98
|
}
|
|
96
99
|
//#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
100
|
//#region src/core/aggregate-builder.ts
|
|
131
101
|
/**
|
|
132
102
|
* Build the aggregate selector factory handed to `.aggregate(agg => …)` and
|
|
@@ -179,33 +149,43 @@ function isAggregateSelector(value) {
|
|
|
179
149
|
* - `sum`/`avg`/`min`/`max` → computed over the non-null numeric values of
|
|
180
150
|
* `field`; `null` when no row has a non-null value (matching Prisma's
|
|
181
151
|
* "aggregate of an empty set is null" and the vendor `coerceAggregateValue`
|
|
182
|
-
* null handling).
|
|
152
|
+
* null handling). Values must match the declared number codec.
|
|
183
153
|
*/
|
|
184
|
-
function reduceAggregate(fn, field, rows) {
|
|
154
|
+
function reduceAggregate(fn, field, rows, codecId) {
|
|
185
155
|
if (fn === "count") return rows.length;
|
|
186
156
|
if (field === void 0) return null;
|
|
187
157
|
const values = [];
|
|
188
158
|
for (const row of rows) {
|
|
189
159
|
const raw = row[field];
|
|
190
160
|
if (raw === null || raw === void 0) continue;
|
|
191
|
-
|
|
192
|
-
if (!Number.
|
|
161
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) throw new TypeError(`${fn}("${field}") requires finite number values`);
|
|
162
|
+
if (codecId === "idb/int32@1" && (!Number.isInteger(raw) || raw < -2147483648 || raw > 2147483647)) throw new TypeError(`${fn}("${field}") requires Int values in the signed 32-bit range`);
|
|
163
|
+
values.push(raw);
|
|
193
164
|
}
|
|
194
165
|
if (values.length === 0) return null;
|
|
195
166
|
switch (fn) {
|
|
196
|
-
case "sum": return values
|
|
197
|
-
case "avg": return values
|
|
198
|
-
case "min": return Math.min(
|
|
199
|
-
case "max": return Math.max(
|
|
167
|
+
case "sum": return sumValues(values, field, codecId === "idb/int32@1");
|
|
168
|
+
case "avg": return sumValues(values, field, codecId === "idb/int32@1") / values.length;
|
|
169
|
+
case "min": return values.reduce((acc, n) => Math.min(acc, n));
|
|
170
|
+
case "max": return values.reduce((acc, n) => Math.max(acc, n));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** Reject overflow before a rounded or nonfinite intermediate total can reach the result. */
|
|
174
|
+
function sumValues(values, field, integer) {
|
|
175
|
+
let total = 0;
|
|
176
|
+
for (const value of values) {
|
|
177
|
+
total += value;
|
|
178
|
+
if (!Number.isFinite(total) || integer && !Number.isSafeInteger(total)) throw new RangeError(`Aggregate total for "${field}" exceeds the ${integer ? "safe integer" : "finite number"} range`);
|
|
200
179
|
}
|
|
180
|
+
return total;
|
|
201
181
|
}
|
|
202
182
|
/**
|
|
203
183
|
* Run every selector in an aggregate spec against `rows`, producing the
|
|
204
184
|
* result object keyed by the spec's aliases.
|
|
205
185
|
*/
|
|
206
|
-
function computeAggregateSpec(spec, rows) {
|
|
186
|
+
function computeAggregateSpec(spec, rows, fields = {}) {
|
|
207
187
|
const result = {};
|
|
208
|
-
for (const [alias, selector] of Object.entries(spec)) result[alias] = reduceAggregate(selector.fn, selector.field, rows);
|
|
188
|
+
for (const [alias, selector] of Object.entries(spec)) result[alias] = reduceAggregate(selector.fn, selector.field, rows, fields[selector.field ?? ""]?.codecId);
|
|
209
189
|
return result;
|
|
210
190
|
}
|
|
211
191
|
/**
|
|
@@ -223,14 +203,20 @@ function toAggregateRequests(spec) {
|
|
|
223
203
|
}
|
|
224
204
|
/**
|
|
225
205
|
* Validate a user-supplied aggregate spec: it must be non-empty and every
|
|
226
|
-
* value must be a real selector
|
|
206
|
+
* value must be a real selector over a declared scalar number field (except
|
|
207
|
+
* count, which has no field). Throws a descriptive error otherwise, so a
|
|
227
208
|
* typo (e.g. returning a plain object) fails loudly rather than silently
|
|
228
209
|
* producing `NaN`/`undefined` results.
|
|
229
210
|
*/
|
|
230
|
-
function assertValidAggregateSpec(spec, context) {
|
|
211
|
+
function assertValidAggregateSpec(spec, context, fields) {
|
|
231
212
|
const entries = Object.entries(spec);
|
|
232
213
|
if (entries.length === 0) throw new Error(`${context} requires at least one aggregation selector`);
|
|
233
|
-
for (const [alias, selector] of entries)
|
|
214
|
+
for (const [alias, selector] of entries) {
|
|
215
|
+
if (!isAggregateSelector(selector)) throw new Error(`${context} selector "${alias}" is invalid`);
|
|
216
|
+
if (selector.fn === "count") continue;
|
|
217
|
+
const field = typeof selector.field === "string" ? fields[selector.field] : void 0;
|
|
218
|
+
if (!field || field.collection || field.codecId !== "idb/int32@1" && field.codecId !== "idb/double@1") throw new TypeError(`${context} selector "${alias}" requires a declared number field (Int or Float)`);
|
|
219
|
+
}
|
|
234
220
|
}
|
|
235
221
|
//#endregion
|
|
236
222
|
//#region src/core/grouped-accessor.ts
|
|
@@ -249,9 +235,11 @@ function groupToken(value) {
|
|
|
249
235
|
}
|
|
250
236
|
/** Build a grouped accessor that reduces materialized rows from the source accessor. */
|
|
251
237
|
function createGroupedAccessor(init) {
|
|
252
|
-
return {
|
|
238
|
+
return {
|
|
239
|
+
/** Materialize matching rows, partition them by group key, and reduce each group. */
|
|
240
|
+
async aggregate(fn) {
|
|
253
241
|
const spec = fn(createAggregateBuilder());
|
|
254
|
-
assertValidAggregateSpec(spec, "groupBy().aggregate()");
|
|
242
|
+
assertValidAggregateSpec(spec, "groupBy().aggregate()", init.fields);
|
|
255
243
|
const ast = {
|
|
256
244
|
kind: "groupBy",
|
|
257
245
|
modelName: init.modelName,
|
|
@@ -277,464 +265,28 @@ function createGroupedAccessor(init) {
|
|
|
277
265
|
}
|
|
278
266
|
return Array.from(groups.values()).map((group) => ({
|
|
279
267
|
...group.key,
|
|
280
|
-
...computeAggregateSpec(spec, group.rows)
|
|
268
|
+
...computeAggregateSpec(spec, group.rows, init.fields)
|
|
281
269
|
}));
|
|
282
270
|
} };
|
|
283
271
|
}
|
|
284
272
|
//#endregion
|
|
285
|
-
//#region src/core/
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
}
|
|
273
|
+
//#region src/core/unique-lookup.ts
|
|
274
|
+
/**
|
|
275
|
+
* Copy the own enumerable properties of an upsert `where` once. Validation and
|
|
276
|
+
* filter parsing then see the same values even when a property is a getter.
|
|
277
|
+
* Non-object inputs pass through so `assertUniqueLookup` can reject them.
|
|
278
|
+
*/
|
|
279
|
+
function snapshotLookup(where) {
|
|
280
|
+
return typeof where === "object" && where !== null && !Array.isArray(where) ? Object.fromEntries(Object.entries(where)) : where;
|
|
630
281
|
}
|
|
631
|
-
//#endregion
|
|
632
|
-
//#region src/core/planner/lower.ts
|
|
633
282
|
/**
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
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.
|
|
283
|
+
* Require equality on a complete native unique key before upsert can mutate.
|
|
284
|
+
* Null and other non-key values are absent from IndexedDB indexes, so even a
|
|
285
|
+
* declared unique index cannot make those filters identify at most one row.
|
|
639
286
|
*/
|
|
640
|
-
function
|
|
641
|
-
const
|
|
642
|
-
|
|
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
|
-
};
|
|
287
|
+
function assertUniqueLookup(catalog, where) {
|
|
288
|
+
const sources = [catalog.primaryKey, ...catalog.indexes];
|
|
289
|
+
if (!(typeof where === "object" && where !== null && !Array.isArray(where) && sources.some((source) => source.unique && !source.multiEntry && source.fields.length > 0 && source.fields.every((field) => Object.prototype.propertyIsEnumerable.call(where, field) && isValidIdbKey(where[field]))))) throw new Error(`Invalid upsert where for "${catalog.storeName}": provide a complete primary key or unique index with non-null IndexedDB key values`);
|
|
738
290
|
}
|
|
739
291
|
//#endregion
|
|
740
292
|
//#region src/core/relation-loader.ts
|
|
@@ -797,7 +349,7 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
|
|
|
797
349
|
const joinWhere = andExpr(targetFields.map((field, i) => fieldFilter(field, "in", [...localTuples.values()].map((tuple) => tuple[i]))));
|
|
798
350
|
const where = refinedWhere === void 0 ? joinWhere : andExpr([joinWhere, refinedWhere]);
|
|
799
351
|
const request = { ...refinedWhere === void 0 ? {} : { where: refinedWhere } };
|
|
800
|
-
const catalog = buildCatalog(contract, relatedStoreName);
|
|
352
|
+
const catalog = buildCatalog(contract, relatedStoreName, relatedModelName);
|
|
801
353
|
const lowered = lowerRows(catalog, planQuery(catalog, { where }), planMeta, request, (row) => {
|
|
802
354
|
const token = tupleToken(row, targetFields);
|
|
803
355
|
return token !== null && localTuples.has(token);
|
|
@@ -806,10 +358,10 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
|
|
|
806
358
|
meta: planMeta,
|
|
807
359
|
idbPlan: lowered.idbPlan
|
|
808
360
|
};
|
|
809
|
-
return attachRelatedRows(relName, entry, rows, lowered.finish(await executor.query(plan).toArray()), relation);
|
|
361
|
+
return attachRelatedRows(relName, entry, rows, lowered.finish(await executor.query(plan).toArray()), relation, catalog.fields);
|
|
810
362
|
}
|
|
811
363
|
/** Attach related rows by their join tuple, shaping each parent's collection independently. */
|
|
812
|
-
function attachRelatedRows(relName, entry, rows, relatedRows, relation) {
|
|
364
|
+
function attachRelatedRows(relName, entry, rows, relatedRows, relation, fields) {
|
|
813
365
|
const { cardinality, on: { localFields, targetFields } } = relation;
|
|
814
366
|
if (cardinality === "1:N") {
|
|
815
367
|
const grouped = /* @__PURE__ */ new Map();
|
|
@@ -828,7 +380,7 @@ function attachRelatedRows(relName, entry, rows, relatedRows, relation) {
|
|
|
828
380
|
...row,
|
|
829
381
|
[relName]: groupFor(row).length
|
|
830
382
|
}));
|
|
831
|
-
const comparator = buildRowComparator(entry.state.orderBy);
|
|
383
|
+
const comparator = buildRowComparator(entry.state.orderBy, fields);
|
|
832
384
|
const skip = entry.state.skip ?? 0;
|
|
833
385
|
const take = entry.state.take;
|
|
834
386
|
return rows.map((row) => {
|
|
@@ -900,6 +452,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
900
452
|
this.#newGroupingKey = newGroupingKey ?? (() => `idb-op-${++_key}`);
|
|
901
453
|
this.#includeRefinementMode = includeRefinementMode;
|
|
902
454
|
}
|
|
455
|
+
/** Add a callback or shorthand predicate to a new accessor without mutating this one. */
|
|
903
456
|
where(filter) {
|
|
904
457
|
const expr = typeof filter === "function" ? filter(createModelAccessor()) : shorthandToFilterExpr(filter);
|
|
905
458
|
if (expr === void 0) return this.#clone({});
|
|
@@ -914,6 +467,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
914
467
|
skip(n) {
|
|
915
468
|
return this.#clone({ skip: n });
|
|
916
469
|
}
|
|
470
|
+
/** Add a collection or count refinement while preserving this accessor state. */
|
|
917
471
|
include(relation, refineFn) {
|
|
918
472
|
const entry = this.#resolveIncludeEntry(relation, refineFn);
|
|
919
473
|
const newState = mergeAccessorState(this.#state, { includes: {
|
|
@@ -922,9 +476,11 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
922
476
|
} });
|
|
923
477
|
return new IdbStoreAccessorImpl(this.#contract, this.#modelName, this.#executor, newState, this.#newGroupingKey, this.#includeRefinementMode);
|
|
924
478
|
}
|
|
479
|
+
/** Narrow scalar output fields while retaining included relations. */
|
|
925
480
|
select(...fields) {
|
|
926
481
|
return this.#clone({ selectedFields: fields });
|
|
927
482
|
}
|
|
483
|
+
/** Read matching rows, load includes, and project fields before yielding. */
|
|
928
484
|
all() {
|
|
929
485
|
const groupingKey = this.#newGroupingKey();
|
|
930
486
|
const buildScanPlan = this.#buildScanPlan.bind(this);
|
|
@@ -942,9 +498,11 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
942
498
|
async first() {
|
|
943
499
|
return this.take(1).all().first();
|
|
944
500
|
}
|
|
501
|
+
/** Reduce all filtered rows; query pagination does not constrain aggregates. */
|
|
945
502
|
async aggregate(fn) {
|
|
946
503
|
const spec = fn(createAggregateBuilder());
|
|
947
|
-
|
|
504
|
+
const { fields } = buildCatalog(this.#contract, this.#storeName, this.#modelName);
|
|
505
|
+
assertValidAggregateSpec(spec, "aggregate()", fields);
|
|
948
506
|
const combined = this.#combinedFilterExpr();
|
|
949
507
|
const ast = {
|
|
950
508
|
kind: "aggregate",
|
|
@@ -952,18 +510,21 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
952
510
|
aggregates: toAggregateRequests(spec),
|
|
953
511
|
...combined !== void 0 ? { where: combined } : {}
|
|
954
512
|
};
|
|
955
|
-
return computeAggregateSpec(spec, await this.#materialize(this.#newGroupingKey(), ast));
|
|
513
|
+
return computeAggregateSpec(spec, await this.#materialize(this.#newGroupingKey(), ast), fields);
|
|
956
514
|
}
|
|
515
|
+
/** Carry this filter into a grouped accessor whose materialization is owned here. */
|
|
957
516
|
groupBy(...fields) {
|
|
958
517
|
const combined = this.#combinedFilterExpr();
|
|
959
518
|
const materialize = (ast) => this.#materialize(this.#newGroupingKey(), ast);
|
|
960
519
|
return createGroupedAccessor({
|
|
961
520
|
modelName: this.#modelName,
|
|
962
521
|
by: fields,
|
|
522
|
+
fields: buildCatalog(this.#contract, this.#storeName, this.#modelName).fields,
|
|
963
523
|
where: combined,
|
|
964
524
|
materialize
|
|
965
525
|
});
|
|
966
526
|
}
|
|
527
|
+
/** Complete and validate one record, using a transaction when relations need checks. */
|
|
967
528
|
async create(data) {
|
|
968
529
|
const record = data;
|
|
969
530
|
if (hasNestedMutationCallbacks(this.#contract, this.#modelName, record)) return await executeNestedCreateMutation({
|
|
@@ -972,8 +533,9 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
972
533
|
modelName: this.#modelName,
|
|
973
534
|
data: record
|
|
974
535
|
});
|
|
975
|
-
const withDefaults =
|
|
536
|
+
const withDefaults = completeCreateRecord(this.#contract, this.#modelName, record, createMutationDefaultsCache());
|
|
976
537
|
assertEnumValues(this.#contract, this.#modelName, withDefaults);
|
|
538
|
+
assertValidRecord(this.#contract, this.#modelName, "create", withDefaults);
|
|
977
539
|
if (hasScalarFkFields(this.#contract, this.#modelName, withDefaults)) return await executeScalarCreateWithFkValidation({
|
|
978
540
|
executor: requireTransactionExecutor(this.#executor),
|
|
979
541
|
contract: this.#contract,
|
|
@@ -996,38 +558,29 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
996
558
|
record: withDefaults
|
|
997
559
|
}
|
|
998
560
|
};
|
|
999
|
-
|
|
1000
|
-
return withDefaults;
|
|
561
|
+
const [row] = await this.#executor.query(plan).toArray();
|
|
562
|
+
return row ?? withDefaults;
|
|
1001
563
|
}
|
|
564
|
+
/** Resolve a primary key numerically for Decimal fields while retaining its stored spelling. */
|
|
1002
565
|
async findUnique(key) {
|
|
1003
|
-
|
|
1004
|
-
const meta = this.#planMeta(
|
|
566
|
+
if (!isValidIdbKey(key)) return null;
|
|
567
|
+
const meta = this.#planMeta(this.#newGroupingKey());
|
|
1005
568
|
const ast = {
|
|
1006
569
|
kind: "findUnique",
|
|
1007
570
|
modelName: this.#modelName,
|
|
1008
571
|
key
|
|
1009
572
|
};
|
|
1010
|
-
const
|
|
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({
|
|
573
|
+
const row = await resolveDecimalKey({ execute: (body) => this.#executor.query({
|
|
1025
574
|
meta,
|
|
1026
575
|
ast,
|
|
1027
|
-
idbPlan:
|
|
1028
|
-
|
|
1029
|
-
|
|
576
|
+
idbPlan: {
|
|
577
|
+
...body,
|
|
578
|
+
meta
|
|
579
|
+
}
|
|
580
|
+
}).toArray() }, this.#contract, this.#modelName, key);
|
|
581
|
+
return row === void 0 ? null : absentAsNull(this.#contract, this.#modelName)(row);
|
|
1030
582
|
}
|
|
583
|
+
/** Delete a keyed row with child actions when its relations require them. */
|
|
1031
584
|
async delete(key) {
|
|
1032
585
|
if (hasEnforceableChildRelations(this.#contract, this.#modelName)) {
|
|
1033
586
|
await executeDeleteWithReferentialActions({
|
|
@@ -1056,6 +609,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1056
609
|
};
|
|
1057
610
|
await this.#executor.query(plan).toArray();
|
|
1058
611
|
}
|
|
612
|
+
/** Update the first matching row with scalar or nested relation validation. */
|
|
1059
613
|
async update(patch) {
|
|
1060
614
|
const patchRecord = patch;
|
|
1061
615
|
if (hasNestedMutationCallbacks(this.#contract, this.#modelName, patchRecord)) return await executeNestedUpdateMutation({
|
|
@@ -1103,10 +657,12 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1103
657
|
async updateCount(patch) {
|
|
1104
658
|
return (await this.updateAll(patch).toArray()).length;
|
|
1105
659
|
}
|
|
660
|
+
/** Select and apply the create or update arm inside one transaction. */
|
|
1106
661
|
async upsert(args) {
|
|
662
|
+
const where = snapshotLookup(args.where);
|
|
663
|
+
assertUniqueLookup(buildCatalog(this.#contract, this.#storeName, this.#modelName), where);
|
|
1107
664
|
const keyPath = getKeyPath(this.#contract, this.#modelName);
|
|
1108
|
-
const whereExpr = shorthandToFilterExpr(
|
|
1109
|
-
const matches = (row) => whereExpr === void 0 || evaluateFilter(whereExpr, row);
|
|
665
|
+
const whereExpr = shorthandToFilterExpr(where);
|
|
1110
666
|
const meta = this.#planMeta(this.#newGroupingKey());
|
|
1111
667
|
const createRecord = args.create;
|
|
1112
668
|
const patchRecord = args.update;
|
|
@@ -1114,9 +670,11 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1114
670
|
const executionDefaults = this.#contract.execution?.mutations.defaults;
|
|
1115
671
|
const exec = requireTransactionExecutor(this.#executor);
|
|
1116
672
|
const effectivePatch = applyUpdateDefaults(executionDefaults, storeName, patchRecord, createMutationDefaultsCache());
|
|
1117
|
-
const createWithDefaults =
|
|
673
|
+
const createWithDefaults = completeCreateRecord(this.#contract, this.#modelName, createRecord, createMutationDefaultsCache());
|
|
1118
674
|
assertEnumValues(this.#contract, this.#modelName, effectivePatch);
|
|
1119
675
|
assertEnumValues(this.#contract, this.#modelName, createWithDefaults);
|
|
676
|
+
assertValidRecord(this.#contract, this.#modelName, "update", effectivePatch);
|
|
677
|
+
assertValidRecord(this.#contract, this.#modelName, "create", createWithDefaults);
|
|
1120
678
|
const { storeNames: onUpdateStoreNames } = collectOnUpdateEnforcementStoreNames(this.#contract, this.#modelName, effectivePatch);
|
|
1121
679
|
const storeNames = [.../* @__PURE__ */ new Set([
|
|
1122
680
|
storeName,
|
|
@@ -1125,13 +683,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1125
683
|
...collectScalarFkStoreNames(this.#contract, this.#modelName, effectivePatch)
|
|
1126
684
|
])];
|
|
1127
685
|
return withMutationScope(exec, storeNames, async (scope) => {
|
|
1128
|
-
const existing = (await scope
|
|
1129
|
-
meta,
|
|
1130
|
-
kind: "cursor-scan",
|
|
1131
|
-
storeName,
|
|
1132
|
-
filter: matches,
|
|
1133
|
-
take: 1
|
|
1134
|
-
}))[0];
|
|
686
|
+
const existing = (await readMutationRows(scope, this.#contract, this.#modelName, whereExpr, 1))[0];
|
|
1135
687
|
if (existing === void 0) {
|
|
1136
688
|
await validateScalarFks(scope, this.#contract, this.#modelName, createWithDefaults);
|
|
1137
689
|
return (await scope.execute({
|
|
@@ -1144,19 +696,20 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1144
696
|
await validateScalarFks(scope, this.#contract, this.#modelName, effectivePatch, existing);
|
|
1145
697
|
const key = extractKeyFromRow(existing, keyPath);
|
|
1146
698
|
await applyReferentialActionsForRowOnUpdate(scope, this.#contract, this.#modelName, existing, effectivePatch);
|
|
1147
|
-
|
|
699
|
+
const rows = await scope.execute({
|
|
1148
700
|
meta,
|
|
1149
701
|
kind: "update",
|
|
1150
702
|
storeName,
|
|
1151
703
|
key,
|
|
1152
704
|
patch: effectivePatch
|
|
1153
|
-
})
|
|
705
|
+
});
|
|
706
|
+
return absentAsNull(this.#contract, this.#modelName)(rows[0] ?? existing);
|
|
1154
707
|
});
|
|
1155
708
|
}
|
|
709
|
+
/** Complete a batch with shared query defaults and insert it atomically. */
|
|
1156
710
|
createAll(data) {
|
|
1157
711
|
const defaultsCache = createMutationDefaultsCache();
|
|
1158
|
-
const
|
|
1159
|
-
const records = data.map((d) => applyCreateDefaults(executionDefaults, this.#storeName, d, defaultsCache));
|
|
712
|
+
const records = data.map((d) => completeCreateRecord(this.#contract, this.#modelName, d, defaultsCache));
|
|
1160
713
|
if (records.some((row) => hasScalarFkFields(this.#contract, this.#modelName, row))) {
|
|
1161
714
|
const executor = requireTransactionExecutor(this.#executor);
|
|
1162
715
|
const contract = this.#contract;
|
|
@@ -1196,7 +749,10 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1196
749
|
const contract = this.#contract;
|
|
1197
750
|
const modelName = this.#modelName;
|
|
1198
751
|
return new AsyncIterableResult((async function* () {
|
|
1199
|
-
for (const record of records)
|
|
752
|
+
for (const record of records) {
|
|
753
|
+
assertEnumValues(contract, modelName, record);
|
|
754
|
+
assertValidRecord(contract, modelName, "create", record);
|
|
755
|
+
}
|
|
1200
756
|
for await (const row of executorQuery(plan)) yield row;
|
|
1201
757
|
})());
|
|
1202
758
|
}
|
|
@@ -1215,7 +771,6 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1215
771
|
*/
|
|
1216
772
|
deleteAll() {
|
|
1217
773
|
const combined = this.#combinedFilterExpr();
|
|
1218
|
-
const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
|
|
1219
774
|
const contract = this.#contract;
|
|
1220
775
|
const modelName = this.#modelName;
|
|
1221
776
|
const executor = requireTransactionExecutor(this.#executor);
|
|
@@ -1224,7 +779,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1224
779
|
executor,
|
|
1225
780
|
contract,
|
|
1226
781
|
modelName,
|
|
1227
|
-
...
|
|
782
|
+
...combined === void 0 ? {} : { where: combined }
|
|
1228
783
|
});
|
|
1229
784
|
for (const row of rows) yield row;
|
|
1230
785
|
})());
|
|
@@ -1232,23 +787,34 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1232
787
|
async deleteCount() {
|
|
1233
788
|
return (await this.deleteAll().toArray()).length;
|
|
1234
789
|
}
|
|
790
|
+
/** Return an include-count descriptor during refinement, or execute a top-level count. */
|
|
1235
791
|
count() {
|
|
1236
792
|
if (this.#includeRefinementMode) return createIncludeScalar(this.#state);
|
|
1237
793
|
return this.#countTerminal();
|
|
1238
794
|
}
|
|
795
|
+
/** Count this filtered page through the planner, without applying select or include. */
|
|
1239
796
|
async #countTerminal() {
|
|
1240
|
-
const scan = this.#buildScanPlan(this.#newGroupingKey());
|
|
1241
797
|
const combined = this.#combinedFilterExpr();
|
|
798
|
+
const { skip, take } = this.#state;
|
|
799
|
+
const request = {
|
|
800
|
+
...combined === void 0 ? {} : { where: combined },
|
|
801
|
+
...skip === void 0 ? {} : { skip },
|
|
802
|
+
...take === void 0 ? {} : { take }
|
|
803
|
+
};
|
|
804
|
+
const meta = this.#planMeta(this.#newGroupingKey());
|
|
805
|
+
const catalog = buildCatalog(this.#contract, this.#storeName, this.#modelName);
|
|
806
|
+
const lowered = lowerCount(catalog, planQuery(catalog, request), meta, request);
|
|
1242
807
|
const ast = {
|
|
1243
808
|
kind: "count",
|
|
1244
809
|
modelName: this.#modelName,
|
|
1245
810
|
...combined !== void 0 ? { where: combined } : {}
|
|
1246
811
|
};
|
|
1247
812
|
const rows = await this.#executor.query({
|
|
1248
|
-
|
|
1249
|
-
ast
|
|
813
|
+
meta,
|
|
814
|
+
ast,
|
|
815
|
+
idbPlan: lowered.idbPlan
|
|
1250
816
|
}).toArray();
|
|
1251
|
-
return
|
|
817
|
+
return lowered.finish(rows);
|
|
1252
818
|
}
|
|
1253
819
|
/**
|
|
1254
820
|
* Resolve an `include()` argument pair into an {@link IncludeEntry}: run the
|
|
@@ -1294,9 +860,10 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1294
860
|
return lowered.finish(rows);
|
|
1295
861
|
}
|
|
1296
862
|
#lowerRows(meta, request) {
|
|
1297
|
-
const catalog = buildCatalog(this.#contract, this.#storeName);
|
|
863
|
+
const catalog = buildCatalog(this.#contract, this.#storeName, this.#modelName);
|
|
1298
864
|
return lowerRows(catalog, planQuery(catalog, request), meta, request);
|
|
1299
865
|
}
|
|
866
|
+
/** Pair the query intent with its planned driver operation and result shaping. */
|
|
1300
867
|
#buildScanPlan(groupingKey) {
|
|
1301
868
|
const combined = this.#combinedFilterExpr();
|
|
1302
869
|
const meta = this.#planMeta(groupingKey);
|
|
@@ -1327,6 +894,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1327
894
|
#combinedFilterExpr() {
|
|
1328
895
|
return combineFilterExprs(this.#state.filters);
|
|
1329
896
|
}
|
|
897
|
+
/** Batch-load each requested relation using the unprojected parent rows. */
|
|
1330
898
|
async #applyIncludes(rows, groupingKey) {
|
|
1331
899
|
const relNames = Object.keys(this.#state.includes);
|
|
1332
900
|
if (relNames.length === 0) return rows;
|
|
@@ -1352,6 +920,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1352
920
|
return out;
|
|
1353
921
|
});
|
|
1354
922
|
}
|
|
923
|
+
/** Attach the contract hash and grouping key shared by this operation. */
|
|
1355
924
|
#planMeta(groupingKey) {
|
|
1356
925
|
return {
|
|
1357
926
|
target: "idb",
|
|
@@ -1360,6 +929,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1360
929
|
annotations: { groupingKey }
|
|
1361
930
|
};
|
|
1362
931
|
}
|
|
932
|
+
/** Preserve shared runtime wiring while replacing only the accessor state. */
|
|
1363
933
|
#clone(overrides) {
|
|
1364
934
|
return new IdbStoreAccessorImpl(this.#contract, this.#modelName, this.#executor, mergeAccessorState(this.#state, overrides), this.#newGroupingKey, this.#includeRefinementMode);
|
|
1365
935
|
}
|
|
@@ -1379,7 +949,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1379
949
|
* import { createIdbRuntime } from "@prisma-idb/runtime-idb/runtime";
|
|
1380
950
|
* import contract from "./prisma/idb-contract";
|
|
1381
951
|
*
|
|
1382
|
-
* const runtime = createIdbRuntime({ adapter, driver });
|
|
952
|
+
* const runtime = createIdbRuntime({ adapter, driver, contract });
|
|
1383
953
|
* const db = idbOrm({ contract, executor: runtime });
|
|
1384
954
|
*
|
|
1385
955
|
* // Typed query builder:
|
|
@@ -1388,7 +958,8 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1388
958
|
* ```
|
|
1389
959
|
*/
|
|
1390
960
|
function idbOrm(options) {
|
|
1391
|
-
const { contract
|
|
961
|
+
const { contract } = options;
|
|
962
|
+
const executor = withDecimalIdentityExecutor(contract, options.executor);
|
|
1392
963
|
let _key = 0;
|
|
1393
964
|
const newGroupingKey = () => `idb-op-${++_key}`;
|
|
1394
965
|
const client = {};
|
|
@@ -1396,6 +967,243 @@ function idbOrm(options) {
|
|
|
1396
967
|
return client;
|
|
1397
968
|
}
|
|
1398
969
|
//#endregion
|
|
1399
|
-
|
|
970
|
+
//#region src/core/open-transaction.ts
|
|
971
|
+
/**
|
|
972
|
+
* An executor bound to one already-open IDB transaction.
|
|
973
|
+
*
|
|
974
|
+
* `db.transaction()` hands its callback ORM accessors built on this executor.
|
|
975
|
+
* Every query the accessors issue runs on the open transaction instead of
|
|
976
|
+
* opening a new one. Operations that normally open their own mutation scope
|
|
977
|
+
* (FK-checked writes, cascade deletes) get a view of the same transaction:
|
|
978
|
+
* `commit()` on the view does nothing, so an inner operation cannot commit the
|
|
979
|
+
* outer transaction, and `rollback()` aborts it.
|
|
980
|
+
*
|
|
981
|
+
* The executor also records how the transaction ended, so the runner can tell
|
|
982
|
+
* a rolled-back transaction from one that IndexedDB auto-committed early
|
|
983
|
+
* (ADR 005).
|
|
984
|
+
*/
|
|
985
|
+
function isTransactionInactive(error) {
|
|
986
|
+
return error instanceof IdbExecuteError && error.code === "TRANSACTION_INACTIVE";
|
|
987
|
+
}
|
|
988
|
+
/** Bind ORM operations to one scope and record aborts or early commits. */
|
|
989
|
+
var OpenTransaction = class {
|
|
990
|
+
#scope;
|
|
991
|
+
#storeNames;
|
|
992
|
+
#meta;
|
|
993
|
+
#state = "open";
|
|
994
|
+
#autoCommitCause;
|
|
995
|
+
#ended = false;
|
|
996
|
+
constructor(scope, storeNames, meta) {
|
|
997
|
+
this.#scope = scope;
|
|
998
|
+
this.#storeNames = new Set(storeNames);
|
|
999
|
+
this.#meta = meta;
|
|
1000
|
+
}
|
|
1001
|
+
/** Stop accepting operations. Called once the callback has settled. */
|
|
1002
|
+
end() {
|
|
1003
|
+
this.#ended = true;
|
|
1004
|
+
}
|
|
1005
|
+
/** The error of the first operation that found the transaction already finished, if any. */
|
|
1006
|
+
get autoCommitCause() {
|
|
1007
|
+
return this.#autoCommitCause;
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Whether IndexedDB has already finished this transaction on its own, so a
|
|
1011
|
+
* rollback can no longer undo its writes. Probes the transaction when no
|
|
1012
|
+
* operation has failed yet, and records the probe error as the cause, so call
|
|
1013
|
+
* it before rolling back or committing.
|
|
1014
|
+
*/
|
|
1015
|
+
async hasAutoCommitted() {
|
|
1016
|
+
if (this.#state !== "open") return this.#state === "auto-committed";
|
|
1017
|
+
const [storeName] = this.#storeNames;
|
|
1018
|
+
try {
|
|
1019
|
+
await this.#scope.execute({
|
|
1020
|
+
meta: this.#meta,
|
|
1021
|
+
kind: "key-get",
|
|
1022
|
+
storeName,
|
|
1023
|
+
key: ""
|
|
1024
|
+
});
|
|
1025
|
+
return false;
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
if (!isTransactionInactive(error)) return false;
|
|
1028
|
+
this.#state = "auto-committed";
|
|
1029
|
+
this.#autoCommitCause = error;
|
|
1030
|
+
return true;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
/** Expose rows from the existing scope through the ORM query-result interface. */
|
|
1034
|
+
query(plan) {
|
|
1035
|
+
const run = executeMutationPlan.bind(null, this);
|
|
1036
|
+
return new AsyncIterableResult((async function* () {
|
|
1037
|
+
for (const row of await run(plan.idbPlan)) yield row;
|
|
1038
|
+
})());
|
|
1039
|
+
}
|
|
1040
|
+
/** Reuse the outer scope after verifying that the requested stores were declared. */
|
|
1041
|
+
async transaction(storeNames) {
|
|
1042
|
+
this.#assertNotEnded();
|
|
1043
|
+
try {
|
|
1044
|
+
this.#assertStoresListed(storeNames);
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
this.#recordFailure(error);
|
|
1047
|
+
throw error;
|
|
1048
|
+
}
|
|
1049
|
+
return this;
|
|
1050
|
+
}
|
|
1051
|
+
/** Run one operation in the scope and abort the transaction when it fails. */
|
|
1052
|
+
async execute(plan) {
|
|
1053
|
+
this.#assertNotEnded();
|
|
1054
|
+
try {
|
|
1055
|
+
this.#assertStoresListed([plan.storeName]);
|
|
1056
|
+
return await this.#scope.execute(plan);
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
this.#recordFailure(error);
|
|
1059
|
+
throw error;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
async commit() {}
|
|
1063
|
+
rollback() {
|
|
1064
|
+
this.#recordFailure(void 0);
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Wrap an ORM accessor so that any call that fails aborts the transaction.
|
|
1068
|
+
*
|
|
1069
|
+
* Some failures, such as enum validation, happen before an operation reaches
|
|
1070
|
+
* this executor. Without this wrapper the callback could catch them and
|
|
1071
|
+
* commit the writes made before. The wrapper covers errors thrown
|
|
1072
|
+
* synchronously, rejected promises, and rejections of a result that is
|
|
1073
|
+
* awaited directly (`AsyncIterableResult`). Objects the accessor returns,
|
|
1074
|
+
* such as `where()` builders, are wrapped too.
|
|
1075
|
+
*/
|
|
1076
|
+
abortOnFailure(target) {
|
|
1077
|
+
return new Proxy(target, { get: (object, property) => {
|
|
1078
|
+
const value = Reflect.get(object, property, object);
|
|
1079
|
+
if (typeof value !== "function") return value;
|
|
1080
|
+
if (property === "then") return this.#observeThen(object, value);
|
|
1081
|
+
return (...args) => {
|
|
1082
|
+
const result = this.#recordThrown(() => value.apply(object, args));
|
|
1083
|
+
if (result instanceof Promise) {
|
|
1084
|
+
result.catch((error) => this.#recordFailure(error));
|
|
1085
|
+
return result;
|
|
1086
|
+
}
|
|
1087
|
+
return typeof result === "object" && result !== null ? this.abortOnFailure(result) : result;
|
|
1088
|
+
};
|
|
1089
|
+
} });
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* `await result` passes its own rejection handler to `then()`, which turns the
|
|
1093
|
+
* rejection into a fulfilled promise. Record the failure before that handler runs.
|
|
1094
|
+
*/
|
|
1095
|
+
#observeThen(object, then) {
|
|
1096
|
+
return (onFulfilled, onRejected) => then.call(object, onFulfilled, (error) => {
|
|
1097
|
+
this.#recordFailure(error);
|
|
1098
|
+
if (typeof onRejected !== "function") throw error;
|
|
1099
|
+
return onRejected(error);
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
/** Record synchronous accessor failures before propagating them to the callback. */
|
|
1103
|
+
#recordThrown(call) {
|
|
1104
|
+
try {
|
|
1105
|
+
return call();
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
this.#recordFailure(error);
|
|
1108
|
+
throw error;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
#assertNotEnded() {
|
|
1112
|
+
if (this.#ended) throw new Error("This transaction has ended. Do not use `tx` after the db.transaction() callback finishes.");
|
|
1113
|
+
}
|
|
1114
|
+
/** Reject undeclared stores before an operation can escape the transaction scope. */
|
|
1115
|
+
#assertStoresListed(storeNames) {
|
|
1116
|
+
const missing = storeNames.filter((name) => !this.#storeNames.has(name));
|
|
1117
|
+
if (missing.length > 0) throw new Error(`Stores ${missing.map((name) => `"${name}"`).join(", ")} are not part of this transaction. List the models that own them in the first argument of db.transaction().`);
|
|
1118
|
+
}
|
|
1119
|
+
/** Any failed operation aborts the whole transaction, even if the callback catches the error. */
|
|
1120
|
+
#recordFailure(error) {
|
|
1121
|
+
if (this.#state !== "open") return;
|
|
1122
|
+
if (isTransactionInactive(error)) {
|
|
1123
|
+
this.#state = "auto-committed";
|
|
1124
|
+
this.#autoCommitCause = error;
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
this.#state = "aborted";
|
|
1128
|
+
this.#scope.rollback();
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
//#endregion
|
|
1132
|
+
//#region src/core/orm-transaction.ts
|
|
1133
|
+
/**
|
|
1134
|
+
* Thrown when IndexedDB finished the transaction before the callback did, so
|
|
1135
|
+
* writes made before that point were committed and could not be rolled back.
|
|
1136
|
+
*
|
|
1137
|
+
* IndexedDB commits a transaction as soon as it has no pending request. That
|
|
1138
|
+
* happens when the callback awaits something other than a `tx` operation, such
|
|
1139
|
+
* as a timer or a network call (ADR 005). `cause` holds the original error.
|
|
1140
|
+
*/
|
|
1141
|
+
var IdbTransactionCommittedEarlyError = class extends Error {
|
|
1142
|
+
constructor(cause) {
|
|
1143
|
+
super("The transaction was committed before the db.transaction() callback finished, so writes made earlier in the callback were saved and not rolled back. Await only `tx` operations inside the callback; do not await timers or network calls.", { cause });
|
|
1144
|
+
this.name = "IdbTransactionCommittedEarlyError";
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
/**
|
|
1148
|
+
* Run `fn` inside one readwrite transaction over the models behind `rootKeys`.
|
|
1149
|
+
*
|
|
1150
|
+
* Besides the listed models, the transaction opens the stores their operations
|
|
1151
|
+
* need: parents for foreign-key checks, children for referential actions, and
|
|
1152
|
+
* related models for `include`. An operation that needs any other store fails
|
|
1153
|
+
* with an error that names it.
|
|
1154
|
+
*/
|
|
1155
|
+
async function runOrmTransaction(options, rootKeys, fn) {
|
|
1156
|
+
const { contract, executor } = options;
|
|
1157
|
+
if (rootKeys.length === 0) throw new Error("db.transaction() needs at least one model to open the transaction on.");
|
|
1158
|
+
const modelNames = rootKeys.map((key) => {
|
|
1159
|
+
const root = contract.roots[key];
|
|
1160
|
+
if (root === void 0) throw new Error(`db.transaction(): "${key}" is not a root of the contract.`);
|
|
1161
|
+
return root.model;
|
|
1162
|
+
});
|
|
1163
|
+
const storeNames = [...new Set(modelNames.flatMap((model) => storeNamesNeededBy(contract, model)))];
|
|
1164
|
+
const scope = await executor.transaction(storeNames, "readwrite");
|
|
1165
|
+
const open = new OpenTransaction(scope, storeNames, makePlanMeta(contract));
|
|
1166
|
+
let result;
|
|
1167
|
+
try {
|
|
1168
|
+
result = await fn(pickRoots(idbOrm({
|
|
1169
|
+
contract,
|
|
1170
|
+
executor: open
|
|
1171
|
+
}), rootKeys, open));
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
open.end();
|
|
1174
|
+
if (await open.hasAutoCommitted()) await failCommittedEarly(scope, error);
|
|
1175
|
+
scope.rollback();
|
|
1176
|
+
throw error;
|
|
1177
|
+
}
|
|
1178
|
+
open.end();
|
|
1179
|
+
if (await open.hasAutoCommitted()) await failCommittedEarly(scope, open.autoCommitCause);
|
|
1180
|
+
await scope.commit();
|
|
1181
|
+
return result;
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Reports a transaction that IndexedDB committed before the callback ended. The writes
|
|
1185
|
+
* persisted, so the scope is committed rather than rolled back: a tracked scope only
|
|
1186
|
+
* announces its writes (for example, sync outbox notifications) when it commits.
|
|
1187
|
+
*/
|
|
1188
|
+
async function failCommittedEarly(scope, cause) {
|
|
1189
|
+
await scope.commit().catch(() => {});
|
|
1190
|
+
throw new IdbTransactionCommittedEarlyError(cause);
|
|
1191
|
+
}
|
|
1192
|
+
/** Collect stores used by relation reads, child actions, and cascading updates. */
|
|
1193
|
+
function storeNamesNeededBy(contract, modelName) {
|
|
1194
|
+
return [
|
|
1195
|
+
...collectRelatedStoreNames(contract, modelName),
|
|
1196
|
+
...collectDeleteStoreNames(contract, modelName),
|
|
1197
|
+
...collectOnUpdateClosureStoreNames(contract, modelName)
|
|
1198
|
+
];
|
|
1199
|
+
}
|
|
1200
|
+
/** Expose only the requested roots and abort the scope on any accessor failure. */
|
|
1201
|
+
function pickRoots(orm, rootKeys, open) {
|
|
1202
|
+
const picked = {};
|
|
1203
|
+
for (const key of rootKeys) picked[key] = open.abortOnFailure(orm[key]);
|
|
1204
|
+
return picked;
|
|
1205
|
+
}
|
|
1206
|
+
//#endregion
|
|
1207
|
+
export { runOrmTransaction as n, idbOrm as r, IdbTransactionCommittedEarlyError as t };
|
|
1400
1208
|
|
|
1401
|
-
//# sourceMappingURL=
|
|
1209
|
+
//# sourceMappingURL=orm-transaction-BGx1eukw.mjs.map
|