@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.
Files changed (92) hide show
  1. package/CHANGELOG.md +450 -0
  2. package/README.md +1698 -193
  3. package/bin/capa-codegen.js +192 -5
  4. package/bin/capa.js +235 -0
  5. package/bin/graphql-project.js +142 -0
  6. package/bin/project-env.js +58 -0
  7. package/dist/client.d.ts +5 -0
  8. package/dist/client.js +17 -0
  9. package/dist/codegen.d.ts +55 -0
  10. package/dist/codegen.js +320 -39
  11. package/dist/config.d.ts +5 -36
  12. package/dist/config.js +47 -1
  13. package/dist/esm/image/index.d.ts +120 -0
  14. package/dist/esm/image/index.js +250 -0
  15. package/dist/esm/image/shared-params.generated.d.ts +190 -0
  16. package/dist/esm/image/shared-params.generated.js +461 -0
  17. package/dist/esm/nextjs/image-loader.d.ts +60 -0
  18. package/dist/esm/nextjs/image-loader.js +67 -0
  19. package/dist/esm/nextjs/overlay.d.ts +30 -0
  20. package/dist/esm/nextjs/overlay.js +75 -0
  21. package/dist/esm/overlay/index.d.ts +32 -0
  22. package/dist/esm/overlay/index.js +576 -0
  23. package/dist/esm/overlay/protocol.d.ts +187 -0
  24. package/dist/esm/overlay/protocol.js +240 -0
  25. package/dist/esm/package.json +4 -0
  26. package/dist/graphql-codegen.d.ts +117 -0
  27. package/dist/graphql-codegen.js +705 -0
  28. package/dist/http.js +1 -1
  29. package/dist/image/index.d.ts +120 -0
  30. package/dist/image/index.js +257 -0
  31. package/dist/image/shared-params.generated.d.ts +190 -0
  32. package/dist/image/shared-params.generated.js +471 -0
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.js +2 -1
  35. package/dist/next/attrs.d.ts +84 -10
  36. package/dist/next/attrs.js +119 -2
  37. package/dist/next/client.d.ts +176 -32
  38. package/dist/next/client.js +212 -90
  39. package/dist/next/entry-fields.d.ts +162 -0
  40. package/dist/next/entry-fields.js +2 -0
  41. package/dist/next/errors.d.ts +136 -0
  42. package/dist/next/errors.js +214 -0
  43. package/dist/next/field-names.d.ts +37 -0
  44. package/dist/next/field-names.js +145 -0
  45. package/dist/next/graphql/build.d.ts +27 -0
  46. package/dist/next/graphql/build.js +98 -0
  47. package/dist/next/graphql/documents.d.ts +67 -0
  48. package/dist/next/graphql/documents.js +35 -0
  49. package/dist/next/graphql/edit-mode.d.ts +16 -0
  50. package/dist/next/graphql/edit-mode.js +93 -0
  51. package/dist/next/graphql/filter-values.d.ts +34 -0
  52. package/dist/next/graphql/filter-values.js +96 -0
  53. package/dist/next/graphql/introspection.d.ts +89 -0
  54. package/dist/next/graphql/introspection.js +102 -0
  55. package/dist/next/graphql/plan.d.ts +115 -0
  56. package/dist/next/graphql/plan.js +531 -0
  57. package/dist/next/graphql/request.d.ts +228 -0
  58. package/dist/next/graphql/request.js +283 -0
  59. package/dist/next/graphql/rest.d.ts +66 -0
  60. package/dist/next/graphql/rest.js +502 -0
  61. package/dist/next/graphql/selection.d.ts +55 -0
  62. package/dist/next/graphql/selection.js +212 -0
  63. package/dist/next/graphql/sha256.d.ts +13 -0
  64. package/dist/next/graphql/sha256.js +86 -0
  65. package/dist/next/graphql/summary.d.ts +83 -0
  66. package/dist/next/graphql/summary.js +151 -0
  67. package/dist/next/graphql/tree-layout.d.ts +36 -0
  68. package/dist/next/graphql/tree-layout.js +20 -0
  69. package/dist/next/graphql/tree.d.ts +171 -0
  70. package/dist/next/graphql/tree.js +249 -0
  71. package/dist/next/graphql/typed.d.ts +261 -0
  72. package/dist/next/graphql/typed.js +146 -0
  73. package/dist/next/index.d.ts +30 -5
  74. package/dist/next/index.js +32 -1
  75. package/dist/next/inflate.d.ts +51 -0
  76. package/dist/next/inflate.js +243 -0
  77. package/dist/next/key-family.d.ts +31 -0
  78. package/dist/next/key-family.js +66 -0
  79. package/dist/next/select-types.d.ts +58 -5
  80. package/dist/next/system-keys.d.ts +27 -0
  81. package/dist/next/system-keys.js +42 -0
  82. package/dist/nextjs/image-loader.d.ts +60 -0
  83. package/dist/nextjs/image-loader.js +71 -0
  84. package/dist/nextjs/index.d.ts +484 -5
  85. package/dist/nextjs/index.js +688 -6
  86. package/dist/nextjs/overlay.d.ts +30 -0
  87. package/dist/nextjs/overlay.js +78 -0
  88. package/dist/overlay/index.d.ts +14 -2
  89. package/dist/overlay/index.js +282 -43
  90. package/dist/overlay/protocol.d.ts +98 -2
  91. package/dist/overlay/protocol.js +151 -4
  92. package/package.json +63 -15
