@capacms/sdk 1.0.0-next.4 → 1.0.0-next.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +345 -0
- package/README.md +1069 -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 -36
- package/dist/config.js +47 -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 +51 -12
- package/dist/next/attrs.js +74 -20
- package/dist/next/client.d.ts +112 -38
- package/dist/next/client.js +131 -83
- 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 +28 -5
- package/dist/next/index.js +25 -1
- package/dist/next/inflate.d.ts +25 -7
- package/dist/next/inflate.js +46 -32
- package/dist/next/key-family.d.ts +34 -0
- package/dist/next/key-family.js +74 -0
- package/dist/next/select-types.d.ts +44 -8
- package/dist/next/system-keys.d.ts +27 -0
- package/dist/next/system-keys.js +42 -0
- package/dist/nextjs/index.d.ts +174 -12
- package/dist/nextjs/index.js +270 -23
- package/dist/nextjs/overlay.d.ts +5 -0
- package/dist/nextjs/overlay.js +35 -0
- package/package.json +31 -13
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* errors.ts — what a failed `/api/` call throws.
|
|
4
|
+
*
|
|
5
|
+
* `CapaError` is one refusal of the whole request: the REST envelope's
|
|
6
|
+
* `{ error }`, or a GraphQL response with `errors` and no `data`, whatever
|
|
7
|
+
* its status. `CapaGraphQLError` is one entry of a GraphQL `errors` array. A
|
|
8
|
+
* GraphQL request the API ran resolves with those in `errors` rather than
|
|
9
|
+
* throwing, because its `data` carries the root fields that did succeed. They
|
|
10
|
+
* are returned, not thrown, so they are plain objects: a result goes through
|
|
11
|
+
* `Response.json` or into a client component's props with every field kept.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.CapaError = void 0;
|
|
15
|
+
exports.retryAfterOf = retryAfterOf;
|
|
16
|
+
exports.isCapaError = isCapaError;
|
|
17
|
+
exports.optionalString = optionalString;
|
|
18
|
+
exports.unparseable = unparseable;
|
|
19
|
+
exports.errorFromEnvelope = errorFromEnvelope;
|
|
20
|
+
exports.isCapaGraphQLError = isCapaGraphQLError;
|
|
21
|
+
exports.graphqlErrorsOf = graphqlErrorsOf;
|
|
22
|
+
exports.errorFromGraphQLBody = errorFromGraphQLBody;
|
|
23
|
+
exports.errorFromGraphQLErrors = errorFromGraphQLErrors;
|
|
24
|
+
exports.graphqlNotServed = graphqlNotServed;
|
|
25
|
+
class CapaError extends Error {
|
|
26
|
+
status;
|
|
27
|
+
type;
|
|
28
|
+
code;
|
|
29
|
+
param;
|
|
30
|
+
hint;
|
|
31
|
+
requestId;
|
|
32
|
+
docs;
|
|
33
|
+
/**
|
|
34
|
+
* Every error of a refused GraphQL request, in order. The fields above come
|
|
35
|
+
* from the first one. Empty for a REST call.
|
|
36
|
+
*/
|
|
37
|
+
graphqlErrors;
|
|
38
|
+
/**
|
|
39
|
+
* Seconds the API asked the caller to wait before trying again, from a 429's
|
|
40
|
+
* `Retry-After` (seconds or an HTTP date). Undefined when it sent none.
|
|
41
|
+
*/
|
|
42
|
+
retryAfter;
|
|
43
|
+
constructor(input) {
|
|
44
|
+
super(input.message);
|
|
45
|
+
this.name = "CapaError";
|
|
46
|
+
this.status = input.status;
|
|
47
|
+
this.type = input.type;
|
|
48
|
+
this.code = input.code;
|
|
49
|
+
this.param = input.param;
|
|
50
|
+
this.hint = input.hint;
|
|
51
|
+
this.requestId = input.requestId;
|
|
52
|
+
this.docs = input.docs;
|
|
53
|
+
this.graphqlErrors = input.graphqlErrors ?? [];
|
|
54
|
+
if (input.retryAfter !== undefined)
|
|
55
|
+
this.retryAfter = input.retryAfter;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.CapaError = CapaError;
|
|
59
|
+
/**
|
|
60
|
+
* A `Retry-After` header in seconds: delta-seconds as sent, an HTTP date as
|
|
61
|
+
* the whole seconds from `now` until it (0 once it has passed). Undefined when
|
|
62
|
+
* the header is absent or neither.
|
|
63
|
+
*/
|
|
64
|
+
function retryAfterOf(value, now = Date.now()) {
|
|
65
|
+
if (value === null || value === undefined)
|
|
66
|
+
return undefined;
|
|
67
|
+
const text = value.trim();
|
|
68
|
+
if (/^\d+$/.test(text))
|
|
69
|
+
return Number(text);
|
|
70
|
+
const at = Date.parse(text);
|
|
71
|
+
if (Number.isNaN(at))
|
|
72
|
+
return undefined;
|
|
73
|
+
return Math.max(0, Math.ceil((at - now) / 1000));
|
|
74
|
+
}
|
|
75
|
+
function isCapaError(error) {
|
|
76
|
+
return error instanceof CapaError;
|
|
77
|
+
}
|
|
78
|
+
function optionalString(value) {
|
|
79
|
+
return typeof value === "string" ? value : undefined;
|
|
80
|
+
}
|
|
81
|
+
function unparseable(status, requestId) {
|
|
82
|
+
return new CapaError({
|
|
83
|
+
status,
|
|
84
|
+
type: "api_error",
|
|
85
|
+
code: "unparseable_response",
|
|
86
|
+
message: "Capa returned a response that was not a valid /api/ JSON envelope.",
|
|
87
|
+
requestId,
|
|
88
|
+
docs: "",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function errorFromEnvelope(status, body, fallbackRequestId, retryAfter) {
|
|
92
|
+
const detail = body.error;
|
|
93
|
+
if (!detail ||
|
|
94
|
+
typeof detail.type !== "string" ||
|
|
95
|
+
typeof detail.code !== "string" ||
|
|
96
|
+
typeof detail.message !== "string" ||
|
|
97
|
+
typeof detail.docs !== "string") {
|
|
98
|
+
return unparseable(status, fallbackRequestId);
|
|
99
|
+
}
|
|
100
|
+
return new CapaError({
|
|
101
|
+
status,
|
|
102
|
+
type: detail.type,
|
|
103
|
+
code: detail.code,
|
|
104
|
+
message: detail.message,
|
|
105
|
+
param: optionalString(detail.param),
|
|
106
|
+
hint: optionalString(detail.hint),
|
|
107
|
+
requestId: optionalString(body.meta?.requestId) ?? fallbackRequestId,
|
|
108
|
+
docs: detail.docs,
|
|
109
|
+
retryAfter,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/** The string values of `keys` in `from`, leaving out each one that is absent or not a string. */
|
|
113
|
+
function stringsOf(from, keys) {
|
|
114
|
+
const out = {};
|
|
115
|
+
for (const key of keys) {
|
|
116
|
+
const value = optionalString(from[key]);
|
|
117
|
+
if (value !== undefined)
|
|
118
|
+
out[key] = value;
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
function capaGraphQLError(item) {
|
|
123
|
+
const extensions = item.extensions && typeof item.extensions === "object" ? item.extensions : {};
|
|
124
|
+
return {
|
|
125
|
+
message: item.message,
|
|
126
|
+
...(Array.isArray(item.locations) ? { locations: item.locations } : {}),
|
|
127
|
+
...(Array.isArray(item.path) ? { path: item.path } : {}),
|
|
128
|
+
extensions,
|
|
129
|
+
code: optionalString(extensions.code) ?? "unknown",
|
|
130
|
+
...stringsOf(extensions, ["type", "param", "hint", "docs"]),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** Whether `value` is a `CapaGraphQLError`: by its shape, so one that went through JSON is one too. */
|
|
134
|
+
function isCapaGraphQLError(value) {
|
|
135
|
+
if (!value || typeof value !== "object" || value instanceof Error)
|
|
136
|
+
return false;
|
|
137
|
+
const error = value;
|
|
138
|
+
return (typeof error.message === "string" &&
|
|
139
|
+
typeof error.code === "string" &&
|
|
140
|
+
!!error.extensions &&
|
|
141
|
+
typeof error.extensions === "object" &&
|
|
142
|
+
!Array.isArray(error.extensions));
|
|
143
|
+
}
|
|
144
|
+
/** Turn a parsed `errors` value into typed errors, skipping anything that is not one. */
|
|
145
|
+
function graphqlErrorsOf(value) {
|
|
146
|
+
if (!Array.isArray(value))
|
|
147
|
+
return [];
|
|
148
|
+
return value
|
|
149
|
+
.filter((item) => !!item && typeof item === "object" && typeof item.message === "string")
|
|
150
|
+
.map(capaGraphQLError);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* A GraphQL request the API refused as a whole: a status other than 200, or
|
|
154
|
+
* a 200 with `errors` and no `data`, passed in as the refusal's `status`. The
|
|
155
|
+
* `CapaError` fields come from the first error, and `graphqlErrors` holds all
|
|
156
|
+
* of them. A body in the REST envelope shape, which a proxy or an older API
|
|
157
|
+
* can send, is read as one.
|
|
158
|
+
*/
|
|
159
|
+
function errorFromGraphQLBody(status, body, fallbackRequestId, retryAfter) {
|
|
160
|
+
const object = body && typeof body === "object" ? body : {};
|
|
161
|
+
const errors = graphqlErrorsOf(object.errors);
|
|
162
|
+
if (errors.length === 0)
|
|
163
|
+
return errorFromEnvelope(status, object, fallbackRequestId, retryAfter);
|
|
164
|
+
return errorFromGraphQLErrors(status, errors, fallbackRequestId, retryAfter);
|
|
165
|
+
}
|
|
166
|
+
/** A `CapaError` for errors already read: the fields of the first, and all of them in `graphqlErrors`. */
|
|
167
|
+
function errorFromGraphQLErrors(status, errors, fallbackRequestId, retryAfter) {
|
|
168
|
+
const [first] = errors;
|
|
169
|
+
return new CapaError({
|
|
170
|
+
status,
|
|
171
|
+
type: first.type ?? "api_error",
|
|
172
|
+
code: first.code,
|
|
173
|
+
message: first.message,
|
|
174
|
+
param: first.param,
|
|
175
|
+
hint: first.hint,
|
|
176
|
+
requestId: optionalString(first.extensions.requestId) ?? fallbackRequestId,
|
|
177
|
+
docs: first.docs ?? "",
|
|
178
|
+
graphqlErrors: errors,
|
|
179
|
+
retryAfter,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* `/api/graphql` answered as a path the API does not serve: a POST
|
|
184
|
+
* `405 mutations_not_enabled`, a GET `404 route_not_found`, in the REST
|
|
185
|
+
* envelope. `CAPA_API_GRAPHQL=off` does that, and so does the admin host for a
|
|
186
|
+
* GET, since it serves GraphQL by POST only. The GraphQL handler never answers
|
|
187
|
+
* in that envelope (a real mutation's refusal is `{ errors }`), so the
|
|
188
|
+
* envelope is what tells "GraphQL is not here" from "this document was
|
|
189
|
+
* refused", and a query is never told it tried to write. Null for any other
|
|
190
|
+
* answer.
|
|
191
|
+
*/
|
|
192
|
+
function graphqlNotServed(status, body, method, fallbackRequestId) {
|
|
193
|
+
if (status !== 404 && status !== 405)
|
|
194
|
+
return null;
|
|
195
|
+
const object = body && typeof body === "object" ? body : {};
|
|
196
|
+
if (Array.isArray(object.errors))
|
|
197
|
+
return null;
|
|
198
|
+
const refusal = errorFromEnvelope(status, object, fallbackRequestId);
|
|
199
|
+
if (refusal.code !== "mutations_not_enabled" && refusal.code !== "route_not_found")
|
|
200
|
+
return null;
|
|
201
|
+
const byGet = method === "GET";
|
|
202
|
+
return new CapaError({
|
|
203
|
+
status,
|
|
204
|
+
type: refusal.type,
|
|
205
|
+
code: refusal.code,
|
|
206
|
+
message: byGet ? "This Capa host does not serve GraphQL by GET." : "This Capa deployment does not serve GraphQL.",
|
|
207
|
+
hint: byGet
|
|
208
|
+
? "GraphQL is switched off here (CAPA_API_GRAPHQL=off), or this is the admin host, which serves GraphQL by POST only. " +
|
|
209
|
+
'Send it by POST (method: "POST", without persisted); if that is refused too, read with client.entries.list or client.entries.get.'
|
|
210
|
+
: "GraphQL is switched off here (CAPA_API_GRAPHQL=off). Read the same content with client.entries.list or client.entries.get until it is back on.",
|
|
211
|
+
requestId: refusal.requestId,
|
|
212
|
+
docs: refusal.docs,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare const NAME_QUOTE = "\"";
|
|
2
|
+
/** Whether REST can name a field namespace at all: every one but an empty one or one starting with `$`. */
|
|
3
|
+
export declare function isNameable(namespace: string): boolean;
|
|
4
|
+
/** A namespace as every REST grammar writes it: bare where it can be, quoted otherwise. */
|
|
5
|
+
export declare function writeName(namespace: string): string;
|
|
6
|
+
/** One segment of a sort or filter path: a system key (`$id`) keeps its sigil, a namespace is written by `writeName`. */
|
|
7
|
+
export declare function writePathSegment(segment: string): string;
|
|
8
|
+
/** A path (`["at.place", "zip.code"]`) as sort and where write it: `"at.place"."zip.code"`. */
|
|
9
|
+
export declare function writePath(path: readonly string[]): string;
|
|
10
|
+
/** The quoted name opening at `text[pos]`, and the index just past its closing quote; null when it never closes. */
|
|
11
|
+
export declare function readQuoted(text: string, pos: number): {
|
|
12
|
+
name: string;
|
|
13
|
+
end: number;
|
|
14
|
+
} | null;
|
|
15
|
+
/** A name as written, read back: `"price.usd"` is `price.usd`, a bare name is itself. Null for a quote that does not close the name. */
|
|
16
|
+
export declare function readName(text: string): string | null;
|
|
17
|
+
/**
|
|
18
|
+
* `text` split on `separator` wherever it stands outside a quoted name and,
|
|
19
|
+
* unless `nested` is false, outside parentheses: a select level is split on
|
|
20
|
+
* its commas outside both, a list of names on its commas outside quotes.
|
|
21
|
+
* Null when a quote never closes.
|
|
22
|
+
*/
|
|
23
|
+
export declare function splitOutside(text: string, separator: string, { nested }?: {
|
|
24
|
+
nested?: boolean | undefined;
|
|
25
|
+
}): string[] | null;
|
|
26
|
+
/** One segment of a sort or filter path, read back, and whether it was quoted: a quoted one is always a field. */
|
|
27
|
+
export interface NameSegment {
|
|
28
|
+
name: string;
|
|
29
|
+
quoted: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A sort or filter path's segments, split on the dots outside quotes, each
|
|
33
|
+
* read back (`"at.place"."zip.code"` is `at.place` then `zip.code`). A bare
|
|
34
|
+
* segment is everything up to the next dot, as the API reads it. Null when a
|
|
35
|
+
* quote never closes or runs into something other than a dot.
|
|
36
|
+
*/
|
|
37
|
+
export declare function readPath(text: string): NameSegment[] | null;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NAME_QUOTE = void 0;
|
|
4
|
+
exports.isNameable = isNameable;
|
|
5
|
+
exports.writeName = writeName;
|
|
6
|
+
exports.writePathSegment = writePathSegment;
|
|
7
|
+
exports.writePath = writePath;
|
|
8
|
+
exports.readQuoted = readQuoted;
|
|
9
|
+
exports.readName = readName;
|
|
10
|
+
exports.splitOutside = splitOutside;
|
|
11
|
+
exports.readPath = readPath;
|
|
12
|
+
/**
|
|
13
|
+
* field-names.ts — how REST's `select`, `sort` and `where` write a field
|
|
14
|
+
* namespace, and how they read one back (spec 17, amendment 121).
|
|
15
|
+
*
|
|
16
|
+
* A namespace is whatever the admin saved, so any character can be in one:
|
|
17
|
+
* `am/pm_indicator`, `price.usd`, `a,b`. All three grammars write it the same
|
|
18
|
+
* way:
|
|
19
|
+
* - BARE when none of its characters means something in any of them and it
|
|
20
|
+
* starts with neither `$` (a system key) nor `-` (a descending sort):
|
|
21
|
+
* `title`, `open-time`, `am/pm_indicator`.
|
|
22
|
+
* - QUOTED otherwise, a quote inside doubled: `"price.usd"`, `"a,b"`,
|
|
23
|
+
* `"say ""hi"""`. A quoted name is always a field, never a system key, a
|
|
24
|
+
* relation hop or a modifier.
|
|
25
|
+
*
|
|
26
|
+
* A namespace that starts with `$` cannot be named at all, since `$tags` is
|
|
27
|
+
* the system key; `select=*` still returns it.
|
|
28
|
+
*
|
|
29
|
+
* The API's own writer is `writeName` in `@capa/shared`. This package ships
|
|
30
|
+
* with no runtime dependencies, so the rule is copied here and in
|
|
31
|
+
* `@capa/mcp`, and test/fixtures/field-names.json pins all three to the same
|
|
32
|
+
* vectors.
|
|
33
|
+
*/
|
|
34
|
+
const system_keys_1 = require("./system-keys");
|
|
35
|
+
exports.NAME_QUOTE = '"';
|
|
36
|
+
/** Characters that mean something in `select`, `sort` or a filter path. */
|
|
37
|
+
const MEANINGFUL = new Set([",", "(", ")", ":", ".", exports.NAME_QUOTE, "*", "[", "]"]);
|
|
38
|
+
/** Whether a character can stand in a bare name. */
|
|
39
|
+
function isNameChar(ch) {
|
|
40
|
+
return !MEANINGFUL.has(ch) && !/\s/.test(ch);
|
|
41
|
+
}
|
|
42
|
+
/** Whether REST can name a field namespace at all: every one but an empty one or one starting with `$`. */
|
|
43
|
+
function isNameable(namespace) {
|
|
44
|
+
return namespace !== "" && !namespace.startsWith(system_keys_1.SYSTEM_KEY_SIGIL);
|
|
45
|
+
}
|
|
46
|
+
/** A namespace as every REST grammar writes it: bare where it can be, quoted otherwise. */
|
|
47
|
+
function writeName(namespace) {
|
|
48
|
+
const bare = isNameable(namespace) && !namespace.startsWith("-") && [...namespace].every(isNameChar);
|
|
49
|
+
if (bare)
|
|
50
|
+
return namespace;
|
|
51
|
+
return `${exports.NAME_QUOTE}${namespace.split(exports.NAME_QUOTE).join(exports.NAME_QUOTE + exports.NAME_QUOTE)}${exports.NAME_QUOTE}`;
|
|
52
|
+
}
|
|
53
|
+
/** One segment of a sort or filter path: a system key (`$id`) keeps its sigil, a namespace is written by `writeName`. */
|
|
54
|
+
function writePathSegment(segment) {
|
|
55
|
+
return segment.startsWith(system_keys_1.SYSTEM_KEY_SIGIL) ? segment : writeName(segment);
|
|
56
|
+
}
|
|
57
|
+
/** A path (`["at.place", "zip.code"]`) as sort and where write it: `"at.place"."zip.code"`. */
|
|
58
|
+
function writePath(path) {
|
|
59
|
+
return path.map(writePathSegment).join(".");
|
|
60
|
+
}
|
|
61
|
+
/** The quoted name opening at `text[pos]`, and the index just past its closing quote; null when it never closes. */
|
|
62
|
+
function readQuoted(text, pos) {
|
|
63
|
+
let name = "";
|
|
64
|
+
let i = pos + 1;
|
|
65
|
+
while (i < text.length) {
|
|
66
|
+
if (text[i] === exports.NAME_QUOTE) {
|
|
67
|
+
if (text[i + 1] !== exports.NAME_QUOTE)
|
|
68
|
+
return { name, end: i + 1 };
|
|
69
|
+
name += exports.NAME_QUOTE;
|
|
70
|
+
i += 2;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
name += text[i];
|
|
74
|
+
i += 1;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/** A name as written, read back: `"price.usd"` is `price.usd`, a bare name is itself. Null for a quote that does not close the name. */
|
|
79
|
+
function readName(text) {
|
|
80
|
+
if (!text.startsWith(exports.NAME_QUOTE))
|
|
81
|
+
return text;
|
|
82
|
+
const quoted = readQuoted(text, 0);
|
|
83
|
+
return quoted && quoted.end === text.length ? quoted.name : null;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* `text` split on `separator` wherever it stands outside a quoted name and,
|
|
87
|
+
* unless `nested` is false, outside parentheses: a select level is split on
|
|
88
|
+
* its commas outside both, a list of names on its commas outside quotes.
|
|
89
|
+
* Null when a quote never closes.
|
|
90
|
+
*/
|
|
91
|
+
function splitOutside(text, separator, { nested = true } = {}) {
|
|
92
|
+
const parts = [];
|
|
93
|
+
let depth = 0;
|
|
94
|
+
let start = 0;
|
|
95
|
+
let i = 0;
|
|
96
|
+
while (i < text.length) {
|
|
97
|
+
const ch = text[i];
|
|
98
|
+
if (ch === exports.NAME_QUOTE) {
|
|
99
|
+
const quoted = readQuoted(text, i);
|
|
100
|
+
if (!quoted)
|
|
101
|
+
return null;
|
|
102
|
+
i = quoted.end;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (nested && ch === "(")
|
|
106
|
+
depth += 1;
|
|
107
|
+
else if (nested && ch === ")")
|
|
108
|
+
depth -= 1;
|
|
109
|
+
else if (ch === separator && depth === 0) {
|
|
110
|
+
parts.push(text.slice(start, i));
|
|
111
|
+
start = i + 1;
|
|
112
|
+
}
|
|
113
|
+
i += 1;
|
|
114
|
+
}
|
|
115
|
+
parts.push(text.slice(start));
|
|
116
|
+
return parts;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A sort or filter path's segments, split on the dots outside quotes, each
|
|
120
|
+
* read back (`"at.place"."zip.code"` is `at.place` then `zip.code`). A bare
|
|
121
|
+
* segment is everything up to the next dot, as the API reads it. Null when a
|
|
122
|
+
* quote never closes or runs into something other than a dot.
|
|
123
|
+
*/
|
|
124
|
+
function readPath(text) {
|
|
125
|
+
const segments = [];
|
|
126
|
+
let i = 0;
|
|
127
|
+
for (;;) {
|
|
128
|
+
if (text[i] === exports.NAME_QUOTE) {
|
|
129
|
+
const quoted = readQuoted(text, i);
|
|
130
|
+
if (!quoted || (quoted.end < text.length && text[quoted.end] !== "."))
|
|
131
|
+
return null;
|
|
132
|
+
segments.push({ name: quoted.name, quoted: true });
|
|
133
|
+
i = quoted.end;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const dot = text.indexOf(".", i);
|
|
137
|
+
const end = dot === -1 ? text.length : dot;
|
|
138
|
+
segments.push({ name: text.slice(i, end), quoted: false });
|
|
139
|
+
i = end;
|
|
140
|
+
}
|
|
141
|
+
if (i >= text.length)
|
|
142
|
+
return segments;
|
|
143
|
+
i += 1;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* build.ts — a query spec as GraphQL text, in the one format every Capa
|
|
3
|
+
* builder emits (G plan 1.3): two-space indent, `id` first in every entry,
|
|
4
|
+
* variables only for the arguments given, and inline literal arguments on a
|
|
5
|
+
* nested array relation, its cursor included. The Explorer, the MCP server and this SDK are pinned
|
|
6
|
+
* to the same output by `test/fixtures/graphql-vectors.json`.
|
|
7
|
+
*/
|
|
8
|
+
import { type GraphQLQuerySpec, type PlannedQuery } from "./plan";
|
|
9
|
+
import type { GraphQLSchemaSummary } from "./summary";
|
|
10
|
+
export interface BuiltGraphQLQuery {
|
|
11
|
+
query: string;
|
|
12
|
+
variables: Record<string, unknown>;
|
|
13
|
+
operationName: string;
|
|
14
|
+
}
|
|
15
|
+
/** Print a checked plan. */
|
|
16
|
+
export declare function printGraphQL(plan: PlannedQuery): BuiltGraphQLQuery;
|
|
17
|
+
/**
|
|
18
|
+
* Build a GraphQL query from a spec, checked against the key's schema.
|
|
19
|
+
*
|
|
20
|
+
* const schema = await capa.graphqlSchema();
|
|
21
|
+
* const { query, variables } = buildGraphQLQuery(schema, { model: "articles", fields: ["title"], first: 5 });
|
|
22
|
+
* const { data } = await capa.graphql(query, variables);
|
|
23
|
+
*
|
|
24
|
+
* Throws `CapaBuildError`, with `didYouMean`, for a model, field or sort value
|
|
25
|
+
* the key's schema does not have.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildGraphQLQuery(summary: GraphQLSchemaSummary, spec: GraphQLQuerySpec): BuiltGraphQLQuery;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.printGraphQL = printGraphQL;
|
|
4
|
+
exports.buildGraphQLQuery = buildGraphQLQuery;
|
|
5
|
+
/**
|
|
6
|
+
* build.ts — a query spec as GraphQL text, in the one format every Capa
|
|
7
|
+
* builder emits (G plan 1.3): two-space indent, `id` first in every entry,
|
|
8
|
+
* variables only for the arguments given, and inline literal arguments on a
|
|
9
|
+
* nested array relation, its cursor included. The Explorer, the MCP server and this SDK are pinned
|
|
10
|
+
* to the same output by `test/fixtures/graphql-vectors.json`.
|
|
11
|
+
*/
|
|
12
|
+
const plan_1 = require("./plan");
|
|
13
|
+
const INDENT = " ";
|
|
14
|
+
function printFields(fields, depth) {
|
|
15
|
+
const pad = INDENT.repeat(depth);
|
|
16
|
+
const lines = [];
|
|
17
|
+
for (const field of fields) {
|
|
18
|
+
if (!field.children) {
|
|
19
|
+
lines.push(`${pad}${field.name}`);
|
|
20
|
+
}
|
|
21
|
+
else if (field.field?.kind === "relationList") {
|
|
22
|
+
const args = [];
|
|
23
|
+
if (field.first !== undefined)
|
|
24
|
+
args.push(`first: ${field.first}`);
|
|
25
|
+
if (field.sort !== undefined)
|
|
26
|
+
args.push(`sort: ${field.sort}`);
|
|
27
|
+
if (field.after !== undefined)
|
|
28
|
+
args.push(`after: ${JSON.stringify(field.after)}`);
|
|
29
|
+
lines.push(`${pad}${field.name}${args.length ? `(${args.join(", ")})` : ""} {`);
|
|
30
|
+
lines.push(`${pad}${INDENT}nodes {`);
|
|
31
|
+
lines.push(...printFields(field.children, depth + 2));
|
|
32
|
+
lines.push(`${pad}${INDENT}}`);
|
|
33
|
+
lines.push(`${pad}}`);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
lines.push(`${pad}${field.name} {`);
|
|
37
|
+
lines.push(...printFields(field.children, depth + 1));
|
|
38
|
+
lines.push(`${pad}}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return lines;
|
|
42
|
+
}
|
|
43
|
+
/** Print a checked plan. */
|
|
44
|
+
function printGraphQL(plan) {
|
|
45
|
+
const { model } = plan;
|
|
46
|
+
const variables = {};
|
|
47
|
+
const declared = [];
|
|
48
|
+
const passed = [];
|
|
49
|
+
const argument = (name, type, value) => {
|
|
50
|
+
if (value === undefined)
|
|
51
|
+
return;
|
|
52
|
+
variables[name] = value;
|
|
53
|
+
declared.push(`$${name}: ${type}`);
|
|
54
|
+
passed.push(`${name}: $${name}`);
|
|
55
|
+
};
|
|
56
|
+
argument("id", "ID!", plan.id);
|
|
57
|
+
// REST's limit beside before is GraphQL's last beside before, the entries
|
|
58
|
+
// just before the cursor; the API refuses first there (spec 17, amendment 55).
|
|
59
|
+
// REST's before=end is last with no before, the end of the list (amendment 132).
|
|
60
|
+
const fromEnd = plan.before === plan_1.END_OF_LIST;
|
|
61
|
+
argument(plan.before === undefined ? "first" : "last", "Int", fromEnd ? plan.first ?? plan_1.LIST_DEFAULT : plan.first);
|
|
62
|
+
argument("after", "String", plan.after);
|
|
63
|
+
if (!fromEnd)
|
|
64
|
+
argument("before", "String", plan.before);
|
|
65
|
+
argument("sort", `[${model.sortType}!]`, plan.sort);
|
|
66
|
+
argument("filter", model.filterType, plan.filter);
|
|
67
|
+
const root = plan.mode === "single" ? model.singleField : model.listField;
|
|
68
|
+
const lines = [`query ${plan.operationName}${declared.length ? `(${declared.join(", ")})` : ""} {`];
|
|
69
|
+
lines.push(`${INDENT}${root}${passed.length ? `(${passed.join(", ")})` : ""} {`);
|
|
70
|
+
if (plan.mode === "single") {
|
|
71
|
+
lines.push(...printFields(plan.fields, 2));
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
lines.push(`${INDENT.repeat(2)}nodes {`);
|
|
75
|
+
lines.push(...printFields(plan.fields, 3));
|
|
76
|
+
lines.push(`${INDENT.repeat(2)}}`);
|
|
77
|
+
// A page read backward pages on back from its startCursor, as REST's page.prev.
|
|
78
|
+
const back = plan.before === undefined ? [] : [`${INDENT.repeat(3)}hasPreviousPage`, `${INDENT.repeat(3)}startCursor`];
|
|
79
|
+
lines.push(`${INDENT.repeat(2)}pageInfo {`, `${INDENT.repeat(3)}hasNextPage`, `${INDENT.repeat(3)}endCursor`, ...back, `${INDENT.repeat(2)}}`);
|
|
80
|
+
if (plan.totalCount)
|
|
81
|
+
lines.push(`${INDENT.repeat(2)}totalCount`);
|
|
82
|
+
}
|
|
83
|
+
lines.push(`${INDENT}}`, "}");
|
|
84
|
+
return { query: lines.join("\n"), variables, operationName: plan.operationName };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Build a GraphQL query from a spec, checked against the key's schema.
|
|
88
|
+
*
|
|
89
|
+
* const schema = await capa.graphqlSchema();
|
|
90
|
+
* const { query, variables } = buildGraphQLQuery(schema, { model: "articles", fields: ["title"], first: 5 });
|
|
91
|
+
* const { data } = await capa.graphql(query, variables);
|
|
92
|
+
*
|
|
93
|
+
* Throws `CapaBuildError`, with `didYouMean`, for a model, field or sort value
|
|
94
|
+
* the key's schema does not have.
|
|
95
|
+
*/
|
|
96
|
+
function buildGraphQLQuery(summary, spec) {
|
|
97
|
+
return printGraphQL((0, plan_1.planQuery)(summary, spec));
|
|
98
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* documents.ts — GraphQL documents written as literals, typed by their text.
|
|
3
|
+
*
|
|
4
|
+
* A document written in a project as a literal keeps its exact text as its
|
|
5
|
+
* type, in any of three forms:
|
|
6
|
+
*
|
|
7
|
+
* const LATEST = `#graphql
|
|
8
|
+
* query Latest($first: Int) { articles(first: $first) { nodes { title } } }
|
|
9
|
+
* `;
|
|
10
|
+
* const ONE = /* capa *\/ `query One($id: ID!) { article(id: $id) { title } }`;
|
|
11
|
+
* const VERSION = gql(`query Version { version }`);
|
|
12
|
+
*
|
|
13
|
+
* `capa-codegen --graphql` finds each one, checks it against the key's schema,
|
|
14
|
+
* and adds an entry to `CapaDocuments` keyed by that text, holding its result
|
|
15
|
+
* and variables types. `client.graphql(LATEST)` and `graphql(LATEST)` from
|
|
16
|
+
* `/nextjs` look the text up, so `data` and `variables` are typed with no cast
|
|
17
|
+
* and no import from the generated file, the way Hydrogen types
|
|
18
|
+
* `storefront.query`.
|
|
19
|
+
*
|
|
20
|
+
* `gql` used as a tag (gql`query ...`) returns a plain string: TypeScript
|
|
21
|
+
* gives a tagged template no literal type. Codegen still checks it and exports
|
|
22
|
+
* a typed `<Name>Document` for it.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* The documents written as literals in your project, by their exact text.
|
|
26
|
+
* Empty here: the module `capa-codegen --graphql` writes adds to it with
|
|
27
|
+
* `declare module "@capacms/sdk/next"`, so keep that file inside your
|
|
28
|
+
* tsconfig's `include`.
|
|
29
|
+
*/
|
|
30
|
+
export interface CapaDocuments {
|
|
31
|
+
}
|
|
32
|
+
/** The result type codegen recorded for a literal document. */
|
|
33
|
+
export type CapaDocumentResult<D> = D extends keyof CapaDocuments ? CapaDocuments[D] extends {
|
|
34
|
+
result: infer R;
|
|
35
|
+
} ? R : never : never;
|
|
36
|
+
/** The variables type codegen recorded for a literal document. */
|
|
37
|
+
export type CapaDocumentVariables<D> = D extends keyof CapaDocuments ? CapaDocuments[D] extends {
|
|
38
|
+
variables: infer V;
|
|
39
|
+
} ? V : never : never;
|
|
40
|
+
/**
|
|
41
|
+
* What a call with a `#graphql` literal that `capa-codegen --graphql` has not
|
|
42
|
+
* seen takes instead of the literal, so the call fails to compile with the
|
|
43
|
+
* fix in the message rather than going untyped.
|
|
44
|
+
*/
|
|
45
|
+
export type RunCapaCodegen = "This #graphql document is not in CapaDocuments yet: run capa-codegen --graphql (or keep capa-codegen --graphql --watch running) to type its data and variables.";
|
|
46
|
+
/**
|
|
47
|
+
* `T`, unless `D` is a document codegen recorded, or a `#graphql` literal it
|
|
48
|
+
* has not. A recorded document is typed by its record, so the untyped
|
|
49
|
+
* signature refuses it: otherwise wrong variables would compile through it. A
|
|
50
|
+
* `#graphql` literal codegen has not seen (new, or edited since the last run)
|
|
51
|
+
* is `RunCapaCodegen`, so it does not compile until codegen has read it.
|
|
52
|
+
* Text built at run time is a plain `string` and stays untyped, and so does a
|
|
53
|
+
* call that names its data type, `capa.graphql<Data>(text)`.
|
|
54
|
+
*/
|
|
55
|
+
export type NotARecordedDocument<D, T> = D extends keyof CapaDocuments ? never : string extends D ? T : D extends `${string}#graphql${string}` ? RunCapaCodegen : T;
|
|
56
|
+
/**
|
|
57
|
+
* A GraphQL document, returned as written.
|
|
58
|
+
*
|
|
59
|
+
* Called with a literal, gql(`query ...`), it returns the literal with its
|
|
60
|
+
* exact text as its type, which is what `client.graphql` looks up once
|
|
61
|
+
* `capa-codegen --graphql` has seen it. Used as a tag, gql`query ...`, it
|
|
62
|
+
* returns a plain string; import the `<Name>Document` codegen writes for its
|
|
63
|
+
* types. Values interpolated into a tag are joined in as text, and codegen
|
|
64
|
+
* skips such a document, since its text is only known at run time.
|
|
65
|
+
*/
|
|
66
|
+
export declare function gql<T extends string>(document: T): T;
|
|
67
|
+
export declare function gql(strings: TemplateStringsArray, ...values: ReadonlyArray<string | number>): string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* documents.ts — GraphQL documents written as literals, typed by their text.
|
|
4
|
+
*
|
|
5
|
+
* A document written in a project as a literal keeps its exact text as its
|
|
6
|
+
* type, in any of three forms:
|
|
7
|
+
*
|
|
8
|
+
* const LATEST = `#graphql
|
|
9
|
+
* query Latest($first: Int) { articles(first: $first) { nodes { title } } }
|
|
10
|
+
* `;
|
|
11
|
+
* const ONE = /* capa *\/ `query One($id: ID!) { article(id: $id) { title } }`;
|
|
12
|
+
* const VERSION = gql(`query Version { version }`);
|
|
13
|
+
*
|
|
14
|
+
* `capa-codegen --graphql` finds each one, checks it against the key's schema,
|
|
15
|
+
* and adds an entry to `CapaDocuments` keyed by that text, holding its result
|
|
16
|
+
* and variables types. `client.graphql(LATEST)` and `graphql(LATEST)` from
|
|
17
|
+
* `/nextjs` look the text up, so `data` and `variables` are typed with no cast
|
|
18
|
+
* and no import from the generated file, the way Hydrogen types
|
|
19
|
+
* `storefront.query`.
|
|
20
|
+
*
|
|
21
|
+
* `gql` used as a tag (gql`query ...`) returns a plain string: TypeScript
|
|
22
|
+
* gives a tagged template no literal type. Codegen still checks it and exports
|
|
23
|
+
* a typed `<Name>Document` for it.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.gql = gql;
|
|
27
|
+
function gql(document, ...values) {
|
|
28
|
+
if (typeof document === "string")
|
|
29
|
+
return document;
|
|
30
|
+
let text = document[0];
|
|
31
|
+
values.forEach((value, i) => {
|
|
32
|
+
text += String(value) + document[i + 1];
|
|
33
|
+
});
|
|
34
|
+
return text;
|
|
35
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CapaFieldNames } from "./introspection";
|
|
2
|
+
/** How long a key's renamed fields are kept before the names are read again. */
|
|
3
|
+
export declare const RENAMED_TTL_MS = 60000;
|
|
4
|
+
/** Per model namespace, its fields whose GraphQL name is not their namespace, by GraphQL name. */
|
|
5
|
+
export type RenamedFields = ReadonlyMap<string, Readonly<Record<string, string>>>;
|
|
6
|
+
/** The fields of each model that N5 renamed, from the N9 descriptions. Models with none are left out. */
|
|
7
|
+
export declare function renamedFields(names: CapaFieldNames): RenamedFields;
|
|
8
|
+
/**
|
|
9
|
+
* Run `read` in edit mode: the key's renamed fields are read beside it (or
|
|
10
|
+
* come from the cache), and its result's entries are marked once both are in.
|
|
11
|
+
*/
|
|
12
|
+
export declare function readMarked<R extends {
|
|
13
|
+
data: unknown;
|
|
14
|
+
}>(document: string, key: string, readNames: () => Promise<CapaFieldNames>, read: () => Promise<R>): Promise<R>;
|
|
15
|
+
/** Forget every key's renamed fields. For tests. */
|
|
16
|
+
export declare function __resetEditSchemaForTests(): void;
|