@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.
- package/CHANGELOG.md +323 -0
- package/README.md +1043 -186
- package/bin/capa-codegen.js +192 -5
- package/bin/capa.js +208 -0
- package/bin/graphql-project.js +142 -0
- package/bin/project-env.js +58 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +17 -0
- package/dist/codegen.d.ts +55 -0
- package/dist/codegen.js +320 -39
- package/dist/config.d.ts +5 -1
- package/dist/graphql-codegen.d.ts +117 -0
- package/dist/graphql-codegen.js +705 -0
- package/dist/http.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/next/attrs.d.ts +51 -12
- package/dist/next/attrs.js +74 -20
- package/dist/next/client.d.ts +111 -38
- package/dist/next/client.js +116 -82
- package/dist/next/entry-fields.d.ts +162 -0
- package/dist/next/entry-fields.js +2 -0
- package/dist/next/errors.d.ts +136 -0
- package/dist/next/errors.js +214 -0
- package/dist/next/field-names.d.ts +37 -0
- package/dist/next/field-names.js +145 -0
- package/dist/next/graphql/build.d.ts +27 -0
- package/dist/next/graphql/build.js +98 -0
- package/dist/next/graphql/documents.d.ts +67 -0
- package/dist/next/graphql/documents.js +35 -0
- package/dist/next/graphql/edit-mode.d.ts +16 -0
- package/dist/next/graphql/edit-mode.js +93 -0
- package/dist/next/graphql/filter-values.d.ts +34 -0
- package/dist/next/graphql/filter-values.js +96 -0
- package/dist/next/graphql/introspection.d.ts +89 -0
- package/dist/next/graphql/introspection.js +102 -0
- package/dist/next/graphql/plan.d.ts +115 -0
- package/dist/next/graphql/plan.js +531 -0
- package/dist/next/graphql/request.d.ts +228 -0
- package/dist/next/graphql/request.js +283 -0
- package/dist/next/graphql/rest.d.ts +66 -0
- package/dist/next/graphql/rest.js +502 -0
- package/dist/next/graphql/selection.d.ts +55 -0
- package/dist/next/graphql/selection.js +212 -0
- package/dist/next/graphql/sha256.d.ts +13 -0
- package/dist/next/graphql/sha256.js +86 -0
- package/dist/next/graphql/summary.d.ts +83 -0
- package/dist/next/graphql/summary.js +151 -0
- package/dist/next/graphql/tree-layout.d.ts +36 -0
- package/dist/next/graphql/tree-layout.js +20 -0
- package/dist/next/graphql/tree.d.ts +171 -0
- package/dist/next/graphql/tree.js +249 -0
- package/dist/next/graphql/typed.d.ts +261 -0
- package/dist/next/graphql/typed.js +146 -0
- package/dist/next/index.d.ts +28 -5
- package/dist/next/index.js +25 -1
- package/dist/next/inflate.d.ts +25 -7
- package/dist/next/inflate.js +46 -32
- package/dist/next/key-family.d.ts +34 -0
- package/dist/next/key-family.js +74 -0
- package/dist/next/select-types.d.ts +44 -8
- package/dist/next/system-keys.d.ts +27 -0
- package/dist/next/system-keys.js +42 -0
- package/dist/nextjs/index.d.ts +165 -12
- package/dist/nextjs/index.js +247 -23
- package/dist/nextjs/overlay.d.ts +5 -0
- package/dist/nextjs/overlay.js +35 -0
- package/package.json +35 -13
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { Entry } from "../client";
|
|
2
|
+
import type { CapaTreeLayout } from "./tree-layout";
|
|
3
|
+
import type { AliasFor, AliasKeys, Base, FieldResult, Flatten, IsUntyped, Nullable, Returns, RuntimeSelection, TreeLayout, treeLayout, UntypedQuery } from "./typed";
|
|
4
|
+
/** One model root field of an untyped builder result, in REST's shape. */
|
|
5
|
+
export type TreeRoot = Entry[] | Entry | null;
|
|
6
|
+
/** GraphQL's system fields, and each one's key on a REST entry. */
|
|
7
|
+
type SystemField = "id" | "model" | "status" | "createdAt" | "updatedAt" | "publishedAt" | "_version" | "_folder" | "_tags";
|
|
8
|
+
type RestSystemKey<K> = K extends "_version" ? "version" : K extends "_folder" ? "folder" : K extends "_tags" ? "tags" : K;
|
|
9
|
+
/** The layout codegen wrote on model type `T`; `never` for any other object (media, a connection). */
|
|
10
|
+
type LayoutOf<T> = typeof treeLayout extends keyof T ? Extract<T[typeof treeLayout], TreeLayout> : never;
|
|
11
|
+
type IsModel<T> = [LayoutOf<T>] extends [never] ? false : true;
|
|
12
|
+
type FieldsOf<T> = LayoutOf<T> extends {
|
|
13
|
+
fields: infer F;
|
|
14
|
+
} ? F : never;
|
|
15
|
+
type NamespaceOf<T, K> = K extends keyof FieldsOf<T> ? Extract<FieldsOf<T>[K], string> : K;
|
|
16
|
+
/** Keys a selection level always selects (`true` or an object), and those selected with a `boolean`. */
|
|
17
|
+
type Always<S> = {
|
|
18
|
+
[K in keyof S]-?: [S[K]] extends [false | undefined] ? never : boolean extends S[K] ? never : K;
|
|
19
|
+
}[keyof S];
|
|
20
|
+
type Maybe<S> = {
|
|
21
|
+
[K in keyof S]-?: boolean extends S[K] ? K : never;
|
|
22
|
+
}[keyof S];
|
|
23
|
+
/** The entries of a connection selection: `nodes`, or `edges { node }`. */
|
|
24
|
+
type EntrySelection<S> = S extends {
|
|
25
|
+
nodes: infer N;
|
|
26
|
+
} ? N : S extends {
|
|
27
|
+
edges: {
|
|
28
|
+
node: infer N;
|
|
29
|
+
};
|
|
30
|
+
} ? N : never;
|
|
31
|
+
/** A relation GraphQL types as an id, as REST writes it unexpanded. */
|
|
32
|
+
type RelationId = {
|
|
33
|
+
id: string;
|
|
34
|
+
model: string | null;
|
|
35
|
+
};
|
|
36
|
+
type IdTree<R> = Nullable<R> extends true ? IdTree<NonNullable<R>> | null : R extends ReadonlyArray<unknown> ? {
|
|
37
|
+
items: RelationId[];
|
|
38
|
+
pageInfo: {
|
|
39
|
+
hasNext: boolean;
|
|
40
|
+
next: string | null;
|
|
41
|
+
};
|
|
42
|
+
} : RelationId;
|
|
43
|
+
/** A relation list as REST writes it expanded; `pageInfo` only when the selection asks for it. */
|
|
44
|
+
type RelationListTree<E, S> = Flatten<{
|
|
45
|
+
items: Array<TreeEntry<E, EntrySelection<S>>>;
|
|
46
|
+
} & (S extends {
|
|
47
|
+
pageInfo: {
|
|
48
|
+
hasNextPage: true;
|
|
49
|
+
};
|
|
50
|
+
} ? {
|
|
51
|
+
pageInfo: {
|
|
52
|
+
limit: number;
|
|
53
|
+
hasNext: boolean;
|
|
54
|
+
next: string | null;
|
|
55
|
+
};
|
|
56
|
+
} : {})>;
|
|
57
|
+
/** One customer field's value in REST's shape, for its GraphQL type `R` and its selection `S`. */
|
|
58
|
+
type FieldTree<R, S> = unknown extends R ? R : Nullable<R> extends true ? FieldTree<NonNullable<R>, S> | null : R extends ReadonlyArray<infer E> ? Array<FieldTree<E, S>> : IsModel<R> extends true ? TreeEntry<R, S> : R extends {
|
|
59
|
+
nodes: ReadonlyArray<infer E>;
|
|
60
|
+
} ? RelationListTree<E, S> : FieldResult<R, S>;
|
|
61
|
+
type CustomerField<T> = Extract<keyof FieldsOf<T>, keyof T>;
|
|
62
|
+
type ValueOf<T, K extends keyof T, S> = K extends LayoutOf<T>["ids"] ? IdTree<Returns<T[K]>> : FieldTree<Returns<T[K]>, S>;
|
|
63
|
+
/**
|
|
64
|
+
* One entry of model type `T` for selection `S`, as REST returns it: `model`
|
|
65
|
+
* always, the system fields selected beside `fields` (`_version` as
|
|
66
|
+
* `version`), every customer field selected under `fields` by its namespace.
|
|
67
|
+
*/
|
|
68
|
+
export type TreeEntry<T, S> = Flatten<{
|
|
69
|
+
model: string;
|
|
70
|
+
} & {
|
|
71
|
+
[K in Always<S> & Exclude<SystemField, "model"> & keyof T as RestSystemKey<K>]: FieldResult<Returns<T[K]>, true>;
|
|
72
|
+
} & {
|
|
73
|
+
[K in Maybe<S> & Exclude<SystemField, "model"> & keyof T as RestSystemKey<K>]?: FieldResult<Returns<T[K]>, true>;
|
|
74
|
+
} & {
|
|
75
|
+
fields: Flatten<{
|
|
76
|
+
[K in Always<S> & CustomerField<T> as NamespaceOf<T, K>]: ValueOf<T, K, S[K & keyof S]>;
|
|
77
|
+
} & {
|
|
78
|
+
[K in Maybe<S> & CustomerField<T> as NamespaceOf<T, K>]?: ValueOf<T, K, true>;
|
|
79
|
+
}>;
|
|
80
|
+
}>;
|
|
81
|
+
/** A root field in REST's shape: a list root as its entries, a single root as its entry, `null` where the root failed. */
|
|
82
|
+
type RootTree<R, S> = NonNullable<R> extends {
|
|
83
|
+
nodes: ReadonlyArray<infer E>;
|
|
84
|
+
} ? Array<TreeEntry<E, EntrySelection<S>>> | null : TreeEntry<NonNullable<R>, S> | null;
|
|
85
|
+
/**
|
|
86
|
+
* A builder result in REST's shape: each model root field as its REST `data`.
|
|
87
|
+
* With a schema typed by `CapaQuery`, a list root is an array of entries and a
|
|
88
|
+
* single root an entry or `null`, each typed from the selection; untyped, each
|
|
89
|
+
* root is a `TreeRoot`.
|
|
90
|
+
*/
|
|
91
|
+
export type TreeData<S, Q = UntypedQuery> = S extends RuntimeSelection ? {
|
|
92
|
+
[rootField: string]: TreeRoot;
|
|
93
|
+
} : IsUntyped<Q> extends true ? {
|
|
94
|
+
[K in Exclude<keyof S, "args">]: TreeRoot;
|
|
95
|
+
} : Flatten<{
|
|
96
|
+
[K in Always<S> & keyof Q]: RootTree<Returns<Q[K]>, S[K]>;
|
|
97
|
+
} & {
|
|
98
|
+
[K in AliasKeys<S, Q>]: RootTree<Returns<Q[Extract<AliasFor<S[K]>, keyof Q>]>, S[K]>;
|
|
99
|
+
}>;
|
|
100
|
+
/**
|
|
101
|
+
* What `toTree` needs selected to return what REST returns, on top of the
|
|
102
|
+
* selection itself: `id` and `status` on every entry, which only GraphQL knows.
|
|
103
|
+
* `model` comes from the layout, and a relation list's `pageInfo` is
|
|
104
|
+
* left out of the tree when it is not selected. A root may be an alias; an
|
|
105
|
+
* alias below a root may not, since REST reads each field once.
|
|
106
|
+
*/
|
|
107
|
+
export type TreeSelection<S, Q> = S extends RuntimeSelection ? unknown : IsUntyped<Q> extends true ? unknown : {
|
|
108
|
+
[K in keyof S & keyof Q]: RootNeeds<Returns<Q[K]>, S[K]>;
|
|
109
|
+
} & {
|
|
110
|
+
[K in AliasKeys<S, Q>]: RootNeeds<Returns<Q[Extract<AliasFor<S[K]>, keyof Q>]>, S[K]>;
|
|
111
|
+
};
|
|
112
|
+
type RootNeeds<R, S> = NonNullable<R> extends {
|
|
113
|
+
nodes: ReadonlyArray<infer E>;
|
|
114
|
+
} ? ConnectionNeeds<E, S> : EntryNeeds<NonNullable<R>, S>;
|
|
115
|
+
type ConnectionNeeds<E, S> = S extends {
|
|
116
|
+
edges: {
|
|
117
|
+
node: infer N;
|
|
118
|
+
};
|
|
119
|
+
} ? S extends {
|
|
120
|
+
nodes: unknown;
|
|
121
|
+
} ? {
|
|
122
|
+
nodes: EntryNeeds<E, EntrySelection<S>>;
|
|
123
|
+
} : {
|
|
124
|
+
edges: {
|
|
125
|
+
node: EntryNeeds<E, N>;
|
|
126
|
+
};
|
|
127
|
+
} : {
|
|
128
|
+
nodes: EntryNeeds<E, EntrySelection<S>>;
|
|
129
|
+
};
|
|
130
|
+
type EntryNeeds<T, S> = {
|
|
131
|
+
id: true;
|
|
132
|
+
status: true;
|
|
133
|
+
} & {
|
|
134
|
+
[K in keyof S & CustomerField<T>]: FieldNeeds<Base<Returns<T[K]>>, S[K]>;
|
|
135
|
+
} & {
|
|
136
|
+
[K in AliasKeys<S, T>]: never;
|
|
137
|
+
};
|
|
138
|
+
type FieldNeeds<B, S> = IsModel<B> extends true ? EntryNeeds<B, S> : B extends {
|
|
139
|
+
nodes: ReadonlyArray<infer E>;
|
|
140
|
+
} ? ConnectionNeeds<E, S> : unknown;
|
|
141
|
+
/**
|
|
142
|
+
* A `client.graphql.query()` result in the shape `/api/entries` answers with:
|
|
143
|
+
* each model root field as its REST `data`, a list root as its entries, a
|
|
144
|
+
* single root as its entry, and `null` for a root that failed (its error is in
|
|
145
|
+
* the GraphQL result's `errors`).
|
|
146
|
+
*
|
|
147
|
+
* import { capaTreeLayout } from "./capa-graphql"; // written by capa-codegen --graphql
|
|
148
|
+
* const selection = { articles: { args: { first: 5 }, nodes: { id: true, status: true, title: true } } } as const;
|
|
149
|
+
* const { data } = await capa.graphql.query(selection);
|
|
150
|
+
* const { articles } = toTree(data, selection, capaTreeLayout);
|
|
151
|
+
* articles?.map((entry) => entry.fields.title); // string | null, as REST's entries.list
|
|
152
|
+
* // deep-equal to (await capa.entries.list("articles", { select: "title", limit: 5 })).data
|
|
153
|
+
*
|
|
154
|
+
* Without codegen, pass the key's schema instead: `await capa.graphqlSchema()`,
|
|
155
|
+
* one introspection request. Either carries the types (`capaTreeLayout`
|
|
156
|
+
* always, a schema when the client is typed by `CapaQuery`): the result is
|
|
157
|
+
* typed from the selection, and the selection must name `id` and `status` on
|
|
158
|
+
* every entry, which REST always returns. Each entry's `model` comes from the
|
|
159
|
+
* layout. A selection from `selectToSelection` names all of that itself, and
|
|
160
|
+
* its tree is untyped, as its fields are known only at run time.
|
|
161
|
+
*
|
|
162
|
+
* A root field may be an alias (`latest: { __aliasFor: "articles", ... }`),
|
|
163
|
+
* and becomes the REST data of the field it names, under its own key. An
|
|
164
|
+
* alias below a root field throws a `TypeError`: REST reads each field once.
|
|
165
|
+
*
|
|
166
|
+
* Only model root fields convert; `version`, `me` and `entry` throw a
|
|
167
|
+
* `TypeError`, since REST has no entry list for them. Page through a list
|
|
168
|
+
* with the GraphQL result's own `pageInfo`.
|
|
169
|
+
*/
|
|
170
|
+
export declare function toTree<S extends object, Q = UntypedQuery>(data: unknown, selection: S & TreeSelection<S, Q>, schema: CapaTreeLayout<Q>): TreeData<S, Q>;
|
|
171
|
+
export {};
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toTree = toTree;
|
|
4
|
+
/**
|
|
5
|
+
* tree.ts — a typed builder result in the shape `/api/entries` answers with.
|
|
6
|
+
*
|
|
7
|
+
* `client.graphql.query()` returns GraphQL's shape, because that is what its
|
|
8
|
+
* types describe: `articles.nodes[0].author.name`, exactly the selection. Code
|
|
9
|
+
* written against REST reads entries instead: `data[0].fields.author.fields.name`,
|
|
10
|
+
* system keys beside `fields`, an array relation as `{ items, pageInfo }`.
|
|
11
|
+
* `toTree` moves a builder result into that shape, so the two surfaces stay
|
|
12
|
+
* interchangeable (G brief, npm item 4).
|
|
13
|
+
*
|
|
14
|
+
* It follows the SELECTION, not the data: the selection says which object is
|
|
15
|
+
* an entry, which is a connection and what `first` a relation list asked for,
|
|
16
|
+
* and the schema's layout says which GraphQL name is which namespace (a field
|
|
17
|
+
* N5 renamed, such as `hero_image` for `hero-image`, goes back under its
|
|
18
|
+
* namespace) and which relations GraphQL types as ids, because the key cannot
|
|
19
|
+
* read their model: those are REST's unexpanded relation, `{ id, model }`, a
|
|
20
|
+
* list of them `{ items, pageInfo }`. The layout is codegen's `capaTreeLayout`,
|
|
21
|
+
* or a schema summary read with `capa.graphqlSchema()` (tree-layout.ts). The
|
|
22
|
+
* input is never modified.
|
|
23
|
+
*
|
|
24
|
+
* An entry read in edit mode keeps its mark, so `capaAttrs(entry, namespace)`
|
|
25
|
+
* tags the tree as it tags a REST entry.
|
|
26
|
+
*
|
|
27
|
+
* The types follow the same two: with `capaTreeLayout`, or a schema read by a
|
|
28
|
+
* client typed with codegen's `CapaQuery`, the result is typed from the
|
|
29
|
+
* selection, keyed by namespace from the `TreeLayout` codegen writes on each
|
|
30
|
+
* model type.
|
|
31
|
+
*
|
|
32
|
+
* Every entry carries its `model`, from the layout, selected or not: the
|
|
33
|
+
* layout knows which model each object of the selection is. A relation
|
|
34
|
+
* list carries `pageInfo` when the selection asks for `pageInfo { hasNextPage
|
|
35
|
+
* endCursor }`, and is `{ items }` alone when it does not, since only GraphQL
|
|
36
|
+
* knows whether there is a next page.
|
|
37
|
+
*
|
|
38
|
+
* The result is deep-equal to the REST `shape=tree` `data` for the same read,
|
|
39
|
+
* given the selection names the rest of what REST always returns (`id` and
|
|
40
|
+
* `status`), `pageInfo` on each relation list, and every media field REST
|
|
41
|
+
* returns (`id url alt type width height`, the one public media shape of a
|
|
42
|
+
* single media field and of each item of a list). Where the two surfaces
|
|
43
|
+
* differ by design, the tree keeps GraphQL's value, because GraphQL does not
|
|
44
|
+
* say what REST says in these places:
|
|
45
|
+
* - a missing single relation (deleted, a draft the key cannot see) is
|
|
46
|
+
* `null` (REST: `{ id, model, missing: true }`);
|
|
47
|
+
* - a missing item of a relation list is left out, so `items` is shorter
|
|
48
|
+
* (REST keeps its slot as `{ id, model, missing: true }`);
|
|
49
|
+
* - a value that does not fit its type is `null` (REST: the raw value).
|
|
50
|
+
*/
|
|
51
|
+
const attrs_1 = require("../attrs");
|
|
52
|
+
const plan_1 = require("./plan");
|
|
53
|
+
const typed_1 = require("./typed");
|
|
54
|
+
/**
|
|
55
|
+
* GraphQL's system fields and their REST keys, in the order REST prints them.
|
|
56
|
+
* An entry REST returns names its system keys plainly even on a model with a
|
|
57
|
+
* field of the same name, since that field sits under `fields`: `$tags` is
|
|
58
|
+
* how a REST request asks for the system key (spec 17, amendment 29), never a
|
|
59
|
+
* key of the answer.
|
|
60
|
+
*/
|
|
61
|
+
const SYSTEM_KEYS = [
|
|
62
|
+
["id", "id"],
|
|
63
|
+
["model", "model"],
|
|
64
|
+
["status", "status"],
|
|
65
|
+
["createdAt", "createdAt"],
|
|
66
|
+
["updatedAt", "updatedAt"],
|
|
67
|
+
["publishedAt", "publishedAt"],
|
|
68
|
+
["_version", "version"],
|
|
69
|
+
["_folder", "folder"],
|
|
70
|
+
["_tags", "tags"],
|
|
71
|
+
];
|
|
72
|
+
const SYSTEM_NAMES = new Set(SYSTEM_KEYS.map(([name]) => name));
|
|
73
|
+
/** A relation list's page size when the selection gives no `first`, as REST's `limit:`. */
|
|
74
|
+
const NESTED_DEFAULT = 100;
|
|
75
|
+
function isObject(value) {
|
|
76
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
77
|
+
}
|
|
78
|
+
/** The entries of a connection value, from `nodes` or `edges[].node`, with the selection that shaped them. */
|
|
79
|
+
function connectionItems(selection, value) {
|
|
80
|
+
if (isObject(selection.nodes) && Array.isArray(value.nodes))
|
|
81
|
+
return { items: value.nodes, entry: selection.nodes };
|
|
82
|
+
const edges = isObject(selection.edges) ? selection.edges : undefined;
|
|
83
|
+
if (edges && isObject(edges.node) && Array.isArray(value.edges)) {
|
|
84
|
+
return { items: value.edges.map((edge) => (isObject(edge) ? edge.node : null)), entry: edges.node };
|
|
85
|
+
}
|
|
86
|
+
throw new TypeError("@capacms/sdk/next: toTree needs nodes, or edges { node }, selected on every list.");
|
|
87
|
+
}
|
|
88
|
+
function modelOf(schema, field) {
|
|
89
|
+
const target = schema.models.find((m) => m.namespace === field.target);
|
|
90
|
+
if (!target)
|
|
91
|
+
throw new TypeError(`@capacms/sdk/next: toTree has no model ${String(field.target)} in the schema.`);
|
|
92
|
+
return target;
|
|
93
|
+
}
|
|
94
|
+
/** A relation list as REST's `{ items, pageInfo }`; `pageInfo` only when the selection asked for it. */
|
|
95
|
+
function relationList(schema, target, selection, value) {
|
|
96
|
+
const { items, entry } = connectionItems(selection, value);
|
|
97
|
+
const out = { items: items.map((item) => (isObject(item) ? entryTree(schema, target, entry, item) : item)) };
|
|
98
|
+
const pageSelection = isObject(selection.pageInfo) ? selection.pageInfo : undefined;
|
|
99
|
+
const page = isObject(value.pageInfo) ? value.pageInfo : undefined;
|
|
100
|
+
if (pageSelection?.hasNextPage === true && page) {
|
|
101
|
+
const args = isObject(selection.args) ? selection.args : {};
|
|
102
|
+
const hasNext = page.hasNextPage === true;
|
|
103
|
+
out.pageInfo = {
|
|
104
|
+
limit: typeof args.first === "number" ? args.first : NESTED_DEFAULT,
|
|
105
|
+
hasNext,
|
|
106
|
+
next: hasNext && typeof page.endCursor === "string" ? page.endCursor : null,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
/** One media value with its keys in REST's order: `{ id, url, alt, type, width, height }`, as far as selected. */
|
|
112
|
+
function mediaTree(value) {
|
|
113
|
+
if (!isObject(value))
|
|
114
|
+
return value;
|
|
115
|
+
const out = {};
|
|
116
|
+
for (const key of plan_1.MEDIA_FIELDS)
|
|
117
|
+
if (key in value)
|
|
118
|
+
out[key] = value[key];
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
/** A relation GraphQL types as an id holds one when it is a non-empty string, as REST reads a stored reference. */
|
|
122
|
+
function isRelationId(value) {
|
|
123
|
+
return typeof value === "string" && value !== "";
|
|
124
|
+
}
|
|
125
|
+
/** One customer field's value in REST's shape. */
|
|
126
|
+
function fieldTree(schema, field, selection, value) {
|
|
127
|
+
if (value === null || value === undefined)
|
|
128
|
+
return value ?? null;
|
|
129
|
+
switch (field.kind) {
|
|
130
|
+
case "relation":
|
|
131
|
+
return isObject(selection) && isObject(value) ? entryTree(schema, modelOf(schema, field), selection, value) : value;
|
|
132
|
+
case "relationList":
|
|
133
|
+
return isObject(selection) && isObject(value) ? relationList(schema, modelOf(schema, field), selection, value) : value;
|
|
134
|
+
case "id":
|
|
135
|
+
return isRelationId(value) ? { id: value, model: field.target } : null;
|
|
136
|
+
case "idList":
|
|
137
|
+
// REST lists the whole stored list here, so there is never a next page.
|
|
138
|
+
return {
|
|
139
|
+
items: Array.isArray(value) ? value.filter(isRelationId).map((id) => ({ id, model: field.target })) : [],
|
|
140
|
+
pageInfo: { hasNext: false, next: null },
|
|
141
|
+
};
|
|
142
|
+
case "media":
|
|
143
|
+
return Array.isArray(value) ? value.map(mediaTree) : mediaTree(value);
|
|
144
|
+
default:
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Throws for an alias anywhere below a root field. REST reads each field once,
|
|
150
|
+
* so a second read of one under another name has no place in its tree. Checked
|
|
151
|
+
* on the selection, so the answer does not depend on which lists came back empty.
|
|
152
|
+
*/
|
|
153
|
+
function refuseAliasesBelow(selection) {
|
|
154
|
+
for (const key of (0, typed_1.selectedKeys)(selection)) {
|
|
155
|
+
const value = selection[key];
|
|
156
|
+
if (!isObject(value))
|
|
157
|
+
continue;
|
|
158
|
+
const aliased = (0, typed_1.aliasedField)(value);
|
|
159
|
+
if (aliased !== undefined) {
|
|
160
|
+
throw new TypeError(`@capacms/sdk/next: toTree: ${key} aliases ${aliased} below the root field, and REST has one value per field: ` +
|
|
161
|
+
`select ${aliased} under its own name, or read ${key} from the GraphQL result.`);
|
|
162
|
+
}
|
|
163
|
+
refuseAliasesBelow(value);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* One entry: system keys at the top in REST's order, `model` from the schema
|
|
168
|
+
* whether selected or not, every customer field under `fields` by namespace.
|
|
169
|
+
*/
|
|
170
|
+
function entryTree(schema, model, selection, node) {
|
|
171
|
+
const keys = (0, typed_1.selectedKeys)(selection);
|
|
172
|
+
const out = {};
|
|
173
|
+
for (const [name, restKey] of SYSTEM_KEYS) {
|
|
174
|
+
if (name === "model")
|
|
175
|
+
out.model = model.namespace;
|
|
176
|
+
else if (keys.includes(name) && name in node)
|
|
177
|
+
out[restKey] = node[name];
|
|
178
|
+
}
|
|
179
|
+
const fields = {};
|
|
180
|
+
for (const key of keys) {
|
|
181
|
+
if (SYSTEM_NAMES.has(key) || !(key in node))
|
|
182
|
+
continue;
|
|
183
|
+
const field = model.fields.find((f) => f.name === key);
|
|
184
|
+
if (!field) {
|
|
185
|
+
fields[key] = node[key];
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
fields[field.namespace] = fieldTree(schema, field, selection[key], node[key]);
|
|
189
|
+
}
|
|
190
|
+
out.fields = fields;
|
|
191
|
+
return (0, attrs_1.carryEditMark)(node, out);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* A `client.graphql.query()` result in the shape `/api/entries` answers with:
|
|
195
|
+
* each model root field as its REST `data`, a list root as its entries, a
|
|
196
|
+
* single root as its entry, and `null` for a root that failed (its error is in
|
|
197
|
+
* the GraphQL result's `errors`).
|
|
198
|
+
*
|
|
199
|
+
* import { capaTreeLayout } from "./capa-graphql"; // written by capa-codegen --graphql
|
|
200
|
+
* const selection = { articles: { args: { first: 5 }, nodes: { id: true, status: true, title: true } } } as const;
|
|
201
|
+
* const { data } = await capa.graphql.query(selection);
|
|
202
|
+
* const { articles } = toTree(data, selection, capaTreeLayout);
|
|
203
|
+
* articles?.map((entry) => entry.fields.title); // string | null, as REST's entries.list
|
|
204
|
+
* // deep-equal to (await capa.entries.list("articles", { select: "title", limit: 5 })).data
|
|
205
|
+
*
|
|
206
|
+
* Without codegen, pass the key's schema instead: `await capa.graphqlSchema()`,
|
|
207
|
+
* one introspection request. Either carries the types (`capaTreeLayout`
|
|
208
|
+
* always, a schema when the client is typed by `CapaQuery`): the result is
|
|
209
|
+
* typed from the selection, and the selection must name `id` and `status` on
|
|
210
|
+
* every entry, which REST always returns. Each entry's `model` comes from the
|
|
211
|
+
* layout. A selection from `selectToSelection` names all of that itself, and
|
|
212
|
+
* its tree is untyped, as its fields are known only at run time.
|
|
213
|
+
*
|
|
214
|
+
* A root field may be an alias (`latest: { __aliasFor: "articles", ... }`),
|
|
215
|
+
* and becomes the REST data of the field it names, under its own key. An
|
|
216
|
+
* alias below a root field throws a `TypeError`: REST reads each field once.
|
|
217
|
+
*
|
|
218
|
+
* Only model root fields convert; `version`, `me` and `entry` throw a
|
|
219
|
+
* `TypeError`, since REST has no entry list for them. Page through a list
|
|
220
|
+
* with the GraphQL result's own `pageInfo`.
|
|
221
|
+
*/
|
|
222
|
+
function toTree(data, selection, schema) {
|
|
223
|
+
if (!isObject(selection))
|
|
224
|
+
throw new TypeError("@capacms/sdk/next: toTree needs the selection the query was built from.");
|
|
225
|
+
const root = isObject(data) ? data : {};
|
|
226
|
+
const out = {};
|
|
227
|
+
for (const key of (0, typed_1.selectedKeys)(selection)) {
|
|
228
|
+
const rootSelection = selection[key];
|
|
229
|
+
const field = (0, typed_1.aliasedField)(rootSelection) ?? key;
|
|
230
|
+
const list = schema.models.find((m) => m.listField === field);
|
|
231
|
+
const single = list ? undefined : schema.models.find((m) => m.singleField === field);
|
|
232
|
+
if ((!list && !single) || !isObject(rootSelection)) {
|
|
233
|
+
throw new TypeError(`@capacms/sdk/next: toTree converts model root fields, and ${key} is not one. Read it from the GraphQL result.`);
|
|
234
|
+
}
|
|
235
|
+
refuseAliasesBelow(rootSelection);
|
|
236
|
+
const value = root[key];
|
|
237
|
+
if (!isObject(value)) {
|
|
238
|
+
out[key] = null;
|
|
239
|
+
}
|
|
240
|
+
else if (list) {
|
|
241
|
+
const { items, entry } = connectionItems(rootSelection, value);
|
|
242
|
+
out[key] = items.map((item) => (isObject(item) ? entryTree(schema, list, entry, item) : item));
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
out[key] = entryTree(schema, single, rootSelection, value);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|