@capacms/sdk 1.0.0-next.1 → 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 +1698 -193
- 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 +84 -10
- package/dist/next/attrs.js +119 -2
- 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 -5
- package/dist/next/index.js +32 -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 +688 -6
- package/dist/nextjs/overlay.d.ts +30 -0
- package/dist/nextjs/overlay.js +78 -0
- package/dist/overlay/index.d.ts +14 -2
- package/dist/overlay/index.js +282 -43
- package/dist/overlay/protocol.d.ts +98 -2
- package/dist/overlay/protocol.js +151 -4
- package/package.json +63 -15
package/dist/client.js
CHANGED
|
@@ -19,6 +19,7 @@ exports.createClient = createClient;
|
|
|
19
19
|
*/
|
|
20
20
|
const config_1 = require("./config");
|
|
21
21
|
const http_1 = require("./http");
|
|
22
|
+
const key_family_1 = require("./next/key-family");
|
|
22
23
|
const webhooks_1 = require("./webhooks");
|
|
23
24
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
24
25
|
/**
|
|
@@ -64,7 +65,23 @@ function contentQuery(options = {}) {
|
|
|
64
65
|
q[k] = v;
|
|
65
66
|
return q;
|
|
66
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* `/v2` answers every `cap_` key "Invalid API key", which reads as a bad key
|
|
70
|
+
* rather than the wrong client, so one is refused here before the tenant id is
|
|
71
|
+
* asked for or anything is sent. The rule is the API's: `cap_`, case-sensitive.
|
|
72
|
+
*/
|
|
73
|
+
function refuseCapKey(apiKey) {
|
|
74
|
+
if ((0, key_family_1.keyFamily)(apiKey) !== "cap")
|
|
75
|
+
return;
|
|
76
|
+
throw new TypeError('@capacms/sdk: this is the legacy /v2/api client, and cap_ keys read /api/: import { createClient } from "@capacms/sdk/next".');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The legacy `/v2/api` client, for a legacy key (`pk_`, `sk_` or unprefixed)
|
|
80
|
+
* and its tenant's id. For `/api/`, GraphQL and `cap_` keys, import
|
|
81
|
+
* `createClient` from `@capacms/sdk/next`.
|
|
82
|
+
*/
|
|
67
83
|
function createClient(config) {
|
|
84
|
+
refuseCapKey(config.apiKey);
|
|
68
85
|
const resolved = (0, config_1.resolveConfig)(config);
|
|
69
86
|
async function listContent(namespace, options = {}) {
|
|
70
87
|
const { body, cacheTags } = await (0, http_1.getJson)(resolved, `/v2/api/${encodeURIComponent(namespace)}`, contentQuery(options));
|
package/dist/codegen.d.ts
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
* committed file stable under renames.
|
|
37
37
|
*/
|
|
38
38
|
import { type CapaConfig } from "./config";
|
|
39
|
+
import type { CapaIntrospection } from "./next/graphql/introspection";
|
|
39
40
|
export interface CodegenResult {
|
|
40
41
|
/** False when the schema checksum was unchanged and nothing was fetched. */
|
|
41
42
|
changed: boolean;
|
|
@@ -49,6 +50,8 @@ export interface SchemaModelField {
|
|
|
49
50
|
type: string;
|
|
50
51
|
arrayType?: string | null;
|
|
51
52
|
relationRef?: string | null;
|
|
53
|
+
enumValues?: string[] | null;
|
|
54
|
+
required?: boolean | null;
|
|
52
55
|
}
|
|
53
56
|
export interface SchemaModel {
|
|
54
57
|
id?: string;
|
|
@@ -60,6 +63,23 @@ export interface SchemaForTypes {
|
|
|
60
63
|
}
|
|
61
64
|
/** Read the checksum a previous run stamped into the generated file. */
|
|
62
65
|
export declare function readStampedChecksum(existing: string | null | undefined): string | null;
|
|
66
|
+
/**
|
|
67
|
+
* The interface name for a name `/v2/schema/types` wrote. That route prints
|
|
68
|
+
* the PascalCase of the namespace whatever it holds, which is not always an
|
|
69
|
+
* identifier (`2024Events`, `Blog.posts`), and it is frozen legacy, so the
|
|
70
|
+
* file is made to parse here. The rule is GraphQL's (N1): every character
|
|
71
|
+
* outside [0-9A-Za-z] dropped and `_` before a leading digit, so
|
|
72
|
+
* `2024_events` is `_2024Events` in REST types and GraphQL types alike.
|
|
73
|
+
*/
|
|
74
|
+
export declare function interfaceName(written: string): string;
|
|
75
|
+
/**
|
|
76
|
+
* Each model's interface name, by namespace. Two namespaces can give one
|
|
77
|
+
* PascalCase name (`twin_a` and `twin-a` are both `TwinA`), as can a model and
|
|
78
|
+
* another's `Select` or `Attrs` alias, or a shared declaration, and a module
|
|
79
|
+
* cannot declare an alias twice. Every model caught in such a collision is
|
|
80
|
+
* named by `namespaceInterface` instead, whatever order the models come in.
|
|
81
|
+
*/
|
|
82
|
+
export declare function modelInterfaceNames(models: readonly SchemaModel[]): Map<string, string>;
|
|
63
83
|
export declare function normalizeTypes(source: string, checksum: string | null, schema?: SchemaForTypes | null): string;
|
|
64
84
|
export declare function typeNames(source: string): string[];
|
|
65
85
|
/**
|
|
@@ -67,3 +87,38 @@ export declare function typeNames(source: string): string[];
|
|
|
67
87
|
* checksum stamped in `existing`.
|
|
68
88
|
*/
|
|
69
89
|
export declare function generate(config: CapaConfig, existing?: string | null): Promise<CodegenResult>;
|
|
90
|
+
/**
|
|
91
|
+
* `/v2/schema/types`'s type map and preamble, so a `cap_` key, which `/v2`
|
|
92
|
+
* does not take, gets the interfaces a legacy key gets. Copied from
|
|
93
|
+
* `apps/api/src/routes/v2/schema.ts`, which is frozen legacy; a test holds the
|
|
94
|
+
* two equal.
|
|
95
|
+
*/
|
|
96
|
+
export declare const LEGACY_TYPE_MAP: Readonly<Record<string, string>>;
|
|
97
|
+
export declare const LEGACY_PREAMBLE = "export interface CapaImage {\n url: string;\n alt?: string;\n width?: number;\n height?: number;\n filesize?: number;\n filename?: string;\n}\n\nexport interface CapaVideo {\n url: string;\n thumbnail?: string;\n duration?: number;\n width?: number;\n height?: number;\n filesize?: number;\n filename?: string;\n}\n\nexport interface CapaFile {\n url: string;\n filename: string;\n filesize?: number;\n type?: string;\n}\n\nexport interface CapaInstance<T = Record<string, unknown>> {\n id: string;\n title?: string;\n slug?: string;\n data: T;\n status: 'draft' | 'published';\n createdAt: string;\n updatedAt: string;\n publishedAt?: string;\n}\n\n";
|
|
98
|
+
export interface RestTypesResult {
|
|
99
|
+
source: string;
|
|
100
|
+
/** Interface names present in the output, sorted. */
|
|
101
|
+
types: string[];
|
|
102
|
+
/**
|
|
103
|
+
* Models the key reads that the GraphQL schema leaves out, since their type
|
|
104
|
+
* names would collide (N1). The schema says nothing of their fields, so no
|
|
105
|
+
* interface is written for them.
|
|
106
|
+
*/
|
|
107
|
+
restOnly: string[];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The REST model interfaces for a key's GraphQL schema: what
|
|
111
|
+
* `/v2/schema/types` and `/v2/schema` give a legacy key, for a `cap_` key.
|
|
112
|
+
* The model namespaces, field namespaces, Capa types and relation targets
|
|
113
|
+
* come from the schema's descriptions (N9), so the interfaces are keyed as
|
|
114
|
+
* REST reads them (`open-time`), not as GraphQL renames them (`open_time`).
|
|
115
|
+
*
|
|
116
|
+
* The text is written as the legacy route writes it and goes through
|
|
117
|
+
* `normalizeTypes`, the one place that turns that text into a module. Three
|
|
118
|
+
* things GraphQL does not say are written as it can: every field is optional,
|
|
119
|
+
* since the schema has no required flag; an enum is `string`, since it lists
|
|
120
|
+
* no values; and a field GraphQL leaves out (its description's `Not exposed:`
|
|
121
|
+
* line) is `unknown`. No checksum is stamped: `CAPA_SCHEMA_CHECKSUM` is the
|
|
122
|
+
* `/v2/schema` checksum, which a `cap_` key cannot read.
|
|
123
|
+
*/
|
|
124
|
+
export declare function restTypesFromIntrospection(introspection: CapaIntrospection): RestTypesResult;
|
package/dist/codegen.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LEGACY_PREAMBLE = exports.LEGACY_TYPE_MAP = void 0;
|
|
3
4
|
exports.readStampedChecksum = readStampedChecksum;
|
|
5
|
+
exports.interfaceName = interfaceName;
|
|
6
|
+
exports.modelInterfaceNames = modelInterfaceNames;
|
|
4
7
|
exports.normalizeTypes = normalizeTypes;
|
|
5
8
|
exports.typeNames = typeNames;
|
|
6
9
|
exports.generate = generate;
|
|
10
|
+
exports.restTypesFromIntrospection = restTypesFromIntrospection;
|
|
7
11
|
/**
|
|
8
12
|
* codegen.ts — pull a tenant's generated types and write them to disk.
|
|
9
13
|
*
|
|
@@ -43,6 +47,7 @@ exports.generate = generate;
|
|
|
43
47
|
*/
|
|
44
48
|
const config_1 = require("./config");
|
|
45
49
|
const http_1 = require("./http");
|
|
50
|
+
const summary_1 = require("./next/graphql/summary");
|
|
46
51
|
const MARKER = "// @capa-schema-checksum ";
|
|
47
52
|
/**
|
|
48
53
|
* The same checksum as a VALUE, not just a comment, so a site can send it back.
|
|
@@ -69,48 +74,174 @@ function readStampedChecksum(existing) {
|
|
|
69
74
|
}
|
|
70
75
|
return null;
|
|
71
76
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Sort declarations by name so the file is stable regardless of the order the
|
|
74
|
-
* server emitted them in. The leading comment block is kept as a preamble; the
|
|
75
|
-
* shared `Capa*` helpers are kept ahead of tenant types because they are the
|
|
76
|
-
* primitives everything else refers to.
|
|
77
|
-
*/
|
|
77
|
+
/** PascalCase of a namespace, as `/v2/schema/types` names a model's interface. */
|
|
78
78
|
function toPascalCase(str) {
|
|
79
79
|
return str
|
|
80
80
|
.split(/[-_]/)
|
|
81
81
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
82
82
|
.join("");
|
|
83
83
|
}
|
|
84
|
-
|
|
84
|
+
/** A TypeScript identifier: what an interface name must be, and a property key may be written bare. */
|
|
85
|
+
const IDENTIFIER = /^[A-Za-z_$][0-9A-Za-z_$]*$/;
|
|
86
|
+
/** A model interface's first line, as `/v2/schema/types` writes it. */
|
|
87
|
+
const INTERFACE_HEADER = /^export interface (.+) \{$/;
|
|
88
|
+
/** A property of a model interface, ` name?: type;`, the name written verbatim, or quoted by an earlier run. */
|
|
89
|
+
const PROPERTY = /^(\s+)(.+?)(\?)?: (.+);$/;
|
|
90
|
+
/**
|
|
91
|
+
* The interface name for a name `/v2/schema/types` wrote. That route prints
|
|
92
|
+
* the PascalCase of the namespace whatever it holds, which is not always an
|
|
93
|
+
* identifier (`2024Events`, `Blog.posts`), and it is frozen legacy, so the
|
|
94
|
+
* file is made to parse here. The rule is GraphQL's (N1): every character
|
|
95
|
+
* outside [0-9A-Za-z] dropped and `_` before a leading digit, so
|
|
96
|
+
* `2024_events` is `_2024Events` in REST types and GraphQL types alike.
|
|
97
|
+
*/
|
|
98
|
+
function interfaceName(written) {
|
|
99
|
+
if (IDENTIFIER.test(written))
|
|
100
|
+
return written;
|
|
101
|
+
const letters = written.replace(/[^0-9A-Za-z]/g, "") || "_";
|
|
102
|
+
return /^[0-9]/.test(letters) ? `_${letters}` : letters;
|
|
103
|
+
}
|
|
104
|
+
/** The interface a model's entries are typed with, before `modelInterfaceNames` settles collisions. */
|
|
105
|
+
function modelInterface(namespace) {
|
|
106
|
+
return interfaceName(toPascalCase(namespace));
|
|
107
|
+
}
|
|
108
|
+
/** Declarations every generated file holds beside the models (`LEGACY_PREAMBLE`, `relationPreamble`). */
|
|
109
|
+
const SHARED_NAMES = ["CapaImage", "CapaVideo", "CapaFile", "CapaInstance", "CapaRelation", "CapaRelationList"];
|
|
110
|
+
/**
|
|
111
|
+
* A model's interface named from its whole namespace: `Model_` and the
|
|
112
|
+
* namespace with each character outside [0-9A-Za-z_] written `$` and its hex
|
|
113
|
+
* code, so `twin-a` is `Model_twin$2da`. No two namespaces give the same one.
|
|
114
|
+
*/
|
|
115
|
+
function namespaceInterface(namespace) {
|
|
116
|
+
return `Model_${[...namespace].map((ch) => (/[0-9A-Za-z_]/.test(ch) ? ch : `$${ch.codePointAt(0).toString(16)}`)).join("")}`;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Each model's interface name, by namespace. Two namespaces can give one
|
|
120
|
+
* PascalCase name (`twin_a` and `twin-a` are both `TwinA`), as can a model and
|
|
121
|
+
* another's `Select` or `Attrs` alias, or a shared declaration, and a module
|
|
122
|
+
* cannot declare an alias twice. Every model caught in such a collision is
|
|
123
|
+
* named by `namespaceInterface` instead, whatever order the models come in.
|
|
124
|
+
*/
|
|
125
|
+
function modelInterfaceNames(models) {
|
|
126
|
+
const claims = new Map(SHARED_NAMES.map((name) => [name, new Set([""])]));
|
|
127
|
+
for (const { namespace } of models) {
|
|
128
|
+
const base = modelInterface(namespace);
|
|
129
|
+
for (const name of [base, `${base}Select`, `${base}Attrs`]) {
|
|
130
|
+
if (!claims.has(name))
|
|
131
|
+
claims.set(name, new Set());
|
|
132
|
+
claims.get(name).add(namespace);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const collided = new Set([...claims.values()].filter((owners) => owners.size > 1).flatMap((owners) => [...owners]));
|
|
136
|
+
return new Map(models.map(({ namespace }) => [namespace, collided.has(namespace) ? namespaceInterface(namespace) : modelInterface(namespace)]));
|
|
137
|
+
}
|
|
138
|
+
/** A field namespace as a property key: bare when it is an identifier, quoted otherwise (`"am/pm_indicator"`). */
|
|
139
|
+
function propertyKey(namespace) {
|
|
140
|
+
return IDENTIFIER.test(namespace) ? namespace : JSON.stringify(namespace);
|
|
141
|
+
}
|
|
142
|
+
/** The namespace a property key names: a key an earlier run quoted is read back. */
|
|
143
|
+
function keyNamespace(key) {
|
|
144
|
+
if (!/^".*"$/.test(key))
|
|
145
|
+
return key;
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(key);
|
|
148
|
+
return typeof parsed === "string" ? parsed : key;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return key;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* An enum value as a TypeScript string literal. Single quotes, as
|
|
156
|
+
* `/v2/schema/types` writes it, so a value with nothing to escape keeps its
|
|
157
|
+
* bytes; that route writes `it's` and `a\b` unescaped, which does not parse or
|
|
158
|
+
* reads another value.
|
|
159
|
+
*/
|
|
160
|
+
function enumLiteral(value) {
|
|
161
|
+
return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n")}'`;
|
|
162
|
+
}
|
|
163
|
+
function relationTarget(ref, models, names) {
|
|
85
164
|
const target = models.find((model) => model.id === ref || model.namespace === ref);
|
|
86
|
-
return target ?
|
|
165
|
+
return target ? names.get(target.namespace) ?? modelInterface(target.namespace) : "unknown";
|
|
87
166
|
}
|
|
88
167
|
function hasRelations(schema) {
|
|
89
168
|
return !!schema?.models?.some((model) => (model.fields ?? []).some((field) => (field.type === "relation" || (field.type === "array" && field.arrayType === "relation")) && field.relationRef));
|
|
90
169
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
170
|
+
/**
|
|
171
|
+
* A field's type from the schema, where the schema says more than the text:
|
|
172
|
+
* a relation as its branded helper (only when the schema has relations, so a
|
|
173
|
+
* schema without keeps its bytes), and an enum's values escaped. Null to keep
|
|
174
|
+
* the type the text wrote.
|
|
175
|
+
*/
|
|
176
|
+
function schemaFieldType(field, models, relations, names) {
|
|
177
|
+
if (relations && field.relationRef) {
|
|
178
|
+
const target = relationTarget(field.relationRef, models, names);
|
|
179
|
+
if (field.type === "relation")
|
|
180
|
+
return `CapaRelation<${target}>`;
|
|
181
|
+
if (field.type === "array" && field.arrayType === "relation")
|
|
182
|
+
return `CapaRelationList<${target}>`;
|
|
183
|
+
}
|
|
184
|
+
if (field.type === "enum" && field.enumValues?.length)
|
|
185
|
+
return field.enumValues.map(enumLiteral).join(" | ");
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
/** A union of single-quoted string literals that parses: each quote and backslash inside escaped. */
|
|
189
|
+
const LITERAL_UNION = /^'(?:[^'\\\n]|\\.)*'(?: \| '(?:[^'\\\n]|\\.)*')*$/;
|
|
190
|
+
/**
|
|
191
|
+
* A type as the text wrote it, made to parse without the schema: a model
|
|
192
|
+
* interface it names (`2024Events`, `2024Events[]`) renamed as its declaration
|
|
193
|
+
* was, and an enum whose values the text cannot carry (`'it's fine'`) a
|
|
194
|
+
* `string`, since only the schema holds the values.
|
|
195
|
+
*/
|
|
196
|
+
function writtenType(written, renamed) {
|
|
197
|
+
if (written.startsWith("'"))
|
|
198
|
+
return LITERAL_UNION.test(written) ? written : "string";
|
|
199
|
+
const [, name, list = ""] = /^(.+?)((?:\[\])?)$/.exec(written) ?? [];
|
|
200
|
+
return name !== undefined && renamed.has(name) ? `${renamed.get(name)}${list}` : written;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* One model interface made to parse and typed from the schema: named `name`,
|
|
204
|
+
* every key that is not an identifier quoted, relations and enums as
|
|
205
|
+
* `schemaFieldType` says.
|
|
206
|
+
*/
|
|
207
|
+
function rewriteModelBlock(block, model, name, module) {
|
|
208
|
+
const lines = block.split("\n");
|
|
209
|
+
lines[0] = `export interface ${name} {`;
|
|
210
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
211
|
+
const property = PROPERTY.exec(lines[i]);
|
|
212
|
+
if (!property)
|
|
109
213
|
continue;
|
|
110
|
-
const
|
|
111
|
-
|
|
214
|
+
const [, indent, key, optional = "", written] = property;
|
|
215
|
+
const namespace = keyNamespace(key);
|
|
216
|
+
const field = model?.fields?.find((candidate) => candidate.namespace === namespace);
|
|
217
|
+
const type = (field && schemaFieldType(field, module.models, module.relations, module.names)) ?? writtenType(written, module.renamed);
|
|
218
|
+
lines[i] = `${indent}${propertyKey(namespace)}${optional}: ${type};`;
|
|
219
|
+
}
|
|
220
|
+
return lines.join("\n");
|
|
221
|
+
}
|
|
222
|
+
/** A field's type as `/v2/schema/types` writes it from the schema: a relation as its interface, anything unmapped `unknown`. */
|
|
223
|
+
function legacySchemaType(field, module) {
|
|
224
|
+
const scalar = (type) => exports.LEGACY_TYPE_MAP[type ?? ""] ?? "unknown";
|
|
225
|
+
const target = () => (field.relationRef ? relationTarget(field.relationRef, module.models, module.names) : "unknown");
|
|
226
|
+
if (field.type === "relation" && field.relationRef)
|
|
227
|
+
return target();
|
|
228
|
+
if (field.type === "array" && field.arrayType)
|
|
229
|
+
return `${field.arrayType === "relation" && field.relationRef ? target() : scalar(field.arrayType)}[]`;
|
|
230
|
+
return scalar(field.type);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* A model's interface written from the schema alone, as `/v2/schema/types`
|
|
234
|
+
* writes one. For models that route names alike (`TwinA` twice), whose text
|
|
235
|
+
* cannot say which block is which model.
|
|
236
|
+
*/
|
|
237
|
+
function schemaModelBlock(model, module) {
|
|
238
|
+
const lines = [`export interface ${module.names.get(model.namespace)} {`];
|
|
239
|
+
for (const field of model.fields ?? []) {
|
|
240
|
+
const type = schemaFieldType(field, module.models, module.relations, module.names) ?? legacySchemaType(field, module);
|
|
241
|
+
lines.push(` ${propertyKey(field.namespace)}${field.required ? "" : "?"}: ${type};`);
|
|
112
242
|
}
|
|
113
|
-
|
|
243
|
+
lines.push("}");
|
|
244
|
+
return lines.join("\n");
|
|
114
245
|
}
|
|
115
246
|
function relationPreamble() {
|
|
116
247
|
return [
|
|
@@ -119,13 +250,18 @@ function relationPreamble() {
|
|
|
119
250
|
"",
|
|
120
251
|
].join("\n");
|
|
121
252
|
}
|
|
122
|
-
function selectAliases(parts, schema) {
|
|
123
|
-
const names = new Set(parts.map((part) => (/^export interface (\w+)/.exec(part) || [])[1]).filter(Boolean));
|
|
253
|
+
function selectAliases(parts, schema, modelNames) {
|
|
254
|
+
const names = new Set(parts.map((part) => (/^export interface ([\w$]+)/.exec(part) || [])[1]).filter(Boolean));
|
|
124
255
|
return schema.models
|
|
125
|
-
.map((model) =>
|
|
256
|
+
.map((model) => modelNames.get(model.namespace) ?? modelInterface(model.namespace))
|
|
126
257
|
.filter((name) => names.has(name))
|
|
127
258
|
.sort()
|
|
128
|
-
.
|
|
259
|
+
.flatMap((name) => [
|
|
260
|
+
`export type ${name}Select = import("@capacms/sdk/next").Select<${name}>;`,
|
|
261
|
+
// M5: fieldAttrs(entry) typed per model, so a wrong field name fails
|
|
262
|
+
// to compile, as in `const a: ArticleAttrs = fieldAttrs(entry)`.
|
|
263
|
+
`export type ${name}Attrs = import("@capacms/sdk/next").FieldAttrs<${name}>;`,
|
|
264
|
+
]);
|
|
129
265
|
}
|
|
130
266
|
function normalizeTypes(source, checksum, schema) {
|
|
131
267
|
// Strip a header a previous run wrote, so normalize(normalize(x)) === normalize(x).
|
|
@@ -141,14 +277,49 @@ function normalizeTypes(source, checksum, schema) {
|
|
|
141
277
|
.replace(/^\n+/, "");
|
|
142
278
|
const parts = withoutHeader.split(/\n(?=export (?:interface|type) )/);
|
|
143
279
|
const preamble = parts.length && !/^export (?:interface|type) /.test(parts[0]) ? parts.shift() : "";
|
|
144
|
-
const nameOf = (b) => (/^export (?:interface|type) (\w+)/.exec(b) || [])[1] ?? "";
|
|
280
|
+
const nameOf = (b) => (/^export (?:interface|type) ([\w$]+)/.exec(b) || [])[1] ?? "";
|
|
145
281
|
const byName = (a, b) => nameOf(a).localeCompare(nameOf(b));
|
|
282
|
+
const isShared = (b) => SHARED_NAMES.includes(nameOf(b));
|
|
146
283
|
const schemaWithRelations = hasRelations(schema) ? schema : null;
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
|
|
284
|
+
const models = schema?.models ?? [];
|
|
285
|
+
const names = modelInterfaceNames(models);
|
|
286
|
+
// The models each block's header can mean: the route writes a model's
|
|
287
|
+
// PascalCase name, an earlier run its settled name.
|
|
288
|
+
const candidates = (written) => {
|
|
289
|
+
const name = interfaceName(written);
|
|
290
|
+
return models.filter((model) => modelInterface(model.namespace) === name || names.get(model.namespace) === name);
|
|
291
|
+
};
|
|
292
|
+
const renamed = new Map();
|
|
293
|
+
for (const part of parts) {
|
|
294
|
+
const written = isShared(part) ? undefined : INTERFACE_HEADER.exec(part.split("\n", 1)[0])?.[1];
|
|
295
|
+
if (written === undefined)
|
|
296
|
+
continue;
|
|
297
|
+
const found = candidates(written);
|
|
298
|
+
const name = found.length === 1 ? names.get(found[0].namespace) : interfaceName(written);
|
|
299
|
+
if (found.length <= 1 && name !== written)
|
|
300
|
+
renamed.set(written, name);
|
|
301
|
+
}
|
|
302
|
+
const module = { models, names, relations: schemaWithRelations !== null, renamed };
|
|
303
|
+
const rewritten = [];
|
|
304
|
+
const fromSchema = new Set();
|
|
305
|
+
for (const part of parts) {
|
|
306
|
+
const header = isShared(part) ? null : INTERFACE_HEADER.exec(part.split("\n", 1)[0]);
|
|
307
|
+
if (!header) {
|
|
308
|
+
rewritten.push(part);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const found = candidates(header[1]);
|
|
312
|
+
if (found.length > 1)
|
|
313
|
+
found.forEach((model) => fromSchema.add(model));
|
|
314
|
+
else
|
|
315
|
+
rewritten.push(rewriteModelBlock(part, found[0], found.length ? names.get(found[0].namespace) : interfaceName(header[1]), module));
|
|
316
|
+
}
|
|
317
|
+
for (const model of fromSchema)
|
|
318
|
+
rewritten.push(schemaModelBlock(model, module));
|
|
319
|
+
const shared = rewritten.filter(isShared).sort(byName);
|
|
320
|
+
const rest = rewritten.filter((p) => !isShared(p)).sort(byName);
|
|
150
321
|
const relationHelpers = schemaWithRelations ? [relationPreamble().trimEnd()] : [];
|
|
151
|
-
const aliases = schemaWithRelations ? selectAliases(rest, schemaWithRelations) : [];
|
|
322
|
+
const aliases = schemaWithRelations ? selectAliases(rest, schemaWithRelations, names) : [];
|
|
152
323
|
const head = [
|
|
153
324
|
"// Generated by @capacms/sdk. Do not edit by hand.",
|
|
154
325
|
"// Re-run `capa-codegen` after changing a model in Capa.",
|
|
@@ -166,7 +337,7 @@ function normalizeTypes(source, checksum, schema) {
|
|
|
166
337
|
return `${head}\n\n${kept}${body}\n`;
|
|
167
338
|
}
|
|
168
339
|
function typeNames(source) {
|
|
169
|
-
return [...source.matchAll(/^export (?:interface|type) (\w+)/gm)].map((m) => m[1]).sort();
|
|
340
|
+
return [...source.matchAll(/^export (?:interface|type) ([\w$]+)/gm)].map((m) => m[1]).sort();
|
|
170
341
|
}
|
|
171
342
|
/**
|
|
172
343
|
* Fetch types if — and only if — the tenant's schema has changed since the
|
|
@@ -186,3 +357,113 @@ async function generate(config, existing) {
|
|
|
186
357
|
const source = normalizeTypes(raw, checksum, schema.body);
|
|
187
358
|
return { changed: source !== (existing ?? null), checksum, source, types: typeNames(source) };
|
|
188
359
|
}
|
|
360
|
+
// ------------------------------------------- from a key's GraphQL schema ---
|
|
361
|
+
/**
|
|
362
|
+
* `/v2/schema/types`'s type map and preamble, so a `cap_` key, which `/v2`
|
|
363
|
+
* does not take, gets the interfaces a legacy key gets. Copied from
|
|
364
|
+
* `apps/api/src/routes/v2/schema.ts`, which is frozen legacy; a test holds the
|
|
365
|
+
* two equal.
|
|
366
|
+
*/
|
|
367
|
+
exports.LEGACY_TYPE_MAP = {
|
|
368
|
+
string: "string",
|
|
369
|
+
markdown: "string",
|
|
370
|
+
html: "string",
|
|
371
|
+
code: "string",
|
|
372
|
+
color: "string",
|
|
373
|
+
number: "number",
|
|
374
|
+
true_false: "boolean",
|
|
375
|
+
date: "string",
|
|
376
|
+
image: "CapaImage",
|
|
377
|
+
video: "CapaVideo",
|
|
378
|
+
file: "CapaFile",
|
|
379
|
+
json: "Record<string, unknown>",
|
|
380
|
+
rich_text: "string",
|
|
381
|
+
};
|
|
382
|
+
exports.LEGACY_PREAMBLE = `export interface CapaImage {
|
|
383
|
+
url: string;
|
|
384
|
+
alt?: string;
|
|
385
|
+
width?: number;
|
|
386
|
+
height?: number;
|
|
387
|
+
filesize?: number;
|
|
388
|
+
filename?: string;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export interface CapaVideo {
|
|
392
|
+
url: string;
|
|
393
|
+
thumbnail?: string;
|
|
394
|
+
duration?: number;
|
|
395
|
+
width?: number;
|
|
396
|
+
height?: number;
|
|
397
|
+
filesize?: number;
|
|
398
|
+
filename?: string;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export interface CapaFile {
|
|
402
|
+
url: string;
|
|
403
|
+
filename: string;
|
|
404
|
+
filesize?: number;
|
|
405
|
+
type?: string;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export interface CapaInstance<T = Record<string, unknown>> {
|
|
409
|
+
id: string;
|
|
410
|
+
title?: string;
|
|
411
|
+
slug?: string;
|
|
412
|
+
data: T;
|
|
413
|
+
status: 'draft' | 'published';
|
|
414
|
+
createdAt: string;
|
|
415
|
+
updatedAt: string;
|
|
416
|
+
publishedAt?: string;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
`;
|
|
420
|
+
/** A field's type as `/v2/schema/types` writes it, from what GraphQL says of it. */
|
|
421
|
+
function legacyFieldType(field) {
|
|
422
|
+
// GraphQL types an enum as String, and lists its values for people only.
|
|
423
|
+
const scalar = (type) => (type === "enum" ? "string" : exports.LEGACY_TYPE_MAP[type ?? ""] ?? "unknown");
|
|
424
|
+
const target = field.target === null ? "unknown" : toPascalCase(field.target);
|
|
425
|
+
if (field.capaType === "relation")
|
|
426
|
+
return target;
|
|
427
|
+
if (field.capaType === "array")
|
|
428
|
+
return `${field.arrayType === "relation" ? target : scalar(field.arrayType)}[]`;
|
|
429
|
+
return scalar(field.capaType);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* The REST model interfaces for a key's GraphQL schema: what
|
|
433
|
+
* `/v2/schema/types` and `/v2/schema` give a legacy key, for a `cap_` key.
|
|
434
|
+
* The model namespaces, field namespaces, Capa types and relation targets
|
|
435
|
+
* come from the schema's descriptions (N9), so the interfaces are keyed as
|
|
436
|
+
* REST reads them (`open-time`), not as GraphQL renames them (`open_time`).
|
|
437
|
+
*
|
|
438
|
+
* The text is written as the legacy route writes it and goes through
|
|
439
|
+
* `normalizeTypes`, the one place that turns that text into a module. Three
|
|
440
|
+
* things GraphQL does not say are written as it can: every field is optional,
|
|
441
|
+
* since the schema has no required flag; an enum is `string`, since it lists
|
|
442
|
+
* no values; and a field GraphQL leaves out (its description's `Not exposed:`
|
|
443
|
+
* line) is `unknown`. No checksum is stamped: `CAPA_SCHEMA_CHECKSUM` is the
|
|
444
|
+
* `/v2/schema` checksum, which a `cap_` key cannot read.
|
|
445
|
+
*/
|
|
446
|
+
function restTypesFromIntrospection(introspection) {
|
|
447
|
+
const summary = (0, summary_1.summarizeIntrospection)(introspection);
|
|
448
|
+
let text = `// Auto-generated Capa CMS types\n\n${exports.LEGACY_PREAMBLE}`;
|
|
449
|
+
const models = [];
|
|
450
|
+
for (const model of summary.models) {
|
|
451
|
+
text += `export interface ${toPascalCase(model.namespace)} {\n`;
|
|
452
|
+
for (const field of model.fields)
|
|
453
|
+
text += ` ${field.namespace}?: ${legacyFieldType(field)};\n`;
|
|
454
|
+
for (const namespace of model.notExposed)
|
|
455
|
+
text += ` ${namespace}?: unknown;\n`;
|
|
456
|
+
text += "}\n\n";
|
|
457
|
+
models.push({
|
|
458
|
+
namespace: model.namespace,
|
|
459
|
+
fields: model.fields.map((field) => ({
|
|
460
|
+
namespace: field.namespace,
|
|
461
|
+
type: field.capaType,
|
|
462
|
+
arrayType: field.arrayType,
|
|
463
|
+
relationRef: field.target,
|
|
464
|
+
})),
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
const source = normalizeTypes(text, null, { models });
|
|
468
|
+
return { source, types: typeNames(source), restOnly: summary.restOnly };
|
|
469
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,42 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Client configuration, and the one thing about it that is not obvious.
|
|
3
|
-
*
|
|
4
|
-
* PREVIEW IS A KEY, NOT A FLAG
|
|
5
|
-
* Capa gates unpublished content on the API key's `environment` column, not on
|
|
6
|
-
* anything in the request:
|
|
7
|
-
*
|
|
8
|
-
* // apps/api/src/routes/v2/api.ts:203
|
|
9
|
-
* const environment = req.apiKeyEnvironment || "production";
|
|
10
|
-
* const includeDrafted = environment === "production" ? false : true;
|
|
11
|
-
*
|
|
12
|
-
* So there is no `?preview=true` to pass, and `getContent(ns, id, {preview})`
|
|
13
|
-
* CANNOT be implemented against a published key — the server would ignore it.
|
|
14
|
-
* The honest surface is one client per key:
|
|
15
|
-
*
|
|
16
|
-
* const capa = createClient({ ...cfg, apiKey: PUBLISHED_KEY })
|
|
17
|
-
* const preview = createClient({ ...cfg, apiKey: PREVIEW_KEY })
|
|
18
|
-
*
|
|
19
|
-
* Note also that the comparison above is exact and case-sensitive against a
|
|
20
|
-
* free-text column, so ANY environment that is not literally "production"
|
|
21
|
-
* returns drafts — `staging`, `development`, `draft`, and equally a typo like
|
|
22
|
-
* "Production".
|
|
23
|
-
*
|
|
24
|
-
* AND A CLIENT CANNOT FIND OUT WHICH IT HAS.
|
|
25
|
-
* There is deliberately no `includesDrafts()` here, because it cannot be
|
|
26
|
-
* implemented: `apiKeyEnvironment` is set in verifyApiKey.ts:57, consumed
|
|
27
|
-
* internally to compute `includeDrafted`, and returned to the caller by NO
|
|
28
|
-
* endpoint. /v2/schema carries only {models, relations, checksum, generatedAt}.
|
|
29
|
-
*
|
|
30
|
-
* So a site handed a `draft`, `staging` or `development` key serves unpublished
|
|
31
|
-
* content to the public, and has no way to detect it — not at startup, not at
|
|
32
|
-
* runtime, not from any response. In production-shaped data 29 of 74 keys are
|
|
33
|
-
* non-production. Whatever this SDK offers, it cannot make that safe; the fix
|
|
34
|
-
* is for Capa to report the key's environment on a read a client already makes.
|
|
35
|
-
*/
|
|
36
1
|
export interface CapaConfig {
|
|
37
2
|
/** Base URL of the Capa API, e.g. https://api.example.com. No trailing slash required. */
|
|
38
3
|
baseUrl: string;
|
|
39
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* A legacy tenant API key: `pk_`, `sk_` or unprefixed. Read scope is all the
|
|
6
|
+
* client needs. A `cap_` key reads `/api/`, with `createClient` from
|
|
7
|
+
* `@capacms/sdk/next`, and is refused here.
|
|
8
|
+
*/
|
|
40
9
|
apiKey: string;
|
|
41
10
|
/** The tenant the key belongs to. */
|
|
42
11
|
tenantId: string;
|
package/dist/config.js
CHANGED
|
@@ -1,13 +1,59 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.resolveConfig = resolveConfig;
|
|
4
|
+
/**
|
|
5
|
+
* Client configuration, and the one thing about it that is not obvious.
|
|
6
|
+
*
|
|
7
|
+
* PREVIEW IS A KEY, NOT A FLAG
|
|
8
|
+
* Capa gates unpublished content on the API key's `environment` column, not on
|
|
9
|
+
* anything in the request:
|
|
10
|
+
*
|
|
11
|
+
* // apps/api/src/routes/v2/api.ts:203
|
|
12
|
+
* const environment = req.apiKeyEnvironment || "production";
|
|
13
|
+
* const includeDrafted = environment === "production" ? false : true;
|
|
14
|
+
*
|
|
15
|
+
* So there is no `?preview=true` to pass, and `getContent(ns, id, {preview})`
|
|
16
|
+
* CANNOT be implemented against a published key — the server would ignore it.
|
|
17
|
+
* The honest surface is one client per key:
|
|
18
|
+
*
|
|
19
|
+
* const capa = createClient({ ...cfg, apiKey: PUBLISHED_KEY })
|
|
20
|
+
* const preview = createClient({ ...cfg, apiKey: PREVIEW_KEY })
|
|
21
|
+
*
|
|
22
|
+
* Note also that the comparison above is exact and case-sensitive against a
|
|
23
|
+
* free-text column, so ANY environment that is not literally "production"
|
|
24
|
+
* returns drafts — `staging`, `development`, `draft`, and equally a typo like
|
|
25
|
+
* "Production".
|
|
26
|
+
*
|
|
27
|
+
* AND A CLIENT CANNOT FIND OUT WHICH IT HAS.
|
|
28
|
+
* There is deliberately no `includesDrafts()` here, because it cannot be
|
|
29
|
+
* implemented: `apiKeyEnvironment` is set in verifyApiKey.ts:57, consumed
|
|
30
|
+
* internally to compute `includeDrafted`, and returned to the caller by NO
|
|
31
|
+
* endpoint. /v2/schema carries only {models, relations, checksum, generatedAt}.
|
|
32
|
+
*
|
|
33
|
+
* So a site handed a `draft`, `staging` or `development` key serves unpublished
|
|
34
|
+
* content to the public, and has no way to detect it — not at startup, not at
|
|
35
|
+
* runtime, not from any response. In production-shaped data 29 of 74 keys are
|
|
36
|
+
* non-production. Whatever this SDK offers, it cannot make that safe; the fix
|
|
37
|
+
* is for Capa to report the key's environment on a read a client already makes.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* The platform fetch, called through `globalThis` on every request. Storing
|
|
41
|
+
* `globalThis.fetch` and calling it as a method of the config object gives it
|
|
42
|
+
* the wrong `this`, and a browser refuses that ("Illegal invocation"). Looking
|
|
43
|
+
* it up per call also picks up a fetch a framework patches in later.
|
|
44
|
+
*/
|
|
45
|
+
function defaultFetch() {
|
|
46
|
+
if (typeof globalThis.fetch !== "function")
|
|
47
|
+
return undefined;
|
|
48
|
+
return ((input, init) => globalThis.fetch(input, init));
|
|
49
|
+
}
|
|
4
50
|
function resolveConfig(config) {
|
|
5
51
|
const missing = ["baseUrl", "apiKey", "tenantId"].filter((k) => !config[k]);
|
|
6
52
|
if (missing.length) {
|
|
7
53
|
throw new Error(`@capacms/sdk: missing ${missing.join(", ")}. ` +
|
|
8
54
|
`createClient needs baseUrl, apiKey and tenantId.`);
|
|
9
55
|
}
|
|
10
|
-
const fetchImpl = config.fetch ??
|
|
56
|
+
const fetchImpl = config.fetch ?? defaultFetch();
|
|
11
57
|
if (typeof fetchImpl !== "function") {
|
|
12
58
|
throw new Error("@capacms/sdk: no fetch available. Pass one via config.fetch on older runtimes.");
|
|
13
59
|
}
|