@capacms/sdk 1.0.0-next.4 → 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.
Files changed (68) hide show
  1. package/CHANGELOG.md +323 -0
  2. package/README.md +1043 -186
  3. package/bin/capa-codegen.js +192 -5
  4. package/bin/capa.js +208 -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 -1
  12. package/dist/graphql-codegen.d.ts +117 -0
  13. package/dist/graphql-codegen.js +705 -0
  14. package/dist/http.js +1 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +2 -1
  17. package/dist/next/attrs.d.ts +51 -12
  18. package/dist/next/attrs.js +74 -20
  19. package/dist/next/client.d.ts +111 -38
  20. package/dist/next/client.js +116 -82
  21. package/dist/next/entry-fields.d.ts +162 -0
  22. package/dist/next/entry-fields.js +2 -0
  23. package/dist/next/errors.d.ts +136 -0
  24. package/dist/next/errors.js +214 -0
  25. package/dist/next/field-names.d.ts +37 -0
  26. package/dist/next/field-names.js +145 -0
  27. package/dist/next/graphql/build.d.ts +27 -0
  28. package/dist/next/graphql/build.js +98 -0
  29. package/dist/next/graphql/documents.d.ts +67 -0
  30. package/dist/next/graphql/documents.js +35 -0
  31. package/dist/next/graphql/edit-mode.d.ts +16 -0
  32. package/dist/next/graphql/edit-mode.js +93 -0
  33. package/dist/next/graphql/filter-values.d.ts +34 -0
  34. package/dist/next/graphql/filter-values.js +96 -0
  35. package/dist/next/graphql/introspection.d.ts +89 -0
  36. package/dist/next/graphql/introspection.js +102 -0
  37. package/dist/next/graphql/plan.d.ts +115 -0
  38. package/dist/next/graphql/plan.js +531 -0
  39. package/dist/next/graphql/request.d.ts +228 -0
  40. package/dist/next/graphql/request.js +283 -0
  41. package/dist/next/graphql/rest.d.ts +66 -0
  42. package/dist/next/graphql/rest.js +502 -0
  43. package/dist/next/graphql/selection.d.ts +55 -0
  44. package/dist/next/graphql/selection.js +212 -0
  45. package/dist/next/graphql/sha256.d.ts +13 -0
  46. package/dist/next/graphql/sha256.js +86 -0
  47. package/dist/next/graphql/summary.d.ts +83 -0
  48. package/dist/next/graphql/summary.js +151 -0
  49. package/dist/next/graphql/tree-layout.d.ts +36 -0
  50. package/dist/next/graphql/tree-layout.js +20 -0
  51. package/dist/next/graphql/tree.d.ts +171 -0
  52. package/dist/next/graphql/tree.js +249 -0
  53. package/dist/next/graphql/typed.d.ts +261 -0
  54. package/dist/next/graphql/typed.js +146 -0
  55. package/dist/next/index.d.ts +28 -5
  56. package/dist/next/index.js +25 -1
  57. package/dist/next/inflate.d.ts +25 -7
  58. package/dist/next/inflate.js +46 -32
  59. package/dist/next/key-family.d.ts +34 -0
  60. package/dist/next/key-family.js +74 -0
  61. package/dist/next/select-types.d.ts +44 -8
  62. package/dist/next/system-keys.d.ts +27 -0
  63. package/dist/next/system-keys.js +42 -0
  64. package/dist/nextjs/index.d.ts +165 -12
  65. package/dist/nextjs/index.js +247 -23
  66. package/dist/nextjs/overlay.d.ts +5 -0
  67. package/dist/nextjs/overlay.js +35 -0
  68. package/package.json +35 -13
