@abinnovision/payloadcms-mcpx 1.1.0-beta.6 → 2.0.0-beta.12
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 +144 -470
- package/dist/admin/capability-matrix.d.mts +33 -0
- package/dist/admin/capability-matrix.mjs +354 -0
- package/dist/admin/capability-toggles.mjs +73 -0
- package/dist/{client → admin}/setup-guide.d.mts +4 -2
- package/dist/{client → admin}/setup-guide.mjs +3 -10
- package/dist/api-keys/capability-matrix.d.mts +12 -8
- package/dist/api-keys/capability-matrix.mjs +14 -86
- package/dist/api-keys/collection.mjs +2 -1
- package/dist/api-keys/fields.mjs +36 -24
- package/dist/auth/resolve.mjs +71 -23
- package/dist/{tools/names.mjs → builtin-tool-names.mjs} +1 -1
- package/dist/capabilities.mjs +44 -46
- package/dist/define-tool.d.mts +23 -0
- package/dist/{types.mjs → define-tool.mjs} +1 -1
- package/dist/endpoint/errors.mjs +5 -6
- package/dist/endpoint/handler.mjs +73 -29
- package/dist/endpoint/server.mjs +85 -22
- package/dist/endpoint/upload.mjs +135 -0
- package/dist/entity.d.mts +9 -0
- package/dist/guards.mjs +7 -0
- package/dist/i18n.mjs +10 -8
- package/dist/index.d.mts +5 -2
- package/dist/index.mjs +3 -2
- package/dist/options.mjs +54 -47
- package/dist/plugin.d.mts +2 -2
- package/dist/plugin.mjs +13 -3
- 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 +44 -93
- package/dist/schema/outline.mjs +5 -15
- package/dist/schema/path.mjs +28 -0
- package/dist/schema/pointer.mjs +18 -25
- package/dist/schema/shape.mjs +25 -71
- package/dist/schema/walk.mjs +43 -45
- package/dist/tools/builtin.mjs +4 -4
- package/dist/tools/create-document.mjs +50 -24
- package/dist/tools/describe-schema.mjs +38 -34
- package/dist/tools/document.mjs +124 -0
- package/dist/tools/find-documents.mjs +32 -19
- package/dist/tools/find-versions.mjs +22 -18
- package/dist/tools/get-document.mjs +60 -33
- package/dist/tools/list-capabilities.mjs +35 -19
- package/dist/tools/patch-document.mjs +122 -66
- package/dist/tools/publish-document.mjs +26 -32
- package/dist/tools/query-paths.mjs +57 -0
- package/dist/tools/read-request.d.mts +10 -0
- package/dist/tools/read-request.mjs +43 -0
- package/dist/tools/shared.mjs +74 -105
- package/dist/tools/validate-document.mjs +18 -30
- package/dist/tools/versions.mjs +37 -20
- package/dist/types.d.mts +135 -75
- package/dist/upload/file.mjs +166 -0
- package/dist/upload/grant.mjs +96 -0
- package/dist/version.mjs +3 -1
- package/dist/write/draft-guard.mjs +23 -41
- package/dist/write/patch.mjs +27 -183
- package/dist/write/publish-blockers.mjs +7 -14
- package/dist/write/publish-intent.mjs +6 -4
- package/dist/write/row-ids.mjs +46 -0
- package/dist/write/transaction.mjs +5 -8
- package/dist/write/write-data.mjs +64 -0
- package/package.json +11 -10
- package/dist/client/capability-matrix.d.mts +0 -26
- 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/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
|
+
const validateWriteValue = (config, target, value) => {
|
|
270
224
|
const problems = [];
|
|
271
225
|
const scope = {
|
|
272
226
|
config,
|
package/dist/schema/walk.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { allowedNodeTypes, nodeOptions } from "./lexical.mjs";
|
|
2
|
+
import { joinPath, splitPath } from "./path.mjs";
|
|
2
3
|
import { translateAny } from "../i18n.mjs";
|
|
3
4
|
import { fieldIsHiddenOrDisabled, fieldIsVirtual } from "payload/shared";
|
|
4
5
|
//#region src/schema/walk.ts
|
|
@@ -9,21 +10,21 @@ const RESERVED_FIELD_NAMES = /* @__PURE__ */ new Set([
|
|
|
9
10
|
"id",
|
|
10
11
|
"updatedAt"
|
|
11
12
|
]);
|
|
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);
|
|
17
13
|
/**
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
* The keys a row carries beside its fields.
|
|
15
|
+
*/ const ROW_KEYS = /* @__PURE__ */ new Set([
|
|
16
|
+
"blockName",
|
|
17
|
+
"blockType",
|
|
18
|
+
"id"
|
|
19
|
+
]);
|
|
20
|
+
/**
|
|
21
|
+
* On a flattened field, whichever of `blockReferences` and `blocks` was
|
|
22
|
+
* declared.
|
|
23
|
+
*/ const blockSlugsOf = (field) => [...new Set((field.blockReferences ?? field.blocks).map((block) => typeof block === "string" ? block : block.slug))];
|
|
23
24
|
/**
|
|
24
25
|
* An inlined definition wins over the registry (`config.blocks`). A block's own
|
|
25
26
|
* fields are identical wherever it appears, but the blocks its children accept
|
|
26
|
-
* are not, so an inline definition
|
|
27
|
+
* are not, so an inline definition must be read at its position.
|
|
27
28
|
*/ const blockOf = (config, field, slug) => {
|
|
28
29
|
const declared = field.blockReferences ?? field.blocks;
|
|
29
30
|
const inline = declared.find((block) => typeof block !== "string" && block.slug === slug);
|
|
@@ -32,9 +33,9 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
|
|
|
32
33
|
};
|
|
33
34
|
/**
|
|
34
35
|
* Payload's `fieldIsHiddenOrDisabled` reads `hidden` and `admin.disabled`, not
|
|
35
|
-
* `admin.hidden`, which
|
|
36
|
+
* `admin.hidden`, which its own upload base fields carry.
|
|
36
37
|
*/ const isAdminHidden = (field) => "admin" in field && field.admin.hidden === true;
|
|
37
|
-
|
|
38
|
+
const isSkipped = (field) => !("name" in field) || field.type === "join" || RESERVED_FIELD_NAMES.has(field.name) || fieldIsVirtual(field) || isAdminHidden(field) || fieldIsHiddenOrDisabled(field);
|
|
38
39
|
const isReadOnly = (field) => "admin" in field && field.admin.readOnly === true;
|
|
39
40
|
const describeBase = (field, { path, readOnly, translate }) => {
|
|
40
41
|
const description = translate("admin" in field ? field.admin.description : void 0);
|
|
@@ -68,28 +69,15 @@ const withRows = (descriptor, field) => ({
|
|
|
68
69
|
...field.minRows === void 0 ? {} : { minRows: field.minRows },
|
|
69
70
|
...field.maxRows === void 0 ? {} : { maxRows: field.maxRows }
|
|
70
71
|
});
|
|
72
|
+
const isContainer = (descriptor) => descriptor.type === "array" || descriptor.type === "group" || descriptor.type === "tab";
|
|
73
|
+
const isInformative = (descriptor) => descriptor.description !== void 0 || descriptor.required === true || descriptor.localized === true;
|
|
71
74
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
*
|
|
78
|
-
* fields beneath it. A group that exists only to nest is not worth reporting.
|
|
79
|
-
*/ const isInformative = (descriptor) => descriptor.description !== void 0 || descriptor.required === true || descriptor.localized === true;
|
|
80
|
-
/**
|
|
81
|
-
* Flattens a field list into descriptors addressed relative to the node.
|
|
82
|
-
*
|
|
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.
|
|
91
|
-
*
|
|
92
|
-
* Omitting `translate` costs language selection, never the description itself.
|
|
75
|
+
* Named tabs, groups and arrays add a path segment and are described
|
|
76
|
+
* themselves only when they declare something of their own: an array always,
|
|
77
|
+
* since its row counts live nowhere else. The walk stops at every blocks field
|
|
78
|
+
* and names the slugs, so a node's size follows the number of blocks it
|
|
79
|
+
* allows, not their definitions. Omitting `translate` costs language
|
|
80
|
+
* selection, never the description itself.
|
|
93
81
|
*/ const describeFields = (fields, translate = translateAny) => {
|
|
94
82
|
const walk = (current, prefix, parentReadOnly) => current.flatMap((field) => {
|
|
95
83
|
if (isSkipped(field)) return [];
|
|
@@ -114,10 +102,20 @@ const withRows = (descriptor, field) => ({
|
|
|
114
102
|
return walk(fields, [], false);
|
|
115
103
|
};
|
|
116
104
|
/**
|
|
117
|
-
* The descriptors that address a value, which
|
|
118
|
-
*
|
|
119
|
-
*
|
|
105
|
+
* The descriptors that address a value, which every walk resolving a path
|
|
106
|
+
* against a document needs. Containers are left out, since only
|
|
107
|
+
* {@link nodeDescriber} reports one.
|
|
120
108
|
*/ const describeAddressableFields = (fields) => describeFields(fields).filter((descriptor) => !isContainer(descriptor));
|
|
109
|
+
/**
|
|
110
|
+
* The addressable descriptors under `prefix`, with their paths split relative
|
|
111
|
+
* to it.
|
|
112
|
+
*/ const descriptorsUnder = (fields, prefix) => describeAddressableFields(fields).flatMap((descriptor) => {
|
|
113
|
+
const parts = splitPath(descriptor.path);
|
|
114
|
+
return prefix.every((part, offset) => part === parts[offset]) ? [{
|
|
115
|
+
descriptor,
|
|
116
|
+
parts: parts.slice(prefix.length)
|
|
117
|
+
}] : [];
|
|
118
|
+
});
|
|
121
119
|
const findFieldAt = (fields, path, type) => {
|
|
122
120
|
for (const field of fields) {
|
|
123
121
|
if (!("name" in field) || field.name !== path[0]) continue;
|
|
@@ -128,18 +126,18 @@ const findFieldAt = (fields, path, type) => {
|
|
|
128
126
|
};
|
|
129
127
|
const findBlocksField = (fields, path) => findFieldAt(fields, path, "blocks");
|
|
130
128
|
/**
|
|
131
|
-
* Locates the rich text field
|
|
132
|
-
*
|
|
129
|
+
* Locates the rich text field a resolved descriptor path refers to, so its
|
|
130
|
+
* editor can be introspected for the fields its nodes carry.
|
|
133
131
|
*/ const findRichTextField = (fields, path) => findFieldAt(fields, path, "richText");
|
|
134
132
|
/**
|
|
135
|
-
* Looks up the sanitized config for a collection or global
|
|
136
|
-
* {@link
|
|
137
|
-
* undefined
|
|
138
|
-
*
|
|
139
|
-
*/ const
|
|
133
|
+
* Looks up the sanitized config for a collection or global as an
|
|
134
|
+
* {@link EntitySchema}. Throws on an unknown slug instead of returning
|
|
135
|
+
* undefined: a reference reaching here was already checked against the key's
|
|
136
|
+
* capabilities, so a miss means the config changed underneath it.
|
|
137
|
+
*/ const schemaOf = (config, ref) => {
|
|
140
138
|
const found = ref.kind === "collection" ? config.collections.find((candidate) => candidate.slug === ref.slug) : config.globals.find((candidate) => candidate.slug === ref.slug);
|
|
141
139
|
if (!found) throw new Error(`Unknown ${ref.kind} "${ref.slug}".`);
|
|
142
140
|
return found;
|
|
143
141
|
};
|
|
144
142
|
//#endregion
|
|
145
|
-
export {
|
|
143
|
+
export { RESERVED_FIELD_NAMES, ROW_KEYS, blockOf, blockSlugsOf, describeAddressableFields, describeFields, descriptorsUnder, findBlocksField, findRichTextField, isAdminHidden, schemaOf };
|
package/dist/tools/builtin.mjs
CHANGED
|
@@ -9,10 +9,10 @@ import { publishDocument } from "./publish-document.mjs";
|
|
|
9
9
|
import { validateDocument } from "./validate-document.mjs";
|
|
10
10
|
//#region src/tools/builtin.ts
|
|
11
11
|
/**
|
|
12
|
-
* The builtin tools, in registration order.
|
|
13
|
-
* or field never changes
|
|
14
|
-
* `isEnabled`, which derives from the key's capabilities
|
|
15
|
-
* checkbox of
|
|
12
|
+
* The builtin tools, in registration order. The set is fixed, so adding a
|
|
13
|
+
* collection, block or field never changes it. They differ from custom tools
|
|
14
|
+
* only in `isEnabled`, which derives from the key's capabilities instead of a
|
|
15
|
+
* checkbox of its own.
|
|
16
16
|
*/ const BUILTIN_TOOLS = [
|
|
17
17
|
listCapabilities,
|
|
18
18
|
describeSchema,
|
|
@@ -1,29 +1,32 @@
|
|
|
1
|
+
import { defineMcpxTool } from "../define-tool.mjs";
|
|
1
2
|
import { errorResult, jsonResult } from "../result.mjs";
|
|
2
3
|
import { validateWriteValue } from "../schema/shape.mjs";
|
|
3
4
|
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";
|
|
7
|
-
import { stripRowIds } from "../write/patch.mjs";
|
|
8
5
|
import { collectPublishBlockers } from "../write/publish-blockers.mjs";
|
|
6
|
+
import { assertAcceptsFile, fileShape, fileWriteRefusal, uploadHandoff, uploadedFile } from "../upload/file.mjs";
|
|
7
|
+
import { liveWriteSentence, localeOf, localeShape, patchOnlySlugs, slugEnum, slugsFor } from "./shared.mjs";
|
|
8
|
+
import { readDraft, resolveEntity } from "./document.mjs";
|
|
9
|
+
import { stripRowIds } from "../write/row-ids.mjs";
|
|
10
|
+
import { APIError } from "payload";
|
|
9
11
|
import { z } from "zod";
|
|
10
12
|
//#region src/tools/create-document.ts
|
|
11
|
-
|
|
13
|
+
const uploadSentence = (scope) => {
|
|
12
14
|
const slugs = patchOnlySlugs(scope);
|
|
13
|
-
return slugs.length === 0 ? "" :
|
|
15
|
+
return slugs.length === 0 ? "" : ` Documents in ${slugs.join(", ")} are files and cannot be created here. Upload the file in the admin panel, then edit its fields with patchDocument.`;
|
|
14
16
|
};
|
|
15
|
-
const DESCRIPTION = (scope) => `Creates a
|
|
17
|
+
const DESCRIPTION = (scope) => `Creates a document in a collection. Use it only when the document does not exist yet. Returns "id", "status", "updatedAt" and, if any, "publishBlockers". "publishBlockersUnavailable" means that check failed. "data" may leave required fields empty for patchDocument to fill later, except in a collection whose listCapabilities entry has "draftValidation" true or "drafts" false. A field describeSchema does not list is refused, and "id" is always assigned.
|
|
16
18
|
|
|
17
|
-
${
|
|
19
|
+
${liveWriteSentence(scope, "create")}${uploadSentence(scope)}`;
|
|
18
20
|
/**
|
|
19
|
-
* Collection-only, because a global always exists
|
|
20
|
-
* collection
|
|
21
|
+
* Collection-only, because a global always exists. A create in an upload
|
|
22
|
+
* collection needs `file`: the call then returns an upload grant, and the PUT
|
|
23
|
+
* to it runs the call again with the bytes.
|
|
21
24
|
*
|
|
22
|
-
* The seed is checked against the collection's fields
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* The seed is checked against the collection's fields first, so an unknown key
|
|
26
|
+
* is refused along with its valid siblings. Row ids in the seed are stripped
|
|
27
|
+
* and a top-level `id` is refused. The new document is re-read privileged to
|
|
28
|
+
* collect publish blockers, so an incomplete seed still succeeds and returns a
|
|
29
|
+
* checklist.
|
|
27
30
|
*/ const createDocument = defineMcpxTool({
|
|
28
31
|
name: "createDocument",
|
|
29
32
|
description: DESCRIPTION,
|
|
@@ -35,18 +38,23 @@ ${draftSentence(scope)}${uploadSentence(scope)}`;
|
|
|
35
38
|
},
|
|
36
39
|
isEnabled: (scope) => slugsFor(scope, "create").collections.length > 0,
|
|
37
40
|
inputSchema: (scope) => ({
|
|
38
|
-
collection: slugEnum(slugsFor(scope, "create").collections)
|
|
41
|
+
collection: slugEnum(slugsFor(scope, "create").collections),
|
|
39
42
|
...localeShape(scope, {
|
|
40
43
|
required: true,
|
|
41
|
-
description: "Locale the localized fields
|
|
44
|
+
description: "Locale of the localized fields in \"data\"."
|
|
42
45
|
}),
|
|
43
|
-
data: z.record(z.string(), z.unknown()).describe("
|
|
46
|
+
data: z.record(z.string(), z.unknown()).describe("Field values by name, e.g. {\"title\":\"Home\",\"slug\":\"home\"}."),
|
|
47
|
+
...fileShape(scope, (slugs) => `Required in ${slugs}. The call returns an "upload" for the bytes instead of writing.`)
|
|
44
48
|
}),
|
|
45
49
|
handler: async ({ args, scope }) => {
|
|
46
|
-
const target =
|
|
50
|
+
const target = resolveEntity(scope, { collection: args.collection }, "create");
|
|
51
|
+
if (target.kind === "global") throw new Error("createDocument resolved a global.");
|
|
47
52
|
const { payload } = scope.req;
|
|
48
53
|
const locale = localeOf(scope, args.locale);
|
|
49
|
-
|
|
54
|
+
const { file } = args;
|
|
55
|
+
if (file !== void 0) assertAcceptsFile(scope, target, file);
|
|
56
|
+
else if (scope.exposure.collections.some((entity) => entity.slug === target.slug && entity.isUpload)) throw new APIError(`"${target.slug}" stores files, so "file" is required.`, 400);
|
|
57
|
+
if (Object.hasOwn(args.data, "id")) return errorResult("Nothing was created.", { problems: ["/id: Payload assigns the id; it cannot be supplied."] });
|
|
50
58
|
const problems = validateWriteValue(payload.config, {
|
|
51
59
|
pointer: "",
|
|
52
60
|
resolution: {
|
|
@@ -55,6 +63,21 @@ ${draftSentence(scope)}${uploadSentence(scope)}`;
|
|
|
55
63
|
}
|
|
56
64
|
}, args.data);
|
|
57
65
|
if (problems.length > 0) return errorResult("Nothing was created.", { problems });
|
|
66
|
+
const bytes = uploadedFile(scope.req);
|
|
67
|
+
if (file !== void 0) {
|
|
68
|
+
const refusal = await fileWriteRefusal(scope, {
|
|
69
|
+
entity: target,
|
|
70
|
+
data: args.data
|
|
71
|
+
});
|
|
72
|
+
if (refusal) return refusal;
|
|
73
|
+
if (!bytes) return await uploadHandoff(scope, {
|
|
74
|
+
tool: "createDocument",
|
|
75
|
+
args: {
|
|
76
|
+
...args,
|
|
77
|
+
file
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
58
81
|
const created = await payload.create({
|
|
59
82
|
collection: args.collection,
|
|
60
83
|
data: stripRowIds(args.data),
|
|
@@ -62,11 +85,14 @@ ${draftSentence(scope)}${uploadSentence(scope)}`;
|
|
|
62
85
|
draft: true,
|
|
63
86
|
overrideAccess: false,
|
|
64
87
|
req: scope.req,
|
|
65
|
-
...locale === void 0 ? {} : { locale }
|
|
88
|
+
...locale === void 0 ? {} : { locale },
|
|
89
|
+
...bytes === void 0 ? {} : { file: bytes }
|
|
66
90
|
});
|
|
67
|
-
const saved = await
|
|
68
|
-
target
|
|
69
|
-
|
|
91
|
+
const saved = await readDraft(scope, {
|
|
92
|
+
target: {
|
|
93
|
+
...target,
|
|
94
|
+
id: created["id"]
|
|
95
|
+
},
|
|
70
96
|
locale,
|
|
71
97
|
privileged: true
|
|
72
98
|
});
|