@capacms/sdk 1.0.0-next.0 → 1.0.0-next.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/CHANGELOG.md +450 -0
  2. package/README.md +1754 -156
  3. package/bin/capa-codegen.js +192 -5
  4. package/bin/capa.js +235 -0
  5. package/bin/graphql-project.js +142 -0
  6. package/bin/project-env.js +58 -0
  7. package/dist/client.d.ts +5 -0
  8. package/dist/client.js +17 -0
  9. package/dist/codegen.d.ts +55 -0
  10. package/dist/codegen.js +320 -39
  11. package/dist/config.d.ts +5 -36
  12. package/dist/config.js +47 -1
  13. package/dist/esm/image/index.d.ts +120 -0
  14. package/dist/esm/image/index.js +250 -0
  15. package/dist/esm/image/shared-params.generated.d.ts +190 -0
  16. package/dist/esm/image/shared-params.generated.js +461 -0
  17. package/dist/esm/nextjs/image-loader.d.ts +60 -0
  18. package/dist/esm/nextjs/image-loader.js +67 -0
  19. package/dist/esm/nextjs/overlay.d.ts +30 -0
  20. package/dist/esm/nextjs/overlay.js +75 -0
  21. package/dist/esm/overlay/index.d.ts +32 -0
  22. package/dist/esm/overlay/index.js +576 -0
  23. package/dist/esm/overlay/protocol.d.ts +187 -0
  24. package/dist/esm/overlay/protocol.js +240 -0
  25. package/dist/esm/package.json +4 -0
  26. package/dist/graphql-codegen.d.ts +117 -0
  27. package/dist/graphql-codegen.js +705 -0
  28. package/dist/http.js +1 -1
  29. package/dist/image/index.d.ts +120 -0
  30. package/dist/image/index.js +257 -0
  31. package/dist/image/shared-params.generated.d.ts +190 -0
  32. package/dist/image/shared-params.generated.js +471 -0
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.js +2 -1
  35. package/dist/next/attrs.d.ts +98 -0
  36. package/dist/next/attrs.js +125 -0
  37. package/dist/next/client.d.ts +176 -32
  38. package/dist/next/client.js +212 -90
  39. package/dist/next/entry-fields.d.ts +162 -0
  40. package/dist/next/entry-fields.js +2 -0
  41. package/dist/next/errors.d.ts +136 -0
  42. package/dist/next/errors.js +214 -0
  43. package/dist/next/field-names.d.ts +37 -0
  44. package/dist/next/field-names.js +145 -0
  45. package/dist/next/graphql/build.d.ts +27 -0
  46. package/dist/next/graphql/build.js +98 -0
  47. package/dist/next/graphql/documents.d.ts +67 -0
  48. package/dist/next/graphql/documents.js +35 -0
  49. package/dist/next/graphql/edit-mode.d.ts +16 -0
  50. package/dist/next/graphql/edit-mode.js +93 -0
  51. package/dist/next/graphql/filter-values.d.ts +34 -0
  52. package/dist/next/graphql/filter-values.js +96 -0
  53. package/dist/next/graphql/introspection.d.ts +89 -0
  54. package/dist/next/graphql/introspection.js +102 -0
  55. package/dist/next/graphql/plan.d.ts +115 -0
  56. package/dist/next/graphql/plan.js +531 -0
  57. package/dist/next/graphql/request.d.ts +228 -0
  58. package/dist/next/graphql/request.js +283 -0
  59. package/dist/next/graphql/rest.d.ts +66 -0
  60. package/dist/next/graphql/rest.js +502 -0
  61. package/dist/next/graphql/selection.d.ts +55 -0
  62. package/dist/next/graphql/selection.js +212 -0
  63. package/dist/next/graphql/sha256.d.ts +13 -0
  64. package/dist/next/graphql/sha256.js +86 -0
  65. package/dist/next/graphql/summary.d.ts +83 -0
  66. package/dist/next/graphql/summary.js +151 -0
  67. package/dist/next/graphql/tree-layout.d.ts +36 -0
  68. package/dist/next/graphql/tree-layout.js +20 -0
  69. package/dist/next/graphql/tree.d.ts +171 -0
  70. package/dist/next/graphql/tree.js +249 -0
  71. package/dist/next/graphql/typed.d.ts +261 -0
  72. package/dist/next/graphql/typed.js +146 -0
  73. package/dist/next/index.d.ts +30 -3
  74. package/dist/next/index.js +34 -1
  75. package/dist/next/inflate.d.ts +51 -0
  76. package/dist/next/inflate.js +243 -0
  77. package/dist/next/key-family.d.ts +31 -0
  78. package/dist/next/key-family.js +66 -0
  79. package/dist/next/select-types.d.ts +58 -5
  80. package/dist/next/system-keys.d.ts +27 -0
  81. package/dist/next/system-keys.js +42 -0
  82. package/dist/nextjs/image-loader.d.ts +60 -0
  83. package/dist/nextjs/image-loader.js +71 -0
  84. package/dist/nextjs/index.d.ts +484 -5
  85. package/dist/nextjs/index.js +704 -9
  86. package/dist/nextjs/overlay.d.ts +30 -0
  87. package/dist/nextjs/overlay.js +78 -0
  88. package/dist/overlay/index.d.ts +32 -0
  89. package/dist/overlay/index.js +596 -0
  90. package/dist/overlay/protocol.d.ts +187 -0
  91. package/dist/overlay/protocol.js +253 -0
  92. package/package.json +70 -15
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseSelectLevels = parseSelectLevels;
4
+ exports.inflate = inflate;
5
+ /**
6
+ * inflate.ts — turn a `shape=flat` response back into the `shape=tree` one.
7
+ *
8
+ * A flat response carries every expanded entry once, in `included`, keyed by
9
+ * model namespace and then id, and every relation as a `{ id, model }`
10
+ * reference. `inflate` walks the SAME select the request sent and puts each
11
+ * referenced entry back where the tree would have nested it, holding exactly
12
+ * the system keys and fields that path selected. The result is deep-equal to
13
+ * the `shape=tree` body for the same request (amendment 16 of the api-next spec
14
+ * names the one exception).
15
+ *
16
+ * WHY IT NEEDS THE SELECT. An included entry holds the UNION of every path that
17
+ * reached it, and an entry in `data` also carries what any relation path asked
18
+ * of it. Only the select says which of those fields belong at which place in
19
+ * the tree, and which references were expanded. A result from this SDK's
20
+ * `shape: "flat"` read carries the select it sent (`response.select`), so
21
+ * `inflate(response)` is enough; for a body fetched by hand, pass the select
22
+ * you sent as the second argument. A request that sent no select expanded
23
+ * nothing, and `inflate` returns its data unchanged.
24
+ *
25
+ * COPIES, NEVER SHARED INSTANCES. Every entry in the result is a new object,
26
+ * and so is every value inside it: the same author reached from twenty
27
+ * articles comes back as twenty equal, independent objects, exactly as the
28
+ * tree response parses. Mutating one never changes another, the input is never
29
+ * modified, and `JSON.stringify` of the result cannot meet a cycle.
30
+ *
31
+ * CYCLES END WHERE THE SELECT ENDS. `a` related to `b` related to `a` is
32
+ * inflated to the depth the select wrote (at most four levels, the API's cap)
33
+ * and no further: the walk follows the select, never the references, so it
34
+ * cannot loop. A reference the select did not expand stays a reference.
35
+ */
36
+ const attrs_1 = require("./attrs");
37
+ const system_keys_1 = require("./system-keys");
38
+ const field_names_1 = require("./field-names");
39
+ const ALWAYS_KEYS = new Set(["id", "model", "status"]);
40
+ /**
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.
47
+ */
48
+ function parseSelectLevels(select) {
49
+ let pos = 0;
50
+ const fail = () => {
51
+ throw new TypeError(`@capacms/sdk/next: inflate could not read the select ${JSON.stringify(select)}.`);
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
+ };
61
+ const level = () => {
62
+ const out = { star: false, items: [] };
63
+ for (;;) {
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
+ }
80
+ if (select[pos] === "(") {
81
+ if (token === "")
82
+ fail();
83
+ pos += 1;
84
+ const inner = level();
85
+ if (select[pos] !== ")")
86
+ fail();
87
+ pos += 1;
88
+ out.items.push({ name: token, expand: inner, ...(quoted ? { quoted } : {}) });
89
+ }
90
+ else if (!quoted && token === "*") {
91
+ out.star = true;
92
+ }
93
+ else if (!quoted && token.includes(":")) {
94
+ // A modifier: `limit:2`, `sort:-name`, `after:<cursor>`.
95
+ }
96
+ else if (quoted || token !== "") {
97
+ out.items.push({ name: token, expand: null, ...(quoted ? { quoted } : {}) });
98
+ }
99
+ else {
100
+ fail();
101
+ }
102
+ if (select[pos] !== ",")
103
+ return out;
104
+ pos += 1;
105
+ }
106
+ };
107
+ const root = level();
108
+ if (pos !== select.length)
109
+ fail();
110
+ return root;
111
+ }
112
+ // ----------------------------------------------------------------- walk ----
113
+ function clone(value) {
114
+ if (Array.isArray(value))
115
+ return value.map(clone);
116
+ if (value && typeof value === "object") {
117
+ const out = {};
118
+ for (const [key, inner] of Object.entries(value))
119
+ out[key] = clone(inner);
120
+ return out;
121
+ }
122
+ return value;
123
+ }
124
+ function isReference(value) {
125
+ return (!!value &&
126
+ typeof value === "object" &&
127
+ !Array.isArray(value) &&
128
+ typeof value.id === "string" &&
129
+ !("fields" in value));
130
+ }
131
+ function resolve(index, ref) {
132
+ if (ref.missing)
133
+ return null;
134
+ const fromIncluded = ref.model ? index.included[ref.model]?.[ref.id] : undefined;
135
+ if (fromIncluded && typeof fromIncluded === "object")
136
+ return fromIncluded;
137
+ return index.data.get(ref.id) ?? null;
138
+ }
139
+ /**
140
+ * One entry as the tree holds it at a place whose select is `level`. `null`
141
+ * is "no select was sent", which is every key and every field, unexpanded.
142
+ */
143
+ function project(entry, level, index) {
144
+ const fields = (entry.fields ?? {});
145
+ const all = level === null || level.star;
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.
150
+ const wanted = new Set(ALWAYS_KEYS);
151
+ if (level) {
152
+ for (const item of level.items) {
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))
157
+ wanted.add(item.name);
158
+ }
159
+ }
160
+ const out = {};
161
+ for (const key of system_keys_1.SYSTEM_KEYS) {
162
+ if (!(key in entry))
163
+ continue;
164
+ if (all || wanted.has(key))
165
+ out[key] = clone(entry[key]);
166
+ }
167
+ const expansions = new Map();
168
+ const names = [];
169
+ if (level) {
170
+ for (const item of level.items) {
171
+ if (item.expand)
172
+ expansions.set(item.name, item.expand);
173
+ if (item.name in fields && !all)
174
+ names.push(item.name);
175
+ }
176
+ }
177
+ if (all)
178
+ names.push(...Object.keys(fields));
179
+ const outFields = {};
180
+ for (const name of names) {
181
+ const expand = expansions.get(name);
182
+ outFields[name] = expand ? expandValue(fields[name], expand, index) : clone(fields[name]);
183
+ }
184
+ out.fields = outFields;
185
+ if ((0, attrs_1.isEditEntry)(entry)) {
186
+ Object.defineProperty(out, attrs_1.CAPA_EDIT, { value: true, enumerable: false, configurable: true });
187
+ }
188
+ return out;
189
+ }
190
+ /** A reference, or an array relation's `{ items, pageInfo }`, put back in place. */
191
+ function expandValue(value, level, index) {
192
+ if (isReference(value)) {
193
+ const target = resolve(index, value);
194
+ return target ? project(target, level, index) : clone(value);
195
+ }
196
+ if (value && typeof value === "object" && Array.isArray(value.items)) {
197
+ const list = value;
198
+ const out = {};
199
+ for (const [key, inner] of Object.entries(list)) {
200
+ out[key] =
201
+ key === "items"
202
+ ? list.items.map((item) => expandValue(item, level, index))
203
+ : clone(inner);
204
+ }
205
+ return out;
206
+ }
207
+ return clone(value);
208
+ }
209
+ /**
210
+ * The tree shape of a flat response: `data` with every expansion nested back
211
+ * in, `included` and `select` removed, everything else (`page`, `meta`,
212
+ * `cacheTags`) carried over as it was.
213
+ *
214
+ * `select` is what the request sent; it defaults to `response.select`, which
215
+ * an SDK flat read fills in. A request with no select expanded nothing.
216
+ */
217
+ function inflate(response, select) {
218
+ if (!response || typeof response !== "object" || !("included" in response)) {
219
+ throw new TypeError("@capacms/sdk/next: inflate takes a shape=flat response, which carries included.");
220
+ }
221
+ const text = select ?? response.select;
222
+ const level = text === undefined || text === null ? null : parseSelectLevels(text);
223
+ const rows = Array.isArray(response.data) ? response.data : [response.data];
224
+ const index = { included: response.included ?? {}, data: new Map() };
225
+ for (const row of rows) {
226
+ if (row && typeof row === "object" && typeof row.id === "string") {
227
+ index.data.set(row.id, row);
228
+ }
229
+ }
230
+ const inflateRow = (row) => row && typeof row === "object" ? project(row, level, index) : row;
231
+ const out = {};
232
+ for (const [key, value] of Object.entries(response)) {
233
+ if (key === "included" || key === "select")
234
+ continue;
235
+ if (key === "data") {
236
+ out.data = Array.isArray(value) ? value.map(inflateRow) : inflateRow(value);
237
+ }
238
+ else {
239
+ out[key] = value;
240
+ }
241
+ }
242
+ return out;
243
+ }
@@ -0,0 +1,31 @@
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
+ * the draft clients take to read drafts. Every other key is legacy, which is
6
+ * the API's rule (`apps/api/src/api-next/auth/key-format.ts`): the `pk_` and
7
+ * `sk_` keys, and the unprefixed keys older tenants were minted, all reach
8
+ * `/api/` with the grants their permission gives. So every read call takes
9
+ * them, `preview()` included: `GET /api/preview` asks for `instance:read`,
10
+ * which every legacy permission grants, and refuses a token minted for another
11
+ * tenant. The first client built with one warns once per process, naming the
12
+ * key to mint.
13
+ *
14
+ * The `cap_` prefix is the whole rule, case-sensitive, as on the API:
15
+ * `CAP_live_…` is not a key Capa mints, so it is looked up as a legacy key.
16
+ * `@capacms/mcp` applies the same rule, and both packages are tested against one
17
+ * vector file, `test/fixtures/key-family.json`.
18
+ */
19
+ export type KeyFamily = "cap" | "legacy";
20
+ /** The family of `apiKey`, or null for a value that is no key at all: not a string, or empty. */
21
+ export declare function keyFamily(apiKey: unknown): KeyFamily | null;
22
+ /** Warn about a legacy key once per process: a site builds a client per request. */
23
+ export declare function warnLegacyKeyOnce(apiKey: string): void;
24
+ /**
25
+ * Throws for a legacy key: a draft read takes a `cap_` key. `holder` names
26
+ * where the key came from (`CAPA_DRAFT_KEY`, `the draft config`), so the
27
+ * message says what to change.
28
+ */
29
+ export declare function requireDraftKey(apiKey: string, holder: string): void;
30
+ /** Forget that the warning was printed. For tests. */
31
+ export declare function __resetKeyWarningForTests(): void;
@@ -0,0 +1,66 @@
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
+ * the draft clients take to read drafts. Every other key is legacy, which is
7
+ * the API's rule (`apps/api/src/api-next/auth/key-format.ts`): the `pk_` and
8
+ * `sk_` keys, and the unprefixed keys older tenants were minted, all reach
9
+ * `/api/` with the grants their permission gives. So every read call takes
10
+ * them, `preview()` included: `GET /api/preview` asks for `instance:read`,
11
+ * which every legacy permission grants, and refuses a token minted for another
12
+ * tenant. The first client built with one warns once per process, naming the
13
+ * key to mint.
14
+ *
15
+ * The `cap_` prefix is the whole rule, case-sensitive, as on the API:
16
+ * `CAP_live_…` is not a key Capa mints, so it is looked up as a legacy key.
17
+ * `@capacms/mcp` applies the same rule, and both packages are tested against one
18
+ * vector file, `test/fixtures/key-family.json`.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.keyFamily = keyFamily;
22
+ exports.warnLegacyKeyOnce = warnLegacyKeyOnce;
23
+ exports.requireDraftKey = requireDraftKey;
24
+ exports.__resetKeyWarningForTests = __resetKeyWarningForTests;
25
+ const LEGACY_PREFIX = /^(pk|sk)_/;
26
+ /** The family of `apiKey`, or null for a value that is no key at all: not a string, or empty. */
27
+ function keyFamily(apiKey) {
28
+ if (typeof apiKey !== "string" || apiKey === "")
29
+ return null;
30
+ return apiKey.startsWith("cap_") ? "cap" : "legacy";
31
+ }
32
+ function isLegacy(apiKey) {
33
+ return keyFamily(apiKey) === "legacy";
34
+ }
35
+ /**
36
+ * How a message names a legacy key: by its `pk_` or `sk_` prefix, and an
37
+ * unprefixed key by that fact alone, since every other character is the secret.
38
+ */
39
+ function legacyKind(apiKey) {
40
+ const prefix = LEGACY_PREFIX.exec(apiKey)?.[0];
41
+ return prefix ? `legacy ${prefix} key` : "legacy key with no pk_ or sk_ prefix";
42
+ }
43
+ const MINT = "in the Capa admin under Developers > Keys.";
44
+ let warned = false;
45
+ /** Warn about a legacy key once per process: a site builds a client per request. */
46
+ function warnLegacyKeyOnce(apiKey) {
47
+ if (warned)
48
+ return;
49
+ warned = true;
50
+ console.warn(`@capacms/sdk/next: this client reads with a ${legacyKind(apiKey)}. Reads and preview links work as they do with a cap_ key. ` +
51
+ `Draft reads need a cap_ key: mint one ${MINT}`);
52
+ }
53
+ /**
54
+ * Throws for a legacy key: a draft read takes a `cap_` key. `holder` names
55
+ * where the key came from (`CAPA_DRAFT_KEY`, `the draft config`), so the
56
+ * message says what to change.
57
+ */
58
+ function requireDraftKey(apiKey, holder) {
59
+ if (!isLegacy(apiKey))
60
+ return;
61
+ throw new TypeError(`@capacms/sdk/next: draft reads need a cap_ key, and ${holder} holds a ${legacyKind(apiKey)}. Mint a development cap_ key ${MINT}`);
62
+ }
63
+ /** Forget that the warning was printed. For tests. */
64
+ function __resetKeyWarningForTests() {
65
+ warned = false;
66
+ }
@@ -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,9 +56,37 @@ 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>>;
74
+ type Depth = [never, 0, 1, 2, 3, 4];
75
+ /** The target types one select item expands, and everything below them. */
76
+ type ItemTargets<T, I, D extends number> = [D] extends [never] ? never : I extends string ? never : {
77
+ [K in Extract<keyof I, RelationKeys<T>>]: RelationTarget<T[K]> | ValueTargets<RelationTarget<T[K]>, I[K], Depth[D]>;
78
+ }[Extract<keyof I, RelationKeys<T>>] | ([Extract<keyof I, UntypedKeys<T>>] extends [never] ? never : Record<string, unknown>);
79
+ type ValueTargets<R, V, D extends number> = V extends "*" ? never : V extends {
80
+ select: infer S;
81
+ } ? ExpandedTargetsAt<R, S, D> : ExpandedTargetsAt<R, V, D>;
82
+ type ExpandedTargetsAt<T, S, D extends number> = S extends ReadonlyArray<infer I> ? ItemTargets<T, I, D> : never;
83
+ /**
84
+ * Every entry type a select EXPANDS, at any depth: what `included` can hold
85
+ * for a `shape: "flat"` read. `[{ author: ["name"] }]` on an article is
86
+ * `Author`; a select given as a string cannot be read by the type system and is
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.
90
+ */
91
+ export type ExpandedTargets<T, S> = S extends string ? Record<string, unknown> : [ExpandedTargetsAt<T, S, 4>] extends [never] ? never : ExpandedTargetsAt<T, S, 4>;
39
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
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `@capacms/sdk/nextjs/image-loader`: a next/image loader that lets Capa's CDN
3
+ * resize, in place of Next's own image optimization.
4
+ *
5
+ * Its own entry point, apart from `@capacms/sdk/nextjs`, because next/image
6
+ * runs the loader in the browser: `images.loaderFile` puts this module in
7
+ * every page that shows an image, and this entry carries the image rules and
8
+ * nothing else. `@capacms/sdk/nextjs` exports the same functions for server
9
+ * code.
10
+ *
11
+ * ```js
12
+ * // next.config.mjs
13
+ * export default { images: { loader: "custom", loaderFile: "./capa-image-loader.js" } };
14
+ *
15
+ * // capa-image-loader.js
16
+ * "use client";
17
+ * import { createCapaImageLoader } from "@capacms/sdk/nextjs/image-loader";
18
+ * export default createCapaImageLoader({ format: "webp" });
19
+ * ```
20
+ */
21
+ import { type ImageFormat } from "../image/index.js";
22
+ /** What next/image passes a loader. Declared here so this module does not import `next`. */
23
+ export interface CapaImageLoaderProps {
24
+ src: string;
25
+ width: number;
26
+ quality?: number;
27
+ }
28
+ /** A next/image loader. */
29
+ export type CapaImageLoader = (props: CapaImageLoaderProps) => string;
30
+ /** What every image through the loader gets, unless next/image's own props say otherwise. */
31
+ export interface CapaImageLoaderOptions {
32
+ /**
33
+ * The format to encode. Left out, each image keeps its own. `auto` picks AVIF
34
+ * or WebP from the browser's `Accept` header; through the CDN it currently
35
+ * returns JPEG, so use `webp` until that is fixed.
36
+ */
37
+ format?: ImageFormat;
38
+ /** Quality when an `<Image>` gives none, 1 to 100. Left out, Capa's default, 80. */
39
+ quality?: number;
40
+ }
41
+ /**
42
+ * A next/image loader with fixed options. A `src` that is a full Capa URL
43
+ * (`https://cdn.capacms.com/files/...`, or `https://api.capacms.com/files/...`,
44
+ * which is built on the CDN host) is resized to the width next/image asks for.
45
+ * Any other `src` (a file in `public/`, a bare file name, another host) is
46
+ * returned unchanged.
47
+ *
48
+ * The loader never throws, since a throw would fail the render of the whole
49
+ * page: a `src` or prop the CDN would refuse (`dpr=2` past the 4096-pixel
50
+ * edge, `quality=abc` in the src, `quality={0}`) gets the `src` back unchanged.
51
+ * The options given here are checked once, when the loader is made, so a bad
52
+ * one fails at build time rather than on every image.
53
+ */
54
+ export declare function createCapaImageLoader(options?: CapaImageLoaderOptions): CapaImageLoader;
55
+ /**
56
+ * The next/image loader with no options: each image keeps its own format, and
57
+ * its quality is the `<Image>`'s `quality` prop or Capa's default.
58
+ */
59
+ export declare const capaImageLoader: CapaImageLoader;
60
+ export default capaImageLoader;
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.capaImageLoader = void 0;
4
+ exports.createCapaImageLoader = createCapaImageLoader;
5
+ /**
6
+ * `@capacms/sdk/nextjs/image-loader`: a next/image loader that lets Capa's CDN
7
+ * resize, in place of Next's own image optimization.
8
+ *
9
+ * Its own entry point, apart from `@capacms/sdk/nextjs`, because next/image
10
+ * runs the loader in the browser: `images.loaderFile` puts this module in
11
+ * every page that shows an image, and this entry carries the image rules and
12
+ * nothing else. `@capacms/sdk/nextjs` exports the same functions for server
13
+ * code.
14
+ *
15
+ * ```js
16
+ * // next.config.mjs
17
+ * export default { images: { loader: "custom", loaderFile: "./capa-image-loader.js" } };
18
+ *
19
+ * // capa-image-loader.js
20
+ * "use client";
21
+ * import { createCapaImageLoader } from "@capacms/sdk/nextjs/image-loader";
22
+ * export default createCapaImageLoader({ format: "webp" });
23
+ * ```
24
+ */
25
+ const index_js_1 = require("../image/index.js");
26
+ /**
27
+ * A next/image loader with fixed options. A `src` that is a full Capa URL
28
+ * (`https://cdn.capacms.com/files/...`, or `https://api.capacms.com/files/...`,
29
+ * which is built on the CDN host) is resized to the width next/image asks for.
30
+ * Any other `src` (a file in `public/`, a bare file name, another host) is
31
+ * returned unchanged.
32
+ *
33
+ * The loader never throws, since a throw would fail the render of the whole
34
+ * page: a `src` or prop the CDN would refuse (`dpr=2` past the 4096-pixel
35
+ * edge, `quality=abc` in the src, `quality={0}`) gets the `src` back unchanged.
36
+ * The options given here are checked once, when the loader is made, so a bad
37
+ * one fails at build time rather than on every image.
38
+ */
39
+ function createCapaImageLoader(options = {}) {
40
+ const { format, quality } = options;
41
+ try {
42
+ (0, index_js_1.imageUrl)("x", { format, quality });
43
+ }
44
+ catch (error) {
45
+ throw new TypeError(String(error.message).replace("imageUrl:", "createCapaImageLoader:"));
46
+ }
47
+ return (props) => {
48
+ // Everything inside the try, the props' own destructuring included: a call
49
+ // with no props object gives back no src rather than throwing.
50
+ try {
51
+ const { src, width, quality: asked } = props;
52
+ if (!(0, index_js_1.isCapaImageUrl)(src))
53
+ return src;
54
+ return (0, index_js_1.imageUrl)(src, {
55
+ // A width past Capa's edge asks for the edge, which keeps the image resized.
56
+ width: Math.min(width, index_js_1.MAX_OUTPUT_EDGE),
57
+ quality: asked ?? quality,
58
+ format,
59
+ });
60
+ }
61
+ catch {
62
+ return props?.src;
63
+ }
64
+ };
65
+ }
66
+ /**
67
+ * The next/image loader with no options: each image keeps its own format, and
68
+ * its quality is the `<Image>`'s `quality` prop or Capa's default.
69
+ */
70
+ exports.capaImageLoader = createCapaImageLoader();
71
+ exports.default = exports.capaImageLoader;