@capacms/sdk 1.0.0-next.3 → 1.0.0-next.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +323 -0
- package/README.md +1163 -186
- package/bin/capa-codegen.js +192 -5
- package/bin/capa.js +208 -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 -1
- package/dist/graphql-codegen.d.ts +117 -0
- package/dist/graphql-codegen.js +705 -0
- package/dist/http.js +1 -1
- 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 +160 -31
- package/dist/next/client.js +178 -89
- 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 +29 -4
- package/dist/next/index.js +31 -1
- package/dist/next/inflate.d.ts +51 -0
- package/dist/next/inflate.js +243 -0
- package/dist/next/key-family.d.ts +34 -0
- package/dist/next/key-family.js +74 -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/index.d.ts +333 -6
- package/dist/nextjs/index.js +450 -4
- package/dist/nextjs/overlay.d.ts +5 -0
- package/dist/nextjs/overlay.js +35 -0
- package/package.json +35 -13
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
|
@@ -36,7 +36,11 @@
|
|
|
36
36
|
export interface CapaConfig {
|
|
37
37
|
/** Base URL of the Capa API, e.g. https://api.example.com. No trailing slash required. */
|
|
38
38
|
baseUrl: string;
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* A legacy tenant API key: `pk_`, `sk_` or unprefixed. Read scope is all the
|
|
41
|
+
* client needs. A `cap_` key reads `/api/`, with `createClient` from
|
|
42
|
+
* `@capacms/sdk/next`, and is refused here.
|
|
43
|
+
*/
|
|
40
44
|
apiKey: string;
|
|
41
45
|
/** The tenant the key belongs to. */
|
|
42
46
|
tenantId: string;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* graphql-codegen.ts — `capa-codegen --graphql` and `capa persist`, the pure half.
|
|
3
|
+
*
|
|
4
|
+
* Reads a key's schema (introspection JSON) and a project's GraphQL documents,
|
|
5
|
+
* validates every document against that schema, and writes ONE TypeScript
|
|
6
|
+
* module holding:
|
|
7
|
+
*
|
|
8
|
+
* - the schema as types, for `createClient<CapaQuery>()` and the typed
|
|
9
|
+
* builder (`client.graphql.query`);
|
|
10
|
+
* - one `TypedDocument` constant per named operation, so
|
|
11
|
+
* `client.graphql(ArticlesPageDocument, vars)` infers the result and the
|
|
12
|
+
* variables with no cast, and beside it `<Name>Models`, the models it
|
|
13
|
+
* reads, for its Next.js cache tags;
|
|
14
|
+
* - for each document written as a literal (`#graphql`, `/* capa *\/` or
|
|
15
|
+
* gql(`...`)), an entry in `CapaDocuments` keyed by its exact text, so
|
|
16
|
+
* `client.graphql(LITERAL, vars)` infers them too, with no import;
|
|
17
|
+
* - `capaTreeLayout`, what `toTree` reads of the schema, so a server lays a
|
|
18
|
+
* builder result out in REST's shape without reading the schema first.
|
|
19
|
+
*
|
|
20
|
+
* The file walk and the network live in `bin/`; everything here is a function
|
|
21
|
+
* of its inputs, so it is tested without either.
|
|
22
|
+
*
|
|
23
|
+
* `graphql` (graphql-js) is loaded on first use, never at import: it is an
|
|
24
|
+
* optional peer dependency that only these commands need, and a site that
|
|
25
|
+
* only calls `client.graphql()` must not pay for it.
|
|
26
|
+
*/
|
|
27
|
+
import type * as GraphQLJs from "graphql";
|
|
28
|
+
import type { CapaIntrospection } from "./next/graphql/introspection";
|
|
29
|
+
/** graphql-js, or an error that says how to get it. */
|
|
30
|
+
export declare function loadGraphQL(): typeof GraphQLJs;
|
|
31
|
+
/** One GraphQL document found in a project, with where it came from. */
|
|
32
|
+
export interface DocumentSource {
|
|
33
|
+
file: string;
|
|
34
|
+
/** 1-based line of the document's first character in `file`. */
|
|
35
|
+
line: number;
|
|
36
|
+
/** The document's text as the program holds it at run time: a template's escapes are applied. */
|
|
37
|
+
text: string;
|
|
38
|
+
/**
|
|
39
|
+
* True for a literal whose type is its text (`#graphql`, `/* capa *\/`,
|
|
40
|
+
* gql(`...`)): it is sent exactly as written, and codegen keys its types by
|
|
41
|
+
* that text.
|
|
42
|
+
*/
|
|
43
|
+
literal?: true;
|
|
44
|
+
/** The variable a literal is assigned to (`const CARD = ...`), which another literal's `${CARD}` names. */
|
|
45
|
+
name?: string;
|
|
46
|
+
/**
|
|
47
|
+
* For a literal with `${NAME}` in it: the text around each `${}` and the
|
|
48
|
+
* names in them, resolved against the project's other literals before the
|
|
49
|
+
* literal is read. `text` holds the template with its `${NAME}`s meanwhile.
|
|
50
|
+
*/
|
|
51
|
+
template?: {
|
|
52
|
+
segments: string[];
|
|
53
|
+
names: string[];
|
|
54
|
+
asConst: boolean;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** A template codegen does not read, and why: the CLI prints each one, so none is dropped in silence. */
|
|
58
|
+
export interface SkippedDocument extends DocumentSource {
|
|
59
|
+
reason: string;
|
|
60
|
+
}
|
|
61
|
+
/** A template's text at run time, from its source text: line ends as LF, escapes applied. */
|
|
62
|
+
export declare function cookTemplate(raw: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* The GraphQL documents in one file: the whole file for `.graphql` and `.gql`,
|
|
65
|
+
* and elsewhere every template that is marked as one: a `#graphql` first
|
|
66
|
+
* line, a `/* capa *\/` comment before it, or `gql` as a tag or a function.
|
|
67
|
+
* One on a comment line is an example, not a document.
|
|
68
|
+
*
|
|
69
|
+
* `skipped` names every template codegen will not read that looks meant for
|
|
70
|
+
* it, with the reason: one with `${}` inside (its text is only known at run
|
|
71
|
+
* time, so it cannot be validated or hashed ahead of it), a named operation
|
|
72
|
+
* left unmarked, and `/nextjs`'s `graphql` used as a tag.
|
|
73
|
+
*/
|
|
74
|
+
export declare function extractDocuments(file: string, text: string): {
|
|
75
|
+
documents: DocumentSource[];
|
|
76
|
+
skipped: SkippedDocument[];
|
|
77
|
+
};
|
|
78
|
+
export interface CodegenProblem {
|
|
79
|
+
file: string;
|
|
80
|
+
line: number;
|
|
81
|
+
column: number;
|
|
82
|
+
message: string;
|
|
83
|
+
}
|
|
84
|
+
export interface GeneratedOperation {
|
|
85
|
+
name: string;
|
|
86
|
+
/** The exact text of its `<Name>Document`, and hashed for persisted queries. */
|
|
87
|
+
document: string;
|
|
88
|
+
sha256: string;
|
|
89
|
+
/** The namespaces of the models it reads, written as `<Name>Models` for its cache tags. */
|
|
90
|
+
models: string[];
|
|
91
|
+
/** For a document written as a literal: the literal's text, which is what a call with it sends, and its hash. */
|
|
92
|
+
literal?: {
|
|
93
|
+
document: string;
|
|
94
|
+
sha256: string;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export interface GraphQLCodegenResult {
|
|
98
|
+
/** The module to write. Null when a document failed. */
|
|
99
|
+
source: string | null;
|
|
100
|
+
operations: GeneratedOperation[];
|
|
101
|
+
problems: CodegenProblem[];
|
|
102
|
+
/**
|
|
103
|
+
* Each use of a deprecated field, argument, input field or enum value, with
|
|
104
|
+
* the reason the schema gives: it still works, and a later Capa-Version
|
|
105
|
+
* removes it.
|
|
106
|
+
*/
|
|
107
|
+
warnings: CodegenProblem[];
|
|
108
|
+
/** Literals with a `${}` that names no literal of the project, so their text is only known at run time. */
|
|
109
|
+
skipped: SkippedDocument[];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The generated module for a schema and a project's documents. Problems (a
|
|
113
|
+
* syntax error, a field the key cannot read, an unnamed operation, a name used
|
|
114
|
+
* twice) are returned with their file and line, and no module is produced
|
|
115
|
+
* while any remain.
|
|
116
|
+
*/
|
|
117
|
+
export declare function generateGraphQLModule(introspection: CapaIntrospection, sources: DocumentSource[]): GraphQLCodegenResult;
|