@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.
Files changed (76) hide show
  1. package/README.md +144 -470
  2. package/dist/admin/capability-matrix.d.mts +33 -0
  3. package/dist/admin/capability-matrix.mjs +354 -0
  4. package/dist/admin/capability-toggles.mjs +73 -0
  5. package/dist/{client → admin}/setup-guide.d.mts +4 -2
  6. package/dist/{client → admin}/setup-guide.mjs +3 -10
  7. package/dist/api-keys/capability-matrix.d.mts +12 -8
  8. package/dist/api-keys/capability-matrix.mjs +14 -86
  9. package/dist/api-keys/collection.mjs +2 -1
  10. package/dist/api-keys/fields.mjs +36 -24
  11. package/dist/auth/resolve.mjs +71 -23
  12. package/dist/{tools/names.mjs → builtin-tool-names.mjs} +1 -1
  13. package/dist/capabilities.mjs +44 -46
  14. package/dist/define-tool.d.mts +23 -0
  15. package/dist/{types.mjs → define-tool.mjs} +1 -1
  16. package/dist/endpoint/errors.mjs +5 -6
  17. package/dist/endpoint/handler.mjs +73 -29
  18. package/dist/endpoint/server.mjs +85 -22
  19. package/dist/endpoint/upload.mjs +135 -0
  20. package/dist/entity.d.mts +9 -0
  21. package/dist/guards.mjs +7 -0
  22. package/dist/i18n.mjs +10 -8
  23. package/dist/index.d.mts +5 -2
  24. package/dist/index.mjs +3 -2
  25. package/dist/options.mjs +54 -47
  26. package/dist/plugin.d.mts +2 -2
  27. package/dist/plugin.mjs +13 -3
  28. package/dist/request.d.mts +2 -3
  29. package/dist/request.mjs +2 -3
  30. package/dist/result.d.mts +2 -3
  31. package/dist/result.mjs +3 -3
  32. package/dist/schema/admin-hidden.mjs +91 -0
  33. package/dist/schema/describe.mjs +17 -33
  34. package/dist/schema/errors.mjs +8 -0
  35. package/dist/schema/index.mjs +5 -2
  36. package/dist/schema/lexical-pointer.mjs +18 -20
  37. package/dist/schema/lexical.mjs +44 -93
  38. package/dist/schema/outline.mjs +5 -15
  39. package/dist/schema/path.mjs +28 -0
  40. package/dist/schema/pointer.mjs +18 -25
  41. package/dist/schema/shape.mjs +25 -71
  42. package/dist/schema/walk.mjs +43 -45
  43. package/dist/tools/builtin.mjs +4 -4
  44. package/dist/tools/create-document.mjs +50 -24
  45. package/dist/tools/describe-schema.mjs +38 -34
  46. package/dist/tools/document.mjs +124 -0
  47. package/dist/tools/find-documents.mjs +32 -19
  48. package/dist/tools/find-versions.mjs +22 -18
  49. package/dist/tools/get-document.mjs +60 -33
  50. package/dist/tools/list-capabilities.mjs +35 -19
  51. package/dist/tools/patch-document.mjs +122 -66
  52. package/dist/tools/publish-document.mjs +26 -32
  53. package/dist/tools/query-paths.mjs +57 -0
  54. package/dist/tools/read-request.d.mts +10 -0
  55. package/dist/tools/read-request.mjs +43 -0
  56. package/dist/tools/shared.mjs +74 -105
  57. package/dist/tools/validate-document.mjs +18 -30
  58. package/dist/tools/versions.mjs +37 -20
  59. package/dist/types.d.mts +135 -75
  60. package/dist/upload/file.mjs +166 -0
  61. package/dist/upload/grant.mjs +96 -0
  62. package/dist/version.mjs +3 -1
  63. package/dist/write/draft-guard.mjs +23 -41
  64. package/dist/write/patch.mjs +27 -183
  65. package/dist/write/publish-blockers.mjs +7 -14
  66. package/dist/write/publish-intent.mjs +6 -4
  67. package/dist/write/row-ids.mjs +46 -0
  68. package/dist/write/transaction.mjs +5 -8
  69. package/dist/write/write-data.mjs +64 -0
  70. package/package.json +11 -10
  71. package/dist/client/capability-matrix.d.mts +0 -26
  72. package/dist/client/capability-matrix.mjs +0 -349
  73. package/dist/endpoint/index.mjs +0 -4
  74. package/dist/tools/target.mjs +0 -45
  75. /package/dist/{client → admin}/index.d.mts +0 -0
  76. /package/dist/{client → admin}/index.mjs +0 -0
