@abinnovision/payloadcms-mcpx 1.1.0-beta.6 → 2.0.0-beta.8
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/README.md +140 -470
- package/dist/{client → admin}/capability-matrix.d.mts +3 -4
- package/dist/admin/capability-matrix.mjs +354 -0
- package/dist/admin/capability-toggles.mjs +73 -0
- package/dist/{client → admin}/setup-guide.d.mts +1 -1
- package/dist/{client → admin}/setup-guide.mjs +3 -10
- package/dist/api-keys/capability-matrix.d.mts +6 -6
- package/dist/api-keys/capability-matrix.mjs +15 -85
- package/dist/api-keys/fields.mjs +15 -24
- package/dist/auth/resolve.mjs +23 -5
- package/dist/{tools/names.mjs → builtin-tool-names.mjs} +1 -1
- package/dist/capabilities.mjs +34 -42
- package/dist/define-tool.d.mts +19 -0
- package/dist/{types.mjs → define-tool.mjs} +1 -1
- package/dist/endpoint/errors.mjs +5 -6
- package/dist/endpoint/handler.mjs +49 -19
- package/dist/endpoint/server.mjs +65 -19
- package/dist/entity.d.mts +9 -0
- package/dist/guards.mjs +5 -0
- package/dist/i18n.mjs +8 -8
- package/dist/index.d.mts +5 -2
- package/dist/index.mjs +3 -2
- package/dist/options.mjs +47 -45
- package/dist/plugin.mjs +0 -1
- package/dist/request.d.mts +2 -3
- package/dist/request.mjs +2 -3
- package/dist/result.d.mts +2 -3
- package/dist/result.mjs +3 -3
- package/dist/schema/admin-hidden.mjs +91 -0
- package/dist/schema/describe.mjs +17 -33
- package/dist/schema/errors.mjs +8 -0
- package/dist/schema/index.mjs +5 -2
- package/dist/schema/lexical-pointer.mjs +18 -20
- package/dist/schema/lexical.mjs +39 -92
- package/dist/schema/outline.mjs +5 -15
- package/dist/schema/path.mjs +22 -0
- package/dist/schema/pointer.mjs +18 -25
- package/dist/schema/shape.mjs +25 -71
- package/dist/schema/walk.mjs +35 -45
- package/dist/tools/builtin.mjs +4 -4
- package/dist/tools/create-document.mjs +26 -23
- package/dist/tools/describe-schema.mjs +38 -34
- package/dist/tools/document.mjs +84 -0
- package/dist/tools/find-documents.mjs +32 -19
- package/dist/tools/find-versions.mjs +22 -18
- package/dist/tools/get-document.mjs +39 -32
- package/dist/tools/list-capabilities.mjs +23 -16
- package/dist/tools/patch-document.mjs +74 -50
- package/dist/tools/publish-document.mjs +17 -28
- package/dist/tools/query-paths.mjs +55 -0
- package/dist/tools/read-request.d.mts +10 -0
- package/dist/tools/read-request.mjs +43 -0
- package/dist/tools/shared.mjs +58 -99
- package/dist/tools/validate-document.mjs +16 -29
- package/dist/tools/versions.mjs +25 -17
- package/dist/types.d.mts +47 -56
- package/dist/write/draft-guard.mjs +17 -37
- package/dist/write/patch.mjs +27 -183
- package/dist/write/publish-blockers.mjs +7 -14
- package/dist/write/publish-intent.mjs +4 -4
- package/dist/write/row-ids.mjs +44 -0
- package/dist/write/transaction.mjs +5 -8
- package/dist/write/write-data.mjs +64 -0
- package/package.json +5 -5
- package/dist/client/capability-matrix.mjs +0 -349
- package/dist/endpoint/index.mjs +0 -4
- package/dist/tools/target.mjs +0 -45
- /package/dist/{client → admin}/index.d.mts +0 -0
- /package/dist/{client → admin}/index.mjs +0 -0
package/dist/schema/lexical.mjs
CHANGED
|
@@ -1,25 +1,15 @@
|
|
|
1
|
+
import { isPlainObject, ownValue } from "../guards.mjs";
|
|
1
2
|
import { flattenAllFields } from "payload";
|
|
2
3
|
//#region src/schema/lexical.ts
|
|
3
|
-
|
|
4
|
-
* `editorConfig.features.nodes` lists only what a feature contributed, so a
|
|
5
|
-
* field whose editor enables nothing but text formatting reports none at all.
|
|
6
|
-
*/ const LEXICAL_CORE_NODES = [
|
|
4
|
+
const LEXICAL_CORE_NODES = [
|
|
7
5
|
"root",
|
|
8
6
|
"paragraph",
|
|
9
7
|
"text",
|
|
10
8
|
"linebreak",
|
|
11
9
|
"tab"
|
|
12
10
|
];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* node, `upload` answers with every enabled collection's upload fields
|
|
16
|
-
* concatenated, so the result describes no single position.
|
|
17
|
-
*/ const OPAQUE_NODE_TYPES = /* @__PURE__ */ new Set(["upload"]);
|
|
18
|
-
/**
|
|
19
|
-
* `null` records a node type that was asked and has nothing to describe, so it
|
|
20
|
-
* is asked only once. Worth caching because describe and validate resolve the
|
|
21
|
-
* same field repeatedly and the block features rebuild their answer every call.
|
|
22
|
-
*/ const subSchemaCache = /* @__PURE__ */ new WeakMap();
|
|
11
|
+
const OPAQUE_NODE_TYPES = /* @__PURE__ */ new Set(["upload"]);
|
|
12
|
+
const subSchemaCache = /* @__PURE__ */ new WeakMap();
|
|
23
13
|
const featuresOf = (field) => field.editor?.editorConfig?.features;
|
|
24
14
|
/** Editors other than Lexical report only the core nodes. */ const allowedNodeTypes = (field) => {
|
|
25
15
|
const registered = (featuresOf(field)?.nodes ?? []).flatMap((entry) => {
|
|
@@ -52,10 +42,7 @@ const lexicalSubSchema = (field, nodeType) => {
|
|
|
52
42
|
return cached.get(nodeType) ?? void 0;
|
|
53
43
|
};
|
|
54
44
|
/** In the order their features registered them. */ const subSchemaNodeTypes = (field) => [...featuresOf(field)?.getSubFields?.keys() ?? []].filter((nodeType) => lexicalSubSchema(field, nodeType) !== void 0);
|
|
55
|
-
|
|
56
|
-
* `direction` carries Payload's own declaration for it, `oneOf` the two
|
|
57
|
-
* directions or null, rather than a looser "string or null".
|
|
58
|
-
*/ const KINDS = {
|
|
45
|
+
const KINDS = {
|
|
59
46
|
array: {
|
|
60
47
|
accepts: (value) => Array.isArray(value),
|
|
61
48
|
needs: "an array"
|
|
@@ -69,11 +56,11 @@ const lexicalSubSchema = (field, nodeType) => {
|
|
|
69
56
|
needs: "a number"
|
|
70
57
|
},
|
|
71
58
|
object: {
|
|
72
|
-
accepts:
|
|
59
|
+
accepts: isPlainObject,
|
|
73
60
|
needs: "an object"
|
|
74
61
|
},
|
|
75
62
|
optionalObject: {
|
|
76
|
-
accepts: (value) => value === null ||
|
|
63
|
+
accepts: (value) => value === null || isPlainObject(value),
|
|
77
64
|
needs: "an object or null"
|
|
78
65
|
},
|
|
79
66
|
string: {
|
|
@@ -88,26 +75,17 @@ const ELEMENT_PROPERTIES = {
|
|
|
88
75
|
direction: "direction",
|
|
89
76
|
indent: "number"
|
|
90
77
|
};
|
|
91
|
-
|
|
78
|
+
const TEXT_PROPERTIES = {
|
|
92
79
|
detail: "number",
|
|
93
80
|
format: "number",
|
|
94
81
|
mode: "string",
|
|
95
82
|
style: "string",
|
|
96
83
|
text: "string"
|
|
97
84
|
};
|
|
85
|
+
const UNIVERSAL_PROPERTIES = { version: "number" };
|
|
98
86
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* Aligned with Payload rather than measured: its `outputSchema` declares `type`
|
|
102
|
-
* and `version` required for every node in the tree, and that declaration is
|
|
103
|
-
* what types the field in `payload-types.ts`. Lexical itself hydrates a node
|
|
104
|
-
* without a `version`, or with the wrong kind of one, unchanged - but a
|
|
105
|
-
* consumer reading the document through the generated types has been promised
|
|
106
|
-
* an integer, and `BlockNode.importJSON` migrates on it.
|
|
107
|
-
*/ const UNIVERSAL_PROPERTIES = { version: "number" };
|
|
108
|
-
/**
|
|
109
|
-
* The root, as Payload declares it and as an editor exports it: these six
|
|
110
|
-
* properties, these kinds, and nothing else.
|
|
87
|
+
* The root as Payload declares it and an editor exports it: these six
|
|
88
|
+
* properties, these kinds, nothing else.
|
|
111
89
|
*/ const ROOT_PROPERTIES = {
|
|
112
90
|
children: "array",
|
|
113
91
|
direction: "direction",
|
|
@@ -118,20 +96,11 @@ const ELEMENT_PROPERTIES = {
|
|
|
118
96
|
};
|
|
119
97
|
/**
|
|
120
98
|
* What a serialized node must carry beyond {@link UNIVERSAL_PROPERTIES}, keyed
|
|
121
|
-
* by node type.
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* table is measured instead: an entry belongs here only if breaking it makes
|
|
127
|
-
* Lexical throw, or changes what the editor reads back. An element's `format`
|
|
128
|
-
* and a paragraph's text defaults are absent for that reason.
|
|
129
|
-
* `lexical.spec.ts` holds every entry to the rule against the node classes
|
|
130
|
-
* `@payloadcms/richtext-lexical` ships, so extend that test first.
|
|
131
|
-
*
|
|
132
|
-
* A node type with no entry is checked for the universal properties only.
|
|
133
|
-
* Guessing at the requirements of a project's own nodes would reject content
|
|
134
|
-
* that works.
|
|
99
|
+
* by node type. Payload stores a state without hydrating it, so a node missing
|
|
100
|
+
* these fails only later, in the admin editor. Payload declares nothing per
|
|
101
|
+
* node type, so the table is measured: an entry belongs only if breaking it
|
|
102
|
+
* makes Lexical throw or changes what the editor reads back. A type with no
|
|
103
|
+
* entry is not guessed at, since that would reject a project's working nodes.
|
|
135
104
|
*/ const REQUIRED_NODE_PROPERTIES = {
|
|
136
105
|
autolink: {
|
|
137
106
|
...ELEMENT_PROPERTIES,
|
|
@@ -175,31 +144,22 @@ const ELEMENT_PROPERTIES = {
|
|
|
175
144
|
}
|
|
176
145
|
};
|
|
177
146
|
/**
|
|
178
|
-
* Whether the table already says what a node type's `fields`
|
|
179
|
-
* sub-field walk does not report the same problem
|
|
180
|
-
*/ const constrainsFields = (type) => "fields" in (REQUIRED_NODE_PROPERTIES
|
|
147
|
+
* Whether the table already says what a node type's `fields` must be, so the
|
|
148
|
+
* sub-field walk does not report the same problem twice.
|
|
149
|
+
*/ const constrainsFields = (type) => "fields" in (ownValue(REQUIRED_NODE_PROPERTIES, type) ?? {});
|
|
181
150
|
const describeConstraints = (constraints) => Object.fromEntries(Object.entries(constraints).map(([property, constraint]) => [property, needs(constraint)]).sort((left, right) => left[0].localeCompare(right[0])));
|
|
182
151
|
/**
|
|
183
|
-
* What each
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
* Keyed by node type rather than reported per field, because that is what it
|
|
189
|
-
* depends on. A field says which types it allows, in its `nodes`; what a `text`
|
|
190
|
-
* node has to carry is the same wherever one is written, so a response that
|
|
191
|
-
* describes twenty rich text fields still states it once.
|
|
192
|
-
*
|
|
193
|
-
* `type` is listed although {@link UNIVERSAL_PROPERTIES} omits it. The
|
|
194
|
-
* validator never reports it missing, because a node's `type` is how it finds
|
|
195
|
-
* the entry to check against, but a client assembling a node from this listing
|
|
196
|
-
* still has to write one.
|
|
152
|
+
* What each node type must carry, phrased as the write side phrases its
|
|
153
|
+
* refusals so the two never disagree. Keyed by node type, since a `text` node
|
|
154
|
+
* needs the same wherever it is written. `type` is listed although
|
|
155
|
+
* {@link UNIVERSAL_PROPERTIES} omits it: the validator finds the entry by
|
|
156
|
+
* `type` and so never reports it missing, but a client must still write one.
|
|
197
157
|
*/ const nodePropertiesFor = (types) => Object.fromEntries([...new Set(types)].sort().map((type) => [type, describeConstraints(type === "root" ? ROOT_PROPERTIES : {
|
|
198
|
-
...REQUIRED_NODE_PROPERTIES
|
|
158
|
+
...ownValue(REQUIRED_NODE_PROPERTIES, type),
|
|
199
159
|
...UNIVERSAL_PROPERTIES,
|
|
200
160
|
type: "string"
|
|
201
161
|
})]));
|
|
202
|
-
|
|
162
|
+
const check = (node, constraints) => {
|
|
203
163
|
const problems = {
|
|
204
164
|
missing: [],
|
|
205
165
|
rejected: []
|
|
@@ -215,36 +175,29 @@ const describeConstraints = (constraints) => Object.fromEntries(Object.entries(c
|
|
|
215
175
|
};
|
|
216
176
|
const nodeProblems = (node) => check(node, {
|
|
217
177
|
...UNIVERSAL_PROPERTIES,
|
|
218
|
-
...REQUIRED_NODE_PROPERTIES
|
|
178
|
+
...ownValue(REQUIRED_NODE_PROPERTIES, node["type"])
|
|
219
179
|
});
|
|
220
180
|
/**
|
|
221
181
|
* The root is the one node Payload describes itself, down to refusing an
|
|
222
|
-
* unknown property, so it is checked against that description
|
|
223
|
-
*
|
|
182
|
+
* unknown property, so it is checked against that description instead of the
|
|
183
|
+
* walk's table.
|
|
224
184
|
*/ const rootProblems = (root) => ({
|
|
225
185
|
...check(root, ROOT_PROPERTIES),
|
|
226
|
-
unexpected: Object.keys(root).filter((property) => !(property
|
|
186
|
+
unexpected: Object.keys(root).filter((property) => !Object.hasOwn(ROOT_PROPERTIES, property))
|
|
227
187
|
});
|
|
228
188
|
/**
|
|
229
|
-
* What one serialized property
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* Absent where the table says nothing, which is the same tolerance the node
|
|
233
|
-
* walk shows: a project's own node may carry any property, and guessing at one
|
|
234
|
-
* would reject content that works.
|
|
189
|
+
* What one serialized property must be, for a write addressing a property
|
|
190
|
+
* instead of a whole node. Absent where the table says nothing, matching the
|
|
191
|
+
* node walk: a project's own node may carry any property.
|
|
235
192
|
*/ const propertyProblem = (nodeType, property, value) => {
|
|
236
|
-
const
|
|
193
|
+
const constraints = nodeType === "root" ? ROOT_PROPERTIES : {
|
|
237
194
|
...UNIVERSAL_PROPERTIES,
|
|
238
|
-
...REQUIRED_NODE_PROPERTIES
|
|
239
|
-
}
|
|
195
|
+
...ownValue(REQUIRED_NODE_PROPERTIES, nodeType)
|
|
196
|
+
};
|
|
197
|
+
const constraint = ownValue(constraints, property);
|
|
240
198
|
return constraint === void 0 || accepts(constraint, value) ? void 0 : { needs: needs(constraint) };
|
|
241
199
|
};
|
|
242
|
-
|
|
243
|
-
* Only properties a feature narrows and Lexical does not check on its own
|
|
244
|
-
* belong here. Everything else a feature restricts is already visible: a
|
|
245
|
-
* link's targets through its sub-schema, a block node's choices through the
|
|
246
|
-
* slugs it accepts.
|
|
247
|
-
*/ const NODE_OPTION_SOURCES = [{
|
|
200
|
+
const NODE_OPTION_SOURCES = [{
|
|
248
201
|
defaults: [
|
|
249
202
|
"h1",
|
|
250
203
|
"h2",
|
|
@@ -258,13 +211,7 @@ const nodeProblems = (node) => check(node, {
|
|
|
258
211
|
nodeProp: "tag",
|
|
259
212
|
nodeType: "heading"
|
|
260
213
|
}];
|
|
261
|
-
|
|
262
|
-
* The props a feature was resolved with.
|
|
263
|
-
*
|
|
264
|
-
* Sanitizing the editor drops every feature's props from `editorConfig.features`
|
|
265
|
-
* but leaves them on `resolvedFeatureMap`. A feature that declares no server
|
|
266
|
-
* props keeps only the client ones, so both are tried.
|
|
267
|
-
*/ const featurePropsOf = (field, featureKey) => {
|
|
214
|
+
const featurePropsOf = (field, featureKey) => {
|
|
268
215
|
const resolved = field.editor?.editorConfig?.resolvedFeatureMap?.get(featureKey);
|
|
269
216
|
const props = resolved?.sanitizedServerFeatureProps ?? resolved?.clientFeatureProps;
|
|
270
217
|
return typeof props === "object" && props !== null ? props : void 0;
|
package/dist/schema/outline.mjs
CHANGED
|
@@ -1,24 +1,14 @@
|
|
|
1
|
+
import { isPlainObject, ownValue } from "../guards.mjs";
|
|
1
2
|
import { allowedNodeTypes, nodeOptions } from "./lexical.mjs";
|
|
2
|
-
import { isPlainObject } from "./walk.mjs";
|
|
3
3
|
//#region src/schema/outline.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* document stays a fraction of the size of its editor state.
|
|
7
|
-
*/ const TEXT_PREVIEW_LENGTH = 80;
|
|
8
|
-
/**
|
|
9
|
-
* Every descendant text node contributes, not only direct children, since a
|
|
10
|
-
* link or a formatting mark nests the text a level deeper.
|
|
11
|
-
*/ const collectText = (node) => {
|
|
4
|
+
const TEXT_PREVIEW_LENGTH = 80;
|
|
5
|
+
const collectText = (node) => {
|
|
12
6
|
if (node["type"] === "text") return typeof node["text"] === "string" ? node["text"] : "";
|
|
13
7
|
const children = node["children"];
|
|
14
8
|
return Array.isArray(children) ? children.filter(isPlainObject).map((child) => collectText(child)).join("") : "";
|
|
15
9
|
};
|
|
16
10
|
const preview = (text) => text.length > TEXT_PREVIEW_LENGTH ? `${text.slice(0, TEXT_PREVIEW_LENGTH)}…` : text;
|
|
17
|
-
|
|
18
|
-
* Only the properties a feature actually narrows for this node type, and only
|
|
19
|
-
* where the node carries a string for one. A node missing the property, or
|
|
20
|
-
* carrying something the feature never produces, says nothing worth reporting.
|
|
21
|
-
*/ const narrowedOptions = (node, narrowed) => {
|
|
11
|
+
const narrowedOptions = (node, narrowed) => {
|
|
22
12
|
if (!narrowed) return;
|
|
23
13
|
const set = Object.keys(narrowed).flatMap((property) => {
|
|
24
14
|
const value = node[property];
|
|
@@ -31,7 +21,7 @@ const walk = (node, pointer, options, entries) => {
|
|
|
31
21
|
if (typeof type !== "string") return;
|
|
32
22
|
const version = node["version"];
|
|
33
23
|
const text = preview(collectText(node));
|
|
34
|
-
const nodeOptionsFound = narrowedOptions(node, options
|
|
24
|
+
const nodeOptionsFound = narrowedOptions(node, ownValue(options, type));
|
|
35
25
|
const children = node["children"];
|
|
36
26
|
const childCount = Array.isArray(children) ? children.length : 0;
|
|
37
27
|
entries.push({
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/schema/path.ts
|
|
2
|
+
const JSON_POINTER_PATTERN = /^(\/([^~/]|~[01])*)*$/;
|
|
3
|
+
/** No segments is the root pointer, `""`. */ const joinPath = (parts) => parts.map((part) => `/${part.replace(/~/g, "~0").replace(/\//g, "~1")}`).join("");
|
|
4
|
+
/** Unescapes `~1` and `~0`. The root pointer yields no segments. */ const splitPath = (path) => path.split("/").slice(1).map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
5
|
+
const PROTOTYPE_SEGMENTS = /* @__PURE__ */ new Set([
|
|
6
|
+
"__proto__",
|
|
7
|
+
"constructor",
|
|
8
|
+
"prototype"
|
|
9
|
+
]);
|
|
10
|
+
/**
|
|
11
|
+
* No field or node property has one of these names. rfc6902 skips them as
|
|
12
|
+
* intermediate tokens but writes them as a final key, so a pointer containing
|
|
13
|
+
* one is refused before anything resolves it.
|
|
14
|
+
*/ const prototypeSegmentProblem = (pointer) => splitPath(pointer).some((segment) => PROTOTYPE_SEGMENTS.has(segment)) ? `"${pointer}" contains a segment named __proto__, constructor or prototype, which no field or node property uses.` : void 0;
|
|
15
|
+
/** `-` included, since RFC 6901 reads it as the position after the last. */ const isIndexSegment = (segment) => segment === "-" || /^\d+$/.test(segment);
|
|
16
|
+
/**
|
|
17
|
+
* A path Payload reports on a validation error (`layout.0.title`) as a JSON
|
|
18
|
+
* Pointer, so everything handed back addresses documents the same way. The
|
|
19
|
+
* path already carries real indices, so it maps directly.
|
|
20
|
+
*/ const pointerFromPayloadPath = (path) => path ? joinPath(path.split(".")) : "";
|
|
21
|
+
//#endregion
|
|
22
|
+
export { JSON_POINTER_PATTERN, isIndexSegment, joinPath, pointerFromPayloadPath, prototypeSegmentProblem, splitPath };
|
package/dist/schema/pointer.mjs
CHANGED
|
@@ -1,39 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isIndexSegment, joinPath, splitPath } from "./path.mjs";
|
|
2
|
+
import { blockOf, blockSlugsOf, describeAddressableFields, findBlocksField, findRichTextField, schemaOf } from "./walk.mjs";
|
|
3
|
+
import { SchemaError } from "./errors.mjs";
|
|
2
4
|
import { resolveLexicalPointer } from "./lexical-pointer.mjs";
|
|
3
5
|
//#region src/schema/pointer.ts
|
|
4
6
|
const partMatches = (part, segment) => segment !== void 0 && (part === "*" ? isIndexSegment(segment) : part === segment);
|
|
5
|
-
|
|
6
|
-
* Longest descriptor whose path is fully consumed by the leading segments.
|
|
7
|
-
*/ const longestMatch = (descriptors, segments) => descriptors.map((descriptor) => ({
|
|
7
|
+
const matchPointerSegments = (descriptors, segments) => descriptors.map((descriptor) => ({
|
|
8
8
|
consumed: splitPath(descriptor.path).length,
|
|
9
9
|
descriptor,
|
|
10
10
|
parts: splitPath(descriptor.path)
|
|
11
11
|
})).filter(({ parts }) => parts.every((part, offset) => partMatches(part, segments[offset]))).sort((left, right) => right.consumed - left.consumed)[0];
|
|
12
|
-
|
|
12
|
+
const isSubtreePrefix = (descriptors, segments) => descriptors.some((descriptor) => {
|
|
13
13
|
const parts = splitPath(descriptor.path);
|
|
14
14
|
return parts.length > segments.length && segments.every((segment, offset) => {
|
|
15
15
|
const part = parts[offset];
|
|
16
16
|
return part !== void 0 && partMatches(part, segment);
|
|
17
17
|
});
|
|
18
18
|
});
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* array fields are descended through rather than skipped.
|
|
22
|
-
*/ const valueAtSegments = (data, segments) => segments.reduce((current, segment) => current === null || typeof current !== "object" ? void 0 : current[segment], data);
|
|
23
|
-
/**
|
|
24
|
-
* The stored row decides which block sits at an index, since a blocks field
|
|
25
|
-
* admits many shapes at the same position. A row the document does not have
|
|
26
|
-
* yet takes its slug from the value being added.
|
|
27
|
-
*/ const stepIntoBlock = (at) => {
|
|
19
|
+
const valueAtSegments = (data, segments) => segments.reduce((current, segment) => current === null || typeof current !== "object" ? void 0 : current[segment], data);
|
|
20
|
+
const stepIntoBlock = (at) => {
|
|
28
21
|
const { addedValue, config, descriptor, rows } = at;
|
|
29
22
|
const [index, ...remaining] = at.rest;
|
|
30
|
-
if (!isIndexSegment(index)) throw new
|
|
23
|
+
if (!isIndexSegment(index)) throw new SchemaError(`"${descriptor.path}" is an array; "${index}" is not an index.`);
|
|
31
24
|
const field = findBlocksField(at.fields, splitPath(descriptor.path));
|
|
32
25
|
const existing = Array.isArray(rows) && index !== "-" ? rows[Number(index)] : void 0;
|
|
33
26
|
const slug = existing?.blockType ?? addedValue?.blockType;
|
|
34
|
-
if (!field || slug === void 0) throw new
|
|
27
|
+
if (!field || slug === void 0) throw new SchemaError(`Cannot tell which block "${descriptor.path}/${index}" is. Supply a "blockType" on the value, one of: ${field ? blockSlugsOf(field).join(", ") : ""}`);
|
|
28
|
+
if (typeof slug !== "string") throw new SchemaError(`"blockType" must be a string naming a block at "${descriptor.path}". Allowed: ${blockSlugsOf(field).join(", ")}`);
|
|
35
29
|
const block = blockOf(config, field, slug);
|
|
36
|
-
if (!block) throw new
|
|
30
|
+
if (!block) throw new SchemaError(`"${slug}" is not allowed at "${descriptor.path}". Allowed: ${blockSlugsOf(field).join(", ")}`);
|
|
37
31
|
return {
|
|
38
32
|
blockType: slug,
|
|
39
33
|
data: existing,
|
|
@@ -42,12 +36,11 @@ const partMatches = (part, segment) => segment !== void 0 && (part === "*" ? isI
|
|
|
42
36
|
};
|
|
43
37
|
};
|
|
44
38
|
/**
|
|
45
|
-
* The stored document chooses the branch at every blocks
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* field admits many shapes at the same index.
|
|
39
|
+
* The stored document is required because it chooses the branch at every blocks
|
|
40
|
+
* element: `/layout/sections/3/modules/1` can only be resolved by reading
|
|
41
|
+
* `blockType` off `sections[3]`.
|
|
49
42
|
*/ const resolveDataPointer = (config, target) => {
|
|
50
|
-
let fields =
|
|
43
|
+
let fields = schemaOf(config, target.ref).flattenedFields;
|
|
51
44
|
let data = target.doc;
|
|
52
45
|
let blockType;
|
|
53
46
|
let segments = splitPath(target.pointer);
|
|
@@ -55,14 +48,14 @@ const partMatches = (part, segment) => segment !== void 0 && (part === "*" ? isI
|
|
|
55
48
|
let inLexical;
|
|
56
49
|
while (segments.length > 0) {
|
|
57
50
|
const descriptors = describeAddressableFields(fields);
|
|
58
|
-
const match =
|
|
51
|
+
const match = matchPointerSegments(descriptors, segments);
|
|
59
52
|
if (!match) {
|
|
60
53
|
if (isSubtreePrefix(descriptors, segments)) return {
|
|
61
54
|
...blockType === void 0 ? {} : { blockType },
|
|
62
55
|
fields,
|
|
63
56
|
prefix: segments.map((segment) => isIndexSegment(segment) ? "*" : segment)
|
|
64
57
|
};
|
|
65
|
-
throw new
|
|
58
|
+
throw new SchemaError(`"${joinPath(segments)}" is not a field here. Available: ${descriptors.map((descriptor) => descriptor.path).join(", ")}`);
|
|
66
59
|
}
|
|
67
60
|
const rest = segments.slice(match.consumed);
|
|
68
61
|
if (rest.length === 0) return {
|
|
@@ -100,7 +93,7 @@ const partMatches = (part, segment) => segment !== void 0 && (part === "*" ? isI
|
|
|
100
93
|
segments = step.rest;
|
|
101
94
|
continue;
|
|
102
95
|
}
|
|
103
|
-
if (match.descriptor.type !== "blocks") throw new
|
|
96
|
+
if (match.descriptor.type !== "blocks") throw new SchemaError(`"${match.descriptor.path}" is a ${match.descriptor.type} field and has no "${joinPath(rest)}" beneath it.`);
|
|
104
97
|
const step = stepIntoBlock({
|
|
105
98
|
addedValue: target.addedValue,
|
|
106
99
|
descriptor: match.descriptor,
|
package/dist/schema/shape.mjs
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
|
+
import { isPlainObject, ownValue } from "../guards.mjs";
|
|
1
2
|
import { ROOT_PROPERTIES, constrainsFields, lexicalSubSchema, nodeProblems, propertyProblem, rootProblems } from "./lexical.mjs";
|
|
2
|
-
import {
|
|
3
|
+
import { splitPath } from "./path.mjs";
|
|
4
|
+
import { ROW_KEYS, blockOf, blockSlugsOf, descriptorsUnder, findBlocksField, findRichTextField } from "./walk.mjs";
|
|
3
5
|
//#region src/schema/shape.ts
|
|
4
|
-
const TOLERATED_VALUE_KEYS = /* @__PURE__ */ new Set([
|
|
5
|
-
"blockName",
|
|
6
|
-
"blockType",
|
|
7
|
-
"id"
|
|
8
|
-
]);
|
|
9
6
|
/**
|
|
10
|
-
* Lexical
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* emptying one that was there.
|
|
7
|
+
* Lexical throws when it hydrates a state whose root holds nothing. An empty
|
|
8
|
+
* field is stored as null, so this is reached only by emptying one that had
|
|
9
|
+
* content.
|
|
14
10
|
*/ const EMPTY_ROOT = "an editor state needs at least one node. Clear the field with null instead.";
|
|
15
11
|
const quoted = (properties) => properties.map((property) => `"${property}"`).join(", ");
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
const refusedSlug = (slug) => {
|
|
13
|
+
if (slug === void 0) return `a row needs a "blockType"`;
|
|
14
|
+
return typeof slug === "string" ? `"${slug}" is not allowed here` : `"blockType" must be a string naming a block`;
|
|
15
|
+
};
|
|
16
|
+
const checkNodeFields = (scope, field, node) => {
|
|
20
17
|
const sub = lexicalSubSchema(field, node.type);
|
|
21
18
|
if (!sub) return;
|
|
22
19
|
const data = node.fields;
|
|
@@ -39,7 +36,7 @@ const quoted = (properties) => properties.map((property) => `"${property}"`).joi
|
|
|
39
36
|
const slug = data["blockType"];
|
|
40
37
|
const block = typeof slug === "string" ? blockOf(scope.config, sub.blocksField, slug) : void 0;
|
|
41
38
|
if (!block) {
|
|
42
|
-
scope.problems.push(`${scope.pointer}/fields:
|
|
39
|
+
scope.problems.push(`${scope.pointer}/fields: ${refusedSlug(slug)}. Allowed: ${blockSlugsOf(sub.blocksField).join(", ")}`);
|
|
43
40
|
return;
|
|
44
41
|
}
|
|
45
42
|
checkValue({
|
|
@@ -47,28 +44,10 @@ const quoted = (properties) => properties.map((property) => `"${property}"`).joi
|
|
|
47
44
|
fields: block.flattenedFields
|
|
48
45
|
}, data);
|
|
49
46
|
};
|
|
50
|
-
|
|
51
|
-
* An absent property is already reported as missing. Anything else present is
|
|
52
|
-
* checked, not just a string: Lexical stores a heading tag of 3 as readily as
|
|
53
|
-
* one of "h3".
|
|
54
|
-
*/ const checkNarrowedProperty = (scope, at, value) => {
|
|
47
|
+
const checkNarrowedProperty = (scope, at, value) => {
|
|
55
48
|
if (value !== void 0 && !(typeof value === "string" && at.values.includes(value))) scope.problems.push(`${at.pointer}: ${JSON.stringify(value)} is not available for a "${at.type}" node in this field's editor. Allowed: ${at.values.join(", ")}`);
|
|
56
49
|
};
|
|
57
|
-
|
|
58
|
-
* One node, wherever it came from. `pointer` addresses the node itself, so a
|
|
59
|
-
* node written at a position and a node written inside a whole editor state
|
|
60
|
-
* are held to the same rules and report them the same way.
|
|
61
|
-
*
|
|
62
|
-
* Payload does not check any of this: the Lexical validator runs node
|
|
63
|
-
* validations only for the few node types that register one, so a `heading`
|
|
64
|
-
* inside a field whose editor has no heading feature is stored without
|
|
65
|
-
* complaint and only fails later, at render or when the document is reopened
|
|
66
|
-
* in the admin editor. A key a node's fields do not declare is dropped just as
|
|
67
|
-
* silently. The same holds one level down, for the node properties a feature
|
|
68
|
-
* narrows: an `h3` in an editor restricted to `h4` is stored as readily as an
|
|
69
|
-
* `h4`, and a node written without the properties its class hydrates from is
|
|
70
|
-
* stored and then throws when the editor opens it.
|
|
71
|
-
*/ const checkNode = (scope, editor, node, pointer) => {
|
|
50
|
+
const checkNode = (scope, editor, node, pointer) => {
|
|
72
51
|
if (!isPlainObject(node) || typeof node["type"] !== "string") {
|
|
73
52
|
scope.problems.push(`${pointer}: every node needs a "type".`);
|
|
74
53
|
return;
|
|
@@ -81,7 +60,7 @@ const quoted = (properties) => properties.map((property) => `"${property}"`).joi
|
|
|
81
60
|
const problems = nodeProblems(node);
|
|
82
61
|
if (problems.missing.length > 0) scope.problems.push(`${pointer}: a "${type}" node is missing ${quoted(problems.missing)}. Write nodes as Lexical serializes them.`);
|
|
83
62
|
for (const problem of problems.rejected) scope.problems.push(`${pointer}/${problem.property}: a "${type}" node needs ${problem.needs} here.`);
|
|
84
|
-
for (const [property, values] of Object.entries(editor.nodeOptions
|
|
63
|
+
for (const [property, values] of Object.entries(ownValue(editor.nodeOptions, type) ?? {})) checkNarrowedProperty(scope, {
|
|
85
64
|
pointer: `${pointer}/${property}`,
|
|
86
65
|
type,
|
|
87
66
|
values
|
|
@@ -95,7 +74,7 @@ const quoted = (properties) => properties.map((property) => `"${property}"`).joi
|
|
|
95
74
|
});
|
|
96
75
|
checkNodes(scope, editor, node["children"], `${pointer}/children`);
|
|
97
76
|
};
|
|
98
|
-
|
|
77
|
+
const checkNodes = (scope, editor, nodes, pointer) => {
|
|
99
78
|
if (!Array.isArray(nodes)) return;
|
|
100
79
|
nodes.forEach((node, index) => {
|
|
101
80
|
checkNode(scope, editor, node, `${pointer}/${String(index)}`);
|
|
@@ -114,34 +93,26 @@ const checkRichText = (scope, editor, value) => {
|
|
|
114
93
|
if (Array.isArray(root["children"]) && root["children"].length === 0) scope.problems.push(`${scope.pointer}/root/children: ${EMPTY_ROOT}`);
|
|
115
94
|
checkNodes(scope, editor, root["children"], `${scope.pointer}/root/children`);
|
|
116
95
|
};
|
|
117
|
-
|
|
118
|
-
* `type` decides how every other property on a node is read, so replacing it
|
|
119
|
-
* alone would leave a heading shaped like a text node. Everything else is held
|
|
120
|
-
* to the constraint the node walk holds it to and nothing more, since a
|
|
121
|
-
* feature may put any property on a node.
|
|
122
|
-
*/ const checkNodeProperty = (scope, editor, position, value) => {
|
|
96
|
+
const checkNodeProperty = (scope, editor, position, value) => {
|
|
123
97
|
const { nodeType: type, property } = position;
|
|
124
98
|
if (property === "type") {
|
|
125
99
|
scope.problems.push(`${scope.pointer}: a node's "type" cannot be replaced on its own. Replace the whole node.`);
|
|
126
100
|
return;
|
|
127
101
|
}
|
|
128
|
-
if (position.isRoot && !(property
|
|
102
|
+
if (position.isRoot && !Object.hasOwn(ROOT_PROPERTIES, property)) {
|
|
129
103
|
scope.problems.push(`${scope.pointer}: no such property on the root node. Available: ${Object.keys(ROOT_PROPERTIES).join(", ")}`);
|
|
130
104
|
return;
|
|
131
105
|
}
|
|
132
106
|
const problem = propertyProblem(type, property, value);
|
|
133
107
|
if (problem) scope.problems.push(`${scope.pointer}: a "${type}" node needs ${problem.needs} here.`);
|
|
134
|
-
const values = editor.nodeOptions
|
|
108
|
+
const values = ownValue(ownValue(editor.nodeOptions, type), property);
|
|
135
109
|
if (values) checkNarrowedProperty(scope, {
|
|
136
110
|
pointer: scope.pointer,
|
|
137
111
|
type,
|
|
138
112
|
values
|
|
139
113
|
}, value);
|
|
140
114
|
};
|
|
141
|
-
|
|
142
|
-
* A value written at a position inside an editor state rather than as the
|
|
143
|
-
* whole state.
|
|
144
|
-
*/ const checkLexicalWrite = (scope, position, value) => {
|
|
115
|
+
const checkLexicalWrite = (scope, position, value) => {
|
|
145
116
|
const editor = {
|
|
146
117
|
allowed: position.descriptor.nodes ?? [],
|
|
147
118
|
field: position.field,
|
|
@@ -193,7 +164,7 @@ const checkLeafValue = (scope, descriptor, value) => {
|
|
|
193
164
|
const slug = isPlainObject(row) ? row["blockType"] : void 0;
|
|
194
165
|
const block = typeof slug === "string" ? blockOf(scope.config, field, slug) : void 0;
|
|
195
166
|
if (!block) {
|
|
196
|
-
scope.problems.push(`${scope.pointer}/${String(index)}:
|
|
167
|
+
scope.problems.push(`${scope.pointer}/${String(index)}: ${refusedSlug(slug)}. Allowed: ${blockSlugsOf(field).join(", ")}`);
|
|
197
168
|
return;
|
|
198
169
|
}
|
|
199
170
|
checkValue({
|
|
@@ -204,27 +175,12 @@ const checkLeafValue = (scope, descriptor, value) => {
|
|
|
204
175
|
}, row);
|
|
205
176
|
});
|
|
206
177
|
};
|
|
207
|
-
|
|
208
|
-
* Reports every shape problem rather than the first.
|
|
209
|
-
*
|
|
210
|
-
* Shape only: unknown field names, unknown block slugs, read-only fields, and
|
|
211
|
-
* rich text nodes or node properties the field's editor cannot produce.
|
|
212
|
-
* Required-ness, row counts, lengths, enum membership and relationship
|
|
213
|
-
* existence stay with Payload, which already checks them and reports them per
|
|
214
|
-
* field. Without this pass a misspelled field inside a new block would be
|
|
215
|
-
* stripped in silence.
|
|
216
|
-
*/ const checkValue = (scope, value) => {
|
|
178
|
+
const checkValue = (scope, value) => {
|
|
217
179
|
if (!isPlainObject(value)) return;
|
|
218
180
|
const prefixParts = scope.prefix;
|
|
219
|
-
const relative =
|
|
220
|
-
const parts = splitPath(descriptor.path);
|
|
221
|
-
return prefixParts.every((part, offset) => part === parts[offset]) ? [{
|
|
222
|
-
descriptor,
|
|
223
|
-
parts: parts.slice(prefixParts.length)
|
|
224
|
-
}] : [];
|
|
225
|
-
});
|
|
181
|
+
const relative = descriptorsUnder(scope.fields, prefixParts);
|
|
226
182
|
for (const [key, entry] of Object.entries(value)) {
|
|
227
|
-
if (
|
|
183
|
+
if (ROW_KEYS.has(key)) continue;
|
|
228
184
|
const candidates = relative.filter(({ parts }) => parts[0] === key);
|
|
229
185
|
const pointer = `${scope.pointer}/${key}`;
|
|
230
186
|
if (candidates.length === 0) {
|
|
@@ -264,9 +220,7 @@ const checkLeafValue = (scope, descriptor, value) => {
|
|
|
264
220
|
}, entry);
|
|
265
221
|
}
|
|
266
222
|
};
|
|
267
|
-
/**
|
|
268
|
-
* Shape problems with a value about to be written at a resolved pointer.
|
|
269
|
-
*/ const validateWriteValue = (config, target, value) => {
|
|
223
|
+
/** Shape problems with a value about to be written at a resolved pointer. */ const validateWriteValue = (config, target, value) => {
|
|
270
224
|
const problems = [];
|
|
271
225
|
const scope = {
|
|
272
226
|
config,
|