content-query 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -3
- package/dist/{build-Dxz7P1ur.mjs → build-CcqIpkes.mjs} +763 -39
- package/dist/build-DL1GzmRy.d.mts +55 -0
- package/dist/build.d.mts +2 -2
- package/dist/build.mjs +1 -1
- package/dist/cli.mjs +14 -8
- package/dist/computed.d.mts +2 -2
- package/dist/{content-N1gYshjg.mjs → content-BXrgP7nL.mjs} +271 -81
- package/dist/{content-G69dPPuo.d.mts → content-CwTKqRs9.d.mts} +49 -24
- package/dist/{types-ChaNhj42.d.mts → descriptor-bcPa6XLk.d.mts} +26 -1
- package/dist/{diagnostics-Cu94mZhl.mjs → diagnostics-DmUZtrJ6.mjs} +3 -1
- package/dist/{errors-C2iuBM0G.mjs → errors-Bfd0R4_P.mjs} +16 -1
- package/dist/file-DSOcdFj0.mjs +81 -0
- package/dist/{glob-Bv0dpvCQ.mjs → glob-BXJuwy_p.mjs} +94 -6
- package/dist/index.d.mts +61 -17
- package/dist/index.mjs +1 -1
- package/dist/internal.d.mts +11 -4
- package/dist/internal.mjs +29 -5
- package/dist/loaders.d.mts +2 -3
- package/dist/loaders.mjs +2 -81
- package/dist/next.d.mts +1 -1
- package/dist/next.mjs +2 -2
- package/dist/{operators-UqWUhKQJ.mjs → operators-Cwy8Kx4o.mjs} +88 -2
- package/dist/query-style-eXENQJ0O.d.mts +22 -0
- package/dist/query.d.mts +25 -2
- package/dist/query.mjs +1 -1
- package/dist/{relational-types-DgK3rLuw.d.mts → relational-types-C8xJtLG5.d.mts} +113 -38
- package/dist/runtime.d.mts +3 -2
- package/dist/runtime.mjs +1 -1
- package/dist/types-DbFfF6JE.d.mts +181 -0
- package/dist/vite.d.mts +15 -3
- package/dist/vite.mjs +25 -2
- package/package.json +2 -2
- package/dist/build-DfKd2bpf.d.mts +0 -25
- package/dist/descriptor-D9Th5hzk.d.mts +0 -18
- package/dist/types-C-kdMblM.d.mts +0 -69
- package/dist/validation-D_XdE1pF.mjs +0 -84
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as invalidQuery, n as contentError, t as ContentError } from "./errors-
|
|
1
|
+
import { a as invalidQuery, n as contentError, o as payloadIdentity, t as ContentError } from "./errors-Bfd0R4_P.mjs";
|
|
2
2
|
import { a as detachSelection, c as isScalar, i as detach, n as compareStrings, s as equal, t as compare } from "./values-BbTV4QT-.mjs";
|
|
3
3
|
import { t as assertServerOnly } from "./server-only-CIJUlyL-.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { D as resolveRelations, E as or, S as not, k as validateExpression, l as fieldProxy, t as and } from "./operators-Cwy8Kx4o.mjs";
|
|
5
5
|
//#region src/artifacts/types.ts
|
|
6
6
|
const CODEC_VERSION = "devalue-6.0.2";
|
|
7
7
|
//#endregion
|
|
@@ -144,6 +144,18 @@ function isRelationship(value) {
|
|
|
144
144
|
].includes(String(value.referenceSide));
|
|
145
145
|
}
|
|
146
146
|
//#endregion
|
|
147
|
+
//#region src/runtime/revision.ts
|
|
148
|
+
/**
|
|
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
|
+
*
|
|
152
|
+
* @param variant - The variant combination.
|
|
153
|
+
* @returns The canonical variant key.
|
|
154
|
+
*/
|
|
155
|
+
function variantKey(variant) {
|
|
156
|
+
return JSON.stringify(Object.fromEntries(Object.keys(variant).sort().map((key) => [key, variant[key]])));
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
147
159
|
//#region src/shared/concurrency.ts
|
|
148
160
|
/**
|
|
149
161
|
* Maps items through an asynchronous callback with at most `limit` callbacks in flight.
|
|
@@ -235,9 +247,17 @@ function emptyPlan(collection) {
|
|
|
235
247
|
with: {}
|
|
236
248
|
};
|
|
237
249
|
}
|
|
238
|
-
/**
|
|
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
|
+
*/
|
|
239
259
|
function hasSelection(plan) {
|
|
240
|
-
return !!plan && (plan.select
|
|
260
|
+
return !!plan && (!!plan.select?.length || !!plan.adjacent || Object.keys(plan.with).length > 0 || Object.keys(plan.payloads ?? {}).length > 0);
|
|
241
261
|
}
|
|
242
262
|
//#endregion
|
|
243
263
|
//#region src/query/relational.ts
|
|
@@ -314,9 +334,9 @@ function relationalWhere(registry, collection, where) {
|
|
|
314
334
|
expressions.push(field === "OR" ? or(...children) : field === "NOT" ? and(...children.map(not)) : and(...children));
|
|
315
335
|
} else if (field === "RAW") {
|
|
316
336
|
if (typeof condition !== "function") invalidQuery("RAW requires a structured-expression callback.");
|
|
317
|
-
const expression = condition(
|
|
337
|
+
const expression = condition(fieldProxy());
|
|
318
338
|
validateExpression(expression);
|
|
319
|
-
expressions.push(expression);
|
|
339
|
+
expressions.push(resolveRelations(registry, collection, expression));
|
|
320
340
|
} else if (Object.hasOwn(registry[collection].relationships, field)) {
|
|
321
341
|
const relation = registry[collection].relationships[field];
|
|
322
342
|
const expression = {
|
|
@@ -350,18 +370,11 @@ function relationalPlan(registry, collection, options = {}, many = true, allowAd
|
|
|
350
370
|
if (Object.keys(options).some((key) => !allowed.includes(key))) invalidQuery("Unknown relational query option or unsupported scope option.");
|
|
351
371
|
const plan = emptyPlan(collection);
|
|
352
372
|
if (options.where !== void 0) plan.filters = [relationalWhere(registry, collection, options.where)];
|
|
353
|
-
if (options.columns
|
|
354
|
-
else {
|
|
373
|
+
if (options.columns !== void 0) {
|
|
355
374
|
record$1(options.columns, "columns");
|
|
356
375
|
const columns = options.columns;
|
|
357
|
-
if (Object.values(columns).some((value) =>
|
|
358
|
-
|
|
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
|
-
}
|
|
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));
|
|
365
378
|
}
|
|
366
379
|
if (options.with !== void 0) {
|
|
367
380
|
record$1(options.with, "with");
|
|
@@ -370,7 +383,8 @@ function relationalPlan(registry, collection, options = {}, many = true, allowAd
|
|
|
370
383
|
const relation = Object.hasOwn(registry[collection].relationships, name) && registry[collection].relationships[name];
|
|
371
384
|
if (!relation) invalidQuery(`Unknown relationship "${name}".`);
|
|
372
385
|
if (selected === false || selected === void 0) continue;
|
|
373
|
-
|
|
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));
|
|
374
388
|
}
|
|
375
389
|
plan.with = related;
|
|
376
390
|
}
|
|
@@ -385,7 +399,37 @@ function relationalPlan(registry, collection, options = {}, many = true, allowAd
|
|
|
385
399
|
const payloads = checkedPayloads(registry, collection, options.payloads);
|
|
386
400
|
if (payloads) plan.payloads = payloads;
|
|
387
401
|
}
|
|
388
|
-
if (options.adjacent !== void 0) plan.adjacent =
|
|
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);
|
|
389
433
|
return plan;
|
|
390
434
|
}
|
|
391
435
|
/** Creates stable, live collection queries in a dedicated, prototype-free namespace. */
|
|
@@ -396,8 +440,8 @@ function createQueryNamespace(registry, run) {
|
|
|
396
440
|
let cached = queries.get(name);
|
|
397
441
|
if (!cached) {
|
|
398
442
|
cached = {
|
|
399
|
-
findMany: (options) => run(relationalPlan(registry(), name, options), "all"),
|
|
400
|
-
findFirst: (options) => run(relationalPlan(registry(), name, options), "first")
|
|
443
|
+
findMany: (options) => run(selectedPlan(relationalPlan(registry(), name, options)), "all"),
|
|
444
|
+
findFirst: (options) => run(selectedPlan(relationalPlan(registry(), name, options)), "first")
|
|
401
445
|
};
|
|
402
446
|
queries.set(name, cached);
|
|
403
447
|
}
|
|
@@ -492,6 +536,27 @@ function objectWhere(where) {
|
|
|
492
536
|
return detach(expression);
|
|
493
537
|
}
|
|
494
538
|
/** Compiles explicit object options into the backend-independent fluent query contract. */
|
|
539
|
+
/**
|
|
540
|
+
* Compiles an object-form `_adjacent` selection. `{ scope: "results", select }` selects neighbors
|
|
541
|
+
* within the current results; other options describe an independent sibling scope.
|
|
542
|
+
*
|
|
543
|
+
* @param registry - The query registry.
|
|
544
|
+
* @param collection - The collection whose neighbors are selected.
|
|
545
|
+
* @param selection - The `_adjacent` value from an object selection.
|
|
546
|
+
* @returns The adjacency plan, which always selects explicit data.
|
|
547
|
+
*/
|
|
548
|
+
function objectAdjacent(registry, collection, selection) {
|
|
549
|
+
record(selection, "_adjacent");
|
|
550
|
+
const { scope, ...options } = selection;
|
|
551
|
+
if (scope !== void 0 && scope !== "results" && scope !== "collection") invalidQuery("Adjacent scope must be \"results\" or \"collection\".");
|
|
552
|
+
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.");
|
|
553
|
+
const plan = objectPlan(registry, collection, options, true, false, false);
|
|
554
|
+
if (!hasSelection(plan)) invalidQuery("Adjacent entries require explicit selection.");
|
|
555
|
+
return {
|
|
556
|
+
scope: scope === "results" ? "results" : "collection",
|
|
557
|
+
plan
|
|
558
|
+
};
|
|
559
|
+
}
|
|
495
560
|
function objectPlan(registry, collection, options, many = true, allowAdjacent = true, pagination = true) {
|
|
496
561
|
if (!Object.hasOwn(registry, collection)) invalidQuery(`Unknown collection "${collection}".`);
|
|
497
562
|
record(options, "Query");
|
|
@@ -504,9 +569,9 @@ function objectPlan(registry, collection, options, many = true, allowAdjacent =
|
|
|
504
569
|
if (Object.keys(options).some((key) => !allowed.includes(key))) invalidQuery("Unknown query option or pagination in an adjacency scope.");
|
|
505
570
|
let plan = emptyPlan(collection);
|
|
506
571
|
if (options.where !== void 0) {
|
|
507
|
-
const expression = typeof options.where === "function" ? options.where(
|
|
572
|
+
const expression = typeof options.where === "function" ? options.where(fieldProxy()) : objectWhere(options.where);
|
|
508
573
|
validateExpression(expression);
|
|
509
|
-
plan.filters = [detach(expression)];
|
|
574
|
+
plan.filters = [detach(resolveRelations(registry, collection, expression))];
|
|
510
575
|
}
|
|
511
576
|
if (options.orderBy !== void 0) for (const order of Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy]) {
|
|
512
577
|
record(order, "orderBy");
|
|
@@ -522,8 +587,7 @@ function objectPlan(registry, collection, options, many = true, allowAdjacent =
|
|
|
522
587
|
const related = Object.create(null);
|
|
523
588
|
for (const [name, selection] of selected) if (name === "_adjacent") {
|
|
524
589
|
if (!allowAdjacent) invalidQuery("Adjacency cannot be selected recursively.");
|
|
525
|
-
plan.adjacent =
|
|
526
|
-
if (plan.adjacent !== true && !plan.adjacent.select) invalidQuery("Adjacent entries require explicit selection.");
|
|
590
|
+
plan.adjacent = objectAdjacent(registry, collection, selection);
|
|
527
591
|
} else if (Object.hasOwn(registry[collection].relationships, name)) {
|
|
528
592
|
const relation = registry[collection].relationships[name];
|
|
529
593
|
const nested = objectPlan(registry, relation.collection, selection, relation.type === "many", allowAdjacent);
|
|
@@ -558,17 +622,13 @@ function createBuilder(registry, run, plan, many = true, root = true, scopeToken
|
|
|
558
622
|
[scopeSymbol]: scopeToken,
|
|
559
623
|
select(...fields) {
|
|
560
624
|
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
|
-
});
|
|
625
|
+
return branch({ select: checkedFields(registry, plan.collection, fields) });
|
|
566
626
|
},
|
|
567
627
|
where(configure) {
|
|
568
628
|
if (typeof configure !== "function") invalidQuery("where() requires a configuration callback.");
|
|
569
|
-
const expression = configure(
|
|
629
|
+
const expression = configure(fieldProxy());
|
|
570
630
|
validateExpression(expression);
|
|
571
|
-
return branch({ filters: [...plan.filters, detach(expression)] });
|
|
631
|
+
return branch({ filters: [...plan.filters, detach(resolveRelations(registry, plan.collection, expression))] });
|
|
572
632
|
},
|
|
573
633
|
orderBy(field, direction = "asc") {
|
|
574
634
|
return branch({ order: checkedOrder(plan, field, direction, many) });
|
|
@@ -598,39 +658,75 @@ function createBuilder(registry, run, plan, many = true, root = true, scopeToken
|
|
|
598
658
|
[name]: checkedPayload(registry, plan.collection, name, variant)
|
|
599
659
|
} });
|
|
600
660
|
},
|
|
601
|
-
|
|
661
|
+
/**
|
|
662
|
+
* Selects previous and next entries. Field names select neighbors within this scope's results;
|
|
663
|
+
* a callback or options object describes an independent sibling scope.
|
|
664
|
+
*/
|
|
665
|
+
selectAdjacent(...arguments_) {
|
|
602
666
|
if (!allowAdjacent || plan.adjacent !== void 0) invalidQuery("Adjacency cannot be repeated or selected recursively.");
|
|
603
|
-
|
|
667
|
+
const [configure] = arguments_;
|
|
668
|
+
if (configure === void 0 || typeof configure === "string") {
|
|
669
|
+
if (!arguments_.length || arguments_.some((field) => typeof field !== "string")) invalidQuery("selectAdjacent() requires neighbor fields, such as selectAdjacent(\"id\"), or a sibling query.");
|
|
670
|
+
const selection = {
|
|
671
|
+
...emptyPlan(plan.collection),
|
|
672
|
+
select: checkedFields(registry, plan.collection, arguments_)
|
|
673
|
+
};
|
|
674
|
+
return branch({ adjacent: {
|
|
675
|
+
scope: "results",
|
|
676
|
+
plan: selection
|
|
677
|
+
} });
|
|
678
|
+
}
|
|
604
679
|
if (typeof configure !== "function") {
|
|
605
680
|
const adjacent = objectPlan(registry, plan.collection, configure, true, false, false);
|
|
606
|
-
if (!adjacent
|
|
607
|
-
return branch({ adjacent
|
|
681
|
+
if (!hasSelection(adjacent)) invalidQuery("Adjacent entries require explicit selection.");
|
|
682
|
+
return branch({ adjacent: {
|
|
683
|
+
scope: "collection",
|
|
684
|
+
plan: adjacent
|
|
685
|
+
} });
|
|
608
686
|
}
|
|
609
687
|
const nestedScope = {};
|
|
610
688
|
const configured = configure(createBuilder(registry, run, emptyPlan(plan.collection), true, false, nestedScope, false, false));
|
|
611
689
|
const adjacent = configured && typeof configured === "object" && planSymbol in configured ? configured[planSymbol] : void 0;
|
|
612
690
|
if (!hasSelection(adjacent) || !(scopeSymbol in configured) || configured[scopeSymbol] !== nestedScope) invalidQuery("selectAdjacent() must return its explicitly selected sibling query.");
|
|
613
|
-
return branch({ adjacent
|
|
691
|
+
return branch({ adjacent: {
|
|
692
|
+
scope: "collection",
|
|
693
|
+
plan: adjacent
|
|
694
|
+
} });
|
|
614
695
|
},
|
|
615
696
|
all() {
|
|
616
|
-
|
|
617
|
-
return run(plan, "all");
|
|
697
|
+
return rows("all");
|
|
618
698
|
},
|
|
619
699
|
/** Executes a selected root query, matching direct await behavior. */
|
|
620
700
|
execute() {
|
|
621
|
-
|
|
622
|
-
return run(plan, "all");
|
|
701
|
+
return rows("all");
|
|
623
702
|
},
|
|
624
703
|
first() {
|
|
625
|
-
|
|
626
|
-
return run(plan, "first");
|
|
704
|
+
return rows("first");
|
|
627
705
|
},
|
|
706
|
+
/** Counts matching entries; counting returns no rows, so it needs no selection. */
|
|
628
707
|
count() {
|
|
629
708
|
if (!root) invalidQuery("Nested builders have no terminal methods.");
|
|
630
709
|
return run(plan, "count");
|
|
631
710
|
}
|
|
632
711
|
};
|
|
633
|
-
if (root
|
|
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);
|
|
718
|
+
}
|
|
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
|
+
}
|
|
634
730
|
/** Checks pagination before creating an independent branch. */
|
|
635
731
|
function pagination(key, value) {
|
|
636
732
|
if (!allowPagination || plan[key] !== void 0) invalidQuery("Pagination requires a nonnegative safe integer, one call per clause, and a many-valued scope.");
|
|
@@ -639,20 +735,20 @@ function createBuilder(registry, run, plan, many = true, root = true, scopeToken
|
|
|
639
735
|
return builder;
|
|
640
736
|
}
|
|
641
737
|
//#endregion
|
|
642
|
-
//#region src/query/
|
|
643
|
-
/**
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
738
|
+
//#region src/query/from.ts
|
|
739
|
+
/**
|
|
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.
|
|
742
|
+
*
|
|
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.
|
|
747
|
+
*/
|
|
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));
|
|
656
752
|
}
|
|
657
753
|
//#endregion
|
|
658
754
|
//#region src/query/normalize.ts
|
|
@@ -667,7 +763,10 @@ function normalizePlan(plan) {
|
|
|
667
763
|
...plan.select === void 0 && Object.keys(plan.with).length ? { select: [] } : {},
|
|
668
764
|
filters: plan.filters.flatMap(conjunction),
|
|
669
765
|
with: Object.fromEntries(Object.entries(plan.with).map(([name, nested]) => [name, normalizePlan(nested)])),
|
|
670
|
-
...plan.adjacent
|
|
766
|
+
...plan.adjacent ? { adjacent: {
|
|
767
|
+
scope: plan.adjacent.scope,
|
|
768
|
+
plan: normalizePlan(plan.adjacent.plan)
|
|
769
|
+
} } : {}
|
|
671
770
|
};
|
|
672
771
|
}
|
|
673
772
|
//#endregion
|
|
@@ -698,6 +797,7 @@ function matches(record, expression, revision, collection) {
|
|
|
698
797
|
if (!revision || !relation || relation.collection !== expression.target) invalidQuery("Relationship filter no longer matches the prepared collection scope.");
|
|
699
798
|
return (revision.relations.get(relationKey(expression.collection, expression.field))?.get(record.id) ?? []).some((candidate) => matches(candidate, expression.expression, revision, expression.target));
|
|
700
799
|
}
|
|
800
|
+
if (expression.kind === "has") invalidQuery("has() filters must be passed to a query's where() before execution.");
|
|
701
801
|
if (expression.kind !== "predicate") return false;
|
|
702
802
|
const actual = Object.hasOwn(record, expression.field) ? record[expression.field] : void 0;
|
|
703
803
|
const expected = expression.value;
|
|
@@ -788,19 +888,17 @@ function pageRecords(records, plan, revision, preserveOrder = false) {
|
|
|
788
888
|
}
|
|
789
889
|
/** Builds a sibling index once per scope, before pagination and within the pinned revision. */
|
|
790
890
|
function neighborScope(plan, records, preserveOrder, scopes, revision) {
|
|
791
|
-
const selection = plan.adjacent;
|
|
792
|
-
const siblingPlan =
|
|
891
|
+
const { scope: neighborsScope, plan: selection } = plan.adjacent;
|
|
892
|
+
const siblingPlan = neighborsScope === "results" ? {
|
|
793
893
|
...plan,
|
|
794
|
-
select:
|
|
795
|
-
|
|
796
|
-
exclude: void 0,
|
|
797
|
-
with: {},
|
|
894
|
+
select: selection.select,
|
|
895
|
+
with: selection.with,
|
|
798
896
|
adjacent: void 0,
|
|
799
897
|
payloads: void 0,
|
|
800
898
|
limit: void 0,
|
|
801
899
|
offset: void 0
|
|
802
900
|
} : selection;
|
|
803
|
-
const key =
|
|
901
|
+
const key = neighborsScope === "results" ? plan : siblingPlan;
|
|
804
902
|
let candidates = scopes.get(key);
|
|
805
903
|
if (!candidates) {
|
|
806
904
|
candidates = /* @__PURE__ */ new WeakMap();
|
|
@@ -808,7 +906,7 @@ function neighborScope(plan, records, preserveOrder, scopes, revision) {
|
|
|
808
906
|
}
|
|
809
907
|
let scope = candidates.get(records);
|
|
810
908
|
if (!scope) {
|
|
811
|
-
const ordered = pageRecords([...new Map(records.map((entry) => [entry.id, entry])).values()], siblingPlan, revision,
|
|
909
|
+
const ordered = pageRecords([...new Map(records.map((entry) => [entry.id, entry])).values()], siblingPlan, revision, neighborsScope === "results" && preserveOrder);
|
|
812
910
|
scope = {
|
|
813
911
|
plan: siblingPlan,
|
|
814
912
|
records: ordered,
|
|
@@ -823,9 +921,8 @@ function neighborScope(plan, records, preserveOrder, scopes, revision) {
|
|
|
823
921
|
* Requested payloads are recorded for the caller to load after projection.
|
|
824
922
|
*/
|
|
825
923
|
function project(record, plan, revision, scopes, scopeRecords, requests, preserveScopeOrder = false) {
|
|
826
|
-
if (!plan
|
|
827
|
-
const
|
|
828
|
-
const result = detachSelection(record, fields);
|
|
924
|
+
if (!hasSelection(plan)) invalidQuery(NO_SELECTION_MESSAGE);
|
|
925
|
+
const result = detachSelection(record, plan.select ?? []);
|
|
829
926
|
for (const [name, nested] of Object.entries(plan.with)) {
|
|
830
927
|
const relation = revision.registry[plan.collection].relationships[name];
|
|
831
928
|
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 +937,7 @@ function project(record, plan, revision, scopes, scopeRecords, requests, preserv
|
|
|
840
937
|
});
|
|
841
938
|
}
|
|
842
939
|
if (plan.adjacent) {
|
|
843
|
-
const candidates = plan.adjacent ===
|
|
940
|
+
const candidates = plan.adjacent.scope === "results" ? scopeRecords : revision.collections[plan.collection].records;
|
|
844
941
|
const scope = neighborScope(plan, candidates, preserveScopeOrder, scopes, revision);
|
|
845
942
|
const index = scope.positions.get(record.id);
|
|
846
943
|
const previous = index === void 0 ? void 0 : scope.records[index - 1];
|
|
@@ -869,12 +966,50 @@ async function executeQuery(revision, plan, terminal, requests = []) {
|
|
|
869
966
|
if (plan.payloads) invalidQuery("Counts cannot request payloads.");
|
|
870
967
|
return queryCandidates(collection.records, plan).filter((record) => plan.filters.every((expression) => matches(record, expression, revision, plan.collection))).length;
|
|
871
968
|
}
|
|
872
|
-
if (!plan
|
|
969
|
+
if (!hasSelection(plan)) invalidQuery(NO_SELECTION_MESSAGE);
|
|
873
970
|
const records = pageRecords(queryCandidates(collection.records, plan), plan, revision);
|
|
874
971
|
const scopes = /* @__PURE__ */ new WeakMap();
|
|
875
972
|
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
973
|
}
|
|
877
974
|
//#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
|
|
878
1013
|
//#region src/runtime/content.ts
|
|
879
1014
|
assertServerOnly();
|
|
880
1015
|
/** Maximum payload loads in flight for one query. */
|
|
@@ -951,11 +1086,48 @@ async function renderedPayload(revision, collection, id) {
|
|
|
951
1086
|
}
|
|
952
1087
|
return rendered;
|
|
953
1088
|
}
|
|
1089
|
+
/** Lazily loads one custom payload variant once per revision, retrying after failures. */
|
|
1090
|
+
function loadCustom(revision, collection, id, name, variant) {
|
|
1091
|
+
const entries = revision.collections[collection].payloads?.get(name);
|
|
1092
|
+
if (!entries) invalidQuery(`Payload "${name}" no longer matches the prepared collection.`);
|
|
1093
|
+
const load = entries.get(id)?.get(variantKey(variant));
|
|
1094
|
+
if (!load) return Promise.reject(new ContentError({
|
|
1095
|
+
code: "variant-not-found",
|
|
1096
|
+
operation: "query",
|
|
1097
|
+
collection,
|
|
1098
|
+
id,
|
|
1099
|
+
payload: name,
|
|
1100
|
+
variant,
|
|
1101
|
+
message: `Entry "${id}" has no ${payloadIdentity(name, variant)} payload.`
|
|
1102
|
+
}));
|
|
1103
|
+
const key = JSON.stringify([
|
|
1104
|
+
collection,
|
|
1105
|
+
id,
|
|
1106
|
+
name,
|
|
1107
|
+
variantKey(variant)
|
|
1108
|
+
]);
|
|
1109
|
+
let loaded = revision.payloadCache.get(key);
|
|
1110
|
+
if (!loaded) {
|
|
1111
|
+
loaded = Promise.resolve().then(load).catch((cause) => {
|
|
1112
|
+
revision.payloadCache.delete(key);
|
|
1113
|
+
throw contentError(cause, {
|
|
1114
|
+
code: "artifact-error",
|
|
1115
|
+
collection,
|
|
1116
|
+
id,
|
|
1117
|
+
payload: name,
|
|
1118
|
+
variant,
|
|
1119
|
+
message: `Could not load the prepared ${payloadIdentity(name, variant)} payload.`
|
|
1120
|
+
});
|
|
1121
|
+
});
|
|
1122
|
+
revision.payloadCache.set(key, loaded);
|
|
1123
|
+
}
|
|
1124
|
+
return loaded;
|
|
1125
|
+
}
|
|
954
1126
|
/** Loads one requested payload value from the pinned revision. */
|
|
955
|
-
async function loadPayload(revision, collection, id, name) {
|
|
1127
|
+
async function loadPayload(revision, collection, id, name, variant) {
|
|
956
1128
|
if (name === "_body") return (await loadBuiltin(revision, collection, id)).body;
|
|
957
1129
|
if (name === "_rendered") return renderedPayload(revision, collection, id);
|
|
958
|
-
|
|
1130
|
+
return loadCustom(revision, collection, id, name, variant);
|
|
959
1131
|
}
|
|
960
1132
|
/**
|
|
961
1133
|
* Loads every requested payload with bounded concurrency, then attaches detached copies under
|
|
@@ -974,13 +1146,14 @@ async function attachPayloads(revision, requests) {
|
|
|
974
1146
|
if (!loads.has(key)) loads.set(key, {
|
|
975
1147
|
collection: request.collection,
|
|
976
1148
|
id: request.id,
|
|
977
|
-
name
|
|
1149
|
+
name,
|
|
1150
|
+
variant: request.payloads[name].variant
|
|
978
1151
|
});
|
|
979
1152
|
}
|
|
980
1153
|
const keys = [...loads.keys()];
|
|
981
1154
|
const values = await mapConcurrent(keys, PAYLOAD_CONCURRENCY, (key) => {
|
|
982
|
-
const { collection, id, name } = loads.get(key);
|
|
983
|
-
return loadPayload(revision, collection, id, name);
|
|
1155
|
+
const { collection, id, name, variant } = loads.get(key);
|
|
1156
|
+
return loadPayload(revision, collection, id, name, variant);
|
|
984
1157
|
});
|
|
985
1158
|
const loaded = new Map(keys.map((key, index) => [key, values[index]]));
|
|
986
1159
|
for (const request of requests) request.target._payloads = Object.fromEntries(Object.keys(request.payloads).map((name) => [name, detach(loaded.get(JSON.stringify([
|
|
@@ -990,7 +1163,13 @@ async function attachPayloads(revision, requests) {
|
|
|
990
1163
|
request.payloads[name]
|
|
991
1164
|
])))]));
|
|
992
1165
|
}
|
|
993
|
-
/**
|
|
1166
|
+
/**
|
|
1167
|
+
* Creates the initialized runtime without importing source configuration or schemas.
|
|
1168
|
+
*
|
|
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.
|
|
1172
|
+
*/
|
|
994
1173
|
function createRuntime(state) {
|
|
995
1174
|
/** Pins query execution and payload loading to exactly the same revision and executor. */
|
|
996
1175
|
const run = (plan, terminal) => pinned(state, async (revision) => {
|
|
@@ -1002,12 +1181,23 @@ function createRuntime(state) {
|
|
|
1002
1181
|
});
|
|
1003
1182
|
/** Reads the current registry without capturing a development revision in either API. */
|
|
1004
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));
|
|
1188
|
+
}
|
|
1189
|
+
const queries = createQueryNamespace(registry, run);
|
|
1005
1190
|
return {
|
|
1006
|
-
|
|
1007
|
-
|
|
1191
|
+
from(collection) {
|
|
1192
|
+
allow("from");
|
|
1193
|
+
return createFrom(registry, run, collection);
|
|
1194
|
+
},
|
|
1195
|
+
get query() {
|
|
1196
|
+
allow("query");
|
|
1197
|
+
return queries;
|
|
1008
1198
|
},
|
|
1009
|
-
query: createQueryNamespace(registry, run),
|
|
1010
1199
|
count(collection, options = {}) {
|
|
1200
|
+
allow("count");
|
|
1011
1201
|
return run(relationalPlan(registry(), collection, options), "count");
|
|
1012
1202
|
}
|
|
1013
1203
|
};
|
|
@@ -1025,4 +1215,4 @@ function createLiveRuntime(initial) {
|
|
|
1025
1215
|
};
|
|
1026
1216
|
}
|
|
1027
1217
|
//#endregion
|
|
1028
|
-
export {
|
|
1218
|
+
export { isRelationship as a, variantKey as i, createRuntime as n, prepareRelationships as o, isQueryStyle as r, CODEC_VERSION as s, createLiveRuntime as t };
|