@capacms/sdk 1.0.0-next.4 → 1.0.0-next.7

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 (69) hide show
  1. package/CHANGELOG.md +345 -0
  2. package/README.md +1069 -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 -36
  12. package/dist/config.js +47 -1
  13. package/dist/graphql-codegen.d.ts +117 -0
  14. package/dist/graphql-codegen.js +705 -0
  15. package/dist/http.js +1 -1
  16. package/dist/index.d.ts +2 -2
  17. package/dist/index.js +2 -1
  18. package/dist/next/attrs.d.ts +51 -12
  19. package/dist/next/attrs.js +74 -20
  20. package/dist/next/client.d.ts +112 -38
  21. package/dist/next/client.js +131 -83
  22. package/dist/next/entry-fields.d.ts +162 -0
  23. package/dist/next/entry-fields.js +2 -0
  24. package/dist/next/errors.d.ts +136 -0
  25. package/dist/next/errors.js +214 -0
  26. package/dist/next/field-names.d.ts +37 -0
  27. package/dist/next/field-names.js +145 -0
  28. package/dist/next/graphql/build.d.ts +27 -0
  29. package/dist/next/graphql/build.js +98 -0
  30. package/dist/next/graphql/documents.d.ts +67 -0
  31. package/dist/next/graphql/documents.js +35 -0
  32. package/dist/next/graphql/edit-mode.d.ts +16 -0
  33. package/dist/next/graphql/edit-mode.js +93 -0
  34. package/dist/next/graphql/filter-values.d.ts +34 -0
  35. package/dist/next/graphql/filter-values.js +96 -0
  36. package/dist/next/graphql/introspection.d.ts +89 -0
  37. package/dist/next/graphql/introspection.js +102 -0
  38. package/dist/next/graphql/plan.d.ts +115 -0
  39. package/dist/next/graphql/plan.js +531 -0
  40. package/dist/next/graphql/request.d.ts +228 -0
  41. package/dist/next/graphql/request.js +283 -0
  42. package/dist/next/graphql/rest.d.ts +66 -0
  43. package/dist/next/graphql/rest.js +502 -0
  44. package/dist/next/graphql/selection.d.ts +55 -0
  45. package/dist/next/graphql/selection.js +212 -0
  46. package/dist/next/graphql/sha256.d.ts +13 -0
  47. package/dist/next/graphql/sha256.js +86 -0
  48. package/dist/next/graphql/summary.d.ts +83 -0
  49. package/dist/next/graphql/summary.js +151 -0
  50. package/dist/next/graphql/tree-layout.d.ts +36 -0
  51. package/dist/next/graphql/tree-layout.js +20 -0
  52. package/dist/next/graphql/tree.d.ts +171 -0
  53. package/dist/next/graphql/tree.js +249 -0
  54. package/dist/next/graphql/typed.d.ts +261 -0
  55. package/dist/next/graphql/typed.js +146 -0
  56. package/dist/next/index.d.ts +28 -5
  57. package/dist/next/index.js +25 -1
  58. package/dist/next/inflate.d.ts +25 -7
  59. package/dist/next/inflate.js +46 -32
  60. package/dist/next/key-family.d.ts +34 -0
  61. package/dist/next/key-family.js +74 -0
  62. package/dist/next/select-types.d.ts +44 -8
  63. package/dist/next/system-keys.d.ts +27 -0
  64. package/dist/next/system-keys.js +42 -0
  65. package/dist/nextjs/index.d.ts +174 -12
  66. package/dist/nextjs/index.js +270 -23
  67. package/dist/nextjs/overlay.d.ts +5 -0
  68. package/dist/nextjs/overlay.js +35 -0
  69. package/package.json +31 -13
@@ -34,39 +34,49 @@ exports.inflate = inflate;
34
34
  * cannot loop. A reference the select did not expand stays a reference.
35
35
  */
36
36
  const attrs_1 = require("./attrs");
37
- /** The system keys, in the order the API prints them. */
38
- const SYSTEM_KEYS = [
39
- "id",
40
- "model",
41
- "status",
42
- "createdAt",
43
- "updatedAt",
44
- "publishedAt",
45
- "version",
46
- "folder",
47
- "tags",
48
- ];
37
+ const system_keys_1 = require("./system-keys");
38
+ const field_names_1 = require("./field-names");
49
39
  const ALWAYS_KEYS = new Set(["id", "model", "status"]);
50
- const SYSTEM_KEY_SET = new Set(SYSTEM_KEYS);
51
40
  /**
52
- * The select grammar, read only as far as `inflate` needs it: names, nesting
53
- * and `*`. Modifiers (`limit:`, `sort:`, `after:`) decide which rows the API
54
- * returned, which the response already reflects, so they are skipped. The API
55
- * validated the select before answering, so this parser trusts its shape and
56
- * throws a `TypeError` only on text it cannot split at all.
41
+ * The select grammar, read only as far as `inflate` needs it: names, bare or
42
+ * quoted (`"price.usd"`, spec 17 amendment 121), nesting and `*`. Modifiers
43
+ * (`limit:`, `sort:`, `after:`) decide which rows the API returned, which the
44
+ * response already reflects, so they are skipped. The API validated the
45
+ * select before answering, so this parser trusts its shape and throws a
46
+ * `TypeError` only on text it cannot split at all.
57
47
  */
