@capacms/sdk 1.0.0-next.0 → 1.0.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +450 -0
- package/README.md +1754 -156
- package/bin/capa-codegen.js +192 -5
- package/bin/capa.js +235 -0
- package/bin/graphql-project.js +142 -0
- package/bin/project-env.js +58 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +17 -0
- package/dist/codegen.d.ts +55 -0
- package/dist/codegen.js +320 -39
- package/dist/config.d.ts +5 -36
- package/dist/config.js +47 -1
- package/dist/esm/image/index.d.ts +120 -0
- package/dist/esm/image/index.js +250 -0
- package/dist/esm/image/shared-params.generated.d.ts +190 -0
- package/dist/esm/image/shared-params.generated.js +461 -0
- package/dist/esm/nextjs/image-loader.d.ts +60 -0
- package/dist/esm/nextjs/image-loader.js +67 -0
- package/dist/esm/nextjs/overlay.d.ts +30 -0
- package/dist/esm/nextjs/overlay.js +75 -0
- package/dist/esm/overlay/index.d.ts +32 -0
- package/dist/esm/overlay/index.js +576 -0
- package/dist/esm/overlay/protocol.d.ts +187 -0
- package/dist/esm/overlay/protocol.js +240 -0
- package/dist/esm/package.json +4 -0
- package/dist/graphql-codegen.d.ts +117 -0
- package/dist/graphql-codegen.js +705 -0
- package/dist/http.js +1 -1
- package/dist/image/index.d.ts +120 -0
- package/dist/image/index.js +257 -0
- package/dist/image/shared-params.generated.d.ts +190 -0
- package/dist/image/shared-params.generated.js +471 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/next/attrs.d.ts +98 -0
- package/dist/next/attrs.js +125 -0
- package/dist/next/client.d.ts +176 -32
- package/dist/next/client.js +212 -90
- package/dist/next/entry-fields.d.ts +162 -0
- package/dist/next/entry-fields.js +2 -0
- package/dist/next/errors.d.ts +136 -0
- package/dist/next/errors.js +214 -0
- package/dist/next/field-names.d.ts +37 -0
- package/dist/next/field-names.js +145 -0
- package/dist/next/graphql/build.d.ts +27 -0
- package/dist/next/graphql/build.js +98 -0
- package/dist/next/graphql/documents.d.ts +67 -0
- package/dist/next/graphql/documents.js +35 -0
- package/dist/next/graphql/edit-mode.d.ts +16 -0
- package/dist/next/graphql/edit-mode.js +93 -0
- package/dist/next/graphql/filter-values.d.ts +34 -0
- package/dist/next/graphql/filter-values.js +96 -0
- package/dist/next/graphql/introspection.d.ts +89 -0
- package/dist/next/graphql/introspection.js +102 -0
- package/dist/next/graphql/plan.d.ts +115 -0
- package/dist/next/graphql/plan.js +531 -0
- package/dist/next/graphql/request.d.ts +228 -0
- package/dist/next/graphql/request.js +283 -0
- package/dist/next/graphql/rest.d.ts +66 -0
- package/dist/next/graphql/rest.js +502 -0
- package/dist/next/graphql/selection.d.ts +55 -0
- package/dist/next/graphql/selection.js +212 -0
- package/dist/next/graphql/sha256.d.ts +13 -0
- package/dist/next/graphql/sha256.js +86 -0
- package/dist/next/graphql/summary.d.ts +83 -0
- package/dist/next/graphql/summary.js +151 -0
- package/dist/next/graphql/tree-layout.d.ts +36 -0
- package/dist/next/graphql/tree-layout.js +20 -0
- package/dist/next/graphql/tree.d.ts +171 -0
- package/dist/next/graphql/tree.js +249 -0
- package/dist/next/graphql/typed.d.ts +261 -0
- package/dist/next/graphql/typed.js +146 -0
- package/dist/next/index.d.ts +30 -3
- package/dist/next/index.js +34 -1
- package/dist/next/inflate.d.ts +51 -0
- package/dist/next/inflate.js +243 -0
- package/dist/next/key-family.d.ts +31 -0
- package/dist/next/key-family.js +66 -0
- package/dist/next/select-types.d.ts +58 -5
- package/dist/next/system-keys.d.ts +27 -0
- package/dist/next/system-keys.js +42 -0
- package/dist/nextjs/image-loader.d.ts +60 -0
- package/dist/nextjs/image-loader.js +71 -0
- package/dist/nextjs/index.d.ts +484 -5
- package/dist/nextjs/index.js +704 -9
- package/dist/nextjs/overlay.d.ts +30 -0
- package/dist/nextjs/overlay.js +78 -0
- package/dist/overlay/index.d.ts +32 -0
- package/dist/overlay/index.js +596 -0
- package/dist/overlay/protocol.d.ts +187 -0
- package/dist/overlay/protocol.js +253 -0
- package/package.json +70 -15
|
@@ -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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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;
|