@abinnovision/payloadcms-mcpx 1.0.0-beta.16 → 1.0.0-beta.17
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
CHANGED
|
@@ -228,6 +228,30 @@ Rules the tools enforce and explain in their own descriptions:
|
|
|
228
228
|
when the document is opened, and a heading whose `tag` is `3` comes back
|
|
229
229
|
untagged, none of which Payload notices on write. A write breaking either is
|
|
230
230
|
refused, and the message names the property and what belongs there.
|
|
231
|
+
- Those requirements are also published, so a node can be built without being
|
|
232
|
+
corrected into shape first. A `describeSchema` response that reached a
|
|
233
|
+
`richText` field ends with a `nodeProperties` entry stating what each node
|
|
234
|
+
type has to carry, in the same words the refusal uses:
|
|
235
|
+
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"text": {
|
|
239
|
+
"detail": "a number",
|
|
240
|
+
"format": "a number",
|
|
241
|
+
"mode": "a string",
|
|
242
|
+
"style": "a string",
|
|
243
|
+
"text": "a string",
|
|
244
|
+
"type": "a string",
|
|
245
|
+
"version": "a number"
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
It is keyed by node type and stated once for the whole response, because what
|
|
251
|
+
a node carries does not vary by where it is written; the field's own `nodes`
|
|
252
|
+
says which of those types it accepts. The listing is read off the same tables
|
|
253
|
+
the validator checks against, so the two cannot drift apart.
|
|
254
|
+
|
|
231
255
|
- A rich text field's value is addressable, so a small edit does not have to
|
|
232
256
|
rewrite the whole state. `/content/root/children/2` is a node,
|
|
233
257
|
`/content/root/children/2/tag` one of its properties, and
|
|
@@ -266,6 +290,14 @@ Rules the tools enforce and explain in their own descriptions:
|
|
|
266
290
|
its slug, where a pointer carries a 0-based index. So `/items/*/title` is
|
|
267
291
|
written at `/items/0/title`, and `/layout/sections/hero` at
|
|
268
292
|
`/layout/sections/0`.
|
|
293
|
+
- Inside a rich text field that substitution does not apply, because an editor
|
|
294
|
+
state is a tree rather than a list per type. A path there names the node type,
|
|
295
|
+
and a block node its slug; a pointer enters the state at `root` and walks
|
|
296
|
+
`children` by an index counted over every child at that level, not over the
|
|
297
|
+
blocks among them, with the node's own fields under `fields`. So the path
|
|
298
|
+
`/content/block/practice-note/variant` is written at the pointer
|
|
299
|
+
`/content/root/children/7/fields/variant`, and only the stored state says
|
|
300
|
+
which index that is. `getDocument` with `outline` answers that.
|
|
269
301
|
- Adding a block requires `blockType` on the value; append with `/-`.
|
|
270
302
|
- Clearing is `replace` with `null`; a list is emptied with `[]` and refuses
|
|
271
303
|
`null`. `remove` is only valid on list elements, because Payload keeps
|
package/dist/schema/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { REQUIRED_NODE_PROPERTIES, ROOT_PROPERTIES, allowedNodeTypes, constrainsFields, lexicalSubSchema, nodeOptions, nodeProblems, propertyProblem, rootProblems, subSchemaNodeTypes } from "./lexical.mjs";
|
|
1
|
+
import { REQUIRED_NODE_PROPERTIES, ROOT_PROPERTIES, allowedNodeTypes, constrainsFields, lexicalSubSchema, nodeOptions, nodeProblems, nodePropertiesFor, propertyProblem, rootProblems, subSchemaNodeTypes } from "./lexical.mjs";
|
|
2
2
|
import { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, blockSlugsOf, describeAddressableFields, describeFields, findBlocksField, findRichTextField, isIndexSegment, isPlainObject, joinPath, pointerFromPayloadPath, splitPath, targetOf } from "./walk.mjs";
|
|
3
3
|
import { nodeDescriber, reachableSchemaPaths } from "./describe.mjs";
|
|
4
4
|
import { resolveLexicalPointer } from "./lexical-pointer.mjs";
|
|
5
5
|
import { lexicalOutline } from "./outline.mjs";
|
|
6
6
|
import { resolveDataPointer } from "./pointer.mjs";
|
|
7
7
|
import { EMPTY_ROOT, validateWriteValue } from "./shape.mjs";
|
|
8
|
-
export { EMPTY_ROOT, JSON_POINTER_PATTERN, REQUIRED_NODE_PROPERTIES, RESERVED_FIELD_NAMES, ROOT_PROPERTIES, allowedNodeTypes, blockOf, blockSlugsOf, constrainsFields, describeAddressableFields, describeFields, findBlocksField, findRichTextField, isIndexSegment, isPlainObject, joinPath, lexicalOutline, lexicalSubSchema, nodeDescriber, nodeOptions, nodeProblems, pointerFromPayloadPath, propertyProblem, reachableSchemaPaths, resolveDataPointer, resolveLexicalPointer, rootProblems, splitPath, subSchemaNodeTypes, targetOf, validateWriteValue };
|
|
8
|
+
export { EMPTY_ROOT, JSON_POINTER_PATTERN, REQUIRED_NODE_PROPERTIES, RESERVED_FIELD_NAMES, ROOT_PROPERTIES, allowedNodeTypes, blockOf, blockSlugsOf, constrainsFields, describeAddressableFields, describeFields, findBlocksField, findRichTextField, isIndexSegment, isPlainObject, joinPath, lexicalOutline, lexicalSubSchema, nodeDescriber, nodeOptions, nodeProblems, nodePropertiesFor, pointerFromPayloadPath, propertyProblem, reachableSchemaPaths, resolveDataPointer, resolveLexicalPointer, rootProblems, splitPath, subSchemaNodeTypes, targetOf, validateWriteValue };
|
|
@@ -45,7 +45,7 @@ import { blockOf, blockSlugsOf, isIndexSegment, isPlainObject, joinPath } from "
|
|
|
45
45
|
};
|
|
46
46
|
if (!isPlainObject(state) || !isPlainObject(state["root"])) throw new Error(`"${descriptor.path}" holds no editor state yet. Write the whole field once, then address positions inside it.`);
|
|
47
47
|
const [entry, ...rest] = at.segments;
|
|
48
|
-
if (entry !== "root") throw new Error(`"${String(entry)}" is not a position in a rich text field. An editor state is entered at "root", e.g. "${descriptor.path}/root/children/0".`);
|
|
48
|
+
if (entry !== "root") throw new Error(`"${String(entry)}" is not a position in a rich text field. An editor state is entered at "root", e.g. "${descriptor.path}/root/children/0". getDocument with "outline" lists every position this field holds.`);
|
|
49
49
|
let node = state["root"];
|
|
50
50
|
let nodeType = "root";
|
|
51
51
|
let segments = rest;
|
|
@@ -72,7 +72,7 @@ import { blockOf, blockSlugsOf, isIndexSegment, isPlainObject, joinPath } from "
|
|
|
72
72
|
}
|
|
73
73
|
};
|
|
74
74
|
const [index, ...beyond] = remaining;
|
|
75
|
-
if (!isIndexSegment(index)) throw new Error(`"${descriptor.path}${joinPath([...walked, "children"])}" is a list; "${index}" is not an index.`);
|
|
75
|
+
if (!isIndexSegment(index)) throw new Error(`"${descriptor.path}${joinPath([...walked, "children"])}" is a list; "${index}" is not an index. getDocument with "outline" reports the pointer of each node in it.`);
|
|
76
76
|
const children = node?.["children"];
|
|
77
77
|
const child = Array.isArray(children) && index !== "-" ? children[Number(index)] : void 0;
|
|
78
78
|
const type = isPlainObject(child) ? child["type"] : addedValue?.type;
|
|
@@ -88,7 +88,7 @@ import { blockOf, blockSlugsOf, isIndexSegment, isPlainObject, joinPath } from "
|
|
|
88
88
|
...walked,
|
|
89
89
|
"children",
|
|
90
90
|
index
|
|
91
|
-
])}" is.
|
|
91
|
+
])}" is. Call getDocument with "outline" for the pointer of each node, or address an existing position.`);
|
|
92
92
|
}
|
|
93
93
|
node = isPlainObject(child) ? child : void 0;
|
|
94
94
|
nodeType = type;
|
package/dist/schema/lexical.mjs
CHANGED
|
@@ -178,6 +178,27 @@ const ELEMENT_PROPERTIES = {
|
|
|
178
178
|
* Whether the table already says what a node type's `fields` has to be, so the
|
|
179
179
|
* sub-field walk does not report the same problem a second time.
|
|
180
180
|
*/ const constrainsFields = (type) => "fields" in (REQUIRED_NODE_PROPERTIES[type] ?? {});
|
|
181
|
+
const describeConstraints = (constraints) => Object.fromEntries(Object.entries(constraints).map(([property, constraint]) => [property, needs(constraint)]).sort((left, right) => left[0].localeCompare(right[0])));
|
|
182
|
+
/**
|
|
183
|
+
* What each of the given node types has to carry, phrased the way the write
|
|
184
|
+
* side phrases it when it refuses one, so the listing and the error message
|
|
185
|
+
* never disagree. Read straight off the tables above, which is what keeps it
|
|
186
|
+
* true: nothing here is stated a second time.
|
|
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.
|
|
197
|
+
*/ const nodePropertiesFor = (types) => Object.fromEntries([...new Set(types)].sort().map((type) => [type, describeConstraints(type === "root" ? ROOT_PROPERTIES : {
|
|
198
|
+
...REQUIRED_NODE_PROPERTIES[type],
|
|
199
|
+
...UNIVERSAL_PROPERTIES,
|
|
200
|
+
type: "string"
|
|
201
|
+
})]));
|
|
181
202
|
/** Present but `null` counts as present: `direction` is serialized that way. */ const check = (node, constraints) => {
|
|
182
203
|
const problems = {
|
|
183
204
|
missing: [],
|
|
@@ -261,4 +282,4 @@ const stringList = (value) => Array.isArray(value) && value.every((entry) => typ
|
|
|
261
282
|
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
262
283
|
};
|
|
263
284
|
//#endregion
|
|
264
|
-
export { REQUIRED_NODE_PROPERTIES, ROOT_PROPERTIES, allowedNodeTypes, constrainsFields, lexicalSubSchema, nodeOptions, nodeProblems, propertyProblem, rootProblems, subSchemaNodeTypes };
|
|
285
|
+
export { REQUIRED_NODE_PROPERTIES, ROOT_PROPERTIES, allowedNodeTypes, constrainsFields, lexicalSubSchema, nodeOptions, nodeProblems, nodePropertiesFor, propertyProblem, rootProblems, subSchemaNodeTypes };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsonResult } from "../result.mjs";
|
|
2
|
+
import { nodePropertiesFor } from "../schema/lexical.mjs";
|
|
2
3
|
import { translatorFor } from "../i18n.mjs";
|
|
3
4
|
import { nodeDescriber, reachableSchemaPaths } from "../schema/describe.mjs";
|
|
4
5
|
import "../schema/index.mjs";
|
|
@@ -22,11 +23,11 @@ Call it with no "paths" to get a collection's own fields. Every "blocks" field s
|
|
|
22
23
|
|
|
23
24
|
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
25
|
|
|
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.
|
|
26
|
+
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. Those requirements are stated rather than left to be discovered: the response carries one final "nodeProperties" entry keyed by node type, naming each property and what belongs there in the same words a refused write uses, so a node can be built from this response alone. A field's own "nodes" says which of those types it accepts. The root takes exactly "children", "direction", "format", "indent", "type" and "version" and refuses anything else. 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. A state whose root holds no children is refused however it is written, because Lexical reads it as empty and throws; clear a field with null instead.
|
|
26
27
|
|
|
27
|
-
A rich text field's value is addressable too, so an edit does not have to rewrite the whole state: "/content/root/children/0" is the first top-level node, "/content/root/children/0/children/1" a node inside it, "/content/root/children/0/tag" one property of a node, and "/content/root/children/0/fields/url" a field of a node, described at the "next" path for that node type. Append a node with "/-".
|
|
28
|
+
A rich text field's value is addressable too, so an edit does not have to rewrite the whole state: "/content/root/children/0" is the first top-level node, "/content/root/children/0/children/1" a node inside it, "/content/root/children/0/tag" one property of a node, and "/content/root/children/0/fields/url" a field of a node, described at the "next" path for that node type. Append a node with "/-". Which node sits at an index is only knowable from what is stored, so read it first: getDocument with "outline" answers with the pointer, type, "version" and a text excerpt for every node, which is far cheaper than reading the whole state.
|
|
28
29
|
|
|
29
|
-
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".
|
|
30
|
+
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". Inside a rich text field that substitution does not apply: a path there names the node type, and a block node its slug, where a pointer enters the stored state at "root" and walks "children" by an index counted over every child at that level, not over the blocks among them, with the node's own fields under "fields". So "/content/block/practice-note/variant" is written at "/content/root/children/7/fields/variant".
|
|
30
31
|
|
|
31
32
|
Fields Payload maintains (id, _status, createdAt, updatedAt) are never listed and cannot be written. Fields marked readOnly are listed but refused on write.`,
|
|
32
33
|
annotations: {
|
|
@@ -57,6 +58,8 @@ Fields Payload maintains (id, _status, createdAt, updatedAt) are never listed an
|
|
|
57
58
|
};
|
|
58
59
|
}
|
|
59
60
|
});
|
|
61
|
+
const nodeTypes = nodes.flatMap((node) => (node.fields ?? []).flatMap((field) => field.nodes ?? []));
|
|
62
|
+
if (nodeTypes.length > 0) nodes.push({ nodeProperties: nodePropertiesFor(nodeTypes) });
|
|
60
63
|
if (expanded?.truncated) nodes.push({ error: `Result truncated after ${String(400)} nodes. Request explicit paths instead.` });
|
|
61
64
|
return Promise.resolve(jsonResult(nodes));
|
|
62
65
|
}
|
|
@@ -14,7 +14,7 @@ Pass exactly one of "collection" and "global". "id" is required with "collection
|
|
|
14
14
|
|
|
15
15
|
${draftSentence(scope)}
|
|
16
16
|
|
|
17
|
-
Only the fields describeSchema lists can be addressed. A pointer that does not resolve is refused with the fields that are valid at that point, and nothing is applied unless every operation in the batch validates first. describeSchema reports field paths in this same pointer syntax; a path becomes a pointer into a document by replacing each "*" and each block slug with its 0-based index.
|
|
17
|
+
Only the fields describeSchema lists can be addressed. A pointer that does not resolve is refused with the fields that are valid at that point, and nothing is applied unless every operation in the batch validates first. describeSchema reports field paths in this same pointer syntax; a path becomes a pointer into a document by replacing each "*" and each block slug with its 0-based index. Inside a rich text field that substitution does not apply: a path there names the node type, and a block node its slug, where a pointer enters the stored state at "root" and walks "children" by an index counted over every child at that level, not over the blocks among them, with the node's own fields under "fields". So "/content/block/practice-note/variant" is written at "/content/root/children/7/fields/variant".
|
|
18
18
|
|
|
19
19
|
Adding a block requires "blockType" on the value. Append with "/-" as the last segment. To clear a field use "replace" with null; an array or blocks field refuses null and is emptied with [] instead. "remove" is only for list elements, because a field left out of a write is kept rather than cleared. Read the document first to learn the indices, and pass its "updatedAt" as expectedUpdatedAt so an edit made since that read is refused rather than overwritten.
|
|
20
20
|
|
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.17",
|
|
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",
|