content-query 0.0.2 → 0.2.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/LICENSE +21 -0
- package/README.md +91 -2
- package/dist/build-DfKd2bpf.d.mts +25 -0
- package/dist/build-Dxz7P1ur.mjs +1439 -0
- package/dist/build.d.mts +2 -0
- package/dist/build.mjs +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +97 -0
- package/dist/computed.d.mts +43 -0
- package/dist/computed.mjs +139 -0
- package/dist/content-G69dPPuo.d.mts +109 -0
- package/dist/content-N1gYshjg.mjs +1028 -0
- package/dist/descriptor-D9Th5hzk.d.mts +18 -0
- package/dist/diagnostics-Cu94mZhl.mjs +80 -0
- package/dist/dist-C7_FOzY_.mjs +5405 -0
- package/dist/errors-C2iuBM0G.mjs +72 -0
- package/dist/github-slugger-BMgR_Q0I.mjs +72 -0
- package/dist/glob-Bv0dpvCQ.mjs +159 -0
- package/dist/index.d.mts +78 -0
- package/dist/index.mjs +46 -0
- package/dist/internal/markdown.d.mts +5 -0
- package/dist/internal/markdown.mjs +1 -0
- package/dist/internal.d.mts +54 -0
- package/dist/internal.mjs +89 -0
- package/dist/loaders.d.mts +11 -0
- package/dist/loaders.mjs +86 -0
- package/dist/markdown-CHvBBJNa.mjs +54 -0
- package/dist/next.d.mts +20 -0
- package/dist/next.mjs +142 -0
- package/dist/operators-UqWUhKQJ.mjs +173 -0
- package/dist/query.d.mts +44 -0
- package/dist/query.mjs +1 -0
- package/dist/relational-types-DgK3rLuw.d.mts +278 -0
- package/dist/rolldown-runtime-Bg1v5YCA.mjs +34 -0
- package/dist/runtime.d.mts +3 -0
- package/dist/runtime.mjs +5 -0
- package/dist/schema-C5Str-E8.mjs +20 -0
- package/dist/schema.d.mts +11 -0
- package/dist/schema.mjs +1 -0
- package/dist/server-only-CIJUlyL-.mjs +7 -0
- package/dist/types-C-kdMblM.d.mts +69 -0
- package/dist/types-ChaNhj42.d.mts +143 -0
- package/dist/types-Dh-Sydyw.d.mts +20 -0
- package/dist/validation-D_XdE1pF.mjs +84 -0
- package/dist/values-BbTV4QT-.mjs +901 -0
- package/dist/vite.d.mts +6 -0
- package/dist/vite.mjs +296 -0
- package/package.json +92 -10
|
@@ -0,0 +1,1028 @@
|
|
|
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";
|
|
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";
|
|
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/shared/concurrency.ts
|
|
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.
|
|
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.
|
|
156
|
+
*/
|
|
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
|
+
}
|
|
171
|
+
}
|
|
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;
|
|
176
|
+
}
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/query/clauses.ts
|
|
179
|
+
/** Checks scalar field selection for both public query frontends without a schema field catalog. */
|
|
180
|
+
function checkedFields(registry, collection, fields) {
|
|
181
|
+
const scope = registry[collection];
|
|
182
|
+
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
|
+
return [...fields];
|
|
184
|
+
}
|
|
185
|
+
/** Checks one sort clause with the same cardinality and duplicate rules in either API. */
|
|
186
|
+
function checkedOrder(plan, field, direction, many) {
|
|
187
|
+
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.");
|
|
188
|
+
return [...plan.order, {
|
|
189
|
+
field,
|
|
190
|
+
direction
|
|
191
|
+
}];
|
|
192
|
+
}
|
|
193
|
+
/** Checks pagination before either frontend constructs its shared query plan. */
|
|
194
|
+
function checkedPagination(value, many) {
|
|
195
|
+
if (!many || !Number.isSafeInteger(value) || value < 0) invalidQuery("Pagination requires a nonnegative safe integer and a many-valued scope.");
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
/** Finds a payload declaration that queries may request, rejecting unknown names. */
|
|
199
|
+
function payloadDescriptor(registry, collection, name) {
|
|
200
|
+
const payloads = registry[collection]?.payloads ?? {};
|
|
201
|
+
if (typeof name !== "string" || !Object.hasOwn(payloads, name)) invalidQuery(`Unknown payload "${String(name)}" for collection "${collection}".`);
|
|
202
|
+
return payloads[name];
|
|
203
|
+
}
|
|
204
|
+
/** Checks one payload request against the collection's payload names and variant keys. */
|
|
205
|
+
function checkedPayload(registry, collection, name, request) {
|
|
206
|
+
const { variants } = payloadDescriptor(registry, collection, name);
|
|
207
|
+
if (request === true || request === void 0) {
|
|
208
|
+
if (variants.length) invalidQuery(`Payload "${name}" requires variant keys: ${variants.join(", ")}.`);
|
|
209
|
+
return { variant: {} };
|
|
210
|
+
}
|
|
211
|
+
const variant = request;
|
|
212
|
+
if (!variant || typeof variant !== "object" || Array.isArray(variant) || Object.keys(variant).length !== variants.length || variants.some((key) => !Object.hasOwn(variant, key) || typeof variant[key] !== "string")) invalidQuery(`Payload "${name}" requires exactly these string variant keys: ${variants.join(", ") || "none"}.`);
|
|
213
|
+
return { variant: Object.fromEntries([...variants].sort().map((key) => [key, variant[key]])) };
|
|
214
|
+
}
|
|
215
|
+
/** Compiles a payloads option into plan requests, omitting payloads set to false. */
|
|
216
|
+
function checkedPayloads(registry, collection, payloads) {
|
|
217
|
+
const requested = Object.create(null);
|
|
218
|
+
for (const [name, request] of Object.entries(payloads)) {
|
|
219
|
+
if (request === false || request === void 0) {
|
|
220
|
+
payloadDescriptor(registry, collection, name);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
requested[name] = checkedPayload(registry, collection, name, request);
|
|
224
|
+
}
|
|
225
|
+
return Object.keys(requested).length ? requested : void 0;
|
|
226
|
+
}
|
|
227
|
+
//#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
|
+
//#region src/query/object.ts
|
|
425
|
+
const operators = {
|
|
426
|
+
equals: "eq",
|
|
427
|
+
not: "ne",
|
|
428
|
+
gt: "gt",
|
|
429
|
+
gte: "gte",
|
|
430
|
+
lt: "lt",
|
|
431
|
+
lte: "lte",
|
|
432
|
+
in: "in-array",
|
|
433
|
+
notIn: "not-in-array",
|
|
434
|
+
contains: "contains",
|
|
435
|
+
startsWith: "starts-with",
|
|
436
|
+
endsWith: "ends-with",
|
|
437
|
+
has: "array-contains",
|
|
438
|
+
hasSome: "array-contains-any",
|
|
439
|
+
hasEvery: "array-contains-all",
|
|
440
|
+
isNull: "is-null",
|
|
441
|
+
isUndefined: "is-undefined",
|
|
442
|
+
isMissing: "is-missing"
|
|
443
|
+
};
|
|
444
|
+
/** Requires a plain options record, rejecting arrays and class instances at API boundaries. */
|
|
445
|
+
function record(value, label) {
|
|
446
|
+
if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) invalidQuery(`${label} must be an options object.`);
|
|
447
|
+
}
|
|
448
|
+
/** Converts object predicates to the same validated expressions used by fluent operators. */
|
|
449
|
+
function objectWhere(where) {
|
|
450
|
+
record(where, "where");
|
|
451
|
+
const expressions = [];
|
|
452
|
+
for (const [field, condition] of Object.entries(where)) {
|
|
453
|
+
if ([
|
|
454
|
+
"AND",
|
|
455
|
+
"OR",
|
|
456
|
+
"NOT"
|
|
457
|
+
].includes(field)) {
|
|
458
|
+
const children = (Array.isArray(condition) ? condition : [condition]).map(objectWhere);
|
|
459
|
+
expressions.push(field === "OR" ? or(...children) : field === "NOT" ? and(...children.map(not)) : and(...children));
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (isScalar(condition)) {
|
|
463
|
+
expressions.push({
|
|
464
|
+
kind: "predicate",
|
|
465
|
+
operator: "eq",
|
|
466
|
+
field,
|
|
467
|
+
value: condition
|
|
468
|
+
});
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
record(condition, `Filter for "${field}"`);
|
|
472
|
+
for (const [name, value] of Object.entries(condition)) {
|
|
473
|
+
if (!Object.hasOwn(operators, name)) invalidQuery(`Unknown filter operator "${name}".`);
|
|
474
|
+
if (name === "not" && !isScalar(value)) {
|
|
475
|
+
expressions.push(not(objectWhere({ [field]: value })));
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
const expression = {
|
|
479
|
+
kind: "predicate",
|
|
480
|
+
operator: operators[name],
|
|
481
|
+
field,
|
|
482
|
+
value
|
|
483
|
+
};
|
|
484
|
+
if (name.startsWith("is")) {
|
|
485
|
+
if (typeof value !== "boolean") invalidQuery("Presence predicates require a boolean.");
|
|
486
|
+
expressions.push(value ? expression : not(expression));
|
|
487
|
+
} else expressions.push(expression);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const expression = and(...expressions);
|
|
491
|
+
validateExpression(expression);
|
|
492
|
+
return detach(expression);
|
|
493
|
+
}
|
|
494
|
+
/** Compiles explicit object options into the backend-independent fluent query contract. */
|
|
495
|
+
function objectPlan(registry, collection, options, many = true, allowAdjacent = true, pagination = true) {
|
|
496
|
+
if (!Object.hasOwn(registry, collection)) invalidQuery(`Unknown collection "${collection}".`);
|
|
497
|
+
record(options, "Query");
|
|
498
|
+
const allowed = [
|
|
499
|
+
"where",
|
|
500
|
+
"select",
|
|
501
|
+
"orderBy",
|
|
502
|
+
...pagination ? ["limit", "offset"] : []
|
|
503
|
+
];
|
|
504
|
+
if (Object.keys(options).some((key) => !allowed.includes(key))) invalidQuery("Unknown query option or pagination in an adjacency scope.");
|
|
505
|
+
let plan = emptyPlan(collection);
|
|
506
|
+
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);
|
|
508
|
+
validateExpression(expression);
|
|
509
|
+
plan.filters = [detach(expression)];
|
|
510
|
+
}
|
|
511
|
+
if (options.orderBy !== void 0) for (const order of Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy]) {
|
|
512
|
+
record(order, "orderBy");
|
|
513
|
+
if (!Object.keys(order).length) invalidQuery("orderBy requires a field.");
|
|
514
|
+
for (const [field, direction] of Object.entries(order)) plan.order = checkedOrder(plan, field, direction, many);
|
|
515
|
+
}
|
|
516
|
+
for (const key of ["limit", "offset"]) if (options[key] !== void 0) plan[key] = checkedPagination(options[key], many);
|
|
517
|
+
if (options.select !== void 0) {
|
|
518
|
+
record(options.select, "select");
|
|
519
|
+
const selected = Object.entries(options.select).filter(([, value]) => value !== false && value !== void 0);
|
|
520
|
+
if (!selected.length) invalidQuery("Row-returning queries require a nonempty selection.");
|
|
521
|
+
const fields = [];
|
|
522
|
+
const related = Object.create(null);
|
|
523
|
+
for (const [name, selection] of selected) if (name === "_adjacent") {
|
|
524
|
+
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.");
|
|
527
|
+
} else if (Object.hasOwn(registry[collection].relationships, name)) {
|
|
528
|
+
const relation = registry[collection].relationships[name];
|
|
529
|
+
const nested = objectPlan(registry, relation.collection, selection, relation.type === "many", allowAdjacent);
|
|
530
|
+
if (!nested.select && !nested.adjacent) invalidQuery("Related entries require explicit selection.");
|
|
531
|
+
related[name] = nested;
|
|
532
|
+
} else {
|
|
533
|
+
if (selection !== true) invalidQuery("Prepared fields must be selected with true.");
|
|
534
|
+
fields.push(name);
|
|
535
|
+
}
|
|
536
|
+
plan = {
|
|
537
|
+
...plan,
|
|
538
|
+
select: checkedFields(registry, collection, fields),
|
|
539
|
+
with: related
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
return plan;
|
|
543
|
+
}
|
|
544
|
+
//#endregion
|
|
545
|
+
//#region src/query/builder.ts
|
|
546
|
+
const planSymbol = Symbol("content-query.query-plan");
|
|
547
|
+
const scopeSymbol = Symbol("content-query.query-scope");
|
|
548
|
+
/** Creates immutable root or relationship builders over one backend-independent plan. */
|
|
549
|
+
function createBuilder(registry, run, plan, many = true, root = true, scopeToken = {}, allowAdjacent = true, allowPagination = true) {
|
|
550
|
+
const scope = registry[plan.collection];
|
|
551
|
+
if (!scope) invalidQuery(`Unknown collection "${plan.collection}".`);
|
|
552
|
+
const branch = (next) => createBuilder(registry, run, {
|
|
553
|
+
...plan,
|
|
554
|
+
...next
|
|
555
|
+
}, many, root, scopeToken, allowAdjacent, allowPagination);
|
|
556
|
+
const builder = {
|
|
557
|
+
[planSymbol]: plan,
|
|
558
|
+
[scopeSymbol]: scopeToken,
|
|
559
|
+
select(...fields) {
|
|
560
|
+
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
|
+
});
|
|
566
|
+
},
|
|
567
|
+
where(configure) {
|
|
568
|
+
if (typeof configure !== "function") invalidQuery("where() requires a configuration callback.");
|
|
569
|
+
const expression = configure(new Proxy(Object.create(null), { get: (_target, name) => ({ name }) }));
|
|
570
|
+
validateExpression(expression);
|
|
571
|
+
return branch({ filters: [...plan.filters, detach(expression)] });
|
|
572
|
+
},
|
|
573
|
+
orderBy(field, direction = "asc") {
|
|
574
|
+
return branch({ order: checkedOrder(plan, field, direction, many) });
|
|
575
|
+
},
|
|
576
|
+
limit(value) {
|
|
577
|
+
return pagination("limit", value);
|
|
578
|
+
},
|
|
579
|
+
offset(value) {
|
|
580
|
+
return pagination("offset", value);
|
|
581
|
+
},
|
|
582
|
+
with(name, configure) {
|
|
583
|
+
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.");
|
|
585
|
+
const nestedScope = {};
|
|
586
|
+
const configured = configure(createBuilder(registry, run, emptyPlan(relation.collection), relation.type === "many", false, nestedScope, allowAdjacent));
|
|
587
|
+
const nested = configured && typeof configured === "object" && planSymbol in configured ? configured[planSymbol] : void 0;
|
|
588
|
+
if (!hasSelection(nested) || nested.collection !== relation.collection || !(scopeSymbol in configured) || configured[scopeSymbol] !== nestedScope) invalidQuery("Each relationship callback must return its explicitly selected scoped builder.");
|
|
589
|
+
return branch({ with: {
|
|
590
|
+
...plan.with,
|
|
591
|
+
[name]: nested
|
|
592
|
+
} });
|
|
593
|
+
},
|
|
594
|
+
withPayload(name, variant) {
|
|
595
|
+
if (!allowAdjacent || Object.hasOwn(plan.payloads ?? {}, name)) invalidQuery("Payloads cannot be repeated or requested in adjacent scopes.");
|
|
596
|
+
return branch({ payloads: {
|
|
597
|
+
...plan.payloads,
|
|
598
|
+
[name]: checkedPayload(registry, plan.collection, name, variant)
|
|
599
|
+
} });
|
|
600
|
+
},
|
|
601
|
+
selectAdjacent(configure) {
|
|
602
|
+
if (!allowAdjacent || plan.adjacent !== void 0) invalidQuery("Adjacency cannot be repeated or selected recursively.");
|
|
603
|
+
if (configure === void 0) return branch({ adjacent: true });
|
|
604
|
+
if (typeof configure !== "function") {
|
|
605
|
+
const adjacent = objectPlan(registry, plan.collection, configure, true, false, false);
|
|
606
|
+
if (!adjacent.select) invalidQuery("Adjacent entries require explicit selection.");
|
|
607
|
+
return branch({ adjacent });
|
|
608
|
+
}
|
|
609
|
+
const nestedScope = {};
|
|
610
|
+
const configured = configure(createBuilder(registry, run, emptyPlan(plan.collection), true, false, nestedScope, false, false));
|
|
611
|
+
const adjacent = configured && typeof configured === "object" && planSymbol in configured ? configured[planSymbol] : void 0;
|
|
612
|
+
if (!hasSelection(adjacent) || !(scopeSymbol in configured) || configured[scopeSymbol] !== nestedScope) invalidQuery("selectAdjacent() must return its explicitly selected sibling query.");
|
|
613
|
+
return branch({ adjacent });
|
|
614
|
+
},
|
|
615
|
+
all() {
|
|
616
|
+
if (!root) invalidQuery("Nested builders have no terminal methods.");
|
|
617
|
+
return run(plan, "all");
|
|
618
|
+
},
|
|
619
|
+
/** Executes a selected root query, matching direct await behavior. */
|
|
620
|
+
execute() {
|
|
621
|
+
if (!root) invalidQuery("Nested builders have no terminal methods.");
|
|
622
|
+
return run(plan, "all");
|
|
623
|
+
},
|
|
624
|
+
first() {
|
|
625
|
+
if (!root) invalidQuery("Nested builders have no terminal methods.");
|
|
626
|
+
return run(plan, "first");
|
|
627
|
+
},
|
|
628
|
+
count() {
|
|
629
|
+
if (!root) invalidQuery("Nested builders have no terminal methods.");
|
|
630
|
+
return run(plan, "count");
|
|
631
|
+
}
|
|
632
|
+
};
|
|
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) });
|
|
638
|
+
}
|
|
639
|
+
return builder;
|
|
640
|
+
}
|
|
641
|
+
//#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
|
+
} };
|
|
656
|
+
}
|
|
657
|
+
//#endregion
|
|
658
|
+
//#region src/query/normalize.ts
|
|
659
|
+
/** Flattens conjunctions so repeated fluent where clauses and object fields share a plan. */
|
|
660
|
+
function conjunction(expression) {
|
|
661
|
+
return expression.kind === "and" ? expression.expressions.flatMap(conjunction) : [expression];
|
|
662
|
+
}
|
|
663
|
+
/** Normalizes both query frontends at their common execution boundary. */
|
|
664
|
+
function normalizePlan(plan) {
|
|
665
|
+
return {
|
|
666
|
+
...plan,
|
|
667
|
+
...plan.select === void 0 && Object.keys(plan.with).length ? { select: [] } : {},
|
|
668
|
+
filters: plan.filters.flatMap(conjunction),
|
|
669
|
+
with: Object.fromEntries(Object.entries(plan.with).map(([name, nested]) => [name, normalizePlan(nested)])),
|
|
670
|
+
...plan.adjacent && plan.adjacent !== true ? { adjacent: normalizePlan(plan.adjacent) } : {}
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
//#endregion
|
|
674
|
+
//#region src/runtime/filter.ts
|
|
675
|
+
/** Matches SQL-style percent/underscore patterns without adopting database collation rules. */
|
|
676
|
+
function matchesPattern(actual, pattern, insensitive) {
|
|
677
|
+
if (typeof actual !== "string") return false;
|
|
678
|
+
let source = "";
|
|
679
|
+
let escaped = false;
|
|
680
|
+
for (const character of pattern) {
|
|
681
|
+
if (!escaped && character === "\\") {
|
|
682
|
+
escaped = true;
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
source += !escaped && character === "%" ? ".*" : !escaped && character === "_" ? "." : character.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
686
|
+
escaped = false;
|
|
687
|
+
}
|
|
688
|
+
if (escaped) source += "\\\\";
|
|
689
|
+
return new RegExp(`^${source}$`, insensitive ? "isu" : "su").exec(actual)?.[0] === actual;
|
|
690
|
+
}
|
|
691
|
+
/** Evaluates the portable expression tree using the agreed scalar semantics. */
|
|
692
|
+
function matches(record, expression, revision, collection) {
|
|
693
|
+
if (expression.kind === "and") return expression.expressions.every((child) => matches(record, child, revision, collection));
|
|
694
|
+
if (expression.kind === "or") return expression.expressions.some((child) => matches(record, child, revision, collection));
|
|
695
|
+
if (expression.kind === "not") return !matches(record, expression.expressions[0], revision, collection);
|
|
696
|
+
if (expression.kind === "relation") {
|
|
697
|
+
const relation = revision && collection === expression.collection && revision.registry[collection]?.relationships[expression.field];
|
|
698
|
+
if (!revision || !relation || relation.collection !== expression.target) invalidQuery("Relationship filter no longer matches the prepared collection scope.");
|
|
699
|
+
return (revision.relations.get(relationKey(expression.collection, expression.field))?.get(record.id) ?? []).some((candidate) => matches(candidate, expression.expression, revision, expression.target));
|
|
700
|
+
}
|
|
701
|
+
if (expression.kind !== "predicate") return false;
|
|
702
|
+
const actual = Object.hasOwn(record, expression.field) ? record[expression.field] : void 0;
|
|
703
|
+
const expected = expression.value;
|
|
704
|
+
switch (expression.operator) {
|
|
705
|
+
case "eq": return equal(actual, expected);
|
|
706
|
+
case "ne": return !equal(actual, expected);
|
|
707
|
+
case "is-null": return actual === null;
|
|
708
|
+
case "is-undefined": return actual === void 0;
|
|
709
|
+
case "is-missing": return !Object.hasOwn(record, expression.field);
|
|
710
|
+
case "in-array": return expected.some((candidate) => equal(actual, candidate));
|
|
711
|
+
case "not-in-array": return !expected.some((candidate) => equal(actual, candidate));
|
|
712
|
+
case "contains": return typeof actual === "string" && actual.includes(expected);
|
|
713
|
+
case "starts-with": return typeof actual === "string" && actual.startsWith(expected);
|
|
714
|
+
case "ends-with": return typeof actual === "string" && actual.endsWith(expected);
|
|
715
|
+
case "like":
|
|
716
|
+
case "ilike": return matchesPattern(actual, expected, expression.operator === "ilike");
|
|
717
|
+
case "array-contains": return Array.isArray(actual) && actual.some((candidate) => equal(candidate, expected));
|
|
718
|
+
case "array-contains-any": return Array.isArray(actual) && expected.some((candidate) => actual.some((item) => equal(item, candidate)));
|
|
719
|
+
case "array-contains-all": return Array.isArray(actual) && expected.every((candidate) => actual.some((item) => equal(item, candidate)));
|
|
720
|
+
case "array-contained": return Array.isArray(actual) && actual.every((candidate) => expected.some((item) => equal(candidate, item)));
|
|
721
|
+
default: {
|
|
722
|
+
const order = compare(actual, expected);
|
|
723
|
+
if (order === void 0) return false;
|
|
724
|
+
return expression.operator === "gt" ? order > 0 : expression.operator === "gte" ? order >= 0 : expression.operator === "lt" ? order < 0 : order <= 0;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
//#endregion
|
|
729
|
+
//#region src/runtime/execute.ts
|
|
730
|
+
const recordIndexes = /* @__PURE__ */ new WeakMap();
|
|
731
|
+
/** Narrows root ID-equality queries using one lazy index per immutable collection revision. */
|
|
732
|
+
function queryCandidates(records, plan) {
|
|
733
|
+
const equality = plan.filters.find((expression) => expression.kind === "predicate" && expression.field === "id" && expression.operator === "eq" && typeof expression.value === "string");
|
|
734
|
+
if (equality?.kind !== "predicate" || typeof equality.value !== "string") return records;
|
|
735
|
+
let index = recordIndexes.get(records);
|
|
736
|
+
if (!index) {
|
|
737
|
+
index = new Map(records.map((record) => [record.id, record]));
|
|
738
|
+
recordIndexes.set(records, index);
|
|
739
|
+
}
|
|
740
|
+
const record = index.get(equality.value);
|
|
741
|
+
return record ? [record] : [];
|
|
742
|
+
}
|
|
743
|
+
/** Classifies nullish and unordered special values for direction-independent placement. */
|
|
744
|
+
function sortGroup(value) {
|
|
745
|
+
return value == null ? 2 : typeof value === "number" && Number.isNaN(value) || value instanceof Date && Number.isNaN(value.getTime()) ? 1 : 0;
|
|
746
|
+
}
|
|
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] : [];
|
|
752
|
+
}
|
|
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) {
|
|
763
|
+
let category;
|
|
764
|
+
for (const record of filtered) {
|
|
765
|
+
const value = Object.hasOwn(record, sort.field) ? record[sort.field] : void 0;
|
|
766
|
+
if (!isScalar(value)) invalidQuery(`Field "${sort.field}" is not sortable.`);
|
|
767
|
+
if (value != null) {
|
|
768
|
+
const current = value instanceof Date ? "date" : typeof value;
|
|
769
|
+
if (category && category !== current) invalidQuery(`Field "${sort.field}" has mixed sort categories.`);
|
|
770
|
+
category = current;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
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
|
+
});
|
|
786
|
+
const offset = plan.offset ?? 0;
|
|
787
|
+
return filtered.slice(offset, plan.limit === void 0 ? void 0 : offset + plan.limit);
|
|
788
|
+
}
|
|
789
|
+
/** Builds a sibling index once per scope, before pagination and within the pinned revision. */
|
|
790
|
+
function neighborScope(plan, records, preserveOrder, scopes, revision) {
|
|
791
|
+
const selection = plan.adjacent;
|
|
792
|
+
const siblingPlan = selection === true ? {
|
|
793
|
+
...plan,
|
|
794
|
+
select: ["id"],
|
|
795
|
+
allFields: void 0,
|
|
796
|
+
exclude: void 0,
|
|
797
|
+
with: {},
|
|
798
|
+
adjacent: void 0,
|
|
799
|
+
payloads: void 0,
|
|
800
|
+
limit: void 0,
|
|
801
|
+
offset: void 0
|
|
802
|
+
} : selection;
|
|
803
|
+
const key = selection === true ? plan : siblingPlan;
|
|
804
|
+
let candidates = scopes.get(key);
|
|
805
|
+
if (!candidates) {
|
|
806
|
+
candidates = /* @__PURE__ */ new WeakMap();
|
|
807
|
+
scopes.set(key, candidates);
|
|
808
|
+
}
|
|
809
|
+
let scope = candidates.get(records);
|
|
810
|
+
if (!scope) {
|
|
811
|
+
const ordered = pageRecords([...new Map(records.map((entry) => [entry.id, entry])).values()], siblingPlan, revision, selection === true && preserveOrder);
|
|
812
|
+
scope = {
|
|
813
|
+
plan: siblingPlan,
|
|
814
|
+
records: ordered,
|
|
815
|
+
positions: new Map(ordered.map((entry, index) => [entry.id, index]))
|
|
816
|
+
};
|
|
817
|
+
candidates.set(records, scope);
|
|
818
|
+
}
|
|
819
|
+
return scope;
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Projects one entry and requested relationships/adjacency from one pinned revision.
|
|
823
|
+
* Requested payloads are recorded for the caller to load after projection.
|
|
824
|
+
*/
|
|
825
|
+
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);
|
|
829
|
+
for (const [name, nested] of Object.entries(plan.with)) {
|
|
830
|
+
const relation = revision.registry[plan.collection].relationships[name];
|
|
831
|
+
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.`);
|
|
832
|
+
const candidates = revision.relations.get(relationKey(plan.collection, name))?.get(record.id) ?? [];
|
|
833
|
+
const preserveOrder = relation.referenceSide === "from" && Array.isArray(record[relation.from]);
|
|
834
|
+
const selected = pageRecords(candidates, nested, revision, preserveOrder).map((entry) => project(entry, nested, revision, scopes, candidates, requests, preserveOrder));
|
|
835
|
+
Object.defineProperty(result, name, {
|
|
836
|
+
value: relation.type === "many" ? selected : selected[0] ?? null,
|
|
837
|
+
enumerable: true,
|
|
838
|
+
configurable: true,
|
|
839
|
+
writable: true
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
if (plan.adjacent) {
|
|
843
|
+
const candidates = plan.adjacent === true ? scopeRecords : revision.collections[plan.collection].records;
|
|
844
|
+
const scope = neighborScope(plan, candidates, preserveScopeOrder, scopes, revision);
|
|
845
|
+
const index = scope.positions.get(record.id);
|
|
846
|
+
const previous = index === void 0 ? void 0 : scope.records[index - 1];
|
|
847
|
+
const next = index === void 0 ? void 0 : scope.records[index + 1];
|
|
848
|
+
result._adjacent = {
|
|
849
|
+
previous: previous ? project(previous, scope.plan, revision, scopes, candidates, requests) : null,
|
|
850
|
+
next: next ? project(next, scope.plan, revision, scopes, candidates, requests) : null
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
if (plan.payloads) requests.push({
|
|
854
|
+
target: result,
|
|
855
|
+
collection: plan.collection,
|
|
856
|
+
id: record.id,
|
|
857
|
+
payloads: plan.payloads
|
|
858
|
+
});
|
|
859
|
+
return result;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Runs the shared JavaScript executor; future adapters can execute the same plan.
|
|
863
|
+
* Rows that requested payloads are appended to `requests` for loading after execution.
|
|
864
|
+
*/
|
|
865
|
+
async function executeQuery(revision, plan, terminal, requests = []) {
|
|
866
|
+
const collection = revision.collections[plan.collection];
|
|
867
|
+
if (!collection) invalidQuery(`Unknown collection "${plan.collection}".`);
|
|
868
|
+
if (terminal === "count") {
|
|
869
|
+
if (plan.payloads) invalidQuery("Counts cannot request payloads.");
|
|
870
|
+
return queryCandidates(collection.records, plan).filter((record) => plan.filters.every((expression) => matches(record, expression, revision, plan.collection))).length;
|
|
871
|
+
}
|
|
872
|
+
if (!plan.select && !plan.allFields && !plan.adjacent) invalidQuery("Row-returning queries require select().");
|
|
873
|
+
const records = pageRecords(queryCandidates(collection.records, plan), plan, revision);
|
|
874
|
+
const scopes = /* @__PURE__ */ new WeakMap();
|
|
875
|
+
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
|
+
}
|
|
877
|
+
//#endregion
|
|
878
|
+
//#region src/runtime/content.ts
|
|
879
|
+
assertServerOnly();
|
|
880
|
+
/** Maximum payload loads in flight for one query. */
|
|
881
|
+
const PAYLOAD_CONCURRENCY = 16;
|
|
882
|
+
/** Pins the latest valid revision for an entire asynchronous operation. */
|
|
883
|
+
async function pinned(state, operation) {
|
|
884
|
+
if (state.error) throw state.error;
|
|
885
|
+
const revision = state.revision;
|
|
886
|
+
if (!revision) throw new ContentError({
|
|
887
|
+
code: "artifact-error",
|
|
888
|
+
message: "No prepared content revision is available."
|
|
889
|
+
});
|
|
890
|
+
revision.active++;
|
|
891
|
+
try {
|
|
892
|
+
return await operation(revision);
|
|
893
|
+
} finally {
|
|
894
|
+
revision.active--;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
/** Checks a decoded built-in payload before any of its values reach a query result. */
|
|
898
|
+
function validBuiltinPayload(payload) {
|
|
899
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
900
|
+
const { body, markdown, rendered } = payload;
|
|
901
|
+
return (body === void 0 || typeof body === "string") && (markdown === void 0 || typeof markdown === "string") && (rendered === void 0 || !!rendered && typeof rendered.html === "string" && Array.isArray(rendered.headings) && rendered.headings.every((heading) => !!heading && Number.isInteger(heading.depth) && heading.depth >= 1 && heading.depth <= 6 && typeof heading.slug === "string" && typeof heading.text === "string"));
|
|
902
|
+
}
|
|
903
|
+
/** Lazily loads one entry's built-in payload once per revision, retrying after failures. */
|
|
904
|
+
function loadBuiltin(revision, collection, id) {
|
|
905
|
+
const key = JSON.stringify([collection, id]);
|
|
906
|
+
let loaded = revision.payloadCache.get(key);
|
|
907
|
+
if (!loaded) {
|
|
908
|
+
loaded = Promise.resolve().then(async () => {
|
|
909
|
+
const payload = await revision.collections[collection].builtins.get(id)?.() ?? {};
|
|
910
|
+
if (!validBuiltinPayload(payload)) throw new Error("Invalid prepared payload shape.");
|
|
911
|
+
return payload;
|
|
912
|
+
}).catch((cause) => {
|
|
913
|
+
revision.payloadCache.delete(key);
|
|
914
|
+
throw contentError(cause, {
|
|
915
|
+
code: "artifact-error",
|
|
916
|
+
collection,
|
|
917
|
+
id,
|
|
918
|
+
message: "Could not load the prepared entry payload."
|
|
919
|
+
});
|
|
920
|
+
});
|
|
921
|
+
revision.payloadCache.set(key, loaded);
|
|
922
|
+
}
|
|
923
|
+
return loaded;
|
|
924
|
+
}
|
|
925
|
+
/** Returns supplied or eagerly compiled output, or renders captured Markdown once per revision. */
|
|
926
|
+
async function renderedPayload(revision, collection, id) {
|
|
927
|
+
const payload = await loadBuiltin(revision, collection, id);
|
|
928
|
+
if (payload.rendered) return payload.rendered;
|
|
929
|
+
if (payload.markdown === void 0) throw new ContentError({
|
|
930
|
+
code: "not-renderable",
|
|
931
|
+
collection,
|
|
932
|
+
id,
|
|
933
|
+
message: "This entry has no supplied output or supported rendering path."
|
|
934
|
+
});
|
|
935
|
+
const key = JSON.stringify([collection, id]);
|
|
936
|
+
let rendered = revision.renderCache.get(key);
|
|
937
|
+
if (!rendered) {
|
|
938
|
+
rendered = Promise.resolve().then(() => {
|
|
939
|
+
if (!revision.renderMarkdown) throw new Error("Missing packaged Markdown renderer.");
|
|
940
|
+
return revision.renderMarkdown(payload.markdown);
|
|
941
|
+
}).catch((cause) => {
|
|
942
|
+
revision.renderCache.delete(key);
|
|
943
|
+
throw contentError(cause, {
|
|
944
|
+
code: "render-error",
|
|
945
|
+
collection,
|
|
946
|
+
id,
|
|
947
|
+
message: "Markdown rendering failed."
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
revision.renderCache.set(key, rendered);
|
|
951
|
+
}
|
|
952
|
+
return rendered;
|
|
953
|
+
}
|
|
954
|
+
/** Loads one requested payload value from the pinned revision. */
|
|
955
|
+
async function loadPayload(revision, collection, id, name) {
|
|
956
|
+
if (name === "_body") return (await loadBuiltin(revision, collection, id)).body;
|
|
957
|
+
if (name === "_rendered") return renderedPayload(revision, collection, id);
|
|
958
|
+
invalidQuery(`Payload "${name}" no longer matches the prepared collection.`);
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Loads every requested payload with bounded concurrency, then attaches detached copies under
|
|
962
|
+
* `_payloads`. Each distinct payload loads once; rows that share an entry get independent values.
|
|
963
|
+
*/
|
|
964
|
+
async function attachPayloads(revision, requests) {
|
|
965
|
+
if (!requests.length) return;
|
|
966
|
+
const loads = /* @__PURE__ */ new Map();
|
|
967
|
+
for (const request of requests) for (const name of Object.keys(request.payloads)) {
|
|
968
|
+
const key = JSON.stringify([
|
|
969
|
+
request.collection,
|
|
970
|
+
request.id,
|
|
971
|
+
name,
|
|
972
|
+
request.payloads[name]
|
|
973
|
+
]);
|
|
974
|
+
if (!loads.has(key)) loads.set(key, {
|
|
975
|
+
collection: request.collection,
|
|
976
|
+
id: request.id,
|
|
977
|
+
name
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
const keys = [...loads.keys()];
|
|
981
|
+
const values = await mapConcurrent(keys, PAYLOAD_CONCURRENCY, (key) => {
|
|
982
|
+
const { collection, id, name } = loads.get(key);
|
|
983
|
+
return loadPayload(revision, collection, id, name);
|
|
984
|
+
});
|
|
985
|
+
const loaded = new Map(keys.map((key, index) => [key, values[index]]));
|
|
986
|
+
for (const request of requests) request.target._payloads = Object.fromEntries(Object.keys(request.payloads).map((name) => [name, detach(loaded.get(JSON.stringify([
|
|
987
|
+
request.collection,
|
|
988
|
+
request.id,
|
|
989
|
+
name,
|
|
990
|
+
request.payloads[name]
|
|
991
|
+
])))]));
|
|
992
|
+
}
|
|
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) => {
|
|
997
|
+
const normalized = normalizePlan(plan);
|
|
998
|
+
const requests = [];
|
|
999
|
+
const result = revision.execute ? await revision.execute(normalized, terminal, requests) : await executeQuery(revision, normalized, terminal, requests);
|
|
1000
|
+
await attachPayloads(revision, requests);
|
|
1001
|
+
return result;
|
|
1002
|
+
});
|
|
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);
|
|
1008
|
+
},
|
|
1009
|
+
query: createQueryNamespace(registry, run),
|
|
1010
|
+
count(collection, options = {}) {
|
|
1011
|
+
return run(relationalPlan(registry(), collection, options), "count");
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
/** Keeps dev builders live across host-delivered revisions while pinning active operations. */
|
|
1016
|
+
function createLiveRuntime(initial) {
|
|
1017
|
+
const state = { ...initial };
|
|
1018
|
+
return {
|
|
1019
|
+
content: createRuntime(state),
|
|
1020
|
+
update(next) {
|
|
1021
|
+
state.revision = next.revision;
|
|
1022
|
+
state.registry = next.registry;
|
|
1023
|
+
state.error = next.error;
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
//#endregion
|
|
1028
|
+
export { CODEC_VERSION as a, prepareRelationships as i, createRuntime as n, isRelationship as r, createLiveRuntime as t };
|