58
48
  function parseSelectLevels(select) {
59
49
  let pos = 0;
60
50
  const fail = () => {
61
51
  throw new TypeError(`@capacms/sdk/next: inflate could not read the select ${JSON.stringify(select)}.`);
62
52
  };
53
+ /** The quoted name at `pos`, read back, with `pos` moved past it. */
54
+ const quotedAt = () => {
55
+ const read = (0, field_names_1.readQuoted)(select, pos);
56
+ if (!read)
57
+ return fail();
58
+ pos = read.end;
59
+ return read.name;
60
+ };
63
61
  const level = () => {
64
62
  const out = { star: false, items: [] };
65
63
  for (;;) {
66
- const start = pos;
67
- while (pos < select.length && !",()".includes(select[pos]))
68
- pos += 1;
69
- const token = select.slice(start, pos);
64
+ const quoted = select[pos] === field_names_1.NAME_QUOTE;
65
+ let token = "";
66
+ if (quoted) {
67
+ token = quotedAt();
68
+ }
69
+ else {
70
+ const start = pos;
71
+ // A modifier's value may quote a name too: `sort:-"zip.code"`.
72
+ while (pos < select.length && !",()".includes(select[pos])) {
73
+ if (select[pos] === field_names_1.NAME_QUOTE)
74
+ quotedAt();
75
+ else
76
+ pos += 1;
77
+ }
78
+ token = select.slice(start, pos);
79
+ }
70
80
  if (select[pos] === "(") {
71
81
  if (token === "")
72
82
  fail();
@@ -75,16 +85,16 @@ function parseSelectLevels(select) {
75
85
  if (select[pos] !== ")")
76
86
  fail();
77
87
  pos += 1;
78
- out.items.push({ name: token, expand: inner });
88
+ out.items.push({ name: token, expand: inner, ...(quoted ? { quoted } : {}) });
79
89
  }
80
- else if (token === "*") {
90
+ else if (!quoted && token === "*") {
81
91
  out.star = true;
82
92
  }
83
- else if (token.includes(":")) {
93
+ else if (!quoted && token.includes(":")) {
84
94
  // A modifier: `limit:2`, `sort:-name`, `after:<cursor>`.
85
95
  }
86
- else if (token !== "") {
87
- out.items.push({ name: token, expand: null });
96
+ else if (quoted || token !== "") {
97
+ out.items.push({ name: token, expand: null, ...(quoted ? { quoted } : {}) });
88
98
  }
89
99
  else {
90
100
  fail();
@@ -133,18 +143,22 @@ function resolve(index, ref) {
133
143
  function project(entry, level, index) {
134
144
  const fields = (entry.fields ?? {});
135
145
  const all = level === null || level.star;
136
- // A name is a FIELD when the entry has a field of that name, otherwise a
137
- // system key: a model field shadows a system key of the same name (spec
138
- // 3.3), and a path that selected the field is exactly what put it here.
146
+ // `$tags` is always the system key (spec 17, amendment 29). A plain name is
147
+ // a FIELD when the entry has a field of that name, otherwise a system key:
148
+ // a model field shadows a system key of the same name (spec 3.3), and a
149
+ // path that selected the field is exactly what put it here.
139
150
  const wanted = new Set(ALWAYS_KEYS);
140
151
  if (level) {
141
152
  for (const item of level.items) {
142
- if (!(item.name in fields) && SYSTEM_KEY_SET.has(item.name))
153
+ const system = (0, system_keys_1.sigilSystemKey)(item.name);
154
+ if (system !== null)
155
+ wanted.add(system);
156
+ else if (!item.quoted && !(item.name in fields) && (0, system_keys_1.isSystemKey)(item.name))
143
157
  wanted.add(item.name);
144
158
  }
145
159
  }
146
160
  const out = {};
147
- for (const key of SYSTEM_KEYS) {
161
+ for (const key of system_keys_1.SYSTEM_KEYS) {
148
162
  if (!(key in entry))
149
163
  continue;
150
164
  if (all || wanted.has(key))
@@ -0,0 +1,34 @@
1
+ /**
2
+ * key-family.ts — which keys `@capacms/sdk/next` takes, and for what.
3
+ *
4
+ * A `cap_` key is the `/api/` key: scoped, hashed at rest, and the only kind
5
+ * a draft preview takes, both to verify a preview token and to read drafts.
6
+ * Every other key is legacy, which is the API's rule
7
+ * (`apps/api/src/api-next/auth/key-format.ts`): the `pk_` and `sk_` keys, and
8
+ * the unprefixed keys older tenants were minted, all reach `/api/` with the
9
+ * grants their permission gives. So every read call takes them, and the first
10
+ * client built with one warns once per process, naming the key to mint.
11
+ *
12
+ * The `cap_` prefix is the whole rule, case-sensitive, as on the API:
13
+ * `CAP_live_…` is not a key Capa mints, so it is looked up as a legacy key.
14
+ * `@capa/mcp` applies the same rule, and both packages are tested against one
15
+ * vector file, `test/fixtures/key-family.json`.
16
+ */
17
+ export type KeyFamily = "cap" | "legacy";
18
+ /** The family of `apiKey`, or null for a value that is no key at all: not a string, or empty. */
19
+ export declare function keyFamily(apiKey: unknown): KeyFamily | null;
20
+ /** Warn about a legacy key once per process: a site builds a client per request. */
21
+ export declare function warnLegacyKeyOnce(apiKey: string): void;
22
+ /**
23
+ * Throws for a legacy key: verifying a preview token takes a `cap_` key. A
24
+ * value that is no key at all is `resolveNextConfig`'s to refuse, by name.
25
+ */
26
+ export declare function requirePreviewKey(apiKey: string): void;
27
+ /**
28
+ * Throws for a legacy key: a draft read takes a `cap_` key. `holder` names
29
+ * where the key came from (`CAPA_DRAFT_KEY`, `the draft config`), so the
30
+ * message says what to change.
31
+ */
32
+ export declare function requireDraftKey(apiKey: string, holder: string): void;
33
+ /** Forget that the warning was printed. For tests. */
34
+ export declare function __resetKeyWarningForTests(): void;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ /**
3
+ * key-family.ts — which keys `@capacms/sdk/next` takes, and for what.
4
+ *
5
+ * A `cap_` key is the `/api/` key: scoped, hashed at rest, and the only kind
6
+ * a draft preview takes, both to verify a preview token and to read drafts.
7
+ * Every other key is legacy, which is the API's rule
8
+ * (`apps/api/src/api-next/auth/key-format.ts`): the `pk_` and `sk_` keys, and
9
+ * the unprefixed keys older tenants were minted, all reach `/api/` with the
10
+ * grants their permission gives. So every read call takes them, and the first
11
+ * client built with one warns once per process, naming the key to mint.
12
+ *
13
+ * The `cap_` prefix is the whole rule, case-sensitive, as on the API:
14
+ * `CAP_live_…` is not a key Capa mints, so it is looked up as a legacy key.
15
+ * `@capa/mcp` applies the same rule, and both packages are tested against one
16
+ * vector file, `test/fixtures/key-family.json`.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.keyFamily = keyFamily;
20
+ exports.warnLegacyKeyOnce = warnLegacyKeyOnce;
21
+ exports.requirePreviewKey = requirePreviewKey;
22
+ exports.requireDraftKey = requireDraftKey;
23
+ exports.__resetKeyWarningForTests = __resetKeyWarningForTests;
24
+ const LEGACY_PREFIX = /^(pk|sk)_/;
25
+ /** The family of `apiKey`, or null for a value that is no key at all: not a string, or empty. */
26
+ function keyFamily(apiKey) {
27
+ if (typeof apiKey !== "string" || apiKey === "")
28
+ return null;
29
+ return apiKey.startsWith("cap_") ? "cap" : "legacy";
30
+ }
31
+ function isLegacy(apiKey) {
32
+ return keyFamily(apiKey) === "legacy";
33
+ }
34
+ /**
35
+ * How a message names a legacy key: by its `pk_` or `sk_` prefix, and an
36
+ * unprefixed key by that fact alone, since every other character is the secret.
37
+ */
38
+ function legacyKind(apiKey) {
39
+ const prefix = LEGACY_PREFIX.exec(apiKey)?.[0];
40
+ return prefix ? `legacy ${prefix} key` : "legacy key with no pk_ or sk_ prefix";
41
+ }
42
+ const MINT = "in the Capa admin under Developers > Keys.";
43
+ let warned = false;
44
+ /** Warn about a legacy key once per process: a site builds a client per request. */
45
+ function warnLegacyKeyOnce(apiKey) {
46
+ if (warned)
47
+ return;
48
+ warned = true;
49
+ console.warn(`@capacms/sdk/next: this client reads with a ${legacyKind(apiKey)}. Reads work as they do with a cap_ key. ` +
50
+ `Draft previews need a cap_ key: mint one ${MINT}`);
51
+ }
52
+ /**
53
+ * Throws for a legacy key: verifying a preview token takes a `cap_` key. A
54
+ * value that is no key at all is `resolveNextConfig`'s to refuse, by name.
55
+ */
56
+ function requirePreviewKey(apiKey) {
57
+ if (!isLegacy(apiKey))
58
+ return;
59
+ throw new TypeError(`@capacms/sdk/next: preview needs a cap_ key, and this client holds a ${legacyKind(apiKey)}. Mint a cap_ key ${MINT}`);
60
+ }
61
+ /**
62
+ * Throws for a legacy key: a draft read takes a `cap_` key. `holder` names
63
+ * where the key came from (`CAPA_DRAFT_KEY`, `the draft config`), so the
64
+ * message says what to change.
65
+ */
66
+ function requireDraftKey(apiKey, holder) {
67
+ if (!isLegacy(apiKey))
68
+ return;
69
+ throw new TypeError(`@capacms/sdk/next: draft reads need a cap_ key, and ${holder} holds a ${legacyKind(apiKey)}. Mint a development cap_ key ${MINT}`);
70
+ }
71
+ /** Forget that the warning was printed. For tests. */
72
+ function __resetKeyWarningForTests() {
73
+ warned = false;
74
+ }
@@ -1,3 +1,4 @@
1
+ import type { SortableSystemKeyName, SystemKeyName } from "./system-keys";
1
2
  /** A single relation field emitted by `capa-codegen`. */
2
3
  export type CapaRelation<T> = T & {
3
4
  readonly __capaRelation: "one";
@@ -13,14 +14,38 @@ type RelationValue = {
13
14
  readonly __capaRelation: "one" | "many";
14
15
  readonly __capaRelationTarget: unknown;
15
16
  };
17
+ /**
18
+ * A field type that says nothing about the field: `unknown`, as every field
19
+ * of an untyped client's `Record<string, unknown>` is, or `any`, as a type
20
+ * inferred from a select's names is. Such a field may be a relation or not.
21
+ */
22
+ type Untyped<V> = 0 extends 1 & V ? true : unknown extends V ? true : false;
23
+ type UntypedKeys<T> = {
24
+ [K in StringKey<T>]-?: Untyped<T[K]> extends true ? K : never;
25
+ }[StringKey<T>];
16
26
  type RelationKeys<T> = {
17
- [K in StringKey<T>]-?: NonNullable<T[K]> extends RelationValue ? K : never;
27
+ [K in StringKey<T>]-?: Untyped<T[K]> extends true ? never : NonNullable<T[K]> extends RelationValue ? K : never;
18
28
  }[StringKey<T>];
19
29
  type ScalarKeys<T> = Exclude<StringKey<T>, RelationKeys<T>>;
20
30
  type RelationTarget<T> = NonNullable<T> extends {
21
31
  readonly __capaRelationTarget: infer R;
22
32
  } ? R : never;
23
- export type SelectSort<T> = ScalarKeys<T> | `-${ScalarKeys<T>}`;
33
+ /**
34
+ * A system key by its `$` name. The API reads a plain name as the model's
35
+ * field first, so `$tags` is how a select or sort asks for the entry's own
36
+ * tags beside a field called `tags` (spec 17, amendment 29).
37
+ */
38
+ export type SystemKey = `$${SystemKeyName}`;
39
+ /** A system key the API sorts by: `$id`, `$createdAt`, `$updatedAt`, `$publishedAt`. */
40
+ export type SortableSystemKey = `$${SortableSystemKeyName}`;
41
+ /**
42
+ * A field the grammar can name: every one but a namespace starting with `$`,
43
+ * which is a system key's spelling (spec 17, amendment 121). `select: "*"`
44
+ * still returns such a field. A namespace holding `.` or `,` is named as it
45
+ * is (`"price.usd"`), and the client writes it quoted.
46
+ */
47
+ type Nameable<K extends string> = Exclude<K, `$${string}`>;
48
+ export type SelectSort<T> = Nameable<ScalarKeys<T>> | `-${Nameable<ScalarKeys<T>>}` | SortableSystemKey | `-${SortableSystemKey}`;
24
49
  export interface RelationSelectOptions<T> {
25
50
  select: Select<T> | "*";
26
51
  limit?: number;
@@ -31,16 +56,26 @@ export interface RelationSelectOptions<T> {
31
56
  type RelationSelect<T, K extends RelationKeys<T>> = {
32
57
  [P in K]: Select<RelationTarget<T[P]>> | "*" | RelationSelectOptions<RelationTarget<T[P]>>;
33
58
  };
34
- export type SelectItem<T> = "*" | ScalarKeys<T> | {
35
- [K in RelationKeys<T>]: RelationSelect<T, K>;
36
- }[RelationKeys<T>];
59
+ /** An expansion of a field whose type says nothing (`Untyped`): its target is read untyped too. */
60
+ type UntypedRelationSelect<K extends string> = {
61
+ [P in K]: Select<Record<string, unknown>> | "*" | RelationSelectOptions<Record<string, unknown>>;
62
+ };
63
+ /**
64
+ * One item of a select: `*`, a field by name (a relation named alone is read
65
+ * as a reference, `{ id, model }`), a system key by its `$` name, or a
66
+ * relation expanded into the fields its own select names. A field whose
67
+ * namespace starts with `$` has no name (`Nameable`).
68
+ */
69
+ export type SelectItem<T> = "*" | Nameable<ScalarKeys<T>> | Nameable<RelationKeys<T>> | SystemKey | {
70
+ [K in Nameable<RelationKeys<T>>]: RelationSelect<T, K>;
71
+ }[Nameable<RelationKeys<T>>] | ([UntypedKeys<T>] extends [never] ? never : UntypedRelationSelect<UntypedKeys<T>>);
37
72
  /** A typed form of the `/api/entries` `select` grammar. */
38
73
  export type Select<T> = ReadonlyArray<SelectItem<T>>;
39
74
  type Depth = [never, 0, 1, 2, 3, 4];
40
75
  /** The target types one select item expands, and everything below them. */
41
76
  type ItemTargets<T, I, D extends number> = [D] extends [never] ? never : I extends string ? never : {
42
77
  [K in Extract<keyof I, RelationKeys<T>>]: RelationTarget<T[K]> | ValueTargets<RelationTarget<T[K]>, I[K], Depth[D]>;
43
- }[Extract<keyof I, RelationKeys<T>>];
78
+ }[Extract<keyof I, RelationKeys<T>>] | ([Extract<keyof I, UntypedKeys<T>>] extends [never] ? never : Record<string, unknown>);
44
79
  type ValueTargets<R, V, D extends number> = V extends "*" ? never : V extends {
45
80
  select: infer S;
46
81
  } ? ExpandedTargetsAt<R, S, D> : ExpandedTargetsAt<R, V, D>;
@@ -49,8 +84,9 @@ type ExpandedTargetsAt<T, S, D extends number> = S extends ReadonlyArray<infer I
49
84
  * Every entry type a select EXPANDS, at any depth: what `included` can hold
50
85
  * for a `shape: "flat"` read. `[{ author: ["name"] }]` on an article is
51
86
  * `Author`; a select given as a string cannot be read by the type system and is
52
- * `Record<string, unknown>`. Bounded at the API's four levels, so a model that
53
- * relates to itself does not recurse forever.
87
+ * `Record<string, unknown>`. Bounded one level past the 4 relations the API
88
+ * reads below the root entry, so a model that relates to itself does not
89
+ * recurse forever.
54
90
  */
55
91
  export type ExpandedTargets<T, S> = S extends string ? Record<string, unknown> : [ExpandedTargetsAt<T, S, 4>] extends [never] ? never : ExpandedTargetsAt<T, S, 4>;
56
92
  export {};
@@ -0,0 +1,27 @@
1
+ /**
2
+ * system-keys.ts — the entry's own keys, as REST names them.
3
+ *
4
+ * Every entry REST returns carries these beside `fields`, in this order. A
5
+ * request names one plainly (`tags`) only where the model has no field of that
6
+ * name, because REST reads a plain name as the field first; `$tags` always
7
+ * means the system key, in `select`, `where` and `sort`, and after a hop as
8
+ * `author.$id` (spec 17, amendment 29). REST names no field whose namespace
9
+ * starts with `$` (field-names.ts), so no field is ever spelled like one.
10
+ */
11
+ /** The system keys, in the order the API prints them on an entry. */
12
+ export declare const SYSTEM_KEYS: readonly ["id", "model", "status", "createdAt", "updatedAt", "publishedAt", "version", "folder", "tags"];
13
+ export type SystemKeyName = (typeof SYSTEM_KEYS)[number];
14
+ /**
15
+ * The system keys the API sorts by, as `packages/shared`'s `SORTABLE_SYSTEM_KEYS`
16
+ * lists them: `$tags` is a list and `$version`, `$model`, `$status` and
17
+ * `$folder` are refused as sort keys.
18
+ */
19
+ export declare const SORTABLE_SYSTEM_KEYS: readonly ["id", "createdAt", "updatedAt", "publishedAt"];
20
+ export type SortableSystemKeyName = (typeof SORTABLE_SYSTEM_KEYS)[number];
21
+ export declare function isSortableSystemKey(name: string): name is SortableSystemKeyName;
22
+ export declare const SYSTEM_KEY_SIGIL = "$";
23
+ export declare function isSystemKey(name: string): name is SystemKeyName;
24
+ /** Every system key by its `$` name, for a message that lists them. */
25
+ export declare const SYSTEM_KEY_LIST: string;
26
+ /** The system key a `$` name spells (`$tags` is `tags`), or null for any other name. */
27
+ export declare function sigilSystemKey(name: string): SystemKeyName | null;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ /**
3
+ * system-keys.ts — the entry's own keys, as REST names them.
4
+ *
5
+ * Every entry REST returns carries these beside `fields`, in this order. A
6
+ * request names one plainly (`tags`) only where the model has no field of that
7
+ * name, because REST reads a plain name as the field first; `$tags` always
8
+ * means the system key, in `select`, `where` and `sort`, and after a hop as
9
+ * `author.$id` (spec 17, amendment 29). REST names no field whose namespace
10
+ * starts with `$` (field-names.ts), so no field is ever spelled like one.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.SYSTEM_KEY_LIST = exports.SYSTEM_KEY_SIGIL = exports.SORTABLE_SYSTEM_KEYS = exports.SYSTEM_KEYS = void 0;
14
+ exports.isSortableSystemKey = isSortableSystemKey;
15
+ exports.isSystemKey = isSystemKey;
16
+ exports.sigilSystemKey = sigilSystemKey;
17
+ /** The system keys, in the order the API prints them on an entry. */
18
+ exports.SYSTEM_KEYS = ["id", "model", "status", "createdAt", "updatedAt", "publishedAt", "version", "folder", "tags"];
19
+ /**
20
+ * The system keys the API sorts by, as `packages/shared`'s `SORTABLE_SYSTEM_KEYS`
21
+ * lists them: `$tags` is a list and `$version`, `$model`, `$status` and
22
+ * `$folder` are refused as sort keys.
23
+ */
24
+ exports.SORTABLE_SYSTEM_KEYS = ["id", "createdAt", "updatedAt", "publishedAt"];
25
+ const SORTABLE_SYSTEM_KEY_SET = new Set(exports.SORTABLE_SYSTEM_KEYS);
26
+ function isSortableSystemKey(name) {
27
+ return SORTABLE_SYSTEM_KEY_SET.has(name);
28
+ }
29
+ exports.SYSTEM_KEY_SIGIL = "$";
30
+ const SYSTEM_KEY_SET = new Set(exports.SYSTEM_KEYS);
31
+ function isSystemKey(name) {
32
+ return SYSTEM_KEY_SET.has(name);
33
+ }
34
+ /** Every system key by its `$` name, for a message that lists them. */
35
+ exports.SYSTEM_KEY_LIST = exports.SYSTEM_KEYS.map((key) => `${exports.SYSTEM_KEY_SIGIL}${key}`).join(", ");
36
+ /** The system key a `$` name spells (`$tags` is `tags`), or null for any other name. */
37
+ function sigilSystemKey(name) {
38
+ if (!name.startsWith(exports.SYSTEM_KEY_SIGIL))
39
+ return null;
40
+ const key = name.slice(exports.SYSTEM_KEY_SIGIL.length);
41
+ return isSystemKey(key) ? key : null;
42
+ }
@@ -1,20 +1,55 @@
1
- import { LAYOUT_PAGE, type CapaNextClient, type CapaNextConfig, type PagesResource, type PreviewClaim } from "../next";
1
+ import { gql, LAYOUT_PAGE, type CapaDocumentResult, type CapaDocuments, type CapaDocumentVariables, type CapaNextClient, type CapaNextConfig, type GraphQLCallOptions, type GraphQLResult, type NotARecordedDocument, type PagesResource, type PreviewClaim, type TypedDocument, type UntypedQuery, type VariablesThenOptions } from "../next";
2
2
  export interface CacheOptions {
3
3
  tags?: string[];
4
4
  revalidate?: number | false;
5
5
  }
6
6
  /** Add Next.js fetch-cache options without importing `next/*`. */
7
7
  export declare function withCache(fetchImpl: typeof fetch, options: CacheOptions): typeof fetch;
8
+ /**
9
+ * The Next.js cache tag for every read of a model, by its namespace. A GraphQL
10
+ * query names its models by namespace (`articles`), not by id, so this is the
11
+ * tag a GraphQL read is cached under, and `revalidateFromWebhook` revalidates
12
+ * it when an entry of that model changes.
13
+ */
14
+ export declare function modelTag(namespace: string): string;
15
+ /**
16
+ * The tag `graphql()` keeps a read under when it gives a `revalidate` and no
17
+ * `tags`, and that `revalidateFromWebhook` revalidates on every content
18
+ * change: such a page is never stale after a publish, at the price of
19
+ * refreshing on any publish. Name the models it reads with
20
+ * `tagsFor({ namespace })` to refresh it only when one of those changes. A
21
+ * read with neither `tags` nor `revalidate` is not kept at all.
22
+ */
23
+ export declare const GRAPHQL_TAG = "capa:graphql";
24
+ /**
25
+ * The tag `tagsFor({ namespace })` adds beside its model tags, and that
26
+ * `revalidateFromWebhook` revalidates on a media event: a GraphQL read shows
27
+ * a file's URL and alt text whichever models it names, so editing a file in
28
+ * the media library refreshes it.
29
+ */
30
+ export declare const MEDIA_TAG = "capa:media";
31
+ /**
32
+ * Next.js cache tags for a read. `model`, `entry`, `key` and `tenant` are the
33
+ * API's surrogate keys (`m:`, `e:`, `k:`, `t:`), by id; `namespace` is one
34
+ * `capa:model:<namespace>` tag per model a GraphQL query reads, which
35
+ * `capa-codegen --graphql` lists as `<Name>Models`, and `MEDIA_TAG`, since
36
+ * the query may show a file from the media library. Each pairs with
37
+ * `revalidateFromWebhook`.
38
+ */
8
39
  export declare function tagsFor(input: {
9
40
  model?: string;
10
41
  entry?: string;
11
42
  key?: string;
12
43
  tenant?: string;
44
+ namespace?: string | readonly string[];
13
45
  }): string[];
14
46
  export interface WebhookPayload {
47
+ type?: unknown;
15
48
  data?: {
16
49
  instanceId?: unknown;
17
50
  modelId?: unknown;
51
+ modelNamespace?: unknown;
52
+ namespace?: unknown;
18
53
  [key: string]: unknown;
19
54
  };
20
55
  instanceId?: unknown;
@@ -22,17 +57,34 @@ export interface WebhookPayload {
22
57
  modelId?: unknown;
23
58
  [key: string]: unknown;
24
59
  }
25
- /** Revalidate the concrete entry and model identities carried by a webhook. */
60
+ /**
61
+ * Revalidate every tag a webhook's entry and model can be cached under: the
62
+ * entry (`e:`), the model by id (`m:`) and by namespace (`capa:model:`, the
63
+ * tag a GraphQL read uses). The namespace is `data.modelNamespace` on
64
+ * `instance.published`, `instance.unpublished` and `model.published`, and
65
+ * `data.namespace` on the other events (docs/WEBHOOKS.md). Any of those, and
66
+ * a `media.` event, also revalidates `GRAPHQL_TAG`, the tag of a `graphql()`
67
+ * read that named no tags. A `media.` event (a file's alt text, name or
68
+ * visibility changed, or the file went) revalidates the file's own key
69
+ * (`f:<fileId>`, as REST's `Surrogate-Key` names it) and `MEDIA_TAG`, which
70
+ * every read tagged by namespace carries, since it may show that file.
71
+ */
26
72
  export declare function revalidateFromWebhook(input: {
27
73
  payload: WebhookPayload;
28
74
  revalidateTag: (tag: string) => void | Promise<void>;
29
75
  }): Promise<string[]>;
30
- /** Select a client using server-only draft state supplied by the caller. */
31
- export declare function draftClient(input: {
76
+ /**
77
+ * Select a client using server-only draft state supplied by the caller. Pass
78
+ * codegen's `CapaQuery` to type the builder, as with `createClient`:
79
+ * `draftClient<CapaQuery>({ ... })`. `production` may hold the legacy key a
80
+ * site already has; `draft` needs a `cap_` key, and a legacy one throws a
81
+ * `TypeError` when draft mode selects it.
82
+ */
83
+ export declare function draftClient<Q = UntypedQuery>(input: {
32
84
  production: CapaNextConfig;
33
85
  draft: CapaNextConfig;
34
86
  isDraft: () => boolean | Promise<boolean>;
35
- }): Promise<CapaNextClient>;
87
+ }): Promise<CapaNextClient<Q>>;
36
88
  export declare function routeOf(file: string): string;
37
89
  /**
38
90
  * Verify a preview token from a Next route handler.
@@ -50,7 +102,7 @@ export declare function preview(token: string, client: CapaNextClient): Promise<
50
102
  * this and never hold the whole client.
51
103
  */
52
104
  export declare function pagesFor(client: CapaNextClient): PagesResource;
53
- export { LAYOUT_PAGE };
105
+ export { gql, LAYOUT_PAGE };
54
106
  export type { PreviewClaim };
55
107
  /**
56
108
  * The query parameter that turns edit mode on for one request without draft
@@ -128,13 +180,19 @@ export declare function resolveEditRequest(request: {
128
180
  has(name: string): boolean;
129
181
  };
130
182
  }, client: Pick<CapaNextClient, "preview">): Promise<EditRequest>;
131
- /** Where the env-driven helpers read their settings (M6). */
183
+ /**
184
+ * Where the env-driven helpers read their settings (M6): the one pair of
185
+ * names the whole product uses (the MCP server, `capa-codegen`, `capa
186
+ * persist`, every curl example in the API docs).
187
+ */
132
188
  export declare const CAPA_ENV: {
133
189
  readonly baseUrl: "CAPA_API_URL";
134
190
  readonly apiKey: "CAPA_KEY";
135
191
  readonly draftKey: "CAPA_DRAFT_KEY";
136
192
  readonly version: "CAPA_API_VERSION";
137
193
  };
194
+ /** Older names that still work, read only when the name above is unset. */
195
+ export declare const CAPA_ENV_ALIASES: Readonly<Record<string, string>>;
138
196
  export declare const DEFAULT_API_VERSION = "2026-10-01";
139
197
  type DraftModeFn = () => {
140
198
  isEnabled: boolean;
@@ -145,20 +203,124 @@ type DraftModeFn = () => {
145
203
  enable?: () => void;
146
204
  disable?: () => void;
147
205
  }>;
148
- /** The published-key client from env: what verifying a token needs. */
149
- export declare function getPublishedClient(overrides?: Partial<CapaNextConfig>): CapaNextClient;
206
+ /**
207
+ * The published-key client from env: what verifying a token needs. `overrides`
208
+ * win over env, and pass codegen's `CapaQuery` to type the builder:
209
+ * `getPublishedClient<CapaQuery>()`.
210
+ */
211
+ export declare function getPublishedClient<Q = UntypedQuery>(overrides?: Partial<CapaNextConfig>): CapaNextClient<Q>;
150
212
  /**
151
213
  * The client for this request, from env: the draft key under draft mode,
152
214
  * otherwise the published key, and `editMode` worked out for you.
153
215
  *
154
216
  * import { draftMode, headers } from "next/headers";
155
- * const capa = await getCapaClient({ draftMode, headers });
217
+ * const capa = await getCapaClient<CapaQuery>({ draftMode, headers });
218
+ * const { data } = await capa.graphql.query(selection, { tags: tagsFor({ namespace: "articles" }) });
219
+ *
220
+ * `CapaQuery` (from `capa-codegen --graphql`) types the builder, as with
221
+ * `createClient`; leave it out for an untyped client. `config` wins over env.
222
+ *
223
+ * Its REST reads are sent as `createClient` sends them, which Next does not
224
+ * keep. Its GraphQL reads, a document or the builder, are sent the same way
225
+ * unless a call gives `tags` or `revalidate`, so a publish shows up on both
226
+ * alike. Given either, a read is kept in Next's data cache exactly as
227
+ * `graphql()` keeps it: a published read with no errors, never a draft or an
228
+ * edit-mode page.
156
229
  */
157
- export declare function getCapaClient(input: {
230
+ export declare function getCapaClient<Q = UntypedQuery>(input: {
158
231
  draftMode: DraftModeFn;
159
232
  headers: () => HeaderReader | Promise<HeaderReader>;
160
233
  config?: Partial<CapaNextConfig>;
161
- }): Promise<CapaNextClient>;
234
+ /** Next's `unstable_cache`, as `graphql()` takes it. Leave it out: Next's own is loaded. */
235
+ unstable_cache?: UnstableCache;
236
+ }): Promise<CapaNextClient<Q, NextCacheOptions>>;
237
+ /** Where a GraphQL read in Next keeps its answer: Next's data cache, as `graphql()` and `getCapaClient` keep it. */
238
+ export interface NextCacheOptions extends GraphQLCallOptions {
239
+ /**
240
+ * Next.js cache tags to keep this read under. `tagsFor({ namespace:
241
+ * <Name>Models })`, with the list `capa-codegen --graphql` writes beside
242
+ * each document, tags it with every model the query reads, which
243
+ * `revalidateFromWebhook` revalidates on a publish. Given a `revalidate`
244
+ * and no tags, the read is kept under `GRAPHQL_TAG`, which every publish
245
+ * revalidates. Given neither, the read is not kept: it is sent as a REST
246
+ * read is, on every render.
247
+ */
248
+ tags?: string[];
249
+ /** Seconds to cache, `false` to cache until a tag is revalidated, or 0 not to cache. */
250
+ revalidate?: number | false;
251
+ }
252
+ export interface NextGraphQLOptions extends NextCacheOptions {
253
+ /**
254
+ * Next's `draftMode` and `headers`, to work out draft and edit mode as
255
+ * `getCapaClient` does: drafts under draft mode, and in edit mode every
256
+ * entry the query reads marked for `capaAttrs`.
257
+ */
258
+ draftMode?: DraftModeFn;
259
+ headers?: () => HeaderReader | Promise<HeaderReader>;
260
+ /**
261
+ * Read drafts with `CAPA_DRAFT_KEY`, uncached. Worked out from `draftMode`
262
+ * when that is passed; set it to decide yourself.
263
+ */
264
+ draft?: boolean;
265
+ /** Overrides for the env-derived client (base URL, key, version, edit mode, fetch). Each one given is not read from env. */
266
+ config?: Partial<CapaNextConfig>;
267
+ /**
268
+ * Next's `unstable_cache`, from `next/cache`. Leave it out: `graphql()`
269
+ * loads Next's own. A published read given `tags` or a `revalidate` is kept
270
+ * in Next's data cache only when it answered with no `errors`, under `tags`
271
+ * for `revalidate` seconds, or with no `revalidate` until one of its tags is
272
+ * revalidated. Next's fetch
273
+ * cache is not used for it: that cache keeps any 200, and a GraphQL error
274
+ * is a 200 (a root field that timed out), and in Next 15 it keeps nothing
275
+ * without a `revalidate`, tags or not. Pass it only to supply another.
276
+ */
277
+ unstable_cache?: UnstableCache;
278
+ }
279
+ /**
280
+ * Next's `unstable_cache` (`import { unstable_cache } from "next/cache"`),
281
+ * typed here so this package never imports `next/*`.
282
+ */
283
+ export type UnstableCache = <T extends (...args: any[]) => Promise<any>>(cb: T, keyParts?: string[], options?: {
284
+ revalidate?: number | false;
285
+ tags?: string[];
286
+ }) => T;
287
+ /**
288
+ * GraphQL in a server component, one line, from env:
289
+ *
290
+ * import { draftMode, headers } from "next/headers";
291
+ * import { graphql, tagsFor } from "@capacms/sdk/nextjs";
292
+ * import { LatestModels } from "./capa-graphql";
293
+ * const LATEST = `#graphql
294
+ * query Latest($first: Int) { articles(first: $first) { nodes { id model title } } }
295
+ * `;
296
+ * const { data } = await graphql(LATEST, { first: 5 }, { draftMode, headers, tags: tagsFor({ namespace: LatestModels }) });
297
+ *
298
+ * Once `capa-codegen --graphql` has seen the literal, `data` and the
299
+ * variables are typed from it, as they are for a `<Name>Document` it writes,
300
+ * and `LatestModels` lists the models it reads.
301
+ *
302
+ * With no `tags` and no `revalidate` a read is sent as the REST reads are,
303
+ * and Next keeps nothing: the page shows a publish on its next render, as a
304
+ * page reading by REST does. Given either, a published read is kept in Next's
305
+ * data cache (`unstable_cache`) when it answered with no errors: under `tags`,
306
+ * for `revalidate` seconds, or with no `revalidate` until
307
+ * `revalidateFromWebhook` revalidates one of its tags on a publish. It goes
308
+ * as a GET, which the CDN and the API also cache for the published key; a
309
+ * document too long for a GET URL goes as a POST, which only Next's data
310
+ * cache keeps. Under draft mode the read uses `CAPA_DRAFT_KEY`, a
311
+ * `cap_` key, and bypasses the cache (the API answers a development key's GET
312
+ * `no-store`), the same split `getCapaClient` makes for REST, and in edit mode
313
+ * each entry that selects `id` and `model` is marked for `capaAttrs`, uncached.
314
+ * Outside a Next request (a script, a test) the read is simply sent.
315
+ *
316
+ * `persisted: true` sends only the hash, and pays off once the documents are
317
+ * stored with `capa persist`, which takes a development key: a production
318
+ * key never stores one. Until then a
319
+ * hash misses, and the client remembers the miss for five minutes and sends
320
+ * the document by POST meanwhile, so it costs one extra GET per five minutes.
321
+ */
322
+ export declare function graphql<D extends keyof CapaDocuments>(document: D, ...rest: VariablesThenOptions<CapaDocumentVariables<D>, NextGraphQLOptions>): Promise<GraphQLResult<CapaDocumentResult<D>>>;
323
+ export declare function graphql<TData = Record<string, unknown>, TVariables = Record<string, unknown>, D extends string = string>(document: NotARecordedDocument<D, TypedDocument<TData, TVariables> | D>, ...rest: VariablesThenOptions<TVariables, NextGraphQLOptions>): Promise<GraphQLResult<TData>>;
162
324
  /** Only a path on this site: never `//elsewhere.example` or a full URL. */
163
325
  export declare function safeSitePath(value: string | null | undefined): string;
164
326
  /**