@@ -0,0 +1,228 @@
1
+ /**
2
+ * request.ts — one GraphQL request to `/api/graphql`, and the persisted-query
3
+ * dance.
4
+ *
5
+ * A request the API RAN resolves, with `errors` beside whatever `data` it
6
+ * could produce: once execution has started a root field that failed is null
7
+ * and the others still carry data (G plan D5). A request the API REFUSED as a
8
+ * whole throws `CapaError`. GraphQL over HTTP marks the refusal by its body,
9
+ * `errors` and no `data`, and sends it as a 200 on `application/json`, which
10
+ * is what this client asks for, or as a 4xx on
11
+ * `application/graphql-response+json` (decision of 2026-09-25). Both throw the
12
+ * same error, with the 400 the API gives every refusal it moves to a 200.
13
+ *
14
+ * A read goes as a GET whenever it fits the API's URL limit, so a production
15
+ * key's read is cached by the CDN and the API with no option set, and as a
16
+ * POST when it does not: a document is never refused for its length. A host
17
+ * that serves GraphQL by POST only (the admin host) answers that GET as a path
18
+ * it does not serve; a GET the caller did not ask for is then repeated as a
19
+ * POST, and that host is read by POST for a few minutes.
20
+ *
21
+ * `persisted: true` sends the document's sha256 alone, as a GET, so the
22
+ * response is cacheable and the document never travels. Variables too long
23
+ * for a GET URL go with the hash in a POST instead, which the API takes the
24
+ * same way but never caches. When the API has not seen that hash it answers
25
+ * 200 with `PersistedQueryNotFound`, and the request is repeated once as a
26
+ * POST carrying the document and the hash.
27
+ *
28
+ * That POST stores the document only on a host that stores documents (G plan
29
+ * D17) and only for a development key (spec 17, amendment 28). A production key, which
30
+ * is the key a site ships, is answered `persistedQuery.registered: false`: the
31
+ * document runs, nothing is stored, and the answer carries no error. Documents
32
+ * are registered at build time instead (`capa persist`), and then a production
33
+ * key's GET by hash hits. A hash the host declined is remembered for a few
34
+ * minutes, so the calls in between go straight to POST instead of paying a GET
35
+ * that is certain to miss; the caller sees the same result either way.
36
+ *
37
+ * The API runs 4 reads at once per project, GraphQL documents and
38
+ * `/api/entries` reads counted together, at most 3 of them for one client of
39
+ * a key, and lets 64 more per client and 256 per key wait for a slot (spec
40
+ * 17, amendments 81, 100 and 131); past that it answers 429 with
41
+ * `Retry-After`. Every request here, each leg of the dances above included,
42
+ * is repeated after that wait, plus up to 250 ms so a burst of callers does
43
+ * not return in step, up to `retries` times (3 unless set; 0 throws the first
44
+ * 429). A wait over 30 seconds is thrown at once rather than slept through,
45
+ * and the caller's `signal` ends a wait early. A 429 that is thrown carries
46
+ * `retryAfter`.
47
+ */
48
+ import { type CapaGraphQLError } from "../errors";
49
+ /** The API's GET limit on `/api/graphql` (path plus query string). */
50
+ export declare const GRAPHQL_GET_URL_LIMIT = 8192;
51
+ /** The message Apollo's persisted-query link, and this client, look for. */
52
+ export declare const PERSISTED_QUERY_NOT_FOUND = "PersistedQueryNotFound";
53
+ /**
54
+ * The status of a refused request, however it was sent. Every refusal the API
55
+ * answers 200 on `application/json` (parse, validation, variables, budget,
56
+ * planner) is a 400 in its error table and on
57
+ * `application/graphql-response+json`, so a `CapaError` reads the same either
58
+ * way. 401, 402, 403, 404, 405 and 429 keep their own statuses on both.
59
+ */
60
+ export declare const REFUSED_STATUS = 400;
61
+ /**
62
+ * A GraphQL document whose result and variables types are known, as emitted by
63
+ * `capa-codegen --graphql`. At runtime it is the document string itself; the
64
+ * two phantom properties exist only so `client.graphql(doc, vars)` can infer
65
+ * both types from it.
66
+ */
67
+ export type TypedDocument<TData = Record<string, unknown>, TVariables = Record<string, unknown>> = string & {
68
+ readonly __capaResult?: TData;
69
+ readonly __capaVariables?: TVariables;
70
+ };
71
+ /**
72
+ * What follows a document in a call: its variables, then options. The
73
+ * variables may be left out only when the document declares no required one;
74
+ * a document with `$id: ID!` needs them, as Hydrogen's `storefront.query` does.
75
+ */
76
+ export type VariablesThenOptions<TVariables, TOptions> = {} extends TVariables ? [variables?: NotInferred<TVariables>, options?: TOptions] : [variables: NotInferred<TVariables>, options?: TOptions];
77
+ /**
78
+ * `T`, but not a place TypeScript infers `T` from: the variables' type comes
79
+ * from the document alone, so `undefined` passed for required variables is
80
+ * refused instead of widening them. (TypeScript 5.4's `NoInfer`, for older
81
+ * compilers too.)
82
+ */
83
+ type NotInferred<T> = [T][T extends unknown ? 0 : never];
84
+ export interface GraphQLCallOptions {
85
+ signal?: AbortSignal;
86
+ /** Which operation to run when the document holds several. */
87
+ operationName?: string;
88
+ /**
89
+ * Unset, a read is a `GET` whenever it fits the API's 8,192-byte URL limit,
90
+ * and a `POST` when it does not. A `GET` is cached by the API and the CDN for
91
+ * a production key; a `POST` never is. `"POST"` always sends a POST. `"GET"`
92
+ * sends a GET that fits and a POST that does not, as unset does, and is not
93
+ * repeated as a POST on a host that serves GraphQL by POST only. Ignored when
94
+ * `persisted` is set, which starts with a GET whenever the hash and variables
95
+ * fit in its URL.
96
+ */
97
+ method?: "POST" | "GET";
98
+ /**
99
+ * Send the document's hash instead of the document, as a cacheable GET. On
100
+ * a miss the document follows as a POST, which is stored only for a
101
+ * development key: register documents with `capa persist` first. See "Persisted queries" in the README.
102
+ */
103
+ persisted?: boolean;
104
+ /**
105
+ * How many times a 429 (more reads at once than the API runs and queues:
106
+ * 4 running per project, 3 of them per client, and 64 waiting per client or
107
+ * 256 per key, REST reads included) is repeated after its `Retry-After`,
108
+ * plus up to 250 ms. 3 unless set; 0 throws the first 429, with
109
+ * `retryAfter` set. A `Retry-After` over 30 seconds is always thrown.
110
+ */
111
+ retries?: number;
112
+ }
113
+ /** How many times a 429 is repeated unless `retries` says otherwise. */
114
+ export declare const DEFAULT_RETRIES = 3;
115
+ /** The longest `Retry-After` waited out; a longer one is thrown to the caller. */
116
+ export declare const MAX_RETRY_WAIT_MS = 30000;
117
+ /** What the API measured for this request (G plan 1.5). */
118
+ export interface CapaGraphQLCost {
119
+ depth: number;
120
+ rootFields: number;
121
+ connections: number;
122
+ nodesBound: number;
123
+ /**
124
+ * Each `totalCount` and each filter or sort through a relation: reads of
125
+ * entries the answer does not hold, 500 entries each against the budget
126
+ * (spec 17, amendment 43).
127
+ */
128
+ scans: number;
129
+ nodes: number;
130
+ fields: number;
131
+ }
132
+ /**
133
+ * `extensions.capa`, which `/api/graphql` sends to a development key and to a
134
+ * request that asks for it (spec 17, amendment 119).
135
+ */
136
+ export interface CapaGraphQLExtensions {
137
+ requestId: string;
138
+ version: string;
139
+ contract: number;
140
+ environment?: string;
141
+ cost?: CapaGraphQLCost;
142
+ /** The REST request each executed root field is equal to. */
143
+ rest?: Array<{
144
+ field: string;
145
+ url: string;
146
+ }>;
147
+ [key: string]: unknown;
148
+ }
149
+ /**
150
+ * `extensions.cost`, the query's cost as Shopify's APIs report it, in entries.
151
+ * There is no per-key budget, so there is no `throttleStatus`.
152
+ */
153
+ export interface GraphQLQueryCost {
154
+ /**
155
+ * The most the query could cost: the entries it could return, each nested
156
+ * list at its `first` (100 without one), capped at 5,000, plus 500 for each
157
+ * of its `scans`. Not what the limit checks: read `budget.counted` for that.
158
+ */
159
+ requestedQueryCost: number;
160
+ /** What it did read, plus 500 for each scan that ran; never above `requestedQueryCost`. */
161
+ actualQueryCost: number;
162
+ /**
163
+ * The figure the 5,000-entry limit is checked against before any SQL, the
164
+ * one to watch: a nested list with no `first` counts 10 for each entry above
165
+ * it. A document is refused `query_too_complex` exactly when `counted`
166
+ * passes `limit`.
167
+ */
168
+ budget: {
169
+ counted: number;
170
+ limit: number;
171
+ };
172
+ }
173
+ /** One deprecated member a document used, in `extensions.deprecations` and the `Capa-Deprecated-Reason` header. */
174
+ export interface GraphQLDeprecation {
175
+ /** The schema coordinate, such as `Articles._folder`. */
176
+ coordinate: string;
177
+ /** What to use instead. */
178
+ reason: string;
179
+ }
180
+ export interface GraphQLResult<TData> {
181
+ /** Null when no root field could be answered. */
182
+ data: TData | null;
183
+ /** Empty when the request succeeded completely. */
184
+ errors: CapaGraphQLError[];
185
+ extensions: {
186
+ capa?: CapaGraphQLExtensions;
187
+ cost?: GraphQLQueryCost;
188
+ /** Each deprecated member the document used, when it validated and used one. */
189
+ deprecations?: GraphQLDeprecation[];
190
+ persistedQuery?: {
191
+ registered: boolean;
192
+ };
193
+ [key: string]: unknown;
194
+ };
195
+ /** Parsed from `Surrogate-Key`: the tags a GET response can be purged by. */
196
+ cacheTags: string[];
197
+ }
198
+ /** What a request needs from the client's config, and nothing more. */
199
+ export interface GraphQLTransport {
200
+ baseUrl: string;
201
+ apiKey: string;
202
+ version: string;
203
+ contract?: number;
204
+ schemaChecksum?: string;
205
+ fetchImpl: typeof fetch;
206
+ }
207
+ interface Payload {
208
+ query?: string;
209
+ variables?: Record<string, unknown>;
210
+ operationName?: string;
211
+ extensions?: {
212
+ persistedQuery: {
213
+ version: 1;
214
+ sha256Hash: string;
215
+ };
216
+ };
217
+ }
218
+ /** The GET form: every payload key as a query parameter, objects as JSON. */
219
+ export declare function graphqlGetUrl(baseUrl: string, payload: Payload): string;
220
+ /** The bytes the API counts against its GET limit (`GRAPHQL_GET_URL_LIMIT`): the path and query string. */
221
+ export declare function graphqlGetBytes(baseUrl: string, payload: Payload): number;
222
+ /** How long a host's `registered: false`, or its refusal of a GET, is trusted before the GET is tried again. */
223
+ export declare const UNSTORED_TTL_MS: number;
224
+ /** Forget every declined hash and every POST-only host. For tests. */
225
+ export declare function __resetPersistedMemoForTests(): void;
226
+ /** Run one document. See the file comment for what resolves and what throws. */
227
+ export declare function runGraphQL<TData>(transport: GraphQLTransport, document: string, variables: Record<string, unknown> | undefined, options?: GraphQLCallOptions): Promise<GraphQLResult<TData>>;
228
+ export {};
@@ -0,0 +1,283 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UNSTORED_TTL_MS = exports.MAX_RETRY_WAIT_MS = exports.DEFAULT_RETRIES = exports.REFUSED_STATUS = exports.PERSISTED_QUERY_NOT_FOUND = exports.GRAPHQL_GET_URL_LIMIT = void 0;
4
+ exports.graphqlGetUrl = graphqlGetUrl;
5
+ exports.graphqlGetBytes = graphqlGetBytes;
6
+ exports.__resetPersistedMemoForTests = __resetPersistedMemoForTests;
7
+ exports.runGraphQL = runGraphQL;
8
+ /**
9
+ * request.ts — one GraphQL request to `/api/graphql`, and the persisted-query
10
+ * dance.
11
+ *
12
+ * A request the API RAN resolves, with `errors` beside whatever `data` it
13
+ * could produce: once execution has started a root field that failed is null
14
+ * and the others still carry data (G plan D5). A request the API REFUSED as a
15
+ * whole throws `CapaError`. GraphQL over HTTP marks the refusal by its body,
16
+ * `errors` and no `data`, and sends it as a 200 on `application/json`, which
17
+ * is what this client asks for, or as a 4xx on
18
+ * `application/graphql-response+json` (decision of 2026-09-25). Both throw the
19
+ * same error, with the 400 the API gives every refusal it moves to a 200.
20
+ *
21
+ * A read goes as a GET whenever it fits the API's URL limit, so a production
22
+ * key's read is cached by the CDN and the API with no option set, and as a
23
+ * POST when it does not: a document is never refused for its length. A host
24
+ * that serves GraphQL by POST only (the admin host) answers that GET as a path
25
+ * it does not serve; a GET the caller did not ask for is then repeated as a
26
+ * POST, and that host is read by POST for a few minutes.
27
+ *
28
+ * `persisted: true` sends the document's sha256 alone, as a GET, so the
29
+ * response is cacheable and the document never travels. Variables too long
30
+ * for a GET URL go with the hash in a POST instead, which the API takes the
31
+ * same way but never caches. When the API has not seen that hash it answers
32
+ * 200 with `PersistedQueryNotFound`, and the request is repeated once as a
33
+ * POST carrying the document and the hash.
34
+ *
35
+ * That POST stores the document only on a host that stores documents (G plan
36
+ * D17) and only for a development key (spec 17, amendment 28). A production key, which
37
+ * is the key a site ships, is answered `persistedQuery.registered: false`: the
38
+ * document runs, nothing is stored, and the answer carries no error. Documents
39
+ * are registered at build time instead (`capa persist`), and then a production
40
+ * key's GET by hash hits. A hash the host declined is remembered for a few
41
+ * minutes, so the calls in between go straight to POST instead of paying a GET
42
+ * that is certain to miss; the caller sees the same result either way.
43
+ *
44
+ * The API runs 4 reads at once per project, GraphQL documents and
45
+ * `/api/entries` reads counted together, at most 3 of them for one client of
46
+ * a key, and lets 64 more per client and 256 per key wait for a slot (spec
47
+ * 17, amendments 81, 100 and 131); past that it answers 429 with
48
+ * `Retry-After`. Every request here, each leg of the dances above included,
49
+ * is repeated after that wait, plus up to 250 ms so a burst of callers does
50
+ * not return in step, up to `retries` times (3 unless set; 0 throws the first
51
+ * 429). A wait over 30 seconds is thrown at once rather than slept through,
52
+ * and the caller's `signal` ends a wait early. A 429 that is thrown carries
53
+ * `retryAfter`.
54
+ */
55
+ const errors_1 = require("../errors");
56
+ const sha256_1 = require("./sha256");
57
+ /** The API's GET limit on `/api/graphql` (path plus query string). */
58
+ exports.GRAPHQL_GET_URL_LIMIT = 8192;
59
+ /** The message Apollo's persisted-query link, and this client, look for. */
60
+ exports.PERSISTED_QUERY_NOT_FOUND = "PersistedQueryNotFound";
61
+ /**
62
+ * The status of a refused request, however it was sent. Every refusal the API
63
+ * answers 200 on `application/json` (parse, validation, variables, budget,
64
+ * planner) is a 400 in its error table and on
65
+ * `application/graphql-response+json`, so a `CapaError` reads the same either
66
+ * way. 401, 402, 403, 404, 405 and 429 keep their own statuses on both.
67
+ */
68
+ exports.REFUSED_STATUS = 400;
69
+ /** How many times a 429 is repeated unless `retries` says otherwise. */
70
+ exports.DEFAULT_RETRIES = 3;
71
+ /** The longest `Retry-After` waited out; a longer one is thrown to the caller. */
72
+ exports.MAX_RETRY_WAIT_MS = 30_000;
73
+ /** Added to each wait, at random, so callers throttled together do not return together. */
74
+ const RETRY_JITTER_MS = 250;
75
+ function headersFor(transport, withBody) {
76
+ const headers = {
77
+ Accept: "application/json",
78
+ "x-api-key": transport.apiKey,
79
+ "Capa-Version": transport.version,
80
+ };
81
+ if (withBody)
82
+ headers["Content-Type"] = "application/json";
83
+ if (transport.contract !== undefined)
84
+ headers["Capa-Contract"] = String(transport.contract);
85
+ if (transport.schemaChecksum !== undefined)
86
+ headers["Capa-Schema"] = transport.schemaChecksum;
87
+ return headers;
88
+ }
89
+ /** The GET form: every payload key as a query parameter, objects as JSON. */
90
+ function graphqlGetUrl(baseUrl, payload) {
91
+ const url = new URL(`${baseUrl}/api/graphql`);
92
+ if (payload.query !== undefined)
93
+ url.searchParams.set("query", payload.query);
94
+ if (payload.variables !== undefined)
95
+ url.searchParams.set("variables", JSON.stringify(payload.variables));
96
+ if (payload.operationName !== undefined)
97
+ url.searchParams.set("operationName", payload.operationName);
98
+ if (payload.extensions !== undefined)
99
+ url.searchParams.set("extensions", JSON.stringify(payload.extensions));
100
+ return url.toString();
101
+ }
102
+ /** The bytes the API counts against its GET limit (`GRAPHQL_GET_URL_LIMIT`): the path and query string. */
103
+ function graphqlGetBytes(baseUrl, payload) {
104
+ return graphqlGetUrl(baseUrl, payload).length - baseUrl.length;
105
+ }
106
+ /** GraphQL over HTTP: a refused request has `errors` and no `data` entry; a body with `data`, null included, ran. */
107
+ function refusedWhole(body, errors) {
108
+ return errors.length > 0 && !Object.prototype.hasOwnProperty.call(body, "data");
109
+ }
110
+ /** Resolves after `ms`, or rejects with the signal's reason once it aborts. */
111
+ function pause(ms, signal) {
112
+ if (signal?.aborted)
113
+ return Promise.reject(signal.reason);
114
+ return new Promise((resolve, reject) => {
115
+ const onAbort = () => {
116
+ clearTimeout(timer);
117
+ reject(signal?.reason);
118
+ };
119
+ const timer = setTimeout(() => {
120
+ signal?.removeEventListener("abort", onAbort);
121
+ resolve();
122
+ }, ms);
123
+ signal?.addEventListener("abort", onAbort, { once: true });
124
+ });
125
+ }
126
+ /**
127
+ * `sendOnce`, repeated after each 429's `Retry-After` (1 second when it sent
128
+ * none) up to `call.retries` times. See the file comment.
129
+ */
130
+ async function send(transport, wanted, payload, call) {
131
+ for (let attempt = 0;; attempt++) {
132
+ try {
133
+ return await sendOnce(transport, wanted, payload, call.signal);
134
+ }
135
+ catch (error) {
136
+ if (!(error instanceof errors_1.CapaError) || error.status !== 429 || attempt >= call.retries)
137
+ throw error;
138
+ const wait = (error.retryAfter ?? 1) * 1000;
139
+ if (wait > exports.MAX_RETRY_WAIT_MS)
140
+ throw error;
141
+ await pause(wait + Math.random() * RETRY_JITTER_MS, call.signal);
142
+ }
143
+ }
144
+ }
145
+ /**
146
+ * One request by `wanted`, except that a GET too long for the API's URL limit
147
+ * goes as a POST. See the file comment for what resolves and what throws.
148
+ */
149
+ async function sendOnce(transport, wanted, payload, signal) {
150
+ const method = wanted === "GET" && graphqlGetBytes(transport.baseUrl, payload) > exports.GRAPHQL_GET_URL_LIMIT ? "POST" : wanted;
151
+ const url = method === "GET" ? graphqlGetUrl(transport.baseUrl, payload) : `${transport.baseUrl}/api/graphql`;
152
+ const response = await transport.fetchImpl(url, {
153
+ method,
154
+ headers: headersFor(transport, method === "POST"),
155
+ body: method === "POST" ? JSON.stringify(payload) : undefined,
156
+ signal,
157
+ });
158
+ const requestId = response.headers?.get?.("X-Request-Id") ?? "";
159
+ const text = await response.text();
160
+ let body;
161
+ try {
162
+ body = JSON.parse(text);
163
+ }
164
+ catch {
165
+ throw (0, errors_1.unparseable)(response.status, requestId);
166
+ }
167
+ if (response.status !== 200) {
168
+ throw ((0, errors_1.graphqlNotServed)(response.status, body, method, requestId) ??
169
+ (0, errors_1.errorFromGraphQLBody)(response.status, body, requestId, (0, errors_1.retryAfterOf)(response.headers?.get?.("Retry-After"))));
170
+ }
171
+ if (!body || typeof body !== "object")
172
+ throw (0, errors_1.unparseable)(response.status, requestId);
173
+ const object = body;
174
+ const errors = (0, errors_1.graphqlErrorsOf)(object.errors);
175
+ // PersistedQueryNotFound is a refusal too, and the one that asks for the
176
+ // document rather than failing: `runGraphQL` answers it with a POST.
177
+ if (refusedWhole(object, errors) && !persistedQueryMissing(errors)) {
178
+ throw (0, errors_1.errorFromGraphQLBody)(exports.REFUSED_STATUS, body, requestId);
179
+ }
180
+ const tags = response.headers?.get?.("Surrogate-Key") ?? "";
181
+ return {
182
+ data: (object.data ?? null),
183
+ errors,
184
+ extensions: object.extensions && typeof object.extensions === "object"
185
+ ? object.extensions
186
+ : {},
187
+ cacheTags: tags.split(/\s+/).filter(Boolean),
188
+ };
189
+ }
190
+ /** How long a host's `registered: false`, or its refusal of a GET, is trusted before the GET is tried again. */
191
+ exports.UNSTORED_TTL_MS = 5 * 60_000;
192
+ /** Keys remembered for `UNSTORED_TTL_MS`, at most a thousand, so a long-lived process cannot grow them without end. */
193
+ class Remembered {
194
+ until = new Map();
195
+ has(key, now) {
196
+ const until = this.until.get(key);
197
+ if (until === undefined)
198
+ return false;
199
+ if (until > now)
200
+ return true;
201
+ this.until.delete(key);
202
+ return false;
203
+ }
204
+ add(key, now) {
205
+ if (this.until.size >= 1000)
206
+ this.until.clear();
207
+ this.until.set(key, now + exports.UNSTORED_TTL_MS);
208
+ }
209
+ clear() {
210
+ this.until.clear();
211
+ }
212
+ }
213
+ /** Hashes a host declined to store, per host and key. */
214
+ const unstored = new Remembered();
215
+ /** Hosts that answered a GET as a path they do not serve. */
216
+ const postOnly = new Remembered();
217
+ function unstoredKey(transport, hash) {
218
+ return `${transport.baseUrl}\n${transport.apiKey}\n${hash}`;
219
+ }
220
+ /** Forget every declined hash and every POST-only host. For tests. */
221
+ function __resetPersistedMemoForTests() {
222
+ unstored.clear();
223
+ postOnly.clear();
224
+ }
225
+ function persistedQueryMissing(errors) {
226
+ return errors.some((error) => error.code === "persisted_query_not_found" || error.message === exports.PERSISTED_QUERY_NOT_FOUND);
227
+ }
228
+ /** `graphqlNotServed`'s answer to a GET: this host serves no GraphQL by GET. */
229
+ function getNotServed(error) {
230
+ return error instanceof errors_1.CapaError && error.status === 404 && error.code === "route_not_found";
231
+ }
232
+ /**
233
+ * A document with its text: by the method asked for, else by GET, repeated
234
+ * once as a POST where the host serves GraphQL by POST only.
235
+ */
236
+ async function read(transport, payload, method, call) {
237
+ if (method !== undefined)
238
+ return send(transport, method, payload, call);
239
+ if (postOnly.has(transport.baseUrl, Date.now()))
240
+ return send(transport, "POST", payload, call);
241
+ try {
242
+ return await send(transport, "GET", payload, call);
243
+ }
244
+ catch (error) {
245
+ if (!getNotServed(error))
246
+ throw error;
247
+ postOnly.add(transport.baseUrl, Date.now());
248
+ return send(transport, "POST", payload, call);
249
+ }
250
+ }
251
+ function checkInput(document, variables) {
252
+ if (typeof document !== "string" || document.trim() === "") {
253
+ throw new TypeError("@capacms/sdk/next: graphql needs a document: a non-empty GraphQL string.");
254
+ }
255
+ if (variables !== undefined && (variables === null || typeof variables !== "object" || Array.isArray(variables))) {
256
+ throw new TypeError("@capacms/sdk/next: graphql variables must be an object keyed by variable name.");
257
+ }
258
+ }
259
+ /** Run one document. See the file comment for what resolves and what throws. */
260
+ async function runGraphQL(transport, document, variables, options = {}) {
261
+ checkInput(document, variables);
262
+ const retries = options.retries ?? exports.DEFAULT_RETRIES;
263
+ if (!Number.isInteger(retries) || retries < 0) {
264
+ throw new TypeError("@capacms/sdk/next: graphql retries must be a whole number, 0 or more.");
265
+ }
266
+ const call = { signal: options.signal, retries };
267
+ const base = { variables, operationName: options.operationName };
268
+ if (!options.persisted)
269
+ return read(transport, { query: document, ...base }, options.method, call);
270
+ const sha256Hash = await (0, sha256_1.sha256Hex)(document);
271
+ const extensions = { persistedQuery: { version: 1, sha256Hash } };
272
+ const post = { query: document, ...base, extensions };
273
+ const key = unstoredKey(transport, sha256Hash);
274
+ if (unstored.has(key, Date.now()))
275
+ return send(transport, "POST", post, call);
276
+ const first = await send(transport, "GET", { ...base, extensions }, call);
277
+ if (!persistedQueryMissing(first.errors))
278
+ return first;
279
+ const second = await send(transport, "POST", post, call);
280
+ if (second.extensions.persistedQuery?.registered === false)
281
+ unstored.add(key, Date.now());
282
+ return second;
283
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * rest.ts — the same read as a REST request, and back.
3
+ *
4
+ * `/api/graphql` runs every root field as the equivalent `/api/entries`
5
+ * request (G plan D2), so a query spec has exactly one REST twin. These two
6
+ * helpers write it down in each direction, so `select` and GraphQL stay
7
+ * interchangeable in code: `specToSelect` for "show me the REST URL of this
8
+ * query" (public as `graphqlToSelect`, which also takes a builder selection),
9
+ * `selectToGraphQL` for "port this REST read to GraphQL".
10
+ *
11
+ * Names cross the boundary through the schema summary, because GraphQL and
12
+ * REST name a few things differently: a field renamed by N5 (`tags_field`,
13
+ * `price_usd`), a relation hop (`author__name_ASC` against `author.name`), the
14
+ * system fields the schema prefixes with `_`, a system key a field of the
15
+ * model shadows, which REST writes with `$` (`$tags`, `$createdAt`,
16
+ * `author.$id`), and a namespace holding a character REST's grammars use,
17
+ * which REST writes quoted (`"price.usd"`, field-names.ts).
18
+ */
19
+ import { type GraphQLQuerySpec } from "./plan";
20
+ import type { GraphQLModelSummary, GraphQLSchemaSummary } from "./summary";
21
+ export interface RestRequest {
22
+ /** `/api/entries/articles` or `/api/entries/articles/{id}`. */
23
+ path: string;
24
+ /** The `select` grammar string. */
25
+ select: string;
26
+ /** Query parameters in the order the API prints them. */
27
+ params: Array<[string, string]>;
28
+ /** `path` plus the query string, readable: commas and colons left bare. */
29
+ url: string;
30
+ }
31
+ /**
32
+ * A GraphQL filter input as the REST `where` object, key for key what the API
33
+ * prints in `extensions.capa.rest`: namespaces for field names, written as
34
+ * REST writes them, a hop as a dotted path after the relation's own
35
+ * operators, keys in the order the
36
+ * filter input type declares them, and null members dropped (GraphQL reads a
37
+ * null as "no condition").
38
+ */
39
+ export declare function restWhere(summary: GraphQLSchemaSummary, model: GraphQLModelSummary, filter: Record<string, unknown>): Record<string, unknown>;
40
+ /**
41
+ * The REST request a query spec is equal to, written the way the API writes
42
+ * it in `extensions.capa.rest`: the filter as `where` JSON, the root's default
43
+ * page size left out, a relation list's `first` written as `limit:` whenever
44
+ * the spec gives it, parameters in the API's order. Public as
45
+ * `graphqlToSelect`, in selection.ts.
46
+ */
47
+ export declare function specToSelect(summary: GraphQLSchemaSummary, spec: GraphQLQuerySpec): RestRequest;
48
+ /** The REST read options `selectToGraphQL` understands, as `entries.list` takes them. */
49
+ export interface RestReadOptions {
50
+ /** A single-entry read. */
51
+ id?: string;
52
+ filter?: Record<string, Record<string, unknown>>;
53
+ where?: Record<string, unknown>;
54
+ sort?: readonly string[];
55
+ limit?: number;
56
+ count?: boolean;
57
+ after?: string;
58
+ before?: string;
59
+ }
60
+ /**
61
+ * The query spec a REST read is equal to. Pass it to `buildGraphQLQuery`.
62
+ *
63
+ * selectToGraphQL(schema, "articles", "title,author(name)", { sort: ["-views"], limit: 5 })
64
+ * // { model: "articles", fields: ["title", { field: "author", fields: ["name"] }], sort: ["views_DESC"], first: 5 }
65
+ */
66
+ export declare function selectToGraphQL(summary: GraphQLSchemaSummary, namespace: string, select?: string, options?: RestReadOptions): GraphQLQuerySpec;