content-query 0.0.1 → 0.1.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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/dist/build-C5ef0kkR.mjs +1401 -0
  4. package/dist/build-DSyoRxkZ.d.mts +24 -0
  5. package/dist/build.d.mts +2 -0
  6. package/dist/build.mjs +2 -0
  7. package/dist/chunk-CJs4Fogs.mjs +34 -0
  8. package/dist/cli.d.mts +1 -0
  9. package/dist/cli.mjs +97 -0
  10. package/dist/computed.d.mts +37 -0
  11. package/dist/computed.mjs +138 -0
  12. package/dist/content-Crag1Uch.d.mts +99 -0
  13. package/dist/content-Dj2fQCJ5.mjs +902 -0
  14. package/dist/descriptor-Xbo9u5eR.d.mts +19 -0
  15. package/dist/diagnostics-Dol6ANtq.mjs +79 -0
  16. package/dist/dist-BQGVLZyl.mjs +5396 -0
  17. package/dist/errors-C8lWyiy2.mjs +72 -0
  18. package/dist/github-slugger-BCAkwEOL.mjs +72 -0
  19. package/dist/glob-DJmebCMM.mjs +159 -0
  20. package/dist/index.d.mts +60 -0
  21. package/dist/index.mjs +43 -0
  22. package/dist/internal/markdown.d.mts +7 -0
  23. package/dist/internal/markdown.mjs +2 -0
  24. package/dist/internal.d.mts +54 -0
  25. package/dist/internal.mjs +89 -0
  26. package/dist/loaders.d.mts +12 -0
  27. package/dist/loaders.mjs +85 -0
  28. package/dist/markdown-CZBL4Ux_.mjs +54 -0
  29. package/dist/next.d.mts +21 -0
  30. package/dist/next.mjs +142 -0
  31. package/dist/operators-BQpLfJlx.mjs +173 -0
  32. package/dist/query.d.mts +45 -0
  33. package/dist/query.mjs +2 -0
  34. package/dist/relational-types-DLniKi5S.d.mts +245 -0
  35. package/dist/runtime.d.mts +3 -0
  36. package/dist/runtime.mjs +6 -0
  37. package/dist/schema-CnCbMosm.mjs +20 -0
  38. package/dist/schema.d.mts +13 -0
  39. package/dist/schema.mjs +2 -0
  40. package/dist/server-only-CIE-FHx0.mjs +7 -0
  41. package/dist/types-BPh2cBPs.d.mts +48 -0
  42. package/dist/types-DcvHRmxO.d.mts +20 -0
  43. package/dist/types-hmDOw87q.d.mts +135 -0
  44. package/dist/validation-BvvjSUFc.mjs +84 -0
  45. package/dist/values-B7lo_2_x.mjs +900 -0
  46. package/dist/vite.d.mts +8 -0
  47. package/dist/vite.mjs +296 -0
  48. package/package.json +90 -10
