@abinnovision/payloadcms-mcpx 1.0.0-beta.14 → 1.0.0-beta.15
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 +13 -0
- package/dist/schema/index.mjs +2 -2
- package/dist/schema/lexical.mjs +153 -1
- package/dist/schema/shape.mjs +16 -5
- package/dist/tools/describe-schema.mjs +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -222,6 +222,19 @@ Rules the tools enforce and explain in their own descriptions:
|
|
|
222
222
|
answers `{ "heading": { "tag": ["h4"] } }`, and a write carrying any other
|
|
223
223
|
heading tag is refused. Lexical stores whatever tag it is given, so this is
|
|
224
224
|
the only place the restriction is checked.
|
|
225
|
+
- A Lexical node must be written the way Lexical serializes it, and carry the
|
|
226
|
+
values Lexical would have written. The admin editor rehydrates nodes through
|
|
227
|
+
their classes, so a list item whose `indent` is absent, `null` or `"0"` throws
|
|
228
|
+
when the document is opened, and a heading whose `tag` is `3` comes back
|
|
229
|
+
untagged, none of which Payload notices on write. A write breaking either is
|
|
230
|
+
refused, and the message names the property and what belongs there.
|
|
231
|
+
- Where Payload states the shape itself, that statement is what is enforced: its
|
|
232
|
+
`outputSchema` declares `version` required on every node, and gives the root
|
|
233
|
+
exactly `children`, `direction`, `format`, `indent`, `type` and `version`, so
|
|
234
|
+
an unknown property on the root is refused too. Payload declares nothing per
|
|
235
|
+
node type, so the rest is measured against the node classes
|
|
236
|
+
`@payloadcms/richtext-lexical` ships. Both halves are pinned by
|
|
237
|
+
`src/schema/lexical.spec.ts` rather than assumed.
|
|
225
238
|
- Field and collection `admin.description` values are included in
|
|
226
239
|
`describeSchema` and `listCapabilities`, so intent written for the admin
|
|
227
240
|
panel reaches the client. A locale-keyed record is resolved to one string for
|
package/dist/schema/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { allowedNodeTypes, lexicalSubSchema, nodeOptions, subSchemaNodeTypes } from "./lexical.mjs";
|
|
1
|
+
import { REQUIRED_NODE_PROPERTIES, ROOT_PROPERTIES, allowedNodeTypes, constrainsFields, lexicalSubSchema, nodeOptions, nodeProblems, rootProblems, subSchemaNodeTypes } from "./lexical.mjs";
|
|
2
2
|
import { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, blockSlugsOf, describeAddressableFields, describeFields, findBlocksField, findRichTextField, joinPath, pointerFromPayloadPath, splitPath, targetOf } from "./walk.mjs";
|
|
3
3
|
import { nodeDescriber, reachableSchemaPaths } from "./describe.mjs";
|
|
4
4
|
import { resolveDataPointer } from "./pointer.mjs";
|
|
5
5
|
import { validateWriteValue } from "./shape.mjs";
|
|
6
|
-
export { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, allowedNodeTypes, blockOf, blockSlugsOf, describeAddressableFields, describeFields, findBlocksField, findRichTextField, joinPath, lexicalSubSchema, nodeDescriber, nodeOptions, pointerFromPayloadPath, reachableSchemaPaths, resolveDataPointer, splitPath, subSchemaNodeTypes, targetOf, validateWriteValue };
|
|
6
|
+
export { JSON_POINTER_PATTERN, REQUIRED_NODE_PROPERTIES, RESERVED_FIELD_NAMES, ROOT_PROPERTIES, allowedNodeTypes, blockOf, blockSlugsOf, constrainsFields, describeAddressableFields, describeFields, findBlocksField, findRichTextField, joinPath, lexicalSubSchema, nodeDescriber, nodeOptions, nodeProblems, pointerFromPayloadPath, reachableSchemaPaths, resolveDataPointer, rootProblems, splitPath, subSchemaNodeTypes, targetOf, validateWriteValue };
|
package/dist/schema/lexical.mjs
CHANGED
|
@@ -53,6 +53,158 @@ const lexicalSubSchema = (field, nodeType) => {
|
|
|
53
53
|
};
|
|
54
54
|
/** In the order their features registered them. */ const subSchemaNodeTypes = (field) => [...featuresOf(field)?.getSubFields?.keys() ?? []].filter((nodeType) => lexicalSubSchema(field, nodeType) !== void 0);
|
|
55
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 = {
|
|
59
|
+
array: {
|
|
60
|
+
accepts: (value) => Array.isArray(value),
|
|
61
|
+
needs: "an array"
|
|
62
|
+
},
|
|
63
|
+
direction: {
|
|
64
|
+
accepts: (value) => value === null || value === "ltr" || value === "rtl",
|
|
65
|
+
needs: "\"ltr\", \"rtl\" or null"
|
|
66
|
+
},
|
|
67
|
+
number: {
|
|
68
|
+
accepts: (value) => typeof value === "number",
|
|
69
|
+
needs: "a number"
|
|
70
|
+
},
|
|
71
|
+
object: {
|
|
72
|
+
accepts: (value) => typeof value === "object" && value !== null && !Array.isArray(value),
|
|
73
|
+
needs: "an object"
|
|
74
|
+
},
|
|
75
|
+
optionalObject: {
|
|
76
|
+
accepts: (value) => value === null || typeof value === "object" && !Array.isArray(value),
|
|
77
|
+
needs: "an object or null"
|
|
78
|
+
},
|
|
79
|
+
string: {
|
|
80
|
+
accepts: (value) => typeof value === "string",
|
|
81
|
+
needs: "a string"
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const accepts = (constraint, value) => typeof constraint === "string" ? KINDS[constraint].accepts(value) : value === constraint.is;
|
|
85
|
+
const needs = (constraint) => typeof constraint === "string" ? KINDS[constraint].needs : JSON.stringify(constraint.is);
|
|
86
|
+
const ELEMENT_PROPERTIES = {
|
|
87
|
+
children: "array",
|
|
88
|
+
direction: "direction",
|
|
89
|
+
indent: "number"
|
|
90
|
+
};
|
|
91
|
+
/** A text node and everything built on one. */ const TEXT_PROPERTIES = {
|
|
92
|
+
detail: "number",
|
|
93
|
+
format: "number",
|
|
94
|
+
mode: "string",
|
|
95
|
+
style: "string",
|
|
96
|
+
text: "string"
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Carried by every node, whatever its type.
|
|
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.
|
|
111
|
+
*/ const ROOT_PROPERTIES = {
|
|
112
|
+
children: "array",
|
|
113
|
+
direction: "direction",
|
|
114
|
+
format: "string",
|
|
115
|
+
indent: "number",
|
|
116
|
+
type: "string",
|
|
117
|
+
version: "number"
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* What a serialized node must carry beyond {@link UNIVERSAL_PROPERTIES}, keyed
|
|
121
|
+
* by node type.
|
|
122
|
+
*
|
|
123
|
+
* Payload stores an editor state without hydrating it, so a node written
|
|
124
|
+
* without these, or with the wrong kind of value, is accepted and only fails
|
|
125
|
+
* later, in the admin editor. Payload declares nothing per node type, so this
|
|
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.
|
|
135
|
+
*/ const REQUIRED_NODE_PROPERTIES = {
|
|
136
|
+
autolink: {
|
|
137
|
+
...ELEMENT_PROPERTIES,
|
|
138
|
+
fields: "object"
|
|
139
|
+
},
|
|
140
|
+
block: { fields: "object" },
|
|
141
|
+
heading: {
|
|
142
|
+
...ELEMENT_PROPERTIES,
|
|
143
|
+
tag: "string"
|
|
144
|
+
},
|
|
145
|
+
inlineBlock: { fields: "object" },
|
|
146
|
+
link: {
|
|
147
|
+
...ELEMENT_PROPERTIES,
|
|
148
|
+
fields: "object"
|
|
149
|
+
},
|
|
150
|
+
list: {
|
|
151
|
+
...ELEMENT_PROPERTIES,
|
|
152
|
+
listType: "string",
|
|
153
|
+
start: "number"
|
|
154
|
+
},
|
|
155
|
+
listitem: {
|
|
156
|
+
...ELEMENT_PROPERTIES,
|
|
157
|
+
value: "number"
|
|
158
|
+
},
|
|
159
|
+
paragraph: ELEMENT_PROPERTIES,
|
|
160
|
+
quote: ELEMENT_PROPERTIES,
|
|
161
|
+
relationship: {
|
|
162
|
+
relationTo: "string",
|
|
163
|
+
value: "number"
|
|
164
|
+
},
|
|
165
|
+
tab: {
|
|
166
|
+
...TEXT_PROPERTIES,
|
|
167
|
+
detail: { is: 2 },
|
|
168
|
+
text: { is: " " }
|
|
169
|
+
},
|
|
170
|
+
text: TEXT_PROPERTIES,
|
|
171
|
+
upload: {
|
|
172
|
+
fields: "optionalObject",
|
|
173
|
+
relationTo: "string",
|
|
174
|
+
value: "number"
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Whether the table already says what a node type's `fields` has to be, so the
|
|
179
|
+
* sub-field walk does not report the same problem a second time.
|
|
180
|
+
*/ const constrainsFields = (type) => "fields" in (REQUIRED_NODE_PROPERTIES[type] ?? {});
|
|
181
|
+
/** Present but `null` counts as present: `direction` is serialized that way. */ const check = (node, constraints) => {
|
|
182
|
+
const problems = {
|
|
183
|
+
missing: [],
|
|
184
|
+
rejected: []
|
|
185
|
+
};
|
|
186
|
+
for (const [property, constraint] of Object.entries(constraints)) if (!(property in node)) problems.missing.push(property);
|
|
187
|
+
else if (!accepts(constraint, node[property])) problems.rejected.push({
|
|
188
|
+
needs: needs(constraint),
|
|
189
|
+
property
|
|
190
|
+
});
|
|
191
|
+
problems.missing.sort();
|
|
192
|
+
problems.rejected.sort((left, right) => left.property.localeCompare(right.property));
|
|
193
|
+
return problems;
|
|
194
|
+
};
|
|
195
|
+
const nodeProblems = (node) => check(node, {
|
|
196
|
+
...UNIVERSAL_PROPERTIES,
|
|
197
|
+
...REQUIRED_NODE_PROPERTIES[node["type"]] ?? {}
|
|
198
|
+
});
|
|
199
|
+
/**
|
|
200
|
+
* The root is the one node Payload describes itself, down to refusing an
|
|
201
|
+
* unknown property, so it is checked against that description rather than
|
|
202
|
+
* against the walk's table.
|
|
203
|
+
*/ const rootProblems = (root) => ({
|
|
204
|
+
...check(root, ROOT_PROPERTIES),
|
|
205
|
+
unexpected: Object.keys(root).filter((property) => !(property in ROOT_PROPERTIES))
|
|
206
|
+
});
|
|
207
|
+
/**
|
|
56
208
|
* Only properties a feature narrows and Lexical does not check on its own
|
|
57
209
|
* belong here. Everything else a feature restricts is already visible: a
|
|
58
210
|
* link's targets through its sub-schema, a block node's choices through the
|
|
@@ -95,4 +247,4 @@ const stringList = (value) => Array.isArray(value) && value.every((entry) => typ
|
|
|
95
247
|
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
96
248
|
};
|
|
97
249
|
//#endregion
|
|
98
|
-
export { allowedNodeTypes, lexicalSubSchema, nodeOptions, subSchemaNodeTypes };
|
|
250
|
+
export { REQUIRED_NODE_PROPERTIES, ROOT_PROPERTIES, allowedNodeTypes, constrainsFields, lexicalSubSchema, nodeOptions, nodeProblems, rootProblems, subSchemaNodeTypes };
|
package/dist/schema/shape.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { lexicalSubSchema } from "./lexical.mjs";
|
|
1
|
+
import { ROOT_PROPERTIES, constrainsFields, lexicalSubSchema, nodeProblems, rootProblems } from "./lexical.mjs";
|
|
2
2
|
import { blockOf, blockSlugsOf, describeAddressableFields, findBlocksField, findRichTextField, splitPath } from "./walk.mjs";
|
|
3
3
|
//#region src/schema/shape.ts
|
|
4
4
|
const TOLERATED_VALUE_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -7,6 +7,7 @@ const TOLERATED_VALUE_KEYS = /* @__PURE__ */ new Set([
|
|
|
7
7
|
"id"
|
|
8
8
|
]);
|
|
9
9
|
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
const quoted = (properties) => properties.map((property) => `"${property}"`).join(", ");
|
|
10
11
|
/**
|
|
11
12
|
* A node with nothing to declare, and one whose sub-fields cannot be named at a
|
|
12
13
|
* position, are both left alone.
|
|
@@ -15,7 +16,7 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
|
|
|
15
16
|
if (!sub) return;
|
|
16
17
|
const data = node.fields;
|
|
17
18
|
if (!isPlainObject(data)) {
|
|
18
|
-
scope.problems.push(`${scope.pointer}: a "${node.type}" node carries a "fields" object.`);
|
|
19
|
+
if (!constrainsFields(node.type)) scope.problems.push(`${scope.pointer}: a "${node.type}" node carries a "fields" object.`);
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
21
22
|
const nested = {
|
|
@@ -48,12 +49,19 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
|
|
|
48
49
|
* later, at render or when the document is reopened in the admin editor. A key
|
|
49
50
|
* a node's fields do not declare is dropped just as silently. The same holds
|
|
50
51
|
* one level down, for the node properties a feature narrows: an `h3` in an
|
|
51
|
-
* editor restricted to `h4` is stored as readily as an `h4
|
|
52
|
+
* editor restricted to `h4` is stored as readily as an `h4`, and a node written
|
|
53
|
+
* without the properties its class hydrates from is stored and then throws when
|
|
54
|
+
* the editor opens it.
|
|
52
55
|
*/ const checkRichText = (scope, editor, value) => {
|
|
53
56
|
if (!isPlainObject(value) || !isPlainObject(value["root"])) {
|
|
54
57
|
scope.problems.push(`${scope.pointer}: expected a Lexical editor state with a "root".`);
|
|
55
58
|
return;
|
|
56
59
|
}
|
|
60
|
+
const root = value["root"];
|
|
61
|
+
const { missing, rejected, unexpected } = rootProblems(root);
|
|
62
|
+
if (missing.length > 0) scope.problems.push(`${scope.pointer}/root: the root node is missing ${quoted(missing)}. Write nodes as Lexical serializes them.`);
|
|
63
|
+
for (const problem of rejected) scope.problems.push(`${scope.pointer}/root/${problem.property}: the root node needs ${problem.needs} here.`);
|
|
64
|
+
for (const property of unexpected) scope.problems.push(`${scope.pointer}/root/${property}: no such property on the root node. Available: ${Object.keys(ROOT_PROPERTIES).join(", ")}`);
|
|
57
65
|
const walk = (nodes, pointer) => {
|
|
58
66
|
if (!Array.isArray(nodes)) return;
|
|
59
67
|
nodes.forEach((node, index) => {
|
|
@@ -66,9 +74,12 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
|
|
|
66
74
|
scope.problems.push(`${at}: "${node["type"]}" is not available in this field's editor. Allowed: ${editor.allowed.join(", ")}`);
|
|
67
75
|
return;
|
|
68
76
|
}
|
|
77
|
+
const problems = nodeProblems(node);
|
|
78
|
+
if (problems.missing.length > 0) scope.problems.push(`${at}: a "${node["type"]}" node is missing ${quoted(problems.missing)}. Write nodes as Lexical serializes them.`);
|
|
79
|
+
for (const problem of problems.rejected) scope.problems.push(`${at}/${problem.property}: a "${node["type"]}" node needs ${problem.needs} here.`);
|
|
69
80
|
for (const [property, values] of Object.entries(editor.nodeOptions?.[node["type"]] ?? {})) {
|
|
70
81
|
const value = node[property];
|
|
71
|
-
if (typeof value === "string" &&
|
|
82
|
+
if (value !== void 0 && !(typeof value === "string" && values.includes(value))) scope.problems.push(`${at}/${property}: ${JSON.stringify(value)} is not available for a "${node["type"]}" node in this field's editor. Allowed: ${values.join(", ")}`);
|
|
72
83
|
}
|
|
73
84
|
if (editor.field) checkNodeFields({
|
|
74
85
|
...scope,
|
|
@@ -80,7 +91,7 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
|
|
|
80
91
|
walk(node["children"], `${at}/children`);
|
|
81
92
|
});
|
|
82
93
|
};
|
|
83
|
-
walk(
|
|
94
|
+
walk(root["children"], `${scope.pointer}/root/children`);
|
|
84
95
|
};
|
|
85
96
|
const checkLeafValue = (scope, descriptor, value) => {
|
|
86
97
|
if (descriptor.readOnly) {
|
|
@@ -22,6 +22,8 @@ Call it with no "paths" to get a collection's own fields. Every "blocks" field s
|
|
|
22
22
|
|
|
23
23
|
A "richText" field stops there too. It lists the Lexical node types it accepts in "nodes", and "next" carries a path for every node type that holds fields of its own: "/content/link" for a link node, "/content/block/callout" and "/content/inlineBlock/badge" for the block nodes. Descend to get the real field list instead of guessing what a node carries. Upload nodes are not addressable, because their fields depend on the collection the node points at.
|
|
24
24
|
|
|
25
|
+
Write each Lexical node the way Lexical serializes it, with every property its type carries rather than a trimmed subset, and with the value Lexical would have written there. Every node needs a "version"; the root takes exactly "children", "direction", "format", "indent", "type" and "version" and refuses anything else; each node type adds its own on top. The admin editor rehydrates nodes through their classes, so a list item whose "indent" is missing, null or a string is stored and then throws on open, and a heading whose "tag" is a number is stored untagged. A write naming a property means exactly that.
|
|
26
|
+
|
|
25
27
|
Paths here use the same JSON Pointer syntax as getDocument and patchDocument, and are already resolved through anything that does not nest in the stored document. The difference is only what stands in an element position: a path names an array element "*" and a block by its slug, where a pointer into a document carries a 0-based index. So "/items/*/title" is written at "/items/0/title", and "/layout/sections/hero" at "/layout/sections/0".
|
|
26
28
|
|
|
27
29
|
Fields Payload maintains (id, _status, createdAt, updatedAt) are never listed and cannot be written. Fields marked readOnly are listed but refused on write.`,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@abinnovision/payloadcms-mcpx",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.15",
|
|
5
5
|
"description": "Payload CMS plugin exposing a fixed, schema-aware MCP tool surface with draft-only writes and per-API-key capabilities.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"payload",
|