@capacms/sdk 1.0.0-next.3 → 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.
Files changed (68) hide show
  1. package/CHANGELOG.md +323 -0
  2. package/README.md +1163 -186
  3. package/bin/capa-codegen.js +192 -5
  4. package/bin/capa.js +208 -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 -1
  12. package/dist/graphql-codegen.d.ts +117 -0
  13. package/dist/graphql-codegen.js +705 -0
  14. package/dist/http.js +1 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +2 -1
  17. package/dist/next/attrs.d.ts +84 -10
  18. package/dist/next/attrs.js +119 -2
  19. package/dist/next/client.d.ts +160 -31
  20. package/dist/next/client.js +178 -89
  21. package/dist/next/entry-fields.d.ts +162 -0
  22. package/dist/next/entry-fields.js +2 -0
  23. package/dist/next/errors.d.ts +136 -0
  24. package/dist/next/errors.js +214 -0
  25. package/dist/next/field-names.d.ts +37 -0
  26. package/dist/next/field-names.js +145 -0
  27. package/dist/next/graphql/build.d.ts +27 -0
  28. package/dist/next/graphql/build.js +98 -0
  29. package/dist/next/graphql/documents.d.ts +67 -0
  30. package/dist/next/graphql/documents.js +35 -0
  31. package/dist/next/graphql/edit-mode.d.ts +16 -0
  32. package/dist/next/graphql/edit-mode.js +93 -0
  33. package/dist/next/graphql/filter-values.d.ts +34 -0
  34. package/dist/next/graphql/filter-values.js +96 -0
  35. package/dist/next/graphql/introspection.d.ts +89 -0
  36. package/dist/next/graphql/introspection.js +102 -0
  37. package/dist/next/graphql/plan.d.ts +115 -0
  38. package/dist/next/graphql/plan.js +531 -0
  39. package/dist/next/graphql/request.d.ts +228 -0
  40. package/dist/next/graphql/request.js +283 -0
  41. package/dist/next/graphql/rest.d.ts +66 -0
  42. package/dist/next/graphql/rest.js +502 -0
  43. package/dist/next/graphql/selection.d.ts +55 -0
  44. package/dist/next/graphql/selection.js +212 -0
  45. package/dist/next/graphql/sha256.d.ts +13 -0
  46. package/dist/next/graphql/sha256.js +86 -0
  47. package/dist/next/graphql/summary.d.ts +83 -0
  48. package/dist/next/graphql/summary.js +151 -0
  49. package/dist/next/graphql/tree-layout.d.ts +36 -0
  50. package/dist/next/graphql/tree-layout.js +20 -0
  51. package/dist/next/graphql/tree.d.ts +171 -0
  52. package/dist/next/graphql/tree.js +249 -0
  53. package/dist/next/graphql/typed.d.ts +261 -0
  54. package/dist/next/graphql/typed.js +146 -0
  55. package/dist/next/index.d.ts +29 -4
  56. package/dist/next/index.js +31 -1
  57. package/dist/next/inflate.d.ts +51 -0
  58. package/dist/next/inflate.js +243 -0
  59. package/dist/next/key-family.d.ts +34 -0
  60. package/dist/next/key-family.js +74 -0
  61. package/dist/next/select-types.d.ts +58 -5
  62. package/dist/next/system-keys.d.ts +27 -0
  63. package/dist/next/system-keys.js +42 -0
  64. package/dist/nextjs/index.d.ts +333 -6
  65. package/dist/nextjs/index.js +450 -4
  66. package/dist/nextjs/overlay.d.ts +5 -0
  67. package/dist/nextjs/overlay.js +35 -0
  68. package/package.json +35 -13
@@ -1,10 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LAYOUT_PAGE = exports.CapaError = void 0;
4
- exports.isCapaError = isCapaError;
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; } });
8
21
  /**
9
22
  * What a `Capa-Schema` value may look like: hex, 8 to 64 characters.
10
23
  *
@@ -16,29 +29,23 @@ exports.createClient = createClient;
16
29
  * appears rather than as an error.
17
30
  */