@@ -0,0 +1,902 @@
1
+ import { a as invalidQuery, n as contentError, t as ContentError } from "./errors-C8lWyiy2.mjs";
2
+ import { a as detachSelection, c as isScalar, i as detach, n as compareStrings, s as equal, t as compare } from "./values-B7lo_2_x.mjs";
3
+ import { t as assertServerOnly } from "./server-only-CIE-FHx0.mjs";
4
+ import { _ as not, t as and, x as validateExpression, y as or } from "./operators-BQpLfJlx.mjs";
5
+ //#region src/artifacts/types.ts
6
+ const CODEC_VERSION = "devalue-6.0.2";
7
+ //#endregion
8
+ //#region src/runtime/relationships.ts
9
+ /** Encodes a scalar match key without coercing strings, numbers, dates, or bigint. */
10
+ function scalarKey(value) {
11
+ if (value == null) return;
12
+ if (!isScalar(value)) throw new Error("Relationship keys must be scalars or scalar arrays.");
13
+ if (value instanceof Date) return `date:${value.getTime()}`;
14
+ return `${typeof value}:${String(value)}`;
15
+ }
16
+ /** Formats an authored reference value for diagnostics without its internal type prefix. */
17
+ function describeValue(value) {
18
+ if (typeof value === "string") return JSON.stringify(value);
19
+ if (typeof value === "bigint") return `${value}n`;
20
+ if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString();
21
+ return String(value);
22
+ }
23
+ /** Returns present relationship keys and preserves forward-array multiplicity. */
24
+ function references(record, field) {
25
+ const value = Object.hasOwn(record, field) ? record[field] : void 0;
26
+ return (Array.isArray(value) ? value : [value]).flatMap((item) => {
27
+ const key = scalarKey(item);
28
+ return key === void 0 ? [] : [{
29
+ key,
30
+ value: item
31
+ }];
32
+ });
33
+ }
34
+ /** Returns present relationship keys for matching. */
35
+ function keys(record, field) {
36
+ return references(record, field).map((reference) => reference.key);
37
+ }
38
+ /** Indexes all matching values once for batched relationship loading. */
39
+ function indexRecords(records, field) {
40
+ const index = /* @__PURE__ */ new Map();
41
+ for (const record of records) for (const key of new Set(keys(record, field))) {
42
+ const matches = index.get(key) ?? [];
43
+ matches.push(record);
44
+ index.set(key, matches);
45
+ }
46
+ return index;
47
+ }
48
+ /**
49
+ * Creates all per-entry relationship matches and validates ownership and cardinality,
50
+ * reporting every invalid reference. Relationships touching a collection in `skipped`
51
+ * are not checked, because its missing entries would produce speculative failures.
52
+ */
53
+ function prepareRelationships(revision, skipped = /* @__PURE__ */ new Set()) {
54
+ const errors = [];
55
+ for (const [collectionName, collection] of Object.entries(revision.collections)) for (const [relationshipName, relation] of Object.entries(collection.relationships)) {
56
+ if (skipped.has(collectionName) || skipped.has(relation.collection)) continue;
57
+ /** Records one failure against the entry and field that own the invalid value. */
58
+ const report = (message, owner, cause) => {
59
+ errors.push(new ContentError({
60
+ code: "invalid-reference",
61
+ operation: "build",
62
+ collection: owner.collection,
63
+ id: owner.entry?.id,
64
+ source: owner.entry ? revision.collections[owner.collection]?.sources?.get(owner.entry.id) : void 0,
65
+ path: [owner.field],
66
+ message: `Invalid relationship ${collectionName}.${relationshipName}: ${message}`,
67
+ cause
68
+ }));
69
+ };
70
+ const declaration = {
71
+ collection: collectionName,
72
+ field: relation.from
73
+ };
74
+ const target = revision.collections[relation.collection];
75
+ if (!target || !["one", "many"].includes(relation.type) || ![
76
+ "from",
77
+ "to",
78
+ "none"
79
+ ].includes(relation.referenceSide) || !relation.from || !relation.to) {
80
+ report("Invalid relationship declaration or unregistered target.", declaration);
81
+ continue;
82
+ }
83
+ if (collection.records.some((record) => Array.isArray(record[relation.from])) && target.records.some((record) => Array.isArray(record[relation.to]))) {
84
+ report("Array-to-array relationships are unsupported.", declaration);
85
+ continue;
86
+ }
87
+ let sourceIndex;
88
+ let targetIndex;
89
+ try {
90
+ sourceIndex = indexRecords(collection.records, relation.from);
91
+ targetIndex = indexRecords(target.records, relation.to);
92
+ } catch (cause) {
93
+ report(cause instanceof Error ? cause.message : String(cause), declaration, cause);
94
+ continue;
95
+ }
96
+ const reportedOwners = /* @__PURE__ */ new Set();
97
+ if (relation.referenceSide !== "none") {
98
+ const inverse = relation.referenceSide === "to";
99
+ const owning = inverse ? target : collection;
100
+ const oppositeIndex = inverse ? sourceIndex : targetIndex;
101
+ const owner = {
102
+ collection: inverse ? relation.collection : collectionName,
103
+ field: inverse ? relation.to : relation.from
104
+ };
105
+ const opposite = inverse ? collectionName : relation.collection;
106
+ for (const record of owning.records) {
107
+ const reported = /* @__PURE__ */ new Set();
108
+ for (const { key, value } of references(record, owner.field)) {
109
+ const matches = oppositeIndex.get(key)?.length ?? 0;
110
+ if (matches === 1 || reported.has(key)) continue;
111
+ reported.add(key);
112
+ if (!inverse) reportedOwners.add(record.id);
113
+ report(matches === 0 ? `Reference ${describeValue(value)} has no target in "${opposite}".` : `Reference ${describeValue(value)} matches ${matches} entries in "${opposite}".`, {
114
+ ...owner,
115
+ entry: record
116
+ });
117
+ }
118
+ }
119
+ }
120
+ const matchesById = /* @__PURE__ */ new Map();
121
+ for (const record of collection.records) {
122
+ const candidates = keys(record, relation.from).flatMap((key) => targetIndex.get(key) ?? []);
123
+ const matches = relation.referenceSide === "from" && Array.isArray(record[relation.from]) ? candidates : [...new Set(candidates)].sort((left, right) => compareStrings(left.id, right.id));
124
+ if (relation.type === "one" && matches.length > 1 && !reportedOwners.has(record.id)) report(`A singular relationship has ${matches.length} unfiltered matches.`, {
125
+ ...declaration,
126
+ entry: record
127
+ });
128
+ matchesById.set(record.id, matches);
129
+ }
130
+ revision.relations.set(relationKey(collectionName, relationshipName), matchesById);
131
+ }
132
+ return errors;
133
+ }
134
+ /** Builds a collision-free internal lookup key for a declared relationship. */
135
+ function relationKey(collection, relationship) {
136
+ return JSON.stringify([collection, relationship]);
137
+ }
138
+ /** Checks the structural declaration independently from observed entry fields. */
139
+ function isRelationship(value) {
140
+ return typeof value === "object" && value !== null && "collection" in value && typeof value.collection === "string" && "from" in value && typeof value.from === "string" && "to" in value && typeof value.to === "string" && "type" in value && (value.type === "one" || value.type === "many") && "referenceSide" in value && [
141
+ "from",
142
+ "to",
143
+ "none"
144
+ ].includes(String(value.referenceSide));
145
+ }
146
+ //#endregion
147
+ //#region src/query/clauses.ts
148
+ /** Checks scalar field selection for both public query frontends without a schema field catalog. */
149
+ function checkedFields(registry, collection, fields) {
150
+ const scope = registry[collection];
151
+ if (!scope || fields.some((field) => typeof field !== "string" || !field || field === "_adjacent" || Object.hasOwn(scope.relationships, field)) || new Set(fields).size !== fields.length) invalidQuery("Selection requires distinct prepared fields; use relationship or adjacency selection explicitly.");
152
+ return [...fields];
153
+ }
154
+ /** Checks one sort clause with the same cardinality and duplicate rules in either API. */
155
+ function checkedOrder(plan, field, direction, many) {
156
+ if (!many || typeof field !== "string" || !field || !["asc", "desc"].includes(direction) || plan.order.some((sort) => sort.field === field)) invalidQuery("Invalid or repeated sort, or sorting a singular relationship.");
157
+ return [...plan.order, {
158
+ field,
159
+ direction
160
+ }];
161
+ }
162
+ /** Checks pagination before either frontend constructs its shared query plan. */
163
+ function checkedPagination(value, many) {
164
+ if (!many || !Number.isSafeInteger(value) || value < 0) invalidQuery("Pagination requires a nonnegative safe integer and a many-valued scope.");
165
+ return value;
166
+ }
167
+ //#endregion
168
+ //#region src/query/plan.ts
169
+ /** Starts a fresh query without binding a content revision until execution. */
170
+ function emptyPlan(collection) {
171
+ return {
172
+ collection,
173
+ filters: [],
174
+ order: [],
175
+ with: {}
176
+ };
177
+ }
178
+ /** Treats explicitly selected relationships and adjacency as row projections too. */
179
+ function hasSelection(plan) {
180
+ return !!plan && (plan.select !== void 0 || !!plan.allFields || !!plan.adjacent || Object.keys(plan.with).length > 0);
181
+ }
182
+ //#endregion
183
+ //#region src/query/relational.ts
184
+ const operators$1 = {
185
+ eq: "eq",
186
+ ne: "ne",
187
+ gt: "gt",
188
+ gte: "gte",
189
+ lt: "lt",
190
+ lte: "lte",
191
+ in: "in-array",
192
+ notIn: "not-in-array",
193
+ like: "like",
194
+ ilike: "ilike",
195
+ notLike: "like",
196
+ notIlike: "ilike",
197
+ contains: "contains",
198
+ startsWith: "starts-with",
199
+ endsWith: "ends-with",
200
+ arrayContains: "array-contains-all",
201
+ arrayOverlaps: "array-contains-any",
202
+ arrayContained: "array-contained",
203
+ isNull: "is-null",
204
+ isNotNull: "is-null",
205
+ isUndefined: "is-undefined",
206
+ isMissing: "is-missing"
207
+ };
208
+ /** Requires plain records at each relational options and filter boundary. */
209
+ function record$1(value, label) {
210
+ if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) invalidQuery(`${label} must be an options object.`);
211
+ }
212
+ /** Compiles a field condition, including field-local boolean composition, into portable expressions. */
213
+ function fieldCondition(field, condition) {
214
+ if (isScalar(condition)) return {
215
+ kind: "predicate",
216
+ operator: "eq",
217
+ field,
218
+ value: condition
219
+ };
220
+ record$1(condition, `Filter for "${field}"`);
221
+ const expressions = [];
222
+ for (const [name, value] of Object.entries(condition)) {
223
+ if (name === "AND" || name === "OR") {
224
+ if (!Array.isArray(value)) invalidQuery("Field AND/OR filters require arrays.");
225
+ const children = value.map((child) => fieldCondition(field, child));
226
+ expressions.push(name === "AND" ? and(...children) : or(...children));
227
+ continue;
228
+ }
229
+ if (name === "NOT") {
230
+ expressions.push(not(fieldCondition(field, value)));
231
+ continue;
232
+ }
233
+ if (!Object.hasOwn(operators$1, name)) invalidQuery(`Unknown filter operator "${name}".`);
234
+ const expression = {
235
+ kind: "predicate",
236
+ operator: operators$1[name],
237
+ field,
238
+ value
239
+ };
240
+ if (name.startsWith("is")) {
241
+ if (typeof value !== "boolean") invalidQuery("Presence predicates require boolean values.");
242
+ expressions.push(value !== (name === "isNotNull") ? expression : not(expression));
243
+ } else expressions.push(name === "notLike" || name === "notIlike" ? not(expression) : expression);
244
+ }
245
+ return and(...expressions);
246
+ }
247
+ /** Compiles object predicates and existential relationship filters without loading bodies. */
248
+ function relationalWhere(registry, collection, where) {
249
+ record$1(where, "where");
250
+ const expressions = [];
251
+ for (const [field, condition] of Object.entries(where)) if (field === "AND" || field === "OR" || field === "NOT") {
252
+ if (field === "OR" && !Array.isArray(condition)) invalidQuery("OR filters require an array.");
253
+ const children = (Array.isArray(condition) ? condition : [condition]).map((child) => relationalWhere(registry, collection, child));
254
+ expressions.push(field === "OR" ? or(...children) : field === "NOT" ? and(...children.map(not)) : and(...children));
255
+ } else if (field === "RAW") {
256
+ if (typeof condition !== "function") invalidQuery("RAW requires a structured-expression callback.");
257
+ const expression = condition(new Proxy(Object.create(null), { get: (_target, name) => ({ name }) }));
258
+ validateExpression(expression);
259
+ expressions.push(expression);
260
+ } else if (Object.hasOwn(registry[collection].relationships, field)) {
261
+ const relation = registry[collection].relationships[field];
262
+ const expression = {
263
+ kind: "relation",
264
+ collection,
265
+ field,
266
+ target: relation.collection,
267
+ expression: relationalWhere(registry, relation.collection, typeof condition === "boolean" ? {} : condition)
268
+ };
269
+ expressions.push(condition === false ? not(expression) : expression);
270
+ } else expressions.push(fieldCondition(field, condition));
271
+ const expression = and(...expressions);
272
+ validateExpression(expression);
273
+ return detach(expression);
274
+ }
275
+ /** Compiles Drizzle-style columns/with options into the shared immutable query plan. */
276
+ function relationalPlan(registry, collection, options = {}, many = true, allowAdjacent = true, pagination = true) {
277
+ if (!Object.hasOwn(registry, collection)) invalidQuery(`Unknown collection "${collection}".`);
278
+ record$1(options, "Query");
279
+ const allowed = [
280
+ "columns",
281
+ "where",
282
+ "with",
283
+ ...many ? ["orderBy"] : [],
284
+ ...many && pagination ? ["limit", "offset"] : [],
285
+ ...allowAdjacent ? ["adjacent"] : []
286
+ ];
287
+ if (Object.keys(options).some((key) => !allowed.includes(key))) invalidQuery("Unknown relational query option or unsupported scope option.");
288
+ const plan = emptyPlan(collection);
289
+ if (options.where !== void 0) plan.filters = [relationalWhere(registry, collection, options.where)];
290
+ if (options.columns === void 0) plan.allFields = true;
291
+ else {
292
+ record$1(options.columns, "columns");
293
+ const columns = options.columns;
294
+ if (Object.values(columns).some((value) => typeof value !== "boolean")) invalidQuery("Column selection requires boolean values.");
295
+ const names = checkedFields(registry, collection, Object.keys(columns));
296
+ const included = names.filter((name) => columns[name] === true);
297
+ if (included.length || !names.length) plan.select = included;
298
+ else {
299
+ plan.allFields = true;
300
+ plan.exclude = names;
301
+ }
302
+ }
303
+ if (options.with !== void 0) {
304
+ record$1(options.with, "with");
305
+ const related = Object.create(null);
306
+ for (const [name, selected] of Object.entries(options.with)) {
307
+ const relation = Object.hasOwn(registry[collection].relationships, name) && registry[collection].relationships[name];
308
+ if (!relation) invalidQuery(`Unknown relationship "${name}".`);
309
+ if (selected === false || selected === void 0) continue;
310
+ related[name] = relationalPlan(registry, relation.collection, selected === true ? {} : selected, relation.type === "many", allowAdjacent);
311
+ }
312
+ plan.with = related;
313
+ }
314
+ if (options.orderBy !== void 0) {
315
+ record$1(options.orderBy, "orderBy");
316
+ if (!Object.keys(options.orderBy).length) invalidQuery("orderBy requires a field.");
317
+ for (const [field, direction] of Object.entries(options.orderBy)) plan.order = checkedOrder(plan, field, direction, many);
318
+ }
319
+ for (const key of ["limit", "offset"]) if (options[key] !== void 0) plan[key] = checkedPagination(options[key], many);
320
+ if (options.adjacent !== void 0) plan.adjacent = options.adjacent === true ? true : relationalPlan(registry, collection, options.adjacent, true, false, false);
321
+ return plan;
322
+ }
323
+ /** Creates stable, live collection queries in a dedicated, prototype-free namespace. */
324
+ function createQueryNamespace(registry, run) {
325
+ const queries = /* @__PURE__ */ new Map();
326
+ /** Reuses each handle while checking the current registry on every query execution. */
327
+ function query(name) {
328
+ let cached = queries.get(name);
329
+ if (!cached) {
330
+ cached = {
331
+ findMany: (options) => run(relationalPlan(registry(), name, options), "all"),
332
+ findFirst: (options) => run(relationalPlan(registry(), name, options), "first")
333
+ };
334
+ queries.set(name, cached);
335
+ }
336
+ return cached;
337
+ }
338
+ return new Proxy(Object.create(null), {
339
+ get(_target, name) {
340
+ return typeof name === "string" && Object.hasOwn(registry(), name) ? query(name) : void 0;
341
+ },
342
+ ownKeys() {
343
+ return Object.keys(registry());
344
+ },
345
+ getOwnPropertyDescriptor(_target, name) {
346
+ return typeof name === "string" && Object.hasOwn(registry(), name) ? {
347
+ configurable: true,
348
+ enumerable: true,
349
+ value: query(name),
350
+ writable: false
351
+ } : void 0;
352
+ }
353
+ });
354
+ }
355
+ //#endregion
356
+ //#region src/query/object.ts
357
+ const operators = {
358
+ equals: "eq",
359
+ not: "ne",
360
+ gt: "gt",
361
+ gte: "gte",
362
+ lt: "lt",
363
+ lte: "lte",
364
+ in: "in-array",
365
+ notIn: "not-in-array",
366
+ contains: "contains",
367
+ startsWith: "starts-with",
368
+ endsWith: "ends-with",
369
+ has: "array-contains",
370
+ hasSome: "array-contains-any",
371
+ hasEvery: "array-contains-all",
372
+ isNull: "is-null",
373
+ isUndefined: "is-undefined",
374
+ isMissing: "is-missing"
375
+ };
376
+ /** Requires a plain options record, rejecting arrays and class instances at API boundaries. */
377
+ function record(value, label) {
378
+ if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) invalidQuery(`${label} must be an options object.`);
379
+ }
380
+ /** Converts object predicates to the same validated expressions used by fluent operators. */
381
+ function objectWhere(where) {
382
+ record(where, "where");
383
+ const expressions = [];
384
+ for (const [field, condition] of Object.entries(where)) {
385
+ if ([
386
+ "AND",
387
+ "OR",
388
+ "NOT"
389
+ ].includes(field)) {
390
+ const children = (Array.isArray(condition) ? condition : [condition]).map(objectWhere);
391
+ expressions.push(field === "OR" ? or(...children) : field === "NOT" ? and(...children.map(not)) : and(...children));
392
+ continue;
393
+ }
394
+ if (isScalar(condition)) {
395
+ expressions.push({
396
+ kind: "predicate",
397
+ operator: "eq",
398
+ field,
399
+ value: condition
400
+ });
401
+ continue;
402
+ }
403
+ record(condition, `Filter for "${field}"`);
404
+ for (const [name, value] of Object.entries(condition)) {
405
+ if (!Object.hasOwn(operators, name)) invalidQuery(`Unknown filter operator "${name}".`);
406
+ if (name === "not" && !isScalar(value)) {
407
+ expressions.push(not(objectWhere({ [field]: value })));
408
+ continue;
409
+ }
410
+ const expression = {
411
+ kind: "predicate",
412
+ operator: operators[name],
413
+ field,
414
+ value
415
+ };
416
+ if (name.startsWith("is")) {
417
+ if (typeof value !== "boolean") invalidQuery("Presence predicates require a boolean.");
418
+ expressions.push(value ? expression : not(expression));
419
+ } else expressions.push(expression);
420
+ }
421
+ }
422
+ const expression = and(...expressions);
423
+ validateExpression(expression);
424
+ return detach(expression);
425
+ }
426
+ /** Compiles explicit object options into the backend-independent fluent query contract. */
427
+ function objectPlan(registry, collection, options, many = true, allowAdjacent = true, pagination = true) {
428
+ if (!Object.hasOwn(registry, collection)) invalidQuery(`Unknown collection "${collection}".`);
429
+ record(options, "Query");
430
+ const allowed = [
431
+ "where",
432
+ "select",
433
+ "orderBy",
434
+ ...pagination ? ["limit", "offset"] : []
435
+ ];
436
+ if (Object.keys(options).some((key) => !allowed.includes(key))) invalidQuery("Unknown query option or pagination in an adjacency scope.");
437
+ let plan = emptyPlan(collection);
438
+ if (options.where !== void 0) {
439
+ const expression = typeof options.where === "function" ? options.where(new Proxy(Object.create(null), { get: (_target, name) => ({ name }) })) : objectWhere(options.where);
440
+ validateExpression(expression);
441
+ plan.filters = [detach(expression)];
442
+ }
443
+ if (options.orderBy !== void 0) for (const order of Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy]) {
444
+ record(order, "orderBy");
445
+ if (!Object.keys(order).length) invalidQuery("orderBy requires a field.");
446
+ for (const [field, direction] of Object.entries(order)) plan.order = checkedOrder(plan, field, direction, many);
447
+ }
448
+ for (const key of ["limit", "offset"]) if (options[key] !== void 0) plan[key] = checkedPagination(options[key], many);
449
+ if (options.select !== void 0) {
450
+ record(options.select, "select");
451
+ const selected = Object.entries(options.select).filter(([, value]) => value !== false && value !== void 0);
452
+ if (!selected.length) invalidQuery("Row-returning queries require a nonempty selection.");
453
+ const fields = [];
454
+ const related = Object.create(null);
455
+ for (const [name, selection] of selected) if (name === "_adjacent") {
456
+ if (!allowAdjacent) invalidQuery("Adjacency cannot be selected recursively.");
457
+ plan.adjacent = selection === true ? true : objectPlan(registry, collection, selection, true, false, false);
458
+ if (plan.adjacent !== true && !plan.adjacent.select) invalidQuery("Adjacent entries require explicit selection.");
459
+ } else if (Object.hasOwn(registry[collection].relationships, name)) {
460
+ const relation = registry[collection].relationships[name];
461
+ const nested = objectPlan(registry, relation.collection, selection, relation.type === "many", allowAdjacent);
462
+ if (!nested.select && !nested.adjacent) invalidQuery("Related entries require explicit selection.");
463
+ related[name] = nested;
464
+ } else {
465
+ if (selection !== true) invalidQuery("Prepared fields must be selected with true.");
466
+ fields.push(name);
467
+ }
468
+ plan = {
469
+ ...plan,
470
+ select: checkedFields(registry, collection, fields),
471
+ with: related
472
+ };
473
+ }
474
+ return plan;
475
+ }
476
+ //#endregion
477
+ //#region src/query/builder.ts
478
+ const planSymbol = Symbol("content-query.query-plan");
479
+ const scopeSymbol = Symbol("content-query.query-scope");
480
+ /** Creates immutable root or relationship builders over one backend-independent plan. */
481
+ function createBuilder(registry, run, plan, many = true, root = true, scopeToken = {}, allowAdjacent = true, allowPagination = true) {
482
+ const scope = registry[plan.collection];
483
+ if (!scope) invalidQuery(`Unknown collection "${plan.collection}".`);
484
+ const branch = (next) => createBuilder(registry, run, {
485
+ ...plan,
486
+ ...next
487
+ }, many, root, scopeToken, allowAdjacent, allowPagination);
488
+ const builder = {
489
+ [planSymbol]: plan,
490
+ [scopeSymbol]: scopeToken,
491
+ select(...fields) {
492
+ if (plan.select || fields.length === 0) invalidQuery("select() requires distinct prepared fields and can be called once per scope.");
493
+ return branch({
494
+ select: checkedFields(registry, plan.collection, fields),
495
+ allFields: void 0,
496
+ exclude: void 0
497
+ });
498
+ },
499
+ where(configure) {
500
+ if (typeof configure !== "function") invalidQuery("where() requires a configuration callback.");
501
+ const expression = configure(new Proxy(Object.create(null), { get: (_target, name) => ({ name }) }));
502
+ validateExpression(expression);
503
+ return branch({ filters: [...plan.filters, detach(expression)] });
504
+ },
505
+ orderBy(field, direction = "asc") {
506
+ return branch({ order: checkedOrder(plan, field, direction, many) });
507
+ },
508
+ limit(value) {
509
+ return pagination("limit", value);
510
+ },
511
+ offset(value) {
512
+ return pagination("offset", value);
513
+ },
514
+ with(name, configure) {
515
+ const relation = Object.hasOwn(scope.relationships, name) ? scope.relationships[name] : void 0;
516
+ if (!relation || Object.hasOwn(plan.with, name) || typeof configure !== "function") invalidQuery("Unknown, repeated, or invalid relationship request.");
517
+ const nestedScope = {};
518
+ const configured = configure(createBuilder(registry, run, emptyPlan(relation.collection), relation.type === "many", false, nestedScope, allowAdjacent));
519
+ const nested = configured && typeof configured === "object" && planSymbol in configured ? configured[planSymbol] : void 0;
520
+ if (!hasSelection(nested) || nested.collection !== relation.collection || !(scopeSymbol in configured) || configured[scopeSymbol] !== nestedScope) invalidQuery("Each relationship callback must return its explicitly selected scoped builder.");
521
+ return branch({ with: {
522
+ ...plan.with,
523
+ [name]: nested
524
+ } });
525
+ },
526
+ selectAdjacent(configure) {
527
+ if (!allowAdjacent || plan.adjacent !== void 0) invalidQuery("Adjacency cannot be repeated or selected recursively.");
528
+ if (configure === void 0) return branch({ adjacent: true });
529
+ if (typeof configure !== "function") {
530
+ const adjacent = objectPlan(registry, plan.collection, configure, true, false, false);
531
+ if (!adjacent.select) invalidQuery("Adjacent entries require explicit selection.");
532
+ return branch({ adjacent });
533
+ }
534
+ const nestedScope = {};
535
+ const configured = configure(createBuilder(registry, run, emptyPlan(plan.collection), true, false, nestedScope, false, false));
536
+ const adjacent = configured && typeof configured === "object" && planSymbol in configured ? configured[planSymbol] : void 0;
537
+ if (!hasSelection(adjacent) || !(scopeSymbol in configured) || configured[scopeSymbol] !== nestedScope) invalidQuery("selectAdjacent() must return its explicitly selected sibling query.");
538
+ return branch({ adjacent });
539
+ },
540
+ all() {
541
+ if (!root) invalidQuery("Nested builders have no terminal methods.");
542
+ return run(plan, "all");
543
+ },
544
+ execute() {
545
+ if (!root) invalidQuery("Nested builders have no terminal methods.");
546
+ return run(plan, "all");
547
+ },
548
+ first() {
549
+ if (!root) invalidQuery("Nested builders have no terminal methods.");
550
+ return run(plan, "first");
551
+ },
552
+ count() {
553
+ if (!root) invalidQuery("Nested builders have no terminal methods.");
554
+ return run(plan, "count");
555
+ }
556
+ };
557
+ if (root && hasSelection(plan)) Object.defineProperty(builder, "then", { value: (fulfilled, rejected) => run(plan, "all").then(fulfilled, rejected) });
558
+ /** Checks pagination before creating an independent branch. */
559
+ function pagination(key, value) {
560
+ if (!allowPagination || plan[key] !== void 0) invalidQuery("Pagination requires a nonnegative safe integer, one call per clause, and a many-valued scope.");
561
+ return branch({ [key]: checkedPagination(value, many) });
562
+ }
563
+ return builder;
564
+ }
565
+ //#endregion
566
+ //#region src/query/select.ts
567
+ /** Defers collection validation until from(), preserving the immutable selection across reuse. */
568
+ function createSelect(registry, run, fields) {
569
+ const selected = [...fields];
570
+ return { from(collection) {
571
+ const current = registry();
572
+ if (!Object.hasOwn(current, collection)) invalidQuery(`Unknown collection "${collection}".`);
573
+ const plan = emptyPlan(collection);
574
+ if (selected.length) plan.select = checkedFields(current, collection, selected);
575
+ else plan.allFields = true;
576
+ return createBuilder(current, run, plan);
577
+ } };
578
+ }
579
+ //#endregion
580
+ //#region src/query/normalize.ts
581
+ /** Flattens conjunctions so repeated fluent where clauses and object fields share a plan. */
582
+ function conjunction(expression) {
583
+ return expression.kind === "and" ? expression.expressions.flatMap(conjunction) : [expression];
584
+ }
585
+ /** Normalizes both query frontends at their common execution boundary. */
586
+ function normalizePlan(plan) {
587
+ return {
588
+ ...plan,
589
+ ...plan.select === void 0 && Object.keys(plan.with).length ? { select: [] } : {},
590
+ filters: plan.filters.flatMap(conjunction),
591
+ with: Object.fromEntries(Object.entries(plan.with).map(([name, nested]) => [name, normalizePlan(nested)])),
592
+ ...plan.adjacent && plan.adjacent !== true ? { adjacent: normalizePlan(plan.adjacent) } : {}
593
+ };
594
+ }
595
+ //#endregion
596
+ //#region src/runtime/filter.ts
597
+ /** Matches SQL-style percent/underscore patterns without adopting database collation rules. */
598
+ function matchesPattern(actual, pattern, insensitive) {
599
+ if (typeof actual !== "string") return false;
600
+ let source = "";
601
+ let escaped = false;
602
+ for (const character of pattern) {
603
+ if (!escaped && character === "\\") {
604
+ escaped = true;
605
+ continue;
606
+ }
607
+ source += !escaped && character === "%" ? ".*" : !escaped && character === "_" ? "." : character.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
608
+ escaped = false;
609
+ }
610
+ if (escaped) source += "\\\\";
611
+ return new RegExp(`^${source}$`, insensitive ? "isu" : "su").exec(actual)?.[0] === actual;
612
+ }
613
+ /** Evaluates the portable expression tree using the agreed scalar semantics. */
614
+ function matches(record, expression, revision, collection) {
615
+ if (expression.kind === "and") return expression.expressions.every((child) => matches(record, child, revision, collection));
616
+ if (expression.kind === "or") return expression.expressions.some((child) => matches(record, child, revision, collection));
617
+ if (expression.kind === "not") return !matches(record, expression.expressions[0], revision, collection);
618
+ if (expression.kind === "relation") {
619
+ const relation = revision && collection === expression.collection && revision.registry[collection]?.relationships[expression.field];
620
+ if (!revision || !relation || relation.collection !== expression.target) invalidQuery("Relationship filter no longer matches the prepared collection scope.");
621
+ return (revision.relations.get(relationKey(expression.collection, expression.field))?.get(record.id) ?? []).some((candidate) => matches(candidate, expression.expression, revision, expression.target));
622
+ }
623
+ if (expression.kind !== "predicate") return false;
624
+ const actual = Object.hasOwn(record, expression.field) ? record[expression.field] : void 0;
625
+ const expected = expression.value;
626
+ switch (expression.operator) {
627
+ case "eq": return equal(actual, expected);
628
+ case "ne": return !equal(actual, expected);
629
+ case "is-null": return actual === null;
630
+ case "is-undefined": return actual === void 0;
631
+ case "is-missing": return !Object.hasOwn(record, expression.field);
632
+ case "in-array": return expected.some((candidate) => equal(actual, candidate));
633
+ case "not-in-array": return !expected.some((candidate) => equal(actual, candidate));
634
+ case "contains": return typeof actual === "string" && actual.includes(expected);
635
+ case "starts-with": return typeof actual === "string" && actual.startsWith(expected);
636
+ case "ends-with": return typeof actual === "string" && actual.endsWith(expected);
637
+ case "like":
638
+ case "ilike": return matchesPattern(actual, expected, expression.operator === "ilike");
639
+ case "array-contains": return Array.isArray(actual) && actual.some((candidate) => equal(candidate, expected));
640
+ case "array-contains-any": return Array.isArray(actual) && expected.some((candidate) => actual.some((item) => equal(item, candidate)));
641
+ case "array-contains-all": return Array.isArray(actual) && expected.every((candidate) => actual.some((item) => equal(item, candidate)));
642
+ case "array-contained": return Array.isArray(actual) && actual.every((candidate) => expected.some((item) => equal(candidate, item)));
643
+ default: {
644
+ const order = compare(actual, expected);
645
+ if (order === void 0) return false;
646
+ return expression.operator === "gt" ? order > 0 : expression.operator === "gte" ? order >= 0 : expression.operator === "lt" ? order < 0 : order <= 0;
647
+ }
648
+ }
649
+ }
650
+ //#endregion
651
+ //#region src/runtime/execute.ts
652
+ const recordIndexes = /* @__PURE__ */ new WeakMap();
653
+ /** Narrows root ID-equality queries using one lazy index per immutable collection revision. */
654
+ function queryCandidates(records, plan) {
655
+ const equality = plan.filters.find((expression) => expression.kind === "predicate" && expression.field === "id" && expression.operator === "eq" && typeof expression.value === "string");
656
+ if (equality?.kind !== "predicate" || typeof equality.value !== "string") return records;
657
+ let index = recordIndexes.get(records);
658
+ if (!index) {
659
+ index = new Map(records.map((record) => [record.id, record]));
660
+ recordIndexes.set(records, index);
661
+ }
662
+ const record = index.get(equality.value);
663
+ return record ? [record] : [];
664
+ }
665
+ /** Classifies nullish and unordered special values for direction-independent placement. */
666
+ function sortGroup(value) {
667
+ return value == null ? 2 : typeof value === "number" && Number.isNaN(value) || value instanceof Date && Number.isNaN(value.getTime()) ? 1 : 0;
668
+ }
669
+ /** Filters, orders, and pages one collection or one parent's related candidates. */
670
+ function pageRecords(records, plan, revision, preserveOrder = false) {
671
+ if (records.length <= 1 && !plan.order.length) {
672
+ const record = records[0];
673
+ return record && plan.filters.every((expression) => matches(record, expression, revision, plan.collection)) && (plan.offset ?? 0) === 0 && plan.limit !== 0 ? [record] : [];
674
+ }
675
+ const filtered = records.filter((record) => plan.filters.every((expression) => matches(record, expression, revision, plan.collection)));
676
+ const order = plan.order.length ? [...plan.order] : preserveOrder ? [] : [{
677
+ field: "id",
678
+ direction: "asc"
679
+ }];
680
+ if (order.length && !order.some((sort) => sort.field === "id")) order.push({
681
+ field: "id",
682
+ direction: "asc"
683
+ });
684
+ for (const sort of order) {
685
+ let category;
686
+ for (const record of filtered) {
687
+ const value = Object.hasOwn(record, sort.field) ? record[sort.field] : void 0;
688
+ if (!isScalar(value)) invalidQuery(`Field "${sort.field}" is not sortable.`);
689
+ if (value != null) {
690
+ const current = value instanceof Date ? "date" : typeof value;
691
+ if (category && category !== current) invalidQuery(`Field "${sort.field}" has mixed sort categories.`);
692
+ category = current;
693
+ }
694
+ }
695
+ }
696
+ if (order.length) filtered.sort((left, right) => {
697
+ for (const sort of order) {
698
+ const leftValue = Object.hasOwn(left, sort.field) ? left[sort.field] : void 0;
699
+ const rightValue = Object.hasOwn(right, sort.field) ? right[sort.field] : void 0;
700
+ const group = sortGroup(leftValue) - sortGroup(rightValue);
701
+ if (group) return group;
702
+ if (sortGroup(leftValue) !== 0) continue;
703
+ const comparison = compare(leftValue, rightValue) ?? 0;
704
+ if (comparison) return sort.direction === "desc" ? -comparison : comparison;
705
+ }
706
+ return 0;
707
+ });
708
+ const offset = plan.offset ?? 0;
709
+ return filtered.slice(offset, plan.limit === void 0 ? void 0 : offset + plan.limit);
710
+ }
711
+ /** Builds a sibling index once per scope, before pagination and within the pinned revision. */
712
+ function neighborScope(plan, records, preserveOrder, scopes, revision) {
713
+ const selection = plan.adjacent;
714
+ const siblingPlan = selection === true ? {
715
+ ...plan,
716
+ select: ["id"],
717
+ allFields: void 0,
718
+ exclude: void 0,
719
+ with: {},
720
+ adjacent: void 0,
721
+ limit: void 0,
722
+ offset: void 0
723
+ } : selection;
724
+ const key = selection === true ? plan : siblingPlan;
725
+ let candidates = scopes.get(key);
726
+ if (!candidates) {
727
+ candidates = /* @__PURE__ */ new WeakMap();
728
+ scopes.set(key, candidates);
729
+ }
730
+ let scope = candidates.get(records);
731
+ if (!scope) {
732
+ const ordered = pageRecords([...new Map(records.map((entry) => [entry.id, entry])).values()], siblingPlan, revision, selection === true && preserveOrder);
733
+ scope = {
734
+ plan: siblingPlan,
735
+ records: ordered,
736
+ positions: new Map(ordered.map((entry, index) => [entry.id, index]))
737
+ };
738
+ candidates.set(records, scope);
739
+ }
740
+ return scope;
741
+ }
742
+ /** Projects one entry and requested relationships/adjacency from one pinned revision. */
743
+ function project(record, plan, revision, scopes, scopeRecords, preserveScopeOrder = false) {
744
+ if (!plan.select && !plan.allFields && !plan.adjacent) invalidQuery("Row-returning queries require select().");
745
+ const result = detachSelection(record, plan.allFields ? Object.keys(record).filter((field) => !plan.exclude?.includes(field)) : plan.select ?? []);
746
+ for (const [name, nested] of Object.entries(plan.with)) {
747
+ const relation = revision.registry[plan.collection].relationships[name];
748
+ 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.`);
749
+ const candidates = revision.relations.get(relationKey(plan.collection, name))?.get(record.id) ?? [];
750
+ const preserveOrder = relation.referenceSide === "from" && Array.isArray(record[relation.from]);
751
+ const selected = pageRecords(candidates, nested, revision, preserveOrder).map((entry) => project(entry, nested, revision, scopes, candidates, preserveOrder));
752
+ Object.defineProperty(result, name, {
753
+ value: relation.type === "many" ? selected : selected[0] ?? null,
754
+ enumerable: true,
755
+ configurable: true,
756
+ writable: true
757
+ });
758
+ }
759
+ if (plan.adjacent) {
760
+ const candidates = plan.adjacent === true ? scopeRecords : revision.collections[plan.collection].records;
761
+ const scope = neighborScope(plan, candidates, preserveScopeOrder, scopes, revision);
762
+ const index = scope.positions.get(record.id);
763
+ const previous = index === void 0 ? void 0 : scope.records[index - 1];
764
+ const next = index === void 0 ? void 0 : scope.records[index + 1];
765
+ result._adjacent = {
766
+ previous: previous ? project(previous, scope.plan, revision, scopes, candidates) : null,
767
+ next: next ? project(next, scope.plan, revision, scopes, candidates) : null
768
+ };
769
+ }
770
+ return result;
771
+ }
772
+ /** Runs the shared JavaScript executor; future adapters can execute the same plan. */
773
+ async function executeQuery(revision, plan, terminal) {
774
+ const collection = revision.collections[plan.collection];
775
+ if (!collection) invalidQuery(`Unknown collection "${plan.collection}".`);
776
+ if (terminal === "count") return queryCandidates(collection.records, plan).filter((record) => plan.filters.every((expression) => matches(record, expression, revision, plan.collection))).length;
777
+ if (!plan.select && !plan.allFields && !plan.adjacent) invalidQuery("Row-returning queries require select().");
778
+ const records = pageRecords(queryCandidates(collection.records, plan), plan, revision);
779
+ const scopes = /* @__PURE__ */ new WeakMap();
780
+ return terminal === "first" ? records[0] ? project(records[0], plan, revision, scopes, collection.records) : void 0 : records.map((record) => project(record, plan, revision, scopes, collection.records));
781
+ }
782
+ //#endregion
783
+ //#region src/runtime/content.ts
784
+ assertServerOnly();
785
+ /** Pins the latest valid revision for an entire asynchronous operation. */
786
+ async function pinned(state, operation) {
787
+ if (state.error) throw state.error;
788
+ const revision = state.revision;
789
+ if (!revision) throw new ContentError({
790
+ code: "artifact-error",
791
+ message: "No prepared content revision is available."
792
+ });
793
+ revision.active++;
794
+ try {
795
+ return await operation(revision);
796
+ } finally {
797
+ revision.active--;
798
+ }
799
+ }
800
+ const idSets = /* @__PURE__ */ new WeakMap();
801
+ /** Indexes immutable prepared records once, so body/render lookups avoid collection scans. */
802
+ function entryIds(records) {
803
+ let ids = idSets.get(records);
804
+ if (!ids) {
805
+ ids = new Set(records.map((record) => record.id));
806
+ idSets.set(records, ids);
807
+ }
808
+ return ids;
809
+ }
810
+ /** Looks up an entry and lazily loads only its revision-specific payload. */
811
+ async function loadPayload(revision, collectionName, id) {
812
+ const collection = revision.collections[collectionName];
813
+ if (!collection || typeof collectionName !== "string" || typeof id !== "string") invalidQuery("Body/render access requires a registered collection and string ID.");
814
+ if (!entryIds(collection.records).has(id)) throw new ContentError({
815
+ code: "entry-not-found",
816
+ collection: collectionName,
817
+ id,
818
+ message: `Entry "${id}" does not exist in "${collectionName}".`
819
+ });
820
+ const key = JSON.stringify([collectionName, id]);
821
+ let loaded = revision.payloadCache.get(key);
822
+ if (!loaded) {
823
+ loaded = Promise.resolve().then(async () => {
824
+ const payload = await (collection.payloads.get(id)?.() ?? {});
825
+ if (!payload || typeof payload !== "object" || Array.isArray(payload) || payload.body !== void 0 && typeof payload.body !== "string" || payload.markdown !== void 0 && typeof payload.markdown !== "string" || payload.rendered !== void 0 && (!payload.rendered || typeof payload.rendered.html !== "string" || !Array.isArray(payload.rendered.headings) || payload.rendered.headings.some((heading) => !heading || !Number.isInteger(heading.depth) || heading.depth < 1 || heading.depth > 6 || typeof heading.slug !== "string" || typeof heading.text !== "string"))) throw new Error("Invalid prepared payload shape.");
826
+ return payload;
827
+ }).catch((cause) => {
828
+ revision.payloadCache.delete(key);
829
+ throw contentError(cause, {
830
+ code: "artifact-error",
831
+ collection: collectionName,
832
+ id,
833
+ message: "Could not load the prepared entry payload."
834
+ });
835
+ });
836
+ revision.payloadCache.set(key, loaded);
837
+ }
838
+ return loaded;
839
+ }
840
+ /** Creates the initialized runtime without importing source configuration or schemas. */
841
+ function createRuntime(state) {
842
+ /** Pins both query frontends to exactly the same revision and backend executor. */
843
+ const run = (plan, terminal) => pinned(state, (revision) => revision.execute ? revision.execute(normalizePlan(plan), terminal) : executeQuery(revision, normalizePlan(plan), terminal));
844
+ /** Reads the current registry without capturing a development revision in either API. */
845
+ const registry = () => state.registry ?? state.revision?.registry ?? {};
846
+ return {
847
+ select(...fields) {
848
+ return createSelect(registry, run, fields);
849
+ },
850
+ query: createQueryNamespace(registry, run),
851
+ count(collection, options = {}) {
852
+ return run(relationalPlan(registry(), collection, options), "count");
853
+ },
854
+ readBody(collection, id) {
855
+ return pinned(state, async (revision) => (await loadPayload(revision, collection, id)).body);
856
+ },
857
+ render(collection, id) {
858
+ return pinned(state, async (revision) => {
859
+ const payload = await loadPayload(revision, collection, id);
860
+ if (payload.rendered) return detach(payload.rendered);
861
+ if (payload.markdown === void 0) throw new ContentError({
862
+ code: "not-renderable",
863
+ collection,
864
+ id,
865
+ message: "This entry has no supplied output or supported rendering path."
866
+ });
867
+ const key = JSON.stringify([collection, id]);
868
+ let rendered = revision.renderCache.get(key);
869
+ if (!rendered) {
870
+ rendered = Promise.resolve().then(() => {
871
+ if (!revision.renderMarkdown) throw new Error("Missing packaged Markdown renderer.");
872
+ return revision.renderMarkdown(payload.markdown);
873
+ }).catch((cause) => {
874
+ revision.renderCache.delete(key);
875
+ throw contentError(cause, {
876
+ code: "render-error",
877
+ collection,
878
+ id,
879
+ message: "Markdown rendering failed."
880
+ });
881
+ });
882
+ revision.renderCache.set(key, rendered);
883
+ }
884
+ return detach(await rendered);
885
+ });
886
+ }
887
+ };
888
+ }
889
+ /** Keeps dev builders live across host-delivered revisions while pinning active operations. */
890
+ function createLiveRuntime(initial) {
891
+ const state = { ...initial };
892
+ return {
893
+ content: createRuntime(state),
894
+ update(next) {
895
+ state.revision = next.revision;
896
+ state.registry = next.registry;
897
+ state.error = next.error;
898
+ }
899
+ };
900
+ }
901
+ //#endregion
902
+ export { CODEC_VERSION as a, prepareRelationships as i, createRuntime as n, isRelationship as r, createLiveRuntime as t };