@capacms/sdk 1.0.0-next.0 → 1.0.0-next.10
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/CHANGELOG.md +450 -0
- package/README.md +1754 -156
- package/bin/capa-codegen.js +192 -5
- package/bin/capa.js +235 -0
- package/bin/graphql-project.js +142 -0
- package/bin/project-env.js +58 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +17 -0
- package/dist/codegen.d.ts +55 -0
- package/dist/codegen.js +320 -39
- package/dist/config.d.ts +5 -36
- package/dist/config.js +47 -1
- package/dist/esm/image/index.d.ts +120 -0
- package/dist/esm/image/index.js +250 -0
- package/dist/esm/image/shared-params.generated.d.ts +190 -0
- package/dist/esm/image/shared-params.generated.js +461 -0
- package/dist/esm/nextjs/image-loader.d.ts +60 -0
- package/dist/esm/nextjs/image-loader.js +67 -0
- package/dist/esm/nextjs/overlay.d.ts +30 -0
- package/dist/esm/nextjs/overlay.js +75 -0
- package/dist/esm/overlay/index.d.ts +32 -0
- package/dist/esm/overlay/index.js +576 -0
- package/dist/esm/overlay/protocol.d.ts +187 -0
- package/dist/esm/overlay/protocol.js +240 -0
- package/dist/esm/package.json +4 -0
- package/dist/graphql-codegen.d.ts +117 -0
- package/dist/graphql-codegen.js +705 -0
- package/dist/http.js +1 -1
- package/dist/image/index.d.ts +120 -0
- package/dist/image/index.js +257 -0
- package/dist/image/shared-params.generated.d.ts +190 -0
- package/dist/image/shared-params.generated.js +471 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/next/attrs.d.ts +98 -0
- package/dist/next/attrs.js +125 -0
- package/dist/next/client.d.ts +176 -32
- package/dist/next/client.js +212 -90
- package/dist/next/entry-fields.d.ts +162 -0
- package/dist/next/entry-fields.js +2 -0
- package/dist/next/errors.d.ts +136 -0
- package/dist/next/errors.js +214 -0
- package/dist/next/field-names.d.ts +37 -0
- package/dist/next/field-names.js +145 -0
- package/dist/next/graphql/build.d.ts +27 -0
- package/dist/next/graphql/build.js +98 -0
- package/dist/next/graphql/documents.d.ts +67 -0
- package/dist/next/graphql/documents.js +35 -0
- package/dist/next/graphql/edit-mode.d.ts +16 -0
- package/dist/next/graphql/edit-mode.js +93 -0
- package/dist/next/graphql/filter-values.d.ts +34 -0
- package/dist/next/graphql/filter-values.js +96 -0
- package/dist/next/graphql/introspection.d.ts +89 -0
- package/dist/next/graphql/introspection.js +102 -0
- package/dist/next/graphql/plan.d.ts +115 -0
- package/dist/next/graphql/plan.js +531 -0
- package/dist/next/graphql/request.d.ts +228 -0
- package/dist/next/graphql/request.js +283 -0
- package/dist/next/graphql/rest.d.ts +66 -0
- package/dist/next/graphql/rest.js +502 -0
- package/dist/next/graphql/selection.d.ts +55 -0
- package/dist/next/graphql/selection.js +212 -0
- package/dist/next/graphql/sha256.d.ts +13 -0
- package/dist/next/graphql/sha256.js +86 -0
- package/dist/next/graphql/summary.d.ts +83 -0
- package/dist/next/graphql/summary.js +151 -0
- package/dist/next/graphql/tree-layout.d.ts +36 -0
- package/dist/next/graphql/tree-layout.js +20 -0
- package/dist/next/graphql/tree.d.ts +171 -0
- package/dist/next/graphql/tree.js +249 -0
- package/dist/next/graphql/typed.d.ts +261 -0
- package/dist/next/graphql/typed.js +146 -0
- package/dist/next/index.d.ts +30 -3
- package/dist/next/index.js +34 -1
- package/dist/next/inflate.d.ts +51 -0
- package/dist/next/inflate.js +243 -0
- package/dist/next/key-family.d.ts +31 -0
- package/dist/next/key-family.js +66 -0
- package/dist/next/select-types.d.ts +58 -5
- package/dist/next/system-keys.d.ts +27 -0
- package/dist/next/system-keys.js +42 -0
- package/dist/nextjs/image-loader.d.ts +60 -0
- package/dist/nextjs/image-loader.js +71 -0
- package/dist/nextjs/index.d.ts +484 -5
- package/dist/nextjs/index.js +704 -9
- package/dist/nextjs/overlay.d.ts +30 -0
- package/dist/nextjs/overlay.js +78 -0
- package/dist/overlay/index.d.ts +32 -0
- package/dist/overlay/index.js +596 -0
- package/dist/overlay/protocol.d.ts +187 -0
- package/dist/overlay/protocol.js +253 -0
- package/package.json +70 -15
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.restWhere = restWhere;
|
|
4
|
+
exports.specToSelect = specToSelect;
|
|
5
|
+
exports.selectToGraphQL = selectToGraphQL;
|
|
6
|
+
/**
|
|
7
|
+
* rest.ts — the same read as a REST request, and back.
|
|
8
|
+
*
|
|
9
|
+
* `/api/graphql` runs every root field as the equivalent `/api/entries`
|
|
10
|
+
* request (G plan D2), so a query spec has exactly one REST twin. These two
|
|
11
|
+
* helpers write it down in each direction, so `select` and GraphQL stay
|
|
12
|
+
* interchangeable in code: `specToSelect` for "show me the REST URL of this
|
|
13
|
+
* query" (public as `graphqlToSelect`, which also takes a builder selection),
|
|
14
|
+
* `selectToGraphQL` for "port this REST read to GraphQL".
|
|
15
|
+
*
|
|
16
|
+
* Names cross the boundary through the schema summary, because GraphQL and
|
|
17
|
+
* REST name a few things differently: a field renamed by N5 (`tags_field`,
|
|
18
|
+
* `price_usd`), a relation hop (`author__name_ASC` against `author.name`), the
|
|
19
|
+
* system fields the schema prefixes with `_`, a system key a field of the
|
|
20
|
+
* model shadows, which REST writes with `$` (`$tags`, `$createdAt`,
|
|
21
|
+
* `author.$id`), and a namespace holding a character REST's grammars use,
|
|
22
|
+
* which REST writes quoted (`"price.usd"`, field-names.ts).
|
|
23
|
+
*/
|
|
24
|
+
const plan_1 = require("./plan");
|
|
25
|
+
const filter_values_1 = require("./filter-values");
|
|
26
|
+
const system_keys_1 = require("../system-keys");
|
|
27
|
+
const field_names_1 = require("../field-names");
|
|
28
|
+
/**
|
|
29
|
+
* The system fields a REST `select` can name, in the order the API prints
|
|
30
|
+
* them, with their REST keys. `id`, `model` and `status` are not here: REST
|
|
31
|
+
* returns them whatever a select says, so the API never writes them.
|
|
32
|
+
*/
|
|
33
|
+
const REST_SYSTEM_SELECT = [
|
|
34
|
+
["createdAt", "createdAt"],
|
|
35
|
+
["updatedAt", "updatedAt"],
|
|
36
|
+
["publishedAt", "publishedAt"],
|
|
37
|
+
["_version", "version"],
|
|
38
|
+
["_folder", "folder"],
|
|
39
|
+
["_tags", "tags"],
|
|
40
|
+
];
|
|
41
|
+
/** REST system keys to GraphQL field names, where the two differ. */
|
|
42
|
+
const GRAPHQL_SYSTEM_NAME = { version: "_version", folder: "_folder", tags: "_tags" };
|
|
43
|
+
/** GraphQL system field names to REST system keys, where the two differ. */
|
|
44
|
+
const REST_SYSTEM_KEY = Object.fromEntries(Object.entries(GRAPHQL_SYSTEM_NAME).map(([restKey, graphqlName]) => [graphqlName, restKey]));
|
|
45
|
+
/** Operators a relation filter input carries beside its hop fields (N10). */
|
|
46
|
+
const RELATION_OPERATORS = new Set(["eq", "ne", "in", "nin", "exists", "null", "has", "hasAny", "hasAll"]);
|
|
47
|
+
/**
|
|
48
|
+
* Field kinds whose filter is the field's own operators plus dotted paths
|
|
49
|
+
* below it: a relation's hops, a media value's `id`. A relation the key cannot
|
|
50
|
+
* read is one too, so anything but its operators prints as the dotted path
|
|
51
|
+
* REST documents (`secret.code`), never as a nested object.
|
|
52
|
+
*/
|
|
53
|
+
const HOP_KINDS = new Set(["relation", "relationList", "id", "idList", "media"]);
|
|
54
|
+
/** Every filter operator, in the order the API's filter input types declare them. */
|
|
55
|
+
const OPERATOR_ORDER = [
|
|
56
|
+
"eq", "ne", "lt", "lte", "gt", "gte", "in", "nin", "contains", "startsWith", "endsWith",
|
|
57
|
+
"has", "hasAny", "hasAll", "exists", "null", "id",
|
|
58
|
+
];
|
|
59
|
+
/** Encoded as the API prints a twin: commas, colons and a system key's `$` stay readable. */
|
|
60
|
+
function encode(value) {
|
|
61
|
+
return encodeURIComponent(value).replace(/%2C/g, ",").replace(/%3A/g, ":").replace(/%24/g, "$");
|
|
62
|
+
}
|
|
63
|
+
function fieldByName(model, name) {
|
|
64
|
+
return model.fields.find((f) => f.name === name);
|
|
65
|
+
}
|
|
66
|
+
function fieldByNamespace(model, namespace) {
|
|
67
|
+
return model.fields.find((f) => f.namespace === namespace);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Whether a field of `model` takes the REST name `namespace`: one GraphQL
|
|
71
|
+
* exposes, or one N5 left out (the type description's `Not exposed:` line),
|
|
72
|
+
* which REST still reads by that name.
|
|
73
|
+
*/
|
|
74
|
+
function hasField(model, namespace) {
|
|
75
|
+
return fieldByNamespace(model, namespace) !== undefined || model.notExposed.includes(namespace);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A system key as REST writes it on `model`: `$tags` where a field is called
|
|
79
|
+
* `tags`, plain `tags` wherever no field takes the name, as the API writes it.
|
|
80
|
+
*/
|
|
81
|
+
function systemKeyName(model, key) {
|
|
82
|
+
return hasField(model, key) ? `${system_keys_1.SYSTEM_KEY_SIGIL}${key}` : key;
|
|
83
|
+
}
|
|
84
|
+
/** The system key a REST name spells: `$tags` always, `tags` only where no field takes the name. */
|
|
85
|
+
function restSystemKey(model, name) {
|
|
86
|
+
if (name.startsWith(system_keys_1.SYSTEM_KEY_SIGIL))
|
|
87
|
+
return (0, system_keys_1.sigilSystemKey)(name);
|
|
88
|
+
return (0, system_keys_1.isSystemKey)(name) && !hasField(model, name) ? name : null;
|
|
89
|
+
}
|
|
90
|
+
/** A REST name for a field GraphQL leaves out has no GraphQL twin: say so, rather than read it as the system field. */
|
|
91
|
+
function refuseNotExposed(model, name) {
|
|
92
|
+
if (!model.notExposed.includes(name))
|
|
93
|
+
return;
|
|
94
|
+
throw new plan_1.CapaBuildError(`${name} on ${model.typeName} is a field GraphQL does not expose, because its name collides with another field's; ` +
|
|
95
|
+
`read it over REST${(0, system_keys_1.isSystemKey)(name) ? `, and write the system key as ${system_keys_1.SYSTEM_KEY_SIGIL}${name}` : ""}`);
|
|
96
|
+
}
|
|
97
|
+
function targetOf(summary, field) {
|
|
98
|
+
return summary.models.find((m) => m.namespace === field.target);
|
|
99
|
+
}
|
|
100
|
+
/** `keys` in `order` first, then the rest as they came. */
|
|
101
|
+
function ordered(keys, order) {
|
|
102
|
+
const rank = (key) => {
|
|
103
|
+
const at = order.indexOf(key);
|
|
104
|
+
return at === -1 ? order.length : at;
|
|
105
|
+
};
|
|
106
|
+
return keys.map((key, i) => ({ key, i })).sort((a, b) => rank(a.key) - rank(b.key) || a.i - b.i).map((k) => k.key);
|
|
107
|
+
}
|
|
108
|
+
// ------------------------------------------------------ GraphQL to REST ---
|
|
109
|
+
function restSort(summary, model, value) {
|
|
110
|
+
const match = /^(.*)_(ASC|DESC)$/.exec(value);
|
|
111
|
+
if (!match)
|
|
112
|
+
throw new plan_1.CapaBuildError(`sort ${value} is not a value of ${model.sortType}`);
|
|
113
|
+
const [relationName, targetName] = match[1].split("__");
|
|
114
|
+
const direction = match[2] === "DESC" ? "-" : "";
|
|
115
|
+
const field = fieldByName(model, relationName);
|
|
116
|
+
if (targetName === undefined)
|
|
117
|
+
return `${direction}${field ? (0, field_names_1.writeName)(field.namespace) : systemKeyName(model, relationName)}`;
|
|
118
|
+
const target = field ? targetOf(summary, field) : undefined;
|
|
119
|
+
const targetField = target ? fieldByName(target, targetName) : undefined;
|
|
120
|
+
return `${direction}${(0, field_names_1.writePath)([field?.namespace ?? relationName, targetField?.namespace ?? targetName])}`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* One level of a REST `select`, written as the API writes the twin in
|
|
124
|
+
* `extensions.capa.rest`: system keys first in the API's order, then fields in
|
|
125
|
+
* the order selected, each namespace bare or quoted as REST writes it, an
|
|
126
|
+
* expanded relation always with parentheses, a level that selects nothing else
|
|
127
|
+
* as `id` (`$id` beside a field called id). A system key whose REST name a
|
|
128
|
+
* field of the model shadows is written `$tags`, so the twin returns what
|
|
129
|
+
* GraphQL did.
|
|
130
|
+
*/
|
|
131
|
+
function restSelectText(summary, model, fields) {
|
|
132
|
+
const system = new Set(fields.filter((f) => !f.field).map((f) => f.name));
|
|
133
|
+
const items = [];
|
|
134
|
+
for (const [graphqlName, restKey] of REST_SYSTEM_SELECT) {
|
|
135
|
+
if (system.has(graphqlName))
|
|
136
|
+
items.push(systemKeyName(model, restKey));
|
|
137
|
+
}
|
|
138
|
+
for (const planned of fields) {
|
|
139
|
+
if (!planned.field)
|
|
140
|
+
continue;
|
|
141
|
+
const namespace = (0, field_names_1.writeName)(planned.field.namespace);
|
|
142
|
+
if (!planned.target || !planned.children) {
|
|
143
|
+
items.push(namespace);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const args = [restSelectText(summary, planned.target, planned.children)];
|
|
147
|
+
if (planned.field.kind === "relationList") {
|
|
148
|
+
// A written `first` is written, 100 included: REST's node bound counts an
|
|
149
|
+
// unwritten limit as 10 and a written one in full (spec 17, amendment 76).
|
|
150
|
+
if (planned.first !== undefined)
|
|
151
|
+
args.push(`limit:${planned.first}`);
|
|
152
|
+
if (planned.sort !== undefined)
|
|
153
|
+
args.push(`sort:${restSort(summary, planned.target, planned.sort)}`);
|
|
154
|
+
if (planned.after !== undefined)
|
|
155
|
+
args.push(`after:${planned.after}`);
|
|
156
|
+
}
|
|
157
|
+
items.push(`${namespace}(${args.join(",")})`);
|
|
158
|
+
}
|
|
159
|
+
return items.length ? items.join(",") : systemKeyName(model, "id");
|
|
160
|
+
}
|
|
161
|
+
/** An operator object without its null members, in the API's operator order; null when empty. */
|
|
162
|
+
function restOperations(value, order = OPERATOR_ORDER) {
|
|
163
|
+
if (!(0, filter_values_1.isObject)(value))
|
|
164
|
+
return null;
|
|
165
|
+
const out = {};
|
|
166
|
+
for (const key of ordered(Object.keys(value), order)) {
|
|
167
|
+
if (value[key] !== null && value[key] !== undefined)
|
|
168
|
+
out[key] = value[key];
|
|
169
|
+
}
|
|
170
|
+
return Object.keys(out).length ? out : null;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* A GraphQL filter input as the REST `where` object, key for key what the API
|
|
174
|
+
* prints in `extensions.capa.rest`: namespaces for field names, written as
|
|
175
|
+
* REST writes them, a hop as a dotted path after the relation's own
|
|
176
|
+
* operators, keys in the order the
|
|
177
|
+
* filter input type declares them, and null members dropped (GraphQL reads a
|
|
178
|
+
* null as "no condition").
|
|
179
|
+
*/
|
|
180
|
+
function restWhere(summary, model, filter) {
|
|
181
|
+
const where = {};
|
|
182
|
+
const order = ["and", "or", "not", ...model.systemFilters.map((system) => system.name), ...model.fields.map((f) => f.name)];
|
|
183
|
+
for (const key of ordered(Object.keys(filter), order)) {
|
|
184
|
+
const value = filter[key];
|
|
185
|
+
if (value === null || value === undefined)
|
|
186
|
+
continue;
|
|
187
|
+
if (key === "and" || key === "or") {
|
|
188
|
+
where[key] = (Array.isArray(value) ? value : [value]).map((part) => restWhere(summary, model, part));
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (key === "not") {
|
|
192
|
+
const inner = restWhere(summary, model, value);
|
|
193
|
+
if (Object.keys(inner).length)
|
|
194
|
+
where.not = inner;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const field = fieldByName(model, key);
|
|
198
|
+
if (!field) {
|
|
199
|
+
// A system field: `_tags` is REST's `tags`, or `$tags` beside a field called tags (amendment 56).
|
|
200
|
+
const system = model.systemFilters.find((f) => f.name === key);
|
|
201
|
+
const operations = restOperations(value, system?.filterOps.length ? system.filterOps : OPERATOR_ORDER);
|
|
202
|
+
if (operations)
|
|
203
|
+
where[systemKeyName(model, REST_SYSTEM_KEY[key] ?? key)] = operations;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const operations = restOperations(value, field.filterOps.length ? field.filterOps : OPERATOR_ORDER);
|
|
207
|
+
if (!operations)
|
|
208
|
+
continue;
|
|
209
|
+
const written = (0, field_names_1.writeName)(field.namespace);
|
|
210
|
+
if (!HOP_KINDS.has(field.kind)) {
|
|
211
|
+
where[written] = operations;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const target = targetOf(summary, field);
|
|
215
|
+
const own = {};
|
|
216
|
+
const hops = [];
|
|
217
|
+
for (const [name, inner] of Object.entries(operations)) {
|
|
218
|
+
if (field.kind === "media" && name === "id") {
|
|
219
|
+
hops.push([`${written}.id`, { eq: inner }]);
|
|
220
|
+
}
|
|
221
|
+
else if (field.kind === "media" || RELATION_OPERATORS.has(name)) {
|
|
222
|
+
own[name] = inner;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const hop = restOperations(inner);
|
|
226
|
+
const hopField = target ? fieldByName(target, name) : undefined;
|
|
227
|
+
const segment = hopField?.namespace ?? (target ? systemKeyName(target, name) : name);
|
|
228
|
+
if (hop)
|
|
229
|
+
hops.push([`${written}.${(0, field_names_1.writePathSegment)(segment)}`, hop]);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (Object.keys(own).length)
|
|
233
|
+
where[written] = own;
|
|
234
|
+
for (const [path, hop] of hops)
|
|
235
|
+
where[path] = hop;
|
|
236
|
+
}
|
|
237
|
+
return where;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* The REST request a query spec is equal to, written the way the API writes
|
|
241
|
+
* it in `extensions.capa.rest`: the filter as `where` JSON, the root's default
|
|
242
|
+
* page size left out, a relation list's `first` written as `limit:` whenever
|
|
243
|
+
* the spec gives it, parameters in the API's order. Public as
|
|
244
|
+
* `graphqlToSelect`, in selection.ts.
|
|
245
|
+
*/
|
|
246
|
+
function specToSelect(summary, spec) {
|
|
247
|
+
const plan = (0, plan_1.planQuery)(summary, spec);
|
|
248
|
+
return restRequestFor(plan);
|
|
249
|
+
}
|
|
250
|
+
/** `specToSelect` for a plan already checked. */
|
|
251
|
+
function restRequestFor(plan) {
|
|
252
|
+
const { model, summary } = plan;
|
|
253
|
+
const select = restSelectText(summary, model, plan.fields);
|
|
254
|
+
const params = [["select", select]];
|
|
255
|
+
if (plan.filter) {
|
|
256
|
+
const where = restWhere(summary, model, plan.filter);
|
|
257
|
+
if (Object.keys(where).length)
|
|
258
|
+
params.push(["where", JSON.stringify(where)]);
|
|
259
|
+
}
|
|
260
|
+
if (plan.sort)
|
|
261
|
+
params.push(["sort", plan.sort.map((value) => restSort(summary, model, value)).join(",")]);
|
|
262
|
+
if (plan.first !== undefined && plan.first !== plan_1.LIST_DEFAULT)
|
|
263
|
+
params.push(["limit", String(plan.first)]);
|
|
264
|
+
if (plan.after !== undefined)
|
|
265
|
+
params.push(["after", plan.after]);
|
|
266
|
+
if (plan.before !== undefined)
|
|
267
|
+
params.push(["before", plan.before]);
|
|
268
|
+
if (plan.totalCount)
|
|
269
|
+
params.push(["count", "true"]);
|
|
270
|
+
const path = plan.mode === "single"
|
|
271
|
+
? `/api/entries/${encodeURIComponent(model.namespace)}/${encodeURIComponent(plan.id)}`
|
|
272
|
+
: `/api/entries/${encodeURIComponent(model.namespace)}`;
|
|
273
|
+
const url = `${path}?${params.map(([key, value]) => `${key}=${encode(value)}`).join("&")}`;
|
|
274
|
+
return { path, select, params, url };
|
|
275
|
+
}
|
|
276
|
+
// ------------------------------------------------------ REST to GraphQL ---
|
|
277
|
+
/** Split on the commas outside quoted names and parentheses. */
|
|
278
|
+
function splitTopLevel(text) {
|
|
279
|
+
const parts = [];
|
|
280
|
+
let depth = 0;
|
|
281
|
+
let start = 0;
|
|
282
|
+
for (let i = 0; i < text.length; i++) {
|
|
283
|
+
const char = text[i];
|
|
284
|
+
if (char === field_names_1.NAME_QUOTE) {
|
|
285
|
+
const quoted = (0, field_names_1.readQuoted)(text, i);
|
|
286
|
+
if (!quoted)
|
|
287
|
+
throw new plan_1.CapaBuildError(`select has an unclosed quote: ${text}`);
|
|
288
|
+
i = quoted.end - 1;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (char === "(")
|
|
292
|
+
depth++;
|
|
293
|
+
else if (char === ")")
|
|
294
|
+
depth--;
|
|
295
|
+
else if (char === "," && depth === 0) {
|
|
296
|
+
parts.push(text.slice(start, i));
|
|
297
|
+
start = i + 1;
|
|
298
|
+
}
|
|
299
|
+
if (depth < 0)
|
|
300
|
+
throw new plan_1.CapaBuildError(`select has an unmatched ")": ${text}`);
|
|
301
|
+
}
|
|
302
|
+
if (depth !== 0)
|
|
303
|
+
throw new plan_1.CapaBuildError(`select has an unclosed "(": ${text}`);
|
|
304
|
+
parts.push(text.slice(start));
|
|
305
|
+
return parts.map((part) => part.trim()).filter(Boolean);
|
|
306
|
+
}
|
|
307
|
+
/** A sort or filter path read back, at most a field and one hop; `what` names it in a refusal. */
|
|
308
|
+
function restPath(text, what) {
|
|
309
|
+
const path = (0, field_names_1.readPath)(text);
|
|
310
|
+
if (!path)
|
|
311
|
+
throw new plan_1.CapaBuildError(`${what} ${text} has a quote that does not close its name`);
|
|
312
|
+
if (path.length > 2)
|
|
313
|
+
throw new plan_1.CapaBuildError(`${what} ${text} goes more than one relation deep, which REST does not read`);
|
|
314
|
+
return path;
|
|
315
|
+
}
|
|
316
|
+
function graphqlSort(summary, model, raw) {
|
|
317
|
+
const direction = raw.startsWith("-") ? "DESC" : "ASC";
|
|
318
|
+
const [relation, hop] = restPath(raw.replace(/^-/, ""), "sort");
|
|
319
|
+
if (hop === undefined)
|
|
320
|
+
return `${graphqlFieldName(model, relation)}_${direction}`;
|
|
321
|
+
const field = fieldByNamespace(model, relation.name);
|
|
322
|
+
const name = field?.name ?? relation.name;
|
|
323
|
+
const target = field ? targetOf(summary, field) : undefined;
|
|
324
|
+
const targetName = target ? graphqlFieldName(target, hop) : hop.name;
|
|
325
|
+
return `${name}__${targetName}_${direction}`;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* A REST name on `model` as its GraphQL field: `$tags` and an unshadowed
|
|
329
|
+
* `tags` are `_tags`. A quoted name is always a field, as the API reads it.
|
|
330
|
+
*/
|
|
331
|
+
function graphqlFieldName(model, { name, quoted }) {
|
|
332
|
+
const system = quoted ? null : restSystemKey(model, name);
|
|
333
|
+
if (system !== null)
|
|
334
|
+
return GRAPHQL_SYSTEM_NAME[system] ?? system;
|
|
335
|
+
refuseNotExposed(model, name);
|
|
336
|
+
return fieldByNamespace(model, name)?.name ?? name;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* A select item's name, read back, and what follows it: `"at.place"(name)` is
|
|
340
|
+
* `at.place` then `(name)`, `title` is `title` then nothing.
|
|
341
|
+
*/
|
|
342
|
+
function selectItem(item) {
|
|
343
|
+
let name;
|
|
344
|
+
let rest;
|
|
345
|
+
if (item.startsWith(field_names_1.NAME_QUOTE)) {
|
|
346
|
+
const quoted = (0, field_names_1.readQuoted)(item, 0); // splitTopLevel refused a quote that does not close
|
|
347
|
+
name = { name: quoted.name, quoted: true };
|
|
348
|
+
rest = item.slice(quoted.end);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
const open = item.indexOf("(");
|
|
352
|
+
name = { name: open === -1 ? item : item.slice(0, open), quoted: false };
|
|
353
|
+
rest = open === -1 ? "" : item.slice(open);
|
|
354
|
+
}
|
|
355
|
+
if (rest === "")
|
|
356
|
+
return { name, args: null };
|
|
357
|
+
if (!rest.startsWith("(") || !rest.endsWith(")")) {
|
|
358
|
+
throw new plan_1.CapaBuildError(`select item ${item} is not a field name, or a relation with its fields in parentheses`);
|
|
359
|
+
}
|
|
360
|
+
return { name, args: rest.slice(1, -1) };
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* A field a REST read returns, as a spec item. A media value is read with all
|
|
364
|
+
* six of its fields, the one public media shape REST returns (spec 17,
|
|
365
|
+
* amendment 31): the builder's `id url alt` default is for a query written
|
|
366
|
+
* without fields, never for the twin of a REST read.
|
|
367
|
+
*/
|
|
368
|
+
function restFieldSpec(field, name) {
|
|
369
|
+
return field?.kind === "media" ? { field: name, fields: [...plan_1.MEDIA_FIELDS] } : name;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* REST's `*`, and a read with no select, as fields: everything that read
|
|
373
|
+
* returns. The system keys REST prints beside `id`, `model` and `status`
|
|
374
|
+
* (`createdAt` to `tags`, as GraphQL names them), then every field,
|
|
375
|
+
* deprecated ones included. A relation list REST returns as references, a
|
|
376
|
+
* page of `{ id, model }` items; GraphQL reads it as a connection, so it is
|
|
377
|
+
* selected as one with `nodes { id }`, the same page of ids. The builder's
|
|
378
|
+
* own default, for a query written without fields, stays shorter.
|
|
379
|
+
*/
|
|
380
|
+
function restStarFields(model) {
|
|
381
|
+
const system = REST_SYSTEM_SELECT.map(([graphqlName]) => graphqlName);
|
|
382
|
+
const fields = model.fields.map((f) => (f.kind === "relationList" ? { field: f.name, fields: ["id"] } : restFieldSpec(f, f.name)));
|
|
383
|
+
return [...system, ...fields];
|
|
384
|
+
}
|
|
385
|
+
function specFields(summary, model, select) {
|
|
386
|
+
const items = splitTopLevel(select);
|
|
387
|
+
const fields = [];
|
|
388
|
+
const add = (spec) => {
|
|
389
|
+
const name = typeof spec === "string" ? spec : spec.field;
|
|
390
|
+
const at = fields.findIndex((f) => (typeof f === "string" ? f : f.field) === name);
|
|
391
|
+
if (at === -1)
|
|
392
|
+
fields.push(spec);
|
|
393
|
+
else if (typeof spec !== "string")
|
|
394
|
+
fields[at] = spec;
|
|
395
|
+
};
|
|
396
|
+
for (const item of items) {
|
|
397
|
+
if (item === "*") {
|
|
398
|
+
restStarFields(model).forEach(add);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const { name: itemName, args } = selectItem(item);
|
|
402
|
+
if (args === null) {
|
|
403
|
+
const name = graphqlFieldName(model, itemName);
|
|
404
|
+
add(restFieldSpec(fieldByName(model, name), name));
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const field = fieldByNamespace(model, itemName.name);
|
|
408
|
+
const target = field ? targetOf(summary, field) : undefined;
|
|
409
|
+
if (!field || !target) {
|
|
410
|
+
throw new plan_1.CapaBuildError(`${itemName.name} on ${model.typeName} is not a relation this key can expand`);
|
|
411
|
+
}
|
|
412
|
+
const spec = { field: field.name };
|
|
413
|
+
const inner = [];
|
|
414
|
+
for (const arg of splitTopLevel(args)) {
|
|
415
|
+
if (arg.startsWith("limit:"))
|
|
416
|
+
spec.first = Number(arg.slice(6));
|
|
417
|
+
else if (arg.startsWith("sort:"))
|
|
418
|
+
spec.sort = graphqlSort(summary, target, arg.slice(5));
|
|
419
|
+
else if (arg.startsWith("after:"))
|
|
420
|
+
spec.after = arg.slice(6);
|
|
421
|
+
else
|
|
422
|
+
inner.push(arg);
|
|
423
|
+
}
|
|
424
|
+
spec.fields = specFields(summary, target, inner.join(",") || "*");
|
|
425
|
+
add(spec);
|
|
426
|
+
}
|
|
427
|
+
return fields;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* REST reads filter values loosely (a query string is all text, and a list
|
|
431
|
+
* operand may be comma-separated), so each is converted to the type its
|
|
432
|
+
* GraphQL filter input declares (filter-values.ts).
|
|
433
|
+
*/
|
|
434
|
+
const REST_TEXT = { listText: true };
|
|
435
|
+
function graphqlFilter(summary, model, where) {
|
|
436
|
+
const filter = {};
|
|
437
|
+
const merge = (key, value) => {
|
|
438
|
+
filter[key] = { ...(filter[key] ?? {}), ...value };
|
|
439
|
+
};
|
|
440
|
+
for (const [path, value] of Object.entries(where)) {
|
|
441
|
+
if (path === "and" || path === "or") {
|
|
442
|
+
filter[path] = value.map((part) => graphqlFilter(summary, model, part));
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (path === "not") {
|
|
446
|
+
filter.not = graphqlFilter(summary, model, value);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const operations = (value && typeof value === "object" ? value : { eq: value });
|
|
450
|
+
const [head, hop] = restPath(path, "where key");
|
|
451
|
+
const system = head.quoted ? null : restSystemKey(model, head.name);
|
|
452
|
+
if (system === null)
|
|
453
|
+
refuseNotExposed(model, head.name);
|
|
454
|
+
const field = system === null ? fieldByNamespace(model, head.name) : undefined;
|
|
455
|
+
const name = field?.name ?? graphqlFieldName(model, head);
|
|
456
|
+
if (hop === undefined) {
|
|
457
|
+
const operands = field ? field.filterInputs ?? {} : model.systemFilters.find((f) => f.name === name)?.filterInputs ?? filter_values_1.SYSTEM_OPERANDS;
|
|
458
|
+
merge(name, (0, filter_values_1.typedOperations)(operations, operands, REST_TEXT));
|
|
459
|
+
}
|
|
460
|
+
else if (field?.kind === "media" && !hop.quoted && hop.name === "id") {
|
|
461
|
+
merge(name, { id: (0, filter_values_1.toScalar)("ID", operations.eq) });
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
const target = field ? targetOf(summary, field) : undefined;
|
|
465
|
+
const hopName = target ? graphqlFieldName(target, hop) : hop.name;
|
|
466
|
+
const hopField = target ? fieldByName(target, hopName) : undefined;
|
|
467
|
+
const operands = hopField?.filterInputs ?? (hopName === "id" ? filter_values_1.SYSTEM_OPERANDS : {});
|
|
468
|
+
merge(name, { [hopName]: (0, filter_values_1.typedOperations)(operations, operands, REST_TEXT) });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return filter;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* The query spec a REST read is equal to. Pass it to `buildGraphQLQuery`.
|
|
475
|
+
*
|
|
476
|
+
* selectToGraphQL(schema, "articles", "title,author(name)", { sort: ["-views"], limit: 5 })
|
|
477
|
+
* // { model: "articles", fields: ["title", { field: "author", fields: ["name"] }], sort: ["views_DESC"], first: 5 }
|
|
478
|
+
*/
|
|
479
|
+
function selectToGraphQL(summary, namespace, select, options = {}) {
|
|
480
|
+
const model = (0, plan_1.findModel)(summary, namespace);
|
|
481
|
+
const spec = { model: model.namespace };
|
|
482
|
+
if (options.id !== undefined) {
|
|
483
|
+
spec.mode = "single";
|
|
484
|
+
spec.id = options.id;
|
|
485
|
+
}
|
|
486
|
+
spec.fields = specFields(summary, model, select ?? "*");
|
|
487
|
+
const where = { ...(options.filter ?? {}), ...(options.where ?? {}) };
|
|
488
|
+
if (Object.keys(where).length)
|
|
489
|
+
spec.filter = graphqlFilter(summary, model, where);
|
|
490
|
+
if (options.sort?.length)
|
|
491
|
+
spec.sort = options.sort.map((raw) => graphqlSort(summary, model, raw));
|
|
492
|
+
if (options.limit !== undefined)
|
|
493
|
+
spec.first = options.limit;
|
|
494
|
+
if (options.count)
|
|
495
|
+
spec.totalCount = true;
|
|
496
|
+
if (options.after !== undefined)
|
|
497
|
+
spec.after = options.after;
|
|
498
|
+
if (options.before !== undefined)
|
|
499
|
+
spec.before = options.before;
|
|
500
|
+
(0, plan_1.planQuery)(summary, spec);
|
|
501
|
+
return spec;
|
|
502
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selection.ts — a REST read as a typed-builder selection, and back.
|
|
3
|
+
*
|
|
4
|
+
* The SDK writes a read three ways: a REST `select` (`entries.list`), the
|
|
5
|
+
* tool spec (`{ model, fields, first, sort, filter }`, which
|
|
6
|
+
* `buildGraphQLQuery` prints and the Explorer and MCP builders share), and the
|
|
7
|
+
* typed builder's selection (`{ articles: { args, nodes: { ... } } }`, which
|
|
8
|
+
* `client.graphql.query` runs and `toTree` reads). `selectToGraphQL` writes a
|
|
9
|
+
* REST read as the tool spec and `selectToSelection` as a selection;
|
|
10
|
+
* `graphqlToSelect` takes either back to REST (G brief, npm item 4). Each goes
|
|
11
|
+
* through the tool spec, which checks every name.
|
|
12
|
+
*
|
|
13
|
+
* A selection written here names what `toTree` needs to answer exactly what
|
|
14
|
+
* REST answers: `id`, `model` and `status` on every entry, `pageInfo {
|
|
15
|
+
* hasNextPage endCursor }` on every relation list, and every media field REST
|
|
16
|
+
* returns.
|
|
17
|
+
*/
|
|
18
|
+
import { type GraphQLQuerySpec } from "./plan";
|
|
19
|
+
import { type RestReadOptions, type RestRequest } from "./rest";
|
|
20
|
+
import type { GraphQLSchemaSummary } from "./summary";
|
|
21
|
+
import { type RuntimeSelection } from "./typed";
|
|
22
|
+
/**
|
|
23
|
+
* The typed-builder selection a REST read is equal to. `client.graphql.query`
|
|
24
|
+
* runs it, and `toTree` turns its result into the REST `data`:
|
|
25
|
+
*
|
|
26
|
+
* const schema = await capa.graphqlSchema();
|
|
27
|
+
* const selection = selectToSelection(schema, "articles", "title,author(name)", { sort: ["-views"], limit: 5 });
|
|
28
|
+
* const { data } = await capa.graphql.query(selection);
|
|
29
|
+
* const { articles } = toTree(data, selection, schema);
|
|
30
|
+
* // deep-equal to (await capa.entries.list("articles", { select: "title,author(name)", sort: ["-views"], limit: 5 })).data
|
|
31
|
+
*
|
|
32
|
+
* It takes what `selectToGraphQL` takes and refuses what it refuses, with
|
|
33
|
+
* `CapaBuildError`. Built at run time, its result is untyped.
|
|
34
|
+
*/
|
|
35
|
+
export declare function selectToSelection(summary: GraphQLSchemaSummary, namespace: string, select?: string, options?: RestReadOptions): RuntimeSelection;
|
|
36
|
+
/**
|
|
37
|
+
* The REST request a read is equal to, written the way the API writes it in
|
|
38
|
+
* `extensions.capa.rest`. It takes the read either way the SDK writes one:
|
|
39
|
+
* the tool spec `selectToGraphQL` returns, or a typed-builder selection, the
|
|
40
|
+
* one `client.graphql.query` runs.
|
|
41
|
+
*
|
|
42
|
+
* graphqlToSelect(schema, { model: "articles", fields: ["title", { field: "author", fields: ["name"] }] }).url
|
|
43
|
+
* graphqlToSelect(schema, { articles: { nodes: { title: true, author: { name: true } } } }).url
|
|
44
|
+
* // both "/api/entries/articles?select=title,author(name)"
|
|
45
|
+
*
|
|
46
|
+
* A read REST cannot make throws `CapaBuildError`: a name the schema does not
|
|
47
|
+
* have, and for a selection another root field (`version`, `entry`), more
|
|
48
|
+
* than one root field, or a relation list read from a cursor in a list read.
|
|
49
|
+
* `last` with no `before`, or with `before: null`, is REST's `before=end`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function graphqlToSelect(summary: GraphQLSchemaSummary, spec: GraphQLQuerySpec): RestRequest;
|
|
52
|
+
export declare function graphqlToSelect(summary: GraphQLSchemaSummary, selection: {
|
|
53
|
+
readonly model?: never;
|
|
54
|
+
readonly [rootField: string]: unknown;
|
|
55
|
+
}): RestRequest;
|