@abinnovision/payloadcms-mcpx 1.0.0-beta.9 → 1.0.0
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 +33 -0
- package/README.md +328 -211
- package/dist/api-keys/fields.mjs +22 -5
- package/dist/api-keys/setup-guide.mjs +6 -4
- package/dist/auth/resolve.mjs +5 -7
- package/dist/capabilities.mjs +23 -4
- package/dist/client/index.d.mts +2 -2
- package/dist/client/setup-guide.d.mts +1 -1
- package/dist/endpoint/{result.mjs → errors.mjs} +4 -23
- package/dist/endpoint/handler.mjs +11 -5
- package/dist/endpoint/index.mjs +4 -0
- package/dist/endpoint/server.mjs +18 -26
- package/dist/i18n.mjs +4 -15
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +4 -3
- package/dist/options.mjs +30 -26
- package/dist/plugin.mjs +1 -0
- package/dist/{write/draft-guard.d.mts → request.d.mts} +2 -2
- package/dist/request.mjs +8 -0
- package/dist/result.d.mts +11 -0
- package/dist/result.mjs +20 -0
- package/dist/schema/describe.mjs +3 -15
- package/dist/schema/index.mjs +8 -0
- package/dist/schema/lexical-pointer.mjs +125 -0
- package/dist/schema/lexical.mjs +195 -27
- package/dist/schema/outline.mjs +67 -0
- package/dist/schema/pointer.mjs +77 -30
- package/dist/schema/shape.mjs +133 -51
- package/dist/schema/walk.mjs +44 -64
- package/dist/tools/{index.mjs → builtin.mjs} +8 -5
- package/dist/tools/create-document.mjs +34 -15
- package/dist/tools/describe-schema.mjs +21 -7
- package/dist/tools/find-documents.mjs +13 -6
- package/dist/tools/get-document.mjs +45 -11
- package/dist/tools/list-capabilities.mjs +19 -9
- package/dist/tools/names.mjs +2 -1
- package/dist/tools/patch-document.mjs +32 -21
- package/dist/tools/publish-document.mjs +79 -0
- package/dist/tools/shared.mjs +84 -32
- package/dist/tools/target.mjs +7 -11
- package/dist/tools/validate-document.mjs +20 -12
- package/dist/types.d.mts +110 -42
- package/dist/types.mjs +3 -4
- package/dist/version.mjs +1 -1
- package/dist/write/draft-guard.mjs +47 -44
- package/dist/write/patch.mjs +174 -92
- package/dist/write/publish-blockers.mjs +13 -12
- package/dist/write/publish-intent.mjs +17 -0
- package/dist/write/transaction.mjs +8 -3
- package/package.json +3 -3
- package/dist/i18n.d.mts +0 -1
- package/dist/options.d.mts +0 -2
- package/dist/schema/lexical.d.mts +0 -1
- package/dist/schema/walk.d.mts +0 -3
- package/dist/tools/target.d.mts +0 -3
- package/dist/tools/types.d.mts +0 -5
- package/dist/write/publish-blockers.d.mts +0 -15
package/dist/schema/shape.mjs
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
|
-
import { lexicalSubSchema } from "./lexical.mjs";
|
|
2
|
-
import { blockOf, blockSlugsOf, describeAddressableFields, findBlocksField, findRichTextField, splitPath } from "./walk.mjs";
|
|
1
|
+
import { ROOT_PROPERTIES, constrainsFields, lexicalSubSchema, nodeProblems, propertyProblem, rootProblems } from "./lexical.mjs";
|
|
2
|
+
import { blockOf, blockSlugsOf, describeAddressableFields, findBlocksField, findRichTextField, isPlainObject, splitPath } from "./walk.mjs";
|
|
3
3
|
//#region src/schema/shape.ts
|
|
4
|
-
|
|
5
|
-
* Keys Payload manages on a row that a client may echo back harmlessly.
|
|
6
|
-
*/ const TOLERATED_VALUE_KEYS = /* @__PURE__ */ new Set([
|
|
4
|
+
const TOLERATED_VALUE_KEYS = /* @__PURE__ */ new Set([
|
|
7
5
|
"blockName",
|
|
8
6
|
"blockType",
|
|
9
7
|
"id"
|
|
10
8
|
]);
|
|
11
|
-
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
9
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
|
|
10
|
+
* Lexical refuses to hydrate a state whose root holds nothing: `isEmpty` is a
|
|
11
|
+
* node map of one, and the editor throws on it rather than rendering nothing.
|
|
12
|
+
* An empty field is stored as null instead, so this is only ever reached by
|
|
13
|
+
* emptying one that was there.
|
|
14
|
+
*/ const EMPTY_ROOT = "an editor state needs at least one node. Clear the field with null instead.";
|
|
15
|
+
const quoted = (properties) => properties.map((property) => `"${property}"`).join(", ");
|
|
16
|
+
/**
|
|
17
|
+
* A node with nothing to declare, and one whose sub-fields cannot be named at a
|
|
18
|
+
* position, are both left alone.
|
|
18
19
|
*/ const checkNodeFields = (scope, field, node) => {
|
|
19
20
|
const sub = lexicalSubSchema(field, node.type);
|
|
20
21
|
if (!sub) return;
|
|
21
22
|
const data = node.fields;
|
|
22
23
|
if (!isPlainObject(data)) {
|
|
23
|
-
scope.problems.push(`${scope.pointer}: a "${node.type}" node carries a "fields" object.`);
|
|
24
|
+
if (!constrainsFields(node.type)) scope.problems.push(`${scope.pointer}: a "${node.type}" node carries a "fields" object.`);
|
|
24
25
|
return;
|
|
25
26
|
}
|
|
26
27
|
const nested = {
|
|
@@ -47,48 +48,126 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
|
|
|
47
48
|
}, data);
|
|
48
49
|
};
|
|
49
50
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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) => {
|
|
55
|
+
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
|
+
};
|
|
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.
|
|
52
61
|
*
|
|
53
|
-
* Payload does not: the Lexical validator runs node
|
|
54
|
-
* few node types that register one, so a `heading`
|
|
55
|
-
* editor has no heading feature is stored without
|
|
56
|
-
* later, at render or when the document is reopened
|
|
57
|
-
* a node's fields do not declare is dropped just as
|
|
58
|
-
* one level down, for the node properties a feature
|
|
59
|
-
* editor restricted to `h4` is stored as readily as an
|
|
60
|
-
|
|
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) => {
|
|
72
|
+
if (!isPlainObject(node) || typeof node["type"] !== "string") {
|
|
73
|
+
scope.problems.push(`${pointer}: every node needs a "type".`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const type = node["type"];
|
|
77
|
+
if (!editor.allowed.includes(type)) {
|
|
78
|
+
scope.problems.push(`${pointer}: "${type}" is not available in this field's editor. Allowed: ${editor.allowed.join(", ")}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const problems = nodeProblems(node);
|
|
82
|
+
if (problems.missing.length > 0) scope.problems.push(`${pointer}: a "${type}" node is missing ${quoted(problems.missing)}. Write nodes as Lexical serializes them.`);
|
|
83
|
+
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?.[type] ?? {})) checkNarrowedProperty(scope, {
|
|
85
|
+
pointer: `${pointer}/${property}`,
|
|
86
|
+
type,
|
|
87
|
+
values
|
|
88
|
+
}, node[property]);
|
|
89
|
+
if (editor.field) checkNodeFields({
|
|
90
|
+
...scope,
|
|
91
|
+
pointer
|
|
92
|
+
}, editor.field, {
|
|
93
|
+
fields: node["fields"],
|
|
94
|
+
type
|
|
95
|
+
});
|
|
96
|
+
checkNodes(scope, editor, node["children"], `${pointer}/children`);
|
|
97
|
+
};
|
|
98
|
+
/** `pointer` addresses the list. A node holding none is left alone. */ const checkNodes = (scope, editor, nodes, pointer) => {
|
|
99
|
+
if (!Array.isArray(nodes)) return;
|
|
100
|
+
nodes.forEach((node, index) => {
|
|
101
|
+
checkNode(scope, editor, node, `${pointer}/${String(index)}`);
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
const checkRichText = (scope, editor, value) => {
|
|
61
105
|
if (!isPlainObject(value) || !isPlainObject(value["root"])) {
|
|
62
106
|
scope.problems.push(`${scope.pointer}: expected a Lexical editor state with a "root".`);
|
|
63
107
|
return;
|
|
64
108
|
}
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
109
|
+
const root = value["root"];
|
|
110
|
+
const { missing, rejected, unexpected } = rootProblems(root);
|
|
111
|
+
if (missing.length > 0) scope.problems.push(`${scope.pointer}/root: the root node is missing ${quoted(missing)}. Write nodes as Lexical serializes them.`);
|
|
112
|
+
for (const problem of rejected) scope.problems.push(`${scope.pointer}/root/${problem.property}: the root node needs ${problem.needs} here.`);
|
|
113
|
+
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(", ")}`);
|
|
114
|
+
if (Array.isArray(root["children"]) && root["children"].length === 0) scope.problems.push(`${scope.pointer}/root/children: ${EMPTY_ROOT}`);
|
|
115
|
+
checkNodes(scope, editor, root["children"], `${scope.pointer}/root/children`);
|
|
116
|
+
};
|
|
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) => {
|
|
123
|
+
const { nodeType: type, property } = position;
|
|
124
|
+
if (property === "type") {
|
|
125
|
+
scope.problems.push(`${scope.pointer}: a node's "type" cannot be replaced on its own. Replace the whole node.`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (position.isRoot && !(property in ROOT_PROPERTIES)) {
|
|
129
|
+
scope.problems.push(`${scope.pointer}: no such property on the root node. Available: ${Object.keys(ROOT_PROPERTIES).join(", ")}`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const problem = propertyProblem(type, property, value);
|
|
133
|
+
if (problem) scope.problems.push(`${scope.pointer}: a "${type}" node needs ${problem.needs} here.`);
|
|
134
|
+
const values = editor.nodeOptions?.[type]?.[property];
|
|
135
|
+
if (values) checkNarrowedProperty(scope, {
|
|
136
|
+
pointer: scope.pointer,
|
|
137
|
+
type,
|
|
138
|
+
values
|
|
139
|
+
}, value);
|
|
140
|
+
};
|
|
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) => {
|
|
145
|
+
const editor = {
|
|
146
|
+
allowed: position.descriptor.nodes ?? [],
|
|
147
|
+
field: position.field,
|
|
148
|
+
nodeOptions: position.descriptor.nodeOptions
|
|
90
149
|
};
|
|
91
|
-
|
|
150
|
+
if (position.kind === "property") {
|
|
151
|
+
checkNodeProperty(scope, editor, position, value);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (position.kind === "nodes") {
|
|
155
|
+
if (!Array.isArray(value)) {
|
|
156
|
+
scope.problems.push(`${scope.pointer}: expected an array of nodes.`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (position.isRoot && value.length === 0) {
|
|
160
|
+
scope.problems.push(`${scope.pointer}: ${EMPTY_ROOT}`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
checkNodes(scope, editor, value, scope.pointer);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (position.isRoot) {
|
|
167
|
+
scope.problems.push(`${scope.pointer}: the root of an editor state cannot be replaced on its own. Write the whole field instead.`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
checkNode(scope, editor, value, scope.pointer);
|
|
92
171
|
};
|
|
93
172
|
const checkLeafValue = (scope, descriptor, value) => {
|
|
94
173
|
if (descriptor.readOnly) {
|
|
@@ -126,8 +205,7 @@ const checkLeafValue = (scope, descriptor, value) => {
|
|
|
126
205
|
});
|
|
127
206
|
};
|
|
128
207
|
/**
|
|
129
|
-
*
|
|
130
|
-
* rather than the first.
|
|
208
|
+
* Reports every shape problem rather than the first.
|
|
131
209
|
*
|
|
132
210
|
* Shape only: unknown field names, unknown block slugs, read-only fields, and
|
|
133
211
|
* rich text nodes or node properties the field's editor cannot produce.
|
|
@@ -197,6 +275,10 @@ const checkLeafValue = (scope, descriptor, value) => {
|
|
|
197
275
|
prefix: target.resolution.prefix,
|
|
198
276
|
problems
|
|
199
277
|
};
|
|
278
|
+
if (target.resolution.lexical) {
|
|
279
|
+
checkLexicalWrite(scope, target.resolution.lexical, value);
|
|
280
|
+
return problems;
|
|
281
|
+
}
|
|
200
282
|
if (target.resolution.descriptor) {
|
|
201
283
|
checkLeafValue(scope, target.resolution.descriptor, value);
|
|
202
284
|
return problems;
|
|
@@ -205,4 +287,4 @@ const checkLeafValue = (scope, descriptor, value) => {
|
|
|
205
287
|
return problems;
|
|
206
288
|
};
|
|
207
289
|
//#endregion
|
|
208
|
-
export { validateWriteValue };
|
|
290
|
+
export { EMPTY_ROOT, validateWriteValue };
|
package/dist/schema/walk.mjs
CHANGED
|
@@ -2,50 +2,39 @@ import { allowedNodeTypes, nodeOptions } from "./lexical.mjs";
|
|
|
2
2
|
import { translateAny } from "../i18n.mjs";
|
|
3
3
|
import { fieldIsHiddenOrDisabled, fieldIsVirtual } from "payload/shared";
|
|
4
4
|
//#region src/schema/walk.ts
|
|
5
|
-
|
|
6
|
-
* Fields Payload maintains, which a client may neither address nor supply.
|
|
7
|
-
*/ const RESERVED_FIELD_NAMES = /* @__PURE__ */ new Set([
|
|
5
|
+
const RESERVED_FIELD_NAMES = /* @__PURE__ */ new Set([
|
|
8
6
|
"_status",
|
|
9
7
|
"createdAt",
|
|
10
8
|
"deletedAt",
|
|
11
9
|
"id",
|
|
12
10
|
"updatedAt"
|
|
13
11
|
]);
|
|
12
|
+
const JSON_POINTER_PATTERN = /^(\/([^~/]|~[01])*)*$/;
|
|
13
|
+
/** No segments is the root pointer, `""`. */ const joinPath = (parts) => parts.map((part) => `/${part.replace(/~/g, "~0").replace(/\//g, "~1")}`).join("");
|
|
14
|
+
/** 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, "~"));
|
|
15
|
+
/** `-` included, since RFC 6901 reads it as the position after the last. */ const isIndexSegment = (segment) => segment === "-" || /^\d+$/.test(segment);
|
|
16
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
17
|
/**
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* Joins segments into a JSON Pointer, so the segments `items`, `*`, `title`
|
|
19
|
-
* read as one path to a subfield of every element of `items`. No segments is
|
|
20
|
-
* the root pointer, `""`.
|
|
21
|
-
*/ const joinPath = (parts) => parts.map((part) => `/${part.replace(/~/g, "~0").replace(/\//g, "~1")}`).join("");
|
|
22
|
-
/**
|
|
23
|
-
* Splits a JSON Pointer into its segments, unescaping `~1` and `~0`. The root
|
|
24
|
-
* pointer yields no segments.
|
|
25
|
-
*/ const splitPath = (path) => path.split("/").slice(1).map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
26
|
-
/**
|
|
27
|
-
* Restates a path Payload reports on a validation error (`layout.0.title`) as
|
|
28
|
-
* a JSON Pointer, so everything this plugin hands back addresses documents the
|
|
29
|
-
* same way. Payload's path already carries real indices, so it maps directly.
|
|
18
|
+
* A path Payload reports on a validation error (`layout.0.title`) as a JSON
|
|
19
|
+
* Pointer, so everything handed back addresses documents the same way. The
|
|
20
|
+
* path already carries real indices, so it maps directly.
|
|
30
21
|
*/ const pointerFromPayloadPath = (path) => path ? joinPath(path.split(".")) : "";
|
|
22
|
+
/** On a flattened field, whichever of `blockReferences` and `blocks` was declared. */ const blockSlugsOf = (field) => [...new Set((field.blockReferences ?? field.blocks).map((block) => typeof block === "string" ? block : block.slug))];
|
|
31
23
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Resolves one of a blocks field's slugs to its definition.
|
|
37
|
-
*
|
|
38
|
-
* A definition inlined on the field wins over the shared registry. A block's
|
|
39
|
-
* own fields are identical wherever it appears, but the blocks its children
|
|
40
|
-
* accept are not, so an inline definition has to be read at its position.
|
|
41
|
-
* The registry (`config.blocks`) is the fallback for slugs referenced by name.
|
|
24
|
+
* An inlined definition wins over the registry (`config.blocks`). A block's own
|
|
25
|
+
* fields are identical wherever it appears, but the blocks its children accept
|
|
26
|
+
* are not, so an inline definition has to be read at its position.
|
|
42
27
|
*/ const blockOf = (config, field, slug) => {
|
|
43
28
|
const declared = field.blockReferences ?? field.blocks;
|
|
44
29
|
const inline = declared.find((block) => typeof block !== "string" && block.slug === slug);
|
|
45
30
|
if (inline) return inline;
|
|
46
31
|
return declared.includes(slug) ? config.blocks?.find((block) => block.slug === slug) : void 0;
|
|
47
32
|
};
|
|
48
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Payload's `fieldIsHiddenOrDisabled` reads `hidden` and `admin.disabled`, not
|
|
35
|
+
* `admin.hidden`, which is what its own upload base fields carry.
|
|
36
|
+
*/ const isAdminHidden = (field) => "admin" in field && field.admin.hidden === true;
|
|
37
|
+
/** A field kept out of the admin panel is kept out of the MCP surface too. */ const isSkipped = (field) => !("name" in field) || field.type === "join" || RESERVED_FIELD_NAMES.has(field.name) || fieldIsVirtual(field) || isAdminHidden(field) || fieldIsHiddenOrDisabled(field);
|
|
49
38
|
const isReadOnly = (field) => "admin" in field && field.admin.readOnly === true;
|
|
50
39
|
const describeBase = (field, { path, readOnly, translate }) => {
|
|
51
40
|
const description = translate("admin" in field ? field.admin.description : void 0);
|
|
@@ -80,11 +69,9 @@ const withRows = (descriptor, field) => ({
|
|
|
80
69
|
...field.maxRows === void 0 ? {} : { maxRows: field.maxRows }
|
|
81
70
|
});
|
|
82
71
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* path to something writable skips them; only {@link describeNode} reports
|
|
87
|
-
* them, to carry what the container itself declares.
|
|
72
|
+
* A container describes a position rather than a value, so everything resolving
|
|
73
|
+
* a path to something writable skips it. Only {@link nodeDescriber} reports one,
|
|
74
|
+
* to carry what the container itself declares.
|
|
88
75
|
*/ const isContainer = (descriptor) => descriptor.type === "array" || descriptor.type === "group" || descriptor.type === "tab";
|
|
89
76
|
/**
|
|
90
77
|
* Whether a container declares anything a client could not infer from the
|
|
@@ -93,20 +80,16 @@ const withRows = (descriptor, field) => ({
|
|
|
93
80
|
/**
|
|
94
81
|
* Flattens a field list into descriptors addressed relative to the node.
|
|
95
82
|
*
|
|
96
|
-
* The input is Payload's own flattened shape,
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* allows rather than to the size of their definitions.
|
|
83
|
+
* The input is Payload's own flattened shape, so the admin-only constructs
|
|
84
|
+
* (unnamed tabs and groups, `row`, `collapsible`, `ui`) are already gone. Named
|
|
85
|
+
* tabs, groups and arrays contribute a path segment, and are described in their
|
|
86
|
+
* own right when they declare something of their own: an array always, since
|
|
87
|
+
* its row counts live nowhere else, a group or tab only when it carries a
|
|
88
|
+
* description or a constraint. The walk stops at every blocks field and names
|
|
89
|
+
* the slugs, which keeps a node proportional to the number of blocks it allows
|
|
90
|
+
* rather than to the size of their definitions.
|
|
105
91
|
*
|
|
106
|
-
* `translate`
|
|
107
|
-
* Callers that walk for paths alone leave it out and get the language-agnostic
|
|
108
|
-
* default, so a missing argument costs language selection, never the
|
|
109
|
-
* description itself.
|
|
92
|
+
* Omitting `translate` costs language selection, never the description itself.
|
|
110
93
|
*/ const describeFields = (fields, translate = translateAny) => {
|
|
111
94
|
const walk = (current, prefix, parentReadOnly) => current.flatMap((field) => {
|
|
112
95
|
if (isSkipped(field)) return [];
|
|
@@ -133,33 +116,30 @@ const withRows = (descriptor, field) => ({
|
|
|
133
116
|
/**
|
|
134
117
|
* The descriptors that address a value, which is what every walk resolving a
|
|
135
118
|
* path against a document needs. A container describes a position rather than
|
|
136
|
-
* a value, so only {@link
|
|
119
|
+
* a value, so only {@link nodeDescriber} reports one.
|
|
137
120
|
*/ const describeAddressableFields = (fields) => describeFields(fields).filter((descriptor) => !isContainer(descriptor));
|
|
138
|
-
|
|
139
|
-
* Locates the blocks field that a resolved descriptor path refers to.
|
|
140
|
-
*/ const findBlocksField = (fields, path) => {
|
|
121
|
+
const findFieldAt = (fields, path, type) => {
|
|
141
122
|
for (const field of fields) {
|
|
142
123
|
if (!("name" in field) || field.name !== path[0]) continue;
|
|
143
|
-
if (field.type ===
|
|
144
|
-
if (field.type === "tab" || field.type === "group") return
|
|
145
|
-
if (field.type === "array" && path[1] === "*") return
|
|
124
|
+
if (field.type === type && path.length === 1) return field;
|
|
125
|
+
if (field.type === "tab" || field.type === "group") return findFieldAt(field.flattenedFields, path.slice(1), type);
|
|
126
|
+
if (field.type === "array" && path[1] === "*") return findFieldAt(field.flattenedFields, path.slice(2), type);
|
|
146
127
|
}
|
|
147
128
|
};
|
|
129
|
+
const findBlocksField = (fields, path) => findFieldAt(fields, path, "blocks");
|
|
148
130
|
/**
|
|
149
131
|
* Locates the rich text field that a resolved descriptor path refers to, so
|
|
150
132
|
* its editor can be introspected for the fields its nodes carry.
|
|
151
|
-
*/ const findRichTextField = (fields, path) =>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
};
|
|
159
|
-
const targetOf = (config, ref) => {
|
|
133
|
+
*/ const findRichTextField = (fields, path) => findFieldAt(fields, path, "richText");
|
|
134
|
+
/**
|
|
135
|
+
* Looks up the sanitized config for a collection or global, as a
|
|
136
|
+
* {@link SchemaTarget}. Throws on an unknown slug rather than returning
|
|
137
|
+
* undefined, because a reference reaching here has already been checked against
|
|
138
|
+
* the key's capabilities and a miss means the config changed underneath it.
|
|
139
|
+
*/ const targetOf = (config, ref) => {
|
|
160
140
|
const found = ref.kind === "collection" ? config.collections.find((candidate) => candidate.slug === ref.slug) : config.globals.find((candidate) => candidate.slug === ref.slug);
|
|
161
141
|
if (!found) throw new Error(`Unknown ${ref.kind} "${ref.slug}".`);
|
|
162
142
|
return found;
|
|
163
143
|
};
|
|
164
144
|
//#endregion
|
|
165
|
-
export { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, blockSlugsOf, describeAddressableFields, describeFields, findBlocksField, findRichTextField, joinPath, pointerFromPayloadPath, splitPath, targetOf };
|
|
145
|
+
export { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, blockSlugsOf, describeAddressableFields, describeFields, findBlocksField, findRichTextField, isIndexSegment, isPlainObject, joinPath, pointerFromPayloadPath, splitPath, targetOf };
|
|
@@ -4,12 +4,14 @@ import { findDocuments } from "./find-documents.mjs";
|
|
|
4
4
|
import { getDocument } from "./get-document.mjs";
|
|
5
5
|
import { listCapabilities } from "./list-capabilities.mjs";
|
|
6
6
|
import { patchDocument } from "./patch-document.mjs";
|
|
7
|
+
import { publishDocument } from "./publish-document.mjs";
|
|
7
8
|
import { validateDocument } from "./validate-document.mjs";
|
|
8
|
-
//#region src/tools/
|
|
9
|
+
//#region src/tools/builtin.ts
|
|
9
10
|
/**
|
|
10
|
-
* The builtin tools in registration order.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* The builtin tools, in registration order. Fixed: adding a collection, block
|
|
12
|
+
* or field never changes the surface. They differ from a custom tool only in
|
|
13
|
+
* `isEnabled`, which derives from the key's capabilities rather than a
|
|
14
|
+
* checkbox of their own.
|
|
13
15
|
*/ const BUILTIN_TOOLS = [
|
|
14
16
|
listCapabilities,
|
|
15
17
|
describeSchema,
|
|
@@ -17,7 +19,8 @@ import { validateDocument } from "./validate-document.mjs";
|
|
|
17
19
|
getDocument,
|
|
18
20
|
patchDocument,
|
|
19
21
|
createDocument,
|
|
20
|
-
validateDocument
|
|
22
|
+
validateDocument,
|
|
23
|
+
publishDocument
|
|
21
24
|
];
|
|
22
25
|
//#endregion
|
|
23
26
|
export { BUILTIN_TOOLS };
|
|
@@ -1,45 +1,63 @@
|
|
|
1
|
-
import { errorResult, jsonResult } from "../
|
|
2
|
-
import { localeOf, localeShape, readTarget, slugEnum } from "./shared.mjs";
|
|
3
|
-
import { resolveTarget } from "./target.mjs";
|
|
1
|
+
import { errorResult, jsonResult } from "../result.mjs";
|
|
4
2
|
import { validateWriteValue } from "../schema/shape.mjs";
|
|
3
|
+
import "../schema/index.mjs";
|
|
4
|
+
import { draftSentence, localeOf, localeShape, patchOnlySlugs, readTarget, slugEnum, slugsFor } from "./shared.mjs";
|
|
5
|
+
import { resolveTarget } from "./target.mjs";
|
|
6
|
+
import { defineMcpxTool } from "../types.mjs";
|
|
5
7
|
import { stripRowIds } from "../write/patch.mjs";
|
|
6
8
|
import { collectPublishBlockers } from "../write/publish-blockers.mjs";
|
|
7
9
|
import { z } from "zod";
|
|
8
10
|
//#region src/tools/create-document.ts
|
|
9
|
-
const
|
|
11
|
+
/** Names the writable slugs this tool leaves out, so the gap reads as intent. */ const uploadSentence = (scope) => {
|
|
12
|
+
const slugs = patchOnlySlugs(scope);
|
|
13
|
+
return slugs.length === 0 ? "" : `\n\nLeft out of "collection" on purpose: ${slugs.join(", ")}. Those documents are files, and no tool here carries one. Upload the file in the admin panel, then edit its fields with patchDocument.`;
|
|
14
|
+
};
|
|
15
|
+
const DESCRIPTION = (scope) => `Creates a new document from a minimal seed. Only the fields describeSchema lists may appear in "data"; unknown keys are refused with the valid siblings, and "id" is Payload's to assign. The document may be incomplete: the response lists "publishBlockers", which patchDocument can then work through, and "publishBlockersUnavailable" when that check itself failed. Use this when no document exists yet; prefer patching an existing draft otherwise.
|
|
16
|
+
|
|
17
|
+
${draftSentence(scope)}${uploadSentence(scope)}`;
|
|
18
|
+
/**
|
|
19
|
+
* Collection-only, because a global always exists, and never reaches an upload
|
|
20
|
+
* collection, because a create there would have to carry the file.
|
|
21
|
+
*
|
|
22
|
+
* The seed is checked against the collection's fields before the create, so an
|
|
23
|
+
* unknown key is refused with its valid siblings rather than dropped. Row ids
|
|
24
|
+
* in the seed are stripped and a top-level `id` is refused outright. The new
|
|
25
|
+
* document is re-read privileged afterwards to collect publish blockers, which
|
|
26
|
+
* is why an incomplete seed still succeeds and comes back with a checklist.
|
|
27
|
+
*/ const createDocument = defineMcpxTool({
|
|
10
28
|
name: "createDocument",
|
|
11
|
-
description:
|
|
29
|
+
description: DESCRIPTION,
|
|
12
30
|
annotations: {
|
|
13
31
|
readOnlyHint: false,
|
|
14
32
|
destructiveHint: false,
|
|
15
33
|
idempotentHint: false,
|
|
16
34
|
openWorldHint: false
|
|
17
35
|
},
|
|
18
|
-
isEnabled: (scope) => scope.
|
|
36
|
+
isEnabled: (scope) => slugsFor(scope, "create").collections.length > 0,
|
|
19
37
|
inputSchema: (scope) => ({
|
|
20
|
-
collection: slugEnum(scope.
|
|
38
|
+
collection: slugEnum(slugsFor(scope, "create").collections).describe("Collection to create the document in."),
|
|
21
39
|
...localeShape(scope, {
|
|
22
40
|
required: true,
|
|
23
41
|
description: "Locale the localized fields of the seed belong to."
|
|
24
42
|
}),
|
|
25
43
|
data: z.record(z.string(), z.unknown()).describe("Initial field values, as describeSchema lists them.")
|
|
26
44
|
}),
|
|
27
|
-
handler: async (args, scope) => {
|
|
28
|
-
const target = resolveTarget(scope, { collection: args.collection }, "
|
|
45
|
+
handler: async ({ args, scope }) => {
|
|
46
|
+
const target = resolveTarget(scope, { collection: args.collection }, "create");
|
|
29
47
|
const { payload } = scope.req;
|
|
30
48
|
const locale = localeOf(scope, args.locale);
|
|
31
|
-
|
|
49
|
+
if ("id" in args.data) return errorResult("Nothing was created.", { problems: ["/id: Payload assigns the id; it cannot be supplied."] });
|
|
32
50
|
const problems = validateWriteValue(payload.config, {
|
|
33
51
|
pointer: "",
|
|
34
52
|
resolution: {
|
|
35
53
|
fields: target.config.flattenedFields,
|
|
36
54
|
prefix: []
|
|
37
55
|
}
|
|
38
|
-
},
|
|
56
|
+
}, args.data);
|
|
39
57
|
if (problems.length > 0) return errorResult("Nothing was created.", { problems });
|
|
40
58
|
const created = await payload.create({
|
|
41
59
|
collection: args.collection,
|
|
42
|
-
data: stripRowIds(
|
|
60
|
+
data: stripRowIds(args.data),
|
|
43
61
|
depth: 0,
|
|
44
62
|
draft: true,
|
|
45
63
|
overrideAccess: false,
|
|
@@ -52,7 +70,7 @@ const createDocument = {
|
|
|
52
70
|
locale,
|
|
53
71
|
privileged: true
|
|
54
72
|
});
|
|
55
|
-
const
|
|
73
|
+
const validation = await collectPublishBlockers(scope.req, {
|
|
56
74
|
doc: saved,
|
|
57
75
|
entity: target
|
|
58
76
|
});
|
|
@@ -60,9 +78,10 @@ const createDocument = {
|
|
|
60
78
|
id: saved["id"],
|
|
61
79
|
status: saved["_status"],
|
|
62
80
|
updatedAt: saved["updatedAt"],
|
|
63
|
-
...
|
|
81
|
+
...validation.blockers.length > 0 ? { publishBlockers: validation.blockers } : {},
|
|
82
|
+
...validation.unavailable ? { publishBlockersUnavailable: true } : {}
|
|
64
83
|
});
|
|
65
84
|
}
|
|
66
|
-
};
|
|
85
|
+
});
|
|
67
86
|
//#endregion
|
|
68
87
|
export { createDocument };
|
|
@@ -1,11 +1,19 @@
|
|
|
1
|
+
import { jsonResult } from "../result.mjs";
|
|
2
|
+
import { nodePropertiesFor } from "../schema/lexical.mjs";
|
|
1
3
|
import { translatorFor } from "../i18n.mjs";
|
|
2
|
-
import {
|
|
4
|
+
import { nodeDescriber, reachableSchemaPaths } from "../schema/describe.mjs";
|
|
5
|
+
import "../schema/index.mjs";
|
|
3
6
|
import { targetShape } from "./shared.mjs";
|
|
4
7
|
import { refOf, resolveTarget } from "./target.mjs";
|
|
5
|
-
import {
|
|
8
|
+
import { defineMcpxTool } from "../types.mjs";
|
|
6
9
|
import { z } from "zod";
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Describes each requested path independently and returns a per
|
|
12
|
+
* path error object instead of failing the call, so a client exploring several
|
|
13
|
+
* branches at once keeps the nodes that did resolve. `expand` swaps the
|
|
14
|
+
* requested paths for every node reachable from the root and appends a
|
|
15
|
+
* truncation notice past {@link REACHABLE_PATHS_LIMIT}.
|
|
16
|
+
*/ const describeSchema = defineMcpxTool({
|
|
9
17
|
name: "describeSchema",
|
|
10
18
|
description: `Describes the writable shape of a document, one node at a time.
|
|
11
19
|
|
|
@@ -15,7 +23,11 @@ Call it with no "paths" to get a collection's own fields. Every "blocks" field s
|
|
|
15
23
|
|
|
16
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.
|
|
17
25
|
|
|
18
|
-
|
|
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.
|
|
27
|
+
|
|
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.
|
|
29
|
+
|
|
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".
|
|
19
31
|
|
|
20
32
|
Fields Payload maintains (id, _status, createdAt, updatedAt) are never listed and cannot be written. Fields marked readOnly are listed but refused on write.`,
|
|
21
33
|
annotations: {
|
|
@@ -31,7 +43,7 @@ Fields Payload maintains (id, _status, createdAt, updatedAt) are never listed an
|
|
|
31
43
|
paths: z.array(z.string()).optional().describe("Schema paths to describe, e.g. \"/layout/sections/sectionWrapper\". Omit for the collection root."),
|
|
32
44
|
expand: z.boolean().optional().describe("Return every node reachable from the root in one response. Ignores paths.")
|
|
33
45
|
}),
|
|
34
|
-
handler: (args, scope) => {
|
|
46
|
+
handler: ({ args, scope }) => {
|
|
35
47
|
const ref = refOf(resolveTarget(scope, args, "read"));
|
|
36
48
|
const { config } = scope.req.payload;
|
|
37
49
|
const describeNode = nodeDescriber(translatorFor(scope.req.i18n));
|
|
@@ -46,9 +58,11 @@ Fields Payload maintains (id, _status, createdAt, updatedAt) are never listed an
|
|
|
46
58
|
};
|
|
47
59
|
}
|
|
48
60
|
});
|
|
61
|
+
const nodeTypes = nodes.flatMap((node) => (node.fields ?? []).flatMap((field) => field.nodes ?? []));
|
|
62
|
+
if (nodeTypes.length > 0) nodes.push({ nodeProperties: nodePropertiesFor(nodeTypes) });
|
|
49
63
|
if (expanded?.truncated) nodes.push({ error: `Result truncated after ${String(400)} nodes. Request explicit paths instead.` });
|
|
50
64
|
return Promise.resolve(jsonResult(nodes));
|
|
51
65
|
}
|
|
52
|
-
};
|
|
66
|
+
});
|
|
53
67
|
//#endregion
|
|
54
68
|
export { describeSchema };
|