@prisma-idb/client-idb 0.6.1 → 0.8.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-C8yXJp6R.mjs} +542 -200
- package/dist/idb-orm-C8yXJp6R.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-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/{managed-client-B_8JZM3h.mjs → managed-client-DofL09P8.mjs} +3 -3
- package/dist/{managed-client-B_8JZM3h.mjs.map → managed-client-DofL09P8.mjs.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 +9 -9
- 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
|
/**
|
|
@@ -463,6 +504,69 @@ function createGroupedAccessor(init) {
|
|
|
463
504
|
} };
|
|
464
505
|
}
|
|
465
506
|
//#endregion
|
|
507
|
+
//#region src/core/enum-validation.ts
|
|
508
|
+
const rulesByContract = /* @__PURE__ */ new WeakMap();
|
|
509
|
+
function resolveEnumRule(contract, field) {
|
|
510
|
+
const ref = field.valueSet;
|
|
511
|
+
if (ref === void 0 || ref.plane !== "domain" || ref.entityKind !== "enum") return void 0;
|
|
512
|
+
const contractEnum = contract.domain.namespaces[ref.namespaceId]?.enum?.[ref.entityName];
|
|
513
|
+
if (contractEnum === void 0) throw new Error(`Enum "${ref.entityName}" referenced by the contract is not declared in its domain.`);
|
|
514
|
+
return {
|
|
515
|
+
enumName: ref.entityName,
|
|
516
|
+
values: new Set(contractEnum.members.map((member) => member.value)),
|
|
517
|
+
nullable: field.nullable,
|
|
518
|
+
many: field.many === true
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
function getModelEnumRules(contract, modelName) {
|
|
522
|
+
let byModel = rulesByContract.get(contract);
|
|
523
|
+
if (byModel === void 0) {
|
|
524
|
+
byModel = /* @__PURE__ */ new Map();
|
|
525
|
+
rulesByContract.set(contract, byModel);
|
|
526
|
+
}
|
|
527
|
+
const cached = byModel.get(modelName);
|
|
528
|
+
if (cached !== void 0) return cached;
|
|
529
|
+
const rules = /* @__PURE__ */ new Map();
|
|
530
|
+
const fields = domainModelsAtDefaultNamespace(contract.domain)[modelName]?.fields ?? {};
|
|
531
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
532
|
+
const rule = resolveEnumRule(contract, field);
|
|
533
|
+
if (rule !== void 0) rules.set(fieldName, rule);
|
|
534
|
+
}
|
|
535
|
+
byModel.set(modelName, rules);
|
|
536
|
+
return rules;
|
|
537
|
+
}
|
|
538
|
+
function describe(value) {
|
|
539
|
+
return typeof value === "string" ? JSON.stringify(value) : `${String(value)} (${typeof value})`;
|
|
540
|
+
}
|
|
541
|
+
function invalidValueError(modelName, fieldName, rule, value) {
|
|
542
|
+
const expected = [...rule.values].map((v) => JSON.stringify(v)).join(", ");
|
|
543
|
+
return /* @__PURE__ */ new Error(`Invalid value ${describe(value)} for enum field "${modelName}.${fieldName}". Expected one of enum "${rule.enumName}": ${expected}.`);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Throws if `data` sets an enum field to a value its enum doesn't declare.
|
|
547
|
+
* Fields left `undefined` are not checked. `null` is accepted only on an
|
|
548
|
+
* optional (`Role?`) field; a list (`Role[]`) field must be an array whose
|
|
549
|
+
* every element is a declared value.
|
|
550
|
+
*/
|
|
551
|
+
function assertEnumValues(contract, modelName, data) {
|
|
552
|
+
const rules = getModelEnumRules(contract, modelName);
|
|
553
|
+
if (rules.size === 0) return;
|
|
554
|
+
for (const [fieldName, rule] of rules) {
|
|
555
|
+
const value = data[fieldName];
|
|
556
|
+
if (value === void 0) continue;
|
|
557
|
+
if (value === null) {
|
|
558
|
+
if (rule.nullable) continue;
|
|
559
|
+
throw new Error(`Enum field "${modelName}.${fieldName}" is required and cannot be null.`);
|
|
560
|
+
}
|
|
561
|
+
if (rule.many) {
|
|
562
|
+
if (!Array.isArray(value)) throw new Error(`Enum list field "${modelName}.${fieldName}" expects an array of "${rule.enumName}" values, got ${describe(value)}.`);
|
|
563
|
+
for (const element of value) if (!rule.values.has(element)) throw invalidValueError(modelName, fieldName, rule, element);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (!rule.values.has(value)) throw invalidValueError(modelName, fieldName, rule, value);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
//#endregion
|
|
466
570
|
//#region src/core/mutation-defaults.ts
|
|
467
571
|
function createMutationDefaultsCache() {
|
|
468
572
|
return /* @__PURE__ */ new Map();
|
|
@@ -533,8 +637,11 @@ function applyUpdateDefaults(defaults, storeName, patch, cache) {
|
|
|
533
637
|
* Batch-load a single named relation for all rows in `rows` and attach the
|
|
534
638
|
* result to each row under the `relName` key.
|
|
535
639
|
*
|
|
536
|
-
* The join
|
|
537
|
-
*
|
|
640
|
+
* The join matches every field of the relation, so compound foreign keys join
|
|
641
|
+
* on the whole tuple. It uses one key-range scan per distinct tuple when an
|
|
642
|
+
* index or the related primary key covers the target fields, and otherwise
|
|
643
|
+
* one full scan with an in-memory filter. Rows are then grouped in memory,
|
|
644
|
+
* which avoids N+1 queries.
|
|
538
645
|
*
|
|
539
646
|
* The `entry` carries any `include()` refinement:
|
|
540
647
|
*
|
|
@@ -562,23 +669,22 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
|
|
|
562
669
|
const relation = rawRelation;
|
|
563
670
|
const { cardinality, on } = relation;
|
|
564
671
|
const relatedModelName = relation.to.model;
|
|
565
|
-
const
|
|
566
|
-
const
|
|
567
|
-
if (
|
|
672
|
+
const localFields = on.localFields;
|
|
673
|
+
const targetFields = on.targetFields;
|
|
674
|
+
if (localFields.length === 0 || localFields.length !== targetFields.length) return rows;
|
|
568
675
|
const relatedModel = models[relatedModelName];
|
|
569
676
|
if (relatedModel === void 0) return rows;
|
|
570
677
|
const relatedStoreName = typeof relatedModel.storage === "object" && relatedModel.storage !== null && "storeName" in relatedModel.storage ? String(relatedModel.storage["storeName"]) : relatedModelName;
|
|
571
678
|
const isScalar = entry.kind === "scalar";
|
|
572
|
-
const
|
|
679
|
+
const localTuples = /* @__PURE__ */ new Map();
|
|
573
680
|
for (const row of rows) {
|
|
574
|
-
const
|
|
575
|
-
if (
|
|
681
|
+
const token = tupleToken(row, localFields);
|
|
682
|
+
if (token !== null) localTuples.set(token, localFields.map((f) => row[f]));
|
|
576
683
|
}
|
|
577
|
-
if (
|
|
684
|
+
if (localTuples.size === 0) return rows.map((row) => ({
|
|
578
685
|
...row,
|
|
579
686
|
[relName]: isScalar ? 0 : cardinality === "1:N" ? [] : null
|
|
580
687
|
}));
|
|
581
|
-
const capturedForeignField = foreignField;
|
|
582
688
|
const refinedWhere = combineFilterExprs(entry.state.filters);
|
|
583
689
|
const planMeta = {
|
|
584
690
|
target: "idb",
|
|
@@ -586,21 +692,25 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
|
|
|
586
692
|
lane: "idb-orm",
|
|
587
693
|
annotations: { groupingKey }
|
|
588
694
|
};
|
|
589
|
-
const
|
|
590
|
-
const targetsRelatedPk = fkIndexName === void 0 && typeof IDBKeyRange !== "undefined" && capturedForeignField === getKeyPath$1(contract, relatedModelName);
|
|
695
|
+
const rangeSource = findRangeSource(contract, relatedStoreName, relatedModelName, targetFields);
|
|
591
696
|
let relatedRows;
|
|
592
|
-
if (
|
|
697
|
+
if (rangeSource !== void 0) {
|
|
593
698
|
const refinedFilter = refinedWhere !== void 0 ? (row) => evaluateFilter(refinedWhere, row) : void 0;
|
|
594
|
-
const
|
|
595
|
-
|
|
699
|
+
const ranges = [];
|
|
700
|
+
for (const values of localTuples.values()) {
|
|
701
|
+
const ordered = keyPathFields(rangeSource.keyPath).map((f) => values[targetFields.indexOf(f)]);
|
|
702
|
+
if (!ordered.every((v) => isValidIdbKey(v))) continue;
|
|
703
|
+
ranges.push(IDBKeyRange.only(typeof rangeSource.keyPath === "string" ? ordered[0] : ordered));
|
|
704
|
+
}
|
|
705
|
+
relatedRows = (await Promise.all(ranges.map(async (range) => {
|
|
596
706
|
const plan = {
|
|
597
707
|
meta: planMeta,
|
|
598
708
|
idbPlan: {
|
|
599
709
|
meta: planMeta,
|
|
600
710
|
kind: "cursor-scan",
|
|
601
711
|
storeName: relatedStoreName,
|
|
602
|
-
...
|
|
603
|
-
range
|
|
712
|
+
...rangeSource.indexName !== void 0 ? { indexName: rangeSource.indexName } : {},
|
|
713
|
+
range,
|
|
604
714
|
...refinedFilter !== void 0 ? { filter: refinedFilter } : {}
|
|
605
715
|
}
|
|
606
716
|
};
|
|
@@ -610,7 +720,10 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
|
|
|
610
720
|
}))).flat();
|
|
611
721
|
} else {
|
|
612
722
|
relatedRows = [];
|
|
613
|
-
const filter = (row) =>
|
|
723
|
+
const filter = (row) => {
|
|
724
|
+
const token = tupleToken(row, targetFields);
|
|
725
|
+
return token !== null && localTuples.has(token) && (refinedWhere === void 0 || evaluateFilter(refinedWhere, row));
|
|
726
|
+
};
|
|
614
727
|
const plan = {
|
|
615
728
|
meta: planMeta,
|
|
616
729
|
idbPlan: {
|
|
@@ -625,20 +738,25 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
|
|
|
625
738
|
if (cardinality === "1:N") {
|
|
626
739
|
const grouped = /* @__PURE__ */ new Map();
|
|
627
740
|
for (const rrow of relatedRows) {
|
|
628
|
-
const gk = rrow
|
|
741
|
+
const gk = tupleToken(rrow, targetFields);
|
|
742
|
+
if (gk === null) continue;
|
|
629
743
|
const group = grouped.get(gk) ?? [];
|
|
630
744
|
group.push(rrow);
|
|
631
745
|
grouped.set(gk, group);
|
|
632
746
|
}
|
|
747
|
+
const groupFor = (row) => {
|
|
748
|
+
const token = tupleToken(row, localFields);
|
|
749
|
+
return token === null ? [] : grouped.get(token) ?? [];
|
|
750
|
+
};
|
|
633
751
|
if (isScalar) return rows.map((row) => ({
|
|
634
752
|
...row,
|
|
635
|
-
[relName]: (
|
|
753
|
+
[relName]: groupFor(row).length
|
|
636
754
|
}));
|
|
637
755
|
const comparator = buildRowComparator(entry.state.orderBy);
|
|
638
756
|
const skip = entry.state.skip ?? 0;
|
|
639
757
|
const take = entry.state.take;
|
|
640
758
|
return rows.map((row) => {
|
|
641
|
-
let group =
|
|
759
|
+
let group = groupFor(row);
|
|
642
760
|
if (comparator !== void 0) group = [...group].sort(comparator);
|
|
643
761
|
if (skip > 0 || take !== void 0) group = group.slice(skip, take !== void 0 ? skip + take : void 0);
|
|
644
762
|
return {
|
|
@@ -648,11 +766,50 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
|
|
|
648
766
|
});
|
|
649
767
|
}
|
|
650
768
|
const indexed = /* @__PURE__ */ new Map();
|
|
651
|
-
for (const rrow of relatedRows)
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
769
|
+
for (const rrow of relatedRows) {
|
|
770
|
+
const token = tupleToken(rrow, targetFields);
|
|
771
|
+
if (token !== null) indexed.set(token, rrow);
|
|
772
|
+
}
|
|
773
|
+
return rows.map((row) => {
|
|
774
|
+
const token = tupleToken(row, localFields);
|
|
775
|
+
return {
|
|
776
|
+
...row,
|
|
777
|
+
[relName]: (token === null ? void 0 : indexed.get(token)) ?? null
|
|
778
|
+
};
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* A `Map` key for the values of `fields` in `row`, or `null` when any of them
|
|
783
|
+
* is null or undefined. Equal `Date`s and binary values give equal tokens.
|
|
784
|
+
*/
|
|
785
|
+
function tupleToken(row, fields) {
|
|
786
|
+
const parts = [];
|
|
787
|
+
for (const field of fields) {
|
|
788
|
+
const value = row[field];
|
|
789
|
+
if (value === null || value === void 0) return null;
|
|
790
|
+
const token = fieldValueToken(value);
|
|
791
|
+
parts.push([typeof token, String(token)]);
|
|
792
|
+
}
|
|
793
|
+
return JSON.stringify(parts);
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Finds an index, or else the store's primary key, whose key path has exactly
|
|
797
|
+
* `fields` (in any order), so a lookup on those fields can use a key range.
|
|
798
|
+
* Returns `undefined` when there is none, or when `IDBKeyRange` isn't available.
|
|
799
|
+
*/
|
|
800
|
+
function findRangeSource(contract, storeName, modelName, fields) {
|
|
801
|
+
if (typeof IDBKeyRange === "undefined") return void 0;
|
|
802
|
+
const coversFields = (keyPath) => {
|
|
803
|
+
const keyFields = keyPathFields(keyPath);
|
|
804
|
+
return keyFields.length === fields.length && keyFields.every((f) => fields.includes(f));
|
|
805
|
+
};
|
|
806
|
+
const indexes = contract.storage.stores[storeName]?.indexes ?? {};
|
|
807
|
+
for (const [indexName, indexDef] of Object.entries(indexes)) if (indexDef.multiEntry !== true && coversFields(indexDef.keyPath)) return {
|
|
808
|
+
indexName,
|
|
809
|
+
keyPath: indexDef.keyPath
|
|
810
|
+
};
|
|
811
|
+
const primaryKey = getKeyPath(contract, modelName);
|
|
812
|
+
return coversFields(primaryKey) ? { keyPath: primaryKey } : void 0;
|
|
656
813
|
}
|
|
657
814
|
//#endregion
|
|
658
815
|
//#region src/core/mutation-scope.ts
|
|
@@ -774,22 +931,49 @@ function requireTransactionExecutor(executor) {
|
|
|
774
931
|
async function executeNestedCreateMutation(options) {
|
|
775
932
|
const { executor, contract, modelName, data } = options;
|
|
776
933
|
const record = data;
|
|
777
|
-
const storeNames = collectStoreNames(contract, modelName, record);
|
|
934
|
+
const storeNames = collectStoreNames(contract, modelName, record, "create");
|
|
778
935
|
const defaultsCache = createMutationDefaultsCache();
|
|
779
936
|
return withMutationScope(executor, storeNames, (scope) => createGraph(scope, contract, modelName, record, defaultsCache));
|
|
780
937
|
}
|
|
781
938
|
async function executeNestedUpdateMutation(options) {
|
|
782
939
|
const { executor, contract, modelName, filters, data } = options;
|
|
783
940
|
const record = data;
|
|
784
|
-
const storeNames = collectStoreNames(contract, modelName, record);
|
|
941
|
+
const storeNames = collectStoreNames(contract, modelName, record, "update");
|
|
785
942
|
const defaultsCache = createMutationDefaultsCache();
|
|
786
943
|
return withMutationScope(executor, storeNames, (scope) => updateFirstGraph(scope, contract, modelName, filters, record, defaultsCache));
|
|
787
944
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
945
|
+
/**
|
|
946
|
+
* Every store a nested write may touch: the model's own store, each related
|
|
947
|
+
* store a relation callback writes to, and the parent stores that foreign-key
|
|
948
|
+
* checks read, for the model and for each related model. An update also
|
|
949
|
+
* declares the stores its `onUpdate` referential actions may touch.
|
|
950
|
+
*
|
|
951
|
+
* Parent stores are declared for every foreign key, not only the ones `data`
|
|
952
|
+
* sets, because a nested write can set foreign keys through `connect()` or
|
|
953
|
+
* through a default.
|
|
954
|
+
*/
|
|
955
|
+
function collectStoreNames(contract, modelName, data, kind) {
|
|
956
|
+
const stores = /* @__PURE__ */ new Set([getStoreName(contract, modelName), ...fkParentStoreNames(contract, modelName)]);
|
|
957
|
+
const scalarData = {};
|
|
958
|
+
const relationNames = /* @__PURE__ */ new Set();
|
|
959
|
+
for (const def of getRelationDefinitions(contract, modelName)) {
|
|
960
|
+
relationNames.add(def.relationName);
|
|
961
|
+
if (def.relationName in data && isRelationMutationCallback(data[def.relationName])) {
|
|
962
|
+
stores.add(def.relatedStoreName);
|
|
963
|
+
for (const store of fkParentStoreNames(contract, def.relatedModelName)) stores.add(store);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (kind === "update") {
|
|
967
|
+
for (const [field, value] of Object.entries(data)) if (!relationNames.has(field)) scalarData[field] = value;
|
|
968
|
+
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, getStoreName(contract, modelName), scalarData, createMutationDefaultsCache());
|
|
969
|
+
for (const store of collectOnUpdateEnforcementStoreNames(contract, modelName, patch).storeNames) stores.add(store);
|
|
970
|
+
}
|
|
791
971
|
return [...stores];
|
|
792
972
|
}
|
|
973
|
+
/** The stores of every parent `modelName` has a foreign key to. */
|
|
974
|
+
function fkParentStoreNames(contract, modelName) {
|
|
975
|
+
return getRelationDefinitions(contract, modelName).filter((def) => def.cardinality === "N:1").map((def) => def.relatedStoreName);
|
|
976
|
+
}
|
|
793
977
|
async function createGraph(scope, contract, modelName, input, defaultsCache) {
|
|
794
978
|
const parsed = parseMutationInput(contract, modelName, input);
|
|
795
979
|
const { parentOwned, childOwned } = partitionByOwnership(parsed.relationMutations);
|
|
@@ -815,9 +999,12 @@ async function updateFirstGraph(scope, contract, modelName, filters, input, defa
|
|
|
815
999
|
let parentRow = existingRow;
|
|
816
1000
|
if (Object.keys(scalarData).length > 0) {
|
|
817
1001
|
const storeName = getStoreName(contract, modelName);
|
|
818
|
-
const key = existingRow
|
|
1002
|
+
const key = extractKeyFromRow(existingRow, getKeyPath(contract, modelName));
|
|
819
1003
|
const meta = makePlanMeta(contract);
|
|
820
1004
|
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, storeName, scalarData, defaultsCache);
|
|
1005
|
+
assertEnumValues(contract, modelName, patch);
|
|
1006
|
+
await validateScalarFks(scope, contract, modelName, patch, existingRow);
|
|
1007
|
+
await applyReferentialActionsForRowOnUpdate(scope, contract, modelName, existingRow, patch);
|
|
821
1008
|
const updated = (await scope.execute({
|
|
822
1009
|
meta,
|
|
823
1010
|
kind: "update",
|
|
@@ -970,6 +1157,8 @@ async function insertSingleRow(scope, contract, modelName, data, defaultsCache)
|
|
|
970
1157
|
const storeName = getStoreName(contract, modelName);
|
|
971
1158
|
const meta = makePlanMeta(contract);
|
|
972
1159
|
const record = applyCreateDefaults(contract.execution?.mutations.defaults, storeName, data, defaultsCache);
|
|
1160
|
+
assertEnumValues(contract, modelName, record);
|
|
1161
|
+
await validateScalarFks(scope, contract, modelName, record);
|
|
973
1162
|
return (await scope.execute({
|
|
974
1163
|
meta,
|
|
975
1164
|
kind: "add",
|
|
@@ -1020,10 +1209,7 @@ function buildCriterionFilter(criterion) {
|
|
|
1020
1209
|
}
|
|
1021
1210
|
function buildParentJoinFilter(parentValues) {
|
|
1022
1211
|
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";
|
|
1212
|
+
return (row) => pairs.every(([childField, parentValue]) => fieldValuesEqual(row[childField], parentValue));
|
|
1027
1213
|
}
|
|
1028
1214
|
function getStoredAction(contract, modelName, relationName, kind) {
|
|
1029
1215
|
return (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.relations?.[relationName]?.[kind];
|
|
@@ -1048,7 +1234,10 @@ function sameFields(a, b) {
|
|
|
1048
1234
|
* action kind on both sides so this fallback never has to arbitrate a real
|
|
1049
1235
|
* conflict.
|
|
1050
1236
|
*/
|
|
1051
|
-
function getReferentialActionForRelation(contract, modelName, def, kind
|
|
1237
|
+
function getReferentialActionForRelation(contract, modelName, def, kind) {
|
|
1238
|
+
return enforcedAction(findDeclaredAction(contract, modelName, def, kind));
|
|
1239
|
+
}
|
|
1240
|
+
function findDeclaredAction(contract, modelName, def, kind) {
|
|
1052
1241
|
const direct = getStoredAction(contract, modelName, def.relationName, kind);
|
|
1053
1242
|
if (direct !== void 0) return direct;
|
|
1054
1243
|
for (const inverse of getRelationDefinitions(contract, def.relatedModelName)) {
|
|
@@ -1058,29 +1247,88 @@ function getReferentialActionForRelation(contract, modelName, def, kind, default
|
|
|
1058
1247
|
const inverseAction = getStoredAction(contract, def.relatedModelName, inverse.relationName, kind);
|
|
1059
1248
|
if (inverseAction !== void 0) return inverseAction;
|
|
1060
1249
|
}
|
|
1061
|
-
return defaultAction;
|
|
1062
|
-
}
|
|
1063
|
-
function getOnDeleteForDeleteRelation(contract, modelName, def) {
|
|
1064
|
-
return getReferentialActionForRelation(contract, modelName, def, "onDelete", "restrict");
|
|
1065
1250
|
}
|
|
1066
1251
|
/**
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1252
|
+
* Applies the default and resolves `noAction`, matching what Postgres does
|
|
1253
|
+
* with the same schema, so a synced app behaves the same on both sides:
|
|
1254
|
+
*
|
|
1255
|
+
* - An undeclared action defaults to `restrict`, for `onDelete` and
|
|
1256
|
+
* `onUpdate` alike. Prisma 8 emits no `ON DELETE`/`ON UPDATE` clause for an
|
|
1257
|
+
* undeclared action, and the database default is `NO ACTION`.
|
|
1258
|
+
* - `noAction` behaves like `restrict`. In SQL, `NO ACTION` also rejects the
|
|
1259
|
+
* change; it only defers the check to the end of the statement, which makes
|
|
1260
|
+
* no difference here.
|
|
1071
1261
|
*/
|
|
1262
|
+
function enforcedAction(declared) {
|
|
1263
|
+
return declared === void 0 || declared === "noAction" ? "restrict" : declared;
|
|
1264
|
+
}
|
|
1265
|
+
function getOnDeleteForDeleteRelation(contract, modelName, def) {
|
|
1266
|
+
return getReferentialActionForRelation(contract, modelName, def, "onDelete");
|
|
1267
|
+
}
|
|
1072
1268
|
function getOnUpdateForRelation(contract, modelName, def) {
|
|
1073
|
-
return getReferentialActionForRelation(contract, modelName, def, "onUpdate"
|
|
1269
|
+
return getReferentialActionForRelation(contract, modelName, def, "onUpdate");
|
|
1074
1270
|
}
|
|
1075
1271
|
function isDeleteEnforcementRelation(contract, modelName, def) {
|
|
1076
1272
|
if (def.cardinality === "1:N") return true;
|
|
1077
1273
|
if (def.cardinality === "1:1") {
|
|
1078
1274
|
const keyPath = getKeyPath(contract, modelName);
|
|
1079
|
-
return def.localFields
|
|
1275
|
+
return sameFields(def.localFields, keyPathFields(keyPath));
|
|
1080
1276
|
}
|
|
1081
1277
|
return false;
|
|
1082
1278
|
}
|
|
1083
1279
|
/**
|
|
1280
|
+
* An `IDBKeyRange` for the lookup "does `modelName` have a row whose `field`
|
|
1281
|
+
* equals `value`?" — but ONLY when `field` is that model's own single-field
|
|
1282
|
+
* primary key (so the answer is a pure key lookup that needs no row value)
|
|
1283
|
+
* and `value` is a legal IDB key. `null` otherwise, and the caller keeps its
|
|
1284
|
+
* value-materializing `cursor-scan`.
|
|
1285
|
+
*
|
|
1286
|
+
* Deliberately `null` for a compound primary key even if `field` is one of its
|
|
1287
|
+
* members (one member can't pin the whole key), and for any non-key field
|
|
1288
|
+
* (that needs an index-resolution step — Phase 10.5's shared primitive, not
|
|
1289
|
+
* this helper's job). `IDBKeyRange.only` throws `DataError` on invalid keys
|
|
1290
|
+
* (`null`, `NaN`, booleans, …), hence the guard.
|
|
1291
|
+
*/
|
|
1292
|
+
function pkEqualityRange(contract, modelName, field, value) {
|
|
1293
|
+
if (typeof IDBKeyRange === "undefined") return null;
|
|
1294
|
+
const keyPath = getKeyPath(contract, modelName);
|
|
1295
|
+
if (typeof keyPath !== "string" || keyPath !== field) return null;
|
|
1296
|
+
if (!isValidIdbKey(value)) return null;
|
|
1297
|
+
return IDBKeyRange.only(value);
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Resolves to the first primary key in `storeName` within `range`, or
|
|
1301
|
+
* `undefined` — one `getKey` request, no row deserialized.
|
|
1302
|
+
*/
|
|
1303
|
+
async function firstKeyInRange(scope, meta, storeName, range) {
|
|
1304
|
+
return (await scope.execute({
|
|
1305
|
+
meta,
|
|
1306
|
+
kind: "keys",
|
|
1307
|
+
storeName,
|
|
1308
|
+
range,
|
|
1309
|
+
take: 1
|
|
1310
|
+
}))[0]?.["key"];
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* `true` if any child row matches the relation against `parentRow`'s values —
|
|
1314
|
+
* the existence check behind `restrict`. Key-only when the relation's single
|
|
1315
|
+
* child field is the child model's own primary key (a shared-PK 1:1); a
|
|
1316
|
+
* value-materializing `cursor-scan` (`take: 1`) otherwise.
|
|
1317
|
+
*/
|
|
1318
|
+
async function childExists(scope, contract, meta, def, parentRow) {
|
|
1319
|
+
if (def.targetFields.length === 1) {
|
|
1320
|
+
const range = pkEqualityRange(contract, def.relatedModelName, def.targetFields[0], parentRow[def.localFields[0]]);
|
|
1321
|
+
if (range !== null) return await firstKeyInRange(scope, meta, def.relatedStoreName, range) !== void 0;
|
|
1322
|
+
}
|
|
1323
|
+
return (await scope.execute({
|
|
1324
|
+
meta,
|
|
1325
|
+
kind: "cursor-scan",
|
|
1326
|
+
storeName: def.relatedStoreName,
|
|
1327
|
+
filter: buildChildFilterFromRow(def, parentRow),
|
|
1328
|
+
take: 1
|
|
1329
|
+
})).length > 0;
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1084
1332
|
* Builds a filter matching a relation's children against one specific parent
|
|
1085
1333
|
* row's values. Shared by `onDelete` cascade (`applyReferentialActionsForRow`)
|
|
1086
1334
|
* and `onUpdate` cascade (`applyReferentialActionsForRowOnUpdate`).
|
|
@@ -1090,7 +1338,7 @@ function buildChildFilterFromRow(def, row) {
|
|
|
1090
1338
|
childField: def.targetFields[i],
|
|
1091
1339
|
parentValue: row[lf]
|
|
1092
1340
|
}));
|
|
1093
|
-
return (child) => pairs.every(({ childField, parentValue }) => child[childField]
|
|
1341
|
+
return (child) => pairs.every(({ childField, parentValue }) => fieldValuesEqual(child[childField], parentValue));
|
|
1094
1342
|
}
|
|
1095
1343
|
/** Reads a field's literal `@default(...)` value from `IdbModelStorage.fieldDefaults`, if declared. */
|
|
1096
1344
|
function getFieldDefault(contract, modelName, fieldName) {
|
|
@@ -1127,10 +1375,8 @@ function buildSetDefaultPatch(contract, def) {
|
|
|
1127
1375
|
* part of the transaction (it's the model literally being written), so no
|
|
1128
1376
|
* store-list changes are needed to call this.
|
|
1129
1377
|
*
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1132
|
-
* field's default is resolved independently, so there's no guarantee the
|
|
1133
|
-
* combination corresponds to a single real row.
|
|
1378
|
+
* A compound relation is checked as one tuple: a single parent row must
|
|
1379
|
+
* match every field's default.
|
|
1134
1380
|
*
|
|
1135
1381
|
* `excludeKey`, when given, excludes the parent row currently being
|
|
1136
1382
|
* deleted/updated from the existence scan. That row is still physically
|
|
@@ -1142,21 +1388,9 @@ function buildSetDefaultPatch(contract, def) {
|
|
|
1142
1388
|
* default references a real row, when after the transaction nothing will.
|
|
1143
1389
|
*/
|
|
1144
1390
|
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.`);
|
|
1391
|
+
const values = def.targetFields.map((f) => patch[f]);
|
|
1392
|
+
if (values.some((v) => v === null || v === void 0)) return;
|
|
1393
|
+
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
1394
|
}
|
|
1161
1395
|
/**
|
|
1162
1396
|
* Collects every store an `onUpdate`-enforced write to `modelName` might
|
|
@@ -1186,7 +1420,6 @@ function collectOnUpdateEnforcementStoreNames(contract, modelName, data) {
|
|
|
1186
1420
|
for (const def of getRelationDefinitions(contract, mName)) {
|
|
1187
1421
|
if (!isDeleteEnforcementRelation(contract, mName, def)) continue;
|
|
1188
1422
|
const action = getOnUpdateForRelation(contract, mName, def);
|
|
1189
|
-
if (action === "noAction") continue;
|
|
1190
1423
|
stores.add(def.relatedStoreName);
|
|
1191
1424
|
if (action === "cascade") walkCascadeChain(def.relatedModelName);
|
|
1192
1425
|
}
|
|
@@ -1196,7 +1429,6 @@ function collectOnUpdateEnforcementStoreNames(contract, modelName, data) {
|
|
|
1196
1429
|
if (!isDeleteEnforcementRelation(contract, modelName, def)) continue;
|
|
1197
1430
|
if (!def.localFields.some((f) => f in data)) continue;
|
|
1198
1431
|
const action = getOnUpdateForRelation(contract, modelName, def);
|
|
1199
|
-
if (action === "noAction") continue;
|
|
1200
1432
|
enforces = true;
|
|
1201
1433
|
stores.add(def.relatedStoreName);
|
|
1202
1434
|
if (action === "cascade") walkCascadeChain(def.relatedModelName);
|
|
@@ -1231,25 +1463,18 @@ function collectOnUpdateEnforcementStoreNames(contract, modelName, data) {
|
|
|
1231
1463
|
*/
|
|
1232
1464
|
async function applyReferentialActionsForRowOnUpdate(scope, contract, modelName, oldRow, patch, visited = /* @__PURE__ */ new Set()) {
|
|
1233
1465
|
const keyPath = getKeyPath(contract, modelName);
|
|
1234
|
-
const rowKey = `${getStoreName(contract, modelName)}::${
|
|
1466
|
+
const rowKey = `${getStoreName(contract, modelName)}::${keyToken(extractKeyFromRow(oldRow, keyPath))}`;
|
|
1235
1467
|
if (visited.has(rowKey)) return;
|
|
1236
1468
|
visited.add(rowKey);
|
|
1237
1469
|
const meta = makePlanMeta(contract);
|
|
1238
1470
|
for (const def of getRelationDefinitions(contract, modelName)) {
|
|
1239
1471
|
if (!isDeleteEnforcementRelation(contract, modelName, def)) continue;
|
|
1240
|
-
const changedFields = def.localFields.filter((f) => f in patch && patch[f]
|
|
1472
|
+
const changedFields = def.localFields.filter((f) => f in patch && !fieldValuesEqual(patch[f], oldRow[f]));
|
|
1241
1473
|
if (changedFields.length === 0) continue;
|
|
1242
1474
|
const action = getOnUpdateForRelation(contract, modelName, def);
|
|
1243
|
-
if (action === "noAction") continue;
|
|
1244
1475
|
const childFilter = buildChildFilterFromRow(def, oldRow);
|
|
1245
1476
|
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'.`);
|
|
1477
|
+
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
1478
|
continue;
|
|
1254
1479
|
}
|
|
1255
1480
|
if (action === "cascade") {
|
|
@@ -1291,7 +1516,7 @@ async function applyReferentialActionsForRowOnUpdate(scope, contract, modelName,
|
|
|
1291
1516
|
}
|
|
1292
1517
|
if (action === "setDefault") {
|
|
1293
1518
|
const childPatch = buildSetDefaultPatch(contract, def);
|
|
1294
|
-
await validateSetDefaultPatch(scope, contract, modelName, def, childPatch, oldRow
|
|
1519
|
+
await validateSetDefaultPatch(scope, contract, modelName, def, childPatch, extractKeyFromRow(oldRow, keyPath));
|
|
1295
1520
|
await scope.execute({
|
|
1296
1521
|
meta,
|
|
1297
1522
|
kind: "scan-write",
|
|
@@ -1323,31 +1548,102 @@ function collectScalarFkStoreNames(contract, modelName, data) {
|
|
|
1323
1548
|
}
|
|
1324
1549
|
return [...stores];
|
|
1325
1550
|
}
|
|
1326
|
-
|
|
1551
|
+
/**
|
|
1552
|
+
* Checks that every foreign key `data` sets points at an existing parent row,
|
|
1553
|
+
* inside the write's transaction.
|
|
1554
|
+
*
|
|
1555
|
+
* A compound foreign key is checked as one tuple: a single parent row must
|
|
1556
|
+
* match every field. Checking the fields one at a time could pass with each
|
|
1557
|
+
* value taken from a different parent. When `data` sets only some fields of a
|
|
1558
|
+
* compound key, the rest come from `existingRow`, the row being updated.
|
|
1559
|
+
* Callers must pass it in that case; see {@link fkCheckNeedsExistingRow}.
|
|
1560
|
+
*
|
|
1561
|
+
* A key with any `null` field isn't checked, like SQL's default `MATCH SIMPLE`.
|
|
1562
|
+
*/
|
|
1563
|
+
async function validateScalarFks(scope, contract, modelName, data, existingRow) {
|
|
1327
1564
|
const meta = makePlanMeta(contract);
|
|
1328
1565
|
for (const def of getRelationDefinitions(contract, modelName)) {
|
|
1329
1566
|
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)}'`);
|
|
1567
|
+
if (!def.localFields.some((f) => f in data)) continue;
|
|
1568
|
+
const values = def.localFields.map((f) => f in data ? data[f] : existingRow?.[f]);
|
|
1569
|
+
if (values.some((v) => v === null || v === void 0)) continue;
|
|
1570
|
+
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
1571
|
}
|
|
1345
1572
|
}
|
|
1573
|
+
/**
|
|
1574
|
+
* `true` when `data` sets some, but not all, fields of a compound foreign
|
|
1575
|
+
* key. Checking it then needs the rest of the key from the row being
|
|
1576
|
+
* updated, so the update has to read each row before writing it.
|
|
1577
|
+
*/
|
|
1578
|
+
function fkCheckNeedsExistingRow(contract, modelName, data) {
|
|
1579
|
+
return getRelationDefinitions(contract, modelName).some((def) => {
|
|
1580
|
+
if (def.cardinality !== "N:1") return false;
|
|
1581
|
+
const set = def.localFields.filter((f) => f in data).length;
|
|
1582
|
+
return set > 0 && set < def.localFields.length;
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Does `parentModel` have a row whose `fields` equal `values`, pairwise?
|
|
1587
|
+
*
|
|
1588
|
+
* When `fields` are exactly the parent's primary key (in any order), this is
|
|
1589
|
+
* one key-only lookup. Otherwise it scans the parent store, comparing values
|
|
1590
|
+
* the way IndexedDB compares keys. `excludeKey` leaves out one parent row,
|
|
1591
|
+
* for checks that run while that row is being deleted or changed.
|
|
1592
|
+
*/
|
|
1593
|
+
async function parentExists(scope, contract, meta, parentModel, fields, values, excludeKey) {
|
|
1594
|
+
const storeName = getStoreName(contract, parentModel);
|
|
1595
|
+
const keyPath = getKeyPath(contract, parentModel);
|
|
1596
|
+
const range = primaryKeyRange(keyPath, fields, values);
|
|
1597
|
+
if (range !== null) {
|
|
1598
|
+
const foundKey = await firstKeyInRange(scope, meta, storeName, range);
|
|
1599
|
+
return foundKey !== void 0 && (excludeKey === void 0 || !keyEquals(foundKey, excludeKey));
|
|
1600
|
+
}
|
|
1601
|
+
const filter = (row) => fields.every((f, i) => fieldValuesEqual(row[f], values[i])) && (excludeKey === void 0 || !keyEquals(extractKeyFromRow(row, keyPath), excludeKey));
|
|
1602
|
+
return (await scope.execute({
|
|
1603
|
+
meta,
|
|
1604
|
+
kind: "cursor-scan",
|
|
1605
|
+
storeName,
|
|
1606
|
+
filter,
|
|
1607
|
+
take: 1
|
|
1608
|
+
})).length > 0;
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* An `IDBKeyRange` for "the row whose primary key is `values`", when `fields`
|
|
1612
|
+
* are exactly the key's fields (in any order) and every value is a valid
|
|
1613
|
+
* IndexedDB key. `null` otherwise, and the caller scans instead.
|
|
1614
|
+
*/
|
|
1615
|
+
function primaryKeyRange(keyPath, fields, values) {
|
|
1616
|
+
if (typeof IDBKeyRange === "undefined") return null;
|
|
1617
|
+
const keyFields = keyPathFields(keyPath);
|
|
1618
|
+
if (keyFields.length !== fields.length || !keyFields.every((f) => fields.includes(f))) return null;
|
|
1619
|
+
const ordered = keyFields.map((f) => values[fields.indexOf(f)]);
|
|
1620
|
+
if (!ordered.every((v) => isValidIdbKey(v))) return null;
|
|
1621
|
+
return IDBKeyRange.only(typeof keyPath === "string" ? ordered[0] : ordered);
|
|
1622
|
+
}
|
|
1623
|
+
/** `id='u1'`, or `orgId='a', id='u1'` for a compound key. */
|
|
1624
|
+
function describeTuple(fields, values) {
|
|
1625
|
+
return fields.map((f, i) => `${f}='${String(values[i])}'`).join(", ");
|
|
1626
|
+
}
|
|
1346
1627
|
async function executeScalarCreateWithFkValidation(options) {
|
|
1347
1628
|
const { executor, contract, modelName, data } = options;
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1629
|
+
const defaultsCache = createMutationDefaultsCache();
|
|
1630
|
+
const record = applyCreateDefaults(contract.execution?.mutations.defaults, getStoreName(contract, modelName), data, defaultsCache);
|
|
1631
|
+
return withMutationScope(executor, collectScalarFkStoreNames(contract, modelName, record), (scope) => insertSingleRow(scope, contract, modelName, record, defaultsCache));
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* `createAll()` for rows that set foreign keys: checks every row's foreign
|
|
1635
|
+
* keys and inserts it, all in one transaction, so one bad row writes nothing.
|
|
1636
|
+
* The batch shares one defaults cache, like the plain `createAll()` path.
|
|
1637
|
+
*/
|
|
1638
|
+
async function executeScalarCreateAllWithFkValidation(options) {
|
|
1639
|
+
const { executor, contract, modelName, data } = options;
|
|
1640
|
+
const defaultsCache = createMutationDefaultsCache();
|
|
1641
|
+
const storeName = getStoreName(contract, modelName);
|
|
1642
|
+
const records = data.map((row) => applyCreateDefaults(contract.execution?.mutations.defaults, storeName, row, defaultsCache));
|
|
1643
|
+
return withMutationScope(executor, [...new Set(records.flatMap((row) => collectScalarFkStoreNames(contract, modelName, row)))], async (scope) => {
|
|
1644
|
+
const inserted = [];
|
|
1645
|
+
for (const record of records) inserted.push(await insertSingleRow(scope, contract, modelName, record, defaultsCache));
|
|
1646
|
+
return inserted;
|
|
1351
1647
|
});
|
|
1352
1648
|
}
|
|
1353
1649
|
/**
|
|
@@ -1368,18 +1664,20 @@ function collectUpdateStoreNames(contract, modelName, data) {
|
|
|
1368
1664
|
}
|
|
1369
1665
|
async function executeScalarUpdateWithFkValidation(options) {
|
|
1370
1666
|
const { executor, contract, modelName, filters, data } = options;
|
|
1371
|
-
const
|
|
1667
|
+
const storeName = getStoreName(contract, modelName);
|
|
1668
|
+
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, storeName, data, createMutationDefaultsCache());
|
|
1669
|
+
assertEnumValues(contract, modelName, patch);
|
|
1670
|
+
const { storeNames, enforcesOnUpdate } = collectUpdateStoreNames(contract, modelName, patch);
|
|
1671
|
+
const needsRowForFks = fkCheckNeedsExistingRow(contract, modelName, patch);
|
|
1372
1672
|
return withMutationScope(executor, storeNames, async (scope) => {
|
|
1373
|
-
await validateScalarFks(scope, contract, modelName,
|
|
1374
|
-
const storeName = getStoreName(contract, modelName);
|
|
1673
|
+
if (!needsRowForFks) await validateScalarFks(scope, contract, modelName, patch);
|
|
1375
1674
|
const meta = makePlanMeta(contract);
|
|
1376
1675
|
const combined = filters.length === 0 ? void 0 : filters.length === 1 ? filters[0] : {
|
|
1377
1676
|
kind: "and",
|
|
1378
1677
|
exprs: filters
|
|
1379
1678
|
};
|
|
1380
1679
|
const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
|
|
1381
|
-
|
|
1382
|
-
if (!enforcesOnUpdate) return (await scope.execute({
|
|
1680
|
+
if (!enforcesOnUpdate && !needsRowForFks) return (await scope.execute({
|
|
1383
1681
|
meta,
|
|
1384
1682
|
kind: "scan-write",
|
|
1385
1683
|
storeName,
|
|
@@ -1396,8 +1694,9 @@ async function executeScalarUpdateWithFkValidation(options) {
|
|
|
1396
1694
|
...filter !== void 0 ? { filter } : {}
|
|
1397
1695
|
}))[0];
|
|
1398
1696
|
if (!oldRow) return null;
|
|
1399
|
-
await
|
|
1400
|
-
|
|
1697
|
+
if (needsRowForFks) await validateScalarFks(scope, contract, modelName, patch, oldRow);
|
|
1698
|
+
if (enforcesOnUpdate) await applyReferentialActionsForRowOnUpdate(scope, contract, modelName, oldRow, patch);
|
|
1699
|
+
const key = extractKeyFromRow(oldRow, getKeyPath(contract, modelName));
|
|
1401
1700
|
return (await scope.execute({
|
|
1402
1701
|
meta,
|
|
1403
1702
|
kind: "update",
|
|
@@ -1426,18 +1725,20 @@ async function executeScalarUpdateWithFkValidation(options) {
|
|
|
1426
1725
|
*/
|
|
1427
1726
|
async function executeBulkUpdateWithFkValidation(options) {
|
|
1428
1727
|
const { executor, contract, modelName, filters, data } = options;
|
|
1429
|
-
const
|
|
1728
|
+
const storeName = getStoreName(contract, modelName);
|
|
1729
|
+
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, storeName, data, createMutationDefaultsCache());
|
|
1730
|
+
assertEnumValues(contract, modelName, patch);
|
|
1731
|
+
const { storeNames, enforcesOnUpdate } = collectUpdateStoreNames(contract, modelName, patch);
|
|
1732
|
+
const needsRowForFks = fkCheckNeedsExistingRow(contract, modelName, patch);
|
|
1430
1733
|
return withMutationScope(executor, storeNames, async (scope) => {
|
|
1431
|
-
await validateScalarFks(scope, contract, modelName,
|
|
1432
|
-
const storeName = getStoreName(contract, modelName);
|
|
1734
|
+
if (!needsRowForFks) await validateScalarFks(scope, contract, modelName, patch);
|
|
1433
1735
|
const meta = makePlanMeta(contract);
|
|
1434
1736
|
const combined = filters.length === 0 ? void 0 : filters.length === 1 ? filters[0] : {
|
|
1435
1737
|
kind: "and",
|
|
1436
1738
|
exprs: filters
|
|
1437
1739
|
};
|
|
1438
1740
|
const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
|
|
1439
|
-
|
|
1440
|
-
if (!enforcesOnUpdate) return scope.execute({
|
|
1741
|
+
if (!enforcesOnUpdate && !needsRowForFks) return scope.execute({
|
|
1441
1742
|
meta,
|
|
1442
1743
|
kind: "scan-write",
|
|
1443
1744
|
storeName,
|
|
@@ -1454,8 +1755,9 @@ async function executeBulkUpdateWithFkValidation(options) {
|
|
|
1454
1755
|
const keyPath = getKeyPath(contract, modelName);
|
|
1455
1756
|
const results = [];
|
|
1456
1757
|
for (const oldRow of oldRows) {
|
|
1457
|
-
await
|
|
1458
|
-
|
|
1758
|
+
if (needsRowForFks) await validateScalarFks(scope, contract, modelName, patch, oldRow);
|
|
1759
|
+
if (enforcesOnUpdate) await applyReferentialActionsForRowOnUpdate(scope, contract, modelName, oldRow, patch);
|
|
1760
|
+
const key = extractKeyFromRow(oldRow, keyPath);
|
|
1459
1761
|
const updated = (await scope.execute({
|
|
1460
1762
|
meta,
|
|
1461
1763
|
kind: "update",
|
|
@@ -1470,16 +1772,11 @@ async function executeBulkUpdateWithFkValidation(options) {
|
|
|
1470
1772
|
}
|
|
1471
1773
|
/**
|
|
1472
1774
|
* 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.
|
|
1775
|
+
* 1:1). Every such relation has an `onDelete` action to enforce: `noAction`
|
|
1776
|
+
* behaves like `restrict`, which is also the default.
|
|
1476
1777
|
*/
|
|
1477
1778
|
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;
|
|
1779
|
+
return getRelationDefinitions(contract, modelName).some((def) => isDeleteEnforcementRelation(contract, modelName, def));
|
|
1483
1780
|
}
|
|
1484
1781
|
/**
|
|
1485
1782
|
* Transitively walks the `onDelete` cascade graph from `modelName`, collecting
|
|
@@ -1502,7 +1799,6 @@ function collectDeleteStoreNames(contract, modelName) {
|
|
|
1502
1799
|
for (const def of getRelationDefinitions(contract, mName)) {
|
|
1503
1800
|
if (!isDeleteEnforcementRelation(contract, mName, def)) continue;
|
|
1504
1801
|
const action = getOnDeleteForDeleteRelation(contract, mName, def);
|
|
1505
|
-
if (action === "noAction") continue;
|
|
1506
1802
|
stores.add(def.relatedStoreName);
|
|
1507
1803
|
if (action === "cascade") walk(def.relatedModelName);
|
|
1508
1804
|
}
|
|
@@ -1530,23 +1826,16 @@ function collectDeleteStoreNames(contract, modelName) {
|
|
|
1530
1826
|
*/
|
|
1531
1827
|
async function applyReferentialActionsForRow(scope, contract, modelName, row, visited = /* @__PURE__ */ new Set()) {
|
|
1532
1828
|
const keyPath = getKeyPath(contract, modelName);
|
|
1533
|
-
const rowKey = `${getStoreName(contract, modelName)}::${
|
|
1829
|
+
const rowKey = `${getStoreName(contract, modelName)}::${keyToken(extractKeyFromRow(row, keyPath))}`;
|
|
1534
1830
|
if (visited.has(rowKey)) return;
|
|
1535
1831
|
visited.add(rowKey);
|
|
1536
1832
|
const meta = makePlanMeta(contract);
|
|
1537
1833
|
for (const def of getRelationDefinitions(contract, modelName)) {
|
|
1538
1834
|
if (!isDeleteEnforcementRelation(contract, modelName, def)) continue;
|
|
1539
1835
|
const action = getOnDeleteForDeleteRelation(contract, modelName, def);
|
|
1540
|
-
if (action === "noAction") continue;
|
|
1541
1836
|
const childFilter = buildChildFilterFromRow(def, row);
|
|
1542
1837
|
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'.`);
|
|
1838
|
+
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
1839
|
continue;
|
|
1551
1840
|
}
|
|
1552
1841
|
if (action === "cascade") {
|
|
@@ -1563,7 +1852,7 @@ async function applyReferentialActionsForRow(scope, contract, modelName, row, vi
|
|
|
1563
1852
|
meta,
|
|
1564
1853
|
kind: "delete",
|
|
1565
1854
|
storeName: def.relatedStoreName,
|
|
1566
|
-
key: child
|
|
1855
|
+
key: extractKeyFromRow(child, childKeyPath)
|
|
1567
1856
|
});
|
|
1568
1857
|
}
|
|
1569
1858
|
continue;
|
|
@@ -1583,7 +1872,7 @@ async function applyReferentialActionsForRow(scope, contract, modelName, row, vi
|
|
|
1583
1872
|
}
|
|
1584
1873
|
if (action === "setDefault") {
|
|
1585
1874
|
const patch = buildSetDefaultPatch(contract, def);
|
|
1586
|
-
await validateSetDefaultPatch(scope, contract, modelName, def, patch, row
|
|
1875
|
+
await validateSetDefaultPatch(scope, contract, modelName, def, patch, extractKeyFromRow(row, keyPath));
|
|
1587
1876
|
await scope.execute({
|
|
1588
1877
|
meta,
|
|
1589
1878
|
kind: "scan-write",
|
|
@@ -1632,7 +1921,7 @@ async function executeDeleteAllWithReferentialActions(options) {
|
|
|
1632
1921
|
});
|
|
1633
1922
|
for (const row of rows) {
|
|
1634
1923
|
await applyReferentialActionsForRow(scope, contract, modelName, row);
|
|
1635
|
-
const key = row
|
|
1924
|
+
const key = extractKeyFromRow(row, keyPath);
|
|
1636
1925
|
await scope.execute({
|
|
1637
1926
|
meta,
|
|
1638
1927
|
kind: "delete",
|
|
@@ -1709,7 +1998,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1709
1998
|
const projectRows = this.#projectRows.bind(this);
|
|
1710
1999
|
const combined = this.#combinedFilterExpr();
|
|
1711
2000
|
const fieldToIndexMap = typeof IDBKeyRange !== "undefined" ? buildFieldToIndexMap(this.#contract, this.#storeName) : void 0;
|
|
1712
|
-
const keyPath = getKeyPath
|
|
2001
|
+
const keyPath = getKeyPath(this.#contract, this.#modelName);
|
|
1713
2002
|
const comparator = buildRowComparator(this.#state.orderBy);
|
|
1714
2003
|
const skip = this.#state.skip;
|
|
1715
2004
|
const take = this.#state.take;
|
|
@@ -1743,7 +2032,17 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1743
2032
|
aggregates: toAggregateRequests(spec),
|
|
1744
2033
|
...combined !== void 0 ? { where: combined } : {}
|
|
1745
2034
|
};
|
|
1746
|
-
|
|
2035
|
+
const groupingKey = this.#newGroupingKey();
|
|
2036
|
+
if (Object.values(spec).every((selector) => selector.fn === "count")) {
|
|
2037
|
+
const scanPlan = this.#buildScanPlan(groupingKey);
|
|
2038
|
+
const total = await this.#executeNativeCount(scanPlan, ast);
|
|
2039
|
+
if (total !== null) {
|
|
2040
|
+
const result = {};
|
|
2041
|
+
for (const alias of Object.keys(spec)) result[alias] = total;
|
|
2042
|
+
return result;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
return computeAggregateSpec(spec, await this.#materialize(groupingKey, ast));
|
|
1747
2046
|
}
|
|
1748
2047
|
groupBy(...fields) {
|
|
1749
2048
|
const combined = this.#combinedFilterExpr();
|
|
@@ -1763,15 +2062,16 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1763
2062
|
modelName: this.#modelName,
|
|
1764
2063
|
data: record
|
|
1765
2064
|
});
|
|
1766
|
-
|
|
2065
|
+
const withDefaults = applyCreateDefaults(this.#contract.execution?.mutations.defaults, this.#storeName, record, createMutationDefaultsCache());
|
|
2066
|
+
assertEnumValues(this.#contract, this.#modelName, withDefaults);
|
|
2067
|
+
if (hasScalarFkFields(this.#contract, this.#modelName, withDefaults)) return await executeScalarCreateWithFkValidation({
|
|
1767
2068
|
executor: requireTransactionExecutor(this.#executor),
|
|
1768
2069
|
contract: this.#contract,
|
|
1769
2070
|
modelName: this.#modelName,
|
|
1770
|
-
data:
|
|
2071
|
+
data: withDefaults
|
|
1771
2072
|
});
|
|
1772
2073
|
const groupingKey = this.#newGroupingKey();
|
|
1773
2074
|
const meta = this.#planMeta(groupingKey);
|
|
1774
|
-
const withDefaults = applyCreateDefaults(this.#contract.execution?.mutations.defaults, this.#storeName, record, createMutationDefaultsCache());
|
|
1775
2075
|
const plan = {
|
|
1776
2076
|
meta,
|
|
1777
2077
|
ast: {
|
|
@@ -1885,7 +2185,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1885
2185
|
return (await this.updateAll(patch).toArray()).length;
|
|
1886
2186
|
}
|
|
1887
2187
|
async upsert(args) {
|
|
1888
|
-
const keyPath = getKeyPath
|
|
2188
|
+
const keyPath = getKeyPath(this.#contract, this.#modelName);
|
|
1889
2189
|
const whereExpr = shorthandToFilterExpr(args.where);
|
|
1890
2190
|
const matches = (row) => whereExpr === void 0 || evaluateFilter(whereExpr, row);
|
|
1891
2191
|
const meta = this.#planMeta(this.#newGroupingKey());
|
|
@@ -1895,8 +2195,16 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1895
2195
|
const executionDefaults = this.#contract.execution?.mutations.defaults;
|
|
1896
2196
|
const exec = requireTransactionExecutor(this.#executor);
|
|
1897
2197
|
const effectivePatch = applyUpdateDefaults(executionDefaults, storeName, patchRecord, createMutationDefaultsCache());
|
|
2198
|
+
const createWithDefaults = applyCreateDefaults(executionDefaults, storeName, createRecord, createMutationDefaultsCache());
|
|
2199
|
+
assertEnumValues(this.#contract, this.#modelName, effectivePatch);
|
|
2200
|
+
assertEnumValues(this.#contract, this.#modelName, createWithDefaults);
|
|
1898
2201
|
const { storeNames: onUpdateStoreNames } = collectOnUpdateEnforcementStoreNames(this.#contract, this.#modelName, effectivePatch);
|
|
1899
|
-
return withMutationScope(exec, [.../* @__PURE__ */ new Set([
|
|
2202
|
+
return withMutationScope(exec, [.../* @__PURE__ */ new Set([
|
|
2203
|
+
storeName,
|
|
2204
|
+
...onUpdateStoreNames,
|
|
2205
|
+
...collectScalarFkStoreNames(this.#contract, this.#modelName, createWithDefaults),
|
|
2206
|
+
...collectScalarFkStoreNames(this.#contract, this.#modelName, effectivePatch)
|
|
2207
|
+
])], async (scope) => {
|
|
1900
2208
|
const existing = (await scope.execute({
|
|
1901
2209
|
meta,
|
|
1902
2210
|
kind: "cursor-scan",
|
|
@@ -1905,15 +2213,16 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1905
2213
|
take: 1
|
|
1906
2214
|
}))[0];
|
|
1907
2215
|
if (existing === void 0) {
|
|
1908
|
-
|
|
2216
|
+
await validateScalarFks(scope, this.#contract, this.#modelName, createWithDefaults);
|
|
1909
2217
|
return (await scope.execute({
|
|
1910
2218
|
meta,
|
|
1911
2219
|
kind: "add",
|
|
1912
2220
|
storeName,
|
|
1913
|
-
record
|
|
1914
|
-
}))[0] ??
|
|
2221
|
+
record: createWithDefaults
|
|
2222
|
+
}))[0] ?? createWithDefaults;
|
|
1915
2223
|
}
|
|
1916
|
-
|
|
2224
|
+
await validateScalarFks(scope, this.#contract, this.#modelName, effectivePatch, existing);
|
|
2225
|
+
const key = extractKeyFromRow(existing, keyPath);
|
|
1917
2226
|
await applyReferentialActionsForRowOnUpdate(scope, this.#contract, this.#modelName, existing, effectivePatch);
|
|
1918
2227
|
return (await scope.execute({
|
|
1919
2228
|
meta,
|
|
@@ -1925,11 +2234,25 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1925
2234
|
});
|
|
1926
2235
|
}
|
|
1927
2236
|
createAll(data) {
|
|
1928
|
-
const groupingKey = this.#newGroupingKey();
|
|
1929
|
-
const meta = this.#planMeta(groupingKey);
|
|
1930
2237
|
const defaultsCache = createMutationDefaultsCache();
|
|
1931
2238
|
const executionDefaults = this.#contract.execution?.mutations.defaults;
|
|
1932
2239
|
const records = data.map((d) => applyCreateDefaults(executionDefaults, this.#storeName, d, defaultsCache));
|
|
2240
|
+
if (records.some((row) => hasScalarFkFields(this.#contract, this.#modelName, row))) {
|
|
2241
|
+
const executor = requireTransactionExecutor(this.#executor);
|
|
2242
|
+
const contract = this.#contract;
|
|
2243
|
+
const modelName = this.#modelName;
|
|
2244
|
+
return new AsyncIterableResult((async function* () {
|
|
2245
|
+
const rows = await executeScalarCreateAllWithFkValidation({
|
|
2246
|
+
executor,
|
|
2247
|
+
contract,
|
|
2248
|
+
modelName,
|
|
2249
|
+
data: records
|
|
2250
|
+
});
|
|
2251
|
+
for (const row of rows) yield row;
|
|
2252
|
+
})());
|
|
2253
|
+
}
|
|
2254
|
+
const groupingKey = this.#newGroupingKey();
|
|
2255
|
+
const meta = this.#planMeta(groupingKey);
|
|
1933
2256
|
const plan = {
|
|
1934
2257
|
meta,
|
|
1935
2258
|
ast: {
|
|
@@ -1950,7 +2273,10 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1950
2273
|
}
|
|
1951
2274
|
};
|
|
1952
2275
|
const executorQuery = this.#executor.query.bind(this.#executor);
|
|
2276
|
+
const contract = this.#contract;
|
|
2277
|
+
const modelName = this.#modelName;
|
|
1953
2278
|
return new AsyncIterableResult((async function* () {
|
|
2279
|
+
for (const record of records) assertEnumValues(contract, modelName, record);
|
|
1954
2280
|
for await (const row of executorQuery(plan)) yield row;
|
|
1955
2281
|
})());
|
|
1956
2282
|
}
|
|
@@ -1995,13 +2321,8 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1995
2321
|
const combined = this.#combinedFilterExpr();
|
|
1996
2322
|
const fieldToIndexMap = typeof IDBKeyRange !== "undefined" ? buildFieldToIndexMap(this.#contract, this.#storeName) : void 0;
|
|
1997
2323
|
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
|
-
}
|
|
2324
|
+
const orHint = extractIndexOrHint(combined, fieldToIndexMap, getKeyPath(this.#contract, this.#modelName));
|
|
2325
|
+
if (orHint !== null) return clampCount((await this.#executeOrRows(orHint, groupingKey, combined)).length, this.#state.skip, this.#state.take);
|
|
2005
2326
|
}
|
|
2006
2327
|
const scanPlan = this.#buildScanPlan(groupingKey, fieldToIndexMap);
|
|
2007
2328
|
const scanAst = scanPlan.ast;
|
|
@@ -2010,6 +2331,8 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
2010
2331
|
modelName: this.#modelName,
|
|
2011
2332
|
...scanAst?.kind === "findMany" && scanAst.where !== void 0 ? { where: scanAst.where } : {}
|
|
2012
2333
|
};
|
|
2334
|
+
const nativeTotal = await this.#executeNativeCount(scanPlan, ast);
|
|
2335
|
+
if (nativeTotal !== null) return clampCount(nativeTotal, this.#state.skip, this.#state.take);
|
|
2013
2336
|
const plan = {
|
|
2014
2337
|
...scanPlan,
|
|
2015
2338
|
ast
|
|
@@ -2046,6 +2369,25 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
2046
2369
|
throw new Error(`include('${relation}') refinement must return the collection (for where/orderBy/take/skip) or a count() selector`);
|
|
2047
2370
|
}
|
|
2048
2371
|
/**
|
|
2372
|
+
* Runs `scanPlan` as a native `count` plan when its cardinality is fully
|
|
2373
|
+
* determined by a key range (no in-memory filter) — returning the
|
|
2374
|
+
* *unpaginated* total — or `null` when it can't (the caller then falls back
|
|
2375
|
+
* to materializing). `ast` is attached so middleware sees the caller's
|
|
2376
|
+
* intent (`count` / `aggregate`) regardless of the physical plan.
|
|
2377
|
+
*/
|
|
2378
|
+
async #executeNativeCount(scanPlan, ast) {
|
|
2379
|
+
const body = scanPlan.idbPlan;
|
|
2380
|
+
if (body.kind !== "cursor-scan" || !isNativelyCountable(body)) return null;
|
|
2381
|
+
const nativePlan = {
|
|
2382
|
+
...scanPlan,
|
|
2383
|
+
ast,
|
|
2384
|
+
idbPlan: toCountPlan(body)
|
|
2385
|
+
};
|
|
2386
|
+
let total = 0;
|
|
2387
|
+
for await (const row of this.#executor.query(nativePlan)) total = row.count;
|
|
2388
|
+
return total;
|
|
2389
|
+
}
|
|
2390
|
+
/**
|
|
2049
2391
|
* Materialise all rows matching the accumulated filters with no pagination —
|
|
2050
2392
|
* used by `aggregate()` / `groupBy()`. The supplied `ast` is attached to the
|
|
2051
2393
|
* scan plan so middleware can observe the aggregate intent.
|
|
@@ -2081,7 +2423,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
2081
2423
|
...this.#state.take !== void 0 ? { take: this.#state.take } : {}
|
|
2082
2424
|
};
|
|
2083
2425
|
if (typeof IDBKeyRange !== "undefined") {
|
|
2084
|
-
const hint = extractIndexEqualityHint(combined, fieldToIndexMap ?? buildFieldToIndexMap(this.#contract, this.#storeName), getKeyPath
|
|
2426
|
+
const hint = extractIndexEqualityHint(combined, fieldToIndexMap ?? buildFieldToIndexMap(this.#contract, this.#storeName), getKeyPath(this.#contract, this.#modelName));
|
|
2085
2427
|
if (hint !== null) {
|
|
2086
2428
|
const { indexName, value, remainingFilter } = hint;
|
|
2087
2429
|
const filter = remainingFilter !== void 0 ? (row) => evaluateFilter(remainingFilter, row) : void 0;
|
|
@@ -2126,7 +2468,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
2126
2468
|
async #executeOrRows(hint, groupingKey, combined) {
|
|
2127
2469
|
const meta = this.#planMeta(groupingKey);
|
|
2128
2470
|
const storeName = this.#storeName;
|
|
2129
|
-
const keyPath = getKeyPath
|
|
2471
|
+
const keyPath = getKeyPath(this.#contract, this.#modelName);
|
|
2130
2472
|
const seen = /* @__PURE__ */ new Set();
|
|
2131
2473
|
const rows = [];
|
|
2132
2474
|
const ast = {
|
|
@@ -2151,7 +2493,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
2151
2493
|
return branchRows;
|
|
2152
2494
|
}));
|
|
2153
2495
|
for (const branchRows of branchResults) for (const row of branchRows) {
|
|
2154
|
-
const pk = row
|
|
2496
|
+
const pk = keyToken(extractKeyFromRow(row, keyPath));
|
|
2155
2497
|
if (!seen.has(pk)) {
|
|
2156
2498
|
seen.add(pk);
|
|
2157
2499
|
rows.push(row);
|
|
@@ -2240,6 +2582,6 @@ function idbOrm(options) {
|
|
|
2240
2582
|
return client;
|
|
2241
2583
|
}
|
|
2242
2584
|
//#endregion
|
|
2243
|
-
export { createRelationMutator as a, withMutationScope as c, hasNestedMutationCallbacks as i,
|
|
2585
|
+
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
2586
|
|
|
2245
|
-
//# sourceMappingURL=idb-orm-
|
|
2587
|
+
//# sourceMappingURL=idb-orm-C8yXJp6R.mjs.map
|