@prisma-idb/client-idb 0.6.1 → 0.7.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 +25 -12
- package/dist/client-auto.d.mts +20 -41
- package/dist/client-auto.d.mts.map +1 -1
- package/dist/client-auto.mjs +20 -41
- package/dist/client-auto.mjs.map +1 -1
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -1
- package/dist/{idb-orm-Cupnnwum.mjs → idb-orm-JBg6si7u.mjs} +469 -200
- package/dist/idb-orm-JBg6si7u.mjs.map +1 -0
- package/dist/{idb-orm-zCRkkD_D.d.mts → idb-orm-MOH1S3ES.d.mts} +93 -23
- package/dist/idb-orm-MOH1S3ES.d.mts.map +1 -0
- package/dist/{managed-client-B_8JZM3h.mjs → managed-client-BLdCyxUi.mjs} +3 -3
- package/dist/{managed-client-B_8JZM3h.mjs.map → managed-client-BLdCyxUi.mjs.map} +1 -1
- package/dist/{managed-client-kdyHm72t.d.mts → managed-client-CJD9lBx5.d.mts} +2 -2
- package/dist/{managed-client-kdyHm72t.d.mts.map → managed-client-CJD9lBx5.d.mts.map} +1 -1
- package/dist/orm.d.mts +2 -2
- package/dist/orm.d.mts.map +1 -1
- package/dist/orm.mjs +2 -2
- package/package.json +6 -6
- package/dist/idb-orm-Cupnnwum.mjs.map +0 -1
- package/dist/idb-orm-zCRkkD_D.d.mts.map +0 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { AsyncIterableResult } from "@prisma/orm-framework/components/runtime";
|
|
2
2
|
import { andExpr, evaluateFilter, fieldFilter, nullCheckExpr, shorthandToFilterExpr } from "@prisma-idb/adapter-idb/runtime";
|
|
3
3
|
import { domainModelsAtDefaultNamespace } from "@prisma/orm-framework/contract/types";
|
|
4
|
+
import { compareFieldValues, fieldValueToken, fieldValuesEqual, isValidIdbKey, keyEquals, keyToken } from "@prisma-idb/target-idb/runtime";
|
|
4
5
|
import { generateId } from "@prisma/orm-framework/ids/runtime";
|
|
6
|
+
import { keyPathFields } from "@prisma-idb/target-idb/pack";
|
|
5
7
|
//#region src/core/types.ts
|
|
6
8
|
/**
|
|
7
9
|
* Extract the `storeName` from a model's storage metadata at runtime.
|
|
@@ -12,22 +14,30 @@ function getStoreName(contract, modelName) {
|
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* Extract the `keyPath` from a model's storage metadata at runtime.
|
|
15
|
-
*
|
|
17
|
+
* A single field name for the common case; an ordered array of field names
|
|
18
|
+
* for a compound primary key.
|
|
19
|
+
*
|
|
20
|
+
* @throws if the model is unknown or its storage has no `keyPath` — a
|
|
21
|
+
* validated contract always has one, so this only fires on a malformed or
|
|
22
|
+
* hand-built contract, where guessing a key name would silently read/write
|
|
23
|
+
* the wrong key.
|
|
16
24
|
*/
|
|
17
|
-
function getKeyPath
|
|
18
|
-
|
|
25
|
+
function getKeyPath(contract, modelName) {
|
|
26
|
+
const keyPath = (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.keyPath;
|
|
27
|
+
if (keyPath === void 0) throw new Error(`Model "${modelName}" has no storage.keyPath in the contract.`);
|
|
28
|
+
return keyPath;
|
|
19
29
|
}
|
|
20
30
|
/**
|
|
21
|
-
*
|
|
22
|
-
* `
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* equality semantics on those are unsupported.
|
|
31
|
+
* Extracts a row's primary key as an {@link IDBValidKey} — the scalar value
|
|
32
|
+
* at `row[keyPath]` for a single-field key, or an ordered array of the
|
|
33
|
+
* compound key's member field values (matching the array form IDB itself
|
|
34
|
+
* expects for a compound `keyPath`). The one construction site every
|
|
35
|
+
* key-get/put/delete/update call should route through, so compound-key field
|
|
36
|
+
* order stays consistent everywhere.
|
|
28
37
|
*/
|
|
29
|
-
function
|
|
30
|
-
|
|
38
|
+
function extractKeyFromRow(row, keyPath) {
|
|
39
|
+
if (typeof keyPath === "string") return row[keyPath];
|
|
40
|
+
return keyPath.map((field) => row[field]);
|
|
31
41
|
}
|
|
32
42
|
/**
|
|
33
43
|
* Build a field → indexName lookup for every single-field, non-multi-entry
|
|
@@ -43,27 +53,6 @@ function buildFieldToIndexMap(contract, storeName) {
|
|
|
43
53
|
return result;
|
|
44
54
|
}
|
|
45
55
|
/**
|
|
46
|
-
* Returns `true` when `value` is a valid {@link IDBValidKey} — i.e. a value
|
|
47
|
-
* that can be passed to `IDBKeyRange.only()` without throwing a DataError.
|
|
48
|
-
* Booleans, `NaN`, `BigInt`, plain objects, and `null`/`undefined` are not
|
|
49
|
-
* valid IDB keys (only number/string/Date/binary/Array are, per the
|
|
50
|
-
* IndexedDB spec). Arrays are validated recursively — an array containing
|
|
51
|
-
* any invalid element (e.g. a nested boolean) is itself not a valid key.
|
|
52
|
-
*
|
|
53
|
-
* Shared by the relation loader (filtering FK values before building
|
|
54
|
-
* `IDBKeyRange.only()` plans) and query-shaping (gating `eq` conditions for
|
|
55
|
-
* index/PK point-range acceleration).
|
|
56
|
-
*/
|
|
57
|
-
function isValidIdbKey(value) {
|
|
58
|
-
if (typeof value === "number") return !Number.isNaN(value);
|
|
59
|
-
if (typeof value === "string") return true;
|
|
60
|
-
if (value instanceof Date) return true;
|
|
61
|
-
if (value instanceof ArrayBuffer) return true;
|
|
62
|
-
if (ArrayBuffer.isView(value)) return true;
|
|
63
|
-
if (Array.isArray(value)) return value.every((v) => isValidIdbKey(v));
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
56
|
* Resolve a model's named relation to a {@link ContractReferenceRelation} at
|
|
68
57
|
* runtime, or `undefined` when the relation is absent or an embedded relation
|
|
69
58
|
* (no `on` join block). Used by `include()` to find the related model name and
|
|
@@ -201,6 +190,21 @@ function isIndexableEqValue(value) {
|
|
|
201
190
|
return isValidIdbKey(value);
|
|
202
191
|
}
|
|
203
192
|
/**
|
|
193
|
+
* `true` when `field` is a *single-field* primary key that a lone `eq`
|
|
194
|
+
* condition can directly point-range-query (`store.get`/`openCursor(range)`
|
|
195
|
+
* against the store's own keyPath, no named index required).
|
|
196
|
+
*
|
|
197
|
+
* Deliberately `false` for a compound (array) `keyPath`, even if `field`
|
|
198
|
+
* happens to be one of its member fields — a single `eq` condition can only
|
|
199
|
+
* pin one member, not the whole compound key, so it can't drive a PK
|
|
200
|
+
* point-range scan on its own. Accelerating that case needs *all* member
|
|
201
|
+
* fields ANDed together, which is a cost-based multi-field planner decision
|
|
202
|
+
* (Phase 10 §3.2), not this peephole optimizer's job.
|
|
203
|
+
*/
|
|
204
|
+
function isPrimaryKeyField(field, keyPath) {
|
|
205
|
+
return typeof keyPath === "string" && field === keyPath;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
204
208
|
* Scan the combined filter expression for the first `eq` field condition whose
|
|
205
209
|
* field either has a matching IDB index (`fieldToIndexName[field]` is set) or
|
|
206
210
|
* *is* the store's own primary key (`keyPath`) — a store's keyPath is always
|
|
@@ -219,7 +223,7 @@ function extractIndexEqualityHint(filter, fieldToIndexName, keyPath) {
|
|
|
219
223
|
if (filter === void 0) return null;
|
|
220
224
|
if (filter.kind === "field" && filter.op === "eq") {
|
|
221
225
|
const indexName = fieldToIndexName[filter.field];
|
|
222
|
-
if ((indexName !== void 0 || filter.field
|
|
226
|
+
if ((indexName !== void 0 || isPrimaryKeyField(filter.field, keyPath)) && isIndexableEqValue(filter.value)) return {
|
|
223
227
|
indexName,
|
|
224
228
|
value: filter.value,
|
|
225
229
|
remainingFilter: void 0
|
|
@@ -232,7 +236,7 @@ function extractIndexEqualityHint(filter, fieldToIndexName, keyPath) {
|
|
|
232
236
|
const expr = flat[i];
|
|
233
237
|
if (expr.kind === "field" && expr.op === "eq") {
|
|
234
238
|
const indexName = fieldToIndexName[expr.field];
|
|
235
|
-
if ((indexName !== void 0 || expr.field
|
|
239
|
+
if ((indexName !== void 0 || isPrimaryKeyField(expr.field, keyPath)) && isIndexableEqValue(expr.value)) {
|
|
236
240
|
const rest = flat.filter((_, j) => j !== i);
|
|
237
241
|
return {
|
|
238
242
|
indexName,
|
|
@@ -257,7 +261,7 @@ function tryExtractOrBranches(orNode, fieldToIndexName, keyPath, remainingFilter
|
|
|
257
261
|
for (const expr of orNode.exprs) {
|
|
258
262
|
if (expr.kind !== "field" || expr.op !== "eq") return null;
|
|
259
263
|
const indexName = fieldToIndexName[expr.field];
|
|
260
|
-
if (indexName === void 0 && expr.field
|
|
264
|
+
if (indexName === void 0 && !isPrimaryKeyField(expr.field, keyPath) || !isIndexableEqValue(expr.value)) return null;
|
|
261
265
|
branches.push({
|
|
262
266
|
indexName,
|
|
263
267
|
value: expr.value
|
|
@@ -297,22 +301,59 @@ function extractIndexOrHint(filter, fieldToIndexName, keyPath) {
|
|
|
297
301
|
* Build an in-memory comparator from an `orderBy` spec (field → direction).
|
|
298
302
|
*
|
|
299
303
|
* Returns `undefined` when there is nothing to sort by. Compares fields in
|
|
300
|
-
* declaration order
|
|
301
|
-
*
|
|
304
|
+
* declaration order, with {@link compareFieldValues}: the same order an
|
|
305
|
+
* IndexedDB index uses, so equal `Date`s tie and fall through to the next
|
|
306
|
+
* field, and `null`s sort last (first when descending).
|
|
302
307
|
*/
|
|
303
308
|
function buildRowComparator(orderBy) {
|
|
304
309
|
if (orderBy === void 0) return void 0;
|
|
305
310
|
return (a, b) => {
|
|
306
311
|
for (const [field, dir] of Object.entries(orderBy)) {
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
if (av === bv) continue;
|
|
310
|
-
const cmp = av < bv ? -1 : 1;
|
|
312
|
+
const cmp = compareFieldValues(a[field], b[field]);
|
|
313
|
+
if (cmp === 0) continue;
|
|
311
314
|
return dir === "desc" ? -cmp : cmp;
|
|
312
315
|
}
|
|
313
316
|
return 0;
|
|
314
317
|
};
|
|
315
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* `true` when a `cursor-scan` plan's result *cardinality* is fully determined
|
|
321
|
+
* by its store/index + key range — i.e. there is no in-memory `filter` that
|
|
322
|
+
* would need each row's value. Only then can `count()` be answered natively
|
|
323
|
+
* (`store.count(range)` / `index.count(range)`) without deserializing rows.
|
|
324
|
+
*
|
|
325
|
+
* `comparator` (ORDER BY), `direction`, `skip` and `take` don't change *which*
|
|
326
|
+
* rows match, so they don't disqualify a plan — `skip`/`take` are applied to
|
|
327
|
+
* the native total afterwards via {@link clampCount}.
|
|
328
|
+
*
|
|
329
|
+
* Safe for index scans because the equality hint that builds `range` never
|
|
330
|
+
* selects a `multiEntry` (or compound) index — `buildFieldToIndexMap` excludes
|
|
331
|
+
* them — so one record maps to at most one index entry and entries == rows.
|
|
332
|
+
*/
|
|
333
|
+
function isNativelyCountable(plan) {
|
|
334
|
+
return plan.filter === void 0;
|
|
335
|
+
}
|
|
336
|
+
/** Rewrites a natively-countable `cursor-scan` as the equivalent native `count` plan. */
|
|
337
|
+
function toCountPlan(plan) {
|
|
338
|
+
return {
|
|
339
|
+
meta: plan.meta,
|
|
340
|
+
kind: "count",
|
|
341
|
+
storeName: plan.storeName,
|
|
342
|
+
...plan.indexName !== void 0 ? { indexName: plan.indexName } : {},
|
|
343
|
+
...plan.range !== void 0 ? { range: plan.range } : {}
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Applies `skip`/`take` to an *unpaginated* total. A paginated count is pure
|
|
348
|
+
* arithmetic on the unpaginated count — `max(0, total - skip)`, capped at
|
|
349
|
+
* `take` — so this is exact, not an approximation.
|
|
350
|
+
*/
|
|
351
|
+
function clampCount(total, skip, take) {
|
|
352
|
+
let n = total;
|
|
353
|
+
if (skip !== void 0) n = Math.max(0, n - skip);
|
|
354
|
+
if (take !== void 0) n = Math.min(take, n);
|
|
355
|
+
return n;
|
|
356
|
+
}
|
|
316
357
|
//#endregion
|
|
317
358
|
//#region src/core/aggregate-builder.ts
|
|
318
359
|
/**
|
|
@@ -533,8 +574,11 @@ function applyUpdateDefaults(defaults, storeName, patch, cache) {
|
|
|
533
574
|
* Batch-load a single named relation for all rows in `rows` and attach the
|
|
534
575
|
* result to each row under the `relName` key.
|
|
535
576
|
*
|
|
536
|
-
* The join
|
|
537
|
-
*
|
|
577
|
+
* The join matches every field of the relation, so compound foreign keys join
|
|
578
|
+
* on the whole tuple. It uses one key-range scan per distinct tuple when an
|
|
579
|
+
* index or the related primary key covers the target fields, and otherwise
|
|
580
|
+
* one full scan with an in-memory filter. Rows are then grouped in memory,
|
|
581
|
+
* which avoids N+1 queries.
|
|
538
582
|
*
|
|
539
583
|
* The `entry` carries any `include()` refinement:
|
|
540
584
|
*
|
|
@@ -562,23 +606,22 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
|
|
|
562
606
|
const relation = rawRelation;
|
|
563
607
|
const { cardinality, on } = relation;
|
|
564
608
|
const relatedModelName = relation.to.model;
|
|
565
|
-
const
|
|
566
|
-
const
|
|
567
|
-
if (
|
|
609
|
+
const localFields = on.localFields;
|
|
610
|
+
const targetFields = on.targetFields;
|
|
611
|
+
if (localFields.length === 0 || localFields.length !== targetFields.length) return rows;
|
|
568
612
|
const relatedModel = models[relatedModelName];
|
|
569
613
|
if (relatedModel === void 0) return rows;
|
|
570
614
|
const relatedStoreName = typeof relatedModel.storage === "object" && relatedModel.storage !== null && "storeName" in relatedModel.storage ? String(relatedModel.storage["storeName"]) : relatedModelName;
|
|
571
615
|
const isScalar = entry.kind === "scalar";
|
|
572
|
-
const
|
|
616
|
+
const localTuples = /* @__PURE__ */ new Map();
|
|
573
617
|
for (const row of rows) {
|
|
574
|
-
const
|
|
575
|
-
if (
|
|
618
|
+
const token = tupleToken(row, localFields);
|
|
619
|
+
if (token !== null) localTuples.set(token, localFields.map((f) => row[f]));
|
|
576
620
|
}
|
|
577
|
-
if (
|
|
621
|
+
if (localTuples.size === 0) return rows.map((row) => ({
|
|
578
622
|
...row,
|
|
579
623
|
[relName]: isScalar ? 0 : cardinality === "1:N" ? [] : null
|
|
580
624
|
}));
|
|
581
|
-
const capturedForeignField = foreignField;
|
|
582
625
|
const refinedWhere = combineFilterExprs(entry.state.filters);
|
|
583
626
|
const planMeta = {
|
|
584
627
|
target: "idb",
|
|
@@ -586,21 +629,25 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
|
|
|
586
629
|
lane: "idb-orm",
|
|
587
630
|
annotations: { groupingKey }
|
|
588
631
|
};
|
|
589
|
-
const
|
|
590
|
-
const targetsRelatedPk = fkIndexName === void 0 && typeof IDBKeyRange !== "undefined" && capturedForeignField === getKeyPath$1(contract, relatedModelName);
|
|
632
|
+
const rangeSource = findRangeSource(contract, relatedStoreName, relatedModelName, targetFields);
|
|
591
633
|
let relatedRows;
|
|
592
|
-
if (
|
|
634
|
+
if (rangeSource !== void 0) {
|
|
593
635
|
const refinedFilter = refinedWhere !== void 0 ? (row) => evaluateFilter(refinedWhere, row) : void 0;
|
|
594
|
-
const
|
|
595
|
-
|
|
636
|
+
const ranges = [];
|
|
637
|
+
for (const values of localTuples.values()) {
|
|
638
|
+
const ordered = keyPathFields(rangeSource.keyPath).map((f) => values[targetFields.indexOf(f)]);
|
|
639
|
+
if (!ordered.every((v) => isValidIdbKey(v))) continue;
|
|
640
|
+
ranges.push(IDBKeyRange.only(typeof rangeSource.keyPath === "string" ? ordered[0] : ordered));
|
|
641
|
+
}
|
|
642
|
+
relatedRows = (await Promise.all(ranges.map(async (range) => {
|
|
596
643
|
const plan = {
|
|
597
644
|
meta: planMeta,
|
|
598
645
|
idbPlan: {
|
|
599
646
|
meta: planMeta,
|
|
600
647
|
kind: "cursor-scan",
|
|
601
648
|
storeName: relatedStoreName,
|
|
602
|
-
...
|
|
603
|
-
range
|
|
649
|
+
...rangeSource.indexName !== void 0 ? { indexName: rangeSource.indexName } : {},
|
|
650
|
+
range,
|
|
604
651
|
...refinedFilter !== void 0 ? { filter: refinedFilter } : {}
|
|
605
652
|
}
|
|
606
653
|
};
|
|
@@ -610,7 +657,10 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
|
|
|
610
657
|
}))).flat();
|
|
611
658
|
} else {
|
|
612
659
|
relatedRows = [];
|
|
613
|
-
const filter = (row) =>
|
|
660
|
+
const filter = (row) => {
|
|
661
|
+
const token = tupleToken(row, targetFields);
|
|
662
|
+
return token !== null && localTuples.has(token) && (refinedWhere === void 0 || evaluateFilter(refinedWhere, row));
|
|
663
|
+
};
|
|
614
664
|
const plan = {
|
|
615
665
|
meta: planMeta,
|
|
616
666
|
idbPlan: {
|
|
@@ -625,20 +675,25 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
|
|
|
625
675
|
if (cardinality === "1:N") {
|
|
626
676
|
const grouped = /* @__PURE__ */ new Map();
|
|
627
677
|
for (const rrow of relatedRows) {
|
|
628
|
-
const gk = rrow
|
|
678
|
+
const gk = tupleToken(rrow, targetFields);
|
|
679
|
+
if (gk === null) continue;
|
|
629
680
|
const group = grouped.get(gk) ?? [];
|
|
630
681
|
group.push(rrow);
|
|
631
682
|
grouped.set(gk, group);
|
|
632
683
|
}
|
|
684
|
+
const groupFor = (row) => {
|
|
685
|
+
const token = tupleToken(row, localFields);
|
|
686
|
+
return token === null ? [] : grouped.get(token) ?? [];
|
|
687
|
+
};
|
|
633
688
|
if (isScalar) return rows.map((row) => ({
|
|
634
689
|
...row,
|
|
635
|
-
[relName]: (
|
|
690
|
+
[relName]: groupFor(row).length
|
|
636
691
|
}));
|
|
637
692
|
const comparator = buildRowComparator(entry.state.orderBy);
|
|
638
693
|
const skip = entry.state.skip ?? 0;
|
|
639
694
|
const take = entry.state.take;
|
|
640
695
|
return rows.map((row) => {
|
|
641
|
-
let group =
|
|
696
|
+
let group = groupFor(row);
|
|
642
697
|
if (comparator !== void 0) group = [...group].sort(comparator);
|
|
643
698
|
if (skip > 0 || take !== void 0) group = group.slice(skip, take !== void 0 ? skip + take : void 0);
|
|
644
699
|
return {
|
|
@@ -648,11 +703,50 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
|
|
|
648
703
|
});
|
|
649
704
|
}
|
|
650
705
|
const indexed = /* @__PURE__ */ new Map();
|
|
651
|
-
for (const rrow of relatedRows)
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
706
|
+
for (const rrow of relatedRows) {
|
|
707
|
+
const token = tupleToken(rrow, targetFields);
|
|
708
|
+
if (token !== null) indexed.set(token, rrow);
|
|
709
|
+
}
|
|
710
|
+
return rows.map((row) => {
|
|
711
|
+
const token = tupleToken(row, localFields);
|
|
712
|
+
return {
|
|
713
|
+
...row,
|
|
714
|
+
[relName]: (token === null ? void 0 : indexed.get(token)) ?? null
|
|
715
|
+
};
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* A `Map` key for the values of `fields` in `row`, or `null` when any of them
|
|
720
|
+
* is null or undefined. Equal `Date`s and binary values give equal tokens.
|
|
721
|
+
*/
|
|
722
|
+
function tupleToken(row, fields) {
|
|
723
|
+
const parts = [];
|
|
724
|
+
for (const field of fields) {
|
|
725
|
+
const value = row[field];
|
|
726
|
+
if (value === null || value === void 0) return null;
|
|
727
|
+
const token = fieldValueToken(value);
|
|
728
|
+
parts.push([typeof token, String(token)]);
|
|
729
|
+
}
|
|
730
|
+
return JSON.stringify(parts);
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Finds an index, or else the store's primary key, whose key path has exactly
|
|
734
|
+
* `fields` (in any order), so a lookup on those fields can use a key range.
|
|
735
|
+
* Returns `undefined` when there is none, or when `IDBKeyRange` isn't available.
|
|
736
|
+
*/
|
|
737
|
+
function findRangeSource(contract, storeName, modelName, fields) {
|
|
738
|
+
if (typeof IDBKeyRange === "undefined") return void 0;
|
|
739
|
+
const coversFields = (keyPath) => {
|
|
740
|
+
const keyFields = keyPathFields(keyPath);
|
|
741
|
+
return keyFields.length === fields.length && keyFields.every((f) => fields.includes(f));
|
|
742
|
+
};
|
|
743
|
+
const indexes = contract.storage.stores[storeName]?.indexes ?? {};
|
|
744
|
+
for (const [indexName, indexDef] of Object.entries(indexes)) if (indexDef.multiEntry !== true && coversFields(indexDef.keyPath)) return {
|
|
745
|
+
indexName,
|
|
746
|
+
keyPath: indexDef.keyPath
|
|
747
|
+
};
|
|
748
|
+
const primaryKey = getKeyPath(contract, modelName);
|
|
749
|
+
return coversFields(primaryKey) ? { keyPath: primaryKey } : void 0;
|
|
656
750
|
}
|
|
657
751
|
//#endregion
|
|
658
752
|
//#region src/core/mutation-scope.ts
|
|
@@ -774,22 +868,49 @@ function requireTransactionExecutor(executor) {
|
|
|
774
868
|
async function executeNestedCreateMutation(options) {
|
|
775
869
|
const { executor, contract, modelName, data } = options;
|
|
776
870
|
const record = data;
|
|
777
|
-
const storeNames = collectStoreNames(contract, modelName, record);
|
|
871
|
+
const storeNames = collectStoreNames(contract, modelName, record, "create");
|
|
778
872
|
const defaultsCache = createMutationDefaultsCache();
|
|
779
873
|
return withMutationScope(executor, storeNames, (scope) => createGraph(scope, contract, modelName, record, defaultsCache));
|
|
780
874
|
}
|
|
781
875
|
async function executeNestedUpdateMutation(options) {
|
|
782
876
|
const { executor, contract, modelName, filters, data } = options;
|
|
783
877
|
const record = data;
|
|
784
|
-
const storeNames = collectStoreNames(contract, modelName, record);
|
|
878
|
+
const storeNames = collectStoreNames(contract, modelName, record, "update");
|
|
785
879
|
const defaultsCache = createMutationDefaultsCache();
|
|
786
880
|
return withMutationScope(executor, storeNames, (scope) => updateFirstGraph(scope, contract, modelName, filters, record, defaultsCache));
|
|
787
881
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
882
|
+
/**
|
|
883
|
+
* Every store a nested write may touch: the model's own store, each related
|
|
884
|
+
* store a relation callback writes to, and the parent stores that foreign-key
|
|
885
|
+
* checks read, for the model and for each related model. An update also
|
|
886
|
+
* declares the stores its `onUpdate` referential actions may touch.
|
|
887
|
+
*
|
|
888
|
+
* Parent stores are declared for every foreign key, not only the ones `data`
|
|
889
|
+
* sets, because a nested write can set foreign keys through `connect()` or
|
|
890
|
+
* through a default.
|
|
891
|
+
*/
|
|
892
|
+
function collectStoreNames(contract, modelName, data, kind) {
|
|
893
|
+
const stores = /* @__PURE__ */ new Set([getStoreName(contract, modelName), ...fkParentStoreNames(contract, modelName)]);
|
|
894
|
+
const scalarData = {};
|
|
895
|
+
const relationNames = /* @__PURE__ */ new Set();
|
|
896
|
+
for (const def of getRelationDefinitions(contract, modelName)) {
|
|
897
|
+
relationNames.add(def.relationName);
|
|
898
|
+
if (def.relationName in data && isRelationMutationCallback(data[def.relationName])) {
|
|
899
|
+
stores.add(def.relatedStoreName);
|
|
900
|
+
for (const store of fkParentStoreNames(contract, def.relatedModelName)) stores.add(store);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (kind === "update") {
|
|
904
|
+
for (const [field, value] of Object.entries(data)) if (!relationNames.has(field)) scalarData[field] = value;
|
|
905
|
+
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, getStoreName(contract, modelName), scalarData, createMutationDefaultsCache());
|
|
906
|
+
for (const store of collectOnUpdateEnforcementStoreNames(contract, modelName, patch).storeNames) stores.add(store);
|
|
907
|
+
}
|
|
791
908
|
return [...stores];
|
|
792
909
|
}
|
|
910
|
+
/** The stores of every parent `modelName` has a foreign key to. */
|
|
911
|
+
function fkParentStoreNames(contract, modelName) {
|
|
912
|
+
return getRelationDefinitions(contract, modelName).filter((def) => def.cardinality === "N:1").map((def) => def.relatedStoreName);
|
|
913
|
+
}
|
|
793
914
|
async function createGraph(scope, contract, modelName, input, defaultsCache) {
|
|
794
915
|
const parsed = parseMutationInput(contract, modelName, input);
|
|
795
916
|
const { parentOwned, childOwned } = partitionByOwnership(parsed.relationMutations);
|
|
@@ -815,9 +936,11 @@ async function updateFirstGraph(scope, contract, modelName, filters, input, defa
|
|
|
815
936
|
let parentRow = existingRow;
|
|
816
937
|
if (Object.keys(scalarData).length > 0) {
|
|
817
938
|
const storeName = getStoreName(contract, modelName);
|
|
818
|
-
const key = existingRow
|
|
939
|
+
const key = extractKeyFromRow(existingRow, getKeyPath(contract, modelName));
|
|
819
940
|
const meta = makePlanMeta(contract);
|
|
820
941
|
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, storeName, scalarData, defaultsCache);
|
|
942
|
+
await validateScalarFks(scope, contract, modelName, patch, existingRow);
|
|
943
|
+
await applyReferentialActionsForRowOnUpdate(scope, contract, modelName, existingRow, patch);
|
|
821
944
|
const updated = (await scope.execute({
|
|
822
945
|
meta,
|
|
823
946
|
kind: "update",
|
|
@@ -970,6 +1093,7 @@ async function insertSingleRow(scope, contract, modelName, data, defaultsCache)
|
|
|
970
1093
|
const storeName = getStoreName(contract, modelName);
|
|
971
1094
|
const meta = makePlanMeta(contract);
|
|
972
1095
|
const record = applyCreateDefaults(contract.execution?.mutations.defaults, storeName, data, defaultsCache);
|
|
1096
|
+
await validateScalarFks(scope, contract, modelName, record);
|
|
973
1097
|
return (await scope.execute({
|
|
974
1098
|
meta,
|
|
975
1099
|
kind: "add",
|
|
@@ -1020,10 +1144,7 @@ function buildCriterionFilter(criterion) {
|
|
|
1020
1144
|
}
|
|
1021
1145
|
function buildParentJoinFilter(parentValues) {
|
|
1022
1146
|
const pairs = [...parentValues.entries()];
|
|
1023
|
-
return (row) => pairs.every(([childField, parentValue]) => row[childField]
|
|
1024
|
-
}
|
|
1025
|
-
function getKeyPath(contract, modelName) {
|
|
1026
|
-
return (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.keyPath ?? "id";
|
|
1147
|
+
return (row) => pairs.every(([childField, parentValue]) => fieldValuesEqual(row[childField], parentValue));
|
|
1027
1148
|
}
|
|
1028
1149
|
function getStoredAction(contract, modelName, relationName, kind) {
|
|
1029
1150
|
return (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.relations?.[relationName]?.[kind];
|
|
@@ -1048,7 +1169,10 @@ function sameFields(a, b) {
|
|
|
1048
1169
|
* action kind on both sides so this fallback never has to arbitrate a real
|
|
1049
1170
|
* conflict.
|
|
1050
1171
|
*/
|
|
1051
|
-
function getReferentialActionForRelation(contract, modelName, def, kind
|
|
1172
|
+
function getReferentialActionForRelation(contract, modelName, def, kind) {
|
|
1173
|
+
return enforcedAction(findDeclaredAction(contract, modelName, def, kind));
|
|
1174
|
+
}
|
|
1175
|
+
function findDeclaredAction(contract, modelName, def, kind) {
|
|
1052
1176
|
const direct = getStoredAction(contract, modelName, def.relationName, kind);
|
|
1053
1177
|
if (direct !== void 0) return direct;
|
|
1054
1178
|
for (const inverse of getRelationDefinitions(contract, def.relatedModelName)) {
|
|
@@ -1058,29 +1182,88 @@ function getReferentialActionForRelation(contract, modelName, def, kind, default
|
|
|
1058
1182
|
const inverseAction = getStoredAction(contract, def.relatedModelName, inverse.relationName, kind);
|
|
1059
1183
|
if (inverseAction !== void 0) return inverseAction;
|
|
1060
1184
|
}
|
|
1061
|
-
return defaultAction;
|
|
1062
|
-
}
|
|
1063
|
-
function getOnDeleteForDeleteRelation(contract, modelName, def) {
|
|
1064
|
-
return getReferentialActionForRelation(contract, modelName, def, "onDelete", "restrict");
|
|
1065
1185
|
}
|
|
1066
1186
|
/**
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1187
|
+
* Applies the default and resolves `noAction`, matching what Postgres does
|
|
1188
|
+
* with the same schema, so a synced app behaves the same on both sides:
|
|
1189
|
+
*
|
|
1190
|
+
* - An undeclared action defaults to `restrict`, for `onDelete` and
|
|
1191
|
+
* `onUpdate` alike. Prisma 8 emits no `ON DELETE`/`ON UPDATE` clause for an
|
|
1192
|
+
* undeclared action, and the database default is `NO ACTION`.
|
|
1193
|
+
* - `noAction` behaves like `restrict`. In SQL, `NO ACTION` also rejects the
|
|
1194
|
+
* change; it only defers the check to the end of the statement, which makes
|
|
1195
|
+
* no difference here.
|
|
1071
1196
|
*/
|
|
1197
|
+
function enforcedAction(declared) {
|
|
1198
|
+
return declared === void 0 || declared === "noAction" ? "restrict" : declared;
|
|
1199
|
+
}
|
|
1200
|
+
function getOnDeleteForDeleteRelation(contract, modelName, def) {
|
|
1201
|
+
return getReferentialActionForRelation(contract, modelName, def, "onDelete");
|
|
1202
|
+
}
|
|
1072
1203
|
function getOnUpdateForRelation(contract, modelName, def) {
|
|
1073
|
-
return getReferentialActionForRelation(contract, modelName, def, "onUpdate"
|
|
1204
|
+
return getReferentialActionForRelation(contract, modelName, def, "onUpdate");
|
|
1074
1205
|
}
|
|
1075
1206
|
function isDeleteEnforcementRelation(contract, modelName, def) {
|
|
1076
1207
|
if (def.cardinality === "1:N") return true;
|
|
1077
1208
|
if (def.cardinality === "1:1") {
|
|
1078
1209
|
const keyPath = getKeyPath(contract, modelName);
|
|
1079
|
-
return def.localFields
|
|
1210
|
+
return sameFields(def.localFields, keyPathFields(keyPath));
|
|
1080
1211
|
}
|
|
1081
1212
|
return false;
|
|
1082
1213
|
}
|
|
1083
1214
|
/**
|
|
1215
|
+
* An `IDBKeyRange` for the lookup "does `modelName` have a row whose `field`
|
|
1216
|
+
* equals `value`?" — but ONLY when `field` is that model's own single-field
|
|
1217
|
+
* primary key (so the answer is a pure key lookup that needs no row value)
|
|
1218
|
+
* and `value` is a legal IDB key. `null` otherwise, and the caller keeps its
|
|
1219
|
+
* value-materializing `cursor-scan`.
|
|
1220
|
+
*
|
|
1221
|
+
* Deliberately `null` for a compound primary key even if `field` is one of its
|
|
1222
|
+
* members (one member can't pin the whole key), and for any non-key field
|
|
1223
|
+
* (that needs an index-resolution step — Phase 10.5's shared primitive, not
|
|
1224
|
+
* this helper's job). `IDBKeyRange.only` throws `DataError` on invalid keys
|
|
1225
|
+
* (`null`, `NaN`, booleans, …), hence the guard.
|
|
1226
|
+
*/
|
|
1227
|
+
function pkEqualityRange(contract, modelName, field, value) {
|
|
1228
|
+
if (typeof IDBKeyRange === "undefined") return null;
|
|
1229
|
+
const keyPath = getKeyPath(contract, modelName);
|
|
1230
|
+
if (typeof keyPath !== "string" || keyPath !== field) return null;
|
|
1231
|
+
if (!isValidIdbKey(value)) return null;
|
|
1232
|
+
return IDBKeyRange.only(value);
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Resolves to the first primary key in `storeName` within `range`, or
|
|
1236
|
+
* `undefined` — one `getKey` request, no row deserialized.
|
|
1237
|
+
*/
|
|
1238
|
+
async function firstKeyInRange(scope, meta, storeName, range) {
|
|
1239
|
+
return (await scope.execute({
|
|
1240
|
+
meta,
|
|
1241
|
+
kind: "keys",
|
|
1242
|
+
storeName,
|
|
1243
|
+
range,
|
|
1244
|
+
take: 1
|
|
1245
|
+
}))[0]?.["key"];
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* `true` if any child row matches the relation against `parentRow`'s values —
|
|
1249
|
+
* the existence check behind `restrict`. Key-only when the relation's single
|
|
1250
|
+
* child field is the child model's own primary key (a shared-PK 1:1); a
|
|
1251
|
+
* value-materializing `cursor-scan` (`take: 1`) otherwise.
|
|
1252
|
+
*/
|
|
1253
|
+
async function childExists(scope, contract, meta, def, parentRow) {
|
|
1254
|
+
if (def.targetFields.length === 1) {
|
|
1255
|
+
const range = pkEqualityRange(contract, def.relatedModelName, def.targetFields[0], parentRow[def.localFields[0]]);
|
|
1256
|
+
if (range !== null) return await firstKeyInRange(scope, meta, def.relatedStoreName, range) !== void 0;
|
|
1257
|
+
}
|
|
1258
|
+
return (await scope.execute({
|
|
1259
|
+
meta,
|
|
1260
|
+
kind: "cursor-scan",
|
|
1261
|
+
storeName: def.relatedStoreName,
|
|
1262
|
+
filter: buildChildFilterFromRow(def, parentRow),
|
|
1263
|
+
take: 1
|
|
1264
|
+
})).length > 0;
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1084
1267
|
* Builds a filter matching a relation's children against one specific parent
|
|
1085
1268
|
* row's values. Shared by `onDelete` cascade (`applyReferentialActionsForRow`)
|
|
1086
1269
|
* and `onUpdate` cascade (`applyReferentialActionsForRowOnUpdate`).
|
|
@@ -1090,7 +1273,7 @@ function buildChildFilterFromRow(def, row) {
|
|
|
1090
1273
|
childField: def.targetFields[i],
|
|
1091
1274
|
parentValue: row[lf]
|
|
1092
1275
|
}));
|
|
1093
|
-
return (child) => pairs.every(({ childField, parentValue }) => child[childField]
|
|
1276
|
+
return (child) => pairs.every(({ childField, parentValue }) => fieldValuesEqual(child[childField], parentValue));
|
|
1094
1277
|
}
|
|
1095
1278
|
/** Reads a field's literal `@default(...)` value from `IdbModelStorage.fieldDefaults`, if declared. */
|
|
1096
1279
|
function getFieldDefault(contract, modelName, fieldName) {
|
|
@@ -1127,10 +1310,8 @@ function buildSetDefaultPatch(contract, def) {
|
|
|
1127
1310
|
* part of the transaction (it's the model literally being written), so no
|
|
1128
1311
|
* store-list changes are needed to call this.
|
|
1129
1312
|
*
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1132
|
-
* field's default is resolved independently, so there's no guarantee the
|
|
1133
|
-
* combination corresponds to a single real row.
|
|
1313
|
+
* A compound relation is checked as one tuple: a single parent row must
|
|
1314
|
+
* match every field's default.
|
|
1134
1315
|
*
|
|
1135
1316
|
* `excludeKey`, when given, excludes the parent row currently being
|
|
1136
1317
|
* deleted/updated from the existence scan. That row is still physically
|
|
@@ -1142,21 +1323,9 @@ function buildSetDefaultPatch(contract, def) {
|
|
|
1142
1323
|
* default references a real row, when after the transaction nothing will.
|
|
1143
1324
|
*/
|
|
1144
1325
|
async function validateSetDefaultPatch(scope, contract, modelName, def, patch, excludeKey) {
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
const parentKeyPath = getKeyPath(contract, modelName);
|
|
1149
|
-
const localField = def.localFields[0];
|
|
1150
|
-
const targetField = def.targetFields[0];
|
|
1151
|
-
const value = patch[targetField];
|
|
1152
|
-
const filter = (row) => row[localField] === value && (excludeKey === void 0 || row[parentKeyPath] !== excludeKey);
|
|
1153
|
-
if ((await scope.execute({
|
|
1154
|
-
meta,
|
|
1155
|
-
kind: "cursor-scan",
|
|
1156
|
-
storeName: parentStoreName,
|
|
1157
|
-
filter,
|
|
1158
|
-
take: 1
|
|
1159
|
-
})).length === 0) throw new Error(`setDefault referential action on relation '${def.relationName}' would set '${def.relatedModelName}.${targetField}' to '${String(value)}', but no ${modelName} with ${localField}='${String(value)}' exists — the declared default does not reference a real row.`);
|
|
1326
|
+
const values = def.targetFields.map((f) => patch[f]);
|
|
1327
|
+
if (values.some((v) => v === null || v === void 0)) return;
|
|
1328
|
+
if (!await parentExists(scope, contract, makePlanMeta(contract), modelName, def.localFields, values, excludeKey)) throw new Error(`setDefault referential action on relation '${def.relationName}' would set ${def.relatedModelName} to ${describeTuple(def.targetFields, values)}, but no ${modelName} with ${describeTuple(def.localFields, values)} exists — the declared default does not reference a real row.`);
|
|
1160
1329
|
}
|
|
1161
1330
|
/**
|
|
1162
1331
|
* Collects every store an `onUpdate`-enforced write to `modelName` might
|
|
@@ -1186,7 +1355,6 @@ function collectOnUpdateEnforcementStoreNames(contract, modelName, data) {
|
|
|
1186
1355
|
for (const def of getRelationDefinitions(contract, mName)) {
|
|
1187
1356
|
if (!isDeleteEnforcementRelation(contract, mName, def)) continue;
|
|
1188
1357
|
const action = getOnUpdateForRelation(contract, mName, def);
|
|
1189
|
-
if (action === "noAction") continue;
|
|
1190
1358
|
stores.add(def.relatedStoreName);
|
|
1191
1359
|
if (action === "cascade") walkCascadeChain(def.relatedModelName);
|
|
1192
1360
|
}
|
|
@@ -1196,7 +1364,6 @@ function collectOnUpdateEnforcementStoreNames(contract, modelName, data) {
|
|
|
1196
1364
|
if (!isDeleteEnforcementRelation(contract, modelName, def)) continue;
|
|
1197
1365
|
if (!def.localFields.some((f) => f in data)) continue;
|
|
1198
1366
|
const action = getOnUpdateForRelation(contract, modelName, def);
|
|
1199
|
-
if (action === "noAction") continue;
|
|
1200
1367
|
enforces = true;
|
|
1201
1368
|
stores.add(def.relatedStoreName);
|
|
1202
1369
|
if (action === "cascade") walkCascadeChain(def.relatedModelName);
|
|
@@ -1231,25 +1398,18 @@ function collectOnUpdateEnforcementStoreNames(contract, modelName, data) {
|
|
|
1231
1398
|
*/
|
|
1232
1399
|
async function applyReferentialActionsForRowOnUpdate(scope, contract, modelName, oldRow, patch, visited = /* @__PURE__ */ new Set()) {
|
|
1233
1400
|
const keyPath = getKeyPath(contract, modelName);
|
|
1234
|
-
const rowKey = `${getStoreName(contract, modelName)}::${
|
|
1401
|
+
const rowKey = `${getStoreName(contract, modelName)}::${keyToken(extractKeyFromRow(oldRow, keyPath))}`;
|
|
1235
1402
|
if (visited.has(rowKey)) return;
|
|
1236
1403
|
visited.add(rowKey);
|
|
1237
1404
|
const meta = makePlanMeta(contract);
|
|
1238
1405
|
for (const def of getRelationDefinitions(contract, modelName)) {
|
|
1239
1406
|
if (!isDeleteEnforcementRelation(contract, modelName, def)) continue;
|
|
1240
|
-
const changedFields = def.localFields.filter((f) => f in patch && patch[f]
|
|
1407
|
+
const changedFields = def.localFields.filter((f) => f in patch && !fieldValuesEqual(patch[f], oldRow[f]));
|
|
1241
1408
|
if (changedFields.length === 0) continue;
|
|
1242
1409
|
const action = getOnUpdateForRelation(contract, modelName, def);
|
|
1243
|
-
if (action === "noAction") continue;
|
|
1244
1410
|
const childFilter = buildChildFilterFromRow(def, oldRow);
|
|
1245
1411
|
if (action === "restrict") {
|
|
1246
|
-
if (
|
|
1247
|
-
meta,
|
|
1248
|
-
kind: "cursor-scan",
|
|
1249
|
-
storeName: def.relatedStoreName,
|
|
1250
|
-
filter: childFilter,
|
|
1251
|
-
take: 1
|
|
1252
|
-
})).length > 0) throw new Error(`Cannot update ${modelName} '${String(oldRow[keyPath])}': changing field(s) ${changedFields.join(", ")} would orphan child records on relation '${def.relationName}'. Use onUpdate: 'cascade', 'setNull', 'setDefault', or 'noAction'.`);
|
|
1412
|
+
if (await childExists(scope, contract, meta, def, oldRow)) throw new Error(`Cannot update ${modelName} '${keyToken(extractKeyFromRow(oldRow, keyPath))}': changing field(s) ${changedFields.join(", ")} would orphan child records on relation '${def.relationName}'. Update or remove those children first, or declare onUpdate: Cascade, SetNull or SetDefault on the relation.`);
|
|
1253
1413
|
continue;
|
|
1254
1414
|
}
|
|
1255
1415
|
if (action === "cascade") {
|
|
@@ -1291,7 +1451,7 @@ async function applyReferentialActionsForRowOnUpdate(scope, contract, modelName,
|
|
|
1291
1451
|
}
|
|
1292
1452
|
if (action === "setDefault") {
|
|
1293
1453
|
const childPatch = buildSetDefaultPatch(contract, def);
|
|
1294
|
-
await validateSetDefaultPatch(scope, contract, modelName, def, childPatch, oldRow
|
|
1454
|
+
await validateSetDefaultPatch(scope, contract, modelName, def, childPatch, extractKeyFromRow(oldRow, keyPath));
|
|
1295
1455
|
await scope.execute({
|
|
1296
1456
|
meta,
|
|
1297
1457
|
kind: "scan-write",
|
|
@@ -1323,31 +1483,102 @@ function collectScalarFkStoreNames(contract, modelName, data) {
|
|
|
1323
1483
|
}
|
|
1324
1484
|
return [...stores];
|
|
1325
1485
|
}
|
|
1326
|
-
|
|
1486
|
+
/**
|
|
1487
|
+
* Checks that every foreign key `data` sets points at an existing parent row,
|
|
1488
|
+
* inside the write's transaction.
|
|
1489
|
+
*
|
|
1490
|
+
* A compound foreign key is checked as one tuple: a single parent row must
|
|
1491
|
+
* match every field. Checking the fields one at a time could pass with each
|
|
1492
|
+
* value taken from a different parent. When `data` sets only some fields of a
|
|
1493
|
+
* compound key, the rest come from `existingRow`, the row being updated.
|
|
1494
|
+
* Callers must pass it in that case; see {@link fkCheckNeedsExistingRow}.
|
|
1495
|
+
*
|
|
1496
|
+
* A key with any `null` field isn't checked, like SQL's default `MATCH SIMPLE`.
|
|
1497
|
+
*/
|
|
1498
|
+
async function validateScalarFks(scope, contract, modelName, data, existingRow) {
|
|
1327
1499
|
const meta = makePlanMeta(contract);
|
|
1328
1500
|
for (const def of getRelationDefinitions(contract, modelName)) {
|
|
1329
1501
|
if (def.cardinality !== "N:1") continue;
|
|
1330
|
-
if (!def.localFields.some((f) => f in data
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
const value = data[localField];
|
|
1335
|
-
const filter = (row) => row[targetField] === value;
|
|
1336
|
-
const plan = {
|
|
1337
|
-
meta,
|
|
1338
|
-
kind: "cursor-scan",
|
|
1339
|
-
storeName: def.relatedStoreName,
|
|
1340
|
-
filter,
|
|
1341
|
-
take: 1
|
|
1342
|
-
};
|
|
1343
|
-
if ((await scope.execute(plan)).length === 0) throw new Error(`FK violation on relation '${def.relationName}': no ${def.relatedModelName} with ${targetField}='${String(value)}'`);
|
|
1502
|
+
if (!def.localFields.some((f) => f in data)) continue;
|
|
1503
|
+
const values = def.localFields.map((f) => f in data ? data[f] : existingRow?.[f]);
|
|
1504
|
+
if (values.some((v) => v === null || v === void 0)) continue;
|
|
1505
|
+
if (!await parentExists(scope, contract, meta, def.relatedModelName, def.targetFields, values)) throw new Error(`FK violation on relation '${def.relationName}': no ${def.relatedModelName} with ${describeTuple(def.targetFields, values)}`);
|
|
1344
1506
|
}
|
|
1345
1507
|
}
|
|
1508
|
+
/**
|
|
1509
|
+
* `true` when `data` sets some, but not all, fields of a compound foreign
|
|
1510
|
+
* key. Checking it then needs the rest of the key from the row being
|
|
1511
|
+
* updated, so the update has to read each row before writing it.
|
|
1512
|
+
*/
|
|
1513
|
+
function fkCheckNeedsExistingRow(contract, modelName, data) {
|
|
1514
|
+
return getRelationDefinitions(contract, modelName).some((def) => {
|
|
1515
|
+
if (def.cardinality !== "N:1") return false;
|
|
1516
|
+
const set = def.localFields.filter((f) => f in data).length;
|
|
1517
|
+
return set > 0 && set < def.localFields.length;
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Does `parentModel` have a row whose `fields` equal `values`, pairwise?
|
|
1522
|
+
*
|
|
1523
|
+
* When `fields` are exactly the parent's primary key (in any order), this is
|
|
1524
|
+
* one key-only lookup. Otherwise it scans the parent store, comparing values
|
|
1525
|
+
* the way IndexedDB compares keys. `excludeKey` leaves out one parent row,
|
|
1526
|
+
* for checks that run while that row is being deleted or changed.
|
|
1527
|
+
*/
|
|
1528
|
+
async function parentExists(scope, contract, meta, parentModel, fields, values, excludeKey) {
|
|
1529
|
+
const storeName = getStoreName(contract, parentModel);
|
|
1530
|
+
const keyPath = getKeyPath(contract, parentModel);
|
|
1531
|
+
const range = primaryKeyRange(keyPath, fields, values);
|
|
1532
|
+
if (range !== null) {
|
|
1533
|
+
const foundKey = await firstKeyInRange(scope, meta, storeName, range);
|
|
1534
|
+
return foundKey !== void 0 && (excludeKey === void 0 || !keyEquals(foundKey, excludeKey));
|
|
1535
|
+
}
|
|
1536
|
+
const filter = (row) => fields.every((f, i) => fieldValuesEqual(row[f], values[i])) && (excludeKey === void 0 || !keyEquals(extractKeyFromRow(row, keyPath), excludeKey));
|
|
1537
|
+
return (await scope.execute({
|
|
1538
|
+
meta,
|
|
1539
|
+
kind: "cursor-scan",
|
|
1540
|
+
storeName,
|
|
1541
|
+
filter,
|
|
1542
|
+
take: 1
|
|
1543
|
+
})).length > 0;
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* An `IDBKeyRange` for "the row whose primary key is `values`", when `fields`
|
|
1547
|
+
* are exactly the key's fields (in any order) and every value is a valid
|
|
1548
|
+
* IndexedDB key. `null` otherwise, and the caller scans instead.
|
|
1549
|
+
*/
|
|
1550
|
+
function primaryKeyRange(keyPath, fields, values) {
|
|
1551
|
+
if (typeof IDBKeyRange === "undefined") return null;
|
|
1552
|
+
const keyFields = keyPathFields(keyPath);
|
|
1553
|
+
if (keyFields.length !== fields.length || !keyFields.every((f) => fields.includes(f))) return null;
|
|
1554
|
+
const ordered = keyFields.map((f) => values[fields.indexOf(f)]);
|
|
1555
|
+
if (!ordered.every((v) => isValidIdbKey(v))) return null;
|
|
1556
|
+
return IDBKeyRange.only(typeof keyPath === "string" ? ordered[0] : ordered);
|
|
1557
|
+
}
|
|
1558
|
+
/** `id='u1'`, or `orgId='a', id='u1'` for a compound key. */
|
|
1559
|
+
function describeTuple(fields, values) {
|
|
1560
|
+
return fields.map((f, i) => `${f}='${String(values[i])}'`).join(", ");
|
|
1561
|
+
}
|
|
1346
1562
|
async function executeScalarCreateWithFkValidation(options) {
|
|
1347
1563
|
const { executor, contract, modelName, data } = options;
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1564
|
+
const defaultsCache = createMutationDefaultsCache();
|
|
1565
|
+
const record = applyCreateDefaults(contract.execution?.mutations.defaults, getStoreName(contract, modelName), data, defaultsCache);
|
|
1566
|
+
return withMutationScope(executor, collectScalarFkStoreNames(contract, modelName, record), (scope) => insertSingleRow(scope, contract, modelName, record, defaultsCache));
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* `createAll()` for rows that set foreign keys: checks every row's foreign
|
|
1570
|
+
* keys and inserts it, all in one transaction, so one bad row writes nothing.
|
|
1571
|
+
* The batch shares one defaults cache, like the plain `createAll()` path.
|
|
1572
|
+
*/
|
|
1573
|
+
async function executeScalarCreateAllWithFkValidation(options) {
|
|
1574
|
+
const { executor, contract, modelName, data } = options;
|
|
1575
|
+
const defaultsCache = createMutationDefaultsCache();
|
|
1576
|
+
const storeName = getStoreName(contract, modelName);
|
|
1577
|
+
const records = data.map((row) => applyCreateDefaults(contract.execution?.mutations.defaults, storeName, row, defaultsCache));
|
|
1578
|
+
return withMutationScope(executor, [...new Set(records.flatMap((row) => collectScalarFkStoreNames(contract, modelName, row)))], async (scope) => {
|
|
1579
|
+
const inserted = [];
|
|
1580
|
+
for (const record of records) inserted.push(await insertSingleRow(scope, contract, modelName, record, defaultsCache));
|
|
1581
|
+
return inserted;
|
|
1351
1582
|
});
|
|
1352
1583
|
}
|
|
1353
1584
|
/**
|
|
@@ -1368,18 +1599,19 @@ function collectUpdateStoreNames(contract, modelName, data) {
|
|
|
1368
1599
|
}
|
|
1369
1600
|
async function executeScalarUpdateWithFkValidation(options) {
|
|
1370
1601
|
const { executor, contract, modelName, filters, data } = options;
|
|
1371
|
-
const
|
|
1602
|
+
const storeName = getStoreName(contract, modelName);
|
|
1603
|
+
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, storeName, data, createMutationDefaultsCache());
|
|
1604
|
+
const { storeNames, enforcesOnUpdate } = collectUpdateStoreNames(contract, modelName, patch);
|
|
1605
|
+
const needsRowForFks = fkCheckNeedsExistingRow(contract, modelName, patch);
|
|
1372
1606
|
return withMutationScope(executor, storeNames, async (scope) => {
|
|
1373
|
-
await validateScalarFks(scope, contract, modelName,
|
|
1374
|
-
const storeName = getStoreName(contract, modelName);
|
|
1607
|
+
if (!needsRowForFks) await validateScalarFks(scope, contract, modelName, patch);
|
|
1375
1608
|
const meta = makePlanMeta(contract);
|
|
1376
1609
|
const combined = filters.length === 0 ? void 0 : filters.length === 1 ? filters[0] : {
|
|
1377
1610
|
kind: "and",
|
|
1378
1611
|
exprs: filters
|
|
1379
1612
|
};
|
|
1380
1613
|
const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
|
|
1381
|
-
|
|
1382
|
-
if (!enforcesOnUpdate) return (await scope.execute({
|
|
1614
|
+
if (!enforcesOnUpdate && !needsRowForFks) return (await scope.execute({
|
|
1383
1615
|
meta,
|
|
1384
1616
|
kind: "scan-write",
|
|
1385
1617
|
storeName,
|
|
@@ -1396,8 +1628,9 @@ async function executeScalarUpdateWithFkValidation(options) {
|
|
|
1396
1628
|
...filter !== void 0 ? { filter } : {}
|
|
1397
1629
|
}))[0];
|
|
1398
1630
|
if (!oldRow) return null;
|
|
1399
|
-
await
|
|
1400
|
-
|
|
1631
|
+
if (needsRowForFks) await validateScalarFks(scope, contract, modelName, patch, oldRow);
|
|
1632
|
+
if (enforcesOnUpdate) await applyReferentialActionsForRowOnUpdate(scope, contract, modelName, oldRow, patch);
|
|
1633
|
+
const key = extractKeyFromRow(oldRow, getKeyPath(contract, modelName));
|
|
1401
1634
|
return (await scope.execute({
|
|
1402
1635
|
meta,
|
|
1403
1636
|
kind: "update",
|
|
@@ -1426,18 +1659,19 @@ async function executeScalarUpdateWithFkValidation(options) {
|
|
|
1426
1659
|
*/
|
|
1427
1660
|
async function executeBulkUpdateWithFkValidation(options) {
|
|
1428
1661
|
const { executor, contract, modelName, filters, data } = options;
|
|
1429
|
-
const
|
|
1662
|
+
const storeName = getStoreName(contract, modelName);
|
|
1663
|
+
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, storeName, data, createMutationDefaultsCache());
|
|
1664
|
+
const { storeNames, enforcesOnUpdate } = collectUpdateStoreNames(contract, modelName, patch);
|
|
1665
|
+
const needsRowForFks = fkCheckNeedsExistingRow(contract, modelName, patch);
|
|
1430
1666
|
return withMutationScope(executor, storeNames, async (scope) => {
|
|
1431
|
-
await validateScalarFks(scope, contract, modelName,
|
|
1432
|
-
const storeName = getStoreName(contract, modelName);
|
|
1667
|
+
if (!needsRowForFks) await validateScalarFks(scope, contract, modelName, patch);
|
|
1433
1668
|
const meta = makePlanMeta(contract);
|
|
1434
1669
|
const combined = filters.length === 0 ? void 0 : filters.length === 1 ? filters[0] : {
|
|
1435
1670
|
kind: "and",
|
|
1436
1671
|
exprs: filters
|
|
1437
1672
|
};
|
|
1438
1673
|
const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
|
|
1439
|
-
|
|
1440
|
-
if (!enforcesOnUpdate) return scope.execute({
|
|
1674
|
+
if (!enforcesOnUpdate && !needsRowForFks) return scope.execute({
|
|
1441
1675
|
meta,
|
|
1442
1676
|
kind: "scan-write",
|
|
1443
1677
|
storeName,
|
|
@@ -1454,8 +1688,9 @@ async function executeBulkUpdateWithFkValidation(options) {
|
|
|
1454
1688
|
const keyPath = getKeyPath(contract, modelName);
|
|
1455
1689
|
const results = [];
|
|
1456
1690
|
for (const oldRow of oldRows) {
|
|
1457
|
-
await
|
|
1458
|
-
|
|
1691
|
+
if (needsRowForFks) await validateScalarFks(scope, contract, modelName, patch, oldRow);
|
|
1692
|
+
if (enforcesOnUpdate) await applyReferentialActionsForRowOnUpdate(scope, contract, modelName, oldRow, patch);
|
|
1693
|
+
const key = extractKeyFromRow(oldRow, keyPath);
|
|
1459
1694
|
const updated = (await scope.execute({
|
|
1460
1695
|
meta,
|
|
1461
1696
|
kind: "update",
|
|
@@ -1470,16 +1705,11 @@ async function executeBulkUpdateWithFkValidation(options) {
|
|
|
1470
1705
|
}
|
|
1471
1706
|
/**
|
|
1472
1707
|
* Returns true if the model has at least one child relation (1:N or parent-side
|
|
1473
|
-
* 1:1)
|
|
1474
|
-
*
|
|
1475
|
-
* explicitly set `noAction` returns true.
|
|
1708
|
+
* 1:1). Every such relation has an `onDelete` action to enforce: `noAction`
|
|
1709
|
+
* behaves like `restrict`, which is also the default.
|
|
1476
1710
|
*/
|
|
1477
1711
|
function hasEnforceableChildRelations(contract, modelName) {
|
|
1478
|
-
|
|
1479
|
-
if (!isDeleteEnforcementRelation(contract, modelName, def)) continue;
|
|
1480
|
-
if (getOnDeleteForDeleteRelation(contract, modelName, def) !== "noAction") return true;
|
|
1481
|
-
}
|
|
1482
|
-
return false;
|
|
1712
|
+
return getRelationDefinitions(contract, modelName).some((def) => isDeleteEnforcementRelation(contract, modelName, def));
|
|
1483
1713
|
}
|
|
1484
1714
|
/**
|
|
1485
1715
|
* Transitively walks the `onDelete` cascade graph from `modelName`, collecting
|
|
@@ -1502,7 +1732,6 @@ function collectDeleteStoreNames(contract, modelName) {
|
|
|
1502
1732
|
for (const def of getRelationDefinitions(contract, mName)) {
|
|
1503
1733
|
if (!isDeleteEnforcementRelation(contract, mName, def)) continue;
|
|
1504
1734
|
const action = getOnDeleteForDeleteRelation(contract, mName, def);
|
|
1505
|
-
if (action === "noAction") continue;
|
|
1506
1735
|
stores.add(def.relatedStoreName);
|
|
1507
1736
|
if (action === "cascade") walk(def.relatedModelName);
|
|
1508
1737
|
}
|
|
@@ -1530,23 +1759,16 @@ function collectDeleteStoreNames(contract, modelName) {
|
|
|
1530
1759
|
*/
|
|
1531
1760
|
async function applyReferentialActionsForRow(scope, contract, modelName, row, visited = /* @__PURE__ */ new Set()) {
|
|
1532
1761
|
const keyPath = getKeyPath(contract, modelName);
|
|
1533
|
-
const rowKey = `${getStoreName(contract, modelName)}::${
|
|
1762
|
+
const rowKey = `${getStoreName(contract, modelName)}::${keyToken(extractKeyFromRow(row, keyPath))}`;
|
|
1534
1763
|
if (visited.has(rowKey)) return;
|
|
1535
1764
|
visited.add(rowKey);
|
|
1536
1765
|
const meta = makePlanMeta(contract);
|
|
1537
1766
|
for (const def of getRelationDefinitions(contract, modelName)) {
|
|
1538
1767
|
if (!isDeleteEnforcementRelation(contract, modelName, def)) continue;
|
|
1539
1768
|
const action = getOnDeleteForDeleteRelation(contract, modelName, def);
|
|
1540
|
-
if (action === "noAction") continue;
|
|
1541
1769
|
const childFilter = buildChildFilterFromRow(def, row);
|
|
1542
1770
|
if (action === "restrict") {
|
|
1543
|
-
if (
|
|
1544
|
-
meta,
|
|
1545
|
-
kind: "cursor-scan",
|
|
1546
|
-
storeName: def.relatedStoreName,
|
|
1547
|
-
filter: childFilter,
|
|
1548
|
-
take: 1
|
|
1549
|
-
})).length > 0) throw new Error(`Cannot delete ${modelName} '${String(row[keyPath])}': child records exist on relation '${def.relationName}'. Use onDelete: 'cascade', 'setNull', or 'noAction'.`);
|
|
1771
|
+
if (await childExists(scope, contract, meta, def, row)) throw new Error(`Cannot delete ${modelName} '${keyToken(extractKeyFromRow(row, keyPath))}': child records exist on relation '${def.relationName}'. Delete those children first, or declare onDelete: Cascade, SetNull or SetDefault on the relation.`);
|
|
1550
1772
|
continue;
|
|
1551
1773
|
}
|
|
1552
1774
|
if (action === "cascade") {
|
|
@@ -1563,7 +1785,7 @@ async function applyReferentialActionsForRow(scope, contract, modelName, row, vi
|
|
|
1563
1785
|
meta,
|
|
1564
1786
|
kind: "delete",
|
|
1565
1787
|
storeName: def.relatedStoreName,
|
|
1566
|
-
key: child
|
|
1788
|
+
key: extractKeyFromRow(child, childKeyPath)
|
|
1567
1789
|
});
|
|
1568
1790
|
}
|
|
1569
1791
|
continue;
|
|
@@ -1583,7 +1805,7 @@ async function applyReferentialActionsForRow(scope, contract, modelName, row, vi
|
|
|
1583
1805
|
}
|
|
1584
1806
|
if (action === "setDefault") {
|
|
1585
1807
|
const patch = buildSetDefaultPatch(contract, def);
|
|
1586
|
-
await validateSetDefaultPatch(scope, contract, modelName, def, patch, row
|
|
1808
|
+
await validateSetDefaultPatch(scope, contract, modelName, def, patch, extractKeyFromRow(row, keyPath));
|
|
1587
1809
|
await scope.execute({
|
|
1588
1810
|
meta,
|
|
1589
1811
|
kind: "scan-write",
|
|
@@ -1632,7 +1854,7 @@ async function executeDeleteAllWithReferentialActions(options) {
|
|
|
1632
1854
|
});
|
|
1633
1855
|
for (const row of rows) {
|
|
1634
1856
|
await applyReferentialActionsForRow(scope, contract, modelName, row);
|
|
1635
|
-
const key = row
|
|
1857
|
+
const key = extractKeyFromRow(row, keyPath);
|
|
1636
1858
|
await scope.execute({
|
|
1637
1859
|
meta,
|
|
1638
1860
|
kind: "delete",
|
|
@@ -1709,7 +1931,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1709
1931
|
const projectRows = this.#projectRows.bind(this);
|
|
1710
1932
|
const combined = this.#combinedFilterExpr();
|
|
1711
1933
|
const fieldToIndexMap = typeof IDBKeyRange !== "undefined" ? buildFieldToIndexMap(this.#contract, this.#storeName) : void 0;
|
|
1712
|
-
const keyPath = getKeyPath
|
|
1934
|
+
const keyPath = getKeyPath(this.#contract, this.#modelName);
|
|
1713
1935
|
const comparator = buildRowComparator(this.#state.orderBy);
|
|
1714
1936
|
const skip = this.#state.skip;
|
|
1715
1937
|
const take = this.#state.take;
|
|
@@ -1743,7 +1965,17 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1743
1965
|
aggregates: toAggregateRequests(spec),
|
|
1744
1966
|
...combined !== void 0 ? { where: combined } : {}
|
|
1745
1967
|
};
|
|
1746
|
-
|
|
1968
|
+
const groupingKey = this.#newGroupingKey();
|
|
1969
|
+
if (Object.values(spec).every((selector) => selector.fn === "count")) {
|
|
1970
|
+
const scanPlan = this.#buildScanPlan(groupingKey);
|
|
1971
|
+
const total = await this.#executeNativeCount(scanPlan, ast);
|
|
1972
|
+
if (total !== null) {
|
|
1973
|
+
const result = {};
|
|
1974
|
+
for (const alias of Object.keys(spec)) result[alias] = total;
|
|
1975
|
+
return result;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
return computeAggregateSpec(spec, await this.#materialize(groupingKey, ast));
|
|
1747
1979
|
}
|
|
1748
1980
|
groupBy(...fields) {
|
|
1749
1981
|
const combined = this.#combinedFilterExpr();
|
|
@@ -1763,15 +1995,15 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1763
1995
|
modelName: this.#modelName,
|
|
1764
1996
|
data: record
|
|
1765
1997
|
});
|
|
1766
|
-
|
|
1998
|
+
const withDefaults = applyCreateDefaults(this.#contract.execution?.mutations.defaults, this.#storeName, record, createMutationDefaultsCache());
|
|
1999
|
+
if (hasScalarFkFields(this.#contract, this.#modelName, withDefaults)) return await executeScalarCreateWithFkValidation({
|
|
1767
2000
|
executor: requireTransactionExecutor(this.#executor),
|
|
1768
2001
|
contract: this.#contract,
|
|
1769
2002
|
modelName: this.#modelName,
|
|
1770
|
-
data:
|
|
2003
|
+
data: withDefaults
|
|
1771
2004
|
});
|
|
1772
2005
|
const groupingKey = this.#newGroupingKey();
|
|
1773
2006
|
const meta = this.#planMeta(groupingKey);
|
|
1774
|
-
const withDefaults = applyCreateDefaults(this.#contract.execution?.mutations.defaults, this.#storeName, record, createMutationDefaultsCache());
|
|
1775
2007
|
const plan = {
|
|
1776
2008
|
meta,
|
|
1777
2009
|
ast: {
|
|
@@ -1885,7 +2117,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1885
2117
|
return (await this.updateAll(patch).toArray()).length;
|
|
1886
2118
|
}
|
|
1887
2119
|
async upsert(args) {
|
|
1888
|
-
const keyPath = getKeyPath
|
|
2120
|
+
const keyPath = getKeyPath(this.#contract, this.#modelName);
|
|
1889
2121
|
const whereExpr = shorthandToFilterExpr(args.where);
|
|
1890
2122
|
const matches = (row) => whereExpr === void 0 || evaluateFilter(whereExpr, row);
|
|
1891
2123
|
const meta = this.#planMeta(this.#newGroupingKey());
|
|
@@ -1895,8 +2127,14 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1895
2127
|
const executionDefaults = this.#contract.execution?.mutations.defaults;
|
|
1896
2128
|
const exec = requireTransactionExecutor(this.#executor);
|
|
1897
2129
|
const effectivePatch = applyUpdateDefaults(executionDefaults, storeName, patchRecord, createMutationDefaultsCache());
|
|
2130
|
+
const createWithDefaults = applyCreateDefaults(executionDefaults, storeName, createRecord, createMutationDefaultsCache());
|
|
1898
2131
|
const { storeNames: onUpdateStoreNames } = collectOnUpdateEnforcementStoreNames(this.#contract, this.#modelName, effectivePatch);
|
|
1899
|
-
return withMutationScope(exec, [.../* @__PURE__ */ new Set([
|
|
2132
|
+
return withMutationScope(exec, [.../* @__PURE__ */ new Set([
|
|
2133
|
+
storeName,
|
|
2134
|
+
...onUpdateStoreNames,
|
|
2135
|
+
...collectScalarFkStoreNames(this.#contract, this.#modelName, createWithDefaults),
|
|
2136
|
+
...collectScalarFkStoreNames(this.#contract, this.#modelName, effectivePatch)
|
|
2137
|
+
])], async (scope) => {
|
|
1900
2138
|
const existing = (await scope.execute({
|
|
1901
2139
|
meta,
|
|
1902
2140
|
kind: "cursor-scan",
|
|
@@ -1905,15 +2143,16 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1905
2143
|
take: 1
|
|
1906
2144
|
}))[0];
|
|
1907
2145
|
if (existing === void 0) {
|
|
1908
|
-
|
|
2146
|
+
await validateScalarFks(scope, this.#contract, this.#modelName, createWithDefaults);
|
|
1909
2147
|
return (await scope.execute({
|
|
1910
2148
|
meta,
|
|
1911
2149
|
kind: "add",
|
|
1912
2150
|
storeName,
|
|
1913
|
-
record
|
|
1914
|
-
}))[0] ??
|
|
2151
|
+
record: createWithDefaults
|
|
2152
|
+
}))[0] ?? createWithDefaults;
|
|
1915
2153
|
}
|
|
1916
|
-
|
|
2154
|
+
await validateScalarFks(scope, this.#contract, this.#modelName, effectivePatch, existing);
|
|
2155
|
+
const key = extractKeyFromRow(existing, keyPath);
|
|
1917
2156
|
await applyReferentialActionsForRowOnUpdate(scope, this.#contract, this.#modelName, existing, effectivePatch);
|
|
1918
2157
|
return (await scope.execute({
|
|
1919
2158
|
meta,
|
|
@@ -1925,11 +2164,25 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1925
2164
|
});
|
|
1926
2165
|
}
|
|
1927
2166
|
createAll(data) {
|
|
1928
|
-
const groupingKey = this.#newGroupingKey();
|
|
1929
|
-
const meta = this.#planMeta(groupingKey);
|
|
1930
2167
|
const defaultsCache = createMutationDefaultsCache();
|
|
1931
2168
|
const executionDefaults = this.#contract.execution?.mutations.defaults;
|
|
1932
2169
|
const records = data.map((d) => applyCreateDefaults(executionDefaults, this.#storeName, d, defaultsCache));
|
|
2170
|
+
if (records.some((row) => hasScalarFkFields(this.#contract, this.#modelName, row))) {
|
|
2171
|
+
const executor = requireTransactionExecutor(this.#executor);
|
|
2172
|
+
const contract = this.#contract;
|
|
2173
|
+
const modelName = this.#modelName;
|
|
2174
|
+
return new AsyncIterableResult((async function* () {
|
|
2175
|
+
const rows = await executeScalarCreateAllWithFkValidation({
|
|
2176
|
+
executor,
|
|
2177
|
+
contract,
|
|
2178
|
+
modelName,
|
|
2179
|
+
data: records
|
|
2180
|
+
});
|
|
2181
|
+
for (const row of rows) yield row;
|
|
2182
|
+
})());
|
|
2183
|
+
}
|
|
2184
|
+
const groupingKey = this.#newGroupingKey();
|
|
2185
|
+
const meta = this.#planMeta(groupingKey);
|
|
1933
2186
|
const plan = {
|
|
1934
2187
|
meta,
|
|
1935
2188
|
ast: {
|
|
@@ -1995,13 +2248,8 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1995
2248
|
const combined = this.#combinedFilterExpr();
|
|
1996
2249
|
const fieldToIndexMap = typeof IDBKeyRange !== "undefined" ? buildFieldToIndexMap(this.#contract, this.#storeName) : void 0;
|
|
1997
2250
|
if (fieldToIndexMap !== void 0) {
|
|
1998
|
-
const orHint = extractIndexOrHint(combined, fieldToIndexMap, getKeyPath
|
|
1999
|
-
if (orHint !== null)
|
|
2000
|
-
let n = (await this.#executeOrRows(orHint, groupingKey, combined)).length;
|
|
2001
|
-
if (this.#state.skip !== void 0) n = Math.max(0, n - this.#state.skip);
|
|
2002
|
-
if (this.#state.take !== void 0) n = Math.min(this.#state.take, n);
|
|
2003
|
-
return n;
|
|
2004
|
-
}
|
|
2251
|
+
const orHint = extractIndexOrHint(combined, fieldToIndexMap, getKeyPath(this.#contract, this.#modelName));
|
|
2252
|
+
if (orHint !== null) return clampCount((await this.#executeOrRows(orHint, groupingKey, combined)).length, this.#state.skip, this.#state.take);
|
|
2005
2253
|
}
|
|
2006
2254
|
const scanPlan = this.#buildScanPlan(groupingKey, fieldToIndexMap);
|
|
2007
2255
|
const scanAst = scanPlan.ast;
|
|
@@ -2010,6 +2258,8 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
2010
2258
|
modelName: this.#modelName,
|
|
2011
2259
|
...scanAst?.kind === "findMany" && scanAst.where !== void 0 ? { where: scanAst.where } : {}
|
|
2012
2260
|
};
|
|
2261
|
+
const nativeTotal = await this.#executeNativeCount(scanPlan, ast);
|
|
2262
|
+
if (nativeTotal !== null) return clampCount(nativeTotal, this.#state.skip, this.#state.take);
|
|
2013
2263
|
const plan = {
|
|
2014
2264
|
...scanPlan,
|
|
2015
2265
|
ast
|
|
@@ -2046,6 +2296,25 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
2046
2296
|
throw new Error(`include('${relation}') refinement must return the collection (for where/orderBy/take/skip) or a count() selector`);
|
|
2047
2297
|
}
|
|
2048
2298
|
/**
|
|
2299
|
+
* Runs `scanPlan` as a native `count` plan when its cardinality is fully
|
|
2300
|
+
* determined by a key range (no in-memory filter) — returning the
|
|
2301
|
+
* *unpaginated* total — or `null` when it can't (the caller then falls back
|
|
2302
|
+
* to materializing). `ast` is attached so middleware sees the caller's
|
|
2303
|
+
* intent (`count` / `aggregate`) regardless of the physical plan.
|
|
2304
|
+
*/
|
|
2305
|
+
async #executeNativeCount(scanPlan, ast) {
|
|
2306
|
+
const body = scanPlan.idbPlan;
|
|
2307
|
+
if (body.kind !== "cursor-scan" || !isNativelyCountable(body)) return null;
|
|
2308
|
+
const nativePlan = {
|
|
2309
|
+
...scanPlan,
|
|
2310
|
+
ast,
|
|
2311
|
+
idbPlan: toCountPlan(body)
|
|
2312
|
+
};
|
|
2313
|
+
let total = 0;
|
|
2314
|
+
for await (const row of this.#executor.query(nativePlan)) total = row.count;
|
|
2315
|
+
return total;
|
|
2316
|
+
}
|
|
2317
|
+
/**
|
|
2049
2318
|
* Materialise all rows matching the accumulated filters with no pagination —
|
|
2050
2319
|
* used by `aggregate()` / `groupBy()`. The supplied `ast` is attached to the
|
|
2051
2320
|
* scan plan so middleware can observe the aggregate intent.
|
|
@@ -2081,7 +2350,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
2081
2350
|
...this.#state.take !== void 0 ? { take: this.#state.take } : {}
|
|
2082
2351
|
};
|
|
2083
2352
|
if (typeof IDBKeyRange !== "undefined") {
|
|
2084
|
-
const hint = extractIndexEqualityHint(combined, fieldToIndexMap ?? buildFieldToIndexMap(this.#contract, this.#storeName), getKeyPath
|
|
2353
|
+
const hint = extractIndexEqualityHint(combined, fieldToIndexMap ?? buildFieldToIndexMap(this.#contract, this.#storeName), getKeyPath(this.#contract, this.#modelName));
|
|
2085
2354
|
if (hint !== null) {
|
|
2086
2355
|
const { indexName, value, remainingFilter } = hint;
|
|
2087
2356
|
const filter = remainingFilter !== void 0 ? (row) => evaluateFilter(remainingFilter, row) : void 0;
|
|
@@ -2126,7 +2395,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
2126
2395
|
async #executeOrRows(hint, groupingKey, combined) {
|
|
2127
2396
|
const meta = this.#planMeta(groupingKey);
|
|
2128
2397
|
const storeName = this.#storeName;
|
|
2129
|
-
const keyPath = getKeyPath
|
|
2398
|
+
const keyPath = getKeyPath(this.#contract, this.#modelName);
|
|
2130
2399
|
const seen = /* @__PURE__ */ new Set();
|
|
2131
2400
|
const rows = [];
|
|
2132
2401
|
const ast = {
|
|
@@ -2151,7 +2420,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
2151
2420
|
return branchRows;
|
|
2152
2421
|
}));
|
|
2153
2422
|
for (const branchRows of branchResults) for (const row of branchRows) {
|
|
2154
|
-
const pk = row
|
|
2423
|
+
const pk = keyToken(extractKeyFromRow(row, keyPath));
|
|
2155
2424
|
if (!seen.has(pk)) {
|
|
2156
2425
|
seen.add(pk);
|
|
2157
2426
|
rows.push(row);
|
|
@@ -2240,6 +2509,6 @@ function idbOrm(options) {
|
|
|
2240
2509
|
return client;
|
|
2241
2510
|
}
|
|
2242
2511
|
//#endregion
|
|
2243
|
-
export { createRelationMutator as a, withMutationScope as c, hasNestedMutationCallbacks as i,
|
|
2512
|
+
export { createRelationMutator as a, withMutationScope as c, getStoreName as d, keyEquals as f, hasNestedMutationCallbacks as i, extractKeyFromRow as l, applyReferentialActionsForRow as n, isRelationMutationCallback as o, keyToken as p, collectDeleteStoreNames as r, isRelationMutationDescriptor as s, idbOrm as t, getKeyPath as u };
|
|
2244
2513
|
|
|
2245
|
-
//# sourceMappingURL=idb-orm-
|
|
2514
|
+
//# sourceMappingURL=idb-orm-JBg6si7u.mjs.map
|