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.
@@ -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-Cwy8Kx4o.mjs";
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/shared/concurrency.ts
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
- * Maps items through an asynchronous callback with at most `limit` callbacks in flight.
162
- * Results keep input order; the first rejection rejects the whole map after started work settles.
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 items - Values to process.
165
- * @param limit - Maximum number of concurrent callbacks; values below one are treated as one.
166
- * @param callback - Asynchronous work for one item and its index.
167
- * @returns The callback results in input order.
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
- async function mapConcurrent(items, limit, callback) {
170
- const results = Array.from({ length: items.length });
171
- let next = 0;
172
- let failure;
173
- /** Claims the next unprocessed item until the list is exhausted or work has failed. */
174
- async function worker() {
175
- while (next < items.length && !failure) {
176
- const index = next++;
177
- try {
178
- results[index] = await callback(items[index], index);
179
- } catch (cause) {
180
- failure ??= { cause };
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
- const workers = Array.from({ length: Math.min(Math.max(1, limit), items.length) }, worker);
185
- await Promise.all(workers);
186
- if (failure) throw failure.cause;
187
- return results;
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") invalidQuery("Unknown, repeated, or invalid relationship request.");
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
- /** Counts matching entries; counting returns no rows, so it needs no selection. */
707
- count() {
708
- if (!root) invalidQuery("Nested builders have no terminal methods.");
709
- return run(plan, "count");
710
- }
711
- };
712
- if (root) Object.defineProperty(builder, "then", { value: (fulfilled, rejected) => {
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
- return result.then(fulfilled, rejected);
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/query/from.ts
874
+ //#region src/shared/concurrency.ts
739
875
  /**
740
- * Starts a fluent root query over one collection. The builder selects nothing until `select()`,
741
- * `with()`, `withPayload()`, or `selectAdjacent()` requests data; `count()` needs no selection.
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 registry - Reads the current query registry, so development builders bind the latest one.
744
- * @param run - Executes a plan against one pinned revision.
745
- * @param collection - The collection to query.
746
- * @returns An immutable, reusable root builder.
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 createFrom(registry, run, collection) {
749
- const current = registry();
750
- if (typeof collection !== "string" || !Object.hasOwn(current, collection)) invalidQuery(`Unknown collection "${String(collection)}".`);
751
- return createBuilder(current, run, emptyPlan(collection));
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
- /** Filters, orders, and pages one collection or one parent's related candidates. */
848
- function pageRecords(records, plan, revision, preserveOrder = false) {
849
- if (records.length <= 1 && !plan.order.length) {
850
- const record = records[0];
851
- return record && plan.filters.every((expression) => matches(record, expression, revision, plan.collection)) && (plan.offset ?? 0) === 0 && plan.limit !== 0 ? [record] : [];
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
- const filtered = records.filter((record) => plan.filters.every((expression) => matches(record, expression, revision, plan.collection)));
854
- const order = plan.order.length ? [...plan.order] : preserveOrder ? [] : [{
855
- field: "id",
856
- direction: "asc"
857
- }];
858
- if (order.length && !order.some((sort) => sort.field === "id")) order.push({
859
- field: "id",
860
- direction: "asc"
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 record of filtered) {
865
- const value = Object.hasOwn(record, sort.field) ? record[sort.field] : void 0;
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) filtered.sort((left, right) => {
875
- for (const sort of order) {
876
- const leftValue = Object.hasOwn(left, sort.field) ? left[sort.field] : void 0;
877
- const rightValue = Object.hasOwn(right, sort.field) ? right[sort.field] : void 0;
878
- const group = sortGroup(leftValue) - sortGroup(rightValue);
879
- if (group) return group;
880
- if (sortGroup(leftValue) !== 0) continue;
881
- const comparison = compare(leftValue, rightValue) ?? 0;
882
- if (comparison) return sort.direction === "desc" ? -comparison : comparison;
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 filtered.slice(offset, plan.limit === void 0 ? void 0 : offset + plan.limit);
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 initialized runtime without importing source configuration or schemas.
1356
+ * Creates the query runner that pins plan execution and payload loading to one revision.
1168
1357
  *
1169
- * @param state - Mutable session or artifact state; each query reads its current revision.
1170
- * @returns The `content` API. Root members outside the current revision's `queryStyle` throw an
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 createRuntime(state) {
1174
- /** Pins query execution and payload loading to exactly the same revision and executor. */
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
- /** Reads the current registry without capturing a development revision in either API. */
1183
- const registry = () => state.registry ?? state.revision?.registry ?? {};
1184
- /** Rejects a root member that the current revision's configured query style disables. */
1185
- function allow(member) {
1186
- const style = state.revision?.queryStyle;
1187
- if (style && styleOfMember(member) !== style) invalidQuery(queryStyleDisabledMessage(member, style));
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 queries = createQueryNamespace(registry, run);
1190
- return {
1191
- from(collection) {
1192
- allow("from");
1193
- return createFrom(registry, run, collection);
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 query() {
1196
- allow("query");
1197
- return queries;
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(collection, options = {}) {
1200
- allow("count");
1201
- return run(relationalPlan(registry(), collection, options), "count");
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
- /** Keeps dev builders live across host-delivered revisions while pinning active operations. */
1206
- function createLiveRuntime(initial) {
1207
- const state = { ...initial };
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
- content: createRuntime(state),
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 { isRelationship as a, variantKey as i, createRuntime as n, prepareRelationships as o, isQueryStyle as r, CODEC_VERSION as s, createLiveRuntime as t };
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 };