@@ -0,0 +1,136 @@
1
+ /**
2
+ * errors.ts — what a failed `/api/` call throws.
3
+ *
4
+ * `CapaError` is one refusal of the whole request: the REST envelope's
5
+ * `{ error }`, or a GraphQL response with `errors` and no `data`, whatever
6
+ * its status. `CapaGraphQLError` is one entry of a GraphQL `errors` array. A
7
+ * GraphQL request the API ran resolves with those in `errors` rather than
8
+ * throwing, because its `data` carries the root fields that did succeed. They
9
+ * are returned, not thrown, so they are plain objects: a result goes through
10
+ * `Response.json` or into a client component's props with every field kept.
11
+ */
12
+ export interface ApiErrorEnvelope {
13
+ error?: {
14
+ type?: unknown;
15
+ code?: unknown;
16
+ message?: unknown;
17
+ param?: unknown;
18
+ hint?: unknown;
19
+ docs?: unknown;
20
+ };
21
+ meta?: {
22
+ requestId?: unknown;
23
+ };
24
+ }
25
+ export declare class CapaError extends Error {
26
+ readonly status: number;
27
+ readonly type: string;
28
+ readonly code: string;
29
+ readonly param?: string;
30
+ readonly hint?: string;
31
+ readonly requestId: string;
32
+ readonly docs: string;
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
+ readonly graphqlErrors: readonly CapaGraphQLError[];
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
+ readonly retryAfter?: number;
43
+ constructor(input: {
44
+ status: number;
45
+ type: string;
46
+ code: string;
47
+ message: string;
48
+ param?: string;
49
+ hint?: string;
50
+ requestId: string;
51
+ docs: string;
52
+ graphqlErrors?: readonly CapaGraphQLError[];
53
+ retryAfter?: number;
54
+ });
55
+ }
56
+ /**
57
+ * A `Retry-After` header in seconds: delta-seconds as sent, an HTTP date as
58
+ * the whole seconds from `now` until it (0 once it has passed). Undefined when
59
+ * the header is absent or neither.
60
+ */
61
+ export declare function retryAfterOf(value: string | null | undefined, now?: number): number | undefined;
62
+ export declare function isCapaError(error: unknown): error is CapaError;
63
+ export declare function optionalString(value: unknown): string | undefined;
64
+ export declare function unparseable(status: number, requestId: string): CapaError;
65
+ export declare function errorFromEnvelope(status: number, body: ApiErrorEnvelope, fallbackRequestId: string, retryAfter?: number): CapaError;
66
+ /** One `errors[]` item as the API sends it. */
67
+ export interface GraphQLErrorItem {
68
+ message: string;
69
+ locations?: Array<{
70
+ line: number;
71
+ column: number;
72
+ }>;
73
+ path?: Array<string | number>;
74
+ extensions?: {
75
+ code?: string;
76
+ type?: string;
77
+ param?: string;
78
+ hint?: string;
79
+ docs?: string;
80
+ requestId?: string;
81
+ [key: string]: unknown;
82
+ };
83
+ }
84
+ /**
85
+ * One GraphQL error: the spec's `{ message, locations, path, extensions }`,
86
+ * with the fields a caller acts on lifted out of `extensions`: `code` to
87
+ * branch on, `hint` for the fix, `docs` for the page that explains it,
88
+ * `param` for the argument at fault. `path` names the root field it belongs
89
+ * to.
90
+ *
91
+ * A plain object, not an `Error`: an `Error`'s `message` is not enumerable,
92
+ * so `JSON.stringify` drops it, and React sends an `Error` to a client
93
+ * component as a generic message in production. Only what the API sent is
94
+ * set, so a JSON round trip gives back an equal object. Check one with
95
+ * `isCapaGraphQLError`.
96
+ */
97
+ export interface CapaGraphQLError {
98
+ readonly message: string;
99
+ readonly locations?: ReadonlyArray<{
100
+ line: number;
101
+ column: number;
102
+ }>;
103
+ readonly path?: ReadonlyArray<string | number>;
104
+ readonly extensions: Readonly<Record<string, unknown>>;
105
+ /** `extensions.code`, or `unknown` when the API sent none. */
106
+ readonly code: string;
107
+ readonly type?: string;
108
+ readonly param?: string;
109
+ readonly hint?: string;
110
+ readonly docs?: string;
111
+ }
112
+ /** Whether `value` is a `CapaGraphQLError`: by its shape, so one that went through JSON is one too. */
113
+ export declare function isCapaGraphQLError(value: unknown): value is CapaGraphQLError;
114
+ /** Turn a parsed `errors` value into typed errors, skipping anything that is not one. */
115
+ export declare function graphqlErrorsOf(value: unknown): CapaGraphQLError[];
116
+ /**
117
+ * A GraphQL request the API refused as a whole: a status other than 200, or
118
+ * a 200 with `errors` and no `data`, passed in as the refusal's `status`. The
119
+ * `CapaError` fields come from the first error, and `graphqlErrors` holds all
120
+ * of them. A body in the REST envelope shape, which a proxy or an older API
121
+ * can send, is read as one.
122
+ */
123
+ export declare function errorFromGraphQLBody(status: number, body: unknown, fallbackRequestId: string, retryAfter?: number): CapaError;
124
+ /** A `CapaError` for errors already read: the fields of the first, and all of them in `graphqlErrors`. */
125
+ export declare function errorFromGraphQLErrors(status: number, errors: readonly CapaGraphQLError[], fallbackRequestId: string, retryAfter?: number): CapaError;
126
+ /**
127
+ * `/api/graphql` answered as a path the API does not serve: a POST
128
+ * `405 mutations_not_enabled`, a GET `404 route_not_found`, in the REST
129
+ * envelope. `CAPA_API_GRAPHQL=off` does that, and so does the admin host for a
130
+ * GET, since it serves GraphQL by POST only. The GraphQL handler never answers
131
+ * in that envelope (a real mutation's refusal is `{ errors }`), so the
132
+ * envelope is what tells "GraphQL is not here" from "this document was
133
+ * refused", and a query is never told it tried to write. Null for any other
134
+ * answer.
135
+ */
136
+ export declare function graphqlNotServed(status: number, body: unknown, method: "GET" | "POST", fallbackRequestId: string): CapaError | null;
@@ -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
+ * `@capacms/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
+ }