content-query 0.3.0 → 0.5.0

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