@@ -1,39 +1,33 @@
1
- import { blockOf, blockSlugsOf, describeAddressableFields, findBlocksField, findRichTextField, isIndexSegment, joinPath, splitPath, targetOf } from "./walk.mjs";
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
- /** Stopping part-way through a descriptor's path means a subtree, not a field. */ const isSubtreePrefix = (descriptors, segments) => descriptors.some((descriptor) => {
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
- * Unlike a descriptor path, the segments carry real indices, so intervening
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 Error(`"${descriptor.path}" is an array; "${index}" is not an index.`);
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 Error(`Cannot tell which block "${descriptor.path}/${index}" is. Supply a "blockType" on the value, one of: ${field ? blockSlugsOf(field).join(", ") : ""}`);
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 Error(`"${slug}" is not allowed at "${descriptor.path}". Allowed: ${blockSlugsOf(field).join(", ")}`);
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 element, and is
46
- * required rather than optional: `/layout/sections/3/modules/1`
47
- * can only be resolved by reading `blockType` off `sections[3]`, since a blocks
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 = targetOf(config, target.ref).flattenedFields;
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 = longestMatch(descriptors, segments);
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 Error(`"${joinPath(segments)}" is not a field here. Available: ${descriptors.map((descriptor) => descriptor.path).join(", ")}`);
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 Error(`"${match.descriptor.path}" is a ${match.descriptor.type} field and has no "${joinPath(rest)}" beneath it.`);
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,
@@ -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 { blockOf, blockSlugsOf, describeAddressableFields, findBlocksField, findRichTextField, isPlainObject, splitPath } from "./walk.mjs";
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 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.
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
- * A node with nothing to declare, and one whose sub-fields cannot be named at a
18
- * position, are both left alone.
19
- */ const checkNodeFields = (scope, field, node) => {
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: "${String(slug)}" is not allowed here. Allowed: ${blockSlugsOf(sub.blocksField).join(", ")}`);
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?.[type] ?? {})) checkNarrowedProperty(scope, {
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
- /** `pointer` addresses the list. A node holding none is left alone. */ const checkNodes = (scope, editor, nodes, pointer) => {
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 in ROOT_PROPERTIES)) {
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?.[type]?.[property];
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)}: "${String(slug)}" is not allowed here. Allowed: ${blockSlugsOf(field).join(", ")}`);
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 = describeAddressableFields(scope.fields).flatMap((descriptor) => {
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 (TOLERATED_VALUE_KEYS.has(key)) continue;
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,
@@ -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
- * 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.
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))];
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 has to be read at its position.
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 is what its own upload base fields carry.
36
+ * `admin.hidden`, which its own upload base fields carry.
36
37
  */ 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);
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
- * 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.
75
- */ const isContainer = (descriptor) => descriptor.type === "array" || descriptor.type === "group" || descriptor.type === "tab";
76
- /**
77
- * Whether a container declares anything a client could not infer from the
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 is what every walk resolving a
118
- * path against a document needs. A container describes a position rather than
119
- * a value, so only {@link nodeDescriber} reports one.
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 that a resolved descriptor path refers to, so
132
- * its editor can be introspected for the fields its nodes carry.
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, 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) => {
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 { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, blockSlugsOf, describeAddressableFields, describeFields, findBlocksField, findRichTextField, isIndexSegment, isPlainObject, joinPath, pointerFromPayloadPath, splitPath, targetOf };
143
+ export { RESERVED_FIELD_NAMES, ROW_KEYS, blockOf, blockSlugsOf, describeAddressableFields, describeFields, descriptorsUnder, findBlocksField, findRichTextField, isAdminHidden, schemaOf };
@@ -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. Fixed: adding a collection, block
13
- * or field never changes the surface. They differ from a custom tool only in
14
- * `isEnabled`, which derives from the key's capabilities rather than a
15
- * checkbox of their own.
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
- /** Names the writable slugs this tool leaves out, so the gap reads as intent. */ const uploadSentence = (scope) => {
13
+ const uploadSentence = (scope) => {
12
14
  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.`;
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 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.
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
- ${draftSentence(scope)}${uploadSentence(scope)}`;
19
+ ${liveWriteSentence(scope, "create")}${uploadSentence(scope)}`;
18
20
  /**
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
+ * 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 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.
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).describe("Collection to create the document in."),
41
+ collection: slugEnum(slugsFor(scope, "create").collections),
39
42
  ...localeShape(scope, {
40
43
  required: true,
41
- description: "Locale the localized fields of the seed belong to."
44
+ description: "Locale of the localized fields in \"data\"."
42
45
  }),
43
- data: z.record(z.string(), z.unknown()).describe("Initial field values, as describeSchema lists them.")
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 = resolveTarget(scope, { collection: args.collection }, "create");
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
- if ("id" in args.data) return errorResult("Nothing was created.", { problems: ["/id: Payload assigns the id; it cannot be supplied."] });
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 readTarget(scope, {
68
- target,
69
- id: created["id"],
91
+ const saved = await readDraft(scope, {
92
+ target: {
93
+ ...target,
94
+ id: created["id"]
95
+ },
70
96
  locale,
71
97
  privileged: true
72
98
  });