content-query 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -15
- package/dist/{build-CI4s0oFP.mjs → build-DWpB69_c.mjs} +826 -69
- package/dist/{build-jKnjDjQE.d.mts → build-DfoncKL8.d.mts} +17 -5
- package/dist/build.d.mts +1 -1
- package/dist/build.mjs +1 -1
- package/dist/cli.mjs +14 -8
- package/dist/{content-N1gYshjg.mjs → collection-Cb5eWGnv.mjs} +998 -344
- package/dist/collection-types-CsQtGw7_.d.mts +119 -0
- package/dist/computed.d.mts +2 -2
- package/dist/{types-FmUJ3cH1.d.mts → descriptor-bcPa6XLk.d.mts} +17 -1
- package/dist/{diagnostics-Cu94mZhl.mjs → diagnostics-DmUZtrJ6.mjs} +3 -1
- package/dist/{errors-C2iuBM0G.mjs → errors-Bfd0R4_P.mjs} +16 -1
- package/dist/file-DSOcdFj0.mjs +81 -0
- package/dist/{glob-Bv0dpvCQ.mjs → glob-BXJuwy_p.mjs} +94 -6
- package/dist/index.d.mts +76 -18
- package/dist/index.mjs +1 -1
- package/dist/internal.d.mts +54 -7
- package/dist/internal.mjs +424 -19
- package/dist/loaders.d.mts +1 -2
- package/dist/loaders.mjs +2 -81
- package/dist/next.d.mts +3 -2
- package/dist/next.mjs +67 -14
- package/dist/operators-D-w1-gDg.mjs +391 -0
- package/dist/query-style-C2FcT1f9.d.mts +23 -0
- package/dist/query.d.mts +25 -2
- package/dist/query.mjs +1 -1
- package/dist/{relational-types-B45zM34W.d.mts → relational-types-CTcalUNW.d.mts} +241 -50
- package/dist/revision-DBwU-aC9.d.mts +56 -0
- package/dist/runtime.d.mts +3 -2
- package/dist/runtime.mjs +1 -1
- package/dist/types-Dz1vTXa2.d.mts +214 -0
- package/dist/vite.d.mts +1 -1
- package/dist/vite.mjs +77 -35
- package/package.json +1 -1
- package/dist/content-lwPxE9Yu.d.mts +0 -109
- package/dist/descriptor-BUPT2J7X.d.mts +0 -18
- package/dist/operators-UqWUhKQJ.mjs +0 -173
- package/dist/types-BC8gVJ6u.d.mts +0 -69
- package/dist/validation-D_XdE1pF.mjs +0 -84
package/dist/next.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { I as collectionNames } from "./operators-D-w1-gDg.mjs";
|
|
2
|
+
import { c as collectionDeclarations, i as resolveConfigPath, t as buildAtRoot } from "./build-DWpB69_c.mjs";
|
|
3
|
+
import { n as serializeContentError } from "./diagnostics-DmUZtrJ6.mjs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { randomUUID } from "node:crypto";
|
|
5
|
-
import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
|
|
6
7
|
import { PHASE_DEVELOPMENT_SERVER, PHASE_PRODUCTION_BUILD } from "next/constants.js";
|
|
7
8
|
//#region src/integrations/next/publication.ts
|
|
9
|
+
const developmentEntry = /^next-dev-[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}\.js$/;
|
|
8
10
|
/** Switches a development entry synchronously so error delivery cannot race a later publication. */
|
|
9
11
|
function writeEntry(filename, source) {
|
|
10
12
|
try {
|
|
@@ -19,25 +21,75 @@ function writeEntry(filename, source) {
|
|
|
19
21
|
rmSync(temporary, { force: true });
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
|
-
/**
|
|
24
|
+
/** Lists one directory's entries, treating a missing directory as empty. */
|
|
25
|
+
function entriesOf(directory) {
|
|
26
|
+
try {
|
|
27
|
+
return readdirSync(directory);
|
|
28
|
+
} catch (cause) {
|
|
29
|
+
const code = cause && typeof cause === "object" && "code" in cause ? cause.code : void 0;
|
|
30
|
+
if (code === "ENOENT" || code === "ENOTDIR") return [];
|
|
31
|
+
throw cause;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Lists the directories of collection entries that exist from an earlier publication. */
|
|
35
|
+
function publishedCollections(outDir) {
|
|
36
|
+
const collections = path.join(outDir, "collections");
|
|
37
|
+
return entriesOf(collections).map((name) => path.join(collections, name)).filter((directory) => existsSync(path.join(directory, "server.js")));
|
|
38
|
+
}
|
|
39
|
+
/** Points one entry at a fresh copy of its runtime, so Next reloads it even for identical bytes. */
|
|
40
|
+
function redirectEntry(directory, exportName, filename) {
|
|
41
|
+
const entry = path.join(directory, "server.js");
|
|
42
|
+
const runtime = path.join(directory, filename);
|
|
43
|
+
writeEntry(runtime, readFileSync(entry, "utf8"));
|
|
44
|
+
writeEntry(entry, `export { ${exportName} } from ${JSON.stringify(`./${filename}`)};\n`);
|
|
45
|
+
return runtime;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Publishes fresh runtime imports and optionally prunes previous runs after a successful build.
|
|
49
|
+
*
|
|
50
|
+
* Each collection entry, `collections/<export name>/server.js`, is pointed at a fresh copy of its
|
|
51
|
+
* runtime. A preparation error replaces every entry, so each route that imports one reports it.
|
|
52
|
+
*
|
|
53
|
+
* @param outDir - Generated output directory.
|
|
54
|
+
* @param config - Absolute path of content-query.config.
|
|
55
|
+
* @param state - The session's current revision or error.
|
|
56
|
+
* @param options - `prune` removes development entries from previous runs.
|
|
57
|
+
* @returns The new runtime modules, or `undefined` when nothing valid was published.
|
|
58
|
+
*/
|
|
23
59
|
function publishDevelopment(outDir, config, state, { prune = false } = {}) {
|
|
24
60
|
if (!state.error) {
|
|
25
61
|
if (!state.revision) return;
|
|
26
|
-
const entry = path.join(outDir, "server.js");
|
|
27
62
|
const filename = `next-dev-${randomUUID()}.js`;
|
|
28
|
-
const
|
|
29
|
-
writeEntry(runtime, readFileSync(entry, "utf8"));
|
|
30
|
-
writeEntry(entry, `export { content } from ${JSON.stringify(`./${filename}`)};\n`);
|
|
63
|
+
const runtimes = [...collectionNames(state.revision.registry).values()].map(({ exportName }) => redirectEntry(path.join(outDir, "collections", exportName), exportName, filename));
|
|
31
64
|
if (prune) pruneDevelopmentEntries(outDir, filename);
|
|
32
|
-
return
|
|
65
|
+
return runtimes;
|
|
66
|
+
}
|
|
67
|
+
const failure = `import { restoreContentError } from "content-query/internal";\nthrow restoreContentError(${JSON.stringify(serializeContentError(state.error))});\n`;
|
|
68
|
+
const directories = new Map(publishedCollections(outDir).map((directory) => [directory, path.basename(directory)]));
|
|
69
|
+
for (const [name, generated] of collectionNames(state.registry ?? {})) {
|
|
70
|
+
const directory = path.join(outDir, "collections", generated.exportName);
|
|
71
|
+
directories.set(directory, generated.exportName);
|
|
72
|
+
writeEntry(path.join(directory, "server.d.ts"), collectionDeclarations(directory, config, name, generated));
|
|
33
73
|
}
|
|
34
74
|
writeEntry(path.join(outDir, "package.json"), "{\"type\":\"module\",\"private\":true}\n");
|
|
35
|
-
writeEntry(path.join(
|
|
36
|
-
writeEntry(path.join(outDir, "server.js"), `import { restoreContentError } from "content-query/internal";\nthrow restoreContentError(${JSON.stringify(serializeContentError(state.error))});\nexport const content = undefined;\n`);
|
|
75
|
+
for (const [directory, exportName] of directories) writeEntry(path.join(directory, "server.js"), `${failure}export const ${exportName} = undefined;\n`);
|
|
37
76
|
}
|
|
38
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Removes owned runtime entries from previous runs, preserving the current entries' target.
|
|
79
|
+
*
|
|
80
|
+
* @param outDir - Generated output directory.
|
|
81
|
+
* @param keep - The current publication's runtime filename, shared by every entry.
|
|
82
|
+
*/
|
|
39
83
|
function pruneDevelopmentEntries(outDir, keep) {
|
|
40
|
-
|
|
84
|
+
const collections = path.join(outDir, "collections");
|
|
85
|
+
const directories = [outDir, ...entriesOf(collections).map((name) => path.join(collections, name))];
|
|
86
|
+
for (const directory of directories) {
|
|
87
|
+
const entries = entriesOf(directory);
|
|
88
|
+
for (const filename of entries) if (filename !== keep && developmentEntry.test(filename)) rmSync(path.join(directory, filename), { force: true });
|
|
89
|
+
if (directory !== outDir && entries.every((filename) => developmentEntry.test(filename))) try {
|
|
90
|
+
rmdirSync(directory);
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
41
93
|
}
|
|
42
94
|
//#endregion
|
|
43
95
|
//#region src/integrations/next/plugin.ts
|
|
@@ -114,7 +166,8 @@ async function startDevelopment(root, options) {
|
|
|
114
166
|
}
|
|
115
167
|
/**
|
|
116
168
|
* Builds content before Next compiles, retaining one managed watcher during `next dev`.
|
|
117
|
-
* Import
|
|
169
|
+
* Import each collection's generated entry, such as `#content-query/posts`, from Server
|
|
170
|
+
* Components; Next's HMR observes their updates, and each route traces only what it imports.
|
|
118
171
|
* Next's detached telemetry helper evaluates configuration without starting content sessions.
|
|
119
172
|
* @param nextConfig Existing object, promise, or phase-aware Next.js configuration.
|
|
120
173
|
* @param options Optional content configuration file and generated output directory.
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { a as invalidQuery } from "./errors-Bfd0R4_P.mjs";
|
|
2
|
+
import { c as isScalar, i as detach } from "./values-BbTV4QT-.mjs";
|
|
3
|
+
//#region src/collection/names.ts
|
|
4
|
+
/** Words that cannot name a `const` binding in an ES module, or that read as module syntax. */
|
|
5
|
+
const reservedWords = new Set("arguments await break case catch class const continue debugger default delete do else enum eval export extends false finally for function if implements import in instanceof interface let new null package private protected public return static super switch this throw true try typeof var void while with yield".split(" "));
|
|
6
|
+
/** Names Windows reserves for devices, in any letter case, which cannot name a directory. */
|
|
7
|
+
const deviceName = /^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])$/i;
|
|
8
|
+
const identifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
9
|
+
/** Uppercases the first character, keeping the rest of a camelCase name. */
|
|
10
|
+
function capitalize(value) {
|
|
11
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Converts a collection name into the identifier its entry exports and its import path uses.
|
|
15
|
+
* Characters that cannot appear in an identifier separate camelCase words, a leading digit gets
|
|
16
|
+
* an `_` prefix, and reserved words and Windows device names get a `Collection` suffix.
|
|
17
|
+
*
|
|
18
|
+
* @param name - The collection's key in the `collections` registry.
|
|
19
|
+
* @returns The export name, such as `blogPosts` for `blog-posts`.
|
|
20
|
+
*/
|
|
21
|
+
function exportNameOf(name) {
|
|
22
|
+
let converted = name.split(/[^A-Za-z0-9_$]+/).filter(Boolean).map((word, index) => index === 0 ? word : capitalize(word)).join("");
|
|
23
|
+
if (!converted) converted = "collection";
|
|
24
|
+
else if (/^[0-9]/.test(converted)) converted = `_${converted}`;
|
|
25
|
+
return reservedWords.has(converted) || deviceName.test(converted) ? `${converted}Collection` : converted;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Derives the singular word for generated type names by dropping one trailing `s`. Names ending
|
|
29
|
+
* in `ss`, such as `address`, keep it. Irregular plurals use the collection's `typeName`.
|
|
30
|
+
*
|
|
31
|
+
* @param exportName - The collection's export name.
|
|
32
|
+
* @returns The singular form, such as `blogPost` for `blogPosts`.
|
|
33
|
+
*/
|
|
34
|
+
function singularOf(exportName) {
|
|
35
|
+
return exportName.length > 1 && exportName.endsWith("s") && !exportName.endsWith("ss") ? exportName.slice(0, -1) : exportName;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Computes every collection's generated names and rejects names that would collide.
|
|
39
|
+
*
|
|
40
|
+
* @param registry - Collection keys with each collection's optional `typeName` override.
|
|
41
|
+
* @returns The names of each collection's entry, keyed by collection name.
|
|
42
|
+
* @throws An `Error` naming the conflict when two collections share an export or type name, or
|
|
43
|
+
* when a `typeName` is not an identifier.
|
|
44
|
+
*/
|
|
45
|
+
function collectionNames(registry) {
|
|
46
|
+
const names = /* @__PURE__ */ new Map();
|
|
47
|
+
const exports = /* @__PURE__ */ new Map();
|
|
48
|
+
const types = /* @__PURE__ */ new Map();
|
|
49
|
+
for (const [name, definition] of Object.entries(registry)) {
|
|
50
|
+
const declared = definition?.typeName;
|
|
51
|
+
if (declared !== void 0 && (typeof declared !== "string" || !identifier.test(declared))) throw new Error(`Collection "${name}" typeName must be an identifier such as "Category"; received ${JSON.stringify(declared)}.`);
|
|
52
|
+
const exportName = exportNameOf(name);
|
|
53
|
+
const typeName = `Content${capitalize(declared ?? singularOf(exportName))}`;
|
|
54
|
+
const exported = exports.get(exportName.toLowerCase());
|
|
55
|
+
if (exported !== void 0) throw new Error(`Collections "${exported}" and "${name}" both export "${exportName}" (ignoring letter case), which their #content-query entries cannot share. Rename one of them.`);
|
|
56
|
+
const payloadTypeName = `${typeName}Payload`;
|
|
57
|
+
for (const generated of [typeName, payloadTypeName]) {
|
|
58
|
+
const typed = types.get(generated);
|
|
59
|
+
if (typed !== void 0) throw new Error(`Collections "${typed}" and "${name}" both generate the type "${generated}". Set typeName on one of them.`);
|
|
60
|
+
types.set(generated, name);
|
|
61
|
+
}
|
|
62
|
+
exports.set(exportName.toLowerCase(), name);
|
|
63
|
+
names.set(name, {
|
|
64
|
+
exportName,
|
|
65
|
+
typeName,
|
|
66
|
+
payloadTypeName
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return names;
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/query/plan.ts
|
|
73
|
+
/**
|
|
74
|
+
* Rejects a declared relationship whose target collection this query scope has not loaded,
|
|
75
|
+
* naming the collection to pass to `using()`.
|
|
76
|
+
*
|
|
77
|
+
* @param registry - The query scope's registry.
|
|
78
|
+
* @param collection - The collection that declares the relationship.
|
|
79
|
+
* @param name - The relationship, field, or filter name a query used.
|
|
80
|
+
*/
|
|
81
|
+
function assertAvailable(registry, collection, name) {
|
|
82
|
+
const unavailable = registry[collection]?.unavailable;
|
|
83
|
+
if (!unavailable || !Object.hasOwn(unavailable, name)) return;
|
|
84
|
+
const target = exportNameOf(unavailable[name]);
|
|
85
|
+
invalidQuery(`Relationship "${name}" targets the ${target} collection, which this query has not loaded. Pass it to using(), such as ${exportNameOf(collection)}.using(${target}).`);
|
|
86
|
+
}
|
|
87
|
+
/** Starts a fresh query without binding a content revision until execution. */
|
|
88
|
+
function emptyPlan(collection) {
|
|
89
|
+
return {
|
|
90
|
+
collection,
|
|
91
|
+
filters: [],
|
|
92
|
+
order: [],
|
|
93
|
+
with: {}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Explains the explicit-selection rule wherever a scope returns rows without selecting data. */
|
|
97
|
+
const NO_SELECTION_MESSAGE = "Select fields, relationships, payloads, or adjacent entries before running this query.";
|
|
98
|
+
/** Explains why `paginate()` is unavailable once a query sets its own limit or offset. */
|
|
99
|
+
const PAGINATE_AFTER_LIMIT_MESSAGE = "paginate() sets the limit and offset itself; remove limit and offset from this query.";
|
|
100
|
+
/**
|
|
101
|
+
* Returns the order a scope's entries follow: explicit clauses, then ascending ID as the final
|
|
102
|
+
* tie-breaker unless ID is already sorted. Scopes without clauses sort by ascending ID, except
|
|
103
|
+
* relationship scopes that preserve an authored ID array's order.
|
|
104
|
+
*
|
|
105
|
+
* @param plan - The scope whose order to resolve.
|
|
106
|
+
* @param preserveOrder - Keeps authored order when the scope has no explicit clauses.
|
|
107
|
+
* @returns The sort clauses to apply, in priority order.
|
|
108
|
+
*/
|
|
109
|
+
function effectiveOrder(plan, preserveOrder = false) {
|
|
110
|
+
if (!plan.order.length) return preserveOrder ? [] : [{
|
|
111
|
+
field: "id",
|
|
112
|
+
direction: "asc"
|
|
113
|
+
}];
|
|
114
|
+
const order = [...plan.order];
|
|
115
|
+
if (!order.some((sort) => sort.field === "id")) order.push({
|
|
116
|
+
field: "id",
|
|
117
|
+
direction: "asc"
|
|
118
|
+
});
|
|
119
|
+
return order;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Checks whether a row-returning scope explicitly requests data. Nothing is implicit, not even
|
|
123
|
+
* `id`: a scope must select fields, include a relationship, request a payload, or select adjacency.
|
|
124
|
+
*
|
|
125
|
+
* @param plan - The scope to check; `undefined` when a callback returned no builder.
|
|
126
|
+
* @returns `true` when the scope requests at least one kind of data.
|
|
127
|
+
*/
|
|
128
|
+
function hasSelection(plan) {
|
|
129
|
+
return !!plan && (!!plan.select?.length || !!plan.adjacent || Object.keys(plan.with).length > 0 || Object.keys(plan.payloads ?? {}).length > 0);
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/query/operators.ts
|
|
133
|
+
/** Builds a checked, backend-independent predicate instead of a per-entry callback. */
|
|
134
|
+
function predicate(operator, field, value) {
|
|
135
|
+
if (!field || typeof field.name !== "string" || !field.name) invalidQuery("A filter must reference a query field.");
|
|
136
|
+
if ([
|
|
137
|
+
"in-array",
|
|
138
|
+
"not-in-array",
|
|
139
|
+
"array-contains-any",
|
|
140
|
+
"array-contains-all",
|
|
141
|
+
"array-contained"
|
|
142
|
+
].includes(operator)) {
|
|
143
|
+
if (!Array.isArray(value) || !value.every(isScalar)) invalidQuery("Choice arguments must be arrays of scalar values.");
|
|
144
|
+
} else if ([
|
|
145
|
+
"contains",
|
|
146
|
+
"starts-with",
|
|
147
|
+
"ends-with",
|
|
148
|
+
"like",
|
|
149
|
+
"ilike"
|
|
150
|
+
].includes(operator)) {
|
|
151
|
+
if (typeof value !== "string") invalidQuery("String predicates require a string argument.");
|
|
152
|
+
} else if (!operator.startsWith("is-") && !isScalar(value)) invalidQuery("This predicate requires a scalar argument.");
|
|
153
|
+
if ([
|
|
154
|
+
"gt",
|
|
155
|
+
"gte",
|
|
156
|
+
"lt",
|
|
157
|
+
"lte"
|
|
158
|
+
].includes(operator) && (value == null || typeof value === "boolean")) invalidQuery("Range predicates require a string, number, bigint, or date.");
|
|
159
|
+
return {
|
|
160
|
+
kind: "predicate",
|
|
161
|
+
operator,
|
|
162
|
+
field: field.name,
|
|
163
|
+
value: detach(value)
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** Matches a scalar using strict value/date equality. */
|
|
167
|
+
function eq(field, value) {
|
|
168
|
+
return predicate("eq", field, value);
|
|
169
|
+
}
|
|
170
|
+
/** Negates scalar equality. */
|
|
171
|
+
function ne(field, value) {
|
|
172
|
+
return predicate("ne", field, value);
|
|
173
|
+
}
|
|
174
|
+
/** Matches values greater than the supplied scalar. */
|
|
175
|
+
function gt(field, value) {
|
|
176
|
+
return predicate("gt", field, value);
|
|
177
|
+
}
|
|
178
|
+
/** Matches values greater than or equal to the supplied scalar. */
|
|
179
|
+
function gte(field, value) {
|
|
180
|
+
return predicate("gte", field, value);
|
|
181
|
+
}
|
|
182
|
+
/** Matches values less than the supplied scalar. */
|
|
183
|
+
function lt(field, value) {
|
|
184
|
+
return predicate("lt", field, value);
|
|
185
|
+
}
|
|
186
|
+
/** Matches values less than or equal to the supplied scalar. */
|
|
187
|
+
function lte(field, value) {
|
|
188
|
+
return predicate("lte", field, value);
|
|
189
|
+
}
|
|
190
|
+
/** Matches one of a list of scalar choices. */
|
|
191
|
+
function inArray(field, values) {
|
|
192
|
+
return predicate("in-array", field, values);
|
|
193
|
+
}
|
|
194
|
+
/** Matches none of a list of scalar choices. */
|
|
195
|
+
function notInArray(field, values) {
|
|
196
|
+
return predicate("not-in-array", field, values);
|
|
197
|
+
}
|
|
198
|
+
/** Matches a literal substring, without SQL wildcard interpretation. */
|
|
199
|
+
function contains(field, value) {
|
|
200
|
+
return predicate("contains", field, value);
|
|
201
|
+
}
|
|
202
|
+
/** Matches a literal string prefix. */
|
|
203
|
+
function startsWith(field, value) {
|
|
204
|
+
return predicate("starts-with", field, value);
|
|
205
|
+
}
|
|
206
|
+
/** Matches a literal string suffix. */
|
|
207
|
+
function endsWith(field, value) {
|
|
208
|
+
return predicate("ends-with", field, value);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Matches a SQL-style pattern: `%` matches any run of characters and `_` matches one character.
|
|
212
|
+
* A backslash escapes a wildcard. Matching is case-sensitive.
|
|
213
|
+
*/
|
|
214
|
+
function like(field, pattern) {
|
|
215
|
+
return predicate("like", field, pattern);
|
|
216
|
+
}
|
|
217
|
+
/** Matches a SQL-style pattern like {@link like}, ignoring case. */
|
|
218
|
+
function ilike(field, pattern) {
|
|
219
|
+
return predicate("ilike", field, pattern);
|
|
220
|
+
}
|
|
221
|
+
/** Negates {@link like}; entries without a matching string, including absent values, match. */
|
|
222
|
+
function notLike(field, pattern) {
|
|
223
|
+
return not(like(field, pattern));
|
|
224
|
+
}
|
|
225
|
+
/** Negates {@link ilike}; entries without a matching string, including absent values, match. */
|
|
226
|
+
function notIlike(field, pattern) {
|
|
227
|
+
return not(ilike(field, pattern));
|
|
228
|
+
}
|
|
229
|
+
/** Tests whether every item of an existing scalar array belongs to the candidates. */
|
|
230
|
+
function arrayContained(field, values) {
|
|
231
|
+
return predicate("array-contained", field, values);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Matches entries with at least one related entry. With a filter, a related entry must also match
|
|
235
|
+
* it, for example `has(fields.author, (author) => eq(author.name, "Ada"))`. Wrap the result in
|
|
236
|
+
* {@link not} to match entries without such a related entry.
|
|
237
|
+
*
|
|
238
|
+
* @param relation - A relationship from the `where()` callback's fields.
|
|
239
|
+
* @param filter - Optional filter over the related entry's fields and relationships.
|
|
240
|
+
* @returns An expression that the query resolves against the relationship's target collection.
|
|
241
|
+
*/
|
|
242
|
+
function has(relation, filter) {
|
|
243
|
+
if (!relation || typeof relation.name !== "string" || !relation.name) invalidQuery("has() requires a relationship from the where() fields.");
|
|
244
|
+
if (filter !== void 0 && typeof filter !== "function") invalidQuery("has() filters must be callbacks over the related entry's fields.");
|
|
245
|
+
const expression = filter === void 0 ? and() : filter(fieldProxy());
|
|
246
|
+
validateExpression(expression);
|
|
247
|
+
return {
|
|
248
|
+
kind: "has",
|
|
249
|
+
field: relation.name,
|
|
250
|
+
expression
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/** Creates the field and relationship accessors passed to filter callbacks. */
|
|
254
|
+
function fieldProxy() {
|
|
255
|
+
return new Proxy(Object.create(null), { get: (_target, name) => ({ name }) });
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Resolves fluent `has()` filters against a scope's declared relationships, producing portable
|
|
259
|
+
* `relation` expressions that both runtime backends evaluate.
|
|
260
|
+
*
|
|
261
|
+
* @param registry - The query registry with each collection's relationships.
|
|
262
|
+
* @param collection - The collection the expression filters.
|
|
263
|
+
* @param expression - A validated expression that may contain `has()` filters.
|
|
264
|
+
* @returns The expression with every `has()` filter resolved.
|
|
265
|
+
*/
|
|
266
|
+
function resolveRelations(registry, collection, expression) {
|
|
267
|
+
switch (expression.kind) {
|
|
268
|
+
case "has": {
|
|
269
|
+
const relationships = registry[collection]?.relationships ?? {};
|
|
270
|
+
const relation = Object.hasOwn(relationships, expression.field) ? relationships[expression.field] : void 0;
|
|
271
|
+
if (!relation) {
|
|
272
|
+
assertAvailable(registry, collection, expression.field);
|
|
273
|
+
invalidQuery(`Unknown relationship "${expression.field}" in has().`);
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
kind: "relation",
|
|
277
|
+
collection,
|
|
278
|
+
field: expression.field,
|
|
279
|
+
target: relation.collection,
|
|
280
|
+
expression: resolveRelations(registry, relation.collection, expression.expression)
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
case "relation": return {
|
|
284
|
+
...expression,
|
|
285
|
+
expression: resolveRelations(registry, expression.target, expression.expression)
|
|
286
|
+
};
|
|
287
|
+
case "and":
|
|
288
|
+
case "or":
|
|
289
|
+
case "not": return {
|
|
290
|
+
...expression,
|
|
291
|
+
expressions: expression.expressions.map((child) => resolveRelations(registry, collection, child))
|
|
292
|
+
};
|
|
293
|
+
default: return expression;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/** Tests scalar membership in an array, ignoring holes. */
|
|
297
|
+
function arrayContains(field, value) {
|
|
298
|
+
return predicate("array-contains", field, value);
|
|
299
|
+
}
|
|
300
|
+
/** Tests whether any candidate belongs to an existing scalar array. */
|
|
301
|
+
function arrayContainsAny(field, values) {
|
|
302
|
+
return predicate("array-contains-any", field, values);
|
|
303
|
+
}
|
|
304
|
+
/** Tests whether every candidate belongs to an existing scalar array. */
|
|
305
|
+
function arrayContainsAll(field, values) {
|
|
306
|
+
return predicate("array-contains-all", field, values);
|
|
307
|
+
}
|
|
308
|
+
/** Checks explicit null independently from undefined and absence. */
|
|
309
|
+
function isNull(field) {
|
|
310
|
+
return predicate("is-null", field);
|
|
311
|
+
}
|
|
312
|
+
/** Checks undefined, including a missing property. */
|
|
313
|
+
function isUndefined(field) {
|
|
314
|
+
return predicate("is-undefined", field);
|
|
315
|
+
}
|
|
316
|
+
/** Checks own-property absence before projection. */
|
|
317
|
+
function isMissing(field) {
|
|
318
|
+
return predicate("is-missing", field);
|
|
319
|
+
}
|
|
320
|
+
/** Requires all child predicates; an empty conjunction is true. */
|
|
321
|
+
function and(...expressions) {
|
|
322
|
+
expressions.forEach(validateExpression);
|
|
323
|
+
return {
|
|
324
|
+
kind: "and",
|
|
325
|
+
expressions
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
/** Requires any child predicate; an empty disjunction is false. */
|
|
329
|
+
function or(...expressions) {
|
|
330
|
+
expressions.forEach(validateExpression);
|
|
331
|
+
return {
|
|
332
|
+
kind: "or",
|
|
333
|
+
expressions
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
/** Negates a predicate's boolean result. */
|
|
337
|
+
function not(expression) {
|
|
338
|
+
validateExpression(expression);
|
|
339
|
+
return {
|
|
340
|
+
kind: "not",
|
|
341
|
+
expressions: [expression]
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/** Checks the shape of expression trees even when TypeScript was bypassed. */
|
|
345
|
+
function validateExpression(expression) {
|
|
346
|
+
if (!expression || typeof expression !== "object") invalidQuery("where() must return a structured expression.");
|
|
347
|
+
if (expression.kind === "predicate") {
|
|
348
|
+
if (![
|
|
349
|
+
"eq",
|
|
350
|
+
"ne",
|
|
351
|
+
"gt",
|
|
352
|
+
"gte",
|
|
353
|
+
"lt",
|
|
354
|
+
"lte",
|
|
355
|
+
"in-array",
|
|
356
|
+
"not-in-array",
|
|
357
|
+
"contains",
|
|
358
|
+
"starts-with",
|
|
359
|
+
"ends-with",
|
|
360
|
+
"like",
|
|
361
|
+
"ilike",
|
|
362
|
+
"array-contains",
|
|
363
|
+
"array-contains-any",
|
|
364
|
+
"array-contains-all",
|
|
365
|
+
"array-contained",
|
|
366
|
+
"is-null",
|
|
367
|
+
"is-undefined",
|
|
368
|
+
"is-missing"
|
|
369
|
+
].includes(expression.operator)) invalidQuery("Unknown filter operator.");
|
|
370
|
+
predicate(expression.operator, { name: expression.field }, expression.value);
|
|
371
|
+
} else if (expression.kind === "relation") {
|
|
372
|
+
if ([
|
|
373
|
+
expression.collection,
|
|
374
|
+
expression.field,
|
|
375
|
+
expression.target
|
|
376
|
+
].some((name) => typeof name !== "string" || !name)) invalidQuery("Relationship filters require registered collection and relationship names.");
|
|
377
|
+
validateExpression(expression.expression);
|
|
378
|
+
} else if (expression.kind === "has") {
|
|
379
|
+
if (typeof expression.field !== "string" || !expression.field) invalidQuery("has() requires a relationship from the where() fields.");
|
|
380
|
+
validateExpression(expression.expression);
|
|
381
|
+
} else if ([
|
|
382
|
+
"and",
|
|
383
|
+
"or",
|
|
384
|
+
"not"
|
|
385
|
+
].includes(expression.kind) && Array.isArray(expression.expressions)) {
|
|
386
|
+
if (expression.kind === "not" && expression.expressions.length !== 1) invalidQuery("not() requires one expression.");
|
|
387
|
+
expression.expressions.forEach(validateExpression);
|
|
388
|
+
} else invalidQuery("Invalid expression structure.");
|
|
389
|
+
}
|
|
390
|
+
//#endregion
|
|
391
|
+
export { NO_SELECTION_MESSAGE as A, notIlike as C, resolveRelations as D, or as E, hasSelection as F, collectionNames as I, assertAvailable as M, effectiveOrder as N, startsWith as O, emptyPlan as P, not as S, notLike as T, isUndefined as _, arrayContainsAny as a, lte as b, eq as c, gte as d, has as f, isNull as g, isMissing as h, arrayContainsAll as i, PAGINATE_AFTER_LIMIT_MESSAGE as j, validateExpression as k, fieldProxy as l, inArray as m, arrayContained as n, contains as o, ilike as p, arrayContains as r, endsWith as s, and as t, gt as u, like as v, notInArray as w, ne as x, lt as y };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/runtime/query-style.d.ts
|
|
2
|
+
/** Query APIs an application can allow through `queryStyle` in content-query.config. */
|
|
3
|
+
type QueryStyle = "fluent" | "relational";
|
|
4
|
+
/**
|
|
5
|
+
* Collection members that start each query style. Every other query API hangs off these members,
|
|
6
|
+
* so gating them gates payloads, relationships, and adjacency as well. `count()` without options
|
|
7
|
+
* belongs to both styles; `count(options)` takes relational options.
|
|
8
|
+
*/
|
|
9
|
+
declare const queryStyleMembers: {
|
|
10
|
+
readonly fluent: readonly ["select", "where", "orderBy", "limit", "offset", "with", "withPayload", "selectAdjacent"];
|
|
11
|
+
readonly relational: readonly ["findMany", "findFirst", "paginate", "count(options)"];
|
|
12
|
+
};
|
|
13
|
+
/** A collection member that belongs to exactly one query style. */
|
|
14
|
+
type QueryStyleMember = (typeof queryStyleMembers)[QueryStyle][number];
|
|
15
|
+
/** Collection members that start a query in the given style. */
|
|
16
|
+
type MembersOf<Style extends QueryStyle> = (typeof queryStyleMembers)[Style][number];
|
|
17
|
+
/**
|
|
18
|
+
* Describes a disabled collection member in the type error itself, for example:
|
|
19
|
+
* `posts.select is unavailable because content-query.config sets queryStyle to "relational".`
|
|
20
|
+
*/
|
|
21
|
+
type QueryStyleDisabled<Collection extends string, Member extends QueryStyleMember, Style extends QueryStyle> = `${Collection}.${Member} is unavailable because content-query.config sets queryStyle to "${Style}".`;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { QueryStyleMember as i, QueryStyle as n, QueryStyleDisabled as r, MembersOf as t };
|
package/dist/query.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { C as Selection, E as Expression, S as ScalarField, T as Where, _ as PaginateResult, a as RelationalQueryOptions, b as RelationField, c as Adjacent, d as Field, f as Fields, g as PaginateOptions, h as PageOptions, i as RelationalQueries, l as CursorOptions, m as Ordered, n as RelationalCountOptions, o as RelationalQueryResult, p as NumberedPage, r as RelationalPaginateOptions, s as RelationalWhere, u as CursorPage, w as SelectionResult, x as Scalar, y as QueryFields } from "./relational-types-CTcalUNW.mjs";
|
|
2
2
|
//#region src/query/operators.d.ts
|
|
3
3
|
/** Matches a scalar using strict value/date equality. */
|
|
4
4
|
export declare function eq<Value>(field: ScalarField<Value>, value: NoInfer<Value>): Expression;
|
|
@@ -22,6 +22,29 @@ export declare function contains<Value>(field: Field<Value> & (NonNullable<Value
|
|
|
22
22
|
export declare function startsWith<Value>(field: Field<Value> & (NonNullable<Value> extends string ? unknown : never), value: string): Expression;
|
|
23
23
|
/** Matches a literal string suffix. */
|
|
24
24
|
export declare function endsWith<Value>(field: Field<Value> & (NonNullable<Value> extends string ? unknown : never), value: string): Expression;
|
|
25
|
+
/**
|
|
26
|
+
* Matches a SQL-style pattern: `%` matches any run of characters and `_` matches one character.
|
|
27
|
+
* A backslash escapes a wildcard. Matching is case-sensitive.
|
|
28
|
+
*/
|
|
29
|
+
export declare function like<Value>(field: Field<Value> & (NonNullable<Value> extends string ? unknown : never), pattern: string): Expression;
|
|
30
|
+
/** Matches a SQL-style pattern like {@link like}, ignoring case. */
|
|
31
|
+
export declare function ilike<Value>(field: Field<Value> & (NonNullable<Value> extends string ? unknown : never), pattern: string): Expression;
|
|
32
|
+
/** Negates {@link like}; entries without a matching string, including absent values, match. */
|
|
33
|
+
export declare function notLike<Value>(field: Field<Value> & (NonNullable<Value> extends string ? unknown : never), pattern: string): Expression;
|
|
34
|
+
/** Negates {@link ilike}; entries without a matching string, including absent values, match. */
|
|
35
|
+
export declare function notIlike<Value>(field: Field<Value> & (NonNullable<Value> extends string ? unknown : never), pattern: string): Expression;
|
|
36
|
+
/** Tests whether every item of an existing scalar array belongs to the candidates. */
|
|
37
|
+
export declare function arrayContained<Value extends Scalar>(field: Field<readonly Value[] | null | undefined>, values: readonly NoInfer<Value>[]): Expression;
|
|
38
|
+
/**
|
|
39
|
+
* Matches entries with at least one related entry. With a filter, a related entry must also match
|
|
40
|
+
* it, for example `has(fields.author, (author) => eq(author.name, "Ada"))`. Wrap the result in
|
|
41
|
+
* {@link not} to match entries without such a related entry.
|
|
42
|
+
*
|
|
43
|
+
* @param relation - A relationship from the `where()` callback's fields.
|
|
44
|
+
* @param filter - Optional filter over the related entry's fields and relationships.
|
|
45
|
+
* @returns An expression that the query resolves against the relationship's target collection.
|
|
46
|
+
*/
|
|
47
|
+
export declare function has<TargetFields>(relation: RelationField<TargetFields>, filter?: (fields: TargetFields) => Expression): Expression;
|
|
25
48
|
/** Tests scalar membership in an array, ignoring holes. */
|
|
26
49
|
export declare function arrayContains<Value extends Scalar>(field: Field<readonly Value[] | null | undefined>, value: NoInfer<Value>): Expression;
|
|
27
50
|
/** Tests whether any candidate belongs to an existing scalar array. */
|
|
@@ -41,4 +64,4 @@ export declare function or(...expressions: Expression[]): Expression;
|
|
|
41
64
|
/** Negates a predicate's boolean result. */
|
|
42
65
|
export declare function not(expression: Expression): Expression;
|
|
43
66
|
//#endregion
|
|
44
|
-
export type { Adjacent, RelationalCountOptions as CountOptions, Expression, Field, Fields, RelationalQueryOptions as QueryOptions, RelationalQueryOptions, RelationalQueries, RelationalQueryResult, RelationalWhere, Selection, SelectionResult, Where };
|
|
67
|
+
export type { Adjacent, RelationalCountOptions as CountOptions, CursorOptions, CursorPage, Expression, Field, Fields, NumberedPage, PageOptions, PaginateOptions, PaginateResult, QueryFields, RelationalQueryOptions as QueryOptions, RelationalQueryOptions, RelationField, RelationalPaginateOptions, RelationalQueries, RelationalQueryResult, RelationalWhere, Selection, SelectionResult, Where };
|
package/dist/query.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as and, n as
|
|
1
|
+
export { t as and, n as arrayContained, r as arrayContains, i as arrayContainsAll, a as arrayContainsAny, o as contains, s as endsWith, c as eq, u as gt, d as gte, f as has, p as ilike, m as inArray, h as isMissing, g as isNull, _ as isUndefined, v as like, y as lt, b as lte, x as ne, S as not, C as notIlike, w as notInArray, T as notLike, E as or, O as startsWith } from "./operators-D-w1-gDg.mjs";
|