@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
package/dist/next/client.js
CHANGED
|
@@ -1,10 +1,36 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
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
|
+
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; } });
|
|
21
|
+
/**
|
|
22
|
+
* The platform fetch, called through `globalThis` on every request. Storing
|
|
23
|
+
* `globalThis.fetch` and calling it as a method of the config object gives it
|
|
24
|
+
* the wrong `this`, and a browser refuses that ("Failed to execute 'fetch' on
|
|
25
|
+
* 'Window': Illegal invocation"); Node does not care, which is why only
|
|
26
|
+
* browser reads broke. Looking it up per call also picks up a fetch a
|
|
27
|
+
* framework patches in after the client is built (Next does).
|
|
28
|
+
*/
|
|
29
|
+
function defaultFetch() {
|
|
30
|
+
if (typeof globalThis.fetch !== "function")
|
|
31
|
+
return undefined;
|
|
32
|
+
return ((input, init) => globalThis.fetch(input, init));
|
|
33
|
+
}
|
|
8
34
|
/**
|
|
9
35
|
* What a `Capa-Schema` value may look like: hex, 8 to 64 characters.
|
|
10
36
|
*
|
|
@@ -16,29 +42,24 @@ exports.createClient = createClient;
|
|
|
16
42
|
* appears rather than as an error.
|
|
17
43
|
*/
|
|
18
44
|
const SCHEMA_CHECKSUM = /^[a-f0-9]{8,64}$/;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
exports.CapaError = CapaError;
|
|
40
|
-
function isCapaError(error) {
|
|
41
|
-
return error instanceof CapaError;
|
|
45
|
+
const BUILDER_NOT_PERSISTED = "@capacms/sdk/next: graphql.query() cannot send persisted: true. It builds its document when it runs, " +
|
|
46
|
+
"so capa persist never stored it and every read would fall back to an uncached POST. " +
|
|
47
|
+
"Leave persisted out (a builder read is already a cached GET), or write the read as a #graphql literal and run capa persist.";
|
|
48
|
+
/**
|
|
49
|
+
* `client.graphql` over one way of reading a document: called with a
|
|
50
|
+
* document, and `query()` printing a selection's document for it. `createClient`
|
|
51
|
+
* reads straight from the API, and `getCapaClient` from `/nextjs` the same way
|
|
52
|
+
* unless a call gives `tags` or `revalidate`, then through Next's data cache,
|
|
53
|
+
* with the same refusals.
|
|
54
|
+
*/
|
|
55
|
+
function graphqlClient(read) {
|
|
56
|
+
const query = async (selection, options) => {
|
|
57
|
+
if (options?.persisted)
|
|
58
|
+
throw new TypeError(BUILDER_NOT_PERSISTED);
|
|
59
|
+
return read((0, typed_1.selectionToDocument)(selection, options?.operationName), undefined, options);
|
|
60
|
+
};
|
|
61
|
+
const call = (document, variables, options) => read(document, variables, options);
|
|
62
|
+
return Object.assign(call, { query });
|
|
42
63
|
}
|
|
43
64
|
function resolveNextConfig(config) {
|
|
44
65
|
const value = (config ?? {});
|
|
@@ -47,19 +68,21 @@ function resolveNextConfig(config) {
|
|
|
47
68
|
throw new Error(`@capacms/sdk/next: missing ${missing.join(", ")}. ` +
|
|
48
69
|
"createClient needs baseUrl, apiKey and version.");
|
|
49
70
|
}
|
|
50
|
-
if (
|
|
51
|
-
throw new Error("@capacms/sdk/next: apiKey must
|
|
71
|
+
if ((0, key_family_1.keyFamily)(value.apiKey) === null) {
|
|
72
|
+
throw new Error("@capacms/sdk/next: apiKey must be a string: a cap_ key, or the legacy key your site already holds. " +
|
|
73
|
+
"Mint a cap_ key in the Capa admin under Developers > Keys.");
|
|
52
74
|
}
|
|
53
75
|
if (value.contract !== undefined && value.contract !== 1) {
|
|
54
76
|
throw new Error("@capacms/sdk/next: contract must be 1 when provided.");
|
|
55
77
|
}
|
|
56
|
-
const fetchImpl = value.fetch ??
|
|
78
|
+
const fetchImpl = value.fetch ?? defaultFetch();
|
|
57
79
|
if (typeof fetchImpl !== "function") {
|
|
58
80
|
throw new Error("@capacms/sdk/next: no fetch available. Pass one via config.fetch.");
|
|
59
81
|
}
|
|
60
82
|
// Checked here as well as per call, so a bad page on the config throws where
|
|
61
83
|
// the client is built rather than on whichever read happens to run first.
|
|
62
84
|
resolvePage(value.page, undefined);
|
|
85
|
+
resolvePath(value.path, undefined);
|
|
63
86
|
// THROWS rather than dropping, the same rule `page` follows and for the same
|
|
64
87
|
// reason: the API must never 400 a running site over a telemetry header, so
|
|
65
88
|
// it ignores what it cannot store, and the SDK is the place a wrong value
|
|
@@ -85,6 +108,20 @@ function resolveNextConfig(config) {
|
|
|
85
108
|
* quietly stops arriving rather than as an error.
|
|
86
109
|
*/
|
|
87
110
|
const PAGE_ID = /^\/[A-Za-z0-9._\-[\]/]{0,199}$/;
|
|
111
|
+
/**
|
|
112
|
+
* The page a layout (or template) reads for: none.
|
|
113
|
+
*
|
|
114
|
+
* A root layout renders around every page on the site, so charging its reads
|
|
115
|
+
* to `/`, the route its file sits at, made the home page look as if it read
|
|
116
|
+
* every Site singleton and nav on the site. `routeOf` returns this for a
|
|
117
|
+
* `layout.*` or `template.*` file, and a read that names it sends NO
|
|
118
|
+
* `Capa-Page` at all, even when the client was built with a `page`.
|
|
119
|
+
*
|
|
120
|
+
* Not sent as a value, because the API would drop it anyway: it is not a
|
|
121
|
+
* `PAGE_ID`, and the API ignores what it cannot store. Staying absent keeps a
|
|
122
|
+
* layout read byte-identical to one from a client that never named a page.
|
|
123
|
+
*/
|
|
124
|
+
exports.LAYOUT_PAGE = "(layout)";
|
|
88
125
|
/**
|
|
89
126
|
* The page for one call: the call's own value, else the client's, else none.
|
|
90
127
|
*
|
|
@@ -97,17 +134,68 @@ function resolvePage(configPage, callPage) {
|
|
|
97
134
|
const value = callPage !== undefined ? callPage : configPage;
|
|
98
135
|
if (value === undefined || value === null)
|
|
99
136
|
return undefined;
|
|
137
|
+
// A layout's read, named on purpose: no header, and the config's page does
|
|
138
|
+
// not stand in for it either.
|
|
139
|
+
if (value === exports.LAYOUT_PAGE)
|
|
140
|
+
return undefined;
|
|
100
141
|
if (typeof value !== "string" || !PAGE_ID.test(value)) {
|
|
101
142
|
throw new TypeError(`@capacms/sdk/next: page must be a path such as "/blog/[slug]" or "/blog/hello". Got ${JSON.stringify(value)}.`);
|
|
102
143
|
}
|
|
103
144
|
return value;
|
|
104
145
|
}
|
|
105
|
-
|
|
146
|
+
/**
|
|
147
|
+
* What a `Capa-Path` value may look like: a site path, query and hash cut off.
|
|
148
|
+
* COPIED from `PAGE_PATH_PATTERN` in `apps/api/src/api-next/page-header.ts`.
|
|
149
|
+
*/
|
|
150
|
+
const PAGE_PATH = /^\/[^\s?#]{0,1023}$/;
|
|
151
|
+
function resolvePath(configPath, callPath) {
|
|
152
|
+
const value = callPath !== undefined ? callPath : configPath;
|
|
153
|
+
if (value === undefined || value === null)
|
|
154
|
+
return undefined;
|
|
155
|
+
if (typeof value !== "string") {
|
|
156
|
+
throw new TypeError(`@capacms/sdk/next: path must be a string such as "/blog/hello".`);
|
|
157
|
+
}
|
|
158
|
+
const bare = value.split("#")[0].split("?")[0];
|
|
159
|
+
if (!PAGE_PATH.test(bare)) {
|
|
160
|
+
throw new TypeError(`@capacms/sdk/next: path must be the concrete path being rendered, such as "/blog/hello". Got ${JSON.stringify(value)}.`);
|
|
161
|
+
}
|
|
162
|
+
return bare;
|
|
163
|
+
}
|
|
164
|
+
/*
|
|
165
|
+
* The array form names each field by its namespace, as saved in the admin,
|
|
166
|
+
* and the select is written in REST's grammar: a namespace that holds a
|
|
167
|
+
* character the grammar uses goes quoted (`"price.usd"`, field-names.ts). A
|
|
168
|
+
* name starting with `$` is a system key, so no field can be named that way;
|
|
169
|
+
* select "*" returns such a field.
|
|
170
|
+
*/
|
|
171
|
+
/** A relation's name: a field, since a system key expands nothing. */
|
|
106
172
|
function selectName(value, context) {
|
|
107
|
-
if (typeof value !== "string" || !
|
|
173
|
+
if (typeof value !== "string" || !(0, field_names_1.isNameable)(value)) {
|
|
108
174
|
throw new TypeError(`@capacms/sdk/next: ${context} must be a field name.`);
|
|
109
175
|
}
|
|
110
|
-
return value;
|
|
176
|
+
return (0, field_names_1.writeName)(value);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* A select item: a field, or a system key by its `$` name, which means the
|
|
180
|
+
* system key even beside a field of the same plain name (spec 17, amendment 29).
|
|
181
|
+
*/
|
|
182
|
+
function selectItemName(value) {
|
|
183
|
+
if ((0, system_keys_1.sigilSystemKey)(value) !== null)
|
|
184
|
+
return value;
|
|
185
|
+
if ((0, field_names_1.isNameable)(value))
|
|
186
|
+
return (0, field_names_1.writeName)(value);
|
|
187
|
+
throw new TypeError(`@capacms/sdk/next: select item must be a field name, or a system key: ${system_keys_1.SYSTEM_KEY_LIST}.`);
|
|
188
|
+
}
|
|
189
|
+
/** One nested sort key: a field, or a system key the API sorts by, either with a leading `-`. */
|
|
190
|
+
function nestedSort(value, relation) {
|
|
191
|
+
const descending = typeof value === "string" && value.startsWith("-");
|
|
192
|
+
const key = descending ? value.slice(1) : value;
|
|
193
|
+
const system = typeof key === "string" ? (0, system_keys_1.sigilSystemKey)(key) : null;
|
|
194
|
+
if (system !== null && (0, system_keys_1.isSortableSystemKey)(system))
|
|
195
|
+
return value;
|
|
196
|
+
if (typeof key === "string" && system === null && (0, field_names_1.isNameable)(key))
|
|
197
|
+
return `${descending ? "-" : ""}${(0, field_names_1.writeName)(key)}`;
|
|
198
|
+
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.`);
|
|
111
199
|
}
|
|
112
200
|
function serializeSelectItems(items) {
|
|
113
201
|
if (items.length === 0)
|
|
@@ -115,7 +203,7 @@ function serializeSelectItems(items) {
|
|
|
115
203
|
return items
|
|
116
204
|
.map((item) => {
|
|
117
205
|
if (typeof item === "string") {
|
|
118
|
-
return item === "*" ? item :
|
|
206
|
+
return item === "*" ? item : selectItemName(item);
|
|
119
207
|
}
|
|
120
208
|
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
121
209
|
throw new TypeError("@capacms/sdk/next: each select item must be a field name or relation object.");
|
|
@@ -124,12 +212,12 @@ function serializeSelectItems(items) {
|
|
|
124
212
|
if (entries.length !== 1) {
|
|
125
213
|
throw new TypeError("@capacms/sdk/next: a relation select object must name exactly one field.");
|
|
126
214
|
}
|
|
127
|
-
const [
|
|
128
|
-
const
|
|
215
|
+
const [name, value] = entries[0];
|
|
216
|
+
const written = selectName(name, "relation select key");
|
|
129
217
|
if (value === "*")
|
|
130
|
-
return `${
|
|
218
|
+
return `${written}(*)`;
|
|
131
219
|
if (Array.isArray(value))
|
|
132
|
-
return `${
|
|
220
|
+
return `${written}(${serializeSelectItems(value)})`;
|
|
133
221
|
if (!value || typeof value !== "object") {
|
|
134
222
|
throw new TypeError(`@capacms/sdk/next: ${name} must select fields, "*", or select options.`);
|
|
135
223
|
}
|
|
@@ -154,19 +242,15 @@ function serializeSelectItems(items) {
|
|
|
154
242
|
}
|
|
155
243
|
args.push(`limit:${options.limit}`);
|
|
156
244
|
}
|
|
157
|
-
if (options.sort !== undefined)
|
|
158
|
-
|
|
159
|
-
throw new TypeError(`@capacms/sdk/next: ${name}.sort must be one field name.`);
|
|
160
|
-
}
|
|
161
|
-
args.push(`sort:${options.sort}`);
|
|
162
|
-
}
|
|
245
|
+
if (options.sort !== undefined)
|
|
246
|
+
args.push(`sort:${nestedSort(options.sort, name)}`);
|
|
163
247
|
if (options.after !== undefined) {
|
|
164
248
|
if (typeof options.after !== "string" || options.after === "") {
|
|
165
249
|
throw new TypeError(`@capacms/sdk/next: ${name}.after must be a non-empty cursor.`);
|
|
166
250
|
}
|
|
167
251
|
args.push(`after:${options.after}`);
|
|
168
252
|
}
|
|
169
|
-
return `${
|
|
253
|
+
return `${written}(${args.join(",")})`;
|
|
170
254
|
})
|
|
171
255
|
.join(",");
|
|
172
256
|
}
|
|
@@ -207,10 +291,21 @@ function filterValue(operator, value) {
|
|
|
207
291
|
}
|
|
208
292
|
return String(value);
|
|
209
293
|
}
|
|
294
|
+
/** `shape` is sent only when it is `flat`, so a tree read's URL is the one it always was. */
|
|
295
|
+
function shapeOf(options) {
|
|
296
|
+
const shape = options.shape;
|
|
297
|
+
if (shape === undefined || shape === "tree")
|
|
298
|
+
return undefined;
|
|
299
|
+
if (shape === "flat")
|
|
300
|
+
return "flat";
|
|
301
|
+
throw new TypeError(`@capacms/sdk/next: shape must be "tree" or "flat". Got ${JSON.stringify(shape)}.`);
|
|
302
|
+
}
|
|
210
303
|
function listQuery(options) {
|
|
211
304
|
const query = new URLSearchParams();
|
|
212
305
|
if (options.select !== undefined)
|
|
213
306
|
query.set("select", serializeSelect(options.select));
|
|
307
|
+
if (shapeOf(options) === "flat")
|
|
308
|
+
query.set("shape", "flat");
|
|
214
309
|
if (options.filter !== undefined) {
|
|
215
310
|
for (const [path, operations] of Object.entries(options.filter)) {
|
|
216
311
|
for (const [rawOperator, value] of Object.entries(operations)) {
|
|
@@ -238,45 +333,12 @@ function listQuery(options) {
|
|
|
238
333
|
query.set("before", options.before);
|
|
239
334
|
return query;
|
|
240
335
|
}
|
|
241
|
-
function optionalString(value) {
|
|
242
|
-
return typeof value === "string" ? value : undefined;
|
|
243
|
-
}
|
|
244
|
-
function unparseable(status, requestId) {
|
|
245
|
-
return new CapaError({
|
|
246
|
-
status,
|
|
247
|
-
type: "api_error",
|
|
248
|
-
code: "unparseable_response",
|
|
249
|
-
message: "Capa returned a response that was not a valid /api/ JSON envelope.",
|
|
250
|
-
requestId,
|
|
251
|
-
docs: "",
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
function errorFromEnvelope(status, body, fallbackRequestId) {
|
|
255
|
-
const detail = body.error;
|
|
256
|
-
if (!detail ||
|
|
257
|
-
typeof detail.type !== "string" ||
|
|
258
|
-
typeof detail.code !== "string" ||
|
|
259
|
-
typeof detail.message !== "string" ||
|
|
260
|
-
typeof detail.docs !== "string") {
|
|
261
|
-
return unparseable(status, fallbackRequestId);
|
|
262
|
-
}
|
|
263
|
-
return new CapaError({
|
|
264
|
-
status,
|
|
265
|
-
type: detail.type,
|
|
266
|
-
code: detail.code,
|
|
267
|
-
message: detail.message,
|
|
268
|
-
param: optionalString(detail.param),
|
|
269
|
-
hint: optionalString(detail.hint),
|
|
270
|
-
requestId: optionalString(body.meta?.requestId) ?? fallbackRequestId,
|
|
271
|
-
docs: detail.docs,
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
336
|
function cacheTags(response) {
|
|
275
337
|
const value = response.headers?.get?.("Surrogate-Key") ?? "";
|
|
276
338
|
return value.split(/\s+/).filter(Boolean);
|
|
277
339
|
}
|
|
278
340
|
function createRequester(config) {
|
|
279
|
-
return async function request(path, query = new URLSearchParams(), signal, page) {
|
|
341
|
+
return async function request(path, query = new URLSearchParams(), signal, page, pagePath) {
|
|
280
342
|
const url = new URL(config.baseUrl + path);
|
|
281
343
|
query.forEach((value, key) => url.searchParams.append(key, value));
|
|
282
344
|
const headers = {
|
|
@@ -290,6 +352,8 @@ function createRequester(config) {
|
|
|
290
352
|
// byte-identical requests to the ones it sent before this option existed.
|
|
291
353
|
if (page !== undefined)
|
|
292
354
|
headers["Capa-Page"] = page;
|
|
355
|
+
if (page !== undefined && pagePath !== undefined)
|
|
356
|
+
headers["Capa-Path"] = pagePath;
|
|
293
357
|
// Same rule, and on EVERY call rather than only the entries reads: the
|
|
294
358
|
// stamp describes the build, so a page whose only Capa call is `me()`
|
|
295
359
|
// still reports which schema it was generated from.
|
|
@@ -303,33 +367,82 @@ function createRequester(config) {
|
|
|
303
367
|
body = JSON.parse(text);
|
|
304
368
|
}
|
|
305
369
|
catch {
|
|
306
|
-
throw unparseable(response.status, requestId);
|
|
370
|
+
throw (0, errors_1.unparseable)(response.status, requestId);
|
|
371
|
+
}
|
|
372
|
+
if (!response.ok) {
|
|
373
|
+
throw (0, errors_1.errorFromEnvelope)(response.status, body, requestId, (0, errors_1.retryAfterOf)(response.headers?.get?.("Retry-After")));
|
|
307
374
|
}
|
|
308
|
-
if (!response.ok)
|
|
309
|
-
throw errorFromEnvelope(response.status, body, requestId);
|
|
310
375
|
if (!body || typeof body !== "object")
|
|
311
|
-
throw unparseable(response.status, requestId);
|
|
376
|
+
throw (0, errors_1.unparseable)(response.status, requestId);
|
|
377
|
+
if (config.editMode === true) {
|
|
378
|
+
// `included` holds a flat read's related entries, which are marked
|
|
379
|
+
// exactly as they are when the tree nests them inside `data`.
|
|
380
|
+
const { data, included } = body;
|
|
381
|
+
(0, attrs_1.markEditEntries)(data);
|
|
382
|
+
if (included !== undefined)
|
|
383
|
+
(0, attrs_1.markEditEntries)(included);
|
|
384
|
+
}
|
|
312
385
|
return { body: body, cacheTags: cacheTags(response) };
|
|
313
386
|
};
|
|
314
387
|
}
|
|
315
388
|
function createClient(config) {
|
|
316
389
|
const resolved = resolveNextConfig(config);
|
|
390
|
+
if ((0, key_family_1.keyFamily)(resolved.apiKey) === "legacy")
|
|
391
|
+
(0, key_family_1.warnLegacyKeyOnce)(resolved.apiKey);
|
|
317
392
|
const request = createRequester(resolved);
|
|
393
|
+
// By POST: introspection is never cached (G plan D16), and the admin host
|
|
394
|
+
// serves GraphQL by POST only.
|
|
395
|
+
const readSchema = async (signal) => {
|
|
396
|
+
const result = await (0, request_1.runGraphQL)(resolved, introspection_1.INTROSPECTION_QUERY, undefined, { signal, method: "POST" });
|
|
397
|
+
const requestId = result.extensions.capa?.requestId ?? "";
|
|
398
|
+
// The API answered 200, so the status says so; the errors say what failed.
|
|
399
|
+
if (result.errors.length > 0)
|
|
400
|
+
throw (0, errors_1.errorFromGraphQLErrors)(200, result.errors, requestId);
|
|
401
|
+
if (!result.data)
|
|
402
|
+
throw (0, errors_1.unparseable)(200, requestId);
|
|
403
|
+
return (0, summary_1.summarizeIntrospection)(result.data);
|
|
404
|
+
};
|
|
405
|
+
// By POST, as the schema above: it selects only `__schema`.
|
|
406
|
+
const readFieldNames = async () => {
|
|
407
|
+
const result = await (0, request_1.runGraphQL)(resolved, introspection_1.FIELD_NAMES_QUERY, undefined, { method: "POST" });
|
|
408
|
+
if (!result.data || result.errors.length > 0)
|
|
409
|
+
throw new Error("the key's field names could not be read");
|
|
410
|
+
return result.data;
|
|
411
|
+
};
|
|
412
|
+
/** One GraphQL read; in edit mode its entries are marked for `capaAttrs`, as REST's are. */
|
|
413
|
+
const readGraphQL = (document, variables, options) => {
|
|
414
|
+
const read = () => (0, request_1.runGraphQL)(resolved, document, variables, options);
|
|
415
|
+
if (resolved.editMode !== true)
|
|
416
|
+
return read();
|
|
417
|
+
return (0, edit_mode_1.readMarked)(document, `${resolved.baseUrl}\n${resolved.apiKey}\n${resolved.version}`, readFieldNames, read);
|
|
418
|
+
};
|
|
419
|
+
const graphql = graphqlClient(readGraphQL);
|
|
420
|
+
/**
|
|
421
|
+
* A flat read's result carries the select it sent, so `inflate(result)`
|
|
422
|
+
* needs nothing else. A tree read's result is exactly what it always was.
|
|
423
|
+
*/
|
|
424
|
+
const withSelect = (result, options) => {
|
|
425
|
+
if (shapeOf(options) !== "flat" || options.select === undefined)
|
|
426
|
+
return result;
|
|
427
|
+
return { ...result, select: serializeSelect(options.select) };
|
|
428
|
+
};
|
|
318
429
|
const entries = {
|
|
319
430
|
async list(namespace, options = {}) {
|
|
320
|
-
const result = await request(`/api/entries/${encodeURIComponent(namespace)}`, listQuery(options), options.signal, resolvePage(resolved.page, options.page));
|
|
321
|
-
return { ...result.body, cacheTags: result.cacheTags };
|
|
431
|
+
const result = await request(`/api/entries/${encodeURIComponent(namespace)}`, listQuery(options), options.signal, resolvePage(resolved.page, options.page), resolvePath(resolved.path, options.path));
|
|
432
|
+
return withSelect({ ...result.body, cacheTags: result.cacheTags }, options);
|
|
322
433
|
},
|
|
323
434
|
async get(namespace, id, options = {}) {
|
|
324
435
|
const query = new URLSearchParams();
|
|
325
436
|
if (options.select !== undefined)
|
|
326
437
|
query.set("select", serializeSelect(options.select));
|
|
438
|
+
if (shapeOf(options) === "flat")
|
|
439
|
+
query.set("shape", "flat");
|
|
327
440
|
try {
|
|
328
|
-
const result = await request(`/api/entries/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}`, query, options.signal, resolvePage(resolved.page, options.page));
|
|
329
|
-
return { ...result.body, cacheTags: result.cacheTags };
|
|
441
|
+
const result = await request(`/api/entries/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}`, query, options.signal, resolvePage(resolved.page, options.page), resolvePath(resolved.path, options.path));
|
|
442
|
+
return withSelect({ ...result.body, cacheTags: result.cacheTags }, options);
|
|
330
443
|
}
|
|
331
444
|
catch (error) {
|
|
332
|
-
if (error instanceof CapaError &&
|
|
445
|
+
if (error instanceof errors_1.CapaError &&
|
|
333
446
|
error.status === 404 &&
|
|
334
447
|
error.type === "not_found" &&
|
|
335
448
|
error.code === "entry_not_found") {
|
|
@@ -339,6 +452,9 @@ function createClient(config) {
|
|
|
339
452
|
}
|
|
340
453
|
},
|
|
341
454
|
async *iterate(namespace, options = {}) {
|
|
455
|
+
if (shapeOf(options) === "flat") {
|
|
456
|
+
throw new TypeError("@capacms/sdk/next: iterate reads the tree shape only. Use list with shape: \"flat\" and page.next.");
|
|
457
|
+
}
|
|
342
458
|
let after = options.after;
|
|
343
459
|
for (;;) {
|
|
344
460
|
const page = await entries.list(namespace, { ...options, after });
|
|
@@ -368,7 +484,7 @@ function createClient(config) {
|
|
|
368
484
|
// Same shape as `entries.get`: a page nobody has declared or read is a
|
|
369
485
|
// null, not a throw, because "is this page known to Capa" is a question
|
|
370
486
|
// a caller asks on purpose.
|
|
371
|
-
if (error instanceof CapaError &&
|
|
487
|
+
if (error instanceof errors_1.CapaError &&
|
|
372
488
|
error.status === 404 &&
|
|
373
489
|
error.code === "page_not_found") {
|
|
374
490
|
return null;
|
|
@@ -380,7 +496,13 @@ function createClient(config) {
|
|
|
380
496
|
return {
|
|
381
497
|
entries,
|
|
382
498
|
pages,
|
|
499
|
+
graphql,
|
|
500
|
+
graphqlSchema(options = {}) {
|
|
501
|
+
return readSchema(options.signal);
|
|
502
|
+
},
|
|
383
503
|
async preview(token, options = {}) {
|
|
504
|
+
// Any key the site holds, legacy included: the API checks the token
|
|
505
|
+
// belongs to the key's own tenant (see key-family.ts).
|
|
384
506
|
const query = new URLSearchParams();
|
|
385
507
|
query.set("token", token);
|
|
386
508
|
try {
|
|
@@ -391,7 +513,7 @@ function createClient(config) {
|
|
|
391
513
|
// preview route has the same fallback for either: render the published
|
|
392
514
|
// page. Anything else (a 500, a network failure, a missing scope) is a
|
|
393
515
|
// real problem and must not be mistaken for a stale link.
|
|
394
|
-
if (error instanceof CapaError &&
|
|
516
|
+
if (error instanceof errors_1.CapaError &&
|
|
395
517
|
(error.code === "preview_token_invalid" || error.code === "preview_token_expired")) {
|
|
396
518
|
return null;
|
|
397
519
|
}
|
|
@@ -399,10 +521,10 @@ function createClient(config) {
|
|
|
399
521
|
}
|
|
400
522
|
},
|
|
401
523
|
async me(options = {}) {
|
|
402
|
-
return (await request("/api/me", new URLSearchParams(), options.signal, resolvePage(resolved.page, options.page))).body;
|
|
524
|
+
return (await request("/api/me", new URLSearchParams(), options.signal, resolvePage(resolved.page, options.page), resolvePath(resolved.path, options.path))).body;
|
|
403
525
|
},
|
|
404
526
|
async versions(options = {}) {
|
|
405
|
-
return (await request("/api/versions", new URLSearchParams(), options.signal, resolvePage(resolved.page, options.page))).body;
|
|
527
|
+
return (await request("/api/versions", new URLSearchParams(), options.signal, resolvePage(resolved.page, options.page), resolvePath(resolved.path, options.path))).body;
|
|
406
528
|
},
|
|
407
529
|
};
|
|
408
530
|
}
|
|
@@ -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
|
+
}
|