@capacms/sdk 1.0.0-next.4 → 1.0.0-next.6
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 +323 -0
- package/README.md +1043 -186
- package/bin/capa-codegen.js +192 -5
- package/bin/capa.js +208 -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 -1
- package/dist/graphql-codegen.d.ts +117 -0
- package/dist/graphql-codegen.js +705 -0
- package/dist/http.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/next/attrs.d.ts +51 -12
- package/dist/next/attrs.js +74 -20
- package/dist/next/client.d.ts +111 -38
- package/dist/next/client.js +116 -82
- 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 +28 -5
- package/dist/next/index.js +25 -1
- package/dist/next/inflate.d.ts +25 -7
- package/dist/next/inflate.js +46 -32
- package/dist/next/key-family.d.ts +34 -0
- package/dist/next/key-family.js +74 -0
- package/dist/next/select-types.d.ts +44 -8
- package/dist/next/system-keys.d.ts +27 -0
- package/dist/next/system-keys.js +42 -0
- package/dist/nextjs/index.d.ts +165 -12
- package/dist/nextjs/index.js +247 -23
- package/dist/nextjs/overlay.d.ts +5 -0
- package/dist/nextjs/overlay.js +35 -0
- package/package.json +35 -13
package/dist/next/client.js
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LAYOUT_PAGE = exports.CapaError = void 0;
|
|
4
|
-
exports.
|
|
3
|
+
exports.LAYOUT_PAGE = exports.isCapaError = exports.CapaError = void 0;
|
|
4
|
+
exports.graphqlClient = graphqlClient;
|
|
5
5
|
exports.resolveNextConfig = resolveNextConfig;
|
|
6
6
|
exports.serializeSelect = serializeSelect;
|
|
7
7
|
exports.createClient = createClient;
|
|
8
8
|
const attrs_1 = require("./attrs");
|
|
9
|
+
const errors_1 = require("./errors");
|
|
10
|
+
const edit_mode_1 = require("./graphql/edit-mode");
|
|
11
|
+
const key_family_1 = require("./key-family");
|
|
12
|
+
const system_keys_1 = require("./system-keys");
|
|
13
|
+
const field_names_1 = require("./field-names");
|
|
14
|
+
const introspection_1 = require("./graphql/introspection");
|
|
15
|
+
const request_1 = require("./graphql/request");
|
|
16
|
+
const summary_1 = require("./graphql/summary");
|
|
17
|
+
const typed_1 = require("./graphql/typed");
|
|
18
|
+
var errors_2 = require("./errors");
|
|
19
|
+
Object.defineProperty(exports, "CapaError", { enumerable: true, get: function () { return errors_2.CapaError; } });
|
|
20
|
+
Object.defineProperty(exports, "isCapaError", { enumerable: true, get: function () { return errors_2.isCapaError; } });
|
|
9
21
|
/**
|
|
10
22
|
* What a `Capa-Schema` value may look like: hex, 8 to 64 characters.
|
|
11
23
|
*
|
|
@@ -17,29 +29,23 @@ const attrs_1 = require("./attrs");
|
|
|
17
29
|
* appears rather than as an error.
|
|
18
30
|
*/
|
|
19
31
|
const SCHEMA_CHECKSUM = /^[a-f0-9]{8,64}$/;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
this.docs = input.docs;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
exports.CapaError = CapaError;
|
|
41
|
-
function isCapaError(error) {
|
|
42
|
-
return error instanceof CapaError;
|
|
32
|
+
const BUILDER_NOT_PERSISTED = "@capacms/sdk/next: graphql.query() cannot send persisted: true. It builds its document when it runs, " +
|
|
33
|
+
"so capa persist never stored it and every read would fall back to an uncached POST. " +
|
|
34
|
+
"Leave persisted out (a builder read is already a cached GET), or write the read as a #graphql literal and run capa persist.";
|
|
35
|
+
/**
|
|
36
|
+
* `client.graphql` over one way of reading a document: called with a
|
|
37
|
+
* document, and `query()` printing a selection's document for it. `createClient`
|
|
38
|
+
* reads straight from the API, and `getCapaClient` from `/nextjs` through
|
|
39
|
+
* Next's data cache, with the same refusals.
|
|
40
|
+
*/
|
|
41
|
+
function graphqlClient(read) {
|
|
42
|
+
const query = async (selection, options) => {
|
|
43
|
+
if (options?.persisted)
|
|
44
|
+
throw new TypeError(BUILDER_NOT_PERSISTED);
|
|
45
|
+
return read((0, typed_1.selectionToDocument)(selection, options?.operationName), undefined, options);
|
|
46
|
+
};
|
|
47
|
+
const call = (document, variables, options) => read(document, variables, options);
|
|
48
|
+
return Object.assign(call, { query });
|
|
43
49
|
}
|
|
44
50
|
function resolveNextConfig(config) {
|
|
45
51
|
const value = (config ?? {});
|
|
@@ -48,8 +54,9 @@ function resolveNextConfig(config) {
|
|
|
48
54
|
throw new Error(`@capacms/sdk/next: missing ${missing.join(", ")}. ` +
|
|
49
55
|
"createClient needs baseUrl, apiKey and version.");
|
|
50
56
|
}
|
|
51
|
-
if (
|
|
52
|
-
throw new Error("@capacms/sdk/next: apiKey must
|
|
57
|
+
if ((0, key_family_1.keyFamily)(value.apiKey) === null) {
|
|
58
|
+
throw new Error("@capacms/sdk/next: apiKey must be a string: a cap_ key, or the legacy key your site already holds. " +
|
|
59
|
+
"Mint a cap_ key in the Capa admin under Developers > Keys.");
|
|
53
60
|
}
|
|
54
61
|
if (value.contract !== undefined && value.contract !== 1) {
|
|
55
62
|
throw new Error("@capacms/sdk/next: contract must be 1 when provided.");
|
|
@@ -140,12 +147,41 @@ function resolvePath(configPath, callPath) {
|
|
|
140
147
|
}
|
|
141
148
|
return bare;
|
|
142
149
|
}
|
|
143
|
-
|
|
150
|
+
/*
|
|
151
|
+
* The array form names each field by its namespace, as saved in the admin,
|
|
152
|
+
* and the select is written in REST's grammar: a namespace that holds a
|
|
153
|
+
* character the grammar uses goes quoted (`"price.usd"`, field-names.ts). A
|
|
154
|
+
* name starting with `$` is a system key, so no field can be named that way;
|
|
155
|
+
* select "*" returns such a field.
|
|
156
|
+
*/
|
|
157
|
+
/** A relation's name: a field, since a system key expands nothing. */
|
|
144
158
|
function selectName(value, context) {
|
|
145
|
-
if (typeof value !== "string" || !
|
|
159
|
+
if (typeof value !== "string" || !(0, field_names_1.isNameable)(value)) {
|
|
146
160
|
throw new TypeError(`@capacms/sdk/next: ${context} must be a field name.`);
|
|
147
161
|
}
|
|
148
|
-
return value;
|
|
162
|
+
return (0, field_names_1.writeName)(value);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* A select item: a field, or a system key by its `$` name, which means the
|
|
166
|
+
* system key even beside a field of the same plain name (spec 17, amendment 29).
|
|
167
|
+
*/
|
|
168
|
+
function selectItemName(value) {
|
|
169
|
+
if ((0, system_keys_1.sigilSystemKey)(value) !== null)
|
|
170
|
+
return value;
|
|
171
|
+
if ((0, field_names_1.isNameable)(value))
|
|
172
|
+
return (0, field_names_1.writeName)(value);
|
|
173
|
+
throw new TypeError(`@capacms/sdk/next: select item must be a field name, or a system key: ${system_keys_1.SYSTEM_KEY_LIST}.`);
|
|
174
|
+
}
|
|
175
|
+
/** One nested sort key: a field, or a system key the API sorts by, either with a leading `-`. */
|
|
176
|
+
function nestedSort(value, relation) {
|
|
177
|
+
const descending = typeof value === "string" && value.startsWith("-");
|
|
178
|
+
const key = descending ? value.slice(1) : value;
|
|
179
|
+
const system = typeof key === "string" ? (0, system_keys_1.sigilSystemKey)(key) : null;
|
|
180
|
+
if (system !== null && (0, system_keys_1.isSortableSystemKey)(system))
|
|
181
|
+
return value;
|
|
182
|
+
if (typeof key === "string" && system === null && (0, field_names_1.isNameable)(key))
|
|
183
|
+
return `${descending ? "-" : ""}${(0, field_names_1.writeName)(key)}`;
|
|
184
|
+
throw new TypeError(`@capacms/sdk/next: ${relation}.sort must be one field name, or one of ${system_keys_1.SORTABLE_SYSTEM_KEYS.map((k) => `$${k}`).join(", ")}, with - to sort descending.`);
|
|
149
185
|
}
|
|
150
186
|
function serializeSelectItems(items) {
|
|
151
187
|
if (items.length === 0)
|
|
@@ -153,7 +189,7 @@ function serializeSelectItems(items) {
|
|
|
153
189
|
return items
|
|
154
190
|
.map((item) => {
|
|
155
191
|
if (typeof item === "string") {
|
|
156
|
-
return item === "*" ? item :
|
|
192
|
+
return item === "*" ? item : selectItemName(item);
|
|
157
193
|
}
|
|
158
194
|
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
159
195
|
throw new TypeError("@capacms/sdk/next: each select item must be a field name or relation object.");
|
|
@@ -162,12 +198,12 @@ function serializeSelectItems(items) {
|
|
|
162
198
|
if (entries.length !== 1) {
|
|
163
199
|
throw new TypeError("@capacms/sdk/next: a relation select object must name exactly one field.");
|
|
164
200
|
}
|
|
165
|
-
const [
|
|
166
|
-
const
|
|
201
|
+
const [name, value] = entries[0];
|
|
202
|
+
const written = selectName(name, "relation select key");
|
|
167
203
|
if (value === "*")
|
|
168
|
-
return `${
|
|
204
|
+
return `${written}(*)`;
|
|
169
205
|
if (Array.isArray(value))
|
|
170
|
-
return `${
|
|
206
|
+
return `${written}(${serializeSelectItems(value)})`;
|
|
171
207
|
if (!value || typeof value !== "object") {
|
|
172
208
|
throw new TypeError(`@capacms/sdk/next: ${name} must select fields, "*", or select options.`);
|
|
173
209
|
}
|
|
@@ -192,19 +228,15 @@ function serializeSelectItems(items) {
|
|
|
192
228
|
}
|
|
193
229
|
args.push(`limit:${options.limit}`);
|
|
194
230
|
}
|
|
195
|
-
if (options.sort !== undefined)
|
|
196
|
-
|
|
197
|
-
throw new TypeError(`@capacms/sdk/next: ${name}.sort must be one field name.`);
|
|
198
|
-
}
|
|
199
|
-
args.push(`sort:${options.sort}`);
|
|
200
|
-
}
|
|
231
|
+
if (options.sort !== undefined)
|
|
232
|
+
args.push(`sort:${nestedSort(options.sort, name)}`);
|
|
201
233
|
if (options.after !== undefined) {
|
|
202
234
|
if (typeof options.after !== "string" || options.after === "") {
|
|
203
235
|
throw new TypeError(`@capacms/sdk/next: ${name}.after must be a non-empty cursor.`);
|
|
204
236
|
}
|
|
205
237
|
args.push(`after:${options.after}`);
|
|
206
238
|
}
|
|
207
|
-
return `${
|
|
239
|
+
return `${written}(${args.join(",")})`;
|
|
208
240
|
})
|
|
209
241
|
.join(",");
|
|
210
242
|
}
|
|
@@ -287,39 +319,6 @@ function listQuery(options) {
|
|
|
287
319
|
query.set("before", options.before);
|
|
288
320
|
return query;
|
|
289
321
|
}
|
|
290
|
-
function optionalString(value) {
|
|
291
|
-
return typeof value === "string" ? value : undefined;
|
|
292
|
-
}
|
|
293
|
-
function unparseable(status, requestId) {
|
|
294
|
-
return new CapaError({
|
|
295
|
-
status,
|
|
296
|
-
type: "api_error",
|
|
297
|
-
code: "unparseable_response",
|
|
298
|
-
message: "Capa returned a response that was not a valid /api/ JSON envelope.",
|
|
299
|
-
requestId,
|
|
300
|
-
docs: "",
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
function errorFromEnvelope(status, body, fallbackRequestId) {
|
|
304
|
-
const detail = body.error;
|
|
305
|
-
if (!detail ||
|
|
306
|
-
typeof detail.type !== "string" ||
|
|
307
|
-
typeof detail.code !== "string" ||
|
|
308
|
-
typeof detail.message !== "string" ||
|
|
309
|
-
typeof detail.docs !== "string") {
|
|
310
|
-
return unparseable(status, fallbackRequestId);
|
|
311
|
-
}
|
|
312
|
-
return new CapaError({
|
|
313
|
-
status,
|
|
314
|
-
type: detail.type,
|
|
315
|
-
code: detail.code,
|
|
316
|
-
message: detail.message,
|
|
317
|
-
param: optionalString(detail.param),
|
|
318
|
-
hint: optionalString(detail.hint),
|
|
319
|
-
requestId: optionalString(body.meta?.requestId) ?? fallbackRequestId,
|
|
320
|
-
docs: detail.docs,
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
322
|
function cacheTags(response) {
|
|
324
323
|
const value = response.headers?.get?.("Surrogate-Key") ?? "";
|
|
325
324
|
return value.split(/\s+/).filter(Boolean);
|
|
@@ -354,12 +353,13 @@ function createRequester(config) {
|
|
|
354
353
|
body = JSON.parse(text);
|
|
355
354
|
}
|
|
356
355
|
catch {
|
|
357
|
-
throw unparseable(response.status, requestId);
|
|
356
|
+
throw (0, errors_1.unparseable)(response.status, requestId);
|
|
357
|
+
}
|
|
358
|
+
if (!response.ok) {
|
|
359
|
+
throw (0, errors_1.errorFromEnvelope)(response.status, body, requestId, (0, errors_1.retryAfterOf)(response.headers?.get?.("Retry-After")));
|
|
358
360
|
}
|
|
359
|
-
if (!response.ok)
|
|
360
|
-
throw errorFromEnvelope(response.status, body, requestId);
|
|
361
361
|
if (!body || typeof body !== "object")
|
|
362
|
-
throw unparseable(response.status, requestId);
|
|
362
|
+
throw (0, errors_1.unparseable)(response.status, requestId);
|
|
363
363
|
if (config.editMode === true) {
|
|
364
364
|
// `included` holds a flat read's related entries, which are marked
|
|
365
365
|
// exactly as they are when the tree nests them inside `data`.
|
|
@@ -373,7 +373,36 @@ function createRequester(config) {
|
|
|
373
373
|
}
|
|
374
374
|
function createClient(config) {
|
|
375
375
|
const resolved = resolveNextConfig(config);
|
|
376
|
+
if ((0, key_family_1.keyFamily)(resolved.apiKey) === "legacy")
|
|
377
|
+
(0, key_family_1.warnLegacyKeyOnce)(resolved.apiKey);
|
|
376
378
|
const request = createRequester(resolved);
|
|
379
|
+
// By POST: introspection is never cached (G plan D16), and the admin host
|
|
380
|
+
// serves GraphQL by POST only.
|
|
381
|
+
const readSchema = async (signal) => {
|
|
382
|
+
const result = await (0, request_1.runGraphQL)(resolved, introspection_1.INTROSPECTION_QUERY, undefined, { signal, method: "POST" });
|
|
383
|
+
const requestId = result.extensions.capa?.requestId ?? "";
|
|
384
|
+
// The API answered 200, so the status says so; the errors say what failed.
|
|
385
|
+
if (result.errors.length > 0)
|
|
386
|
+
throw (0, errors_1.errorFromGraphQLErrors)(200, result.errors, requestId);
|
|
387
|
+
if (!result.data)
|
|
388
|
+
throw (0, errors_1.unparseable)(200, requestId);
|
|
389
|
+
return (0, summary_1.summarizeIntrospection)(result.data);
|
|
390
|
+
};
|
|
391
|
+
// By POST, as the schema above: it selects only `__schema`.
|
|
392
|
+
const readFieldNames = async () => {
|
|
393
|
+
const result = await (0, request_1.runGraphQL)(resolved, introspection_1.FIELD_NAMES_QUERY, undefined, { method: "POST" });
|
|
394
|
+
if (!result.data || result.errors.length > 0)
|
|
395
|
+
throw new Error("the key's field names could not be read");
|
|
396
|
+
return result.data;
|
|
397
|
+
};
|
|
398
|
+
/** One GraphQL read; in edit mode its entries are marked for `capaAttrs`, as REST's are. */
|
|
399
|
+
const readGraphQL = (document, variables, options) => {
|
|
400
|
+
const read = () => (0, request_1.runGraphQL)(resolved, document, variables, options);
|
|
401
|
+
if (resolved.editMode !== true)
|
|
402
|
+
return read();
|
|
403
|
+
return (0, edit_mode_1.readMarked)(document, `${resolved.baseUrl}\n${resolved.apiKey}\n${resolved.version}`, readFieldNames, read);
|
|
404
|
+
};
|
|
405
|
+
const graphql = graphqlClient(readGraphQL);
|
|
377
406
|
/**
|
|
378
407
|
* A flat read's result carries the select it sent, so `inflate(result)`
|
|
379
408
|
* needs nothing else. A tree read's result is exactly what it always was.
|
|
@@ -399,7 +428,7 @@ function createClient(config) {
|
|
|
399
428
|
return withSelect({ ...result.body, cacheTags: result.cacheTags }, options);
|
|
400
429
|
}
|
|
401
430
|
catch (error) {
|
|
402
|
-
if (error instanceof CapaError &&
|
|
431
|
+
if (error instanceof errors_1.CapaError &&
|
|
403
432
|
error.status === 404 &&
|
|
404
433
|
error.type === "not_found" &&
|
|
405
434
|
error.code === "entry_not_found") {
|
|
@@ -441,7 +470,7 @@ function createClient(config) {
|
|
|
441
470
|
// Same shape as `entries.get`: a page nobody has declared or read is a
|
|
442
471
|
// null, not a throw, because "is this page known to Capa" is a question
|
|
443
472
|
// a caller asks on purpose.
|
|
444
|
-
if (error instanceof CapaError &&
|
|
473
|
+
if (error instanceof errors_1.CapaError &&
|
|
445
474
|
error.status === 404 &&
|
|
446
475
|
error.code === "page_not_found") {
|
|
447
476
|
return null;
|
|
@@ -453,7 +482,12 @@ function createClient(config) {
|
|
|
453
482
|
return {
|
|
454
483
|
entries,
|
|
455
484
|
pages,
|
|
485
|
+
graphql,
|
|
486
|
+
graphqlSchema(options = {}) {
|
|
487
|
+
return readSchema(options.signal);
|
|
488
|
+
},
|
|
456
489
|
async preview(token, options = {}) {
|
|
490
|
+
(0, key_family_1.requirePreviewKey)(resolved.apiKey);
|
|
457
491
|
const query = new URLSearchParams();
|
|
458
492
|
query.set("token", token);
|
|
459
493
|
try {
|
|
@@ -464,7 +498,7 @@ function createClient(config) {
|
|
|
464
498
|
// preview route has the same fallback for either: render the published
|
|
465
499
|
// page. Anything else (a 500, a network failure, a missing scope) is a
|
|
466
500
|
// real problem and must not be mistaken for a stale link.
|
|
467
|
-
if (error instanceof CapaError &&
|
|
501
|
+
if (error instanceof errors_1.CapaError &&
|
|
468
502
|
(error.code === "preview_token_invalid" || error.code === "preview_token_expired")) {
|
|
469
503
|
return null;
|
|
470
504
|
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* entry-fields.ts — an entry's `fields` as `/api/entries` returns them, typed
|
|
3
|
+
* from the model and the select.
|
|
4
|
+
*
|
|
5
|
+
* A model type says what an editor stores: `capa-codegen` writes a relation as
|
|
6
|
+
* the related model (`CapaRelation<Authors>`), a relation list as a list of
|
|
7
|
+
* them, and media as the upload (`CapaImage`). A read answers in its own shape
|
|
8
|
+
* (docs/api/entries.md), and these types say which:
|
|
9
|
+
*
|
|
10
|
+
* - a field the select names is always there, and `null` when it was never
|
|
11
|
+
* filled, as the API writes it: optional in the model, never absent here;
|
|
12
|
+
* - a field the select does not name is not there;
|
|
13
|
+
* - a relation the select does not expand is a reference, `{ id, model }`,
|
|
14
|
+
* or null when it is empty;
|
|
15
|
+
* - an expanded relation is the related entry, with `fields` of its own, or
|
|
16
|
+
* `{ id, model, missing: true }` when that entry was deleted, is
|
|
17
|
+
* unpublished for a production key, or is in a model the key cannot read;
|
|
18
|
+
* - a relation list is `{ items, pageInfo }`, expanded or not;
|
|
19
|
+
* - media is `{ id, url, alt, type, width, height }`, never the upload's
|
|
20
|
+
* own keys.
|
|
21
|
+
*
|
|
22
|
+
* A select the compiler can read exactly, a literal passed as its type
|
|
23
|
+
* (`list<Articles, typeof select>`), types what it names. One it cannot, a
|
|
24
|
+
* string or a value typed as `Select<T>` itself, types every field, and each
|
|
25
|
+
* relation as any of the three it may be, which the caller narrows
|
|
26
|
+
* (`"fields" in author`). An untyped model (`Record<string, unknown>`) stays
|
|
27
|
+
* untyped.
|
|
28
|
+
*/
|
|
29
|
+
import type { Entry } from "./client";
|
|
30
|
+
import type { Select } from "./select-types";
|
|
31
|
+
/** A relation a read did not expand: the entry's id, and its model's namespace, null when the key cannot read that model. */
|
|
32
|
+
export interface EntryReference {
|
|
33
|
+
id: string;
|
|
34
|
+
model: string | null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* An expanded relation the API looked for and did not find: the entry was
|
|
38
|
+
* deleted, is unpublished for a production key, or is in a model the key
|
|
39
|
+
* cannot read.
|
|
40
|
+
*/
|
|
41
|
+
export interface MissingEntry extends EntryReference {
|
|
42
|
+
missing: true;
|
|
43
|
+
}
|
|
44
|
+
/** A relation list, expanded or not. `limit` is there when the read expanded it. */
|
|
45
|
+
export interface RelationItems<E> {
|
|
46
|
+
items: E[];
|
|
47
|
+
pageInfo: {
|
|
48
|
+
limit?: number;
|
|
49
|
+
hasNext: boolean;
|
|
50
|
+
next: string | null;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** An image, video or file as `/api/` returns it. */
|
|
54
|
+
export interface EntryMedia {
|
|
55
|
+
id: string;
|
|
56
|
+
url: string | null;
|
|
57
|
+
alt: string | null;
|
|
58
|
+
type: string | null;
|
|
59
|
+
width: number | null;
|
|
60
|
+
height: number | null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Levels of entries left to type: the root entry and 4 below it, the API's 5
|
|
64
|
+
* levels. A relation past the last level is typed as a reference, which is all
|
|
65
|
+
* the API returns there.
|
|
66
|
+
*/
|
|
67
|
+
type Depth = [never, 0, 1, 2, 3, 4];
|
|
68
|
+
type Flatten<T> = {
|
|
69
|
+
[K in keyof T]: T[K];
|
|
70
|
+
};
|
|
71
|
+
/** A field type that says nothing: `unknown`, or `any`. */
|
|
72
|
+
type Untyped<V> = 0 extends 1 & V ? true : unknown extends V ? true : false;
|
|
73
|
+
type RelationBrand = {
|
|
74
|
+
readonly __capaRelation: "one" | "many";
|
|
75
|
+
readonly __capaRelationTarget: unknown;
|
|
76
|
+
};
|
|
77
|
+
type IsRelation<V> = Untyped<V> extends true ? false : NonNullable<V> extends RelationBrand ? true : false;
|
|
78
|
+
type IsList<V> = NonNullable<V> extends {
|
|
79
|
+
readonly __capaRelation: "many";
|
|
80
|
+
} ? true : false;
|
|
81
|
+
type TargetOf<V> = NonNullable<V> extends {
|
|
82
|
+
readonly __capaRelationTarget: infer R;
|
|
83
|
+
} ? R : never;
|
|
84
|
+
/** Media as codegen types it: `CapaImage`, `CapaVideo` and `CapaFile` each have a `url`. */
|
|
85
|
+
type StoredMedia = {
|
|
86
|
+
url: string;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* A field that is no relation: as stored, or `null` when it was never filled,
|
|
90
|
+
* but media, which the read returns in its public shape.
|
|
91
|
+
*/
|
|
92
|
+
type ValueTree<V> = Untyped<V> extends true ? V : NonNullable<V> extends ReadonlyArray<infer M> ? [M] extends [StoredMedia] ? EntryMedia[] | null : NonNullable<V> | null : NonNullable<V> extends StoredMedia ? EntryMedia | null : NonNullable<V> | null;
|
|
93
|
+
/** A relation the read did not expand. */
|
|
94
|
+
type ReferenceTree<V> = IsList<V> extends true ? RelationItems<EntryReference> : EntryReference | null;
|
|
95
|
+
/** A field the read names without expanding it. */
|
|
96
|
+
type FieldTree<V> = IsRelation<V> extends true ? ReferenceTree<V> : ValueTree<V>;
|
|
97
|
+
/** An expanded relation, its entries read with `S`, at level `D`. */
|
|
98
|
+
type ExpandedTree<V, S, D extends number> = IsList<V> extends true ? RelationItems<Entry<FieldsAt<TargetOf<V>, S, D>> | MissingEntry> : Entry<FieldsAt<TargetOf<V>, S, D>> | MissingEntry | null;
|
|
99
|
+
/** A relation the read may or may not have expanded, with any select, at level `D`. */
|
|
100
|
+
type EitherTree<V, D extends number> = IsList<V> extends true ? RelationItems<EntryReference | MissingEntry | Entry<FieldsAt<TargetOf<V>, string, D>>> : EntryReference | MissingEntry | Entry<FieldsAt<TargetOf<V>, string, D>> | null;
|
|
101
|
+
/** Every field, relations as references: a read with no select, or `*`. */
|
|
102
|
+
type ReferenceFields<T> = {
|
|
103
|
+
[K in keyof T]-?: FieldTree<T[K]>;
|
|
104
|
+
};
|
|
105
|
+
/** Every field, each relation as whatever it may be: a select the compiler cannot read. */
|
|
106
|
+
type LooseFields<T, D extends number> = {
|
|
107
|
+
[K in keyof T]-?: IsRelation<T[K]> extends true ? EitherTree<T[K], Depth[D]> : ValueTree<T[K]>;
|
|
108
|
+
};
|
|
109
|
+
/** The relations a select item expands, by name. */
|
|
110
|
+
type ExpandedIn<I> = I extends string ? never : Extract<keyof I, string>;
|
|
111
|
+
/** What the select writes for relation `K`: its list, `*`, or its options' `select`. */
|
|
112
|
+
type SubSelect<I, K extends string> = I extends {
|
|
113
|
+
readonly [P in K]: infer V;
|
|
114
|
+
} ? V extends {
|
|
115
|
+
readonly select: infer S;
|
|
116
|
+
} ? S : V : never;
|
|
117
|
+
/**
|
|
118
|
+
* The fields a literal select names, `I` being the union of its items: each
|
|
119
|
+
* named field, every field for `*`, and each expanded relation as the entries
|
|
120
|
+
* its own select reads. A system key (`$tags`) sits beside `fields`, on the entry.
|
|
121
|
+
*/
|
|
122
|
+
type SelectedFields<T, I, D extends number> = Flatten<{
|
|
123
|
+
[K in keyof T as K extends ExpandedIn<I> ? IsRelation<T[K]> extends true ? never : K : "*" extends I ? K : K extends I ? K : never]-?: FieldTree<T[K]>;
|
|
124
|
+
} & {
|
|
125
|
+
[K in keyof T as K extends ExpandedIn<I> ? (IsRelation<T[K]> extends true ? K : never) : never]-?: ExpandedTree<T[K], SubSelect<I, K & string>, Depth[D]>;
|
|
126
|
+
}>;
|
|
127
|
+
type FieldsAt<T, S, D extends number> = string extends keyof T ? T : [D] extends [never] ? ReferenceFields<T> : [S] extends [undefined] ? ReferenceFields<T> : [S] extends ["*"] ? ReferenceFields<T> : S extends ReadonlyArray<infer I> ? Select<T> extends S ? LooseFields<T, D> : SelectedFields<T, I, D> : LooseFields<T, D>;
|
|
128
|
+
/**
|
|
129
|
+
* `fields` of an entry of model `T` read with select `S`, as `/api/entries`
|
|
130
|
+
* returns them. Pass the select's own type for exact fields
|
|
131
|
+
* (`list<Articles, typeof select>`); without it every field is typed, and
|
|
132
|
+
* each relation as whatever it may be.
|
|
133
|
+
*/
|
|
134
|
+
export type EntryFields<T, S = Select<T> | string> = FieldsAt<T, S, 4>;
|
|
135
|
+
/** A field of a `shape=flat` read: each relation a reference, since the entry it names is in `included`. */
|
|
136
|
+
type FlatTree<V> = IsRelation<V> extends true ? IsList<V> extends true ? RelationItems<EntryReference | MissingEntry> : EntryReference | MissingEntry | null : ValueTree<V>;
|
|
137
|
+
/** The fields a select names at its root: every field for none, `*`, a string or `Select<T>`. */
|
|
138
|
+
type NamedKeys<T, S> = [S] extends [undefined] ? keyof T : S extends ReadonlyArray<infer I> ? Select<T> extends S ? keyof T : "*" extends I ? keyof T : Extract<keyof T, I | ExpandedIn<I>> : keyof T;
|
|
139
|
+
/** `fields` of an entry in a `shape=flat` read's `data`. */
|
|
140
|
+
export type FlatFields<T, S = Select<T>> = string extends keyof T ? T : {
|
|
141
|
+
[K in keyof T as K extends NamedKeys<T, S> ? K : never]-?: FlatTree<T[K]>;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* `fields` of an entry in `included`: the union of what every path that
|
|
145
|
+
* reached it selected, so any field may be absent.
|
|
146
|
+
*/
|
|
147
|
+
export type IncludedFields<I> = I extends unknown ? string extends keyof I ? I : {
|
|
148
|
+
[K in keyof I]?: FlatTree<I[K]>;
|
|
149
|
+
} : never;
|
|
150
|
+
/**
|
|
151
|
+
* Where a flat result keeps the model and select it was read with, for
|
|
152
|
+
* `inflate` to type the tree it returns. A symbol, never a field, and never a
|
|
153
|
+
* runtime value.
|
|
154
|
+
*/
|
|
155
|
+
declare const flatRead: unique symbol;
|
|
156
|
+
export type { flatRead };
|
|
157
|
+
export interface FlatRead<T, S> {
|
|
158
|
+
readonly [flatRead]?: {
|
|
159
|
+
model: T;
|
|
160
|
+
select: S;
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* errors.ts — what a failed `/api/` call throws.
|
|
3
|
+
*
|
|
4
|
+
* `CapaError` is one refusal of the whole request: the REST envelope's
|
|
5
|
+
* `{ error }`, or a GraphQL response with `errors` and no `data`, whatever
|
|
6
|
+
* its status. `CapaGraphQLError` is one entry of a GraphQL `errors` array. A
|
|
7
|
+
* GraphQL request the API ran resolves with those in `errors` rather than
|
|
8
|
+
* throwing, because its `data` carries the root fields that did succeed. They
|
|
9
|
+
* are returned, not thrown, so they are plain objects: a result goes through
|
|
10
|
+
* `Response.json` or into a client component's props with every field kept.
|
|
11
|
+
*/
|
|
12
|
+
export interface ApiErrorEnvelope {
|
|
13
|
+
error?: {
|
|
14
|
+
type?: unknown;
|
|
15
|
+
code?: unknown;
|
|
16
|
+
message?: unknown;
|
|
17
|
+
param?: unknown;
|
|
18
|
+
hint?: unknown;
|
|
19
|
+
docs?: unknown;
|
|
20
|
+
};
|
|
21
|
+
meta?: {
|
|
22
|
+
requestId?: unknown;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export declare class CapaError extends Error {
|
|
26
|
+
readonly status: number;
|
|
27
|
+
readonly type: string;
|
|
28
|
+
readonly code: string;
|
|
29
|
+
readonly param?: string;
|
|
30
|
+
readonly hint?: string;
|
|
31
|
+
readonly requestId: string;
|
|
32
|
+
readonly docs: string;
|
|
33
|
+
/**
|
|
34
|
+
* Every error of a refused GraphQL request, in order. The fields above come
|
|
35
|
+
* from the first one. Empty for a REST call.
|
|
36
|
+
*/
|
|
37
|
+
readonly graphqlErrors: readonly CapaGraphQLError[];
|
|
38
|
+
/**
|
|
39
|
+
* Seconds the API asked the caller to wait before trying again, from a 429's
|
|
40
|
+
* `Retry-After` (seconds or an HTTP date). Undefined when it sent none.
|
|
41
|
+
*/
|
|
42
|
+
readonly retryAfter?: number;
|
|
43
|
+
constructor(input: {
|
|
44
|
+
status: number;
|
|
45
|
+
type: string;
|
|
46
|
+
code: string;
|
|
47
|
+
message: string;
|
|
48
|
+
param?: string;
|
|
49
|
+
hint?: string;
|
|
50
|
+
requestId: string;
|
|
51
|
+
docs: string;
|
|
52
|
+
graphqlErrors?: readonly CapaGraphQLError[];
|
|
53
|
+
retryAfter?: number;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A `Retry-After` header in seconds: delta-seconds as sent, an HTTP date as
|
|
58
|
+
* the whole seconds from `now` until it (0 once it has passed). Undefined when
|
|
59
|
+
* the header is absent or neither.
|
|
60
|
+
*/
|
|
61
|
+
export declare function retryAfterOf(value: string | null | undefined, now?: number): number | undefined;
|
|
62
|
+
export declare function isCapaError(error: unknown): error is CapaError;
|
|
63
|
+
export declare function optionalString(value: unknown): string | undefined;
|
|
64
|
+
export declare function unparseable(status: number, requestId: string): CapaError;
|
|
65
|
+
export declare function errorFromEnvelope(status: number, body: ApiErrorEnvelope, fallbackRequestId: string, retryAfter?: number): CapaError;
|
|
66
|
+
/** One `errors[]` item as the API sends it. */
|
|
67
|
+
export interface GraphQLErrorItem {
|
|
68
|
+
message: string;
|
|
69
|
+
locations?: Array<{
|
|
70
|
+
line: number;
|
|
71
|
+
column: number;
|
|
72
|
+
}>;
|
|
73
|
+
path?: Array<string | number>;
|
|
74
|
+
extensions?: {
|
|
75
|
+
code?: string;
|
|
76
|
+
type?: string;
|
|
77
|
+
param?: string;
|
|
78
|
+
hint?: string;
|
|
79
|
+
docs?: string;
|
|
80
|
+
requestId?: string;
|
|
81
|
+
[key: string]: unknown;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* One GraphQL error: the spec's `{ message, locations, path, extensions }`,
|
|
86
|
+
* with the fields a caller acts on lifted out of `extensions`: `code` to
|
|
87
|
+
* branch on, `hint` for the fix, `docs` for the page that explains it,
|
|
88
|
+
* `param` for the argument at fault. `path` names the root field it belongs
|
|
89
|
+
* to.
|
|
90
|
+
*
|
|
91
|
+
* A plain object, not an `Error`: an `Error`'s `message` is not enumerable,
|
|
92
|
+
* so `JSON.stringify` drops it, and React sends an `Error` to a client
|
|
93
|
+
* component as a generic message in production. Only what the API sent is
|
|
94
|
+
* set, so a JSON round trip gives back an equal object. Check one with
|
|
95
|
+
* `isCapaGraphQLError`.
|
|
96
|
+
*/
|
|
97
|
+
export interface CapaGraphQLError {
|
|
98
|
+
readonly message: string;
|
|
99
|
+
readonly locations?: ReadonlyArray<{
|
|
100
|
+
line: number;
|
|
101
|
+
column: number;
|
|
102
|
+
}>;
|
|
103
|
+
readonly path?: ReadonlyArray<string | number>;
|
|
104
|
+
readonly extensions: Readonly<Record<string, unknown>>;
|
|
105
|
+
/** `extensions.code`, or `unknown` when the API sent none. */
|
|
106
|
+
readonly code: string;
|
|
107
|
+
readonly type?: string;
|
|
108
|
+
readonly param?: string;
|
|
109
|
+
readonly hint?: string;
|
|
110
|
+
readonly docs?: string;
|
|
111
|
+
}
|
|
112
|
+
/** Whether `value` is a `CapaGraphQLError`: by its shape, so one that went through JSON is one too. */
|
|
113
|
+
export declare function isCapaGraphQLError(value: unknown): value is CapaGraphQLError;
|
|
114
|
+
/** Turn a parsed `errors` value into typed errors, skipping anything that is not one. */
|
|
115
|
+
export declare function graphqlErrorsOf(value: unknown): CapaGraphQLError[];
|
|
116
|
+
/**
|
|
117
|
+
* A GraphQL request the API refused as a whole: a status other than 200, or
|
|
118
|
+
* a 200 with `errors` and no `data`, passed in as the refusal's `status`. The
|
|
119
|
+
* `CapaError` fields come from the first error, and `graphqlErrors` holds all
|
|
120
|
+
* of them. A body in the REST envelope shape, which a proxy or an older API
|
|
121
|
+
* can send, is read as one.
|
|
122
|
+
*/
|
|
123
|
+
export declare function errorFromGraphQLBody(status: number, body: unknown, fallbackRequestId: string, retryAfter?: number): CapaError;
|
|
124
|
+
/** A `CapaError` for errors already read: the fields of the first, and all of them in `graphqlErrors`. */
|
|
125
|
+
export declare function errorFromGraphQLErrors(status: number, errors: readonly CapaGraphQLError[], fallbackRequestId: string, retryAfter?: number): CapaError;
|
|
126
|
+
/**
|
|
127
|
+
* `/api/graphql` answered as a path the API does not serve: a POST
|
|
128
|
+
* `405 mutations_not_enabled`, a GET `404 route_not_found`, in the REST
|
|
129
|
+
* envelope. `CAPA_API_GRAPHQL=off` does that, and so does the admin host for a
|
|
130
|
+
* GET, since it serves GraphQL by POST only. The GraphQL handler never answers
|
|
131
|
+
* in that envelope (a real mutation's refusal is `{ errors }`), so the
|
|
132
|
+
* envelope is what tells "GraphQL is not here" from "this document was
|
|
133
|
+
* refused", and a query is never told it tried to write. Null for any other
|
|
134
|
+
* answer.
|
|
135
|
+
*/
|
|
136
|
+
export declare function graphqlNotServed(status: number, body: unknown, method: "GET" | "POST", fallbackRequestId: string): CapaError | null;
|