@prisma-idb/client-idb 0.9.0 → 0.10.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 +2 -0
- package/dist/client-auto.d.mts +3 -19
- package/dist/client-auto.d.mts.map +1 -1
- package/dist/client-auto.mjs +21 -16
- 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-MOH1S3ES.d.mts → executor-CevtdBfz.d.mts} +3 -396
- package/dist/executor-CevtdBfz.d.mts.map +1 -0
- package/dist/idb-orm-DsfllvzD.mjs +1256 -0
- package/dist/idb-orm-DsfllvzD.mjs.map +1 -0
- package/dist/idb-orm-gEKHfG4H.d.mts +399 -0
- package/dist/idb-orm-gEKHfG4H.d.mts.map +1 -0
- package/dist/internal.d.mts +39 -0
- package/dist/internal.d.mts.map +1 -0
- package/dist/internal.mjs +2 -0
- package/dist/{managed-client-CJD9lBx5.d.mts → managed-client-B_xGXdjz.d.mts} +3 -2
- package/dist/managed-client-B_xGXdjz.d.mts.map +1 -0
- package/dist/{managed-client-DofL09P8.mjs → managed-client-Cp01irrm.mjs} +3 -2
- package/dist/{managed-client-DofL09P8.mjs.map → managed-client-Cp01irrm.mjs.map} +1 -1
- package/dist/{idb-orm-C8yXJp6R.mjs → mutation-executor-CD5n_kKT.mjs} +41 -1361
- package/dist/mutation-executor-CD5n_kKT.mjs.map +1 -0
- package/dist/mutation-scope-pOn0QzvV.d.mts +33 -0
- package/dist/mutation-scope-pOn0QzvV.d.mts.map +1 -0
- package/dist/orm.d.mts +4 -75
- package/dist/orm.d.mts.map +1 -1
- package/dist/orm.mjs +3 -2
- package/dist/orm.mjs.map +1 -1
- package/package.json +7 -6
- package/dist/idb-orm-C8yXJp6R.mjs.map +0 -1
- package/dist/idb-orm-MOH1S3ES.d.mts.map +0 -1
- package/dist/managed-client-CJD9lBx5.d.mts.map +0 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { andExpr, evaluateFilter, fieldFilter, nullCheckExpr, shorthandToFilterExpr } from "@prisma-idb/adapter-idb/runtime";
|
|
1
|
+
import { evaluateFilter, shorthandToFilterExpr } from "@prisma-idb/adapter-idb/runtime";
|
|
3
2
|
import { domainModelsAtDefaultNamespace } from "@prisma/orm-framework/contract/types";
|
|
4
|
-
import {
|
|
3
|
+
import { fieldValueToken, fieldValuesEqual, isValidIdbKey, keyEquals, keyToken } from "@prisma-idb/target-idb/runtime";
|
|
5
4
|
import { generateId } from "@prisma/orm-framework/ids/runtime";
|
|
6
5
|
import { keyPathFields } from "@prisma-idb/target-idb/pack";
|
|
7
6
|
//#region src/core/types.ts
|
|
@@ -41,9 +40,9 @@ function extractKeyFromRow(row, keyPath) {
|
|
|
41
40
|
}
|
|
42
41
|
/**
|
|
43
42
|
* Build a field → indexName lookup for every single-field, non-multi-entry
|
|
44
|
-
* index on `storeName`.
|
|
45
|
-
*
|
|
46
|
-
*
|
|
43
|
+
* index on `storeName`. Top-level scans use this map to find indexed equality
|
|
44
|
+
* conditions in combined filter expressions. Compound and multi-entry indexes
|
|
45
|
+
* cannot serve these single-field point lookups.
|
|
47
46
|
*/
|
|
48
47
|
function buildFieldToIndexMap(contract, storeName) {
|
|
49
48
|
const storeDef = contract.storage.stores[storeName];
|
|
@@ -64,446 +63,6 @@ function getRelation(contract, modelName, relName) {
|
|
|
64
63
|
return relation;
|
|
65
64
|
}
|
|
66
65
|
//#endregion
|
|
67
|
-
//#region src/core/model-accessor.ts
|
|
68
|
-
/**
|
|
69
|
-
* Proxy-based typed accessor handed to `where(fn)` callbacks.
|
|
70
|
-
*
|
|
71
|
-
* Mirrors `createModelAccessor()` from
|
|
72
|
-
* Prisma ORM's SQL client, but without
|
|
73
|
-
* the codec-trait gating — IDB stores native JS values, so every operator
|
|
74
|
-
* is available on every field. The runtime accessor object is a Proxy
|
|
75
|
-
* that materialises an `IdbFieldAccessor` on demand for whichever field
|
|
76
|
-
* name is read; the type-level surface narrows that to the model's
|
|
77
|
-
* actual field set so callbacks get autocomplete.
|
|
78
|
-
*/
|
|
79
|
-
/**
|
|
80
|
-
* Build an {@link IdbFieldAccessor} for a single field. The accessor is
|
|
81
|
-
* shared via the proxy below, but each accessor instance is bound to its
|
|
82
|
-
* own field name so the produced AST nodes carry the right field.
|
|
83
|
-
*/
|
|
84
|
-
function createFieldAccessor(field) {
|
|
85
|
-
return {
|
|
86
|
-
eq: (value) => fieldFilter(field, "eq", value),
|
|
87
|
-
neq: (value) => fieldFilter(field, "neq", value),
|
|
88
|
-
gt: (value) => fieldFilter(field, "gt", value),
|
|
89
|
-
lt: (value) => fieldFilter(field, "lt", value),
|
|
90
|
-
gte: (value) => fieldFilter(field, "gte", value),
|
|
91
|
-
lte: (value) => fieldFilter(field, "lte", value),
|
|
92
|
-
in: (values) => fieldFilter(field, "in", values),
|
|
93
|
-
notIn: (values) => fieldFilter(field, "notIn", values),
|
|
94
|
-
contains: (sub) => fieldFilter(field, "contains", sub),
|
|
95
|
-
startsWith: (sub) => fieldFilter(field, "startsWith", sub),
|
|
96
|
-
endsWith: (sub) => fieldFilter(field, "endsWith", sub),
|
|
97
|
-
isNull: () => nullCheckExpr(field, true),
|
|
98
|
-
isNotNull: () => nullCheckExpr(field, false)
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Create the typed model accessor used by `where(fn)` callbacks.
|
|
103
|
-
*
|
|
104
|
-
* Implemented as a Proxy keyed on field names — every read returns an
|
|
105
|
-
* {@link IdbFieldAccessor} bound to that field. The accessor cache means
|
|
106
|
-
* `m.name.eq(...)` and `m.name.startsWith(...)` reuse the same per-field
|
|
107
|
-
* accessor object across the same `where()` invocation.
|
|
108
|
-
*
|
|
109
|
-
* The type parameter is structural — no contract is consulted at
|
|
110
|
-
* runtime, because TS already gates the visible field set at compile
|
|
111
|
-
* time. (Misspelled fields surface as accessor calls on names that
|
|
112
|
-
* don't exist in stored rows, which is consistent with how the
|
|
113
|
-
* shorthand form behaves today.)
|
|
114
|
-
*/
|
|
115
|
-
function createModelAccessor() {
|
|
116
|
-
const cache = /* @__PURE__ */ new Map();
|
|
117
|
-
return new Proxy({}, { get(_target, prop) {
|
|
118
|
-
if (typeof prop !== "string") return void 0;
|
|
119
|
-
let acc = cache.get(prop);
|
|
120
|
-
if (acc === void 0) {
|
|
121
|
-
acc = createFieldAccessor(prop);
|
|
122
|
-
cache.set(prop, acc);
|
|
123
|
-
}
|
|
124
|
-
return acc;
|
|
125
|
-
} });
|
|
126
|
-
}
|
|
127
|
-
//#endregion
|
|
128
|
-
//#region src/core/store-state.ts
|
|
129
|
-
/** Build an {@link IdbIncludeScalar} marker for a `count()` reducer. */
|
|
130
|
-
function createIncludeScalar(state) {
|
|
131
|
-
return {
|
|
132
|
-
kind: "includeScalar",
|
|
133
|
-
fn: "count",
|
|
134
|
-
state
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
/** Type guard: is `value` an {@link IdbIncludeScalar} marker? */
|
|
138
|
-
function isIncludeScalar(value) {
|
|
139
|
-
if (typeof value !== "object" || value === null) return false;
|
|
140
|
-
const candidate = value;
|
|
141
|
-
return candidate.kind === "includeScalar" && candidate.fn === "count";
|
|
142
|
-
}
|
|
143
|
-
/** Create a fresh, empty accessor state (no filters, no ordering, no includes). */
|
|
144
|
-
function emptyAccessorState() {
|
|
145
|
-
return {
|
|
146
|
-
filters: [],
|
|
147
|
-
includes: {}
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
/** Return a shallow-merged copy of `state` with the given overrides applied. */
|
|
151
|
-
function mergeAccessorState(state, overrides) {
|
|
152
|
-
return {
|
|
153
|
-
...state,
|
|
154
|
-
...overrides
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
//#endregion
|
|
158
|
-
//#region src/core/query-shaping.ts
|
|
159
|
-
/**
|
|
160
|
-
* Combine accumulated filter expressions with AND.
|
|
161
|
-
*
|
|
162
|
-
* Returns `undefined` when no filter is present so callers can skip building a
|
|
163
|
-
* row-filter closure. Shared by {@link IdbStoreAccessorImpl} (top-level scans)
|
|
164
|
-
* and the relation loader (refined `include()` child scans).
|
|
165
|
-
*/
|
|
166
|
-
function combineFilterExprs(filters) {
|
|
167
|
-
if (filters.length === 0) return void 0;
|
|
168
|
-
if (filters.length === 1) return filters[0];
|
|
169
|
-
return andExpr(filters);
|
|
170
|
-
}
|
|
171
|
-
/** Recursively flatten nested AND nodes into a single array of children. */
|
|
172
|
-
function flattenAnd(filter) {
|
|
173
|
-
if (filter.kind !== "and") return [filter];
|
|
174
|
-
return filter.exprs.flatMap(flattenAnd);
|
|
175
|
-
}
|
|
176
|
-
/** Fold the leftover AND children (after peeling an indexed condition) back into a single filter, or `undefined` if none remain. */
|
|
177
|
-
function foldRemainder(rest) {
|
|
178
|
-
if (rest.length === 0) return void 0;
|
|
179
|
-
if (rest.length === 1) return rest[0];
|
|
180
|
-
return andExpr(rest);
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* `IDBKeyRange.only()` throws `DataError` for values that aren't valid
|
|
184
|
-
* IndexedDB keys (`null`/`undefined`, booleans, `NaN`, `BigInt`, plain
|
|
185
|
-
* objects, or arrays containing any of those) — an `eq` condition on such a
|
|
186
|
-
* value can't be served by an index point-range scan and must fall back to
|
|
187
|
-
* a full scan. Delegates to the shared {@link isValidIdbKey} check.
|
|
188
|
-
*/
|
|
189
|
-
function isIndexableEqValue(value) {
|
|
190
|
-
return isValidIdbKey(value);
|
|
191
|
-
}
|
|
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
|
-
/**
|
|
208
|
-
* Scan the combined filter expression for the first `eq` field condition whose
|
|
209
|
-
* field either has a matching IDB index (`fieldToIndexName[field]` is set) or
|
|
210
|
-
* *is* the store's own primary key (`keyPath`) — a store's keyPath is always
|
|
211
|
-
* point-range-queryable directly, no named index required.
|
|
212
|
-
*
|
|
213
|
-
* When found, the indexed condition is peeled off and returned as an
|
|
214
|
-
* {@link IndexEqualityHint}; any remaining conditions are preserved in
|
|
215
|
-
* `remainingFilter`. Returns `null` when no usable index can be found.
|
|
216
|
-
*
|
|
217
|
-
* Nested AND nodes are flattened before scanning, so deeply-composed filters
|
|
218
|
-
* are handled correctly. Only `eq` conditions are considered — range operators
|
|
219
|
-
* (`gt`, `lt`, etc.) could also use an index range but that optimization is
|
|
220
|
-
* not yet implemented.
|
|
221
|
-
*/
|
|
222
|
-
function extractIndexEqualityHint(filter, fieldToIndexName, keyPath) {
|
|
223
|
-
if (filter === void 0) return null;
|
|
224
|
-
if (filter.kind === "field" && filter.op === "eq") {
|
|
225
|
-
const indexName = fieldToIndexName[filter.field];
|
|
226
|
-
if ((indexName !== void 0 || isPrimaryKeyField(filter.field, keyPath)) && isIndexableEqValue(filter.value)) return {
|
|
227
|
-
indexName,
|
|
228
|
-
value: filter.value,
|
|
229
|
-
remainingFilter: void 0
|
|
230
|
-
};
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
if (filter.kind === "and") {
|
|
234
|
-
const flat = flattenAnd(filter);
|
|
235
|
-
for (let i = 0; i < flat.length; i++) {
|
|
236
|
-
const expr = flat[i];
|
|
237
|
-
if (expr.kind === "field" && expr.op === "eq") {
|
|
238
|
-
const indexName = fieldToIndexName[expr.field];
|
|
239
|
-
if ((indexName !== void 0 || isPrimaryKeyField(expr.field, keyPath)) && isIndexableEqValue(expr.value)) {
|
|
240
|
-
const rest = flat.filter((_, j) => j !== i);
|
|
241
|
-
return {
|
|
242
|
-
indexName,
|
|
243
|
-
value: expr.value,
|
|
244
|
-
remainingFilter: foldRemainder(rest)
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Check whether every branch of an OR node is a single `field eq value`
|
|
254
|
-
* condition whose field has a matching index. Returns an {@link IndexOrHint}
|
|
255
|
-
* when the whole OR can be satisfied by N point-range cursor scans, `null`
|
|
256
|
-
* otherwise.
|
|
257
|
-
*/
|
|
258
|
-
function tryExtractOrBranches(orNode, fieldToIndexName, keyPath, remainingFilter) {
|
|
259
|
-
if (orNode.exprs.length === 0) return null;
|
|
260
|
-
const branches = [];
|
|
261
|
-
for (const expr of orNode.exprs) {
|
|
262
|
-
if (expr.kind !== "field" || expr.op !== "eq") return null;
|
|
263
|
-
const indexName = fieldToIndexName[expr.field];
|
|
264
|
-
if (indexName === void 0 && !isPrimaryKeyField(expr.field, keyPath) || !isIndexableEqValue(expr.value)) return null;
|
|
265
|
-
branches.push({
|
|
266
|
-
indexName,
|
|
267
|
-
value: expr.value
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
return {
|
|
271
|
-
branches,
|
|
272
|
-
remainingFilter
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Attempt to extract an {@link IndexOrHint} from `filter`.
|
|
277
|
-
*
|
|
278
|
-
* Handles two shapes:
|
|
279
|
-
* - Top-level `or` where every branch is `field eq value` on an indexed field.
|
|
280
|
-
* - Top-level `and` (with nested ANDs flattened) that contains exactly one `or`
|
|
281
|
-
* child fitting the above pattern; the remaining AND children become
|
|
282
|
-
* `remainingFilter` applied in-memory after the union.
|
|
283
|
-
*
|
|
284
|
-
* Returns `null` when the filter cannot be accelerated via OR index scans.
|
|
285
|
-
*/
|
|
286
|
-
function extractIndexOrHint(filter, fieldToIndexName, keyPath) {
|
|
287
|
-
if (filter === void 0) return null;
|
|
288
|
-
if (filter.kind === "or") return tryExtractOrBranches(filter, fieldToIndexName, keyPath, void 0);
|
|
289
|
-
if (filter.kind === "and") {
|
|
290
|
-
const flat = flattenAnd(filter);
|
|
291
|
-
const orIndices = [];
|
|
292
|
-
for (let i = 0; i < flat.length; i++) if (flat[i].kind === "or") orIndices.push(i);
|
|
293
|
-
if (orIndices.length !== 1) return null;
|
|
294
|
-
const orIdx = orIndices[0];
|
|
295
|
-
const orNode = flat[orIdx];
|
|
296
|
-
return tryExtractOrBranches(orNode, fieldToIndexName, keyPath, foldRemainder(flat.filter((_, j) => j !== orIdx)));
|
|
297
|
-
}
|
|
298
|
-
return null;
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Build an in-memory comparator from an `orderBy` spec (field → direction).
|
|
302
|
-
*
|
|
303
|
-
* Returns `undefined` when there is nothing to sort by. Compares fields in
|
|
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).
|
|
307
|
-
*/
|
|
308
|
-
function buildRowComparator(orderBy) {
|
|
309
|
-
if (orderBy === void 0) return void 0;
|
|
310
|
-
return (a, b) => {
|
|
311
|
-
for (const [field, dir] of Object.entries(orderBy)) {
|
|
312
|
-
const cmp = compareFieldValues(a[field], b[field]);
|
|
313
|
-
if (cmp === 0) continue;
|
|
314
|
-
return dir === "desc" ? -cmp : cmp;
|
|
315
|
-
}
|
|
316
|
-
return 0;
|
|
317
|
-
};
|
|
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
|
-
}
|
|
357
|
-
//#endregion
|
|
358
|
-
//#region src/core/aggregate-builder.ts
|
|
359
|
-
/**
|
|
360
|
-
* Build the aggregate selector factory handed to `.aggregate(agg => …)` and
|
|
361
|
-
* `groupBy(...).aggregate(agg => …)`.
|
|
362
|
-
*
|
|
363
|
-
* Each method returns a frozen {@link IdbAggregateSelector} marker; the actual
|
|
364
|
-
* reduction happens in {@link reduceAggregate} once the matching rows are
|
|
365
|
-
* materialised in memory. Mirrors `createAggregateBuilder` from the vendor
|
|
366
|
-
* `sql-orm-client/aggregate-builder.ts`, minus the field→column mapping (IDB
|
|
367
|
-
* stores native field names).
|
|
368
|
-
*/
|
|
369
|
-
function createAggregateBuilder() {
|
|
370
|
-
return {
|
|
371
|
-
count: () => ({
|
|
372
|
-
kind: "aggregate",
|
|
373
|
-
fn: "count"
|
|
374
|
-
}),
|
|
375
|
-
sum: (field) => ({
|
|
376
|
-
kind: "aggregate",
|
|
377
|
-
fn: "sum",
|
|
378
|
-
field
|
|
379
|
-
}),
|
|
380
|
-
avg: (field) => ({
|
|
381
|
-
kind: "aggregate",
|
|
382
|
-
fn: "avg",
|
|
383
|
-
field
|
|
384
|
-
}),
|
|
385
|
-
min: (field) => ({
|
|
386
|
-
kind: "aggregate",
|
|
387
|
-
fn: "min",
|
|
388
|
-
field
|
|
389
|
-
}),
|
|
390
|
-
max: (field) => ({
|
|
391
|
-
kind: "aggregate",
|
|
392
|
-
fn: "max",
|
|
393
|
-
field
|
|
394
|
-
})
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
/** Type guard: is `value` an {@link IdbAggregateSelector}? */
|
|
398
|
-
function isAggregateSelector(value) {
|
|
399
|
-
if (typeof value !== "object" || value === null) return false;
|
|
400
|
-
const candidate = value;
|
|
401
|
-
return candidate.kind === "aggregate" && (candidate.fn === "count" || candidate.fn === "sum" || candidate.fn === "avg" || candidate.fn === "min" || candidate.fn === "max");
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Reduce a set of materialised rows to a single aggregate value.
|
|
405
|
-
*
|
|
406
|
-
* - `count` → the number of rows (always a number, never null).
|
|
407
|
-
* - `sum`/`avg`/`min`/`max` → computed over the non-null numeric values of
|
|
408
|
-
* `field`; `null` when no row has a non-null value (matching Prisma's
|
|
409
|
-
* "aggregate of an empty set is null" and the vendor `coerceAggregateValue`
|
|
410
|
-
* null handling). String-encoded numbers are coerced via `Number()`.
|
|
411
|
-
*/
|
|
412
|
-
function reduceAggregate(fn, field, rows) {
|
|
413
|
-
if (fn === "count") return rows.length;
|
|
414
|
-
if (field === void 0) return null;
|
|
415
|
-
const values = [];
|
|
416
|
-
for (const row of rows) {
|
|
417
|
-
const raw = row[field];
|
|
418
|
-
if (raw === null || raw === void 0) continue;
|
|
419
|
-
const n = typeof raw === "bigint" ? Number(raw) : Number(raw);
|
|
420
|
-
if (!Number.isNaN(n)) values.push(n);
|
|
421
|
-
}
|
|
422
|
-
if (values.length === 0) return null;
|
|
423
|
-
switch (fn) {
|
|
424
|
-
case "sum": return values.reduce((acc, n) => acc + n, 0);
|
|
425
|
-
case "avg": return values.reduce((acc, n) => acc + n, 0) / values.length;
|
|
426
|
-
case "min": return Math.min(...values);
|
|
427
|
-
case "max": return Math.max(...values);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
/**
|
|
431
|
-
* Run every selector in an aggregate spec against `rows`, producing the
|
|
432
|
-
* result object keyed by the spec's aliases.
|
|
433
|
-
*/
|
|
434
|
-
function computeAggregateSpec(spec, rows) {
|
|
435
|
-
const result = {};
|
|
436
|
-
for (const [alias, selector] of Object.entries(spec)) result[alias] = reduceAggregate(selector.fn, selector.field, rows);
|
|
437
|
-
return result;
|
|
438
|
-
}
|
|
439
|
-
/**
|
|
440
|
-
* Project an aggregate spec down to the plain `{ fn, field? }` shape carried by
|
|
441
|
-
* the query AST (`IdbAggregateAst.aggregates` / `IdbGroupByAst.aggregates`), so
|
|
442
|
-
* middleware can observe the requested aggregations.
|
|
443
|
-
*/
|
|
444
|
-
function toAggregateRequests(spec) {
|
|
445
|
-
const out = {};
|
|
446
|
-
for (const [alias, selector] of Object.entries(spec)) out[alias] = selector.field !== void 0 ? {
|
|
447
|
-
fn: selector.fn,
|
|
448
|
-
field: selector.field
|
|
449
|
-
} : { fn: selector.fn };
|
|
450
|
-
return out;
|
|
451
|
-
}
|
|
452
|
-
/**
|
|
453
|
-
* Validate a user-supplied aggregate spec: it must be non-empty and every
|
|
454
|
-
* value must be a real selector. Throws a descriptive error otherwise, so a
|
|
455
|
-
* typo (e.g. returning a plain object) fails loudly rather than silently
|
|
456
|
-
* producing `NaN`/`undefined` results.
|
|
457
|
-
*/
|
|
458
|
-
function assertValidAggregateSpec(spec, context) {
|
|
459
|
-
const entries = Object.entries(spec);
|
|
460
|
-
if (entries.length === 0) throw new Error(`${context} requires at least one aggregation selector`);
|
|
461
|
-
for (const [alias, selector] of entries) if (!isAggregateSelector(selector)) throw new Error(`${context} selector "${alias}" is invalid`);
|
|
462
|
-
}
|
|
463
|
-
//#endregion
|
|
464
|
-
//#region src/core/grouped-accessor.ts
|
|
465
|
-
/**
|
|
466
|
-
* Build the composite group key for a row. Uses a JSON encoding of the ordered
|
|
467
|
-
* key-field values so multi-field groups and primitive value types (string,
|
|
468
|
-
* number, boolean, null) partition correctly.
|
|
469
|
-
*/
|
|
470
|
-
function groupKeyOf(by, row) {
|
|
471
|
-
return JSON.stringify(by.map((field) => row[field] ?? null));
|
|
472
|
-
}
|
|
473
|
-
function createGroupedAccessor(init) {
|
|
474
|
-
return { async aggregate(fn) {
|
|
475
|
-
const spec = fn(createAggregateBuilder());
|
|
476
|
-
assertValidAggregateSpec(spec, "groupBy().aggregate()");
|
|
477
|
-
const ast = {
|
|
478
|
-
kind: "groupBy",
|
|
479
|
-
modelName: init.modelName,
|
|
480
|
-
by: init.by,
|
|
481
|
-
aggregates: toAggregateRequests(spec),
|
|
482
|
-
...init.where !== void 0 ? { where: init.where } : {}
|
|
483
|
-
};
|
|
484
|
-
const rows = await init.materialize(ast);
|
|
485
|
-
const groups = /* @__PURE__ */ new Map();
|
|
486
|
-
for (const row of rows) {
|
|
487
|
-
const gk = groupKeyOf(init.by, row);
|
|
488
|
-
let group = groups.get(gk);
|
|
489
|
-
if (group === void 0) {
|
|
490
|
-
const key = {};
|
|
491
|
-
for (const field of init.by) key[field] = row[field];
|
|
492
|
-
group = {
|
|
493
|
-
key,
|
|
494
|
-
rows: []
|
|
495
|
-
};
|
|
496
|
-
groups.set(gk, group);
|
|
497
|
-
}
|
|
498
|
-
group.rows.push(row);
|
|
499
|
-
}
|
|
500
|
-
return Array.from(groups.values()).map((group) => ({
|
|
501
|
-
...group.key,
|
|
502
|
-
...computeAggregateSpec(spec, group.rows)
|
|
503
|
-
}));
|
|
504
|
-
} };
|
|
505
|
-
}
|
|
506
|
-
//#endregion
|
|
507
66
|
//#region src/core/enum-validation.ts
|
|
508
67
|
const rulesByContract = /* @__PURE__ */ new WeakMap();
|
|
509
68
|
function resolveEnumRule(contract, field) {
|
|
@@ -632,186 +191,6 @@ function applyUpdateDefaults(defaults, storeName, patch, cache) {
|
|
|
632
191
|
return result;
|
|
633
192
|
}
|
|
634
193
|
//#endregion
|
|
635
|
-
//#region src/core/relation-loader.ts
|
|
636
|
-
/**
|
|
637
|
-
* Batch-load a single named relation for all rows in `rows` and attach the
|
|
638
|
-
* result to each row under the `relName` key.
|
|
639
|
-
*
|
|
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.
|
|
645
|
-
*
|
|
646
|
-
* The `entry` carries any `include()` refinement:
|
|
647
|
-
*
|
|
648
|
-
* - `collection` — the refined `where` further filters the child scan;
|
|
649
|
-
* `orderBy` / `skip` / `take` are applied **per parent group** for `1:N`
|
|
650
|
-
* relations (each parent's children are independently sorted and paginated).
|
|
651
|
-
* - `scalar` — the relation field becomes the `count` of matching children
|
|
652
|
-
* (to-many only; `include()` rejects scalar refinements on to-one relations).
|
|
653
|
-
*
|
|
654
|
-
* @param relName - The relation key to load (e.g. `"posts"`, `"author"`).
|
|
655
|
-
* @param entry - How to materialise the relation (collection vs scalar + refinement state).
|
|
656
|
-
* @param rows - The parent rows to attach related data to.
|
|
657
|
-
* @param contract - The resolved IDB contract.
|
|
658
|
-
* @param modelName - The source model name (owner of the relation).
|
|
659
|
-
* @param executor - The query executor used to run the related-store scan.
|
|
660
|
-
*/
|
|
661
|
-
async function loadRelation(relName, entry, rows, contract, modelName, executor, groupingKey) {
|
|
662
|
-
if (rows.length === 0) return rows;
|
|
663
|
-
const models = domainModelsAtDefaultNamespace(contract.domain);
|
|
664
|
-
const model = models[modelName];
|
|
665
|
-
if (model === void 0) return rows;
|
|
666
|
-
const rawRelation = model.relations[relName];
|
|
667
|
-
if (rawRelation === void 0) return rows;
|
|
668
|
-
if (!("on" in rawRelation)) return rows;
|
|
669
|
-
const relation = rawRelation;
|
|
670
|
-
const { cardinality, on } = relation;
|
|
671
|
-
const relatedModelName = relation.to.model;
|
|
672
|
-
const localFields = on.localFields;
|
|
673
|
-
const targetFields = on.targetFields;
|
|
674
|
-
if (localFields.length === 0 || localFields.length !== targetFields.length) return rows;
|
|
675
|
-
const relatedModel = models[relatedModelName];
|
|
676
|
-
if (relatedModel === void 0) return rows;
|
|
677
|
-
const relatedStoreName = typeof relatedModel.storage === "object" && relatedModel.storage !== null && "storeName" in relatedModel.storage ? String(relatedModel.storage["storeName"]) : relatedModelName;
|
|
678
|
-
const isScalar = entry.kind === "scalar";
|
|
679
|
-
const localTuples = /* @__PURE__ */ new Map();
|
|
680
|
-
for (const row of rows) {
|
|
681
|
-
const token = tupleToken(row, localFields);
|
|
682
|
-
if (token !== null) localTuples.set(token, localFields.map((f) => row[f]));
|
|
683
|
-
}
|
|
684
|
-
if (localTuples.size === 0) return rows.map((row) => ({
|
|
685
|
-
...row,
|
|
686
|
-
[relName]: isScalar ? 0 : cardinality === "1:N" ? [] : null
|
|
687
|
-
}));
|
|
688
|
-
const refinedWhere = combineFilterExprs(entry.state.filters);
|
|
689
|
-
const planMeta = {
|
|
690
|
-
target: "idb",
|
|
691
|
-
storageHash: contract.storage.storageHash,
|
|
692
|
-
lane: "idb-orm",
|
|
693
|
-
annotations: { groupingKey }
|
|
694
|
-
};
|
|
695
|
-
const rangeSource = findRangeSource(contract, relatedStoreName, relatedModelName, targetFields);
|
|
696
|
-
let relatedRows;
|
|
697
|
-
if (rangeSource !== void 0) {
|
|
698
|
-
const refinedFilter = refinedWhere !== void 0 ? (row) => evaluateFilter(refinedWhere, row) : void 0;
|
|
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) => {
|
|
706
|
-
const plan = {
|
|
707
|
-
meta: planMeta,
|
|
708
|
-
idbPlan: {
|
|
709
|
-
meta: planMeta,
|
|
710
|
-
kind: "cursor-scan",
|
|
711
|
-
storeName: relatedStoreName,
|
|
712
|
-
...rangeSource.indexName !== void 0 ? { indexName: rangeSource.indexName } : {},
|
|
713
|
-
range,
|
|
714
|
-
...refinedFilter !== void 0 ? { filter: refinedFilter } : {}
|
|
715
|
-
}
|
|
716
|
-
};
|
|
717
|
-
const rows = [];
|
|
718
|
-
for await (const row of executor.query(plan)) rows.push(row);
|
|
719
|
-
return rows;
|
|
720
|
-
}))).flat();
|
|
721
|
-
} else {
|
|
722
|
-
relatedRows = [];
|
|
723
|
-
const filter = (row) => {
|
|
724
|
-
const token = tupleToken(row, targetFields);
|
|
725
|
-
return token !== null && localTuples.has(token) && (refinedWhere === void 0 || evaluateFilter(refinedWhere, row));
|
|
726
|
-
};
|
|
727
|
-
const plan = {
|
|
728
|
-
meta: planMeta,
|
|
729
|
-
idbPlan: {
|
|
730
|
-
meta: planMeta,
|
|
731
|
-
kind: "cursor-scan",
|
|
732
|
-
storeName: relatedStoreName,
|
|
733
|
-
filter
|
|
734
|
-
}
|
|
735
|
-
};
|
|
736
|
-
for await (const row of executor.query(plan)) relatedRows.push(row);
|
|
737
|
-
}
|
|
738
|
-
if (cardinality === "1:N") {
|
|
739
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
740
|
-
for (const rrow of relatedRows) {
|
|
741
|
-
const gk = tupleToken(rrow, targetFields);
|
|
742
|
-
if (gk === null) continue;
|
|
743
|
-
const group = grouped.get(gk) ?? [];
|
|
744
|
-
group.push(rrow);
|
|
745
|
-
grouped.set(gk, group);
|
|
746
|
-
}
|
|
747
|
-
const groupFor = (row) => {
|
|
748
|
-
const token = tupleToken(row, localFields);
|
|
749
|
-
return token === null ? [] : grouped.get(token) ?? [];
|
|
750
|
-
};
|
|
751
|
-
if (isScalar) return rows.map((row) => ({
|
|
752
|
-
...row,
|
|
753
|
-
[relName]: groupFor(row).length
|
|
754
|
-
}));
|
|
755
|
-
const comparator = buildRowComparator(entry.state.orderBy);
|
|
756
|
-
const skip = entry.state.skip ?? 0;
|
|
757
|
-
const take = entry.state.take;
|
|
758
|
-
return rows.map((row) => {
|
|
759
|
-
let group = groupFor(row);
|
|
760
|
-
if (comparator !== void 0) group = [...group].sort(comparator);
|
|
761
|
-
if (skip > 0 || take !== void 0) group = group.slice(skip, take !== void 0 ? skip + take : void 0);
|
|
762
|
-
return {
|
|
763
|
-
...row,
|
|
764
|
-
[relName]: group
|
|
765
|
-
};
|
|
766
|
-
});
|
|
767
|
-
}
|
|
768
|
-
const indexed = /* @__PURE__ */ new Map();
|
|
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;
|
|
813
|
-
}
|
|
814
|
-
//#endregion
|
|
815
194
|
//#region src/core/mutation-scope.ts
|
|
816
195
|
/**
|
|
817
196
|
* Run a callback inside a single multi-store IDB readwrite transaction.
|
|
@@ -980,7 +359,7 @@ async function createGraph(scope, contract, modelName, input, defaultsCache) {
|
|
|
980
359
|
const scalarData = { ...parsed.scalarData };
|
|
981
360
|
for (const item of parentOwned) {
|
|
982
361
|
if (item.mutation.kind === "disconnect") throw new Error("disconnect() is only supported in update() nested mutations");
|
|
983
|
-
await applyParentOwnedMutation(scope, contract,
|
|
362
|
+
await applyParentOwnedMutation(scope, contract, scalarData, item.relation, item.mutation, defaultsCache);
|
|
984
363
|
}
|
|
985
364
|
const parentRow = await insertSingleRow(scope, contract, modelName, scalarData, defaultsCache);
|
|
986
365
|
for (const item of childOwned) {
|
|
@@ -995,7 +374,7 @@ async function updateFirstGraph(scope, contract, modelName, filters, input, defa
|
|
|
995
374
|
const parsed = parseMutationInput(contract, modelName, input);
|
|
996
375
|
const { parentOwned, childOwned } = partitionByOwnership(parsed.relationMutations);
|
|
997
376
|
const scalarData = { ...parsed.scalarData };
|
|
998
|
-
for (const item of parentOwned) await applyParentOwnedMutation(scope, contract,
|
|
377
|
+
for (const item of parentOwned) await applyParentOwnedMutation(scope, contract, scalarData, item.relation, item.mutation, defaultsCache);
|
|
999
378
|
let parentRow = existingRow;
|
|
1000
379
|
if (Object.keys(scalarData).length > 0) {
|
|
1001
380
|
const storeName = getStoreName(contract, modelName);
|
|
@@ -1056,7 +435,7 @@ function partitionByOwnership(mutations) {
|
|
|
1056
435
|
childOwned
|
|
1057
436
|
};
|
|
1058
437
|
}
|
|
1059
|
-
async function applyParentOwnedMutation(scope, contract,
|
|
438
|
+
async function applyParentOwnedMutation(scope, contract, scalarData, relation, mutation, defaultsCache) {
|
|
1060
439
|
if (mutation.kind === "disconnect") {
|
|
1061
440
|
for (const localField of relation.localFields) scalarData[localField] = null;
|
|
1062
441
|
return;
|
|
@@ -1064,16 +443,16 @@ async function applyParentOwnedMutation(scope, contract, parentModelName, scalar
|
|
|
1064
443
|
if (mutation.kind === "create") {
|
|
1065
444
|
const row = mutation.data[0];
|
|
1066
445
|
if (!row) throw new Error(`create() nested mutation for relation "${relation.relationName}" requires data`);
|
|
1067
|
-
copyRelatedValuesToParent(relation, scalarData, await insertSingleRow(scope, contract, relation.relatedModelName, row, defaultsCache)
|
|
446
|
+
copyRelatedValuesToParent(relation, scalarData, await insertSingleRow(scope, contract, relation.relatedModelName, row, defaultsCache));
|
|
1068
447
|
return;
|
|
1069
448
|
}
|
|
1070
449
|
const criterion = mutation.criteria[0];
|
|
1071
450
|
if (!criterion) throw new Error(`connect() nested mutation for relation "${relation.relationName}" requires a criterion`);
|
|
1072
451
|
const relatedRow = await findRowByCriterion(scope, contract, relation.relatedModelName, criterion);
|
|
1073
452
|
if (!relatedRow) throw new Error(`connect() nested mutation for relation "${relation.relationName}" did not find a matching row`);
|
|
1074
|
-
copyRelatedValuesToParent(relation, scalarData, relatedRow
|
|
453
|
+
copyRelatedValuesToParent(relation, scalarData, relatedRow);
|
|
1075
454
|
}
|
|
1076
|
-
function copyRelatedValuesToParent(relation, scalarData, relatedRow
|
|
455
|
+
function copyRelatedValuesToParent(relation, scalarData, relatedRow) {
|
|
1077
456
|
for (let i = 0; i < relation.localFields.length; i++) {
|
|
1078
457
|
const localField = relation.localFields[i];
|
|
1079
458
|
const targetField = relation.targetFields[i];
|
|
@@ -1262,13 +641,7 @@ function findDeclaredAction(contract, modelName, def, kind) {
|
|
|
1262
641
|
function enforcedAction(declared) {
|
|
1263
642
|
return declared === void 0 || declared === "noAction" ? "restrict" : declared;
|
|
1264
643
|
}
|
|
1265
|
-
function
|
|
1266
|
-
return getReferentialActionForRelation(contract, modelName, def, "onDelete");
|
|
1267
|
-
}
|
|
1268
|
-
function getOnUpdateForRelation(contract, modelName, def) {
|
|
1269
|
-
return getReferentialActionForRelation(contract, modelName, def, "onUpdate");
|
|
1270
|
-
}
|
|
1271
|
-
function isDeleteEnforcementRelation(contract, modelName, def) {
|
|
644
|
+
function isChildEnforcementRelation(contract, modelName, def) {
|
|
1272
645
|
if (def.cardinality === "1:N") return true;
|
|
1273
646
|
if (def.cardinality === "1:1") {
|
|
1274
647
|
const keyPath = getKeyPath(contract, modelName);
|
|
@@ -1418,17 +791,17 @@ function collectOnUpdateEnforcementStoreNames(contract, modelName, data) {
|
|
|
1418
791
|
if (visitedModels.has(mName)) return;
|
|
1419
792
|
visitedModels.add(mName);
|
|
1420
793
|
for (const def of getRelationDefinitions(contract, mName)) {
|
|
1421
|
-
if (!
|
|
1422
|
-
const action =
|
|
794
|
+
if (!isChildEnforcementRelation(contract, mName, def)) continue;
|
|
795
|
+
const action = getReferentialActionForRelation(contract, mName, def, "onUpdate");
|
|
1423
796
|
stores.add(def.relatedStoreName);
|
|
1424
797
|
if (action === "cascade") walkCascadeChain(def.relatedModelName);
|
|
1425
798
|
}
|
|
1426
799
|
}
|
|
1427
800
|
visitedModels.add(modelName);
|
|
1428
801
|
for (const def of getRelationDefinitions(contract, modelName)) {
|
|
1429
|
-
if (!
|
|
802
|
+
if (!isChildEnforcementRelation(contract, modelName, def)) continue;
|
|
1430
803
|
if (!def.localFields.some((f) => f in data)) continue;
|
|
1431
|
-
const action =
|
|
804
|
+
const action = getReferentialActionForRelation(contract, modelName, def, "onUpdate");
|
|
1432
805
|
enforces = true;
|
|
1433
806
|
stores.add(def.relatedStoreName);
|
|
1434
807
|
if (action === "cascade") walkCascadeChain(def.relatedModelName);
|
|
@@ -1455,11 +828,8 @@ function collectOnUpdateEnforcementStoreNames(contract, modelName, data) {
|
|
|
1455
828
|
* `setNull`/`setDefault` are leaf actions: the child's FK field changes but
|
|
1456
829
|
* its own key doesn't, so nothing below it needs re-enforcement.
|
|
1457
830
|
*
|
|
1458
|
-
*
|
|
1459
|
-
*
|
|
1460
|
-
* from the same `oldRow`/`patch`, so there's no risk of assembling a value
|
|
1461
|
-
* from two unrelated rows the way independently-validated FK-existence
|
|
1462
|
-
* checks could.
|
|
831
|
+
* Compound relations use values from the same `oldRow`/`patch`, so every
|
|
832
|
+
* child is matched against the full pre-change tuple.
|
|
1463
833
|
*/
|
|
1464
834
|
async function applyReferentialActionsForRowOnUpdate(scope, contract, modelName, oldRow, patch, visited = /* @__PURE__ */ new Set()) {
|
|
1465
835
|
const keyPath = getKeyPath(contract, modelName);
|
|
@@ -1468,10 +838,10 @@ async function applyReferentialActionsForRowOnUpdate(scope, contract, modelName,
|
|
|
1468
838
|
visited.add(rowKey);
|
|
1469
839
|
const meta = makePlanMeta(contract);
|
|
1470
840
|
for (const def of getRelationDefinitions(contract, modelName)) {
|
|
1471
|
-
if (!
|
|
841
|
+
if (!isChildEnforcementRelation(contract, modelName, def)) continue;
|
|
1472
842
|
const changedFields = def.localFields.filter((f) => f in patch && !fieldValuesEqual(patch[f], oldRow[f]));
|
|
1473
843
|
if (changedFields.length === 0) continue;
|
|
1474
|
-
const action =
|
|
844
|
+
const action = getReferentialActionForRelation(contract, modelName, def, "onUpdate");
|
|
1475
845
|
const childFilter = buildChildFilterFromRow(def, oldRow);
|
|
1476
846
|
if (action === "restrict") {
|
|
1477
847
|
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.`);
|
|
@@ -1650,8 +1020,8 @@ async function executeScalarCreateAllWithFkValidation(options) {
|
|
|
1650
1020
|
* Combines FK-existence store scope (this model's own N:1 relations) with
|
|
1651
1021
|
* `onUpdate` enforcement store scope (this model's child relations) for a
|
|
1652
1022
|
* single write. Returns the union alongside whether `onUpdate` enforcement
|
|
1653
|
-
* actually applies
|
|
1654
|
-
*
|
|
1023
|
+
* actually applies, including self-relations that touch only this model's
|
|
1024
|
+
* own store. Callers use that flag to choose between the existing
|
|
1655
1025
|
* fast blind-write path and the read-before-write enforcement path.
|
|
1656
1026
|
*/
|
|
1657
1027
|
function collectUpdateStoreNames(contract, modelName, data) {
|
|
@@ -1662,68 +1032,25 @@ function collectUpdateStoreNames(contract, modelName, data) {
|
|
|
1662
1032
|
enforcesOnUpdate: enforces
|
|
1663
1033
|
};
|
|
1664
1034
|
}
|
|
1035
|
+
/** Updates the first matching row with FK validation and referential actions. */
|
|
1665
1036
|
async function executeScalarUpdateWithFkValidation(options) {
|
|
1666
|
-
|
|
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);
|
|
1672
|
-
return withMutationScope(executor, storeNames, async (scope) => {
|
|
1673
|
-
if (!needsRowForFks) await validateScalarFks(scope, contract, modelName, patch);
|
|
1674
|
-
const meta = makePlanMeta(contract);
|
|
1675
|
-
const combined = filters.length === 0 ? void 0 : filters.length === 1 ? filters[0] : {
|
|
1676
|
-
kind: "and",
|
|
1677
|
-
exprs: filters
|
|
1678
|
-
};
|
|
1679
|
-
const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
|
|
1680
|
-
if (!enforcesOnUpdate && !needsRowForFks) return (await scope.execute({
|
|
1681
|
-
meta,
|
|
1682
|
-
kind: "scan-write",
|
|
1683
|
-
storeName,
|
|
1684
|
-
write: "put-merged",
|
|
1685
|
-
patch,
|
|
1686
|
-
take: 1,
|
|
1687
|
-
...filter !== void 0 ? { filter } : {}
|
|
1688
|
-
}))[0] ?? null;
|
|
1689
|
-
const oldRow = (await scope.execute({
|
|
1690
|
-
meta,
|
|
1691
|
-
kind: "cursor-scan",
|
|
1692
|
-
storeName,
|
|
1693
|
-
take: 1,
|
|
1694
|
-
...filter !== void 0 ? { filter } : {}
|
|
1695
|
-
}))[0];
|
|
1696
|
-
if (!oldRow) return null;
|
|
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));
|
|
1700
|
-
return (await scope.execute({
|
|
1701
|
-
meta,
|
|
1702
|
-
kind: "update",
|
|
1703
|
-
storeName,
|
|
1704
|
-
key,
|
|
1705
|
-
patch
|
|
1706
|
-
}))[0] ?? null;
|
|
1707
|
-
});
|
|
1037
|
+
return (await executeScalarUpdates(options, 1))[0] ?? null;
|
|
1708
1038
|
}
|
|
1709
1039
|
/**
|
|
1710
1040
|
* Bulk counterpart to {@link executeScalarUpdateWithFkValidation}: same FK
|
|
1711
1041
|
* validation and `onUpdate` enforcement, but applies the patch to every row
|
|
1712
1042
|
* the filter matches (no `take: 1`) and returns all of them.
|
|
1713
1043
|
*
|
|
1714
|
-
*
|
|
1715
|
-
*
|
|
1716
|
-
*
|
|
1717
|
-
*
|
|
1718
|
-
* transaction is exactly what the sync interceptor needs to track the write
|
|
1719
|
-
* correctly: its transaction-scope hook (`SyncInterceptingTransactionScope#maybeTrack`'s
|
|
1720
|
-
* `scan-write`/`update` cases) writes one outbox event per row it's handed,
|
|
1721
|
-
* atomically with the write itself. The plan-level path `update()` falls
|
|
1722
|
-
* back to for a non-enforced single row has no equivalent hook — a plan is
|
|
1723
|
-
* extended with outbox ops BEFORE it runs, which only works when the
|
|
1724
|
-
* affected key is knowable up front.
|
|
1044
|
+
* The transaction scope lets the sync interceptor observe the affected rows
|
|
1045
|
+
* and write one outbox event per row in the same transaction. A bulk write's
|
|
1046
|
+
* affected keys are only known after it runs, so tracking cannot happen by
|
|
1047
|
+
* extending the plan before execution.
|
|
1725
1048
|
*/
|
|
1726
1049
|
async function executeBulkUpdateWithFkValidation(options) {
|
|
1050
|
+
return executeScalarUpdates(options);
|
|
1051
|
+
}
|
|
1052
|
+
/** Shares validation and atomic writes; only single-row updates limit the scan. */
|
|
1053
|
+
async function executeScalarUpdates(options, take) {
|
|
1727
1054
|
const { executor, contract, modelName, filters, data } = options;
|
|
1728
1055
|
const storeName = getStoreName(contract, modelName);
|
|
1729
1056
|
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, storeName, data, createMutationDefaultsCache());
|
|
@@ -1744,12 +1071,14 @@ async function executeBulkUpdateWithFkValidation(options) {
|
|
|
1744
1071
|
storeName,
|
|
1745
1072
|
write: "put-merged",
|
|
1746
1073
|
patch,
|
|
1074
|
+
...take !== void 0 ? { take } : {},
|
|
1747
1075
|
...filter !== void 0 ? { filter } : {}
|
|
1748
1076
|
});
|
|
1749
1077
|
const oldRows = await scope.execute({
|
|
1750
1078
|
meta,
|
|
1751
1079
|
kind: "cursor-scan",
|
|
1752
1080
|
storeName,
|
|
1081
|
+
...take !== void 0 ? { take } : {},
|
|
1753
1082
|
...filter !== void 0 ? { filter } : {}
|
|
1754
1083
|
});
|
|
1755
1084
|
const keyPath = getKeyPath(contract, modelName);
|
|
@@ -1776,7 +1105,7 @@ async function executeBulkUpdateWithFkValidation(options) {
|
|
|
1776
1105
|
* behaves like `restrict`, which is also the default.
|
|
1777
1106
|
*/
|
|
1778
1107
|
function hasEnforceableChildRelations(contract, modelName) {
|
|
1779
|
-
return getRelationDefinitions(contract, modelName).some((def) =>
|
|
1108
|
+
return getRelationDefinitions(contract, modelName).some((def) => isChildEnforcementRelation(contract, modelName, def));
|
|
1780
1109
|
}
|
|
1781
1110
|
/**
|
|
1782
1111
|
* Transitively walks the `onDelete` cascade graph from `modelName`, collecting
|
|
@@ -1797,8 +1126,8 @@ function collectDeleteStoreNames(contract, modelName) {
|
|
|
1797
1126
|
visitedModels.add(mName);
|
|
1798
1127
|
stores.add(getStoreName(contract, mName));
|
|
1799
1128
|
for (const def of getRelationDefinitions(contract, mName)) {
|
|
1800
|
-
if (!
|
|
1801
|
-
const action =
|
|
1129
|
+
if (!isChildEnforcementRelation(contract, mName, def)) continue;
|
|
1130
|
+
const action = getReferentialActionForRelation(contract, mName, def, "onDelete");
|
|
1802
1131
|
stores.add(def.relatedStoreName);
|
|
1803
1132
|
if (action === "cascade") walk(def.relatedModelName);
|
|
1804
1133
|
}
|
|
@@ -1831,8 +1160,8 @@ async function applyReferentialActionsForRow(scope, contract, modelName, row, vi
|
|
|
1831
1160
|
visited.add(rowKey);
|
|
1832
1161
|
const meta = makePlanMeta(contract);
|
|
1833
1162
|
for (const def of getRelationDefinitions(contract, modelName)) {
|
|
1834
|
-
if (!
|
|
1835
|
-
const action =
|
|
1163
|
+
if (!isChildEnforcementRelation(contract, modelName, def)) continue;
|
|
1164
|
+
const action = getReferentialActionForRelation(contract, modelName, def, "onDelete");
|
|
1836
1165
|
const childFilter = buildChildFilterFromRow(def, row);
|
|
1837
1166
|
if (action === "restrict") {
|
|
1838
1167
|
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.`);
|
|
@@ -1933,655 +1262,6 @@ async function executeDeleteAllWithReferentialActions(options) {
|
|
|
1933
1262
|
});
|
|
1934
1263
|
}
|
|
1935
1264
|
//#endregion
|
|
1936
|
-
|
|
1937
|
-
/**
|
|
1938
|
-
* Concrete immutable query builder.
|
|
1939
|
-
*
|
|
1940
|
-
* Internal details:
|
|
1941
|
-
* - All state is in `#state` (filters, orderBy, skip, take, includes, selectedFields).
|
|
1942
|
-
* - Builder methods clone via `#clone()` — O(1) copies since state is
|
|
1943
|
-
* structurally shared.
|
|
1944
|
-
* - `all()` materialises the main rows first, then batch-loads each included
|
|
1945
|
-
* relation, then applies any `.select()` projection before yielding.
|
|
1946
|
-
* - `#includeRefinementMode` flips `count()` from an async terminal to an
|
|
1947
|
-
* {@link IdbIncludeScalar} marker so it can be used inside `include()`.
|
|
1948
|
-
*/
|
|
1949
|
-
var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
1950
|
-
#contract;
|
|
1951
|
-
#modelName;
|
|
1952
|
-
#executor;
|
|
1953
|
-
#storeName;
|
|
1954
|
-
#state;
|
|
1955
|
-
#newGroupingKey;
|
|
1956
|
-
#includeRefinementMode;
|
|
1957
|
-
constructor(contract, modelName, executor, state, newGroupingKey, includeRefinementMode = false) {
|
|
1958
|
-
this.#contract = contract;
|
|
1959
|
-
this.#modelName = modelName;
|
|
1960
|
-
this.#executor = executor;
|
|
1961
|
-
this.#storeName = getStoreName(contract, modelName);
|
|
1962
|
-
this.#state = state ?? emptyAccessorState();
|
|
1963
|
-
let _key = 0;
|
|
1964
|
-
this.#newGroupingKey = newGroupingKey ?? (() => `idb-op-${++_key}`);
|
|
1965
|
-
this.#includeRefinementMode = includeRefinementMode;
|
|
1966
|
-
}
|
|
1967
|
-
where(filter) {
|
|
1968
|
-
const expr = typeof filter === "function" ? filter(createModelAccessor()) : shorthandToFilterExpr(filter);
|
|
1969
|
-
if (expr === void 0) return this.#clone({});
|
|
1970
|
-
return this.#clone({ filters: [...this.#state.filters, expr] });
|
|
1971
|
-
}
|
|
1972
|
-
orderBy(spec) {
|
|
1973
|
-
return this.#clone({ orderBy: spec });
|
|
1974
|
-
}
|
|
1975
|
-
take(n) {
|
|
1976
|
-
return this.#clone({ take: n });
|
|
1977
|
-
}
|
|
1978
|
-
skip(n) {
|
|
1979
|
-
return this.#clone({ skip: n });
|
|
1980
|
-
}
|
|
1981
|
-
include(relation, refineFn) {
|
|
1982
|
-
const entry = this.#resolveIncludeEntry(relation, refineFn);
|
|
1983
|
-
const newState = mergeAccessorState(this.#state, { includes: {
|
|
1984
|
-
...this.#state.includes,
|
|
1985
|
-
[relation]: entry
|
|
1986
|
-
} });
|
|
1987
|
-
return new IdbStoreAccessorImpl(this.#contract, this.#modelName, this.#executor, newState, this.#newGroupingKey, this.#includeRefinementMode);
|
|
1988
|
-
}
|
|
1989
|
-
select(...fields) {
|
|
1990
|
-
return this.#clone({ selectedFields: fields });
|
|
1991
|
-
}
|
|
1992
|
-
all() {
|
|
1993
|
-
const groupingKey = this.#newGroupingKey();
|
|
1994
|
-
const buildScanPlan = this.#buildScanPlan.bind(this);
|
|
1995
|
-
const executeOrRows = this.#executeOrRows.bind(this);
|
|
1996
|
-
const executorQuery = this.#executor.query.bind(this.#executor);
|
|
1997
|
-
const applyIncludes = this.#applyIncludes.bind(this);
|
|
1998
|
-
const projectRows = this.#projectRows.bind(this);
|
|
1999
|
-
const combined = this.#combinedFilterExpr();
|
|
2000
|
-
const fieldToIndexMap = typeof IDBKeyRange !== "undefined" ? buildFieldToIndexMap(this.#contract, this.#storeName) : void 0;
|
|
2001
|
-
const keyPath = getKeyPath(this.#contract, this.#modelName);
|
|
2002
|
-
const comparator = buildRowComparator(this.#state.orderBy);
|
|
2003
|
-
const skip = this.#state.skip;
|
|
2004
|
-
const take = this.#state.take;
|
|
2005
|
-
return new AsyncIterableResult((async function* () {
|
|
2006
|
-
let rows;
|
|
2007
|
-
const orHint = fieldToIndexMap !== void 0 ? extractIndexOrHint(combined, fieldToIndexMap, keyPath) : null;
|
|
2008
|
-
if (orHint !== null) {
|
|
2009
|
-
rows = await executeOrRows(orHint, groupingKey, combined);
|
|
2010
|
-
if (comparator !== void 0) rows.sort(comparator);
|
|
2011
|
-
if (skip !== void 0) rows = rows.slice(skip);
|
|
2012
|
-
if (take !== void 0) rows = rows.slice(0, take);
|
|
2013
|
-
} else {
|
|
2014
|
-
const scanPlan = buildScanPlan(groupingKey, fieldToIndexMap);
|
|
2015
|
-
rows = [];
|
|
2016
|
-
for await (const row of executorQuery(scanPlan)) rows.push(row);
|
|
2017
|
-
}
|
|
2018
|
-
const withIncludes = await applyIncludes(rows, groupingKey);
|
|
2019
|
-
for (const row of projectRows(withIncludes)) yield row;
|
|
2020
|
-
})());
|
|
2021
|
-
}
|
|
2022
|
-
async first() {
|
|
2023
|
-
return this.take(1).all().first();
|
|
2024
|
-
}
|
|
2025
|
-
async aggregate(fn) {
|
|
2026
|
-
const spec = fn(createAggregateBuilder());
|
|
2027
|
-
assertValidAggregateSpec(spec, "aggregate()");
|
|
2028
|
-
const combined = this.#combinedFilterExpr();
|
|
2029
|
-
const ast = {
|
|
2030
|
-
kind: "aggregate",
|
|
2031
|
-
modelName: this.#modelName,
|
|
2032
|
-
aggregates: toAggregateRequests(spec),
|
|
2033
|
-
...combined !== void 0 ? { where: combined } : {}
|
|
2034
|
-
};
|
|
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));
|
|
2046
|
-
}
|
|
2047
|
-
groupBy(...fields) {
|
|
2048
|
-
const combined = this.#combinedFilterExpr();
|
|
2049
|
-
const materialize = (ast) => this.#materialize(this.#newGroupingKey(), ast);
|
|
2050
|
-
return createGroupedAccessor({
|
|
2051
|
-
modelName: this.#modelName,
|
|
2052
|
-
by: fields,
|
|
2053
|
-
where: combined,
|
|
2054
|
-
materialize
|
|
2055
|
-
});
|
|
2056
|
-
}
|
|
2057
|
-
async create(data) {
|
|
2058
|
-
const record = data;
|
|
2059
|
-
if (hasNestedMutationCallbacks(this.#contract, this.#modelName, record)) return await executeNestedCreateMutation({
|
|
2060
|
-
executor: requireTransactionExecutor(this.#executor),
|
|
2061
|
-
contract: this.#contract,
|
|
2062
|
-
modelName: this.#modelName,
|
|
2063
|
-
data: record
|
|
2064
|
-
});
|
|
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({
|
|
2068
|
-
executor: requireTransactionExecutor(this.#executor),
|
|
2069
|
-
contract: this.#contract,
|
|
2070
|
-
modelName: this.#modelName,
|
|
2071
|
-
data: withDefaults
|
|
2072
|
-
});
|
|
2073
|
-
const groupingKey = this.#newGroupingKey();
|
|
2074
|
-
const meta = this.#planMeta(groupingKey);
|
|
2075
|
-
const plan = {
|
|
2076
|
-
meta,
|
|
2077
|
-
ast: {
|
|
2078
|
-
kind: "create",
|
|
2079
|
-
modelName: this.#modelName,
|
|
2080
|
-
data: withDefaults
|
|
2081
|
-
},
|
|
2082
|
-
idbPlan: {
|
|
2083
|
-
meta,
|
|
2084
|
-
kind: "add",
|
|
2085
|
-
storeName: this.#storeName,
|
|
2086
|
-
record: withDefaults
|
|
2087
|
-
}
|
|
2088
|
-
};
|
|
2089
|
-
for await (const row of this.#executor.query(plan)) return row;
|
|
2090
|
-
return withDefaults;
|
|
2091
|
-
}
|
|
2092
|
-
async findUnique(key) {
|
|
2093
|
-
const groupingKey = this.#newGroupingKey();
|
|
2094
|
-
const meta = this.#planMeta(groupingKey);
|
|
2095
|
-
const plan = {
|
|
2096
|
-
meta,
|
|
2097
|
-
ast: {
|
|
2098
|
-
kind: "findUnique",
|
|
2099
|
-
modelName: this.#modelName,
|
|
2100
|
-
key
|
|
2101
|
-
},
|
|
2102
|
-
idbPlan: {
|
|
2103
|
-
meta,
|
|
2104
|
-
kind: "key-get",
|
|
2105
|
-
storeName: this.#storeName,
|
|
2106
|
-
key
|
|
2107
|
-
}
|
|
2108
|
-
};
|
|
2109
|
-
for await (const row of this.#executor.query(plan)) return row;
|
|
2110
|
-
return null;
|
|
2111
|
-
}
|
|
2112
|
-
async delete(key) {
|
|
2113
|
-
if (hasEnforceableChildRelations(this.#contract, this.#modelName)) {
|
|
2114
|
-
await executeDeleteWithReferentialActions({
|
|
2115
|
-
executor: requireTransactionExecutor(this.#executor),
|
|
2116
|
-
contract: this.#contract,
|
|
2117
|
-
modelName: this.#modelName,
|
|
2118
|
-
key
|
|
2119
|
-
});
|
|
2120
|
-
return;
|
|
2121
|
-
}
|
|
2122
|
-
const groupingKey = this.#newGroupingKey();
|
|
2123
|
-
const meta = this.#planMeta(groupingKey);
|
|
2124
|
-
const plan = {
|
|
2125
|
-
meta,
|
|
2126
|
-
ast: {
|
|
2127
|
-
kind: "delete",
|
|
2128
|
-
modelName: this.#modelName,
|
|
2129
|
-
key
|
|
2130
|
-
},
|
|
2131
|
-
idbPlan: {
|
|
2132
|
-
meta,
|
|
2133
|
-
kind: "delete",
|
|
2134
|
-
storeName: this.#storeName,
|
|
2135
|
-
key
|
|
2136
|
-
}
|
|
2137
|
-
};
|
|
2138
|
-
await this.#executor.query(plan).toArray();
|
|
2139
|
-
}
|
|
2140
|
-
async update(patch) {
|
|
2141
|
-
const patchRecord = patch;
|
|
2142
|
-
if (hasNestedMutationCallbacks(this.#contract, this.#modelName, patchRecord)) return await executeNestedUpdateMutation({
|
|
2143
|
-
executor: requireTransactionExecutor(this.#executor),
|
|
2144
|
-
contract: this.#contract,
|
|
2145
|
-
modelName: this.#modelName,
|
|
2146
|
-
filters: this.#state.filters,
|
|
2147
|
-
data: patchRecord
|
|
2148
|
-
});
|
|
2149
|
-
return await executeScalarUpdateWithFkValidation({
|
|
2150
|
-
executor: requireTransactionExecutor(this.#executor),
|
|
2151
|
-
contract: this.#contract,
|
|
2152
|
-
modelName: this.#modelName,
|
|
2153
|
-
filters: this.#state.filters,
|
|
2154
|
-
data: patchRecord
|
|
2155
|
-
});
|
|
2156
|
-
}
|
|
2157
|
-
/**
|
|
2158
|
-
* Always routes through the transaction scope (`executeBulkUpdateWithFkValidation`),
|
|
2159
|
-
* unlike single-row `update()`'s plan-level fallback for the no-FK-field
|
|
2160
|
-
* case. A bulk scan-write's affected row set isn't knowable until it
|
|
2161
|
-
* actually runs — going through the transaction scope is what lets the
|
|
2162
|
-
* sync interceptor observe the real affected rows and write one outbox
|
|
2163
|
-
* event per row (see `SyncInterceptingTransactionScope#maybeTrack`'s
|
|
2164
|
-
* `scan-write` case), atomically with the write itself, instead of one
|
|
2165
|
-
* event for the whole batch with no way to recover which rows it covered.
|
|
2166
|
-
*/
|
|
2167
|
-
updateAll(patch) {
|
|
2168
|
-
const contract = this.#contract;
|
|
2169
|
-
const modelName = this.#modelName;
|
|
2170
|
-
const filters = this.#state.filters;
|
|
2171
|
-
const patchRecord = patch;
|
|
2172
|
-
const executor = requireTransactionExecutor(this.#executor);
|
|
2173
|
-
return new AsyncIterableResult((async function* () {
|
|
2174
|
-
const rows = await executeBulkUpdateWithFkValidation({
|
|
2175
|
-
executor,
|
|
2176
|
-
contract,
|
|
2177
|
-
modelName,
|
|
2178
|
-
filters,
|
|
2179
|
-
data: patchRecord
|
|
2180
|
-
});
|
|
2181
|
-
for (const row of rows) yield row;
|
|
2182
|
-
})());
|
|
2183
|
-
}
|
|
2184
|
-
async updateCount(patch) {
|
|
2185
|
-
return (await this.updateAll(patch).toArray()).length;
|
|
2186
|
-
}
|
|
2187
|
-
async upsert(args) {
|
|
2188
|
-
const keyPath = getKeyPath(this.#contract, this.#modelName);
|
|
2189
|
-
const whereExpr = shorthandToFilterExpr(args.where);
|
|
2190
|
-
const matches = (row) => whereExpr === void 0 || evaluateFilter(whereExpr, row);
|
|
2191
|
-
const meta = this.#planMeta(this.#newGroupingKey());
|
|
2192
|
-
const createRecord = args.create;
|
|
2193
|
-
const patchRecord = args.update;
|
|
2194
|
-
const storeName = this.#storeName;
|
|
2195
|
-
const executionDefaults = this.#contract.execution?.mutations.defaults;
|
|
2196
|
-
const exec = requireTransactionExecutor(this.#executor);
|
|
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);
|
|
2201
|
-
const { storeNames: onUpdateStoreNames } = collectOnUpdateEnforcementStoreNames(this.#contract, this.#modelName, effectivePatch);
|
|
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) => {
|
|
2208
|
-
const existing = (await scope.execute({
|
|
2209
|
-
meta,
|
|
2210
|
-
kind: "cursor-scan",
|
|
2211
|
-
storeName,
|
|
2212
|
-
filter: matches,
|
|
2213
|
-
take: 1
|
|
2214
|
-
}))[0];
|
|
2215
|
-
if (existing === void 0) {
|
|
2216
|
-
await validateScalarFks(scope, this.#contract, this.#modelName, createWithDefaults);
|
|
2217
|
-
return (await scope.execute({
|
|
2218
|
-
meta,
|
|
2219
|
-
kind: "add",
|
|
2220
|
-
storeName,
|
|
2221
|
-
record: createWithDefaults
|
|
2222
|
-
}))[0] ?? createWithDefaults;
|
|
2223
|
-
}
|
|
2224
|
-
await validateScalarFks(scope, this.#contract, this.#modelName, effectivePatch, existing);
|
|
2225
|
-
const key = extractKeyFromRow(existing, keyPath);
|
|
2226
|
-
await applyReferentialActionsForRowOnUpdate(scope, this.#contract, this.#modelName, existing, effectivePatch);
|
|
2227
|
-
return (await scope.execute({
|
|
2228
|
-
meta,
|
|
2229
|
-
kind: "update",
|
|
2230
|
-
storeName,
|
|
2231
|
-
key,
|
|
2232
|
-
patch: effectivePatch
|
|
2233
|
-
}))[0] ?? existing;
|
|
2234
|
-
});
|
|
2235
|
-
}
|
|
2236
|
-
createAll(data) {
|
|
2237
|
-
const defaultsCache = createMutationDefaultsCache();
|
|
2238
|
-
const executionDefaults = this.#contract.execution?.mutations.defaults;
|
|
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);
|
|
2256
|
-
const plan = {
|
|
2257
|
-
meta,
|
|
2258
|
-
ast: {
|
|
2259
|
-
kind: "createAll",
|
|
2260
|
-
modelName: this.#modelName,
|
|
2261
|
-
data: records
|
|
2262
|
-
},
|
|
2263
|
-
idbPlan: {
|
|
2264
|
-
meta,
|
|
2265
|
-
kind: "batch",
|
|
2266
|
-
storeNames: [this.#storeName],
|
|
2267
|
-
ops: records.map((record) => ({
|
|
2268
|
-
meta,
|
|
2269
|
-
kind: "add",
|
|
2270
|
-
storeName: this.#storeName,
|
|
2271
|
-
record
|
|
2272
|
-
}))
|
|
2273
|
-
}
|
|
2274
|
-
};
|
|
2275
|
-
const executorQuery = this.#executor.query.bind(this.#executor);
|
|
2276
|
-
const contract = this.#contract;
|
|
2277
|
-
const modelName = this.#modelName;
|
|
2278
|
-
return new AsyncIterableResult((async function* () {
|
|
2279
|
-
for (const record of records) assertEnumValues(contract, modelName, record);
|
|
2280
|
-
for await (const row of executorQuery(plan)) yield row;
|
|
2281
|
-
})());
|
|
2282
|
-
}
|
|
2283
|
-
async createCount(data) {
|
|
2284
|
-
return (await this.createAll(data).toArray()).length;
|
|
2285
|
-
}
|
|
2286
|
-
/**
|
|
2287
|
-
* Always routes through the transaction scope (`executeDeleteAllWithReferentialActions`),
|
|
2288
|
-
* not just when there are enforceable child relations to cascade — that
|
|
2289
|
-
* function already handles the no-relations case correctly (an empty
|
|
2290
|
-
* relation list is a no-op loop), and going through it unconditionally is
|
|
2291
|
-
* what lets the sync interceptor's transaction-scope tracking write one
|
|
2292
|
-
* outbox event per actually-deleted row instead of one lump event for
|
|
2293
|
-
* the whole batch with no way to know which rows it covered. See
|
|
2294
|
-
* `updateAll()`'s doc comment for the same reasoning.
|
|
2295
|
-
*/
|
|
2296
|
-
deleteAll() {
|
|
2297
|
-
const combined = this.#combinedFilterExpr();
|
|
2298
|
-
const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
|
|
2299
|
-
const contract = this.#contract;
|
|
2300
|
-
const modelName = this.#modelName;
|
|
2301
|
-
const executor = requireTransactionExecutor(this.#executor);
|
|
2302
|
-
return new AsyncIterableResult((async function* () {
|
|
2303
|
-
const rows = await executeDeleteAllWithReferentialActions({
|
|
2304
|
-
executor,
|
|
2305
|
-
contract,
|
|
2306
|
-
modelName,
|
|
2307
|
-
...filter !== void 0 ? { filter } : {}
|
|
2308
|
-
});
|
|
2309
|
-
for (const row of rows) yield row;
|
|
2310
|
-
})());
|
|
2311
|
-
}
|
|
2312
|
-
async deleteCount() {
|
|
2313
|
-
return (await this.deleteAll().toArray()).length;
|
|
2314
|
-
}
|
|
2315
|
-
count() {
|
|
2316
|
-
if (this.#includeRefinementMode) return createIncludeScalar(this.#state);
|
|
2317
|
-
return this.#countTerminal();
|
|
2318
|
-
}
|
|
2319
|
-
async #countTerminal() {
|
|
2320
|
-
const groupingKey = this.#newGroupingKey();
|
|
2321
|
-
const combined = this.#combinedFilterExpr();
|
|
2322
|
-
const fieldToIndexMap = typeof IDBKeyRange !== "undefined" ? buildFieldToIndexMap(this.#contract, this.#storeName) : void 0;
|
|
2323
|
-
if (fieldToIndexMap !== void 0) {
|
|
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);
|
|
2326
|
-
}
|
|
2327
|
-
const scanPlan = this.#buildScanPlan(groupingKey, fieldToIndexMap);
|
|
2328
|
-
const scanAst = scanPlan.ast;
|
|
2329
|
-
const ast = {
|
|
2330
|
-
kind: "count",
|
|
2331
|
-
modelName: this.#modelName,
|
|
2332
|
-
...scanAst?.kind === "findMany" && scanAst.where !== void 0 ? { where: scanAst.where } : {}
|
|
2333
|
-
};
|
|
2334
|
-
const nativeTotal = await this.#executeNativeCount(scanPlan, ast);
|
|
2335
|
-
if (nativeTotal !== null) return clampCount(nativeTotal, this.#state.skip, this.#state.take);
|
|
2336
|
-
const plan = {
|
|
2337
|
-
...scanPlan,
|
|
2338
|
-
ast
|
|
2339
|
-
};
|
|
2340
|
-
let n = 0;
|
|
2341
|
-
for await (const _ of this.#executor.query(plan)) n++;
|
|
2342
|
-
return n;
|
|
2343
|
-
}
|
|
2344
|
-
/**
|
|
2345
|
-
* Resolve an `include()` argument pair into an {@link IncludeEntry}: run the
|
|
2346
|
-
* optional refinement against a fresh refinement-mode child accessor, then
|
|
2347
|
-
* classify the result as a scalar count or a refined collection.
|
|
2348
|
-
*/
|
|
2349
|
-
#resolveIncludeEntry(relation, refineFn) {
|
|
2350
|
-
if (refineFn === void 0) return {
|
|
2351
|
-
kind: "collection",
|
|
2352
|
-
state: emptyAccessorState()
|
|
2353
|
-
};
|
|
2354
|
-
const rel = getRelation(this.#contract, this.#modelName, relation);
|
|
2355
|
-
const relatedModelName = rel?.to.model ?? relation;
|
|
2356
|
-
const refined = refineFn(new IdbStoreAccessorImpl(this.#contract, relatedModelName, this.#executor, emptyAccessorState(), this.#newGroupingKey, true));
|
|
2357
|
-
if (isIncludeScalar(refined)) {
|
|
2358
|
-
if (rel !== void 0 && rel.cardinality !== "1:N") throw new Error(`include('${relation}'): count() is only supported for to-many (1:N) relations`);
|
|
2359
|
-
return {
|
|
2360
|
-
kind: "scalar",
|
|
2361
|
-
fn: refined.fn,
|
|
2362
|
-
state: refined.state
|
|
2363
|
-
};
|
|
2364
|
-
}
|
|
2365
|
-
if (refined instanceof IdbStoreAccessorImpl) return {
|
|
2366
|
-
kind: "collection",
|
|
2367
|
-
state: refined.#state
|
|
2368
|
-
};
|
|
2369
|
-
throw new Error(`include('${relation}') refinement must return the collection (for where/orderBy/take/skip) or a count() selector`);
|
|
2370
|
-
}
|
|
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
|
-
/**
|
|
2391
|
-
* Materialise all rows matching the accumulated filters with no pagination —
|
|
2392
|
-
* used by `aggregate()` / `groupBy()`. The supplied `ast` is attached to the
|
|
2393
|
-
* scan plan so middleware can observe the aggregate intent.
|
|
2394
|
-
*/
|
|
2395
|
-
async #materialize(groupingKey, ast) {
|
|
2396
|
-
const combined = this.#combinedFilterExpr();
|
|
2397
|
-
const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
|
|
2398
|
-
const meta = this.#planMeta(groupingKey);
|
|
2399
|
-
const plan = {
|
|
2400
|
-
meta,
|
|
2401
|
-
ast,
|
|
2402
|
-
idbPlan: {
|
|
2403
|
-
meta,
|
|
2404
|
-
kind: "cursor-scan",
|
|
2405
|
-
storeName: this.#storeName,
|
|
2406
|
-
...filter !== void 0 ? { filter } : {}
|
|
2407
|
-
}
|
|
2408
|
-
};
|
|
2409
|
-
const rows = [];
|
|
2410
|
-
for await (const row of this.#executor.query(plan)) rows.push(row);
|
|
2411
|
-
return rows;
|
|
2412
|
-
}
|
|
2413
|
-
#buildScanPlan(groupingKey, fieldToIndexMap) {
|
|
2414
|
-
const combined = this.#combinedFilterExpr();
|
|
2415
|
-
const comparator = buildRowComparator(this.#state.orderBy);
|
|
2416
|
-
const meta = this.#planMeta(groupingKey);
|
|
2417
|
-
const ast = {
|
|
2418
|
-
kind: "findMany",
|
|
2419
|
-
modelName: this.#modelName,
|
|
2420
|
-
...combined !== void 0 ? { where: combined } : {},
|
|
2421
|
-
...this.#state.orderBy !== void 0 ? { orderBy: this.#state.orderBy } : {},
|
|
2422
|
-
...this.#state.skip !== void 0 ? { skip: this.#state.skip } : {},
|
|
2423
|
-
...this.#state.take !== void 0 ? { take: this.#state.take } : {}
|
|
2424
|
-
};
|
|
2425
|
-
if (typeof IDBKeyRange !== "undefined") {
|
|
2426
|
-
const hint = extractIndexEqualityHint(combined, fieldToIndexMap ?? buildFieldToIndexMap(this.#contract, this.#storeName), getKeyPath(this.#contract, this.#modelName));
|
|
2427
|
-
if (hint !== null) {
|
|
2428
|
-
const { indexName, value, remainingFilter } = hint;
|
|
2429
|
-
const filter = remainingFilter !== void 0 ? (row) => evaluateFilter(remainingFilter, row) : void 0;
|
|
2430
|
-
return {
|
|
2431
|
-
meta,
|
|
2432
|
-
ast,
|
|
2433
|
-
idbPlan: {
|
|
2434
|
-
meta,
|
|
2435
|
-
kind: "cursor-scan",
|
|
2436
|
-
storeName: this.#storeName,
|
|
2437
|
-
...indexName !== void 0 ? { indexName } : {},
|
|
2438
|
-
range: IDBKeyRange.only(value),
|
|
2439
|
-
...filter !== void 0 ? { filter } : {},
|
|
2440
|
-
...comparator !== void 0 ? { comparator } : {},
|
|
2441
|
-
...this.#state.skip !== void 0 ? { skip: this.#state.skip } : {},
|
|
2442
|
-
...this.#state.take !== void 0 ? { take: this.#state.take } : {}
|
|
2443
|
-
}
|
|
2444
|
-
};
|
|
2445
|
-
}
|
|
2446
|
-
}
|
|
2447
|
-
const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
|
|
2448
|
-
return {
|
|
2449
|
-
meta,
|
|
2450
|
-
ast,
|
|
2451
|
-
idbPlan: {
|
|
2452
|
-
meta,
|
|
2453
|
-
kind: "cursor-scan",
|
|
2454
|
-
storeName: this.#storeName,
|
|
2455
|
-
...filter !== void 0 ? { filter } : {},
|
|
2456
|
-
...comparator !== void 0 ? { comparator } : {},
|
|
2457
|
-
...this.#state.skip !== void 0 ? { skip: this.#state.skip } : {},
|
|
2458
|
-
...this.#state.take !== void 0 ? { take: this.#state.take } : {}
|
|
2459
|
-
}
|
|
2460
|
-
};
|
|
2461
|
-
}
|
|
2462
|
-
/**
|
|
2463
|
-
* Execute one cursor-scan per OR branch, union the results, deduplicate by
|
|
2464
|
-
* primary key, and apply `hint.remainingFilter` in-memory. Skip/take are
|
|
2465
|
-
* NOT applied here — the caller slices the array after union so pagination
|
|
2466
|
-
* is correct across branches.
|
|
2467
|
-
*/
|
|
2468
|
-
async #executeOrRows(hint, groupingKey, combined) {
|
|
2469
|
-
const meta = this.#planMeta(groupingKey);
|
|
2470
|
-
const storeName = this.#storeName;
|
|
2471
|
-
const keyPath = getKeyPath(this.#contract, this.#modelName);
|
|
2472
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2473
|
-
const rows = [];
|
|
2474
|
-
const ast = {
|
|
2475
|
-
kind: "findMany",
|
|
2476
|
-
modelName: this.#modelName,
|
|
2477
|
-
...combined !== void 0 ? { where: combined } : {}
|
|
2478
|
-
};
|
|
2479
|
-
const branchResults = await Promise.all(hint.branches.map(async (branch) => {
|
|
2480
|
-
const plan = {
|
|
2481
|
-
meta,
|
|
2482
|
-
ast,
|
|
2483
|
-
idbPlan: {
|
|
2484
|
-
meta,
|
|
2485
|
-
kind: "cursor-scan",
|
|
2486
|
-
storeName,
|
|
2487
|
-
...branch.indexName !== void 0 ? { indexName: branch.indexName } : {},
|
|
2488
|
-
range: IDBKeyRange.only(branch.value)
|
|
2489
|
-
}
|
|
2490
|
-
};
|
|
2491
|
-
const branchRows = [];
|
|
2492
|
-
for await (const row of this.#executor.query(plan)) branchRows.push(row);
|
|
2493
|
-
return branchRows;
|
|
2494
|
-
}));
|
|
2495
|
-
for (const branchRows of branchResults) for (const row of branchRows) {
|
|
2496
|
-
const pk = keyToken(extractKeyFromRow(row, keyPath));
|
|
2497
|
-
if (!seen.has(pk)) {
|
|
2498
|
-
seen.add(pk);
|
|
2499
|
-
rows.push(row);
|
|
2500
|
-
}
|
|
2501
|
-
}
|
|
2502
|
-
if (hint.remainingFilter === void 0) return rows;
|
|
2503
|
-
const { remainingFilter } = hint;
|
|
2504
|
-
return rows.filter((row) => evaluateFilter(remainingFilter, row));
|
|
2505
|
-
}
|
|
2506
|
-
/**
|
|
2507
|
-
* Combine all accumulated filter expressions with AND.
|
|
2508
|
-
*
|
|
2509
|
-
* Returns `undefined` when no filter has been installed so the driver can
|
|
2510
|
-
* skip building a row filter closure (a small perf and readability win on
|
|
2511
|
-
* `.all()` paths). Delegates to the shared {@link combineFilterExprs}.
|
|
2512
|
-
*/
|
|
2513
|
-
#combinedFilterExpr() {
|
|
2514
|
-
return combineFilterExprs(this.#state.filters);
|
|
2515
|
-
}
|
|
2516
|
-
async #applyIncludes(rows, groupingKey) {
|
|
2517
|
-
const relNames = Object.keys(this.#state.includes);
|
|
2518
|
-
if (relNames.length === 0) return rows;
|
|
2519
|
-
let result = rows;
|
|
2520
|
-
for (const relName of relNames) {
|
|
2521
|
-
const entry = this.#state.includes[relName];
|
|
2522
|
-
result = await loadRelation(relName, entry, result, this.#contract, this.#modelName, this.#executor, groupingKey);
|
|
2523
|
-
}
|
|
2524
|
-
return result;
|
|
2525
|
-
}
|
|
2526
|
-
/**
|
|
2527
|
-
* Apply a `.select()` projection (if any) to materialised rows. Keeps the
|
|
2528
|
-
* selected scalar fields plus every included relation key (which `include()`
|
|
2529
|
-
* attached during {@link #applyIncludes}); a no-op when nothing is selected.
|
|
2530
|
-
*/
|
|
2531
|
-
#projectRows(rows) {
|
|
2532
|
-
const selected = this.#state.selectedFields;
|
|
2533
|
-
if (selected === void 0) return rows;
|
|
2534
|
-
const keep = [...selected, ...Object.keys(this.#state.includes)];
|
|
2535
|
-
return rows.map((row) => {
|
|
2536
|
-
const out = {};
|
|
2537
|
-
for (const field of keep) if (field in row) out[field] = row[field];
|
|
2538
|
-
return out;
|
|
2539
|
-
});
|
|
2540
|
-
}
|
|
2541
|
-
#planMeta(groupingKey) {
|
|
2542
|
-
return {
|
|
2543
|
-
target: "idb",
|
|
2544
|
-
storageHash: this.#contract.storage.storageHash,
|
|
2545
|
-
lane: "idb-orm",
|
|
2546
|
-
annotations: { groupingKey }
|
|
2547
|
-
};
|
|
2548
|
-
}
|
|
2549
|
-
#clone(overrides) {
|
|
2550
|
-
return new IdbStoreAccessorImpl(this.#contract, this.#modelName, this.#executor, mergeAccessorState(this.#state, overrides), this.#newGroupingKey, this.#includeRefinementMode);
|
|
2551
|
-
}
|
|
2552
|
-
};
|
|
2553
|
-
//#endregion
|
|
2554
|
-
//#region src/core/idb-orm.ts
|
|
2555
|
-
/**
|
|
2556
|
-
* Create a typed IDB ORM client from a contract and executor.
|
|
2557
|
-
*
|
|
2558
|
-
* The client exposes one `IdbStoreAccessor` per entry in `contract.roots`.
|
|
2559
|
-
* Only roots-declared stores are accessible at the top level — other stores
|
|
2560
|
-
* can be reached via `.include()` on any accessor.
|
|
2561
|
-
*
|
|
2562
|
-
* @example
|
|
2563
|
-
* ```ts
|
|
2564
|
-
* import { idbOrm } from "@prisma-idb/client-idb/orm";
|
|
2565
|
-
* import { createIdbRuntime } from "@prisma-idb/runtime-idb/runtime";
|
|
2566
|
-
* import contract from "./prisma/idb-contract";
|
|
2567
|
-
*
|
|
2568
|
-
* const runtime = createIdbRuntime({ adapter, driver });
|
|
2569
|
-
* const db = idbOrm({ contract, executor: runtime });
|
|
2570
|
-
*
|
|
2571
|
-
* // Typed query builder:
|
|
2572
|
-
* const alice = await db.users.where({ email: "alice@example.com" }).first();
|
|
2573
|
-
* const postsWithAuthor = await db.posts.include("author").all();
|
|
2574
|
-
* ```
|
|
2575
|
-
*/
|
|
2576
|
-
function idbOrm(options) {
|
|
2577
|
-
const { contract, executor } = options;
|
|
2578
|
-
let _key = 0;
|
|
2579
|
-
const newGroupingKey = () => `idb-op-${++_key}`;
|
|
2580
|
-
const client = {};
|
|
2581
|
-
for (const [rootKey, ref] of Object.entries(contract.roots)) client[rootKey] = new IdbStoreAccessorImpl(contract, ref.model, executor, void 0, newGroupingKey);
|
|
2582
|
-
return client;
|
|
2583
|
-
}
|
|
2584
|
-
//#endregion
|
|
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 };
|
|
1265
|
+
export { isValidIdbKey as A, assertEnumValues as C, getKeyPath as D, fieldValueToken as E, keyToken as M, getRelation as O, createMutationDefaultsCache as S, extractKeyFromRow as T, requireTransactionExecutor as _, collectScalarFkStoreNames as a, applyCreateDefaults as b, executeDeleteWithReferentialActions as c, executeScalarCreateAllWithFkValidation as d, executeScalarCreateWithFkValidation as f, hasScalarFkFields as g, hasNestedMutationCallbacks as h, collectOnUpdateEnforcementStoreNames as i, keyEquals as j, getStoreName as k, executeNestedCreateMutation as l, hasEnforceableChildRelations as m, applyReferentialActionsForRowOnUpdate as n, executeBulkUpdateWithFkValidation as o, executeScalarUpdateWithFkValidation as p, collectDeleteStoreNames as r, executeDeleteAllWithReferentialActions as s, applyReferentialActionsForRow as t, executeNestedUpdateMutation as u, validateScalarFks as v, buildFieldToIndexMap as w, applyUpdateDefaults as x, withMutationScope as y };
|
|
2586
1266
|
|
|
2587
|
-
//# sourceMappingURL=
|
|
1267
|
+
//# sourceMappingURL=mutation-executor-CD5n_kKT.mjs.map
|