content-query 0.4.0 → 0.5.1
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 +49 -27
- package/dist/{build-DL1GzmRy.d.mts → build-Aoi7Unm0.d.mts} +10 -5
- package/dist/{build-CcqIpkes.mjs → build-CIw4yWs9.mjs} +139 -51
- package/dist/build.d.mts +1 -1
- package/dist/build.mjs +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/{content-BXrgP7nL.mjs → collection-Cb5eWGnv.mjs} +871 -407
- package/dist/collection-types-DhIxnCSM.d.mts +119 -0
- package/dist/computed.d.mts +1 -1
- package/dist/index.d.mts +18 -3
- package/dist/internal.d.mts +48 -8
- package/dist/internal.mjs +398 -17
- package/dist/next.d.mts +3 -2
- package/dist/next.mjs +66 -13
- package/dist/{operators-Cwy8Kx4o.mjs → operators-D-w1-gDg.mjs} +134 -2
- package/dist/query-style-C2FcT1f9.d.mts +23 -0
- package/dist/query.d.mts +2 -2
- package/dist/query.mjs +1 -1
- package/dist/{relational-types-C8xJtLG5.d.mts → relational-types-PtrzENt0.d.mts} +141 -23
- package/dist/revision-aqGUf6YV.d.mts +56 -0
- package/dist/runtime.d.mts +3 -3
- package/dist/{types-DbFfF6JE.d.mts → types-WOsFmNnN.d.mts} +52 -2
- package/dist/vite.d.mts +1 -1
- package/dist/vite.mjs +76 -34
- package/package.json +1 -1
- package/dist/content-CwTKqRs9.d.mts +0 -134
- package/dist/query-style-eXENQJ0O.d.mts +0 -22
|
@@ -1,7 +1,7 @@
|
|
|
1
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, s as equal, t as compare } from "./values-BbTV4QT-.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 { D as resolveRelations, E as or, S as not, k as validateExpression, l as fieldProxy, t as and } from "./operators-
|
|
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
|
|
@@ -156,41 +156,140 @@ function variantKey(variant) {
|
|
|
156
156
|
return JSON.stringify(Object.fromEntries(Object.keys(variant).sort().map((key) => [key, variant[key]])));
|
|
157
157
|
}
|
|
158
158
|
//#endregion
|
|
159
|
-
//#region src/
|
|
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
|
+
}
|
|
160
198
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
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.
|
|
163
202
|
*
|
|
164
|
-
* @param
|
|
165
|
-
* @param
|
|
166
|
-
* @param
|
|
167
|
-
* @returns
|
|
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.
|
|
168
207
|
*/
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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);
|
|
183
234
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (
|
|
187
|
-
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;
|
|
188
239
|
}
|
|
189
240
|
//#endregion
|
|
190
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
|
+
}
|
|
191
289
|
/** Checks scalar field selection for both public query frontends without a schema field catalog. */
|
|
192
290
|
function checkedFields(registry, collection, fields) {
|
|
193
291
|
const scope = registry[collection];
|
|
292
|
+
for (const field of fields) if (typeof field === "string") assertAvailable(registry, collection, field);
|
|
194
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.");
|
|
195
294
|
return [...fields];
|
|
196
295
|
}
|
|
@@ -237,236 +336,8 @@ function checkedPayloads(registry, collection, payloads) {
|
|
|
237
336
|
return Object.keys(requested).length ? requested : void 0;
|
|
238
337
|
}
|
|
239
338
|
//#endregion
|
|
240
|
-
//#region src/query/plan.ts
|
|
241
|
-
/** Starts a fresh query without binding a content revision until execution. */
|
|
242
|
-
function emptyPlan(collection) {
|
|
243
|
-
return {
|
|
244
|
-
collection,
|
|
245
|
-
filters: [],
|
|
246
|
-
order: [],
|
|
247
|
-
with: {}
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
/** Explains the explicit-selection rule wherever a scope returns rows without selecting data. */
|
|
251
|
-
const NO_SELECTION_MESSAGE = "Select fields, relationships, payloads, or adjacent entries before running this query.";
|
|
252
|
-
/**
|
|
253
|
-
* Checks whether a row-returning scope explicitly requests data. Nothing is implicit, not even
|
|
254
|
-
* `id`: a scope must select fields, include a relationship, request a payload, or select adjacency.
|
|
255
|
-
*
|
|
256
|
-
* @param plan - The scope to check; `undefined` when a callback returned no builder.
|
|
257
|
-
* @returns `true` when the scope requests at least one kind of data.
|
|
258
|
-
*/
|
|
259
|
-
function hasSelection(plan) {
|
|
260
|
-
return !!plan && (!!plan.select?.length || !!plan.adjacent || Object.keys(plan.with).length > 0 || Object.keys(plan.payloads ?? {}).length > 0);
|
|
261
|
-
}
|
|
262
|
-
//#endregion
|
|
263
|
-
//#region src/query/relational.ts
|
|
264
|
-
const operators$1 = {
|
|
265
|
-
eq: "eq",
|
|
266
|
-
ne: "ne",
|
|
267
|
-
gt: "gt",
|
|
268
|
-
gte: "gte",
|
|
269
|
-
lt: "lt",
|
|
270
|
-
lte: "lte",
|
|
271
|
-
in: "in-array",
|
|
272
|
-
notIn: "not-in-array",
|
|
273
|
-
like: "like",
|
|
274
|
-
ilike: "ilike",
|
|
275
|
-
notLike: "like",
|
|
276
|
-
notIlike: "ilike",
|
|
277
|
-
contains: "contains",
|
|
278
|
-
startsWith: "starts-with",
|
|
279
|
-
endsWith: "ends-with",
|
|
280
|
-
arrayContains: "array-contains-all",
|
|
281
|
-
arrayOverlaps: "array-contains-any",
|
|
282
|
-
arrayContained: "array-contained",
|
|
283
|
-
isNull: "is-null",
|
|
284
|
-
isNotNull: "is-null",
|
|
285
|
-
isUndefined: "is-undefined",
|
|
286
|
-
isMissing: "is-missing"
|
|
287
|
-
};
|
|
288
|
-
/** Requires plain records at each relational options and filter boundary. */
|
|
289
|
-
function record$1(value, label) {
|
|
290
|
-
if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) invalidQuery(`${label} must be an options object.`);
|
|
291
|
-
}
|
|
292
|
-
/** Compiles a field condition, including field-local boolean composition, into portable expressions. */
|
|
293
|
-
function fieldCondition(field, condition) {
|
|
294
|
-
if (isScalar(condition)) return {
|
|
295
|
-
kind: "predicate",
|
|
296
|
-
operator: "eq",
|
|
297
|
-
field,
|
|
298
|
-
value: condition
|
|
299
|
-
};
|
|
300
|
-
record$1(condition, `Filter for "${field}"`);
|
|
301
|
-
const expressions = [];
|
|
302
|
-
for (const [name, value] of Object.entries(condition)) {
|
|
303
|
-
if (name === "AND" || name === "OR") {
|
|
304
|
-
if (!Array.isArray(value)) invalidQuery("Field AND/OR filters require arrays.");
|
|
305
|
-
const children = value.map((child) => fieldCondition(field, child));
|
|
306
|
-
expressions.push(name === "AND" ? and(...children) : or(...children));
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
if (name === "NOT") {
|
|
310
|
-
expressions.push(not(fieldCondition(field, value)));
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
if (!Object.hasOwn(operators$1, name)) invalidQuery(`Unknown filter operator "${name}".`);
|
|
314
|
-
const expression = {
|
|
315
|
-
kind: "predicate",
|
|
316
|
-
operator: operators$1[name],
|
|
317
|
-
field,
|
|
318
|
-
value
|
|
319
|
-
};
|
|
320
|
-
if (name.startsWith("is")) {
|
|
321
|
-
if (typeof value !== "boolean") invalidQuery("Presence predicates require boolean values.");
|
|
322
|
-
expressions.push(value !== (name === "isNotNull") ? expression : not(expression));
|
|
323
|
-
} else expressions.push(name === "notLike" || name === "notIlike" ? not(expression) : expression);
|
|
324
|
-
}
|
|
325
|
-
return and(...expressions);
|
|
326
|
-
}
|
|
327
|
-
/** Compiles object predicates and existential relationship filters without loading bodies. */
|
|
328
|
-
function relationalWhere(registry, collection, where) {
|
|
329
|
-
record$1(where, "where");
|
|
330
|
-
const expressions = [];
|
|
331
|
-
for (const [field, condition] of Object.entries(where)) if (field === "AND" || field === "OR" || field === "NOT") {
|
|
332
|
-
if (field === "OR" && !Array.isArray(condition)) invalidQuery("OR filters require an array.");
|
|
333
|
-
const children = (Array.isArray(condition) ? condition : [condition]).map((child) => relationalWhere(registry, collection, child));
|
|
334
|
-
expressions.push(field === "OR" ? or(...children) : field === "NOT" ? and(...children.map(not)) : and(...children));
|
|
335
|
-
} else if (field === "RAW") {
|
|
336
|
-
if (typeof condition !== "function") invalidQuery("RAW requires a structured-expression callback.");
|
|
337
|
-
const expression = condition(fieldProxy());
|
|
338
|
-
validateExpression(expression);
|
|
339
|
-
expressions.push(resolveRelations(registry, collection, expression));
|
|
340
|
-
} else if (Object.hasOwn(registry[collection].relationships, field)) {
|
|
341
|
-
const relation = registry[collection].relationships[field];
|
|
342
|
-
const expression = {
|
|
343
|
-
kind: "relation",
|
|
344
|
-
collection,
|
|
345
|
-
field,
|
|
346
|
-
target: relation.collection,
|
|
347
|
-
expression: relationalWhere(registry, relation.collection, typeof condition === "boolean" ? {} : condition)
|
|
348
|
-
};
|
|
349
|
-
expressions.push(condition === false ? not(expression) : expression);
|
|
350
|
-
} else expressions.push(fieldCondition(field, condition));
|
|
351
|
-
const expression = and(...expressions);
|
|
352
|
-
validateExpression(expression);
|
|
353
|
-
return detach(expression);
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* Compiles Drizzle-style columns/with options into the shared immutable query plan.
|
|
357
|
-
* Payloads are allowed wherever adjacency is, so sibling scopes never load them.
|
|
358
|
-
*/
|
|
359
|
-
function relationalPlan(registry, collection, options = {}, many = true, allowAdjacent = true, pagination = true) {
|
|
360
|
-
if (!Object.hasOwn(registry, collection)) invalidQuery(`Unknown collection "${collection}".`);
|
|
361
|
-
record$1(options, "Query");
|
|
362
|
-
const allowed = [
|
|
363
|
-
"columns",
|
|
364
|
-
"where",
|
|
365
|
-
"with",
|
|
366
|
-
...many ? ["orderBy"] : [],
|
|
367
|
-
...many && pagination ? ["limit", "offset"] : [],
|
|
368
|
-
...allowAdjacent ? ["adjacent", "payloads"] : []
|
|
369
|
-
];
|
|
370
|
-
if (Object.keys(options).some((key) => !allowed.includes(key))) invalidQuery("Unknown relational query option or unsupported scope option.");
|
|
371
|
-
const plan = emptyPlan(collection);
|
|
372
|
-
if (options.where !== void 0) plan.filters = [relationalWhere(registry, collection, options.where)];
|
|
373
|
-
if (options.columns !== void 0) {
|
|
374
|
-
record$1(options.columns, "columns");
|
|
375
|
-
const columns = options.columns;
|
|
376
|
-
if (Object.values(columns).some((value) => value !== true)) invalidQuery("Columns list the fields to include with `true`; omit fields you do not need.");
|
|
377
|
-
plan.select = checkedFields(registry, collection, Object.keys(columns));
|
|
378
|
-
}
|
|
379
|
-
if (options.with !== void 0) {
|
|
380
|
-
record$1(options.with, "with");
|
|
381
|
-
const related = Object.create(null);
|
|
382
|
-
for (const [name, selected] of Object.entries(options.with)) {
|
|
383
|
-
const relation = Object.hasOwn(registry[collection].relationships, name) && registry[collection].relationships[name];
|
|
384
|
-
if (!relation) invalidQuery(`Unknown relationship "${name}".`);
|
|
385
|
-
if (selected === false || selected === void 0) continue;
|
|
386
|
-
if (selected === true) invalidQuery(`Relationship "${name}" requires nested options that select data, such as { columns: { id: true } }.`);
|
|
387
|
-
related[name] = selectedPlan(relationalPlan(registry, relation.collection, selected, relation.type === "many", allowAdjacent));
|
|
388
|
-
}
|
|
389
|
-
plan.with = related;
|
|
390
|
-
}
|
|
391
|
-
if (options.orderBy !== void 0) {
|
|
392
|
-
record$1(options.orderBy, "orderBy");
|
|
393
|
-
if (!Object.keys(options.orderBy).length) invalidQuery("orderBy requires a field.");
|
|
394
|
-
for (const [field, direction] of Object.entries(options.orderBy)) plan.order = checkedOrder(plan, field, direction, many);
|
|
395
|
-
}
|
|
396
|
-
for (const key of ["limit", "offset"]) if (options[key] !== void 0) plan[key] = checkedPagination(options[key], many);
|
|
397
|
-
if (options.payloads !== void 0) {
|
|
398
|
-
record$1(options.payloads, "payloads");
|
|
399
|
-
const payloads = checkedPayloads(registry, collection, options.payloads);
|
|
400
|
-
if (payloads) plan.payloads = payloads;
|
|
401
|
-
}
|
|
402
|
-
if (options.adjacent !== void 0) plan.adjacent = relationalAdjacent(registry, collection, options.adjacent);
|
|
403
|
-
return plan;
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* Compiles relational `adjacent` options. `{ scope: "results", columns, with }` selects neighbors
|
|
407
|
-
* within the current results; other options describe an independent sibling scope.
|
|
408
|
-
*
|
|
409
|
-
* @param registry - The query registry.
|
|
410
|
-
* @param collection - The collection whose neighbors are selected.
|
|
411
|
-
* @param adjacent - The `adjacent` option value.
|
|
412
|
-
* @returns The adjacency plan, which always selects explicit data.
|
|
413
|
-
*/
|
|
414
|
-
function relationalAdjacent(registry, collection, adjacent) {
|
|
415
|
-
if (adjacent === true) invalidQuery("Adjacent entries select their own data, such as { scope: \"results\", columns: { id: true } }.");
|
|
416
|
-
record$1(adjacent, "adjacent");
|
|
417
|
-
const { scope, ...options } = adjacent;
|
|
418
|
-
if (scope !== void 0 && scope !== "results" && scope !== "collection") invalidQuery("Adjacent scope must be \"results\" or \"collection\".");
|
|
419
|
-
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.");
|
|
420
|
-
return {
|
|
421
|
-
scope: scope === "results" ? "results" : "collection",
|
|
422
|
-
plan: selectedPlan(relationalPlan(registry, collection, options, true, false, false))
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Rejects a row-returning scope that requests no data.
|
|
427
|
-
*
|
|
428
|
-
* @param plan - A root, relationship, or sibling scope.
|
|
429
|
-
* @returns The same plan when it selects fields, relationships, payloads, or adjacency.
|
|
430
|
-
*/
|
|
431
|
-
function selectedPlan(plan) {
|
|
432
|
-
if (!hasSelection(plan)) invalidQuery(NO_SELECTION_MESSAGE);
|
|
433
|
-
return plan;
|
|
434
|
-
}
|
|
435
|
-
/** Creates stable, live collection queries in a dedicated, prototype-free namespace. */
|
|
436
|
-
function createQueryNamespace(registry, run) {
|
|
437
|
-
const queries = /* @__PURE__ */ new Map();
|
|
438
|
-
/** Reuses each handle while checking the current registry on every query execution. */
|
|
439
|
-
function query(name) {
|
|
440
|
-
let cached = queries.get(name);
|
|
441
|
-
if (!cached) {
|
|
442
|
-
cached = {
|
|
443
|
-
findMany: (options) => run(selectedPlan(relationalPlan(registry(), name, options)), "all"),
|
|
444
|
-
findFirst: (options) => run(selectedPlan(relationalPlan(registry(), name, options)), "first")
|
|
445
|
-
};
|
|
446
|
-
queries.set(name, cached);
|
|
447
|
-
}
|
|
448
|
-
return cached;
|
|
449
|
-
}
|
|
450
|
-
return new Proxy(Object.create(null), {
|
|
451
|
-
get(_target, name) {
|
|
452
|
-
return typeof name === "string" && Object.hasOwn(registry(), name) ? query(name) : void 0;
|
|
453
|
-
},
|
|
454
|
-
ownKeys() {
|
|
455
|
-
return Object.keys(registry());
|
|
456
|
-
},
|
|
457
|
-
getOwnPropertyDescriptor(_target, name) {
|
|
458
|
-
return typeof name === "string" && Object.hasOwn(registry(), name) ? {
|
|
459
|
-
configurable: true,
|
|
460
|
-
enumerable: true,
|
|
461
|
-
value: query(name),
|
|
462
|
-
writable: false
|
|
463
|
-
} : void 0;
|
|
464
|
-
}
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
//#endregion
|
|
468
339
|
//#region src/query/object.ts
|
|
469
|
-
const operators = {
|
|
340
|
+
const operators$1 = {
|
|
470
341
|
equals: "eq",
|
|
471
342
|
not: "ne",
|
|
472
343
|
gt: "gt",
|
|
@@ -486,12 +357,12 @@ const operators = {
|
|
|
486
357
|
isMissing: "is-missing"
|
|
487
358
|
};
|
|
488
359
|
/** Requires a plain options record, rejecting arrays and class instances at API boundaries. */
|
|
489
|
-
function record(value, label) {
|
|
360
|
+
function record$1(value, label) {
|
|
490
361
|
if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) invalidQuery(`${label} must be an options object.`);
|
|
491
362
|
}
|
|
492
363
|
/** Converts object predicates to the same validated expressions used by fluent operators. */
|
|
493
364
|
function objectWhere(where) {
|
|
494
|
-
record(where, "where");
|
|
365
|
+
record$1(where, "where");
|
|
495
366
|
const expressions = [];
|
|
496
367
|
for (const [field, condition] of Object.entries(where)) {
|
|
497
368
|
if ([
|
|
@@ -512,16 +383,16 @@ function objectWhere(where) {
|
|
|
512
383
|
});
|
|
513
384
|
continue;
|
|
514
385
|
}
|
|
515
|
-
record(condition, `Filter for "${field}"`);
|
|
386
|
+
record$1(condition, `Filter for "${field}"`);
|
|
516
387
|
for (const [name, value] of Object.entries(condition)) {
|
|
517
|
-
if (!Object.hasOwn(operators, name)) invalidQuery(`Unknown filter operator "${name}".`);
|
|
388
|
+
if (!Object.hasOwn(operators$1, name)) invalidQuery(`Unknown filter operator "${name}".`);
|
|
518
389
|
if (name === "not" && !isScalar(value)) {
|
|
519
390
|
expressions.push(not(objectWhere({ [field]: value })));
|
|
520
391
|
continue;
|
|
521
392
|
}
|
|
522
393
|
const expression = {
|
|
523
394
|
kind: "predicate",
|
|
524
|
-
operator: operators[name],
|
|
395
|
+
operator: operators$1[name],
|
|
525
396
|
field,
|
|
526
397
|
value
|
|
527
398
|
};
|
|
@@ -546,7 +417,7 @@ function objectWhere(where) {
|
|
|
546
417
|
* @returns The adjacency plan, which always selects explicit data.
|
|
547
418
|
*/
|
|
548
419
|
function objectAdjacent(registry, collection, selection) {
|
|
549
|
-
record(selection, "_adjacent");
|
|
420
|
+
record$1(selection, "_adjacent");
|
|
550
421
|
const { scope, ...options } = selection;
|
|
551
422
|
if (scope !== void 0 && scope !== "results" && scope !== "collection") invalidQuery("Adjacent scope must be \"results\" or \"collection\".");
|
|
552
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.");
|
|
@@ -559,7 +430,7 @@ function objectAdjacent(registry, collection, selection) {
|
|
|
559
430
|
}
|
|
560
431
|
function objectPlan(registry, collection, options, many = true, allowAdjacent = true, pagination = true) {
|
|
561
432
|
if (!Object.hasOwn(registry, collection)) invalidQuery(`Unknown collection "${collection}".`);
|
|
562
|
-
record(options, "Query");
|
|
433
|
+
record$1(options, "Query");
|
|
563
434
|
const allowed = [
|
|
564
435
|
"where",
|
|
565
436
|
"select",
|
|
@@ -574,13 +445,13 @@ function objectPlan(registry, collection, options, many = true, allowAdjacent =
|
|
|
574
445
|
plan.filters = [detach(resolveRelations(registry, collection, expression))];
|
|
575
446
|
}
|
|
576
447
|
if (options.orderBy !== void 0) for (const order of Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy]) {
|
|
577
|
-
record(order, "orderBy");
|
|
448
|
+
record$1(order, "orderBy");
|
|
578
449
|
if (!Object.keys(order).length) invalidQuery("orderBy requires a field.");
|
|
579
450
|
for (const [field, direction] of Object.entries(order)) plan.order = checkedOrder(plan, field, direction, many);
|
|
580
451
|
}
|
|
581
452
|
for (const key of ["limit", "offset"]) if (options[key] !== void 0) plan[key] = checkedPagination(options[key], many);
|
|
582
453
|
if (options.select !== void 0) {
|
|
583
|
-
record(options.select, "select");
|
|
454
|
+
record$1(options.select, "select");
|
|
584
455
|
const selected = Object.entries(options.select).filter(([, value]) => value !== false && value !== void 0);
|
|
585
456
|
if (!selected.length) invalidQuery("Row-returning queries require a nonempty selection.");
|
|
586
457
|
const fields = [];
|
|
@@ -594,6 +465,7 @@ function objectPlan(registry, collection, options, many = true, allowAdjacent =
|
|
|
594
465
|
if (!nested.select && !nested.adjacent) invalidQuery("Related entries require explicit selection.");
|
|
595
466
|
related[name] = nested;
|
|
596
467
|
} else {
|
|
468
|
+
assertAvailable(registry, collection, name);
|
|
597
469
|
if (selection !== true) invalidQuery("Prepared fields must be selected with true.");
|
|
598
470
|
fields.push(name);
|
|
599
471
|
}
|
|
@@ -641,7 +513,10 @@ function createBuilder(registry, run, plan, many = true, root = true, scopeToken
|
|
|
641
513
|
},
|
|
642
514
|
with(name, configure) {
|
|
643
515
|
const relation = Object.hasOwn(scope.relationships, name) ? scope.relationships[name] : void 0;
|
|
644
|
-
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
|
+
}
|
|
645
520
|
const nestedScope = {};
|
|
646
521
|
const configured = configure(createBuilder(registry, run, emptyPlan(relation.collection), relation.type === "many", false, nestedScope, allowAdjacent));
|
|
647
522
|
const nested = configured && typeof configured === "object" && planSymbol in configured ? configured[planSymbol] : void 0;
|
|
@@ -700,55 +575,331 @@ function createBuilder(registry, run, plan, many = true, root = true, scopeToken
|
|
|
700
575
|
execute() {
|
|
701
576
|
return rows("all");
|
|
702
577
|
},
|
|
703
|
-
first() {
|
|
704
|
-
return rows("first");
|
|
578
|
+
first() {
|
|
579
|
+
return rows("first");
|
|
580
|
+
},
|
|
581
|
+
/** Counts matching entries; counting returns no rows, so it needs no selection. */
|
|
582
|
+
count() {
|
|
583
|
+
if (!root) invalidQuery("Nested builders have no terminal methods.");
|
|
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));
|
|
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))
|
|
807
|
+
};
|
|
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;
|
|
855
|
+
}
|
|
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());
|
|
705
862
|
},
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
let result;
|
|
714
|
-
try {
|
|
715
|
-
result = rows("all");
|
|
716
|
-
} catch (cause) {
|
|
717
|
-
result = Promise.reject(cause);
|
|
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;
|
|
718
870
|
}
|
|
719
|
-
|
|
720
|
-
} });
|
|
721
|
-
/**
|
|
722
|
-
* Runs a row-returning terminal. Awaiting an unselected root rejects rather than resolving to the
|
|
723
|
-
* builder, so a missing selection fails loudly in JavaScript as well as in TypeScript.
|
|
724
|
-
*/
|
|
725
|
-
function rows(terminal) {
|
|
726
|
-
if (!root) invalidQuery("Nested builders have no terminal methods.");
|
|
727
|
-
if (!hasSelection(plan)) invalidQuery(NO_SELECTION_MESSAGE);
|
|
728
|
-
return run(plan, terminal);
|
|
729
|
-
}
|
|
730
|
-
/** Checks pagination before creating an independent branch. */
|
|
731
|
-
function pagination(key, value) {
|
|
732
|
-
if (!allowPagination || plan[key] !== void 0) invalidQuery("Pagination requires a nonnegative safe integer, one call per clause, and a many-valued scope.");
|
|
733
|
-
return branch({ [key]: checkedPagination(value, many) });
|
|
734
|
-
}
|
|
735
|
-
return builder;
|
|
871
|
+
});
|
|
736
872
|
}
|
|
737
873
|
//#endregion
|
|
738
|
-
//#region src/
|
|
874
|
+
//#region src/shared/concurrency.ts
|
|
739
875
|
/**
|
|
740
|
-
*
|
|
741
|
-
*
|
|
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.
|
|
742
878
|
*
|
|
743
|
-
* @param
|
|
744
|
-
* @param
|
|
745
|
-
* @param
|
|
746
|
-
* @returns
|
|
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.
|
|
747
883
|
*/
|
|
748
|
-
function
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
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;
|
|
752
903
|
}
|
|
753
904
|
//#endregion
|
|
754
905
|
//#region src/query/normalize.ts
|
|
@@ -844,25 +995,32 @@ function queryCandidates(records, plan) {
|
|
|
844
995
|
function sortGroup(value) {
|
|
845
996
|
return value == null ? 2 : typeof value === "number" && Number.isNaN(value) || value instanceof Date && Number.isNaN(value.getTime()) ? 1 : 0;
|
|
846
997
|
}
|
|
847
|
-
/**
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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;
|
|
852
1011
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
field:
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
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()) {
|
|
863
1021
|
let category;
|
|
864
|
-
for (const
|
|
865
|
-
const value =
|
|
1022
|
+
for (const { key } of entries) {
|
|
1023
|
+
const value = key[index];
|
|
866
1024
|
if (!isScalar(value)) invalidQuery(`Field "${sort.field}" is not sortable.`);
|
|
867
1025
|
if (value != null) {
|
|
868
1026
|
const current = value instanceof Date ? "date" : typeof value;
|
|
@@ -871,20 +1029,71 @@ function pageRecords(records, plan, revision, preserveOrder = false) {
|
|
|
871
1029
|
}
|
|
872
1030
|
}
|
|
873
1031
|
}
|
|
874
|
-
if (order.length)
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
}
|
|
884
|
-
return 0;
|
|
885
|
-
});
|
|
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
|
+
}
|
|
886
1041
|
const offset = plan.offset ?? 0;
|
|
887
|
-
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
|
+
};
|
|
888
1097
|
}
|
|
889
1098
|
/** Builds a sibling index once per scope, before pagination and within the pinned revision. */
|
|
890
1099
|
function neighborScope(plan, records, preserveOrder, scopes, revision) {
|
|
@@ -896,7 +1105,8 @@ function neighborScope(plan, records, preserveOrder, scopes, revision) {
|
|
|
896
1105
|
adjacent: void 0,
|
|
897
1106
|
payloads: void 0,
|
|
898
1107
|
limit: void 0,
|
|
899
|
-
offset: void 0
|
|
1108
|
+
offset: void 0,
|
|
1109
|
+
paginate: void 0
|
|
900
1110
|
} : selection;
|
|
901
1111
|
const key = neighborsScope === "results" ? plan : siblingPlan;
|
|
902
1112
|
let candidates = scopes.get(key);
|
|
@@ -958,6 +1168,13 @@ function project(record, plan, revision, scopes, scopeRecords, requests, preserv
|
|
|
958
1168
|
/**
|
|
959
1169
|
* Runs the shared JavaScript executor; future adapters can execute the same plan.
|
|
960
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.
|
|
961
1178
|
*/
|
|
962
1179
|
async function executeQuery(revision, plan, terminal, requests = []) {
|
|
963
1180
|
const collection = revision.collections[plan.collection];
|
|
@@ -967,49 +1184,21 @@ async function executeQuery(revision, plan, terminal, requests = []) {
|
|
|
967
1184
|
return queryCandidates(collection.records, plan).filter((record) => plan.filters.every((expression) => matches(record, expression, revision, plan.collection))).length;
|
|
968
1185
|
}
|
|
969
1186
|
if (!hasSelection(plan)) invalidQuery(NO_SELECTION_MESSAGE);
|
|
970
|
-
const records = pageRecords(queryCandidates(collection.records, plan), plan, revision);
|
|
971
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);
|
|
972
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));
|
|
973
1200
|
}
|
|
974
1201
|
//#endregion
|
|
975
|
-
//#region src/runtime/query-style.ts
|
|
976
|
-
/**
|
|
977
|
-
* Root `content` members that start each query style. Every other query API hangs off these
|
|
978
|
-
* members, so gating them gates payloads, relationships, and adjacency as well.
|
|
979
|
-
*/
|
|
980
|
-
const queryStyleMembers = {
|
|
981
|
-
fluent: ["from"],
|
|
982
|
-
relational: ["query", "count"]
|
|
983
|
-
};
|
|
984
|
-
/**
|
|
985
|
-
* Checks whether a configured or manifest value names a supported query style.
|
|
986
|
-
*
|
|
987
|
-
* @param value - The untrusted `queryStyle` value.
|
|
988
|
-
* @returns `true` for `"fluent"` or `"relational"`.
|
|
989
|
-
*/
|
|
990
|
-
function isQueryStyle(value) {
|
|
991
|
-
return value === "fluent" || value === "relational";
|
|
992
|
-
}
|
|
993
|
-
/**
|
|
994
|
-
* Finds the style that owns a root member.
|
|
995
|
-
*
|
|
996
|
-
* @param member - A gated root `content` member.
|
|
997
|
-
* @returns The style whose queries start at that member.
|
|
998
|
-
*/
|
|
999
|
-
function styleOfMember(member) {
|
|
1000
|
-
return queryStyleMembers.fluent.includes(member) ? "fluent" : "relational";
|
|
1001
|
-
}
|
|
1002
|
-
/**
|
|
1003
|
-
* Formats the runtime message that matches {@link QueryStyleDisabled}.
|
|
1004
|
-
*
|
|
1005
|
-
* @param member - The disabled root member.
|
|
1006
|
-
* @param style - The only style the configuration allows.
|
|
1007
|
-
* @returns The error message.
|
|
1008
|
-
*/
|
|
1009
|
-
function queryStyleDisabledMessage(member, style) {
|
|
1010
|
-
return `content.${member} is unavailable because content-query.config sets queryStyle to "${style}".`;
|
|
1011
|
-
}
|
|
1012
|
-
//#endregion
|
|
1013
1202
|
//#region src/runtime/content.ts
|
|
1014
1203
|
assertServerOnly();
|
|
1015
1204
|
/** Maximum payload loads in flight for one query. */
|
|
@@ -1164,49 +1353,324 @@ async function attachPayloads(revision, requests) {
|
|
|
1164
1353
|
])))]));
|
|
1165
1354
|
}
|
|
1166
1355
|
/**
|
|
1167
|
-
* Creates the
|
|
1356
|
+
* Creates the query runner that pins plan execution and payload loading to one revision.
|
|
1168
1357
|
*
|
|
1169
|
-
* @param state - Mutable session or
|
|
1170
|
-
* @returns
|
|
1171
|
-
* `invalid-query` ContentError when accessed or called.
|
|
1358
|
+
* @param state - Mutable session, artifact, or combined state; each query reads its revision.
|
|
1359
|
+
* @returns A runner for fluent and relational plans.
|
|
1172
1360
|
*/
|
|
1173
|
-
function
|
|
1174
|
-
|
|
1175
|
-
const run = (plan, terminal) => pinned(state, async (revision) => {
|
|
1361
|
+
function createRunner(state) {
|
|
1362
|
+
return (plan, terminal) => pinned(state, async (revision) => {
|
|
1176
1363
|
const normalized = normalizePlan(plan);
|
|
1177
1364
|
const requests = [];
|
|
1178
1365
|
const result = revision.execute ? await revision.execute(normalized, terminal, requests) : await executeQuery(revision, normalized, terminal, requests);
|
|
1179
1366
|
await attachPayloads(revision, requests);
|
|
1180
1367
|
return result;
|
|
1181
1368
|
});
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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);
|
|
1188
1526
|
}
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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);
|
|
1194
1573
|
},
|
|
1195
|
-
get
|
|
1196
|
-
|
|
1197
|
-
|
|
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]);
|
|
1198
1593
|
},
|
|
1199
|
-
count(
|
|
1200
|
-
allow("count");
|
|
1201
|
-
return run(relationalPlan(registry(),
|
|
1594
|
+
count(options) {
|
|
1595
|
+
if (options !== void 0) allow("count(options)");
|
|
1596
|
+
return run(relationalPlan(registry(), name, options ?? {}), "count");
|
|
1202
1597
|
}
|
|
1203
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
|
+
});
|
|
1204
1655
|
}
|
|
1205
|
-
/**
|
|
1206
|
-
|
|
1207
|
-
|
|
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
|
+
};
|
|
1208
1669
|
return {
|
|
1209
|
-
|
|
1670
|
+
collection: createCollection({
|
|
1671
|
+
name,
|
|
1672
|
+
state
|
|
1673
|
+
}),
|
|
1210
1674
|
update(next) {
|
|
1211
1675
|
state.revision = next.revision;
|
|
1212
1676
|
state.registry = next.registry;
|
|
@@ -1215,4 +1679,4 @@ function createLiveRuntime(initial) {
|
|
|
1215
1679
|
};
|
|
1216
1680
|
}
|
|
1217
1681
|
//#endregion
|
|
1218
|
-
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 };
|