@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.
Files changed (92) hide show
  1. package/CHANGELOG.md +450 -0
  2. package/README.md +1754 -156
  3. package/bin/capa-codegen.js +192 -5
  4. package/bin/capa.js +235 -0
  5. package/bin/graphql-project.js +142 -0
  6. package/bin/project-env.js +58 -0
  7. package/dist/client.d.ts +5 -0
  8. package/dist/client.js +17 -0
  9. package/dist/codegen.d.ts +55 -0
  10. package/dist/codegen.js +320 -39
  11. package/dist/config.d.ts +5 -36
  12. package/dist/config.js +47 -1
  13. package/dist/esm/image/index.d.ts +120 -0
  14. package/dist/esm/image/index.js +250 -0
  15. package/dist/esm/image/shared-params.generated.d.ts +190 -0
  16. package/dist/esm/image/shared-params.generated.js +461 -0
  17. package/dist/esm/nextjs/image-loader.d.ts +60 -0
  18. package/dist/esm/nextjs/image-loader.js +67 -0
  19. package/dist/esm/nextjs/overlay.d.ts +30 -0
  20. package/dist/esm/nextjs/overlay.js +75 -0
  21. package/dist/esm/overlay/index.d.ts +32 -0
  22. package/dist/esm/overlay/index.js +576 -0
  23. package/dist/esm/overlay/protocol.d.ts +187 -0
  24. package/dist/esm/overlay/protocol.js +240 -0
  25. package/dist/esm/package.json +4 -0
  26. package/dist/graphql-codegen.d.ts +117 -0
  27. package/dist/graphql-codegen.js +705 -0
  28. package/dist/http.js +1 -1
  29. package/dist/image/index.d.ts +120 -0
  30. package/dist/image/index.js +257 -0
  31. package/dist/image/shared-params.generated.d.ts +190 -0
  32. package/dist/image/shared-params.generated.js +471 -0
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.js +2 -1
  35. package/dist/next/attrs.d.ts +98 -0
  36. package/dist/next/attrs.js +125 -0
  37. package/dist/next/client.d.ts +176 -32
  38. package/dist/next/client.js +212 -90
  39. package/dist/next/entry-fields.d.ts +162 -0
  40. package/dist/next/entry-fields.js +2 -0
  41. package/dist/next/errors.d.ts +136 -0
  42. package/dist/next/errors.js +214 -0
  43. package/dist/next/field-names.d.ts +37 -0
  44. package/dist/next/field-names.js +145 -0
  45. package/dist/next/graphql/build.d.ts +27 -0
  46. package/dist/next/graphql/build.js +98 -0
  47. package/dist/next/graphql/documents.d.ts +67 -0
  48. package/dist/next/graphql/documents.js +35 -0
  49. package/dist/next/graphql/edit-mode.d.ts +16 -0
  50. package/dist/next/graphql/edit-mode.js +93 -0
  51. package/dist/next/graphql/filter-values.d.ts +34 -0
  52. package/dist/next/graphql/filter-values.js +96 -0
  53. package/dist/next/graphql/introspection.d.ts +89 -0
  54. package/dist/next/graphql/introspection.js +102 -0
  55. package/dist/next/graphql/plan.d.ts +115 -0
  56. package/dist/next/graphql/plan.js +531 -0
  57. package/dist/next/graphql/request.d.ts +228 -0
  58. package/dist/next/graphql/request.js +283 -0
  59. package/dist/next/graphql/rest.d.ts +66 -0
  60. package/dist/next/graphql/rest.js +502 -0
  61. package/dist/next/graphql/selection.d.ts +55 -0
  62. package/dist/next/graphql/selection.js +212 -0
  63. package/dist/next/graphql/sha256.d.ts +13 -0
  64. package/dist/next/graphql/sha256.js +86 -0
  65. package/dist/next/graphql/summary.d.ts +83 -0
  66. package/dist/next/graphql/summary.js +151 -0
  67. package/dist/next/graphql/tree-layout.d.ts +36 -0
  68. package/dist/next/graphql/tree-layout.js +20 -0
  69. package/dist/next/graphql/tree.d.ts +171 -0
  70. package/dist/next/graphql/tree.js +249 -0
  71. package/dist/next/graphql/typed.d.ts +261 -0
  72. package/dist/next/graphql/typed.js +146 -0
  73. package/dist/next/index.d.ts +30 -3
  74. package/dist/next/index.js +34 -1
  75. package/dist/next/inflate.d.ts +51 -0
  76. package/dist/next/inflate.js +243 -0
  77. package/dist/next/key-family.d.ts +31 -0
  78. package/dist/next/key-family.js +66 -0
  79. package/dist/next/select-types.d.ts +58 -5
  80. package/dist/next/system-keys.d.ts +27 -0
  81. package/dist/next/system-keys.js +42 -0
  82. package/dist/nextjs/image-loader.d.ts +60 -0
  83. package/dist/nextjs/image-loader.js +71 -0
  84. package/dist/nextjs/index.d.ts +484 -5
  85. package/dist/nextjs/index.js +704 -9
  86. package/dist/nextjs/overlay.d.ts +30 -0
  87. package/dist/nextjs/overlay.js +78 -0
  88. package/dist/overlay/index.d.ts +32 -0
  89. package/dist/overlay/index.js +596 -0
  90. package/dist/overlay/protocol.d.ts +187 -0
  91. package/dist/overlay/protocol.js +253 -0
  92. 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;