content-query 0.3.0 → 0.5.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 +46 -15
- package/dist/{build-CI4s0oFP.mjs → build-DWpB69_c.mjs} +826 -69
- package/dist/{build-jKnjDjQE.d.mts → build-DfoncKL8.d.mts} +17 -5
- package/dist/build.d.mts +1 -1
- package/dist/build.mjs +1 -1
- package/dist/cli.mjs +14 -8
- package/dist/{content-N1gYshjg.mjs → collection-Cb5eWGnv.mjs} +998 -344
- package/dist/collection-types-CsQtGw7_.d.mts +119 -0
- package/dist/computed.d.mts +2 -2
- package/dist/{types-FmUJ3cH1.d.mts → descriptor-bcPa6XLk.d.mts} +17 -1
- package/dist/{diagnostics-Cu94mZhl.mjs → diagnostics-DmUZtrJ6.mjs} +3 -1
- package/dist/{errors-C2iuBM0G.mjs → errors-Bfd0R4_P.mjs} +16 -1
- package/dist/file-DSOcdFj0.mjs +81 -0
- package/dist/{glob-Bv0dpvCQ.mjs → glob-BXJuwy_p.mjs} +94 -6
- package/dist/index.d.mts +76 -18
- package/dist/index.mjs +1 -1
- package/dist/internal.d.mts +54 -7
- package/dist/internal.mjs +424 -19
- package/dist/loaders.d.mts +1 -2
- package/dist/loaders.mjs +2 -81
- package/dist/next.d.mts +3 -2
- package/dist/next.mjs +67 -14
- package/dist/operators-D-w1-gDg.mjs +391 -0
- package/dist/query-style-C2FcT1f9.d.mts +23 -0
- package/dist/query.d.mts +25 -2
- package/dist/query.mjs +1 -1
- package/dist/{relational-types-B45zM34W.d.mts → relational-types-CTcalUNW.d.mts} +241 -50
- package/dist/revision-DBwU-aC9.d.mts +56 -0
- package/dist/runtime.d.mts +3 -2
- package/dist/runtime.mjs +1 -1
- package/dist/types-Dz1vTXa2.d.mts +214 -0
- package/dist/vite.d.mts +1 -1
- package/dist/vite.mjs +77 -35
- package/package.json +1 -1
- package/dist/content-lwPxE9Yu.d.mts +0 -109
- package/dist/descriptor-BUPT2J7X.d.mts +0 -18
- package/dist/operators-UqWUhKQJ.mjs +0 -173
- package/dist/types-BC8gVJ6u.d.mts +0 -69
- package/dist/validation-D_XdE1pF.mjs +0 -84
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as invalidQuery, n as contentError, t as ContentError } from "./errors-
|
|
2
|
-
import { a as detachSelection, c as isScalar, i as detach, n as compareStrings, s as equal, t as compare } from "./values-BbTV4QT-.mjs";
|
|
1
|
+
import { a as invalidQuery, n as contentError, o as payloadIdentity, t as ContentError } from "./errors-Bfd0R4_P.mjs";
|
|
2
|
+
import { a as detachSelection, c as isScalar, i as detach, n as compareStrings, o as encode, r as decode, s as equal, t as compare } from "./values-BbTV4QT-.mjs";
|
|
3
3
|
import { t as assertServerOnly } from "./server-only-CIJUlyL-.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { A as NO_SELECTION_MESSAGE, D as resolveRelations, E as or, F as hasSelection, M as assertAvailable, N as effectiveOrder, P as emptyPlan, S as not, j as PAGINATE_AFTER_LIMIT_MESSAGE, k as validateExpression, l as fieldProxy, t as and } from "./operators-D-w1-gDg.mjs";
|
|
5
5
|
//#region src/artifacts/types.ts
|
|
6
6
|
const CODEC_VERSION = "devalue-6.0.2";
|
|
7
7
|
//#endregion
|
|
@@ -144,41 +144,152 @@ function isRelationship(value) {
|
|
|
144
144
|
].includes(String(value.referenceSide));
|
|
145
145
|
}
|
|
146
146
|
//#endregion
|
|
147
|
-
//#region src/
|
|
147
|
+
//#region src/runtime/revision.ts
|
|
148
148
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
149
|
+
* Serializes a variant combination with sorted keys, so preparation, artifacts, and query plans
|
|
150
|
+
* agree on one identity. A payload without variants uses `{}`.
|
|
151
151
|
*
|
|
152
|
-
* @param
|
|
153
|
-
* @
|
|
154
|
-
* @param callback - Asynchronous work for one item and its index.
|
|
155
|
-
* @returns The callback results in input order.
|
|
152
|
+
* @param variant - The variant combination.
|
|
153
|
+
* @returns The canonical variant key.
|
|
156
154
|
*/
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
155
|
+
function variantKey(variant) {
|
|
156
|
+
return JSON.stringify(Object.fromEntries(Object.keys(variant).sort().map((key) => [key, variant[key]])));
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/query/cursor.ts
|
|
160
|
+
/** Changes whenever the encoded cursor layout changes, so older cursors fail clearly. */
|
|
161
|
+
const CURSOR_VERSION = 1;
|
|
162
|
+
/** Bounds the work spent on client-supplied cursors, which usually arrive in URLs. */
|
|
163
|
+
const MAX_CURSOR_LENGTH = 4096;
|
|
164
|
+
const BASE64URL = /^[A-Za-z0-9_-]+$/;
|
|
165
|
+
/** Encodes UTF-8 text as unpadded base64url without relying on Node's `Buffer`. */
|
|
166
|
+
function toBase64Url(text) {
|
|
167
|
+
let binary = "";
|
|
168
|
+
for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);
|
|
169
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
170
|
+
}
|
|
171
|
+
/** Decodes unpadded base64url into UTF-8 text, rejecting invalid byte sequences. */
|
|
172
|
+
function fromBase64Url(encoded) {
|
|
173
|
+
const binary = atob(encoded.replace(/-/g, "+").replace(/_/g, "/"));
|
|
174
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
175
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
176
|
+
}
|
|
177
|
+
/** Describes an order as `[field, direction]` pairs, the form a cursor records. */
|
|
178
|
+
function orderPairs(order) {
|
|
179
|
+
return order.map((sort) => [sort.field, sort.direction]);
|
|
180
|
+
}
|
|
181
|
+
/** Checks a decoded cursor order against the query's order without trusting its shape. */
|
|
182
|
+
function sameOrder(candidate, order) {
|
|
183
|
+
return Array.isArray(candidate) && candidate.length === order.length && order.every((sort, index) => {
|
|
184
|
+
const pair = candidate[index];
|
|
185
|
+
return Array.isArray(pair) && pair.length === 2 && pair[0] === sort.field && pair[1] === sort.direction;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/** Rejects a client-supplied cursor that cannot position this query. */
|
|
189
|
+
function invalidCursor(collection, message, cause) {
|
|
190
|
+
throw new ContentError({
|
|
191
|
+
code: "invalid-cursor",
|
|
192
|
+
operation: "query",
|
|
193
|
+
collection,
|
|
194
|
+
message,
|
|
195
|
+
cause
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Encodes an entry's position in a query's order as an opaque cursor. The cursor records the
|
|
200
|
+
* collection, the effective order, and the entry's sort values, with rich values such as dates and
|
|
201
|
+
* `BigInt`s preserved.
|
|
202
|
+
*
|
|
203
|
+
* @param collection - The queried collection.
|
|
204
|
+
* @param order - The query's effective order, including the ID tie-breaker.
|
|
205
|
+
* @param key - The entry's values for each sort clause, in order.
|
|
206
|
+
* @returns An opaque, URL-safe cursor string.
|
|
207
|
+
*/
|
|
208
|
+
function encodeCursor(collection, order, key) {
|
|
209
|
+
return toBase64Url(encode([
|
|
210
|
+
CURSOR_VERSION,
|
|
211
|
+
collection,
|
|
212
|
+
orderPairs(order),
|
|
213
|
+
[...key]
|
|
214
|
+
]));
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Decodes a cursor into sort values for this query. A cursor stays valid when filters change, but
|
|
218
|
+
* only for the same collection and effective order it was created with.
|
|
219
|
+
*
|
|
220
|
+
* @param cursor - A cursor from an earlier page of the same query.
|
|
221
|
+
* @param collection - The queried collection.
|
|
222
|
+
* @param order - The query's effective order, including the ID tie-breaker.
|
|
223
|
+
* @returns The boundary entry's sort values, in order.
|
|
224
|
+
* @throws ContentError with code `invalid-cursor` when the cursor is malformed or belongs to a
|
|
225
|
+
* different collection or order.
|
|
226
|
+
*/
|
|
227
|
+
function decodeCursor(cursor, collection, order) {
|
|
228
|
+
if (typeof cursor !== "string" || cursor.length > MAX_CURSOR_LENGTH || !BASE64URL.test(cursor)) invalidCursor(collection, "The cursor is not a cursor returned by paginate().");
|
|
229
|
+
let decoded;
|
|
230
|
+
try {
|
|
231
|
+
decoded = decode(fromBase64Url(cursor));
|
|
232
|
+
} catch (cause) {
|
|
233
|
+
invalidCursor(collection, "The cursor is not a cursor returned by paginate().", cause);
|
|
171
234
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (
|
|
175
|
-
return
|
|
235
|
+
if (!Array.isArray(decoded) || decoded.length !== 4 || decoded[0] !== CURSOR_VERSION || !Array.isArray(decoded[3]) || !decoded[3].every(isScalar)) invalidCursor(collection, "The cursor is not a cursor returned by paginate().");
|
|
236
|
+
const [, cursorCollection, cursorOrder, key] = decoded;
|
|
237
|
+
if (cursorCollection !== collection || !sameOrder(cursorOrder, order) || key.length !== order.length) invalidCursor(collection, "The cursor belongs to a query with a different collection or sort order.");
|
|
238
|
+
return key;
|
|
176
239
|
}
|
|
177
240
|
//#endregion
|
|
178
241
|
//#region src/query/clauses.ts
|
|
242
|
+
/** Option names that `paginate()` reads; relational options pass the rest to the query plan. */
|
|
243
|
+
const PAGINATE_KEYS = [
|
|
244
|
+
"page",
|
|
245
|
+
"size",
|
|
246
|
+
"after",
|
|
247
|
+
"before",
|
|
248
|
+
"total"
|
|
249
|
+
];
|
|
250
|
+
/**
|
|
251
|
+
* Checks `paginate()` options against a selected root plan and compiles them into a page request.
|
|
252
|
+
* Cursors are decoded here, so execution and future backends receive sort values only.
|
|
253
|
+
*
|
|
254
|
+
* @param plan - The root plan to paginate; it must not set its own limit or offset.
|
|
255
|
+
* @param options - `{ page, size }` for page numbers, or `{ size, after?, before?, total? }` for
|
|
256
|
+
* cursors. A `null` cursor starts at the first entry (`after`) or the last entry (`before`).
|
|
257
|
+
* @returns The compiled page request.
|
|
258
|
+
* @throws ContentError with code `invalid-query` for invalid options, or `invalid-cursor` for a
|
|
259
|
+
* cursor from a different collection or order.
|
|
260
|
+
*/
|
|
261
|
+
function checkedPaginate(plan, options) {
|
|
262
|
+
if (plan.limit !== void 0 || plan.offset !== void 0) invalidQuery(PAGINATE_AFTER_LIMIT_MESSAGE);
|
|
263
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) invalidQuery("paginate() requires options, such as { page: 1, size: 10 } or { size: 10 }.");
|
|
264
|
+
const { page, size, after, before, total, ...rest } = options;
|
|
265
|
+
const [extra] = Object.keys(rest);
|
|
266
|
+
if (extra !== void 0) invalidQuery(`Unknown paginate() option "${extra}".`);
|
|
267
|
+
if (!Number.isSafeInteger(size) || size < 1) invalidQuery("paginate() requires a positive safe integer size.");
|
|
268
|
+
if (page !== void 0) {
|
|
269
|
+
if (after !== void 0 || before !== void 0 || total !== void 0) invalidQuery("Page-number pagination cannot use after, before, or total; its results always include total.");
|
|
270
|
+
if (!Number.isSafeInteger(page) || page < 1) invalidQuery("paginate() requires a positive safe integer page, starting at 1.");
|
|
271
|
+
return {
|
|
272
|
+
mode: "page",
|
|
273
|
+
page,
|
|
274
|
+
size
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (after !== void 0 && before !== void 0) invalidQuery("paginate() accepts an after or a before cursor, not both.");
|
|
278
|
+
if (total !== void 0 && typeof total !== "boolean") invalidQuery("paginate() requires total to be a boolean.");
|
|
279
|
+
const direction = before !== void 0 ? "before" : "after";
|
|
280
|
+
const cursor = direction === "before" ? before : after;
|
|
281
|
+
return {
|
|
282
|
+
mode: "cursor",
|
|
283
|
+
size,
|
|
284
|
+
direction,
|
|
285
|
+
total: total === true,
|
|
286
|
+
...cursor === null || cursor === void 0 ? {} : { key: decodeCursor(cursor, plan.collection, effectiveOrder(plan)) }
|
|
287
|
+
};
|
|
288
|
+
}
|
|
179
289
|
/** Checks scalar field selection for both public query frontends without a schema field catalog. */
|
|
180
290
|
function checkedFields(registry, collection, fields) {
|
|
181
291
|
const scope = registry[collection];
|
|
292
|
+
for (const field of fields) if (typeof field === "string") assertAvailable(registry, collection, field);
|
|
182
293
|
if (!scope || fields.some((field) => typeof field !== "string" || !field || field === "_adjacent" || field === "_payloads" || Object.hasOwn(scope.relationships, field)) || new Set(fields).size !== fields.length) invalidQuery("Selection requires distinct prepared fields; use relationship or adjacency selection explicitly.");
|
|
183
294
|
return [...fields];
|
|
184
295
|
}
|
|
@@ -225,204 +336,8 @@ function checkedPayloads(registry, collection, payloads) {
|
|
|
225
336
|
return Object.keys(requested).length ? requested : void 0;
|
|
226
337
|
}
|
|
227
338
|
//#endregion
|
|
228
|
-
//#region src/query/plan.ts
|
|
229
|
-
/** Starts a fresh query without binding a content revision until execution. */
|
|
230
|
-
function emptyPlan(collection) {
|
|
231
|
-
return {
|
|
232
|
-
collection,
|
|
233
|
-
filters: [],
|
|
234
|
-
order: [],
|
|
235
|
-
with: {}
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
/** Treats explicitly selected relationships and adjacency as row projections too. */
|
|
239
|
-
function hasSelection(plan) {
|
|
240
|
-
return !!plan && (plan.select !== void 0 || !!plan.allFields || !!plan.adjacent || Object.keys(plan.with).length > 0);
|
|
241
|
-
}
|
|
242
|
-
//#endregion
|
|
243
|
-
//#region src/query/relational.ts
|
|
244
|
-
const operators$1 = {
|
|
245
|
-
eq: "eq",
|
|
246
|
-
ne: "ne",
|
|
247
|
-
gt: "gt",
|
|
248
|
-
gte: "gte",
|
|
249
|
-
lt: "lt",
|
|
250
|
-
lte: "lte",
|
|
251
|
-
in: "in-array",
|
|
252
|
-
notIn: "not-in-array",
|
|
253
|
-
like: "like",
|
|
254
|
-
ilike: "ilike",
|
|
255
|
-
notLike: "like",
|
|
256
|
-
notIlike: "ilike",
|
|
257
|
-
contains: "contains",
|
|
258
|
-
startsWith: "starts-with",
|
|
259
|
-
endsWith: "ends-with",
|
|
260
|
-
arrayContains: "array-contains-all",
|
|
261
|
-
arrayOverlaps: "array-contains-any",
|
|
262
|
-
arrayContained: "array-contained",
|
|
263
|
-
isNull: "is-null",
|
|
264
|
-
isNotNull: "is-null",
|
|
265
|
-
isUndefined: "is-undefined",
|
|
266
|
-
isMissing: "is-missing"
|
|
267
|
-
};
|
|
268
|
-
/** Requires plain records at each relational options and filter boundary. */
|
|
269
|
-
function record$1(value, label) {
|
|
270
|
-
if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) invalidQuery(`${label} must be an options object.`);
|
|
271
|
-
}
|
|
272
|
-
/** Compiles a field condition, including field-local boolean composition, into portable expressions. */
|
|
273
|
-
function fieldCondition(field, condition) {
|
|
274
|
-
if (isScalar(condition)) return {
|
|
275
|
-
kind: "predicate",
|
|
276
|
-
operator: "eq",
|
|
277
|
-
field,
|
|
278
|
-
value: condition
|
|
279
|
-
};
|
|
280
|
-
record$1(condition, `Filter for "${field}"`);
|
|
281
|
-
const expressions = [];
|
|
282
|
-
for (const [name, value] of Object.entries(condition)) {
|
|
283
|
-
if (name === "AND" || name === "OR") {
|
|
284
|
-
if (!Array.isArray(value)) invalidQuery("Field AND/OR filters require arrays.");
|
|
285
|
-
const children = value.map((child) => fieldCondition(field, child));
|
|
286
|
-
expressions.push(name === "AND" ? and(...children) : or(...children));
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
if (name === "NOT") {
|
|
290
|
-
expressions.push(not(fieldCondition(field, value)));
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
if (!Object.hasOwn(operators$1, name)) invalidQuery(`Unknown filter operator "${name}".`);
|
|
294
|
-
const expression = {
|
|
295
|
-
kind: "predicate",
|
|
296
|
-
operator: operators$1[name],
|
|
297
|
-
field,
|
|
298
|
-
value
|
|
299
|
-
};
|
|
300
|
-
if (name.startsWith("is")) {
|
|
301
|
-
if (typeof value !== "boolean") invalidQuery("Presence predicates require boolean values.");
|
|
302
|
-
expressions.push(value !== (name === "isNotNull") ? expression : not(expression));
|
|
303
|
-
} else expressions.push(name === "notLike" || name === "notIlike" ? not(expression) : expression);
|
|
304
|
-
}
|
|
305
|
-
return and(...expressions);
|
|
306
|
-
}
|
|
307
|
-
/** Compiles object predicates and existential relationship filters without loading bodies. */
|
|
308
|
-
function relationalWhere(registry, collection, where) {
|
|
309
|
-
record$1(where, "where");
|
|
310
|
-
const expressions = [];
|
|
311
|
-
for (const [field, condition] of Object.entries(where)) if (field === "AND" || field === "OR" || field === "NOT") {
|
|
312
|
-
if (field === "OR" && !Array.isArray(condition)) invalidQuery("OR filters require an array.");
|
|
313
|
-
const children = (Array.isArray(condition) ? condition : [condition]).map((child) => relationalWhere(registry, collection, child));
|
|
314
|
-
expressions.push(field === "OR" ? or(...children) : field === "NOT" ? and(...children.map(not)) : and(...children));
|
|
315
|
-
} else if (field === "RAW") {
|
|
316
|
-
if (typeof condition !== "function") invalidQuery("RAW requires a structured-expression callback.");
|
|
317
|
-
const expression = condition(new Proxy(Object.create(null), { get: (_target, name) => ({ name }) }));
|
|
318
|
-
validateExpression(expression);
|
|
319
|
-
expressions.push(expression);
|
|
320
|
-
} else if (Object.hasOwn(registry[collection].relationships, field)) {
|
|
321
|
-
const relation = registry[collection].relationships[field];
|
|
322
|
-
const expression = {
|
|
323
|
-
kind: "relation",
|
|
324
|
-
collection,
|
|
325
|
-
field,
|
|
326
|
-
target: relation.collection,
|
|
327
|
-
expression: relationalWhere(registry, relation.collection, typeof condition === "boolean" ? {} : condition)
|
|
328
|
-
};
|
|
329
|
-
expressions.push(condition === false ? not(expression) : expression);
|
|
330
|
-
} else expressions.push(fieldCondition(field, condition));
|
|
331
|
-
const expression = and(...expressions);
|
|
332
|
-
validateExpression(expression);
|
|
333
|
-
return detach(expression);
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Compiles Drizzle-style columns/with options into the shared immutable query plan.
|
|
337
|
-
* Payloads are allowed wherever adjacency is, so sibling scopes never load them.
|
|
338
|
-
*/
|
|
339
|
-
function relationalPlan(registry, collection, options = {}, many = true, allowAdjacent = true, pagination = true) {
|
|
340
|
-
if (!Object.hasOwn(registry, collection)) invalidQuery(`Unknown collection "${collection}".`);
|
|
341
|
-
record$1(options, "Query");
|
|
342
|
-
const allowed = [
|
|
343
|
-
"columns",
|
|
344
|
-
"where",
|
|
345
|
-
"with",
|
|
346
|
-
...many ? ["orderBy"] : [],
|
|
347
|
-
...many && pagination ? ["limit", "offset"] : [],
|
|
348
|
-
...allowAdjacent ? ["adjacent", "payloads"] : []
|
|
349
|
-
];
|
|
350
|
-
if (Object.keys(options).some((key) => !allowed.includes(key))) invalidQuery("Unknown relational query option or unsupported scope option.");
|
|
351
|
-
const plan = emptyPlan(collection);
|
|
352
|
-
if (options.where !== void 0) plan.filters = [relationalWhere(registry, collection, options.where)];
|
|
353
|
-
if (options.columns === void 0) plan.allFields = true;
|
|
354
|
-
else {
|
|
355
|
-
record$1(options.columns, "columns");
|
|
356
|
-
const columns = options.columns;
|
|
357
|
-
if (Object.values(columns).some((value) => typeof value !== "boolean")) invalidQuery("Column selection requires boolean values.");
|
|
358
|
-
const names = checkedFields(registry, collection, Object.keys(columns));
|
|
359
|
-
const included = names.filter((name) => columns[name] === true);
|
|
360
|
-
if (included.length || !names.length) plan.select = included;
|
|
361
|
-
else {
|
|
362
|
-
plan.allFields = true;
|
|
363
|
-
plan.exclude = names;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
if (options.with !== void 0) {
|
|
367
|
-
record$1(options.with, "with");
|
|
368
|
-
const related = Object.create(null);
|
|
369
|
-
for (const [name, selected] of Object.entries(options.with)) {
|
|
370
|
-
const relation = Object.hasOwn(registry[collection].relationships, name) && registry[collection].relationships[name];
|
|
371
|
-
if (!relation) invalidQuery(`Unknown relationship "${name}".`);
|
|
372
|
-
if (selected === false || selected === void 0) continue;
|
|
373
|
-
related[name] = relationalPlan(registry, relation.collection, selected === true ? {} : selected, relation.type === "many", allowAdjacent);
|
|
374
|
-
}
|
|
375
|
-
plan.with = related;
|
|
376
|
-
}
|
|
377
|
-
if (options.orderBy !== void 0) {
|
|
378
|
-
record$1(options.orderBy, "orderBy");
|
|
379
|
-
if (!Object.keys(options.orderBy).length) invalidQuery("orderBy requires a field.");
|
|
380
|
-
for (const [field, direction] of Object.entries(options.orderBy)) plan.order = checkedOrder(plan, field, direction, many);
|
|
381
|
-
}
|
|
382
|
-
for (const key of ["limit", "offset"]) if (options[key] !== void 0) plan[key] = checkedPagination(options[key], many);
|
|
383
|
-
if (options.payloads !== void 0) {
|
|
384
|
-
record$1(options.payloads, "payloads");
|
|
385
|
-
const payloads = checkedPayloads(registry, collection, options.payloads);
|
|
386
|
-
if (payloads) plan.payloads = payloads;
|
|
387
|
-
}
|
|
388
|
-
if (options.adjacent !== void 0) plan.adjacent = options.adjacent === true ? true : relationalPlan(registry, collection, options.adjacent, true, false, false);
|
|
389
|
-
return plan;
|
|
390
|
-
}
|
|
391
|
-
/** Creates stable, live collection queries in a dedicated, prototype-free namespace. */
|
|
392
|
-
function createQueryNamespace(registry, run) {
|
|
393
|
-
const queries = /* @__PURE__ */ new Map();
|
|
394
|
-
/** Reuses each handle while checking the current registry on every query execution. */
|
|
395
|
-
function query(name) {
|
|
396
|
-
let cached = queries.get(name);
|
|
397
|
-
if (!cached) {
|
|
398
|
-
cached = {
|
|
399
|
-
findMany: (options) => run(relationalPlan(registry(), name, options), "all"),
|
|
400
|
-
findFirst: (options) => run(relationalPlan(registry(), name, options), "first")
|
|
401
|
-
};
|
|
402
|
-
queries.set(name, cached);
|
|
403
|
-
}
|
|
404
|
-
return cached;
|
|
405
|
-
}
|
|
406
|
-
return new Proxy(Object.create(null), {
|
|
407
|
-
get(_target, name) {
|
|
408
|
-
return typeof name === "string" && Object.hasOwn(registry(), name) ? query(name) : void 0;
|
|
409
|
-
},
|
|
410
|
-
ownKeys() {
|
|
411
|
-
return Object.keys(registry());
|
|
412
|
-
},
|
|
413
|
-
getOwnPropertyDescriptor(_target, name) {
|
|
414
|
-
return typeof name === "string" && Object.hasOwn(registry(), name) ? {
|
|
415
|
-
configurable: true,
|
|
416
|
-
enumerable: true,
|
|
417
|
-
value: query(name),
|
|
418
|
-
writable: false
|
|
419
|
-
} : void 0;
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
//#endregion
|
|
424
339
|
//#region src/query/object.ts
|
|
425
|
-
const operators = {
|
|
340
|
+
const operators$1 = {
|
|
426
341
|
equals: "eq",
|
|
427
342
|
not: "ne",
|
|
428
343
|
gt: "gt",
|
|
@@ -442,12 +357,12 @@ const operators = {
|
|
|
442
357
|
isMissing: "is-missing"
|
|
443
358
|
};
|
|
444
359
|
/** Requires a plain options record, rejecting arrays and class instances at API boundaries. */
|
|
445
|
-
function record(value, label) {
|
|
360
|
+
function record$1(value, label) {
|
|
446
361
|
if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) invalidQuery(`${label} must be an options object.`);
|
|
447
362
|
}
|
|
448
363
|
/** Converts object predicates to the same validated expressions used by fluent operators. */
|
|
449
364
|
function objectWhere(where) {
|
|
450
|
-
record(where, "where");
|
|
365
|
+
record$1(where, "where");
|
|
451
366
|
const expressions = [];
|
|
452
367
|
for (const [field, condition] of Object.entries(where)) {
|
|
453
368
|
if ([
|
|
@@ -468,16 +383,16 @@ function objectWhere(where) {
|
|
|
468
383
|
});
|
|
469
384
|
continue;
|
|
470
385
|
}
|
|
471
|
-
record(condition, `Filter for "${field}"`);
|
|
386
|
+
record$1(condition, `Filter for "${field}"`);
|
|
472
387
|
for (const [name, value] of Object.entries(condition)) {
|
|
473
|
-
if (!Object.hasOwn(operators, name)) invalidQuery(`Unknown filter operator "${name}".`);
|
|
388
|
+
if (!Object.hasOwn(operators$1, name)) invalidQuery(`Unknown filter operator "${name}".`);
|
|
474
389
|
if (name === "not" && !isScalar(value)) {
|
|
475
390
|
expressions.push(not(objectWhere({ [field]: value })));
|
|
476
391
|
continue;
|
|
477
392
|
}
|
|
478
393
|
const expression = {
|
|
479
394
|
kind: "predicate",
|
|
480
|
-
operator: operators[name],
|
|
395
|
+
operator: operators$1[name],
|
|
481
396
|
field,
|
|
482
397
|
value
|
|
483
398
|
};
|
|
@@ -492,9 +407,30 @@ function objectWhere(where) {
|
|
|
492
407
|
return detach(expression);
|
|
493
408
|
}
|
|
494
409
|
/** Compiles explicit object options into the backend-independent fluent query contract. */
|
|
410
|
+
/**
|
|
411
|
+
* Compiles an object-form `_adjacent` selection. `{ scope: "results", select }` selects neighbors
|
|
412
|
+
* within the current results; other options describe an independent sibling scope.
|
|
413
|
+
*
|
|
414
|
+
* @param registry - The query registry.
|
|
415
|
+
* @param collection - The collection whose neighbors are selected.
|
|
416
|
+
* @param selection - The `_adjacent` value from an object selection.
|
|
417
|
+
* @returns The adjacency plan, which always selects explicit data.
|
|
418
|
+
*/
|
|
419
|
+
function objectAdjacent(registry, collection, selection) {
|
|
420
|
+
record$1(selection, "_adjacent");
|
|
421
|
+
const { scope, ...options } = selection;
|
|
422
|
+
if (scope !== void 0 && scope !== "results" && scope !== "collection") invalidQuery("Adjacent scope must be \"results\" or \"collection\".");
|
|
423
|
+
if (scope === "results" && Object.keys(options).some((key) => key !== "select")) invalidQuery("Results-scope neighbors only select data; they share the query's filters and order.");
|
|
424
|
+
const plan = objectPlan(registry, collection, options, true, false, false);
|
|
425
|
+
if (!hasSelection(plan)) invalidQuery("Adjacent entries require explicit selection.");
|
|
426
|
+
return {
|
|
427
|
+
scope: scope === "results" ? "results" : "collection",
|
|
428
|
+
plan
|
|
429
|
+
};
|
|
430
|
+
}
|
|
495
431
|
function objectPlan(registry, collection, options, many = true, allowAdjacent = true, pagination = true) {
|
|
496
432
|
if (!Object.hasOwn(registry, collection)) invalidQuery(`Unknown collection "${collection}".`);
|
|
497
|
-
record(options, "Query");
|
|
433
|
+
record$1(options, "Query");
|
|
498
434
|
const allowed = [
|
|
499
435
|
"where",
|
|
500
436
|
"select",
|
|
@@ -504,32 +440,32 @@ function objectPlan(registry, collection, options, many = true, allowAdjacent =
|
|
|
504
440
|
if (Object.keys(options).some((key) => !allowed.includes(key))) invalidQuery("Unknown query option or pagination in an adjacency scope.");
|
|
505
441
|
let plan = emptyPlan(collection);
|
|
506
442
|
if (options.where !== void 0) {
|
|
507
|
-
const expression = typeof options.where === "function" ? options.where(
|
|
443
|
+
const expression = typeof options.where === "function" ? options.where(fieldProxy()) : objectWhere(options.where);
|
|
508
444
|
validateExpression(expression);
|
|
509
|
-
plan.filters = [detach(expression)];
|
|
445
|
+
plan.filters = [detach(resolveRelations(registry, collection, expression))];
|
|
510
446
|
}
|
|
511
447
|
if (options.orderBy !== void 0) for (const order of Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy]) {
|
|
512
|
-
record(order, "orderBy");
|
|
448
|
+
record$1(order, "orderBy");
|
|
513
449
|
if (!Object.keys(order).length) invalidQuery("orderBy requires a field.");
|
|
514
450
|
for (const [field, direction] of Object.entries(order)) plan.order = checkedOrder(plan, field, direction, many);
|
|
515
451
|
}
|
|
516
452
|
for (const key of ["limit", "offset"]) if (options[key] !== void 0) plan[key] = checkedPagination(options[key], many);
|
|
517
453
|
if (options.select !== void 0) {
|
|
518
|
-
record(options.select, "select");
|
|
454
|
+
record$1(options.select, "select");
|
|
519
455
|
const selected = Object.entries(options.select).filter(([, value]) => value !== false && value !== void 0);
|
|
520
456
|
if (!selected.length) invalidQuery("Row-returning queries require a nonempty selection.");
|
|
521
457
|
const fields = [];
|
|
522
458
|
const related = Object.create(null);
|
|
523
459
|
for (const [name, selection] of selected) if (name === "_adjacent") {
|
|
524
460
|
if (!allowAdjacent) invalidQuery("Adjacency cannot be selected recursively.");
|
|
525
|
-
plan.adjacent =
|
|
526
|
-
if (plan.adjacent !== true && !plan.adjacent.select) invalidQuery("Adjacent entries require explicit selection.");
|
|
461
|
+
plan.adjacent = objectAdjacent(registry, collection, selection);
|
|
527
462
|
} else if (Object.hasOwn(registry[collection].relationships, name)) {
|
|
528
463
|
const relation = registry[collection].relationships[name];
|
|
529
464
|
const nested = objectPlan(registry, relation.collection, selection, relation.type === "many", allowAdjacent);
|
|
530
465
|
if (!nested.select && !nested.adjacent) invalidQuery("Related entries require explicit selection.");
|
|
531
466
|
related[name] = nested;
|
|
532
467
|
} else {
|
|
468
|
+
assertAvailable(registry, collection, name);
|
|
533
469
|
if (selection !== true) invalidQuery("Prepared fields must be selected with true.");
|
|
534
470
|
fields.push(name);
|
|
535
471
|
}
|
|
@@ -558,17 +494,13 @@ function createBuilder(registry, run, plan, many = true, root = true, scopeToken
|
|
|
558
494
|
[scopeSymbol]: scopeToken,
|
|
559
495
|
select(...fields) {
|
|
560
496
|
if (plan.select || fields.length === 0) invalidQuery("select() requires distinct prepared fields and can be called once per scope.");
|
|
561
|
-
return branch({
|
|
562
|
-
select: checkedFields(registry, plan.collection, fields),
|
|
563
|
-
allFields: void 0,
|
|
564
|
-
exclude: void 0
|
|
565
|
-
});
|
|
497
|
+
return branch({ select: checkedFields(registry, plan.collection, fields) });
|
|
566
498
|
},
|
|
567
499
|
where(configure) {
|
|
568
500
|
if (typeof configure !== "function") invalidQuery("where() requires a configuration callback.");
|
|
569
|
-
const expression = configure(
|
|
501
|
+
const expression = configure(fieldProxy());
|
|
570
502
|
validateExpression(expression);
|
|
571
|
-
return branch({ filters: [...plan.filters, detach(expression)] });
|
|
503
|
+
return branch({ filters: [...plan.filters, detach(resolveRelations(registry, plan.collection, expression))] });
|
|
572
504
|
},
|
|
573
505
|
orderBy(field, direction = "asc") {
|
|
574
506
|
return branch({ order: checkedOrder(plan, field, direction, many) });
|
|
@@ -581,7 +513,10 @@ function createBuilder(registry, run, plan, many = true, root = true, scopeToken
|
|
|
581
513
|
},
|
|
582
514
|
with(name, configure) {
|
|
583
515
|
const relation = Object.hasOwn(scope.relationships, name) ? scope.relationships[name] : void 0;
|
|
584
|
-
if (!relation || Object.hasOwn(plan.with, name) || typeof configure !== "function")
|
|
516
|
+
if (!relation || Object.hasOwn(plan.with, name) || typeof configure !== "function") {
|
|
517
|
+
assertAvailable(registry, plan.collection, name);
|
|
518
|
+
invalidQuery("Unknown, repeated, or invalid relationship request.");
|
|
519
|
+
}
|
|
585
520
|
const nestedScope = {};
|
|
586
521
|
const configured = configure(createBuilder(registry, run, emptyPlan(relation.collection), relation.type === "many", false, nestedScope, allowAdjacent));
|
|
587
522
|
const nested = configured && typeof configured === "object" && planSymbol in configured ? configured[planSymbol] : void 0;
|
|
@@ -598,61 +533,373 @@ function createBuilder(registry, run, plan, many = true, root = true, scopeToken
|
|
|
598
533
|
[name]: checkedPayload(registry, plan.collection, name, variant)
|
|
599
534
|
} });
|
|
600
535
|
},
|
|
601
|
-
|
|
536
|
+
/**
|
|
537
|
+
* Selects previous and next entries. Field names select neighbors within this scope's results;
|
|
538
|
+
* a callback or options object describes an independent sibling scope.
|
|
539
|
+
*/
|
|
540
|
+
selectAdjacent(...arguments_) {
|
|
602
541
|
if (!allowAdjacent || plan.adjacent !== void 0) invalidQuery("Adjacency cannot be repeated or selected recursively.");
|
|
603
|
-
|
|
542
|
+
const [configure] = arguments_;
|
|
543
|
+
if (configure === void 0 || typeof configure === "string") {
|
|
544
|
+
if (!arguments_.length || arguments_.some((field) => typeof field !== "string")) invalidQuery("selectAdjacent() requires neighbor fields, such as selectAdjacent(\"id\"), or a sibling query.");
|
|
545
|
+
const selection = {
|
|
546
|
+
...emptyPlan(plan.collection),
|
|
547
|
+
select: checkedFields(registry, plan.collection, arguments_)
|
|
548
|
+
};
|
|
549
|
+
return branch({ adjacent: {
|
|
550
|
+
scope: "results",
|
|
551
|
+
plan: selection
|
|
552
|
+
} });
|
|
553
|
+
}
|
|
604
554
|
if (typeof configure !== "function") {
|
|
605
555
|
const adjacent = objectPlan(registry, plan.collection, configure, true, false, false);
|
|
606
|
-
if (!adjacent
|
|
607
|
-
return branch({ adjacent
|
|
556
|
+
if (!hasSelection(adjacent)) invalidQuery("Adjacent entries require explicit selection.");
|
|
557
|
+
return branch({ adjacent: {
|
|
558
|
+
scope: "collection",
|
|
559
|
+
plan: adjacent
|
|
560
|
+
} });
|
|
608
561
|
}
|
|
609
562
|
const nestedScope = {};
|
|
610
563
|
const configured = configure(createBuilder(registry, run, emptyPlan(plan.collection), true, false, nestedScope, false, false));
|
|
611
564
|
const adjacent = configured && typeof configured === "object" && planSymbol in configured ? configured[planSymbol] : void 0;
|
|
612
565
|
if (!hasSelection(adjacent) || !(scopeSymbol in configured) || configured[scopeSymbol] !== nestedScope) invalidQuery("selectAdjacent() must return its explicitly selected sibling query.");
|
|
613
|
-
return branch({ adjacent
|
|
566
|
+
return branch({ adjacent: {
|
|
567
|
+
scope: "collection",
|
|
568
|
+
plan: adjacent
|
|
569
|
+
} });
|
|
614
570
|
},
|
|
615
571
|
all() {
|
|
616
|
-
|
|
617
|
-
return run(plan, "all");
|
|
572
|
+
return rows("all");
|
|
618
573
|
},
|
|
619
574
|
/** Executes a selected root query, matching direct await behavior. */
|
|
620
575
|
execute() {
|
|
621
|
-
|
|
622
|
-
return run(plan, "all");
|
|
576
|
+
return rows("all");
|
|
623
577
|
},
|
|
624
578
|
first() {
|
|
625
|
-
|
|
626
|
-
return run(plan, "first");
|
|
579
|
+
return rows("first");
|
|
627
580
|
},
|
|
581
|
+
/** Counts matching entries; counting returns no rows, so it needs no selection. */
|
|
628
582
|
count() {
|
|
629
583
|
if (!root) invalidQuery("Nested builders have no terminal methods.");
|
|
630
584
|
return run(plan, "count");
|
|
585
|
+
},
|
|
586
|
+
/**
|
|
587
|
+
* Returns one page with page-number details for `{ page, size }`, or cursor details for
|
|
588
|
+
* `{ size, after?, before?, total? }`, from one revision.
|
|
589
|
+
*/
|
|
590
|
+
paginate(options) {
|
|
591
|
+
if (!root) invalidQuery("Nested builders have no terminal methods.");
|
|
592
|
+
if (!hasSelection(plan)) invalidQuery(NO_SELECTION_MESSAGE);
|
|
593
|
+
return run({
|
|
594
|
+
...plan,
|
|
595
|
+
paginate: checkedPaginate(plan, options)
|
|
596
|
+
}, "paginate");
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
if (root) Object.defineProperty(builder, "then", { value: (fulfilled, rejected) => {
|
|
600
|
+
let result;
|
|
601
|
+
try {
|
|
602
|
+
result = rows("all");
|
|
603
|
+
} catch (cause) {
|
|
604
|
+
result = Promise.reject(cause);
|
|
605
|
+
}
|
|
606
|
+
return result.then(fulfilled, rejected);
|
|
607
|
+
} });
|
|
608
|
+
/**
|
|
609
|
+
* Runs a row-returning terminal. Awaiting an unselected root rejects rather than resolving to the
|
|
610
|
+
* builder, so a missing selection fails loudly in JavaScript as well as in TypeScript.
|
|
611
|
+
*/
|
|
612
|
+
function rows(terminal) {
|
|
613
|
+
if (!root) invalidQuery("Nested builders have no terminal methods.");
|
|
614
|
+
if (!hasSelection(plan)) invalidQuery(NO_SELECTION_MESSAGE);
|
|
615
|
+
return run(plan, terminal);
|
|
616
|
+
}
|
|
617
|
+
/** Checks pagination before creating an independent branch. */
|
|
618
|
+
function pagination(key, value) {
|
|
619
|
+
if (!allowPagination || plan[key] !== void 0) invalidQuery("Pagination requires a nonnegative safe integer, one call per clause, and a many-valued scope.");
|
|
620
|
+
return branch({ [key]: checkedPagination(value, many) });
|
|
621
|
+
}
|
|
622
|
+
return builder;
|
|
623
|
+
}
|
|
624
|
+
//#endregion
|
|
625
|
+
//#region src/query/from.ts
|
|
626
|
+
/**
|
|
627
|
+
* Starts a fluent root query over one collection. The builder selects nothing until `select()`,
|
|
628
|
+
* `with()`, `withPayload()`, or `selectAdjacent()` requests data; `count()` needs no selection.
|
|
629
|
+
*
|
|
630
|
+
* @param registry - Reads the current query registry, so development builders bind the latest one.
|
|
631
|
+
* @param run - Executes a plan against one pinned revision.
|
|
632
|
+
* @param collection - The collection to query.
|
|
633
|
+
* @returns An immutable, reusable root builder.
|
|
634
|
+
*/
|
|
635
|
+
function createFrom(registry, run, collection) {
|
|
636
|
+
const current = registry();
|
|
637
|
+
if (typeof collection !== "string" || !Object.hasOwn(current, collection)) invalidQuery(`Unknown collection "${String(collection)}".`);
|
|
638
|
+
return createBuilder(current, run, emptyPlan(collection));
|
|
639
|
+
}
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region src/query/relational.ts
|
|
642
|
+
const operators = {
|
|
643
|
+
eq: "eq",
|
|
644
|
+
ne: "ne",
|
|
645
|
+
gt: "gt",
|
|
646
|
+
gte: "gte",
|
|
647
|
+
lt: "lt",
|
|
648
|
+
lte: "lte",
|
|
649
|
+
in: "in-array",
|
|
650
|
+
notIn: "not-in-array",
|
|
651
|
+
like: "like",
|
|
652
|
+
ilike: "ilike",
|
|
653
|
+
notLike: "like",
|
|
654
|
+
notIlike: "ilike",
|
|
655
|
+
contains: "contains",
|
|
656
|
+
startsWith: "starts-with",
|
|
657
|
+
endsWith: "ends-with",
|
|
658
|
+
arrayContains: "array-contains-all",
|
|
659
|
+
arrayOverlaps: "array-contains-any",
|
|
660
|
+
arrayContained: "array-contained",
|
|
661
|
+
isNull: "is-null",
|
|
662
|
+
isNotNull: "is-null",
|
|
663
|
+
isUndefined: "is-undefined",
|
|
664
|
+
isMissing: "is-missing"
|
|
665
|
+
};
|
|
666
|
+
/** Requires plain records at each relational options and filter boundary. */
|
|
667
|
+
function record(value, label) {
|
|
668
|
+
if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) invalidQuery(`${label} must be an options object.`);
|
|
669
|
+
}
|
|
670
|
+
/** Compiles a field condition, including field-local boolean composition, into portable expressions. */
|
|
671
|
+
function fieldCondition(field, condition) {
|
|
672
|
+
if (isScalar(condition)) return {
|
|
673
|
+
kind: "predicate",
|
|
674
|
+
operator: "eq",
|
|
675
|
+
field,
|
|
676
|
+
value: condition
|
|
677
|
+
};
|
|
678
|
+
record(condition, `Filter for "${field}"`);
|
|
679
|
+
const expressions = [];
|
|
680
|
+
for (const [name, value] of Object.entries(condition)) {
|
|
681
|
+
if (name === "AND" || name === "OR") {
|
|
682
|
+
if (!Array.isArray(value)) invalidQuery("Field AND/OR filters require arrays.");
|
|
683
|
+
const children = value.map((child) => fieldCondition(field, child));
|
|
684
|
+
expressions.push(name === "AND" ? and(...children) : or(...children));
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
if (name === "NOT") {
|
|
688
|
+
expressions.push(not(fieldCondition(field, value)));
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
if (!Object.hasOwn(operators, name)) invalidQuery(`Unknown filter operator "${name}".`);
|
|
692
|
+
const expression = {
|
|
693
|
+
kind: "predicate",
|
|
694
|
+
operator: operators[name],
|
|
695
|
+
field,
|
|
696
|
+
value
|
|
697
|
+
};
|
|
698
|
+
if (name.startsWith("is")) {
|
|
699
|
+
if (typeof value !== "boolean") invalidQuery("Presence predicates require boolean values.");
|
|
700
|
+
expressions.push(value !== (name === "isNotNull") ? expression : not(expression));
|
|
701
|
+
} else expressions.push(name === "notLike" || name === "notIlike" ? not(expression) : expression);
|
|
702
|
+
}
|
|
703
|
+
return and(...expressions);
|
|
704
|
+
}
|
|
705
|
+
/** Compiles object predicates and existential relationship filters without loading bodies. */
|
|
706
|
+
function relationalWhere(registry, collection, where) {
|
|
707
|
+
record(where, "where");
|
|
708
|
+
const expressions = [];
|
|
709
|
+
for (const [field, condition] of Object.entries(where)) if (field === "AND" || field === "OR" || field === "NOT") {
|
|
710
|
+
if (field === "OR" && !Array.isArray(condition)) invalidQuery("OR filters require an array.");
|
|
711
|
+
const children = (Array.isArray(condition) ? condition : [condition]).map((child) => relationalWhere(registry, collection, child));
|
|
712
|
+
expressions.push(field === "OR" ? or(...children) : field === "NOT" ? and(...children.map(not)) : and(...children));
|
|
713
|
+
} else if (field === "RAW") {
|
|
714
|
+
if (typeof condition !== "function") invalidQuery("RAW requires a structured-expression callback.");
|
|
715
|
+
const expression = condition(fieldProxy());
|
|
716
|
+
validateExpression(expression);
|
|
717
|
+
expressions.push(resolveRelations(registry, collection, expression));
|
|
718
|
+
} else if (Object.hasOwn(registry[collection].relationships, field)) {
|
|
719
|
+
const relation = registry[collection].relationships[field];
|
|
720
|
+
const expression = {
|
|
721
|
+
kind: "relation",
|
|
722
|
+
collection,
|
|
723
|
+
field,
|
|
724
|
+
target: relation.collection,
|
|
725
|
+
expression: relationalWhere(registry, relation.collection, typeof condition === "boolean" ? {} : condition)
|
|
726
|
+
};
|
|
727
|
+
expressions.push(condition === false ? not(expression) : expression);
|
|
728
|
+
} else {
|
|
729
|
+
assertAvailable(registry, collection, field);
|
|
730
|
+
expressions.push(fieldCondition(field, condition));
|
|
731
|
+
}
|
|
732
|
+
const expression = and(...expressions);
|
|
733
|
+
validateExpression(expression);
|
|
734
|
+
return detach(expression);
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Compiles Drizzle-style columns/with options into the shared immutable query plan.
|
|
738
|
+
* Payloads are allowed wherever adjacency is, so sibling scopes never load them.
|
|
739
|
+
*/
|
|
740
|
+
function relationalPlan(registry, collection, options = {}, many = true, allowAdjacent = true, pagination = true) {
|
|
741
|
+
if (!Object.hasOwn(registry, collection)) invalidQuery(`Unknown collection "${collection}".`);
|
|
742
|
+
record(options, "Query");
|
|
743
|
+
const allowed = [
|
|
744
|
+
"columns",
|
|
745
|
+
"where",
|
|
746
|
+
"with",
|
|
747
|
+
...many ? ["orderBy"] : [],
|
|
748
|
+
...many && pagination ? ["limit", "offset"] : [],
|
|
749
|
+
...allowAdjacent ? ["adjacent", "payloads"] : []
|
|
750
|
+
];
|
|
751
|
+
if (Object.keys(options).some((key) => !allowed.includes(key))) invalidQuery("Unknown relational query option or unsupported scope option.");
|
|
752
|
+
const plan = emptyPlan(collection);
|
|
753
|
+
if (options.where !== void 0) plan.filters = [relationalWhere(registry, collection, options.where)];
|
|
754
|
+
if (options.columns !== void 0) {
|
|
755
|
+
record(options.columns, "columns");
|
|
756
|
+
const columns = options.columns;
|
|
757
|
+
if (Object.values(columns).some((value) => value !== true)) invalidQuery("Columns list the fields to include with `true`; omit fields you do not need.");
|
|
758
|
+
plan.select = checkedFields(registry, collection, Object.keys(columns));
|
|
759
|
+
}
|
|
760
|
+
if (options.with !== void 0) {
|
|
761
|
+
record(options.with, "with");
|
|
762
|
+
const related = Object.create(null);
|
|
763
|
+
for (const [name, selected] of Object.entries(options.with)) {
|
|
764
|
+
const relation = Object.hasOwn(registry[collection].relationships, name) && registry[collection].relationships[name];
|
|
765
|
+
if (!relation) {
|
|
766
|
+
assertAvailable(registry, collection, name);
|
|
767
|
+
invalidQuery(`Unknown relationship "${name}".`);
|
|
768
|
+
}
|
|
769
|
+
if (selected === false || selected === void 0) continue;
|
|
770
|
+
if (selected === true) invalidQuery(`Relationship "${name}" requires nested options that select data, such as { columns: { id: true } }.`);
|
|
771
|
+
related[name] = selectedPlan(relationalPlan(registry, relation.collection, selected, relation.type === "many", allowAdjacent));
|
|
631
772
|
}
|
|
773
|
+
plan.with = related;
|
|
774
|
+
}
|
|
775
|
+
if (options.orderBy !== void 0) {
|
|
776
|
+
record(options.orderBy, "orderBy");
|
|
777
|
+
if (!Object.keys(options.orderBy).length) invalidQuery("orderBy requires a field.");
|
|
778
|
+
for (const [field, direction] of Object.entries(options.orderBy)) plan.order = checkedOrder(plan, field, direction, many);
|
|
779
|
+
}
|
|
780
|
+
for (const key of ["limit", "offset"]) if (options[key] !== void 0) plan[key] = checkedPagination(options[key], many);
|
|
781
|
+
if (options.payloads !== void 0) {
|
|
782
|
+
record(options.payloads, "payloads");
|
|
783
|
+
const payloads = checkedPayloads(registry, collection, options.payloads);
|
|
784
|
+
if (payloads) plan.payloads = payloads;
|
|
785
|
+
}
|
|
786
|
+
if (options.adjacent !== void 0) plan.adjacent = relationalAdjacent(registry, collection, options.adjacent);
|
|
787
|
+
return plan;
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Compiles relational `adjacent` options. `{ scope: "results", columns, with }` selects neighbors
|
|
791
|
+
* within the current results; other options describe an independent sibling scope.
|
|
792
|
+
*
|
|
793
|
+
* @param registry - The query registry.
|
|
794
|
+
* @param collection - The collection whose neighbors are selected.
|
|
795
|
+
* @param adjacent - The `adjacent` option value.
|
|
796
|
+
* @returns The adjacency plan, which always selects explicit data.
|
|
797
|
+
*/
|
|
798
|
+
function relationalAdjacent(registry, collection, adjacent) {
|
|
799
|
+
if (adjacent === true) invalidQuery("Adjacent entries select their own data, such as { scope: \"results\", columns: { id: true } }.");
|
|
800
|
+
record(adjacent, "adjacent");
|
|
801
|
+
const { scope, ...options } = adjacent;
|
|
802
|
+
if (scope !== void 0 && scope !== "results" && scope !== "collection") invalidQuery("Adjacent scope must be \"results\" or \"collection\".");
|
|
803
|
+
if (scope === "results" && Object.keys(options).some((key) => key !== "columns" && key !== "with")) invalidQuery("Results-scope neighbors only select data; they share the query's filters and order.");
|
|
804
|
+
return {
|
|
805
|
+
scope: scope === "results" ? "results" : "collection",
|
|
806
|
+
plan: selectedPlan(relationalPlan(registry, collection, options, true, false, false))
|
|
632
807
|
};
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Rejects a row-returning scope that requests no data.
|
|
811
|
+
*
|
|
812
|
+
* @param plan - A root, relationship, or sibling scope.
|
|
813
|
+
* @returns The same plan when it selects fields, relationships, payloads, or adjacency.
|
|
814
|
+
*/
|
|
815
|
+
function selectedPlan(plan) {
|
|
816
|
+
if (!hasSelection(plan)) invalidQuery(NO_SELECTION_MESSAGE);
|
|
817
|
+
return plan;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Separates `paginate()` options from the query options they accompany.
|
|
821
|
+
*
|
|
822
|
+
* @param options - Relational query options plus `page`, `size`, `after`, `before`, or `total`.
|
|
823
|
+
* @returns The query options, then the pagination options.
|
|
824
|
+
*/
|
|
825
|
+
function splitPaginate(options) {
|
|
826
|
+
record(options, "Query");
|
|
827
|
+
if (Object.hasOwn(options, "limit") || Object.hasOwn(options, "offset")) invalidQuery(PAGINATE_AFTER_LIMIT_MESSAGE);
|
|
828
|
+
const query = {};
|
|
829
|
+
const request = {};
|
|
830
|
+
for (const [key, value] of Object.entries(options)) (PAGINATE_KEYS.includes(key) ? request : query)[key] = value;
|
|
831
|
+
return [query, request];
|
|
832
|
+
}
|
|
833
|
+
/** Creates stable, live collection queries in a dedicated, prototype-free namespace. */
|
|
834
|
+
function createQueryNamespace(registry, run) {
|
|
835
|
+
const queries = /* @__PURE__ */ new Map();
|
|
836
|
+
/** Reuses each handle while checking the current registry on every query execution. */
|
|
837
|
+
function query(name) {
|
|
838
|
+
let cached = queries.get(name);
|
|
839
|
+
if (!cached) {
|
|
840
|
+
cached = {
|
|
841
|
+
findMany: (options) => run(selectedPlan(relationalPlan(registry(), name, options)), "all"),
|
|
842
|
+
findFirst: (options) => run(selectedPlan(relationalPlan(registry(), name, options)), "first"),
|
|
843
|
+
paginate: (options) => {
|
|
844
|
+
const [query, request] = splitPaginate(options);
|
|
845
|
+
const plan = selectedPlan(relationalPlan(registry(), name, query, true, true, false));
|
|
846
|
+
return run({
|
|
847
|
+
...plan,
|
|
848
|
+
paginate: checkedPaginate(plan, request)
|
|
849
|
+
}, "paginate");
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
queries.set(name, cached);
|
|
853
|
+
}
|
|
854
|
+
return cached;
|
|
638
855
|
}
|
|
639
|
-
return
|
|
856
|
+
return new Proxy(Object.create(null), {
|
|
857
|
+
get(_target, name) {
|
|
858
|
+
return typeof name === "string" && Object.hasOwn(registry(), name) ? query(name) : void 0;
|
|
859
|
+
},
|
|
860
|
+
ownKeys() {
|
|
861
|
+
return Object.keys(registry());
|
|
862
|
+
},
|
|
863
|
+
getOwnPropertyDescriptor(_target, name) {
|
|
864
|
+
return typeof name === "string" && Object.hasOwn(registry(), name) ? {
|
|
865
|
+
configurable: true,
|
|
866
|
+
enumerable: true,
|
|
867
|
+
value: query(name),
|
|
868
|
+
writable: false
|
|
869
|
+
} : void 0;
|
|
870
|
+
}
|
|
871
|
+
});
|
|
640
872
|
}
|
|
641
873
|
//#endregion
|
|
642
|
-
//#region src/
|
|
643
|
-
/**
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
874
|
+
//#region src/shared/concurrency.ts
|
|
875
|
+
/**
|
|
876
|
+
* Maps items through an asynchronous callback with at most `limit` callbacks in flight.
|
|
877
|
+
* Results keep input order; the first rejection rejects the whole map after started work settles.
|
|
878
|
+
*
|
|
879
|
+
* @param items - Values to process.
|
|
880
|
+
* @param limit - Maximum number of concurrent callbacks; values below one are treated as one.
|
|
881
|
+
* @param callback - Asynchronous work for one item and its index.
|
|
882
|
+
* @returns The callback results in input order.
|
|
883
|
+
*/
|
|
884
|
+
async function mapConcurrent(items, limit, callback) {
|
|
885
|
+
const results = Array.from({ length: items.length });
|
|
886
|
+
let next = 0;
|
|
887
|
+
let failure;
|
|
888
|
+
/** Claims the next unprocessed item until the list is exhausted or work has failed. */
|
|
889
|
+
async function worker() {
|
|
890
|
+
while (next < items.length && !failure) {
|
|
891
|
+
const index = next++;
|
|
892
|
+
try {
|
|
893
|
+
results[index] = await callback(items[index], index);
|
|
894
|
+
} catch (cause) {
|
|
895
|
+
failure ??= { cause };
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
const workers = Array.from({ length: Math.min(Math.max(1, limit), items.length) }, worker);
|
|
900
|
+
await Promise.all(workers);
|
|
901
|
+
if (failure) throw failure.cause;
|
|
902
|
+
return results;
|
|
656
903
|
}
|
|
657
904
|
//#endregion
|
|
658
905
|
//#region src/query/normalize.ts
|
|
@@ -667,7 +914,10 @@ function normalizePlan(plan) {
|
|
|
667
914
|
...plan.select === void 0 && Object.keys(plan.with).length ? { select: [] } : {},
|
|
668
915
|
filters: plan.filters.flatMap(conjunction),
|
|
669
916
|
with: Object.fromEntries(Object.entries(plan.with).map(([name, nested]) => [name, normalizePlan(nested)])),
|
|
670
|
-
...plan.adjacent
|
|
917
|
+
...plan.adjacent ? { adjacent: {
|
|
918
|
+
scope: plan.adjacent.scope,
|
|
919
|
+
plan: normalizePlan(plan.adjacent.plan)
|
|
920
|
+
} } : {}
|
|
671
921
|
};
|
|
672
922
|
}
|
|
673
923
|
//#endregion
|
|
@@ -698,6 +948,7 @@ function matches(record, expression, revision, collection) {
|
|
|
698
948
|
if (!revision || !relation || relation.collection !== expression.target) invalidQuery("Relationship filter no longer matches the prepared collection scope.");
|
|
699
949
|
return (revision.relations.get(relationKey(expression.collection, expression.field))?.get(record.id) ?? []).some((candidate) => matches(candidate, expression.expression, revision, expression.target));
|
|
700
950
|
}
|
|
951
|
+
if (expression.kind === "has") invalidQuery("has() filters must be passed to a query's where() before execution.");
|
|
701
952
|
if (expression.kind !== "predicate") return false;
|
|
702
953
|
const actual = Object.hasOwn(record, expression.field) ? record[expression.field] : void 0;
|
|
703
954
|
const expected = expression.value;
|
|
@@ -744,25 +995,32 @@ function queryCandidates(records, plan) {
|
|
|
744
995
|
function sortGroup(value) {
|
|
745
996
|
return value == null ? 2 : typeof value === "number" && Number.isNaN(value) || value instanceof Date && Number.isNaN(value.getTime()) ? 1 : 0;
|
|
746
997
|
}
|
|
747
|
-
/**
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
998
|
+
/**
|
|
999
|
+
* Orders two entries by their sort values. Null, undefined, and missing values sort last in both
|
|
1000
|
+
* directions, preceded by `NaN` and invalid dates; ties fall through to the next clause.
|
|
1001
|
+
*/
|
|
1002
|
+
function compareKeys(left, right, order) {
|
|
1003
|
+
for (const [index, sort] of order.entries()) {
|
|
1004
|
+
const leftValue = left[index];
|
|
1005
|
+
const rightValue = right[index];
|
|
1006
|
+
const group = sortGroup(leftValue) - sortGroup(rightValue);
|
|
1007
|
+
if (group) return group;
|
|
1008
|
+
if (sortGroup(leftValue) !== 0) continue;
|
|
1009
|
+
const comparison = compare(leftValue, rightValue) ?? 0;
|
|
1010
|
+
if (comparison) return sort.direction === "desc" ? -comparison : comparison;
|
|
752
1011
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
field:
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
for (const sort of order) {
|
|
1012
|
+
return 0;
|
|
1013
|
+
}
|
|
1014
|
+
/** Filters and orders one collection or one parent's related candidates, keeping sort keys. */
|
|
1015
|
+
function orderedEntries(records, plan, revision, order) {
|
|
1016
|
+
const entries = records.filter((record) => plan.filters.every((expression) => matches(record, expression, revision, plan.collection))).map((record) => ({
|
|
1017
|
+
record,
|
|
1018
|
+
key: order.map((sort) => Object.hasOwn(record, sort.field) ? record[sort.field] : void 0)
|
|
1019
|
+
}));
|
|
1020
|
+
for (const [index, sort] of order.entries()) {
|
|
763
1021
|
let category;
|
|
764
|
-
for (const
|
|
765
|
-
const value =
|
|
1022
|
+
for (const { key } of entries) {
|
|
1023
|
+
const value = key[index];
|
|
766
1024
|
if (!isScalar(value)) invalidQuery(`Field "${sort.field}" is not sortable.`);
|
|
767
1025
|
if (value != null) {
|
|
768
1026
|
const current = value instanceof Date ? "date" : typeof value;
|
|
@@ -771,36 +1029,86 @@ function pageRecords(records, plan, revision, preserveOrder = false) {
|
|
|
771
1029
|
}
|
|
772
1030
|
}
|
|
773
1031
|
}
|
|
774
|
-
if (order.length)
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
}
|
|
784
|
-
return 0;
|
|
785
|
-
});
|
|
1032
|
+
if (order.length) entries.sort((left, right) => compareKeys(left.key, right.key, order));
|
|
1033
|
+
return entries;
|
|
1034
|
+
}
|
|
1035
|
+
/** Filters, orders, and pages one collection or one parent's related candidates. */
|
|
1036
|
+
function pageRecords(records, plan, revision, preserveOrder = false) {
|
|
1037
|
+
if (records.length <= 1 && !plan.order.length) {
|
|
1038
|
+
const record = records[0];
|
|
1039
|
+
return record && plan.filters.every((expression) => matches(record, expression, revision, plan.collection)) && (plan.offset ?? 0) === 0 && plan.limit !== 0 ? [record] : [];
|
|
1040
|
+
}
|
|
786
1041
|
const offset = plan.offset ?? 0;
|
|
787
|
-
return
|
|
1042
|
+
return orderedEntries(records, plan, revision, effectiveOrder(plan, preserveOrder)).slice(offset, plan.limit === void 0 ? void 0 : offset + plan.limit).map((entry) => entry.record);
|
|
1043
|
+
}
|
|
1044
|
+
/** Finds the first position where a predicate that flips from false to true once becomes true. */
|
|
1045
|
+
function firstIndex(items, predicate) {
|
|
1046
|
+
let low = 0;
|
|
1047
|
+
let high = items.length;
|
|
1048
|
+
while (low < high) {
|
|
1049
|
+
const middle = low + high >>> 1;
|
|
1050
|
+
if (predicate(items[middle])) high = middle;
|
|
1051
|
+
else low = middle + 1;
|
|
1052
|
+
}
|
|
1053
|
+
return low;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Selects one page of a root scope's ordered entries and describes where it sits. Page numbers
|
|
1057
|
+
* count from 1, and page 1 exists even when nothing matches. Cursors position a page strictly after
|
|
1058
|
+
* or before the boundary entry's sort values, so a deleted boundary entry still pages correctly.
|
|
1059
|
+
*/
|
|
1060
|
+
function pageOf(entries, request, order, collection) {
|
|
1061
|
+
if (request.mode === "page") {
|
|
1062
|
+
const total = entries.length;
|
|
1063
|
+
const pageCount = Math.max(1, Math.ceil(total / request.size));
|
|
1064
|
+
const start = (request.page - 1) * request.size;
|
|
1065
|
+
return {
|
|
1066
|
+
entries: entries.slice(start, start + request.size),
|
|
1067
|
+
info: {
|
|
1068
|
+
total,
|
|
1069
|
+
page: request.page,
|
|
1070
|
+
pageCount,
|
|
1071
|
+
hasNextPage: request.page < pageCount,
|
|
1072
|
+
hasPreviousPage: request.page > 1
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
const { key } = request;
|
|
1077
|
+
let start;
|
|
1078
|
+
let end;
|
|
1079
|
+
if (request.direction === "after") {
|
|
1080
|
+
start = key ? firstIndex(entries, (entry) => compareKeys(entry.key, key, order) > 0) : 0;
|
|
1081
|
+
end = Math.min(entries.length, start + request.size);
|
|
1082
|
+
} else {
|
|
1083
|
+
end = key ? firstIndex(entries, (entry) => compareKeys(entry.key, key, order) >= 0) : entries.length;
|
|
1084
|
+
start = Math.max(0, end - request.size);
|
|
1085
|
+
}
|
|
1086
|
+
const page = entries.slice(start, end);
|
|
1087
|
+
return {
|
|
1088
|
+
entries: page,
|
|
1089
|
+
info: {
|
|
1090
|
+
startCursor: page.length ? encodeCursor(collection, order, page[0].key) : null,
|
|
1091
|
+
endCursor: page.length ? encodeCursor(collection, order, page.at(-1).key) : null,
|
|
1092
|
+
hasNextPage: end < entries.length,
|
|
1093
|
+
hasPreviousPage: start > 0,
|
|
1094
|
+
...request.total ? { total: entries.length } : {}
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
788
1097
|
}
|
|
789
1098
|
/** Builds a sibling index once per scope, before pagination and within the pinned revision. */
|
|
790
1099
|
function neighborScope(plan, records, preserveOrder, scopes, revision) {
|
|
791
|
-
const selection = plan.adjacent;
|
|
792
|
-
const siblingPlan =
|
|
1100
|
+
const { scope: neighborsScope, plan: selection } = plan.adjacent;
|
|
1101
|
+
const siblingPlan = neighborsScope === "results" ? {
|
|
793
1102
|
...plan,
|
|
794
|
-
select:
|
|
795
|
-
|
|
796
|
-
exclude: void 0,
|
|
797
|
-
with: {},
|
|
1103
|
+
select: selection.select,
|
|
1104
|
+
with: selection.with,
|
|
798
1105
|
adjacent: void 0,
|
|
799
1106
|
payloads: void 0,
|
|
800
1107
|
limit: void 0,
|
|
801
|
-
offset: void 0
|
|
1108
|
+
offset: void 0,
|
|
1109
|
+
paginate: void 0
|
|
802
1110
|
} : selection;
|
|
803
|
-
const key =
|
|
1111
|
+
const key = neighborsScope === "results" ? plan : siblingPlan;
|
|
804
1112
|
let candidates = scopes.get(key);
|
|
805
1113
|
if (!candidates) {
|
|
806
1114
|
candidates = /* @__PURE__ */ new WeakMap();
|
|
@@ -808,7 +1116,7 @@ function neighborScope(plan, records, preserveOrder, scopes, revision) {
|
|
|
808
1116
|
}
|
|
809
1117
|
let scope = candidates.get(records);
|
|
810
1118
|
if (!scope) {
|
|
811
|
-
const ordered = pageRecords([...new Map(records.map((entry) => [entry.id, entry])).values()], siblingPlan, revision,
|
|
1119
|
+
const ordered = pageRecords([...new Map(records.map((entry) => [entry.id, entry])).values()], siblingPlan, revision, neighborsScope === "results" && preserveOrder);
|
|
812
1120
|
scope = {
|
|
813
1121
|
plan: siblingPlan,
|
|
814
1122
|
records: ordered,
|
|
@@ -823,9 +1131,8 @@ function neighborScope(plan, records, preserveOrder, scopes, revision) {
|
|
|
823
1131
|
* Requested payloads are recorded for the caller to load after projection.
|
|
824
1132
|
*/
|
|
825
1133
|
function project(record, plan, revision, scopes, scopeRecords, requests, preserveScopeOrder = false) {
|
|
826
|
-
if (!plan
|
|
827
|
-
const
|
|
828
|
-
const result = detachSelection(record, fields);
|
|
1134
|
+
if (!hasSelection(plan)) invalidQuery(NO_SELECTION_MESSAGE);
|
|
1135
|
+
const result = detachSelection(record, plan.select ?? []);
|
|
829
1136
|
for (const [name, nested] of Object.entries(plan.with)) {
|
|
830
1137
|
const relation = revision.registry[plan.collection].relationships[name];
|
|
831
1138
|
if (!relation || relation.collection !== nested.collection || relation.type === "one" && (nested.limit !== void 0 || nested.offset !== void 0 || nested.order.length > 0)) invalidQuery(`Relationship "${name}" no longer matches the prepared query scope.`);
|
|
@@ -840,7 +1147,7 @@ function project(record, plan, revision, scopes, scopeRecords, requests, preserv
|
|
|
840
1147
|
});
|
|
841
1148
|
}
|
|
842
1149
|
if (plan.adjacent) {
|
|
843
|
-
const candidates = plan.adjacent ===
|
|
1150
|
+
const candidates = plan.adjacent.scope === "results" ? scopeRecords : revision.collections[plan.collection].records;
|
|
844
1151
|
const scope = neighborScope(plan, candidates, preserveScopeOrder, scopes, revision);
|
|
845
1152
|
const index = scope.positions.get(record.id);
|
|
846
1153
|
const previous = index === void 0 ? void 0 : scope.records[index - 1];
|
|
@@ -861,6 +1168,13 @@ function project(record, plan, revision, scopes, scopeRecords, requests, preserv
|
|
|
861
1168
|
/**
|
|
862
1169
|
* Runs the shared JavaScript executor; future adapters can execute the same plan.
|
|
863
1170
|
* Rows that requested payloads are appended to `requests` for loading after execution.
|
|
1171
|
+
*
|
|
1172
|
+
* @param revision - The pinned revision to read.
|
|
1173
|
+
* @param plan - A normalized query plan.
|
|
1174
|
+
* @param terminal - `all` returns rows, `first` one row or undefined, `count` a number, and
|
|
1175
|
+
* `paginate` the page object for the plan's `paginate` request.
|
|
1176
|
+
* @param requests - Collects rows that requested payloads.
|
|
1177
|
+
* @returns The terminal's result.
|
|
864
1178
|
*/
|
|
865
1179
|
async function executeQuery(revision, plan, terminal, requests = []) {
|
|
866
1180
|
const collection = revision.collections[plan.collection];
|
|
@@ -869,9 +1183,19 @@ async function executeQuery(revision, plan, terminal, requests = []) {
|
|
|
869
1183
|
if (plan.payloads) invalidQuery("Counts cannot request payloads.");
|
|
870
1184
|
return queryCandidates(collection.records, plan).filter((record) => plan.filters.every((expression) => matches(record, expression, revision, plan.collection))).length;
|
|
871
1185
|
}
|
|
872
|
-
if (!plan
|
|
873
|
-
const records = pageRecords(queryCandidates(collection.records, plan), plan, revision);
|
|
1186
|
+
if (!hasSelection(plan)) invalidQuery(NO_SELECTION_MESSAGE);
|
|
874
1187
|
const scopes = /* @__PURE__ */ new WeakMap();
|
|
1188
|
+
if (terminal === "paginate") {
|
|
1189
|
+
if (!plan.paginate) invalidQuery("paginate requires a page request.");
|
|
1190
|
+
if (plan.limit !== void 0 || plan.offset !== void 0) invalidQuery(PAGINATE_AFTER_LIMIT_MESSAGE);
|
|
1191
|
+
const order = effectiveOrder(plan);
|
|
1192
|
+
const { entries, info } = pageOf(orderedEntries(queryCandidates(collection.records, plan), plan, revision, order), plan.paginate, order, plan.collection);
|
|
1193
|
+
return {
|
|
1194
|
+
items: entries.map((entry) => project(entry.record, plan, revision, scopes, collection.records, requests)),
|
|
1195
|
+
...info
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
const records = pageRecords(queryCandidates(collection.records, plan), plan, revision);
|
|
875
1199
|
return terminal === "first" ? records[0] ? project(records[0], plan, revision, scopes, collection.records, requests) : void 0 : records.map((record) => project(record, plan, revision, scopes, collection.records, requests));
|
|
876
1200
|
}
|
|
877
1201
|
//#endregion
|
|
@@ -951,11 +1275,48 @@ async function renderedPayload(revision, collection, id) {
|
|
|
951
1275
|
}
|
|
952
1276
|
return rendered;
|
|
953
1277
|
}
|
|
1278
|
+
/** Lazily loads one custom payload variant once per revision, retrying after failures. */
|
|
1279
|
+
function loadCustom(revision, collection, id, name, variant) {
|
|
1280
|
+
const entries = revision.collections[collection].payloads?.get(name);
|
|
1281
|
+
if (!entries) invalidQuery(`Payload "${name}" no longer matches the prepared collection.`);
|
|
1282
|
+
const load = entries.get(id)?.get(variantKey(variant));
|
|
1283
|
+
if (!load) return Promise.reject(new ContentError({
|
|
1284
|
+
code: "variant-not-found",
|
|
1285
|
+
operation: "query",
|
|
1286
|
+
collection,
|
|
1287
|
+
id,
|
|
1288
|
+
payload: name,
|
|
1289
|
+
variant,
|
|
1290
|
+
message: `Entry "${id}" has no ${payloadIdentity(name, variant)} payload.`
|
|
1291
|
+
}));
|
|
1292
|
+
const key = JSON.stringify([
|
|
1293
|
+
collection,
|
|
1294
|
+
id,
|
|
1295
|
+
name,
|
|
1296
|
+
variantKey(variant)
|
|
1297
|
+
]);
|
|
1298
|
+
let loaded = revision.payloadCache.get(key);
|
|
1299
|
+
if (!loaded) {
|
|
1300
|
+
loaded = Promise.resolve().then(load).catch((cause) => {
|
|
1301
|
+
revision.payloadCache.delete(key);
|
|
1302
|
+
throw contentError(cause, {
|
|
1303
|
+
code: "artifact-error",
|
|
1304
|
+
collection,
|
|
1305
|
+
id,
|
|
1306
|
+
payload: name,
|
|
1307
|
+
variant,
|
|
1308
|
+
message: `Could not load the prepared ${payloadIdentity(name, variant)} payload.`
|
|
1309
|
+
});
|
|
1310
|
+
});
|
|
1311
|
+
revision.payloadCache.set(key, loaded);
|
|
1312
|
+
}
|
|
1313
|
+
return loaded;
|
|
1314
|
+
}
|
|
954
1315
|
/** Loads one requested payload value from the pinned revision. */
|
|
955
|
-
async function loadPayload(revision, collection, id, name) {
|
|
1316
|
+
async function loadPayload(revision, collection, id, name, variant) {
|
|
956
1317
|
if (name === "_body") return (await loadBuiltin(revision, collection, id)).body;
|
|
957
1318
|
if (name === "_rendered") return renderedPayload(revision, collection, id);
|
|
958
|
-
|
|
1319
|
+
return loadCustom(revision, collection, id, name, variant);
|
|
959
1320
|
}
|
|
960
1321
|
/**
|
|
961
1322
|
* Loads every requested payload with bounded concurrency, then attaches detached copies under
|
|
@@ -974,13 +1335,14 @@ async function attachPayloads(revision, requests) {
|
|
|
974
1335
|
if (!loads.has(key)) loads.set(key, {
|
|
975
1336
|
collection: request.collection,
|
|
976
1337
|
id: request.id,
|
|
977
|
-
name
|
|
1338
|
+
name,
|
|
1339
|
+
variant: request.payloads[name].variant
|
|
978
1340
|
});
|
|
979
1341
|
}
|
|
980
1342
|
const keys = [...loads.keys()];
|
|
981
1343
|
const values = await mapConcurrent(keys, PAYLOAD_CONCURRENCY, (key) => {
|
|
982
|
-
const { collection, id, name } = loads.get(key);
|
|
983
|
-
return loadPayload(revision, collection, id, name);
|
|
1344
|
+
const { collection, id, name, variant } = loads.get(key);
|
|
1345
|
+
return loadPayload(revision, collection, id, name, variant);
|
|
984
1346
|
});
|
|
985
1347
|
const loaded = new Map(keys.map((key, index) => [key, values[index]]));
|
|
986
1348
|
for (const request of requests) request.target._payloads = Object.fromEntries(Object.keys(request.payloads).map((name) => [name, detach(loaded.get(JSON.stringify([
|
|
@@ -990,33 +1352,325 @@ async function attachPayloads(revision, requests) {
|
|
|
990
1352
|
request.payloads[name]
|
|
991
1353
|
])))]));
|
|
992
1354
|
}
|
|
993
|
-
/**
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1355
|
+
/**
|
|
1356
|
+
* Creates the query runner that pins plan execution and payload loading to one revision.
|
|
1357
|
+
*
|
|
1358
|
+
* @param state - Mutable session, artifact, or combined state; each query reads its revision.
|
|
1359
|
+
* @returns A runner for fluent and relational plans.
|
|
1360
|
+
*/
|
|
1361
|
+
function createRunner(state) {
|
|
1362
|
+
return (plan, terminal) => pinned(state, async (revision) => {
|
|
997
1363
|
const normalized = normalizePlan(plan);
|
|
998
1364
|
const requests = [];
|
|
999
1365
|
const result = revision.execute ? await revision.execute(normalized, terminal, requests) : await executeQuery(revision, normalized, terminal, requests);
|
|
1000
1366
|
await attachPayloads(revision, requests);
|
|
1001
1367
|
return result;
|
|
1002
1368
|
});
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1369
|
+
}
|
|
1370
|
+
//#endregion
|
|
1371
|
+
//#region src/runtime/query-style.ts
|
|
1372
|
+
/**
|
|
1373
|
+
* Collection members that start each query style. Every other query API hangs off these members,
|
|
1374
|
+
* so gating them gates payloads, relationships, and adjacency as well. `count()` without options
|
|
1375
|
+
* belongs to both styles; `count(options)` takes relational options.
|
|
1376
|
+
*/
|
|
1377
|
+
const queryStyleMembers = {
|
|
1378
|
+
fluent: [
|
|
1379
|
+
"select",
|
|
1380
|
+
"where",
|
|
1381
|
+
"orderBy",
|
|
1382
|
+
"limit",
|
|
1383
|
+
"offset",
|
|
1384
|
+
"with",
|
|
1385
|
+
"withPayload",
|
|
1386
|
+
"selectAdjacent"
|
|
1387
|
+
],
|
|
1388
|
+
relational: [
|
|
1389
|
+
"findMany",
|
|
1390
|
+
"findFirst",
|
|
1391
|
+
"paginate",
|
|
1392
|
+
"count(options)"
|
|
1393
|
+
]
|
|
1394
|
+
};
|
|
1395
|
+
/**
|
|
1396
|
+
* Checks whether a configured or manifest value names a supported query style.
|
|
1397
|
+
*
|
|
1398
|
+
* @param value - The untrusted `queryStyle` value.
|
|
1399
|
+
* @returns `true` for `"fluent"` or `"relational"`.
|
|
1400
|
+
*/
|
|
1401
|
+
function isQueryStyle(value) {
|
|
1402
|
+
return value === "fluent" || value === "relational";
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Finds the style that owns a collection member.
|
|
1406
|
+
*
|
|
1407
|
+
* @param member - A gated collection member.
|
|
1408
|
+
* @returns The style whose queries start at that member.
|
|
1409
|
+
*/
|
|
1410
|
+
function styleOfMember(member) {
|
|
1411
|
+
return queryStyleMembers.fluent.includes(member) ? "fluent" : "relational";
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Formats the runtime message that matches {@link QueryStyleDisabled}.
|
|
1415
|
+
*
|
|
1416
|
+
* @param collection - The queried collection.
|
|
1417
|
+
* @param member - The disabled member.
|
|
1418
|
+
* @param style - The only style the configuration allows.
|
|
1419
|
+
* @returns The error message.
|
|
1420
|
+
*/
|
|
1421
|
+
function queryStyleDisabledMessage(collection, member, style) {
|
|
1422
|
+
return `${collection}.${member} is unavailable because content-query.config sets queryStyle to "${style}".`;
|
|
1423
|
+
}
|
|
1424
|
+
//#endregion
|
|
1425
|
+
//#region src/runtime/collection.ts
|
|
1426
|
+
assertServerOnly();
|
|
1427
|
+
/** The collections each handle loads: its own first, then supplied ones in name order. */
|
|
1428
|
+
const handleSources = /* @__PURE__ */ new WeakMap();
|
|
1429
|
+
/** Combined revisions by the primary collection's revision and the loaded collection names. */
|
|
1430
|
+
const combinations = /* @__PURE__ */ new WeakMap();
|
|
1431
|
+
const relationalMembers = [
|
|
1432
|
+
"findMany",
|
|
1433
|
+
"findFirst",
|
|
1434
|
+
"paginate"
|
|
1435
|
+
];
|
|
1436
|
+
/** Reads the source state's current revision, which must still contain the collection. */
|
|
1437
|
+
function currentRevision({ name, state }) {
|
|
1438
|
+
if (state.error) throw state.error;
|
|
1439
|
+
const revision = state.revision;
|
|
1440
|
+
if (!revision) throw new ContentError({
|
|
1441
|
+
code: "artifact-error",
|
|
1442
|
+
message: "No prepared content revision is available."
|
|
1443
|
+
});
|
|
1444
|
+
if (!Object.hasOwn(revision.collections, name) || !Object.hasOwn(revision.registry, name)) invalidQuery(`Unknown collection "${name}".`);
|
|
1445
|
+
return revision;
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Describes exactly the loaded collections. Relationships whose target is not loaded become
|
|
1449
|
+
* `unavailable`, so queries that use them report which collection to supply.
|
|
1450
|
+
*/
|
|
1451
|
+
function scopeRegistry(sources, descriptors) {
|
|
1452
|
+
const loaded = new Set(sources.map((source) => source.name));
|
|
1453
|
+
const registry = Object.create(null);
|
|
1454
|
+
sources.forEach((source, index) => {
|
|
1455
|
+
const descriptor = descriptors[index] ?? { relationships: {} };
|
|
1456
|
+
const relationships = Object.create(null);
|
|
1457
|
+
const unavailable = Object.create(null);
|
|
1458
|
+
for (const [name, relation] of Object.entries(descriptor.relationships)) if (loaded.has(relation.collection)) relationships[name] = relation;
|
|
1459
|
+
else unavailable[name] = relation.collection;
|
|
1460
|
+
registry[source.name] = {
|
|
1461
|
+
relationships,
|
|
1462
|
+
...descriptor.payloads ? { payloads: descriptor.payloads } : {},
|
|
1463
|
+
...Object.keys(unavailable).length ? { unavailable } : {}
|
|
1464
|
+
};
|
|
1465
|
+
});
|
|
1466
|
+
return registry;
|
|
1467
|
+
}
|
|
1468
|
+
/** Builds one revision over exactly the loaded collections, relating only loaded relationships. */
|
|
1469
|
+
function combine(sources, parts) {
|
|
1470
|
+
const registry = scopeRegistry(sources, sources.map((source, index) => parts[index].registry[source.name]));
|
|
1471
|
+
const collections = Object.create(null);
|
|
1472
|
+
sources.forEach((source, index) => {
|
|
1473
|
+
const prepared = parts[index].collections[source.name];
|
|
1474
|
+
collections[source.name] = {
|
|
1475
|
+
get records() {
|
|
1476
|
+
return prepared.records;
|
|
1477
|
+
},
|
|
1478
|
+
get builtins() {
|
|
1479
|
+
return prepared.builtins;
|
|
1480
|
+
},
|
|
1481
|
+
get payloads() {
|
|
1482
|
+
return prepared.payloads;
|
|
1483
|
+
},
|
|
1484
|
+
get sources() {
|
|
1485
|
+
return prepared.sources;
|
|
1486
|
+
},
|
|
1487
|
+
relationships: registry[source.name].relationships
|
|
1488
|
+
};
|
|
1489
|
+
});
|
|
1490
|
+
const shared = parts.every((part) => part === parts[0]);
|
|
1491
|
+
const revision = {
|
|
1492
|
+
registry,
|
|
1493
|
+
collections,
|
|
1494
|
+
relations: /* @__PURE__ */ new Map(),
|
|
1495
|
+
active: 0,
|
|
1496
|
+
payloadCache: shared ? parts[0].payloadCache : /* @__PURE__ */ new Map(),
|
|
1497
|
+
renderCache: shared ? parts[0].renderCache : /* @__PURE__ */ new Map(),
|
|
1498
|
+
get renderMarkdown() {
|
|
1499
|
+
return parts.find((part) => part.renderMarkdown)?.renderMarkdown;
|
|
1500
|
+
},
|
|
1501
|
+
...parts[0].queryStyle ? { queryStyle: parts[0].queryStyle } : {}
|
|
1502
|
+
};
|
|
1503
|
+
let active = 0;
|
|
1504
|
+
Object.defineProperty(revision, "active", {
|
|
1505
|
+
get: () => active,
|
|
1506
|
+
set: (value) => {
|
|
1507
|
+
for (const part of new Set(parts)) part.active += value - active;
|
|
1508
|
+
active = value;
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1511
|
+
const errors = prepareRelationships(revision);
|
|
1512
|
+
if (errors.length) throw new ContentError({
|
|
1513
|
+
code: "artifact-error",
|
|
1514
|
+
message: "Could not relate the supplied collections. Rebuild content so every collection comes from the same build.",
|
|
1515
|
+
cause: errors[0]
|
|
1516
|
+
});
|
|
1517
|
+
return revision;
|
|
1518
|
+
}
|
|
1519
|
+
/** Returns the cached revision over the loaded collections, rebuilding it when a source changed. */
|
|
1520
|
+
function loadedRevision(sources) {
|
|
1521
|
+
const parts = sources.map(currentRevision);
|
|
1522
|
+
let cache = combinations.get(parts[0]);
|
|
1523
|
+
if (!cache) {
|
|
1524
|
+
cache = /* @__PURE__ */ new Map();
|
|
1525
|
+
combinations.set(parts[0], cache);
|
|
1526
|
+
}
|
|
1527
|
+
const key = JSON.stringify(sources.map((source) => source.name));
|
|
1528
|
+
const cached = cache.get(key);
|
|
1529
|
+
if (cached && cached.parts.every((part, index) => part === parts[index])) return cached.revision;
|
|
1530
|
+
const revision = combine(sources, parts);
|
|
1531
|
+
cache.set(key, {
|
|
1532
|
+
parts,
|
|
1533
|
+
revision
|
|
1534
|
+
});
|
|
1535
|
+
return revision;
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Reads the registry that queries build against. While a source holds an error, it falls back to
|
|
1539
|
+
* the declared registries, so queries still build and running them reports the error.
|
|
1540
|
+
*/
|
|
1541
|
+
function loadedRegistry(sources) {
|
|
1542
|
+
if (sources.every((source) => !source.state.error && source.state.revision)) return loadedRevision(sources).registry;
|
|
1543
|
+
return scopeRegistry(sources, sources.map(({ name, state }) => {
|
|
1544
|
+
const registry = state.revision?.registry ?? state.registry;
|
|
1545
|
+
return registry && Object.hasOwn(registry, name) ? registry[name] : void 0;
|
|
1546
|
+
}));
|
|
1547
|
+
}
|
|
1548
|
+
/** Orders sources canonically and rejects two different copies of one collection. */
|
|
1549
|
+
function normalizeSources(primary, supplied) {
|
|
1550
|
+
const byName = /* @__PURE__ */ new Map([[primary.name, primary]]);
|
|
1551
|
+
for (const source of supplied) {
|
|
1552
|
+
const existing = byName.get(source.name);
|
|
1553
|
+
if (existing && existing.state !== source.state) invalidQuery(`using() received two different copies of collection "${source.name}".`);
|
|
1554
|
+
byName.set(source.name, existing ?? source);
|
|
1555
|
+
}
|
|
1556
|
+
const others = [...byName.values()].filter((source) => source !== primary).sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
1557
|
+
return Object.freeze([primary, ...others]);
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Creates the query handle for one collection. Its queries load only that collection plus the
|
|
1561
|
+
* collections passed to `using()`; relationships into any other collection are unavailable.
|
|
1562
|
+
*
|
|
1563
|
+
* @param primary - The queried collection and the state that holds it.
|
|
1564
|
+
* @param supplied - Collections that relationships may load.
|
|
1565
|
+
* @returns The collection handle.
|
|
1566
|
+
*/
|
|
1567
|
+
function createCollection(primary, supplied = []) {
|
|
1568
|
+
const sources = normalizeSources(primary, supplied);
|
|
1569
|
+
const name = primary.name;
|
|
1570
|
+
const run = createRunner({
|
|
1571
|
+
get error() {
|
|
1572
|
+
return sources.map((source) => source.state.error).find(Boolean);
|
|
1573
|
+
},
|
|
1574
|
+
get revision() {
|
|
1575
|
+
return loadedRevision(sources);
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
const registry = () => loadedRegistry(sources);
|
|
1579
|
+
const queries = createQueryNamespace(registry, run);
|
|
1580
|
+
/** Rejects a member that the collection's configured query style disables. */
|
|
1581
|
+
function allow(member) {
|
|
1582
|
+
const style = primary.state.revision?.queryStyle;
|
|
1583
|
+
if (style && styleOfMember(member) !== style) invalidQuery(queryStyleDisabledMessage(name, member, style));
|
|
1584
|
+
}
|
|
1585
|
+
const handle = {
|
|
1586
|
+
using(...collections) {
|
|
1587
|
+
const extra = collections.flatMap((collection) => {
|
|
1588
|
+
const found = collection && typeof collection === "object" ? handleSources.get(collection) : void 0;
|
|
1589
|
+
if (!found) invalidQuery("using() accepts collections imported from their #content-query entries.");
|
|
1590
|
+
return found;
|
|
1591
|
+
});
|
|
1592
|
+
return createCollection(primary, [...sources.slice(1), ...extra]);
|
|
1008
1593
|
},
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
return run(relationalPlan(registry(),
|
|
1594
|
+
count(options) {
|
|
1595
|
+
if (options !== void 0) allow("count(options)");
|
|
1596
|
+
return run(relationalPlan(registry(), name, options ?? {}), "count");
|
|
1012
1597
|
}
|
|
1013
1598
|
};
|
|
1599
|
+
for (const member of relationalMembers) handle[member] = (options) => {
|
|
1600
|
+
allow(member);
|
|
1601
|
+
return queries[name][member](options);
|
|
1602
|
+
};
|
|
1603
|
+
for (const member of queryStyleMembers.fluent) handle[member] = (...arguments_) => {
|
|
1604
|
+
allow(member);
|
|
1605
|
+
return createFrom(registry, run, name)[member](...arguments_);
|
|
1606
|
+
};
|
|
1607
|
+
handleSources.set(handle, sources);
|
|
1608
|
+
return handle;
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Creates a live map of collection handles over one state, for sessions and tests. Handles are
|
|
1612
|
+
* created on first access and keep reading the state's current revision.
|
|
1613
|
+
*
|
|
1614
|
+
* @param state - Session or artifact state.
|
|
1615
|
+
* @returns Collection handles keyed by collection name.
|
|
1616
|
+
*/
|
|
1617
|
+
function createCollections(state) {
|
|
1618
|
+
const handles = /* @__PURE__ */ new Map();
|
|
1619
|
+
const names = () => Object.keys(state.revision?.registry ?? state.registry ?? {});
|
|
1620
|
+
/** Reuses one handle per name, so `using()` can recognize it. */
|
|
1621
|
+
function handle(name) {
|
|
1622
|
+
let cached = handles.get(name);
|
|
1623
|
+
if (!cached) {
|
|
1624
|
+
cached = createCollection({
|
|
1625
|
+
name,
|
|
1626
|
+
state
|
|
1627
|
+
});
|
|
1628
|
+
handles.set(name, cached);
|
|
1629
|
+
}
|
|
1630
|
+
return cached;
|
|
1631
|
+
}
|
|
1632
|
+
/** Exposes current collections, and any name while an error makes queries report it. */
|
|
1633
|
+
function exposed(name) {
|
|
1634
|
+
return typeof name === "string" && (names().includes(name) || state.error !== void 0 && name !== "then");
|
|
1635
|
+
}
|
|
1636
|
+
return new Proxy(Object.create(null), {
|
|
1637
|
+
get(_target, name) {
|
|
1638
|
+
return exposed(name) ? handle(name) : void 0;
|
|
1639
|
+
},
|
|
1640
|
+
has(_target, name) {
|
|
1641
|
+
return exposed(name);
|
|
1642
|
+
},
|
|
1643
|
+
ownKeys() {
|
|
1644
|
+
return names();
|
|
1645
|
+
},
|
|
1646
|
+
getOwnPropertyDescriptor(_target, name) {
|
|
1647
|
+
return exposed(name) ? {
|
|
1648
|
+
configurable: true,
|
|
1649
|
+
enumerable: true,
|
|
1650
|
+
value: handle(name),
|
|
1651
|
+
writable: false
|
|
1652
|
+
} : void 0;
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1014
1655
|
}
|
|
1015
|
-
/**
|
|
1016
|
-
|
|
1017
|
-
|
|
1656
|
+
/**
|
|
1657
|
+
* Keeps one collection handle live across host-delivered development revisions.
|
|
1658
|
+
*
|
|
1659
|
+
* @param initial - The first prepared state.
|
|
1660
|
+
* @param name - The collection the handle queries.
|
|
1661
|
+
* @returns The handle and an `update()` that switches its state to a newer revision or error.
|
|
1662
|
+
*/
|
|
1663
|
+
function createLiveCollection(initial, name) {
|
|
1664
|
+
const state = {
|
|
1665
|
+
revision: initial.revision,
|
|
1666
|
+
registry: initial.registry,
|
|
1667
|
+
error: initial.error
|
|
1668
|
+
};
|
|
1018
1669
|
return {
|
|
1019
|
-
|
|
1670
|
+
collection: createCollection({
|
|
1671
|
+
name,
|
|
1672
|
+
state
|
|
1673
|
+
}),
|
|
1020
1674
|
update(next) {
|
|
1021
1675
|
state.revision = next.revision;
|
|
1022
1676
|
state.registry = next.registry;
|
|
@@ -1025,4 +1679,4 @@ function createLiveRuntime(initial) {
|
|
|
1025
1679
|
};
|
|
1026
1680
|
}
|
|
1027
1681
|
//#endregion
|
|
1028
|
-
export {
|
|
1682
|
+
export { variantKey as a, CODEC_VERSION as c, isQueryStyle as i, createCollections as n, isRelationship as o, createLiveCollection as r, prepareRelationships as s, createCollection as t };
|