@capacms/sdk 1.0.0-next.0 → 1.0.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +450 -0
- package/README.md +1754 -156
- package/bin/capa-codegen.js +192 -5
- package/bin/capa.js +235 -0
- package/bin/graphql-project.js +142 -0
- package/bin/project-env.js +58 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +17 -0
- package/dist/codegen.d.ts +55 -0
- package/dist/codegen.js +320 -39
- package/dist/config.d.ts +5 -36
- package/dist/config.js +47 -1
- package/dist/esm/image/index.d.ts +120 -0
- package/dist/esm/image/index.js +250 -0
- package/dist/esm/image/shared-params.generated.d.ts +190 -0
- package/dist/esm/image/shared-params.generated.js +461 -0
- package/dist/esm/nextjs/image-loader.d.ts +60 -0
- package/dist/esm/nextjs/image-loader.js +67 -0
- package/dist/esm/nextjs/overlay.d.ts +30 -0
- package/dist/esm/nextjs/overlay.js +75 -0
- package/dist/esm/overlay/index.d.ts +32 -0
- package/dist/esm/overlay/index.js +576 -0
- package/dist/esm/overlay/protocol.d.ts +187 -0
- package/dist/esm/overlay/protocol.js +240 -0
- package/dist/esm/package.json +4 -0
- package/dist/graphql-codegen.d.ts +117 -0
- package/dist/graphql-codegen.js +705 -0
- package/dist/http.js +1 -1
- package/dist/image/index.d.ts +120 -0
- package/dist/image/index.js +257 -0
- package/dist/image/shared-params.generated.d.ts +190 -0
- package/dist/image/shared-params.generated.js +471 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/next/attrs.d.ts +98 -0
- package/dist/next/attrs.js +125 -0
- package/dist/next/client.d.ts +176 -32
- package/dist/next/client.js +212 -90
- package/dist/next/entry-fields.d.ts +162 -0
- package/dist/next/entry-fields.js +2 -0
- package/dist/next/errors.d.ts +136 -0
- package/dist/next/errors.js +214 -0
- package/dist/next/field-names.d.ts +37 -0
- package/dist/next/field-names.js +145 -0
- package/dist/next/graphql/build.d.ts +27 -0
- package/dist/next/graphql/build.js +98 -0
- package/dist/next/graphql/documents.d.ts +67 -0
- package/dist/next/graphql/documents.js +35 -0
- package/dist/next/graphql/edit-mode.d.ts +16 -0
- package/dist/next/graphql/edit-mode.js +93 -0
- package/dist/next/graphql/filter-values.d.ts +34 -0
- package/dist/next/graphql/filter-values.js +96 -0
- package/dist/next/graphql/introspection.d.ts +89 -0
- package/dist/next/graphql/introspection.js +102 -0
- package/dist/next/graphql/plan.d.ts +115 -0
- package/dist/next/graphql/plan.js +531 -0
- package/dist/next/graphql/request.d.ts +228 -0
- package/dist/next/graphql/request.js +283 -0
- package/dist/next/graphql/rest.d.ts +66 -0
- package/dist/next/graphql/rest.js +502 -0
- package/dist/next/graphql/selection.d.ts +55 -0
- package/dist/next/graphql/selection.js +212 -0
- package/dist/next/graphql/sha256.d.ts +13 -0
- package/dist/next/graphql/sha256.js +86 -0
- package/dist/next/graphql/summary.d.ts +83 -0
- package/dist/next/graphql/summary.js +151 -0
- package/dist/next/graphql/tree-layout.d.ts +36 -0
- package/dist/next/graphql/tree-layout.js +20 -0
- package/dist/next/graphql/tree.d.ts +171 -0
- package/dist/next/graphql/tree.js +249 -0
- package/dist/next/graphql/typed.d.ts +261 -0
- package/dist/next/graphql/typed.js +146 -0
- package/dist/next/index.d.ts +30 -3
- package/dist/next/index.js +34 -1
- package/dist/next/inflate.d.ts +51 -0
- package/dist/next/inflate.js +243 -0
- package/dist/next/key-family.d.ts +31 -0
- package/dist/next/key-family.js +66 -0
- package/dist/next/select-types.d.ts +58 -5
- package/dist/next/system-keys.d.ts +27 -0
- package/dist/next/system-keys.js +42 -0
- package/dist/nextjs/image-loader.d.ts +60 -0
- package/dist/nextjs/image-loader.js +71 -0
- package/dist/nextjs/index.d.ts +484 -5
- package/dist/nextjs/index.js +704 -9
- package/dist/nextjs/overlay.d.ts +30 -0
- package/dist/nextjs/overlay.js +78 -0
- package/dist/overlay/index.d.ts +32 -0
- package/dist/overlay/index.js +596 -0
- package/dist/overlay/protocol.d.ts +187 -0
- package/dist/overlay/protocol.js +253 -0
- package/package.json +70 -15
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.selectToSelection = selectToSelection;
|
|
4
|
+
exports.graphqlToSelect = graphqlToSelect;
|
|
5
|
+
/**
|
|
6
|
+
* selection.ts — a REST read as a typed-builder selection, and back.
|
|
7
|
+
*
|
|
8
|
+
* The SDK writes a read three ways: a REST `select` (`entries.list`), the
|
|
9
|
+
* tool spec (`{ model, fields, first, sort, filter }`, which
|
|
10
|
+
* `buildGraphQLQuery` prints and the Explorer and MCP builders share), and the
|
|
11
|
+
* typed builder's selection (`{ articles: { args, nodes: { ... } } }`, which
|
|
12
|
+
* `client.graphql.query` runs and `toTree` reads). `selectToGraphQL` writes a
|
|
13
|
+
* REST read as the tool spec and `selectToSelection` as a selection;
|
|
14
|
+
* `graphqlToSelect` takes either back to REST (G brief, npm item 4). Each goes
|
|
15
|
+
* through the tool spec, which checks every name.
|
|
16
|
+
*
|
|
17
|
+
* A selection written here names what `toTree` needs to answer exactly what
|
|
18
|
+
* REST answers: `id`, `model` and `status` on every entry, `pageInfo {
|
|
19
|
+
* hasNextPage endCursor }` on every relation list, and every media field REST
|
|
20
|
+
* returns.
|
|
21
|
+
*/
|
|
22
|
+
const plan_1 = require("./plan");
|
|
23
|
+
const rest_1 = require("./rest");
|
|
24
|
+
const typed_1 = require("./typed");
|
|
25
|
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
26
|
+
/** The system fields REST returns on every entry, whatever the select says. */
|
|
27
|
+
const ALWAYS = new Set(["id", "model", "status"]);
|
|
28
|
+
const PAGE_INFO = { hasNextPage: true, endCursor: true };
|
|
29
|
+
// ------------------------------------------------ REST read to selection ---
|
|
30
|
+
/** One entry's selection: id, model and status, then each planned field. */
|
|
31
|
+
function entrySelection(fields) {
|
|
32
|
+
const out = { id: true, model: true, status: true };
|
|
33
|
+
for (const planned of fields) {
|
|
34
|
+
if (ALWAYS.has(planned.name))
|
|
35
|
+
continue;
|
|
36
|
+
const kind = planned.field?.kind;
|
|
37
|
+
if (!planned.children) {
|
|
38
|
+
out[planned.name] = true;
|
|
39
|
+
}
|
|
40
|
+
else if (kind === "media") {
|
|
41
|
+
out[planned.name] = Object.fromEntries(planned.children.map((child) => [child.name, true]));
|
|
42
|
+
}
|
|
43
|
+
else if (kind === "relationList") {
|
|
44
|
+
const args = {};
|
|
45
|
+
if (planned.first !== undefined)
|
|
46
|
+
args.first = planned.first;
|
|
47
|
+
if (planned.sort !== undefined)
|
|
48
|
+
args.sort = planned.sort;
|
|
49
|
+
if (planned.after !== undefined)
|
|
50
|
+
args.after = planned.after;
|
|
51
|
+
out[planned.name] = { ...(Object.keys(args).length ? { args } : {}), nodes: entrySelection(planned.children), pageInfo: { ...PAGE_INFO } };
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
out[planned.name] = entrySelection(planned.children);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The typed-builder selection a REST read is equal to. `client.graphql.query`
|
|
61
|
+
* runs it, and `toTree` turns its result into the REST `data`:
|
|
62
|
+
*
|
|
63
|
+
* const schema = await capa.graphqlSchema();
|
|
64
|
+
* const selection = selectToSelection(schema, "articles", "title,author(name)", { sort: ["-views"], limit: 5 });
|
|
65
|
+
* const { data } = await capa.graphql.query(selection);
|
|
66
|
+
* const { articles } = toTree(data, selection, schema);
|
|
67
|
+
* // deep-equal to (await capa.entries.list("articles", { select: "title,author(name)", sort: ["-views"], limit: 5 })).data
|
|
68
|
+
*
|
|
69
|
+
* It takes what `selectToGraphQL` takes and refuses what it refuses, with
|
|
70
|
+
* `CapaBuildError`. Built at run time, its result is untyped.
|
|
71
|
+
*/
|
|
72
|
+
function selectToSelection(summary, namespace, select, options = {}) {
|
|
73
|
+
const plan = (0, plan_1.planQuery)(summary, (0, rest_1.selectToGraphQL)(summary, namespace, select, options));
|
|
74
|
+
const entry = entrySelection(plan.fields);
|
|
75
|
+
if (plan.mode === "single") {
|
|
76
|
+
return { [plan.model.singleField]: { args: { id: plan.id }, ...entry } };
|
|
77
|
+
}
|
|
78
|
+
const args = {};
|
|
79
|
+
// REST's limit before a cursor is GraphQL's last (spec 17, amendment 55),
|
|
80
|
+
// and before=end is last with no before, the end of the list (amendment 132).
|
|
81
|
+
const fromEnd = plan.before === plan_1.END_OF_LIST;
|
|
82
|
+
if (fromEnd)
|
|
83
|
+
args.last = plan.first ?? plan_1.LIST_DEFAULT;
|
|
84
|
+
else if (plan.first !== undefined)
|
|
85
|
+
args[plan.before === undefined ? "first" : "last"] = plan.first;
|
|
86
|
+
if (plan.after !== undefined)
|
|
87
|
+
args.after = plan.after;
|
|
88
|
+
if (plan.before !== undefined && !fromEnd)
|
|
89
|
+
args.before = plan.before;
|
|
90
|
+
if (plan.sort !== undefined)
|
|
91
|
+
args.sort = plan.sort;
|
|
92
|
+
if (plan.filter !== undefined)
|
|
93
|
+
args.filter = plan.filter;
|
|
94
|
+
// A page read backward pages on back from its startCursor, as REST's page.prev.
|
|
95
|
+
const pageInfo = plan.before === undefined ? { ...PAGE_INFO } : { ...PAGE_INFO, hasPreviousPage: true, startCursor: true };
|
|
96
|
+
const root = { ...(Object.keys(args).length ? { args } : {}), nodes: entry, pageInfo };
|
|
97
|
+
if (plan.totalCount)
|
|
98
|
+
root.totalCount = true;
|
|
99
|
+
return { [plan.model.listField]: root };
|
|
100
|
+
}
|
|
101
|
+
// ------------------------------------------------ selection to REST read ---
|
|
102
|
+
/** A connection's entry selection: `nodes`, or `edges { node }`. */
|
|
103
|
+
function entriesOf(selection, name) {
|
|
104
|
+
if (isObject(selection.nodes))
|
|
105
|
+
return selection.nodes;
|
|
106
|
+
if (isObject(selection.edges) && isObject(selection.edges.node))
|
|
107
|
+
return selection.edges.node;
|
|
108
|
+
throw new plan_1.CapaBuildError(`${name} selects no entries: select nodes { ... }, or edges { node { ... } }`);
|
|
109
|
+
}
|
|
110
|
+
/** A level's `args`, refusing any not in `allowed`. */
|
|
111
|
+
function argsOf(selection, allowed, name) {
|
|
112
|
+
const args = selection.args ?? {};
|
|
113
|
+
if (!isObject(args))
|
|
114
|
+
throw new plan_1.CapaBuildError(`args of ${name} must be an object`);
|
|
115
|
+
for (const key of Object.keys(args)) {
|
|
116
|
+
if (args[key] !== undefined && !allowed.includes(key)) {
|
|
117
|
+
throw new plan_1.CapaBuildError(`${key} on ${name} has no REST twin here. Its args: ${allowed.join(", ")}`, [], [...allowed]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return args;
|
|
121
|
+
}
|
|
122
|
+
/** An entry's selection as tool-spec fields; the builder checks every name when the spec is planned. */
|
|
123
|
+
function specFieldsOf(summary, model, selection) {
|
|
124
|
+
const fields = [];
|
|
125
|
+
for (const key of (0, typed_1.selectedKeys)(selection)) {
|
|
126
|
+
const value = selection[key];
|
|
127
|
+
const aliased = (0, typed_1.aliasedField)(value);
|
|
128
|
+
if (aliased !== undefined) {
|
|
129
|
+
throw new plan_1.CapaBuildError(`${key} aliases ${aliased} below the root field, and REST has one value per field: select ${aliased} under its own name`);
|
|
130
|
+
}
|
|
131
|
+
if (ALWAYS.has(key))
|
|
132
|
+
continue;
|
|
133
|
+
const field = model.fields.find((f) => f.name === key);
|
|
134
|
+
if (!isObject(value) || !field) {
|
|
135
|
+
fields.push(key);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const target = summary.models.find((m) => m.namespace === field.target);
|
|
139
|
+
if (field.kind === "media") {
|
|
140
|
+
fields.push({ field: key, fields: (0, typed_1.selectedKeys)(value) });
|
|
141
|
+
}
|
|
142
|
+
else if (field.kind === "relationList" && target) {
|
|
143
|
+
const args = argsOf(value, ["first", "sort", "after"], key);
|
|
144
|
+
const spec = { field: key, fields: specFieldsOf(summary, target, entriesOf(value, key)) };
|
|
145
|
+
if (args.first !== undefined)
|
|
146
|
+
spec.first = args.first;
|
|
147
|
+
// `sort: "name_ASC"` or `["name_ASC"]`: the plan takes either, and refuses more than one.
|
|
148
|
+
if (args.sort !== undefined)
|
|
149
|
+
spec.sort = args.sort;
|
|
150
|
+
// The plan refuses a cursor on a relation list outside a read of one entry, as the API does.
|
|
151
|
+
if (args.after !== undefined)
|
|
152
|
+
spec.after = args.after;
|
|
153
|
+
fields.push(spec);
|
|
154
|
+
}
|
|
155
|
+
else if (target) {
|
|
156
|
+
fields.push({ field: key, fields: specFieldsOf(summary, target, value) });
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
fields.push({ field: key, fields: (0, typed_1.selectedKeys)(value) });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return fields;
|
|
163
|
+
}
|
|
164
|
+
function graphqlToSelect(summary, read) {
|
|
165
|
+
// A tool spec names its model; a selection's keys are root fields, each an object.
|
|
166
|
+
if (isObject(read) && typeof read.model === "string")
|
|
167
|
+
return (0, rest_1.specToSelect)(summary, read);
|
|
168
|
+
return selectionRequest(summary, read);
|
|
169
|
+
}
|
|
170
|
+
/** A selection's REST request: one model root field, a list or a single entry. */
|
|
171
|
+
function selectionRequest(summary, selection) {
|
|
172
|
+
if (!isObject(selection))
|
|
173
|
+
throw new plan_1.CapaBuildError("graphqlToSelect needs a tool spec, { model: \"articles\", ... }, or a builder selection, { articles: { nodes: { ... } } }");
|
|
174
|
+
const roots = (0, typed_1.selectedKeys)(selection);
|
|
175
|
+
if (roots.length !== 1) {
|
|
176
|
+
throw new plan_1.CapaBuildError(`graphqlToSelect reads one model root field, and this selection has ${roots.length}: ${roots.join(", ") || "none"}`);
|
|
177
|
+
}
|
|
178
|
+
const value = selection[roots[0]];
|
|
179
|
+
// REST has no aliases: an alias root reads as the field it names.
|
|
180
|
+
const root = (0, typed_1.aliasedField)(value) ?? roots[0];
|
|
181
|
+
const list = summary.models.find((m) => m.listField === root);
|
|
182
|
+
const single = list ? undefined : summary.models.find((m) => m.singleField === root);
|
|
183
|
+
if ((!list && !single) || !isObject(value)) {
|
|
184
|
+
throw new plan_1.CapaBuildError(`${root} is not a model root field, so it has no REST read`, [], summary.models.flatMap((m) => [m.listField, m.singleField]));
|
|
185
|
+
}
|
|
186
|
+
if (single) {
|
|
187
|
+
const args = argsOf(value, ["id"], root);
|
|
188
|
+
const spec = { model: single.namespace, mode: "single", id: args.id, fields: specFieldsOf(summary, single, value) };
|
|
189
|
+
return (0, rest_1.specToSelect)(summary, spec);
|
|
190
|
+
}
|
|
191
|
+
const model = (0, plan_1.findModel)(summary, list.namespace);
|
|
192
|
+
const args = argsOf(value, ["first", "last", "after", "before", "sort", "filter"], root);
|
|
193
|
+
const spec = { model: model.namespace, fields: specFieldsOf(summary, model, entriesOf(value, root)) };
|
|
194
|
+
const first = args.first ?? args.last;
|
|
195
|
+
if (first !== undefined)
|
|
196
|
+
spec.first = first;
|
|
197
|
+
if (args.after !== undefined)
|
|
198
|
+
spec.after = args.after;
|
|
199
|
+
// GraphQL reads a null cursor as none, as Relay's first backward fetch sends it.
|
|
200
|
+
if (args.before !== undefined && args.before !== null)
|
|
201
|
+
spec.before = args.before;
|
|
202
|
+
// last with no cursor reads the end of the list: REST's before=end (spec 17, amendment 132).
|
|
203
|
+
else if (args.last !== undefined && args.last !== null)
|
|
204
|
+
spec.before = plan_1.END_OF_LIST;
|
|
205
|
+
if (args.sort !== undefined)
|
|
206
|
+
spec.sort = (Array.isArray(args.sort) ? args.sort : [args.sort]);
|
|
207
|
+
if (args.filter !== undefined)
|
|
208
|
+
spec.filter = args.filter;
|
|
209
|
+
if (value.totalCount === true)
|
|
210
|
+
spec.totalCount = true;
|
|
211
|
+
return (0, rest_1.specToSelect)(summary, spec);
|
|
212
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sha256.ts — the hash a persisted query is stored under.
|
|
3
|
+
*
|
|
4
|
+
* Web Crypto when the runtime has it (browsers, edge runtimes, Node 19+), and a
|
|
5
|
+
* small pure implementation otherwise. The fallback exists instead of a
|
|
6
|
+
* `node:crypto` import because this entry point is also bundled for the
|
|
7
|
+
* browser and the edge, where a `node:` specifier fails the build, and Node 18
|
|
8
|
+
* (still inside `engines`) has no global `crypto`.
|
|
9
|
+
*/
|
|
10
|
+
/** SHA-256 of `bytes`, synchronously. Exported for tests. */
|
|
11
|
+
export declare function sha256Pure(bytes: Uint8Array): string;
|
|
12
|
+
/** Lowercase hex SHA-256 of the UTF-8 bytes of `text`. */
|
|
13
|
+
export declare function sha256Hex(text: string): Promise<string>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* sha256.ts — the hash a persisted query is stored under.
|
|
4
|
+
*
|
|
5
|
+
* Web Crypto when the runtime has it (browsers, edge runtimes, Node 19+), and a
|
|
6
|
+
* small pure implementation otherwise. The fallback exists instead of a
|
|
7
|
+
* `node:crypto` import because this entry point is also bundled for the
|
|
8
|
+
* browser and the edge, where a `node:` specifier fails the build, and Node 18
|
|
9
|
+
* (still inside `engines`) has no global `crypto`.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.sha256Pure = sha256Pure;
|
|
13
|
+
exports.sha256Hex = sha256Hex;
|
|
14
|
+
const K = new Uint32Array([
|
|
15
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
16
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
17
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
18
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
19
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
20
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
21
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
22
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
23
|
+
]);
|
|
24
|
+
const rotr = (x, n) => (x >>> n) | (x << (32 - n));
|
|
25
|
+
function hex(bytes) {
|
|
26
|
+
let out = "";
|
|
27
|
+
for (const byte of bytes)
|
|
28
|
+
out += byte.toString(16).padStart(2, "0");
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
/** SHA-256 of `bytes`, synchronously. Exported for tests. */
|
|
32
|
+
function sha256Pure(bytes) {
|
|
33
|
+
const bitLength = bytes.length * 8;
|
|
34
|
+
const padded = new Uint8Array(((bytes.length + 9 + 63) >> 6) << 6);
|
|
35
|
+
padded.set(bytes);
|
|
36
|
+
padded[bytes.length] = 0x80;
|
|
37
|
+
const view = new DataView(padded.buffer);
|
|
38
|
+
view.setUint32(padded.length - 8, Math.floor(bitLength / 0x100000000));
|
|
39
|
+
view.setUint32(padded.length - 4, bitLength >>> 0);
|
|
40
|
+
const h = new Uint32Array([
|
|
41
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
|
42
|
+
]);
|
|
43
|
+
const w = new Uint32Array(64);
|
|
44
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
45
|
+
for (let i = 0; i < 16; i++)
|
|
46
|
+
w[i] = view.getUint32(offset + i * 4);
|
|
47
|
+
for (let i = 16; i < 64; i++) {
|
|
48
|
+
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
|
|
49
|
+
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
|
|
50
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
|
|
51
|
+
}
|
|
52
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
53
|
+
for (let i = 0; i < 64; i++) {
|
|
54
|
+
const t1 = (hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i]) >>> 0;
|
|
55
|
+
const t2 = ((rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
|
|
56
|
+
hh = g;
|
|
57
|
+
g = f;
|
|
58
|
+
f = e;
|
|
59
|
+
e = (d + t1) >>> 0;
|
|
60
|
+
d = c;
|
|
61
|
+
c = b;
|
|
62
|
+
b = a;
|
|
63
|
+
a = (t1 + t2) >>> 0;
|
|
64
|
+
}
|
|
65
|
+
h[0] += a;
|
|
66
|
+
h[1] += b;
|
|
67
|
+
h[2] += c;
|
|
68
|
+
h[3] += d;
|
|
69
|
+
h[4] += e;
|
|
70
|
+
h[5] += f;
|
|
71
|
+
h[6] += g;
|
|
72
|
+
h[7] += hh;
|
|
73
|
+
}
|
|
74
|
+
const out = new Uint8Array(32);
|
|
75
|
+
const outView = new DataView(out.buffer);
|
|
76
|
+
h.forEach((word, i) => outView.setUint32(i * 4, word));
|
|
77
|
+
return hex(out);
|
|
78
|
+
}
|
|
79
|
+
/** Lowercase hex SHA-256 of the UTF-8 bytes of `text`. */
|
|
80
|
+
async function sha256Hex(text) {
|
|
81
|
+
const bytes = new TextEncoder().encode(text);
|
|
82
|
+
const subtle = globalThis.crypto?.subtle;
|
|
83
|
+
if (subtle)
|
|
84
|
+
return hex(new Uint8Array(await subtle.digest("SHA-256", bytes)));
|
|
85
|
+
return sha256Pure(bytes);
|
|
86
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type CapaIntrospection } from "./introspection";
|
|
2
|
+
export type GraphQLFieldKind = "scalar" | "media" | "json" | "relation" | "relationList" | "id" | "idList";
|
|
3
|
+
export interface GraphQLFieldSummary {
|
|
4
|
+
/** The GraphQL field name, which differs from the namespace when N5 renamed it. */
|
|
5
|
+
name: string;
|
|
6
|
+
namespace: string;
|
|
7
|
+
capaType: string;
|
|
8
|
+
arrayType: string | null;
|
|
9
|
+
graphqlType: string;
|
|
10
|
+
kind: GraphQLFieldKind;
|
|
11
|
+
/** The related model's namespace, for a relation this key can read. */
|
|
12
|
+
target: string | null;
|
|
13
|
+
sortable: boolean;
|
|
14
|
+
/** The operators and one-hop fields its filter input accepts. */
|
|
15
|
+
filterOps: string[];
|
|
16
|
+
/**
|
|
17
|
+
* The GraphQL type of each of `filterOps`: `String` for `has` on a list of
|
|
18
|
+
* text or true/false values, `Boolean` for `exists`, `StringFilter` for a
|
|
19
|
+
* hop. What a REST filter value is converted to (`selectToGraphQL`).
|
|
20
|
+
*/
|
|
21
|
+
filterInputs: Record<string, string>;
|
|
22
|
+
/** Why a later Capa-Version phases this field out, or null while it is current. */
|
|
23
|
+
deprecationReason: string | null;
|
|
24
|
+
}
|
|
25
|
+
/** A system field a model's filter takes (`id`, `createdAt`, `_tags`), with its operators as for a field. */
|
|
26
|
+
export type GraphQLSystemFilterSummary = Pick<GraphQLFieldSummary, "name" | "filterOps" | "filterInputs">;
|
|
27
|
+
export interface GraphQLModelSummary {
|
|
28
|
+
namespace: string;
|
|
29
|
+
typeName: string;
|
|
30
|
+
description: string;
|
|
31
|
+
listField: string;
|
|
32
|
+
singleField: string;
|
|
33
|
+
filterType: string;
|
|
34
|
+
/**
|
|
35
|
+
* The system fields `filterType` filters, in the order it declares them,
|
|
36
|
+
* each with its operators: `id`, `createdAt`, `updatedAt`, `publishedAt`
|
|
37
|
+
* and `_tags` on the API today (spec 17, amendment 56).
|
|
38
|
+
*/
|
|
39
|
+
systemFilters: GraphQLSystemFilterSummary[];
|
|
40
|
+
sortType: string;
|
|
41
|
+
sortValues: string[];
|
|
42
|
+
fields: GraphQLFieldSummary[];
|
|
43
|
+
/**
|
|
44
|
+
* Namespaces of the model's fields the type leaves out, because their
|
|
45
|
+
* GraphQL names collide (N5): the description's `Not exposed:` line. REST
|
|
46
|
+
* still reads them, so a system key they shadow is `$`-named there too.
|
|
47
|
+
*/
|
|
48
|
+
notExposed: string[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* `Q` is the `CapaQuery` type of the client that read the schema, when it was
|
|
52
|
+
* created with one: `toTree` reads the types from it. It is never a runtime
|
|
53
|
+
* value.
|
|
54
|
+
*/
|
|
55
|
+
export interface GraphQLSchemaSummary<Q = unknown> {
|
|
56
|
+
version: string;
|
|
57
|
+
models: GraphQLModelSummary[];
|
|
58
|
+
/**
|
|
59
|
+
* Namespaces of the models this key reads that GraphQL leaves out, because
|
|
60
|
+
* their type names would collide (N1): the `Query` description's
|
|
61
|
+
* `Models not exposed in GraphQL:` list. REST reads them at
|
|
62
|
+
* `/api/entries/<namespace>`.
|
|
63
|
+
*/
|
|
64
|
+
restOnly: string[];
|
|
65
|
+
readonly __capaQuery?: Q;
|
|
66
|
+
}
|
|
67
|
+
export declare function modelNamespaceOf(description: string | null | undefined): string | null;
|
|
68
|
+
/** A model's display name, the one the Capa admin shows: its description's first line without `(model <namespace>)`. */
|
|
69
|
+
export declare function modelDisplayName(model: GraphQLModelSummary): string;
|
|
70
|
+
/** The namespaces a model type's description lists as not exposed. */
|
|
71
|
+
export declare function notExposedFields(description: string | null | undefined): string[];
|
|
72
|
+
export declare function parseFieldDescription(description: string | null | undefined): {
|
|
73
|
+
namespace: string;
|
|
74
|
+
capaType: string;
|
|
75
|
+
arrayType: string | null;
|
|
76
|
+
target: string | null;
|
|
77
|
+
} | null;
|
|
78
|
+
/**
|
|
79
|
+
* Compact a `CapaIntrospection` answer. Models sorted by namespace. Pass the
|
|
80
|
+
* codegen'd `CapaQuery` as `Q` (`summarizeIntrospection<CapaQuery>(json)`) to
|
|
81
|
+
* type `toTree` with a schema read from a file.
|
|
82
|
+
*/
|
|
83
|
+
export declare function summarizeIntrospection<Q = unknown>(introspection: CapaIntrospection): GraphQLSchemaSummary<Q>;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.modelNamespaceOf = modelNamespaceOf;
|
|
4
|
+
exports.modelDisplayName = modelDisplayName;
|
|
5
|
+
exports.notExposedFields = notExposedFields;
|
|
6
|
+
exports.parseFieldDescription = parseFieldDescription;
|
|
7
|
+
exports.summarizeIntrospection = summarizeIntrospection;
|
|
8
|
+
/**
|
|
9
|
+
* summary.ts — a key's GraphQL schema, compacted to what a query builder needs.
|
|
10
|
+
*
|
|
11
|
+
* Every name here is READ from introspection, never computed: the API owns the
|
|
12
|
+
* naming rules (G plan N1 to N10), and a builder that re-derived `blogPosts`
|
|
13
|
+
* from `blog_posts` would drift from the server the first time a rule moved.
|
|
14
|
+
* The model namespace and Capa field type are not GraphQL concepts, so the API
|
|
15
|
+
* writes them into descriptions in fixed formats (N9), parsed here.
|
|
16
|
+
*/
|
|
17
|
+
const field_names_1 = require("../field-names");
|
|
18
|
+
const introspection_1 = require("./introspection");
|
|
19
|
+
/** N9: `<modelName> (model <namespace>)`, on the first line of a model type's description. */
|
|
20
|
+
const MODEL_DESCRIPTION = /\(model ([^()\s]+)\)$/;
|
|
21
|
+
/**
|
|
22
|
+
* N9: `Capa field <namespace>, type <type>[ of <arrayType>][, model <target>]`,
|
|
23
|
+
* on the description's last line. The lines above it are the field's label
|
|
24
|
+
* and an enum's options, for people (spec 17, amendment 49). The namespace is
|
|
25
|
+
* written as REST writes it, quoted when it holds a character REST's grammars
|
|
26
|
+
* use (`"price.usd"`, amendment 121), so it is read back before use.
|
|
27
|
+
*/
|
|
28
|
+
const FIELD_DESCRIPTION = /(?:^|\n)Capa field ("(?:[^"]|"")*"|[^,\s]+), type (\S+)(?: of (\S+))?(?:, model ([^,\s]+))?$/;
|
|
29
|
+
/** N9: the second line of a model type's description, when N5 left fields out. */
|
|
30
|
+
const NOT_EXPOSED = /^Not exposed: (.+)$/m;
|
|
31
|
+
/** N9: the end of the `Query` description, when N1 left models out. */
|
|
32
|
+
const MODELS_NOT_EXPOSED = /Models not exposed in GraphQL: ([^\n]+?)\.?$/m;
|
|
33
|
+
/** The keys of a `<Type>Filter` that combine conditions rather than name a field. */
|
|
34
|
+
const FILTER_LOGIC = new Set(["and", "or", "not"]);
|
|
35
|
+
function modelNamespaceOf(description) {
|
|
36
|
+
const firstLine = (description ?? "").split("\n")[0];
|
|
37
|
+
return MODEL_DESCRIPTION.exec(firstLine)?.[1] ?? null;
|
|
38
|
+
}
|
|
39
|
+
/** A model's display name, the one the Capa admin shows: its description's first line without `(model <namespace>)`. */
|
|
40
|
+
function modelDisplayName(model) {
|
|
41
|
+
return model.description.split("\n")[0].replace(/ \(model [^()]+\)$/, "") || model.namespace;
|
|
42
|
+
}
|
|
43
|
+
/** The namespaces of an N9 list line, each bare or quoted as REST writes it. */
|
|
44
|
+
function namespaceList(line) {
|
|
45
|
+
if (!line)
|
|
46
|
+
return [];
|
|
47
|
+
const items = (0, field_names_1.splitOutside)(line[1], ",", { nested: false }) ?? line[1].split(",");
|
|
48
|
+
return items.map((item) => item.trim()).filter(Boolean).map((item) => (0, field_names_1.readName)(item) ?? item);
|
|
49
|
+
}
|
|
50
|
+
/** The namespaces a model type's description lists as not exposed. */
|
|
51
|
+
function notExposedFields(description) {
|
|
52
|
+
return namespaceList(NOT_EXPOSED.exec(description ?? ""));
|
|
53
|
+
}
|
|
54
|
+
function parseFieldDescription(description) {
|
|
55
|
+
const match = FIELD_DESCRIPTION.exec(description ?? "");
|
|
56
|
+
const namespace = match ? (0, field_names_1.readName)(match[1]) : null;
|
|
57
|
+
if (!match || namespace === null)
|
|
58
|
+
return null;
|
|
59
|
+
return { namespace, capaType: match[2], arrayType: match[3] ?? null, target: match[4] ?? null };
|
|
60
|
+
}
|
|
61
|
+
function kindOf(field, types, modelTypes) {
|
|
62
|
+
const named = (0, introspection_1.namedType)(field.type);
|
|
63
|
+
const list = (0, introspection_1.isListType)(field.type);
|
|
64
|
+
if (named === "ID")
|
|
65
|
+
return list ? "idList" : "id";
|
|
66
|
+
if (named === "Media")
|
|
67
|
+
return "media";
|
|
68
|
+
if (named === "JSON")
|
|
69
|
+
return "json";
|
|
70
|
+
if (modelTypes.has(named))
|
|
71
|
+
return "relation";
|
|
72
|
+
if (named.endsWith("RelationConnection") && types.get(named)?.kind === "OBJECT")
|
|
73
|
+
return "relationList";
|
|
74
|
+
return "scalar";
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Compact a `CapaIntrospection` answer. Models sorted by namespace. Pass the
|
|
78
|
+
* codegen'd `CapaQuery` as `Q` (`summarizeIntrospection<CapaQuery>(json)`) to
|
|
79
|
+
* type `toTree` with a schema read from a file.
|
|
80
|
+
*/
|
|
81
|
+
function summarizeIntrospection(introspection) {
|
|
82
|
+
const types = new Map(introspection.__schema.types.map((type) => [type.name, type]));
|
|
83
|
+
const query = types.get(introspection.__schema.queryType.name);
|
|
84
|
+
const roots = query?.fields ?? [];
|
|
85
|
+
const namespaceByType = new Map();
|
|
86
|
+
for (const type of introspection.__schema.types) {
|
|
87
|
+
if (type.kind !== "OBJECT")
|
|
88
|
+
continue;
|
|
89
|
+
const namespace = modelNamespaceOf(type.description);
|
|
90
|
+
if (namespace)
|
|
91
|
+
namespaceByType.set(type.name, namespace);
|
|
92
|
+
}
|
|
93
|
+
const modelTypes = new Set(namespaceByType.keys());
|
|
94
|
+
const models = [];
|
|
95
|
+
for (const [typeName, namespace] of namespaceByType) {
|
|
96
|
+
const list = roots.find((root) => (0, introspection_1.namedType)(root.type) === `${typeName}Connection` && root.args.some((arg) => arg.name === "first"));
|
|
97
|
+
const single = roots.find((root) => (0, introspection_1.namedType)(root.type) === typeName && root.args.some((arg) => arg.name === "id"));
|
|
98
|
+
if (!list || !single)
|
|
99
|
+
continue;
|
|
100
|
+
const filterType = (0, introspection_1.namedType)(list.args.find((arg) => arg.name === "filter")?.type ?? { kind: "", name: "" });
|
|
101
|
+
const sortType = (0, introspection_1.namedType)(list.args.find((arg) => arg.name === "sort")?.type ?? { kind: "", name: "" });
|
|
102
|
+
const sortValues = (types.get(sortType)?.enumValues ?? []).map((value) => value.name);
|
|
103
|
+
const filterFields = new Map((types.get(filterType)?.inputFields ?? []).map((input) => [input.name, input]));
|
|
104
|
+
const type = types.get(typeName);
|
|
105
|
+
const operandsOf = (name) => {
|
|
106
|
+
const input = filterFields.get(name);
|
|
107
|
+
const operands = input ? (types.get((0, introspection_1.namedType)(input.type))?.inputFields ?? []) : [];
|
|
108
|
+
return {
|
|
109
|
+
filterOps: operands.map((operand) => operand.name),
|
|
110
|
+
filterInputs: Object.fromEntries(operands.map((operand) => [operand.name, (0, introspection_1.namedType)(operand.type)])),
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
const fields = [];
|
|
114
|
+
for (const field of type.fields ?? []) {
|
|
115
|
+
const parsed = parseFieldDescription(field.description);
|
|
116
|
+
if (!parsed)
|
|
117
|
+
continue;
|
|
118
|
+
fields.push({
|
|
119
|
+
name: field.name,
|
|
120
|
+
namespace: parsed.namespace,
|
|
121
|
+
capaType: parsed.capaType,
|
|
122
|
+
arrayType: parsed.arrayType,
|
|
123
|
+
graphqlType: (0, introspection_1.printTypeRef)(field.type),
|
|
124
|
+
kind: kindOf(field, types, modelTypes),
|
|
125
|
+
target: parsed.target,
|
|
126
|
+
sortable: sortValues.includes(`${field.name}_ASC`),
|
|
127
|
+
...operandsOf(field.name),
|
|
128
|
+
deprecationReason: field.isDeprecated ? field.deprecationReason ?? "No longer supported" : null,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const fieldNames = new Set(fields.map((field) => field.name));
|
|
132
|
+
models.push({
|
|
133
|
+
namespace,
|
|
134
|
+
typeName,
|
|
135
|
+
description: type.description ?? "",
|
|
136
|
+
listField: list.name,
|
|
137
|
+
singleField: single.name,
|
|
138
|
+
filterType,
|
|
139
|
+
systemFilters: [...filterFields.keys()]
|
|
140
|
+
.filter((name) => !FILTER_LOGIC.has(name) && !fieldNames.has(name))
|
|
141
|
+
.map((name) => ({ name, ...operandsOf(name) })),
|
|
142
|
+
sortType,
|
|
143
|
+
sortValues,
|
|
144
|
+
fields,
|
|
145
|
+
notExposed: notExposedFields(type.description),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
models.sort((a, b) => (a.namespace < b.namespace ? -1 : a.namespace > b.namespace ? 1 : 0));
|
|
149
|
+
const restOnly = namespaceList(MODELS_NOT_EXPOSED.exec(query?.description ?? ""));
|
|
150
|
+
return { version: introspection.version ?? "", models, restOnly };
|
|
151
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tree-layout.ts — the part of a key's schema `toTree` reads, small enough to
|
|
3
|
+
* ship in the code codegen writes.
|
|
4
|
+
*
|
|
5
|
+
* `toTree` needs to know, for each model, its root fields, and for each field
|
|
6
|
+
* whether it is a plain value under its own name or something to lay out: a
|
|
7
|
+
* field N5 renamed (`hero_image` is `hero-image` under REST's `fields`), a
|
|
8
|
+
* relation or a relation list to expand, a relation the key cannot read (an
|
|
9
|
+
* id), or media. A `GraphQLSchemaSummary` knows that, from one introspection
|
|
10
|
+
* request. `capa-codegen --graphql` knows it too, when it writes the types, so
|
|
11
|
+
* it writes it down as `capaTreeLayout`, and a server that imports it reads no
|
|
12
|
+
* schema at all before its first `toTree`.
|
|
13
|
+
*/
|
|
14
|
+
import type { GraphQLFieldSummary, GraphQLSchemaSummary } from "./summary";
|
|
15
|
+
/** A field `toTree` lays out rather than copies, by its GraphQL `name`. */
|
|
16
|
+
export type TreeLayoutField = Pick<GraphQLFieldSummary, "name" | "namespace" | "kind" | "target">;
|
|
17
|
+
export interface TreeLayoutModel {
|
|
18
|
+
/** The model's namespace: each entry's `model`, and what a relation's `target` names. */
|
|
19
|
+
namespace: string;
|
|
20
|
+
listField: string;
|
|
21
|
+
singleField: string;
|
|
22
|
+
/** The fields that are not a plain value under their own name. Any other field is copied as it is. */
|
|
23
|
+
fields: ReadonlyArray<TreeLayoutField>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* What `toTree` reads of a schema. A `GraphQLSchemaSummary` is one, and so is
|
|
27
|
+
* the `capaTreeLayout` constant `capa-codegen --graphql` writes, typed with the
|
|
28
|
+
* codegen'd `CapaQuery` as `Q` so the tree is typed from the selection.
|
|
29
|
+
*/
|
|
30
|
+
export interface CapaTreeLayout<Q = unknown> {
|
|
31
|
+
readonly models: ReadonlyArray<TreeLayoutModel>;
|
|
32
|
+
/** The `CapaQuery` type the layout was written for. Never a runtime value. */
|
|
33
|
+
readonly __capaQuery?: Q;
|
|
34
|
+
}
|
|
35
|
+
/** The layout of a schema summary: every model, and only the fields `toTree` lays out. */
|
|
36
|
+
export declare function treeLayoutOf(summary: GraphQLSchemaSummary): CapaTreeLayout;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.treeLayoutOf = treeLayoutOf;
|
|
4
|
+
/** Whether `toTree` copies a field as it is: a scalar or JSON value under its own name. */
|
|
5
|
+
function copiedAsIs(field) {
|
|
6
|
+
return (field.kind === "scalar" || field.kind === "json") && field.name === field.namespace;
|
|
7
|
+
}
|
|
8
|
+
/** The layout of a schema summary: every model, and only the fields `toTree` lays out. */
|
|
9
|
+
function treeLayoutOf(summary) {
|
|
10
|
+
return {
|
|
11
|
+
models: summary.models.map((model) => ({
|
|
12
|
+
namespace: model.namespace,
|
|
13
|
+
listField: model.listField,
|
|
14
|
+
singleField: model.singleField,
|
|
15
|
+
fields: model.fields
|
|
16
|
+
.filter((field) => !copiedAsIs(field))
|
|
17
|
+
.map(({ name, namespace, kind, target }) => ({ name, namespace, kind, target })),
|
|
18
|
+
})),
|
|
19
|
+
};
|
|
20
|
+
}
|