@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.
- package/CHANGELOG.md +450 -0
- package/README.md +1698 -193
- 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 +84 -10
- package/dist/next/attrs.js +119 -2
- 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 -5
- package/dist/next/index.js +32 -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 +688 -6
- package/dist/nextjs/overlay.d.ts +30 -0
- package/dist/nextjs/overlay.js +78 -0
- package/dist/overlay/index.d.ts +14 -2
- package/dist/overlay/index.js +282 -43
- package/dist/overlay/protocol.d.ts +98 -2
- package/dist/overlay/protocol.js +151 -4
- package/package.json +63 -15
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* documents.ts — GraphQL documents written as literals, typed by their text.
|
|
3
|
+
*
|
|
4
|
+
* A document written in a project as a literal keeps its exact text as its
|
|
5
|
+
* type, in any of three forms:
|
|
6
|
+
*
|
|
7
|
+
* const LATEST = `#graphql
|
|
8
|
+
* query Latest($first: Int) { articles(first: $first) { nodes { title } } }
|
|
9
|
+
* `;
|
|
10
|
+
* const ONE = /* capa *\/ `query One($id: ID!) { article(id: $id) { title } }`;
|
|
11
|
+
* const VERSION = gql(`query Version { version }`);
|
|
12
|
+
*
|
|
13
|
+
* `capa-codegen --graphql` finds each one, checks it against the key's schema,
|
|
14
|
+
* and adds an entry to `CapaDocuments` keyed by that text, holding its result
|
|
15
|
+
* and variables types. `client.graphql(LATEST)` and `graphql(LATEST)` from
|
|
16
|
+
* `/nextjs` look the text up, so `data` and `variables` are typed with no cast
|
|
17
|
+
* and no import from the generated file, the way Hydrogen types
|
|
18
|
+
* `storefront.query`.
|
|
19
|
+
*
|
|
20
|
+
* `gql` used as a tag (gql`query ...`) returns a plain string: TypeScript
|
|
21
|
+
* gives a tagged template no literal type. Codegen still checks it and exports
|
|
22
|
+
* a typed `<Name>Document` for it.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* The documents written as literals in your project, by their exact text.
|
|
26
|
+
* Empty here: the module `capa-codegen --graphql` writes adds to it with
|
|
27
|
+
* `declare module "@capacms/sdk/next"`, so keep that file inside your
|
|
28
|
+
* tsconfig's `include`.
|
|
29
|
+
*/
|
|
30
|
+
export interface CapaDocuments {
|
|
31
|
+
}
|
|
32
|
+
/** The result type codegen recorded for a literal document. */
|
|
33
|
+
export type CapaDocumentResult<D> = D extends keyof CapaDocuments ? CapaDocuments[D] extends {
|
|
34
|
+
result: infer R;
|
|
35
|
+
} ? R : never : never;
|
|
36
|
+
/** The variables type codegen recorded for a literal document. */
|
|
37
|
+
export type CapaDocumentVariables<D> = D extends keyof CapaDocuments ? CapaDocuments[D] extends {
|
|
38
|
+
variables: infer V;
|
|
39
|
+
} ? V : never : never;
|
|
40
|
+
/**
|
|
41
|
+
* What a call with a `#graphql` literal that `capa-codegen --graphql` has not
|
|
42
|
+
* seen takes instead of the literal, so the call fails to compile with the
|
|
43
|
+
* fix in the message rather than going untyped.
|
|
44
|
+
*/
|
|
45
|
+
export type RunCapaCodegen = "This #graphql document is not in CapaDocuments yet: run capa-codegen --graphql (or keep capa-codegen --graphql --watch running) to type its data and variables.";
|
|
46
|
+
/**
|
|
47
|
+
* `T`, unless `D` is a document codegen recorded, or a `#graphql` literal it
|
|
48
|
+
* has not. A recorded document is typed by its record, so the untyped
|
|
49
|
+
* signature refuses it: otherwise wrong variables would compile through it. A
|
|
50
|
+
* `#graphql` literal codegen has not seen (new, or edited since the last run)
|
|
51
|
+
* is `RunCapaCodegen`, so it does not compile until codegen has read it.
|
|
52
|
+
* Text built at run time is a plain `string` and stays untyped, and so does a
|
|
53
|
+
* call that names its data type, `capa.graphql<Data>(text)`.
|
|
54
|
+
*/
|
|
55
|
+
export type NotARecordedDocument<D, T> = D extends keyof CapaDocuments ? never : string extends D ? T : D extends `${string}#graphql${string}` ? RunCapaCodegen : T;
|
|
56
|
+
/**
|
|
57
|
+
* A GraphQL document, returned as written.
|
|
58
|
+
*
|
|
59
|
+
* Called with a literal, gql(`query ...`), it returns the literal with its
|
|
60
|
+
* exact text as its type, which is what `client.graphql` looks up once
|
|
61
|
+
* `capa-codegen --graphql` has seen it. Used as a tag, gql`query ...`, it
|
|
62
|
+
* returns a plain string; import the `<Name>Document` codegen writes for its
|
|
63
|
+
* types. Values interpolated into a tag are joined in as text, and codegen
|
|
64
|
+
* skips such a document, since its text is only known at run time.
|
|
65
|
+
*/
|
|
66
|
+
export declare function gql<T extends string>(document: T): T;
|
|
67
|
+
export declare function gql(strings: TemplateStringsArray, ...values: ReadonlyArray<string | number>): string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* documents.ts — GraphQL documents written as literals, typed by their text.
|
|
4
|
+
*
|
|
5
|
+
* A document written in a project as a literal keeps its exact text as its
|
|
6
|
+
* type, in any of three forms:
|
|
7
|
+
*
|
|
8
|
+
* const LATEST = `#graphql
|
|
9
|
+
* query Latest($first: Int) { articles(first: $first) { nodes { title } } }
|
|
10
|
+
* `;
|
|
11
|
+
* const ONE = /* capa *\/ `query One($id: ID!) { article(id: $id) { title } }`;
|
|
12
|
+
* const VERSION = gql(`query Version { version }`);
|
|
13
|
+
*
|
|
14
|
+
* `capa-codegen --graphql` finds each one, checks it against the key's schema,
|
|
15
|
+
* and adds an entry to `CapaDocuments` keyed by that text, holding its result
|
|
16
|
+
* and variables types. `client.graphql(LATEST)` and `graphql(LATEST)` from
|
|
17
|
+
* `/nextjs` look the text up, so `data` and `variables` are typed with no cast
|
|
18
|
+
* and no import from the generated file, the way Hydrogen types
|
|
19
|
+
* `storefront.query`.
|
|
20
|
+
*
|
|
21
|
+
* `gql` used as a tag (gql`query ...`) returns a plain string: TypeScript
|
|
22
|
+
* gives a tagged template no literal type. Codegen still checks it and exports
|
|
23
|
+
* a typed `<Name>Document` for it.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.gql = gql;
|
|
27
|
+
function gql(document, ...values) {
|
|
28
|
+
if (typeof document === "string")
|
|
29
|
+
return document;
|
|
30
|
+
let text = document[0];
|
|
31
|
+
values.forEach((value, i) => {
|
|
32
|
+
text += String(value) + document[i + 1];
|
|
33
|
+
});
|
|
34
|
+
return text;
|
|
35
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CapaFieldNames } from "./introspection";
|
|
2
|
+
/** How long a key's renamed fields are kept before the names are read again. */
|
|
3
|
+
export declare const RENAMED_TTL_MS = 60000;
|
|
4
|
+
/** Per model namespace, its fields whose GraphQL name is not their namespace, by GraphQL name. */
|
|
5
|
+
export type RenamedFields = ReadonlyMap<string, Readonly<Record<string, string>>>;
|
|
6
|
+
/** The fields of each model that N5 renamed, from the N9 descriptions. Models with none are left out. */
|
|
7
|
+
export declare function renamedFields(names: CapaFieldNames): RenamedFields;
|
|
8
|
+
/**
|
|
9
|
+
* Run `read` in edit mode: the key's renamed fields are read beside it (or
|
|
10
|
+
* come from the cache), and its result's entries are marked once both are in.
|
|
11
|
+
*/
|
|
12
|
+
export declare function readMarked<R extends {
|
|
13
|
+
data: unknown;
|
|
14
|
+
}>(document: string, key: string, readNames: () => Promise<CapaFieldNames>, read: () => Promise<R>): Promise<R>;
|
|
15
|
+
/** Forget every key's renamed fields. For tests. */
|
|
16
|
+
export declare function __resetEditSchemaForTests(): void;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RENAMED_TTL_MS = void 0;
|
|
4
|
+
exports.renamedFields = renamedFields;
|
|
5
|
+
exports.readMarked = readMarked;
|
|
6
|
+
exports.__resetEditSchemaForTests = __resetEditSchemaForTests;
|
|
7
|
+
/**
|
|
8
|
+
* edit-mode.ts — a GraphQL read in edit mode, marked for live preview.
|
|
9
|
+
*
|
|
10
|
+
* A client created with `editMode: true` marks what it reads, so `capaAttrs`
|
|
11
|
+
* tags it for the Capa editor (attrs.ts). In a GraphQL result an entry is an
|
|
12
|
+
* object that selected `id` and `model`. A field GraphQL renamed (N5:
|
|
13
|
+
* `hero_image` for `hero-image`, `status_field` for `status`) must still be
|
|
14
|
+
* tagged by its namespace, since the editor knows fields by namespace, and
|
|
15
|
+
* only the schema's descriptions say which fields those are.
|
|
16
|
+
*
|
|
17
|
+
* So a marked read also reads the key's type and field names
|
|
18
|
+
* (`FIELD_NAMES_QUERY`), kept for a minute per host, key and version, the
|
|
19
|
+
* same lifetime as the MCP server's copy. That read starts beside the data
|
|
20
|
+
* read, so the result waits for the slower of the two rather than for both
|
|
21
|
+
* in turn. A document that never says `model` cannot select it, so its
|
|
22
|
+
* result holds no entry and it reads no names. The data request itself is
|
|
23
|
+
* the same with or without edit mode.
|
|
24
|
+
*/
|
|
25
|
+
const attrs_1 = require("../attrs");
|
|
26
|
+
const summary_1 = require("./summary");
|
|
27
|
+
/** How long a key's renamed fields are kept before the names are read again. */
|
|
28
|
+
exports.RENAMED_TTL_MS = 60_000;
|
|
29
|
+
const RENAMED_LIMIT = 100;
|
|
30
|
+
const renamedCache = new Map();
|
|
31
|
+
/** The fields of each model that N5 renamed, from the N9 descriptions. Models with none are left out. */
|
|
32
|
+
function renamedFields(names) {
|
|
33
|
+
const out = new Map();
|
|
34
|
+
for (const type of names.__schema.types) {
|
|
35
|
+
const namespace = type.kind === "OBJECT" ? (0, summary_1.modelNamespaceOf)(type.description) : null;
|
|
36
|
+
if (!namespace)
|
|
37
|
+
continue;
|
|
38
|
+
const renamed = {};
|
|
39
|
+
for (const field of type.fields ?? []) {
|
|
40
|
+
const parsed = (0, summary_1.parseFieldDescription)(field.description);
|
|
41
|
+
if (parsed && parsed.namespace !== field.name)
|
|
42
|
+
renamed[field.name] = parsed.namespace;
|
|
43
|
+
}
|
|
44
|
+
if (Object.keys(renamed).length)
|
|
45
|
+
out.set(namespace, renamed);
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The renamed fields for `key` (host, key and version), from the cache or by
|
|
51
|
+
* `readNames`. A failed read is not kept, and gives none: a preview that
|
|
52
|
+
* cannot read the names still marks its entries, tagging each field by the
|
|
53
|
+
* name it was selected with, and never fails the page.
|
|
54
|
+
*/
|
|
55
|
+
function renamedFor(key, readNames, now) {
|
|
56
|
+
const cached = renamedCache.get(key);
|
|
57
|
+
if (cached && cached.until > now)
|
|
58
|
+
return cached.renamed;
|
|
59
|
+
if (renamedCache.size >= RENAMED_LIMIT)
|
|
60
|
+
renamedCache.clear();
|
|
61
|
+
const entry = { until: now + exports.RENAMED_TTL_MS, renamed: Promise.resolve(new Map()) };
|
|
62
|
+
entry.renamed = readNames().then(renamedFields, () => {
|
|
63
|
+
if (renamedCache.get(key) === entry)
|
|
64
|
+
renamedCache.delete(key);
|
|
65
|
+
return new Map();
|
|
66
|
+
});
|
|
67
|
+
renamedCache.set(key, entry);
|
|
68
|
+
return entry.renamed;
|
|
69
|
+
}
|
|
70
|
+
/** Whether a result of `document` can hold an entry: an entry selected `model`, so the document says `model`. */
|
|
71
|
+
function canSelectModel(document) {
|
|
72
|
+
return /\bmodel\b/.test(document);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Run `read` in edit mode: the key's renamed fields are read beside it (or
|
|
76
|
+
* come from the cache), and its result's entries are marked once both are in.
|
|
77
|
+
*/
|
|
78
|
+
async function readMarked(document, key, readNames, read) {
|
|
79
|
+
const result = read();
|
|
80
|
+
if (!canSelectModel(document))
|
|
81
|
+
return result;
|
|
82
|
+
const renamed = renamedFor(key, readNames, Date.now());
|
|
83
|
+
const { data } = await result;
|
|
84
|
+
if ((0, attrs_1.hasGraphQLEntry)(data)) {
|
|
85
|
+
const byModel = await renamed;
|
|
86
|
+
(0, attrs_1.markGraphQLEntries)(data, (model) => byModel.get(model));
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
/** Forget every key's renamed fields. For tests. */
|
|
91
|
+
function __resetEditSchemaForTests() {
|
|
92
|
+
renamedCache.clear();
|
|
93
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* filter-values.ts — filter values as the scalar types the schema declares.
|
|
3
|
+
*
|
|
4
|
+
* GraphQL refuses a variable of the wrong type outright, while REST reads
|
|
5
|
+
* loosely: a query string carries everything as text, and a list of
|
|
6
|
+
* true/false values takes `true` or `"true"` alike (spec 17, amendment 30).
|
|
7
|
+
* GraphQL types a list's filter by its items (`BooleanListFilter`, amendment
|
|
8
|
+
* 59), so `has: "true"` sent as GraphQL is refused. Every filter a builder
|
|
9
|
+
* sends is therefore converted to the operand types its filter input declares,
|
|
10
|
+
* whichever side it was written on: `has: "true"` on a `[Boolean]` field goes
|
|
11
|
+
* as `true`, `gte: "10"` on a number as `10`. A value that cannot be converted
|
|
12
|
+
* is left as written, so the API refuses it with its own message.
|
|
13
|
+
*/
|
|
14
|
+
import type { GraphQLModelSummary, GraphQLSchemaSummary } from "./summary";
|
|
15
|
+
/** The operand types of a system filter (`IDFilter`, `DateTimeFilter`). */
|
|
16
|
+
export declare const SYSTEM_OPERANDS: Readonly<Record<string, string>>;
|
|
17
|
+
/** A JSON object: a filter, or one field's operators. */
|
|
18
|
+
export declare function isObject(value: unknown): value is Record<string, unknown>;
|
|
19
|
+
/** One operand as the GraphQL scalar `type`, when it reads as one. */
|
|
20
|
+
export declare function toScalar(type: string | undefined, value: unknown): unknown;
|
|
21
|
+
/**
|
|
22
|
+
* An operator object with each operand as the type `operands` gives its
|
|
23
|
+
* operator. `listText` also reads a list operand written as comma-separated
|
|
24
|
+
* text (`in: "1,2"`), as a REST query string carries it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function typedOperations(operations: Record<string, unknown>, operands: Readonly<Record<string, string>>, { listText }?: {
|
|
27
|
+
listText?: boolean;
|
|
28
|
+
}): Record<string, unknown>;
|
|
29
|
+
/**
|
|
30
|
+
* A `<Type>Filter` value with every operand converted to the type the schema
|
|
31
|
+
* declares for it: through `and`, `or` and `not`, on system keys and on every
|
|
32
|
+
* field, and one hop into a relation's target. Keys keep the order written.
|
|
33
|
+
*/
|
|
34
|
+
export declare function typedFilter(summary: GraphQLSchemaSummary, model: GraphQLModelSummary, filter: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SYSTEM_OPERANDS = void 0;
|
|
4
|
+
exports.isObject = isObject;
|
|
5
|
+
exports.toScalar = toScalar;
|
|
6
|
+
exports.typedOperations = typedOperations;
|
|
7
|
+
exports.typedFilter = typedFilter;
|
|
8
|
+
/** Operators whose operand is a list. */
|
|
9
|
+
const LIST_OPERATORS = new Set(["in", "nin", "hasAny", "hasAll"]);
|
|
10
|
+
/** The operand types of a system filter (`IDFilter`, `DateTimeFilter`). */
|
|
11
|
+
exports.SYSTEM_OPERANDS = {
|
|
12
|
+
eq: "String", ne: "String", lt: "String", lte: "String", gt: "String", gte: "String",
|
|
13
|
+
in: "String", nin: "String", exists: "Boolean", null: "Boolean",
|
|
14
|
+
};
|
|
15
|
+
/** Filter keys that combine filters rather than name a field. */
|
|
16
|
+
const LOGIC_KEYS = new Set(["and", "or", "not"]);
|
|
17
|
+
/** A JSON object: a filter, or one field's operators. */
|
|
18
|
+
function isObject(value) {
|
|
19
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
/** One operand as the GraphQL scalar `type`, when it reads as one. */
|
|
22
|
+
function toScalar(type, value) {
|
|
23
|
+
switch (type) {
|
|
24
|
+
case "Float":
|
|
25
|
+
case "Int":
|
|
26
|
+
return typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value)) ? Number(value) : value;
|
|
27
|
+
case "Boolean":
|
|
28
|
+
return value === "true" ? true : value === "false" ? false : value;
|
|
29
|
+
case "String":
|
|
30
|
+
case "ID":
|
|
31
|
+
case "DateTime":
|
|
32
|
+
return typeof value === "number" || typeof value === "boolean" ? String(value) : value;
|
|
33
|
+
default:
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** One operand of `operator` as `type`; a list operand item by item. */
|
|
38
|
+
function typedOperand(operator, value, type, listText) {
|
|
39
|
+
if (value === null || value === undefined || !LIST_OPERATORS.has(operator))
|
|
40
|
+
return toScalar(type, value);
|
|
41
|
+
const items = Array.isArray(value) ? value : listText && typeof value === "string" ? value.split(",") : [value];
|
|
42
|
+
return items.map((item) => toScalar(type, item));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* An operator object with each operand as the type `operands` gives its
|
|
46
|
+
* operator. `listText` also reads a list operand written as comma-separated
|
|
47
|
+
* text (`in: "1,2"`), as a REST query string carries it.
|
|
48
|
+
*/
|
|
49
|
+
function typedOperations(operations, operands, { listText = false } = {}) {
|
|
50
|
+
return Object.fromEntries(Object.entries(operations).map(([operator, value]) => [operator, typedOperand(operator, value, operands[operator], listText)]));
|
|
51
|
+
}
|
|
52
|
+
function targetOf(summary, namespace) {
|
|
53
|
+
return namespace === null ? undefined : summary.models.find((m) => m.namespace === namespace);
|
|
54
|
+
}
|
|
55
|
+
/** A relation's own operators typed, and each hop into its target typed by the target field's operands. */
|
|
56
|
+
function typedRelation(operands, target, operations) {
|
|
57
|
+
const out = {};
|
|
58
|
+
for (const [name, value] of Object.entries(operations)) {
|
|
59
|
+
const hop = target?.fields.find((f) => f.name === name);
|
|
60
|
+
out[name] =
|
|
61
|
+
isObject(value) && (hop || name === "id")
|
|
62
|
+
? typedOperations(value, hop ? hop.filterInputs ?? {} : exports.SYSTEM_OPERANDS)
|
|
63
|
+
: typedOperand(name, value, operands[name], false);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A `<Type>Filter` value with every operand converted to the type the schema
|
|
69
|
+
* declares for it: through `and`, `or` and `not`, on system keys and on every
|
|
70
|
+
* field, and one hop into a relation's target. Keys keep the order written.
|
|
71
|
+
*/
|
|
72
|
+
function typedFilter(summary, model, filter) {
|
|
73
|
+
const out = {};
|
|
74
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
75
|
+
if (LOGIC_KEYS.has(key)) {
|
|
76
|
+
const typed = (part) => (isObject(part) ? typedFilter(summary, model, part) : part);
|
|
77
|
+
out[key] = Array.isArray(value) ? value.map(typed) : typed(value);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (!isObject(value)) {
|
|
81
|
+
out[key] = value;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const field = model.fields.find((f) => f.name === key);
|
|
85
|
+
if (!field) {
|
|
86
|
+
out[key] = typedOperations(value, model.systemFilters.find((system) => system.name === key)?.filterInputs ?? exports.SYSTEM_OPERANDS);
|
|
87
|
+
}
|
|
88
|
+
else if (field.kind === "relation" || field.kind === "relationList") {
|
|
89
|
+
out[key] = typedRelation(field.filterInputs ?? {}, targetOf(summary, field.target), value);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
out[key] = typedOperations(value, field.filterInputs ?? {});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* introspection.ts — the introspection query this package sends, and the part
|
|
3
|
+
* of the answer it reads.
|
|
4
|
+
*
|
|
5
|
+
* One request carries both the standard introspection selection and the
|
|
6
|
+
* `version` root field, so a schema summary always says which platform version
|
|
7
|
+
* it describes. Kept byte-stable: `@capacms/mcp` sends the same text, and a
|
|
8
|
+
* persisted copy of it is one hash for every client.
|
|
9
|
+
*
|
|
10
|
+
* Deprecated arguments and input fields are asked for too, with their
|
|
11
|
+
* reasons: graphql-js leaves them out otherwise, and a document using one
|
|
12
|
+
* would then fail codegen as unknown the day Capa deprecates it, long before
|
|
13
|
+
* a later Capa-Version removes it.
|
|
14
|
+
*/
|
|
15
|
+
export declare const INTROSPECTION_QUERY = "query CapaIntrospection {\n version\n __schema {\n queryType { name }\n types { ...FullType }\n }\n}\n\nfragment FullType on __Type {\n kind\n name\n description\n fields(includeDeprecated: true) {\n name\n description\n args(includeDeprecated: true) { ...InputValue }\n type { ...TypeRef }\n isDeprecated\n deprecationReason\n }\n inputFields(includeDeprecated: true) { ...InputValue }\n interfaces { ...TypeRef }\n enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason }\n possibleTypes { ...TypeRef }\n}\n\nfragment InputValue on __InputValue {\n name\n description\n type { ...TypeRef }\n defaultValue\n isDeprecated\n deprecationReason\n}\n\nfragment TypeRef on __Type {\n kind\n name\n ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } }\n}";
|
|
16
|
+
/**
|
|
17
|
+
* The part of the schema edit mode reads (`edit-mode.ts`): each type's name
|
|
18
|
+
* and description, and each field's name and description, which is all that
|
|
19
|
+
* says which GraphQL name is which Capa namespace (N9), and none of the
|
|
20
|
+
* arguments, input types and enum values that make up most of
|
|
21
|
+
* `INTROSPECTION_QUERY`'s answer. It selects the schema and nothing else, so
|
|
22
|
+
* the API answers a repeat from its memo of that answer.
|
|
23
|
+
*/
|
|
24
|
+
export declare const FIELD_NAMES_QUERY = "query CapaFieldNames {\n __schema {\n types {\n kind\n name\n description\n fields(includeDeprecated: true) { name description }\n }\n }\n}";
|
|
25
|
+
/** `data` of a `CapaFieldNames` response. */
|
|
26
|
+
export interface CapaFieldNames {
|
|
27
|
+
__schema: {
|
|
28
|
+
types: Array<{
|
|
29
|
+
kind: string;
|
|
30
|
+
name: string;
|
|
31
|
+
description?: string | null;
|
|
32
|
+
fields?: Array<{
|
|
33
|
+
name: string;
|
|
34
|
+
description?: string | null;
|
|
35
|
+
}> | null;
|
|
36
|
+
}>;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export interface IntrospectionTypeRef {
|
|
40
|
+
kind: string;
|
|
41
|
+
name: string | null;
|
|
42
|
+
ofType?: IntrospectionTypeRef | null;
|
|
43
|
+
}
|
|
44
|
+
export interface IntrospectionInputValue {
|
|
45
|
+
name: string;
|
|
46
|
+
description?: string | null;
|
|
47
|
+
type: IntrospectionTypeRef;
|
|
48
|
+
defaultValue?: string | null;
|
|
49
|
+
isDeprecated?: boolean;
|
|
50
|
+
deprecationReason?: string | null;
|
|
51
|
+
}
|
|
52
|
+
export interface IntrospectionField {
|
|
53
|
+
name: string;
|
|
54
|
+
description?: string | null;
|
|
55
|
+
args: IntrospectionInputValue[];
|
|
56
|
+
type: IntrospectionTypeRef;
|
|
57
|
+
isDeprecated?: boolean;
|
|
58
|
+
deprecationReason?: string | null;
|
|
59
|
+
}
|
|
60
|
+
export interface IntrospectionType {
|
|
61
|
+
kind: string;
|
|
62
|
+
name: string;
|
|
63
|
+
description?: string | null;
|
|
64
|
+
fields?: IntrospectionField[] | null;
|
|
65
|
+
inputFields?: IntrospectionInputValue[] | null;
|
|
66
|
+
enumValues?: Array<{
|
|
67
|
+
name: string;
|
|
68
|
+
description?: string | null;
|
|
69
|
+
isDeprecated?: boolean;
|
|
70
|
+
deprecationReason?: string | null;
|
|
71
|
+
}> | null;
|
|
72
|
+
interfaces?: IntrospectionTypeRef[] | null;
|
|
73
|
+
possibleTypes?: IntrospectionTypeRef[] | null;
|
|
74
|
+
}
|
|
75
|
+
/** `data` of a `CapaIntrospection` response. */
|
|
76
|
+
export interface CapaIntrospection {
|
|
77
|
+
version?: string;
|
|
78
|
+
__schema: {
|
|
79
|
+
queryType: {
|
|
80
|
+
name: string;
|
|
81
|
+
};
|
|
82
|
+
types: IntrospectionType[];
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** The named type under any `NON_NULL` and `LIST` wrappers. */
|
|
86
|
+
export declare function namedType(ref: IntrospectionTypeRef): string;
|
|
87
|
+
/** The type as SDL writes it: `[Authors!]!`. */
|
|
88
|
+
export declare function printTypeRef(ref: IntrospectionTypeRef): string;
|
|
89
|
+
export declare function isListType(ref: IntrospectionTypeRef): boolean;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* introspection.ts — the introspection query this package sends, and the part
|
|
4
|
+
* of the answer it reads.
|
|
5
|
+
*
|
|
6
|
+
* One request carries both the standard introspection selection and the
|
|
7
|
+
* `version` root field, so a schema summary always says which platform version
|
|
8
|
+
* it describes. Kept byte-stable: `@capacms/mcp` sends the same text, and a
|
|
9
|
+
* persisted copy of it is one hash for every client.
|
|
10
|
+
*
|
|
11
|
+
* Deprecated arguments and input fields are asked for too, with their
|
|
12
|
+
* reasons: graphql-js leaves them out otherwise, and a document using one
|
|
13
|
+
* would then fail codegen as unknown the day Capa deprecates it, long before
|
|
14
|
+
* a later Capa-Version removes it.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.FIELD_NAMES_QUERY = exports.INTROSPECTION_QUERY = void 0;
|
|
18
|
+
exports.namedType = namedType;
|
|
19
|
+
exports.printTypeRef = printTypeRef;
|
|
20
|
+
exports.isListType = isListType;
|
|
21
|
+
exports.INTROSPECTION_QUERY = `query CapaIntrospection {
|
|
22
|
+
version
|
|
23
|
+
__schema {
|
|
24
|
+
queryType { name }
|
|
25
|
+
types { ...FullType }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fragment FullType on __Type {
|
|
30
|
+
kind
|
|
31
|
+
name
|
|
32
|
+
description
|
|
33
|
+
fields(includeDeprecated: true) {
|
|
34
|
+
name
|
|
35
|
+
description
|
|
36
|
+
args(includeDeprecated: true) { ...InputValue }
|
|
37
|
+
type { ...TypeRef }
|
|
38
|
+
isDeprecated
|
|
39
|
+
deprecationReason
|
|
40
|
+
}
|
|
41
|
+
inputFields(includeDeprecated: true) { ...InputValue }
|
|
42
|
+
interfaces { ...TypeRef }
|
|
43
|
+
enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason }
|
|
44
|
+
possibleTypes { ...TypeRef }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fragment InputValue on __InputValue {
|
|
48
|
+
name
|
|
49
|
+
description
|
|
50
|
+
type { ...TypeRef }
|
|
51
|
+
defaultValue
|
|
52
|
+
isDeprecated
|
|
53
|
+
deprecationReason
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fragment TypeRef on __Type {
|
|
57
|
+
kind
|
|
58
|
+
name
|
|
59
|
+
ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } }
|
|
60
|
+
}`;
|
|
61
|
+
/**
|
|
62
|
+
* The part of the schema edit mode reads (`edit-mode.ts`): each type's name
|
|
63
|
+
* and description, and each field's name and description, which is all that
|
|
64
|
+
* says which GraphQL name is which Capa namespace (N9), and none of the
|
|
65
|
+
* arguments, input types and enum values that make up most of
|
|
66
|
+
* `INTROSPECTION_QUERY`'s answer. It selects the schema and nothing else, so
|
|
67
|
+
* the API answers a repeat from its memo of that answer.
|
|
68
|
+
*/
|
|
69
|
+
exports.FIELD_NAMES_QUERY = `query CapaFieldNames {
|
|
70
|
+
__schema {
|
|
71
|
+
types {
|
|
72
|
+
kind
|
|
73
|
+
name
|
|
74
|
+
description
|
|
75
|
+
fields(includeDeprecated: true) { name description }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}`;
|
|
79
|
+
/** The named type under any `NON_NULL` and `LIST` wrappers. */
|
|
80
|
+
function namedType(ref) {
|
|
81
|
+
let current = ref;
|
|
82
|
+
while (current && current.name === null)
|
|
83
|
+
current = current.ofType;
|
|
84
|
+
return current?.name ?? "";
|
|
85
|
+
}
|
|
86
|
+
/** The type as SDL writes it: `[Authors!]!`. */
|
|
87
|
+
function printTypeRef(ref) {
|
|
88
|
+
if (ref.kind === "NON_NULL" && ref.ofType)
|
|
89
|
+
return `${printTypeRef(ref.ofType)}!`;
|
|
90
|
+
if (ref.kind === "LIST" && ref.ofType)
|
|
91
|
+
return `[${printTypeRef(ref.ofType)}]`;
|
|
92
|
+
return ref.name ?? "";
|
|
93
|
+
}
|
|
94
|
+
function isListType(ref) {
|
|
95
|
+
let current = ref;
|
|
96
|
+
while (current) {
|
|
97
|
+
if (current.kind === "LIST")
|
|
98
|
+
return true;
|
|
99
|
+
current = current.ofType;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { type GraphQLFieldSummary, type GraphQLModelSummary, type GraphQLSchemaSummary } from "./summary";
|
|
2
|
+
/** One selected field: a name, or a relation or media field with its own fields. */
|
|
3
|
+
/**
|
|
4
|
+
* A relation list's sort: one value of the target's sort enum, e.g.
|
|
5
|
+
* `name_ASC`, or a list of that one value, the root's shape.
|
|
6
|
+
*/
|
|
7
|
+
export type GraphQLFieldSpecSort = string | readonly [string];
|
|
8
|
+
export type GraphQLFieldSpec = string | {
|
|
9
|
+
field: string;
|
|
10
|
+
fields?: GraphQLFieldSpec[];
|
|
11
|
+
/** Array relations only: how many related entries, 1 to 200 (default 100). */
|
|
12
|
+
first?: number;
|
|
13
|
+
/** Array relations only: see `GraphQLFieldSpecSort`. */
|
|
14
|
+
sort?: GraphQLFieldSpecSort;
|
|
15
|
+
/**
|
|
16
|
+
* Array relations in a read of one entry only: the list's `endCursor`,
|
|
17
|
+
* to read on after it. A cursor names one entry's list, so a list read
|
|
18
|
+
* refuses it, as the API does.
|
|
19
|
+
*/
|
|
20
|
+
after?: string;
|
|
21
|
+
};
|
|
22
|
+
export interface GraphQLQuerySpec {
|
|
23
|
+
/** Namespace (`articles`) or GraphQL type name (`Articles`). */
|
|
24
|
+
model: string;
|
|
25
|
+
/** `list`, or `single`, which needs `id`. The default is `single` when `id` is given, else `list`. */
|
|
26
|
+
mode?: "list" | "single";
|
|
27
|
+
/** The entry's UUID: reads that one entry. Refused beside `mode: "list"`. */
|
|
28
|
+
id?: string;
|
|
29
|
+
/** Omit for every non-relation field, single relations as `{ id }`, media as `{ id url alt }`. */
|
|
30
|
+
fields?: GraphQLFieldSpec[];
|
|
31
|
+
/**
|
|
32
|
+
* How many entries, 1 to 200 (default 25), as REST's `limit`. Beside
|
|
33
|
+
* `before` they are the entries just before that cursor, which GraphQL
|
|
34
|
+
* writes as `last`, so the query sends `last`.
|
|
35
|
+
*/
|
|
36
|
+
first?: number;
|
|
37
|
+
/** An `endCursor`: the page after it. */
|
|
38
|
+
after?: string;
|
|
39
|
+
/**
|
|
40
|
+
* A `startCursor` to page back from: the `first` entries just before it.
|
|
41
|
+
* Or `"end"`, REST's `before=end`: the last `first` entries of the list,
|
|
42
|
+
* which GraphQL reads as `last` with no `before` (spec 17, amendment 132).
|
|
43
|
+
*/
|
|
44
|
+
before?: string;
|
|
45
|
+
/** Up to 3 values of the model's sort enum, e.g. `["views_DESC"]`, or one alone, `"views_DESC"`. */
|
|
46
|
+
sort?: string | readonly string[];
|
|
47
|
+
/**
|
|
48
|
+
* The model's filter input, e.g. `{ views: { gte: 10 } }`. Each value is sent
|
|
49
|
+
* as the type the input declares: `{ views: { gte: "10" } }` goes as `10`,
|
|
50
|
+
* and `{ a_bool: { has: "true" } }` on a list of true/false values as `true`,
|
|
51
|
+
* which its `BooleanListFilter` takes.
|
|
52
|
+
*/
|
|
53
|
+
filter?: Record<string, unknown>;
|
|
54
|
+
totalCount?: boolean;
|
|
55
|
+
operationName?: string;
|
|
56
|
+
}
|
|
57
|
+
/** REST's `before=end`: the last entries of the list, read back from its end (spec 17, amendment 132). */
|
|
58
|
+
export declare const END_OF_LIST = "end";
|
|
59
|
+
/** REST's and GraphQL's root page size when none is given. */
|
|
60
|
+
export declare const LIST_DEFAULT = 25;
|
|
61
|
+
export declare class CapaBuildError extends Error {
|
|
62
|
+
/** The closest real names, nearest first. */
|
|
63
|
+
readonly didYouMean: string[];
|
|
64
|
+
/** Every name that would have been accepted. */
|
|
65
|
+
readonly available: string[];
|
|
66
|
+
constructor(message: string, didYouMean?: string[], available?: string[]);
|
|
67
|
+
}
|
|
68
|
+
/** The system fields every model type has (N6), in schema order. */
|
|
69
|
+
export declare const SYSTEM_FIELDS: readonly ["id", "model", "status", "createdAt", "updatedAt", "publishedAt", "_version", "_tags", "_folder"];
|
|
70
|
+
export declare const MEDIA_FIELDS: readonly ["id", "url", "alt", "type", "width", "height"];
|
|
71
|
+
/**
|
|
72
|
+
* Relations a query may nest below its root entry. The API reads 5 levels of
|
|
73
|
+
* entries per root field and counts the root's own as the first, so 4
|
|
74
|
+
* relations below it (spec 17, amendment 78).
|
|
75
|
+
*/
|
|
76
|
+
export declare const MAX_RELATION_DEPTH = 4;
|
|
77
|
+
export interface PlannedField {
|
|
78
|
+
/** GraphQL name. */
|
|
79
|
+
name: string;
|
|
80
|
+
/** The customer field, or null for a system field or a media subfield. */
|
|
81
|
+
field: GraphQLFieldSummary | null;
|
|
82
|
+
/** Relation, relation list and media fields carry what they select. */
|
|
83
|
+
children?: PlannedField[];
|
|
84
|
+
/** The target model of an expanded relation. */
|
|
85
|
+
target?: GraphQLModelSummary;
|
|
86
|
+
first?: number;
|
|
87
|
+
sort?: string;
|
|
88
|
+
after?: string;
|
|
89
|
+
}
|
|
90
|
+
export interface PlannedQuery {
|
|
91
|
+
model: GraphQLModelSummary;
|
|
92
|
+
mode: "list" | "single";
|
|
93
|
+
id?: string;
|
|
94
|
+
fields: PlannedField[];
|
|
95
|
+
first?: number;
|
|
96
|
+
after?: string;
|
|
97
|
+
before?: string;
|
|
98
|
+
sort?: string[];
|
|
99
|
+
filter?: Record<string, unknown>;
|
|
100
|
+
totalCount: boolean;
|
|
101
|
+
operationName: string;
|
|
102
|
+
summary: GraphQLSchemaSummary;
|
|
103
|
+
}
|
|
104
|
+
/** Names within two edits of `wanted`, nearest first then by name, at most three. */
|
|
105
|
+
export declare function didYouMean(wanted: string, candidates: readonly string[]): string[];
|
|
106
|
+
/**
|
|
107
|
+
* A model by namespace or type name, then by any of those or its display name
|
|
108
|
+
* ignoring case (`Writers`, `scalar specimens`). An unknown name is refused
|
|
109
|
+
* with the nearest models, each by namespace, whichever of its names was near.
|
|
110
|
+
*/
|
|
111
|
+
export declare function findModel(summary: GraphQLSchemaSummary, wanted: string): GraphQLModelSummary;
|
|
112
|
+
/** Whether `key` under `field`'s filter is a hop into a field of the related model rather than an operator. */
|
|
113
|
+
export declare function isHop(field: GraphQLFieldSummary, key: string): boolean;
|
|
114
|
+
/** Resolve and check a spec. Throws `CapaBuildError`. */
|
|
115
|
+
export declare function planQuery(summary: GraphQLSchemaSummary, spec: GraphQLQuerySpec): PlannedQuery;
|