18
31
  const SCHEMA_CHECKSUM = /^[a-f0-9]{8,64}$/;
19
- class CapaError extends Error {
20
- status;
21
- type;
22
- code;
23
- param;
24
- hint;
25
- requestId;
26
- docs;
27
- constructor(input) {
28
- super(input.message);
29
- this.name = "CapaError";
30
- this.status = input.status;
31
- this.type = input.type;
32
- this.code = input.code;
33
- this.param = input.param;
34
- this.hint = input.hint;
35
- this.requestId = input.requestId;
36
- this.docs = input.docs;
37
- }
38
- }
39
- exports.CapaError = CapaError;
40
- function isCapaError(error) {
41
- 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 });
42
49
  }
43
50
  function resolveNextConfig(config) {
44
51
  const value = (config ?? {});
@@ -47,8 +54,9 @@ function resolveNextConfig(config) {
47
54
  throw new Error(`@capacms/sdk/next: missing ${missing.join(", ")}. ` +
48
55
  "createClient needs baseUrl, apiKey and version.");
49
56
  }
50
- if (!value.apiKey.startsWith("cap_")) {
51
- throw new Error("@capacms/sdk/next: apiKey must start with cap_. Use the legacy @capacms/sdk client for pk_ and sk_ keys.");
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.");
52
60
  }
53
61
  if (value.contract !== undefined && value.contract !== 1) {
54
62
  throw new Error("@capacms/sdk/next: contract must be 1 when provided.");
@@ -60,6 +68,7 @@ function resolveNextConfig(config) {
60
68
  // Checked here as well as per call, so a bad page on the config throws where
61
69
  // the client is built rather than on whichever read happens to run first.
62
70
  resolvePage(value.page, undefined);
71
+ resolvePath(value.path, undefined);
63
72
  // THROWS rather than dropping, the same rule `page` follows and for the same
64
73
  // reason: the API must never 400 a running site over a telemetry header, so
65
74
  // it ignores what it cannot store, and the SDK is the place a wrong value
@@ -120,12 +129,59 @@ function resolvePage(configPage, callPage) {
120
129
  }
121
130
  return value;
122
131
  }
123
- const NAME = /^[A-Za-z0-9_-]+$/;
132
+ /**
133
+ * What a `Capa-Path` value may look like: a site path, query and hash cut off.
134
+ * COPIED from `PAGE_PATH_PATTERN` in `apps/api/src/api-next/page-header.ts`.
135
+ */
136
+ const PAGE_PATH = /^\/[^\s?#]{0,1023}$/;
137
+ function resolvePath(configPath, callPath) {
138
+ const value = callPath !== undefined ? callPath : configPath;
139
+ if (value === undefined || value === null)
140
+ return undefined;
141
+ if (typeof value !== "string") {
142
+ throw new TypeError(`@capacms/sdk/next: path must be a string such as "/blog/hello".`);
143
+ }
144
+ const bare = value.split("#")[0].split("?")[0];
145
+ if (!PAGE_PATH.test(bare)) {
146
+ throw new TypeError(`@capacms/sdk/next: path must be the concrete path being rendered, such as "/blog/hello". Got ${JSON.stringify(value)}.`);
147
+ }
148
+ return bare;
149
+ }
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. */
124
158
  function selectName(value, context) {
125
- if (typeof value !== "string" || !NAME.test(value)) {
159
+ if (typeof value !== "string" || !(0, field_names_1.isNameable)(value)) {
126
160
  throw new TypeError(`@capacms/sdk/next: ${context} must be a field name.`);
127
161
  }
128
- 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.`);
129
185
  }
130
186
  function serializeSelectItems(items) {
131
187
  if (items.length === 0)
@@ -133,7 +189,7 @@ function serializeSelectItems(items) {
133
189
  return items
134
190
  .map((item) => {
135
191
  if (typeof item === "string") {
136
- return item === "*" ? item : selectName(item, "select item");
192
+ return item === "*" ? item : selectItemName(item);
137
193
  }
138
194
  if (!item || typeof item !== "object" || Array.isArray(item)) {
139
195
  throw new TypeError("@capacms/sdk/next: each select item must be a field name or relation object.");
@@ -142,12 +198,12 @@ function serializeSelectItems(items) {
142
198
  if (entries.length !== 1) {
143
199
  throw new TypeError("@capacms/sdk/next: a relation select object must name exactly one field.");
144
200
  }
145
- const [rawName, value] = entries[0];
146
- const name = selectName(rawName, "relation select key");
201
+ const [name, value] = entries[0];
202
+ const written = selectName(name, "relation select key");
147
203
  if (value === "*")
148
- return `${name}(*)`;
204
+ return `${written}(*)`;
149
205
  if (Array.isArray(value))
150
- return `${name}(${serializeSelectItems(value)})`;
206
+ return `${written}(${serializeSelectItems(value)})`;
151
207
  if (!value || typeof value !== "object") {
152
208
  throw new TypeError(`@capacms/sdk/next: ${name} must select fields, "*", or select options.`);
153
209
  }
@@ -172,19 +228,15 @@ function serializeSelectItems(items) {
172
228
  }
173
229
  args.push(`limit:${options.limit}`);
174
230
  }
175
- if (options.sort !== undefined) {
176
- if (typeof options.sort !== "string" || !/^-?[A-Za-z0-9_-]+$/.test(options.sort)) {
177
- throw new TypeError(`@capacms/sdk/next: ${name}.sort must be one field name.`);
178
- }
179
- args.push(`sort:${options.sort}`);
180
- }
231
+ if (options.sort !== undefined)
232
+ args.push(`sort:${nestedSort(options.sort, name)}`);
181
233
  if (options.after !== undefined) {
182
234
  if (typeof options.after !== "string" || options.after === "") {
183
235
  throw new TypeError(`@capacms/sdk/next: ${name}.after must be a non-empty cursor.`);
184
236
  }
185
237
  args.push(`after:${options.after}`);
186
238
  }
187
- return `${name}(${args.join(",")})`;
239
+ return `${written}(${args.join(",")})`;
188
240
  })
189
241
  .join(",");
190
242
  }
@@ -225,10 +277,21 @@ function filterValue(operator, value) {
225
277
  }
226
278
  return String(value);
227
279
  }
280
+ /** `shape` is sent only when it is `flat`, so a tree read's URL is the one it always was. */
281
+ function shapeOf(options) {
282
+ const shape = options.shape;
283
+ if (shape === undefined || shape === "tree")
284
+ return undefined;
285
+ if (shape === "flat")
286
+ return "flat";
287
+ throw new TypeError(`@capacms/sdk/next: shape must be "tree" or "flat". Got ${JSON.stringify(shape)}.`);
288
+ }
228
289
  function listQuery(options) {
229
290
  const query = new URLSearchParams();
230
291
  if (options.select !== undefined)
231
292
  query.set("select", serializeSelect(options.select));
293
+ if (shapeOf(options) === "flat")
294
+ query.set("shape", "flat");
232
295
  if (options.filter !== undefined) {
233
296
  for (const [path, operations] of Object.entries(options.filter)) {
234
297
  for (const [rawOperator, value] of Object.entries(operations)) {
@@ -256,45 +319,12 @@ function listQuery(options) {
256
319
  query.set("before", options.before);
257
320
  return query;
258
321
  }
259
- function optionalString(value) {
260
- return typeof value === "string" ? value : undefined;
261
- }
262
- function unparseable(status, requestId) {
263
- return new CapaError({
264
- status,
265
- type: "api_error",
266
- code: "unparseable_response",
267
- message: "Capa returned a response that was not a valid /api/ JSON envelope.",
268
- requestId,
269
- docs: "",
270
- });
271
- }
272
- function errorFromEnvelope(status, body, fallbackRequestId) {
273
- const detail = body.error;
274
- if (!detail ||
275
- typeof detail.type !== "string" ||
276
- typeof detail.code !== "string" ||
277
- typeof detail.message !== "string" ||
278
- typeof detail.docs !== "string") {
279
- return unparseable(status, fallbackRequestId);
280
- }
281
- return new CapaError({
282
- status,
283
- type: detail.type,
284
- code: detail.code,
285
- message: detail.message,
286
- param: optionalString(detail.param),
287
- hint: optionalString(detail.hint),
288
- requestId: optionalString(body.meta?.requestId) ?? fallbackRequestId,
289
- docs: detail.docs,
290
- });
291
- }
292
322
  function cacheTags(response) {
293
323
  const value = response.headers?.get?.("Surrogate-Key") ?? "";
294
324
  return value.split(/\s+/).filter(Boolean);
295
325
  }
296
326
  function createRequester(config) {
297
- return async function request(path, query = new URLSearchParams(), signal, page) {
327
+ return async function request(path, query = new URLSearchParams(), signal, page, pagePath) {
298
328
  const url = new URL(config.baseUrl + path);
299
329
  query.forEach((value, key) => url.searchParams.append(key, value));
300
330
  const headers = {
@@ -308,6 +338,8 @@ function createRequester(config) {
308
338
  // byte-identical requests to the ones it sent before this option existed.
309
339
  if (page !== undefined)
310
340
  headers["Capa-Page"] = page;
341
+ if (page !== undefined && pagePath !== undefined)
342
+ headers["Capa-Path"] = pagePath;
311
343
  // Same rule, and on EVERY call rather than only the entries reads: the
312
344
  // stamp describes the build, so a page whose only Capa call is `me()`
313
345
  // still reports which schema it was generated from.
@@ -321,33 +353,82 @@ function createRequester(config) {
321
353
  body = JSON.parse(text);
322
354
  }
323
355
  catch {
324
- 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")));
325
360
  }
326
- if (!response.ok)
327
- throw errorFromEnvelope(response.status, body, requestId);
328
361
  if (!body || typeof body !== "object")
329
- throw unparseable(response.status, requestId);
362
+ throw (0, errors_1.unparseable)(response.status, requestId);
363
+ if (config.editMode === true) {
364
+ // `included` holds a flat read's related entries, which are marked
365
+ // exactly as they are when the tree nests them inside `data`.
366
+ const { data, included } = body;
367
+ (0, attrs_1.markEditEntries)(data);
368
+ if (included !== undefined)
369
+ (0, attrs_1.markEditEntries)(included);
370
+ }
330
371
  return { body: body, cacheTags: cacheTags(response) };
331
372
  };
332
373
  }
333
374
  function createClient(config) {
334
375
  const resolved = resolveNextConfig(config);
376
+ if ((0, key_family_1.keyFamily)(resolved.apiKey) === "legacy")
377
+ (0, key_family_1.warnLegacyKeyOnce)(resolved.apiKey);
335
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);
406
+ /**
407
+ * A flat read's result carries the select it sent, so `inflate(result)`
408
+ * needs nothing else. A tree read's result is exactly what it always was.
409
+ */
410
+ const withSelect = (result, options) => {
411
+ if (shapeOf(options) !== "flat" || options.select === undefined)
412
+ return result;
413
+ return { ...result, select: serializeSelect(options.select) };
414
+ };
336
415
  const entries = {
337
416
  async list(namespace, options = {}) {
338
- const result = await request(`/api/entries/${encodeURIComponent(namespace)}`, listQuery(options), options.signal, resolvePage(resolved.page, options.page));
339
- return { ...result.body, cacheTags: result.cacheTags };
417
+ const result = await request(`/api/entries/${encodeURIComponent(namespace)}`, listQuery(options), options.signal, resolvePage(resolved.page, options.page), resolvePath(resolved.path, options.path));
418
+ return withSelect({ ...result.body, cacheTags: result.cacheTags }, options);
340
419
  },
341
420
  async get(namespace, id, options = {}) {
342
421
  const query = new URLSearchParams();
343
422
  if (options.select !== undefined)
344
423
  query.set("select", serializeSelect(options.select));
424
+ if (shapeOf(options) === "flat")
425
+ query.set("shape", "flat");
345
426
  try {
346
- const result = await request(`/api/entries/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}`, query, options.signal, resolvePage(resolved.page, options.page));
347
- return { ...result.body, cacheTags: result.cacheTags };
427
+ const result = await request(`/api/entries/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}`, query, options.signal, resolvePage(resolved.page, options.page), resolvePath(resolved.path, options.path));
428
+ return withSelect({ ...result.body, cacheTags: result.cacheTags }, options);
348
429
  }
349
430
  catch (error) {
350
- if (error instanceof CapaError &&
431
+ if (error instanceof errors_1.CapaError &&
351
432
  error.status === 404 &&
352
433
  error.type === "not_found" &&
353
434
  error.code === "entry_not_found") {
@@ -357,6 +438,9 @@ function createClient(config) {
357
438
  }
358
439
  },
359
440
  async *iterate(namespace, options = {}) {
441
+ if (shapeOf(options) === "flat") {
442
+ throw new TypeError("@capacms/sdk/next: iterate reads the tree shape only. Use list with shape: \"flat\" and page.next.");
443
+ }
360
444
  let after = options.after;
361
445
  for (;;) {
362
446
  const page = await entries.list(namespace, { ...options, after });
@@ -386,7 +470,7 @@ function createClient(config) {
386
470
  // Same shape as `entries.get`: a page nobody has declared or read is a
387
471
  // null, not a throw, because "is this page known to Capa" is a question
388
472
  // a caller asks on purpose.
389
- if (error instanceof CapaError &&
473
+ if (error instanceof errors_1.CapaError &&
390
474
  error.status === 404 &&
391
475
  error.code === "page_not_found") {
392
476
  return null;
@@ -398,7 +482,12 @@ function createClient(config) {
398
482
  return {
399
483
  entries,
400
484
  pages,
485
+ graphql,
486
+ graphqlSchema(options = {}) {
487
+ return readSchema(options.signal);
488
+ },
401
489
  async preview(token, options = {}) {
490
+ (0, key_family_1.requirePreviewKey)(resolved.apiKey);
402
491
  const query = new URLSearchParams();
403
492
  query.set("token", token);
404
493
  try {
@@ -409,7 +498,7 @@ function createClient(config) {
409
498
  // preview route has the same fallback for either: render the published
410
499
  // page. Anything else (a 500, a network failure, a missing scope) is a
411
500
  // real problem and must not be mistaken for a stale link.
412
- if (error instanceof CapaError &&
501
+ if (error instanceof errors_1.CapaError &&
413
502
  (error.code === "preview_token_invalid" || error.code === "preview_token_expired")) {
414
503
  return null;
415
504
  }
@@ -417,10 +506,10 @@ function createClient(config) {
417
506
  }
418
507
  },
419
508
  async me(options = {}) {
420
- return (await request("/api/me", new URLSearchParams(), options.signal, resolvePage(resolved.page, options.page))).body;
509
+ return (await request("/api/me", new URLSearchParams(), options.signal, resolvePage(resolved.page, options.page), resolvePath(resolved.path, options.path))).body;
421
510
  },
422
511
  async versions(options = {}) {
423
- return (await request("/api/versions", new URLSearchParams(), options.signal, resolvePage(resolved.page, options.page))).body;
512
+ return (await request("/api/versions", new URLSearchParams(), options.signal, resolvePage(resolved.page, options.page), resolvePath(resolved.path, options.path))).body;
424
513
  },
425
514
  };
426
515
  }
@@ -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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });