@abinnovision/payloadcms-mcpx 2.0.0-beta.9 → 2.1.0-beta.1

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 (77) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +56 -36
  3. package/dist/admin/capability-matrix.d.mts +5 -3
  4. package/dist/admin/capability-matrix.mjs +374 -272
  5. package/dist/admin/capability-toggles.mjs +79 -42
  6. package/dist/admin/confirmations-panel.mjs +232 -0
  7. package/dist/admin/confirmations.d.mts +15 -0
  8. package/dist/admin/confirmations.mjs +71 -0
  9. package/dist/admin/endpoint-url.mjs +7 -0
  10. package/dist/admin/index.d.mts +2 -1
  11. package/dist/admin/index.mjs +2 -1
  12. package/dist/admin/setup-guide.d.mts +3 -3
  13. package/dist/admin/setup-guide.mjs +51 -33
  14. package/dist/api-keys/capability-matrix.d.mts +7 -9
  15. package/dist/api-keys/capability-matrix.mjs +15 -37
  16. package/dist/api-keys/collection.mjs +16 -18
  17. package/dist/api-keys/confirmation-view.mjs +7 -0
  18. package/dist/api-keys/fields.mjs +50 -57
  19. package/dist/auth/resolve.mjs +69 -40
  20. package/dist/capabilities.d.mts +2 -0
  21. package/dist/capabilities.mjs +64 -16
  22. package/dist/define-tool.d.mts +4 -4
  23. package/dist/define-tool.mjs +8 -1
  24. package/dist/endpoint/confirmations.mjs +124 -0
  25. package/dist/endpoint/handler.mjs +25 -27
  26. package/dist/endpoint/http.mjs +27 -0
  27. package/dist/endpoint/server.mjs +48 -29
  28. package/dist/endpoint/upload.mjs +104 -0
  29. package/dist/guards.mjs +8 -1
  30. package/dist/index.d.mts +2 -2
  31. package/dist/index.mjs +1 -1
  32. package/dist/leases/confirmation.mjs +74 -0
  33. package/dist/leases/grant.mjs +29 -0
  34. package/dist/leases/lease.mjs +115 -0
  35. package/dist/options.mjs +68 -61
  36. package/dist/plugin.d.mts +3 -2
  37. package/dist/plugin.mjs +19 -4
  38. package/dist/request.mjs +5 -1
  39. package/dist/result.mjs +11 -1
  40. package/dist/schema/admin-hidden.mjs +4 -4
  41. package/dist/schema/describe.mjs +12 -18
  42. package/dist/schema/index.mjs +5 -6
  43. package/dist/schema/lexical-pointer.mjs +8 -8
  44. package/dist/schema/lexical.mjs +15 -13
  45. package/dist/schema/outline.mjs +4 -5
  46. package/dist/schema/path.mjs +4 -1
  47. package/dist/schema/pointer.mjs +11 -17
  48. package/dist/schema/shape.mjs +38 -50
  49. package/dist/schema/walk.mjs +57 -18
  50. package/dist/tools/builtin.mjs +7 -1
  51. package/dist/tools/create-document.mjs +38 -26
  52. package/dist/tools/delete-document.mjs +154 -0
  53. package/dist/tools/describe-schema.mjs +7 -5
  54. package/dist/tools/document.mjs +131 -45
  55. package/dist/tools/duplicate-document.mjs +50 -0
  56. package/dist/tools/find-documents.mjs +15 -20
  57. package/dist/tools/find-versions.mjs +11 -15
  58. package/dist/tools/get-document.mjs +31 -15
  59. package/dist/tools/links.mjs +53 -0
  60. package/dist/tools/list-capabilities.mjs +17 -7
  61. package/dist/tools/patch-document.mjs +57 -53
  62. package/dist/tools/publish-document.mjs +17 -29
  63. package/dist/tools/query-paths.mjs +1 -1
  64. package/dist/tools/run-confirmed.mjs +114 -0
  65. package/dist/tools/shared.mjs +53 -46
  66. package/dist/tools/validate-document.mjs +6 -8
  67. package/dist/tools/versions.mjs +11 -6
  68. package/dist/types.d.mts +158 -43
  69. package/dist/upload/file.mjs +178 -0
  70. package/dist/version.mjs +4 -2
  71. package/dist/write/draft-guard.mjs +35 -28
  72. package/dist/write/patch.mjs +4 -4
  73. package/dist/write/publish-intent.mjs +23 -11
  74. package/dist/write/row-ids.mjs +3 -1
  75. package/dist/write/write-data.mjs +18 -31
  76. package/package.json +14 -14
  77. package/dist/builtin-tool-names.mjs +0 -16
@@ -1,5 +1,5 @@
1
1
  import { isPlainObject, ownValue } from "../guards.mjs";
2
- import { allowedNodeTypes, nodeOptions } from "./lexical.mjs";
2
+ import { allowedNodeTypes, editorRoot, nodeOptions } from "./lexical.mjs";
3
3
  //#region src/schema/outline.ts
4
4
  const TEXT_PREVIEW_LENGTH = 80;
5
5
  const collectText = (node) => {
@@ -43,12 +43,11 @@ const walk = (node, pointer, options, entries) => {
43
43
  * entry's `pointer` extends it with "/root" and the node's real indices, which
44
44
  * makes it directly usable in a patch operation.
45
45
  */ const lexicalOutline = (state, basePointer, field) => {
46
- if (!isPlainObject(state)) return [];
47
- const root = state["root"];
48
- if (!isPlainObject(root) || !Array.isArray(root["children"])) return [];
46
+ const rootChildren = editorRoot(state)?.["children"];
47
+ if (!Array.isArray(rootChildren)) return [];
49
48
  const options = nodeOptions(field, allowedNodeTypes(field));
50
49
  const entries = [];
51
- root["children"].forEach((child, index) => {
50
+ rootChildren.forEach((child, index) => {
52
51
  if (isPlainObject(child)) walk(child, `${basePointer}/root/children/${String(index)}`, options, entries);
53
52
  });
54
53
  return entries;
@@ -24,5 +24,8 @@ const PROTOTYPE_SEGMENTS = /* @__PURE__ */ new Set([
24
24
  * Pointer, so everything handed back addresses documents the same way. The
25
25
  * path already carries real indices, so it maps directly.
26
26
  */ const pointerFromPayloadPath = (path) => path ? joinPath(path.split(".")) : "";
27
+ /**
28
+ * The pointer one segment up. The root pointer stays the root.
29
+ */ const parentPointer = (pointer) => joinPath(splitPath(pointer).slice(0, -1));
27
30
  //#endregion
28
- export { JSON_POINTER_PATTERN, isIndexSegment, joinPath, pointerFromPayloadPath, prototypeSegmentProblem, splitPath };
31
+ export { JSON_POINTER_PATTERN, isIndexSegment, joinPath, parentPointer, pointerFromPayloadPath, prototypeSegmentProblem, splitPath };
@@ -1,14 +1,10 @@
1
- import { isIndexSegment, joinPath, splitPath } from "./path.mjs";
2
- import { blockOf, blockSlugsOf, describeAddressableFields, findBlocksField, findRichTextField, schemaOf } from "./walk.mjs";
1
+ import { propOf } from "../guards.mjs";
3
2
  import { SchemaError } from "./errors.mjs";
3
+ import { isIndexSegment, joinPath, splitPath } from "./path.mjs";
4
+ import { blockSlugsOf, describeAddressableFields, findFieldAt, longestMatch, requireBlock, schemaOf } from "./walk.mjs";
4
5
  import { resolveLexicalPointer } from "./lexical-pointer.mjs";
5
6
  //#region src/schema/pointer.ts
6
7
  const partMatches = (part, segment) => segment !== void 0 && (part === "*" ? isIndexSegment(segment) : part === segment);
7
- const matchPointerSegments = (descriptors, segments) => descriptors.map((descriptor) => ({
8
- consumed: splitPath(descriptor.path).length,
9
- descriptor,
10
- parts: splitPath(descriptor.path)
11
- })).filter(({ parts }) => parts.every((part, offset) => partMatches(part, segments[offset]))).sort((left, right) => right.consumed - left.consumed)[0];
12
8
  const isSubtreePrefix = (descriptors, segments) => descriptors.some((descriptor) => {
13
9
  const parts = splitPath(descriptor.path);
14
10
  return parts.length > segments.length && segments.every((segment, offset) => {
@@ -21,17 +17,15 @@ const stepIntoBlock = (at) => {
21
17
  const { addedValue, config, descriptor, rows } = at;
22
18
  const [index, ...remaining] = at.rest;
23
19
  if (!isIndexSegment(index)) throw new SchemaError(`"${descriptor.path}" is an array; "${index}" is not an index.`);
24
- const field = findBlocksField(at.fields, splitPath(descriptor.path));
20
+ const field = findFieldAt(at.fields, splitPath(descriptor.path), "blocks");
25
21
  const existing = Array.isArray(rows) && index !== "-" ? rows[Number(index)] : void 0;
26
- const slug = existing?.blockType ?? addedValue?.blockType;
22
+ const slug = propOf(existing, "blockType") ?? propOf(addedValue, "blockType");
27
23
  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
24
  if (typeof slug !== "string") throw new SchemaError(`"blockType" must be a string naming a block at "${descriptor.path}". Allowed: ${blockSlugsOf(field).join(", ")}`);
29
- const block = blockOf(config, field, slug);
30
- if (!block) throw new SchemaError(`"${slug}" is not allowed at "${descriptor.path}". Allowed: ${blockSlugsOf(field).join(", ")}`);
31
25
  return {
32
26
  blockType: slug,
33
27
  data: existing,
34
- fields: block.flattenedFields,
28
+ fields: requireBlock(config, field, slug, descriptor.path).flattenedFields,
35
29
  rest: remaining
36
30
  };
37
31
  };
@@ -48,7 +42,7 @@ const stepIntoBlock = (at) => {
48
42
  let inLexical;
49
43
  while (segments.length > 0) {
50
44
  const descriptors = describeAddressableFields(fields);
51
- const match = matchPointerSegments(descriptors, segments);
45
+ const match = longestMatch(descriptors, segments, partMatches);
52
46
  if (!match) {
53
47
  if (isSubtreePrefix(descriptors, segments)) return {
54
48
  ...blockType === void 0 ? {} : { blockType },
@@ -57,7 +51,7 @@ const stepIntoBlock = (at) => {
57
51
  };
58
52
  throw new SchemaError(`"${joinPath(segments)}" is not a field here. Available: ${descriptors.map((descriptor) => descriptor.path).join(", ")}`);
59
53
  }
60
- const rest = segments.slice(match.consumed);
54
+ const rest = segments.slice(match.parts.length);
61
55
  if (rest.length === 0) return {
62
56
  ...blockType === void 0 ? {} : { blockType },
63
57
  descriptor: match.descriptor,
@@ -67,7 +61,7 @@ const stepIntoBlock = (at) => {
67
61
  ...readOnly === void 0 ? {} : { readOnly }
68
62
  };
69
63
  if (match.descriptor.type === "richText") {
70
- const field = findRichTextField(fields, splitPath(match.descriptor.path));
64
+ const field = findFieldAt(fields, splitPath(match.descriptor.path), "richText");
71
65
  if (!field) throw new Error(`"${match.descriptor.path}" could not be resolved.`);
72
66
  const step = resolveLexicalPointer({
73
67
  ...target.addedValue === void 0 ? {} : { addedValue: target.addedValue },
@@ -75,7 +69,7 @@ const stepIntoBlock = (at) => {
75
69
  descriptor: match.descriptor,
76
70
  field,
77
71
  segments: rest,
78
- state: valueAtSegments(data, segments.slice(0, match.consumed))
72
+ state: valueAtSegments(data, segments.slice(0, match.parts.length))
79
73
  });
80
74
  if (step.kind === "position") return {
81
75
  ...blockType === void 0 ? {} : { blockType },
@@ -99,7 +93,7 @@ const stepIntoBlock = (at) => {
99
93
  descriptor: match.descriptor,
100
94
  fields,
101
95
  rest,
102
- rows: valueAtSegments(data, segments.slice(0, match.consumed)),
96
+ rows: valueAtSegments(data, segments.slice(0, match.parts.length)),
103
97
  config
104
98
  });
105
99
  readOnly = match.descriptor.readOnly ?? readOnly;
@@ -1,7 +1,7 @@
1
- import { isPlainObject, ownValue } from "../guards.mjs";
2
- import { ROOT_PROPERTIES, constrainsFields, lexicalSubSchema, nodeProblems, propertyProblem, rootProblems } from "./lexical.mjs";
1
+ import { isPlainObject, ownValue, propOf } from "../guards.mjs";
2
+ import { ROOT_PROPERTIES, constrainsFields, editorRoot, lexicalSubSchema, nodeProblems, propertyProblem, rootProblems } from "./lexical.mjs";
3
3
  import { splitPath } from "./path.mjs";
4
- import { ROW_KEYS, blockOf, blockSlugsOf, descriptorsUnder, findBlocksField, findRichTextField } from "./walk.mjs";
4
+ import { ROW_KEYS, blockForRow, blockSlugsOf, classifyKey, descriptorsUnder, findFieldAt } from "./walk.mjs";
5
5
  //#region src/schema/shape.ts
6
6
  /**
7
7
  * Lexical throws when it hydrates a state whose root holds nothing. An empty
@@ -33,10 +33,9 @@ const checkNodeFields = (scope, field, node) => {
33
33
  }, data);
34
34
  return;
35
35
  }
36
- const slug = data["blockType"];
37
- const block = typeof slug === "string" ? blockOf(scope.config, sub.blocksField, slug) : void 0;
36
+ const block = blockForRow(scope.config, sub.blocksField, data);
38
37
  if (!block) {
39
- scope.problems.push(`${scope.pointer}/fields: ${refusedSlug(slug)}. Allowed: ${blockSlugsOf(sub.blocksField).join(", ")}`);
38
+ scope.problems.push(`${scope.pointer}/fields: ${refusedSlug(data["blockType"])}. Allowed: ${blockSlugsOf(sub.blocksField).join(", ")}`);
40
39
  return;
41
40
  }
42
41
  checkValue({
@@ -81,11 +80,11 @@ const checkNodes = (scope, editor, nodes, pointer) => {
81
80
  });
82
81
  };
83
82
  const checkRichText = (scope, editor, value) => {
84
- if (!isPlainObject(value) || !isPlainObject(value["root"])) {
83
+ const root = editorRoot(value);
84
+ if (!root) {
85
85
  scope.problems.push(`${scope.pointer}: expected a Lexical editor state with a "root".`);
86
86
  return;
87
87
  }
88
- const root = value["root"];
89
88
  const { missing, rejected, unexpected } = rootProblems(root);
90
89
  if (missing.length > 0) scope.problems.push(`${scope.pointer}/root: the root node is missing ${quoted(missing)}. Write nodes as Lexical serializes them.`);
91
90
  for (const problem of rejected) scope.problems.push(`${scope.pointer}/root/${problem.property}: the root node needs ${problem.needs} here.`);
@@ -148,7 +147,7 @@ const checkLeafValue = (scope, descriptor, value) => {
148
147
  if (descriptor.type === "richText") {
149
148
  checkRichText(scope, {
150
149
  allowed: descriptor.nodes ?? [],
151
- field: findRichTextField(scope.fields, splitPath(descriptor.path)),
150
+ field: findFieldAt(scope.fields, splitPath(descriptor.path), "richText"),
152
151
  nodeOptions: descriptor.nodeOptions
153
152
  }, value);
154
153
  return;
@@ -158,13 +157,12 @@ const checkLeafValue = (scope, descriptor, value) => {
158
157
  scope.problems.push(`${scope.pointer}: expected an array of blocks.`);
159
158
  return;
160
159
  }
161
- const field = findBlocksField(scope.fields, splitPath(descriptor.path));
162
- if (!field) return;
160
+ const field = findFieldAt(scope.fields, splitPath(descriptor.path), "blocks");
163
161
  value.forEach((row, index) => {
164
- const slug = isPlainObject(row) ? row["blockType"] : void 0;
165
- const block = typeof slug === "string" ? blockOf(scope.config, field, slug) : void 0;
162
+ const block = field && blockForRow(scope.config, field, row);
163
+ if (!field) return;
166
164
  if (!block) {
167
- scope.problems.push(`${scope.pointer}/${String(index)}: ${refusedSlug(slug)}. Allowed: ${blockSlugsOf(field).join(", ")}`);
165
+ scope.problems.push(`${scope.pointer}/${String(index)}: ${refusedSlug(propOf(row, "blockType"))}. Allowed: ${blockSlugsOf(field).join(", ")}`);
168
166
  return;
169
167
  }
170
168
  checkValue({
@@ -177,47 +175,37 @@ const checkLeafValue = (scope, descriptor, value) => {
177
175
  };
178
176
  const checkValue = (scope, value) => {
179
177
  if (!isPlainObject(value)) return;
180
- const prefixParts = scope.prefix;
181
- const relative = descriptorsUnder(scope.fields, prefixParts);
178
+ const relative = descriptorsUnder(scope.fields, scope.prefix);
182
179
  for (const [key, entry] of Object.entries(value)) {
183
180
  if (ROW_KEYS.has(key)) continue;
184
- const candidates = relative.filter(({ parts }) => parts[0] === key);
185
181
  const pointer = `${scope.pointer}/${key}`;
186
- if (candidates.length === 0) {
187
- scope.problems.push(`${pointer}: no such field. Available: ${[...new Set(relative.map(({ parts }) => parts[0]))].join(", ")}`);
188
- continue;
189
- }
190
- const exact = candidates.find(({ parts }) => parts.length === 1);
191
- if (exact) {
192
- checkLeafValue({
193
- ...scope,
194
- pointer
195
- }, exact.descriptor, entry);
196
- continue;
197
- }
198
- if (candidates.some(({ parts }) => parts[1] === "*")) {
199
- if (!Array.isArray(entry)) {
200
- scope.problems.push(`${pointer}: expected an array.`);
201
- continue;
202
- }
203
- entry.forEach((row, index) => {
204
- checkValue({
182
+ const found = classifyKey(relative, scope.prefix, key);
183
+ switch (found.kind) {
184
+ case "none":
185
+ scope.problems.push(`${pointer}: no such field. Available: ${[...new Set(relative.map(({ parts }) => parts[0]))].join(", ")}`);
186
+ break;
187
+ case "leaf":
188
+ checkLeafValue({
205
189
  ...scope,
206
- pointer: `${pointer}/${String(index)}`,
207
- prefix: [
208
- ...prefixParts,
209
- key,
210
- "*"
211
- ]
212
- }, row);
213
- });
214
- continue;
190
+ pointer
191
+ }, found.descriptor, entry);
192
+ break;
193
+ case "rows":
194
+ if (Array.isArray(entry)) entry.forEach((row, index) => {
195
+ checkValue({
196
+ ...scope,
197
+ pointer: `${pointer}/${String(index)}`,
198
+ prefix: found.prefix
199
+ }, row);
200
+ });
201
+ else scope.problems.push(`${pointer}: expected an array.`);
202
+ break;
203
+ case "group": checkValue({
204
+ ...scope,
205
+ pointer,
206
+ prefix: found.prefix
207
+ }, entry);
215
208
  }
216
- checkValue({
217
- ...scope,
218
- pointer,
219
- prefix: [...prefixParts, key]
220
- }, entry);
221
209
  }
222
210
  };
223
211
  const validateWriteValue = (config, target, value) => {
@@ -1,3 +1,5 @@
1
+ import { propOf } from "../guards.mjs";
2
+ import { SchemaError } from "./errors.mjs";
1
3
  import { allowedNodeTypes, nodeOptions } from "./lexical.mjs";
2
4
  import { joinPath, splitPath } from "./path.mjs";
3
5
  import { translateAny } from "../i18n.mjs";
@@ -34,9 +36,17 @@ const RESERVED_FIELD_NAMES = /* @__PURE__ */ new Set([
34
36
  /**
35
37
  * Payload's `fieldIsHiddenOrDisabled` reads `hidden` and `admin.disabled`, not
36
38
  * `admin.hidden`, which its own upload base fields carry.
37
- */ const isAdminHidden = (field) => "admin" in field && field.admin.hidden === true;
38
- const isSkipped = (field) => !("name" in field) || field.type === "join" || RESERVED_FIELD_NAMES.has(field.name) || fieldIsVirtual(field) || isAdminHidden(field) || fieldIsHiddenOrDisabled(field);
39
- const isReadOnly = (field) => "admin" in field && field.admin.readOnly === true;
39
+ */ const adminFlag = (field, key) => "admin" in field && field.admin[key] === true;
40
+ const blockForRow = (config, field, row) => {
41
+ const slug = propOf(row, "blockType");
42
+ return typeof slug === "string" ? blockOf(config, field, slug) : void 0;
43
+ };
44
+ const requireBlock = (config, field, slug, at) => {
45
+ const block = blockOf(config, field, slug);
46
+ if (!block) throw new SchemaError(`"${slug}" is not allowed at "${at}". Allowed: ${blockSlugsOf(field).join(", ")}`);
47
+ return block;
48
+ };
49
+ const isSkipped = (field) => !("name" in field) || field.type === "join" || RESERVED_FIELD_NAMES.has(field.name) || fieldIsVirtual(field) || adminFlag(field, "hidden") || fieldIsHiddenOrDisabled(field);
40
50
  const describeBase = (field, { path, readOnly, translate }) => {
41
51
  const description = translate("admin" in field ? field.admin.description : void 0);
42
52
  return {
@@ -71,17 +81,11 @@ const withRows = (descriptor, field) => ({
71
81
  });
72
82
  const isContainer = (descriptor) => descriptor.type === "array" || descriptor.type === "group" || descriptor.type === "tab";
73
83
  const isInformative = (descriptor) => descriptor.description !== void 0 || descriptor.required === true || descriptor.localized === true;
74
- /**
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.
81
- */ const describeFields = (fields, translate = translateAny) => {
84
+ const describedByFields = /* @__PURE__ */ new WeakMap();
85
+ const describeWith = (fields, translate) => {
82
86
  const walk = (current, prefix, parentReadOnly) => current.flatMap((field) => {
83
87
  if (isSkipped(field)) return [];
84
- const readOnly = parentReadOnly || isReadOnly(field);
88
+ const readOnly = parentReadOnly || adminFlag(field, "readOnly");
85
89
  const path = [...prefix, field.name];
86
90
  const at = {
87
91
  path: joinPath(path),
@@ -102,6 +106,22 @@ const isInformative = (descriptor) => descriptor.description !== void 0 || descr
102
106
  return walk(fields, [], false);
103
107
  };
104
108
  /**
109
+ * Named tabs, groups and arrays add a path segment and are described
110
+ * themselves only when they declare something of their own: an array always,
111
+ * since its row counts live nowhere else. The walk stops at every blocks field
112
+ * and names the slugs, so a node's size follows the number of blocks it
113
+ * allows, not their definitions. Omitting `translate` costs language
114
+ * selection, never the description itself.
115
+ */ const describeFields = (fields, translate = translateAny) => {
116
+ if (translate !== translateAny) return describeWith(fields, translate);
117
+ let described = describedByFields.get(fields);
118
+ if (!described) {
119
+ described = describeWith(fields, translate);
120
+ describedByFields.set(fields, described);
121
+ }
122
+ return described;
123
+ };
124
+ /**
105
125
  * The descriptors that address a value, which every walk resolving a path
106
126
  * against a document needs. Containers are left out, since only
107
127
  * {@link nodeDescriber} reports one.
@@ -124,11 +144,30 @@ const findFieldAt = (fields, path, type) => {
124
144
  if (field.type === "array" && path[1] === "*") return findFieldAt(field.flattenedFields, path.slice(2), type);
125
145
  }
126
146
  };
127
- const findBlocksField = (fields, path) => findFieldAt(fields, path, "blocks");
128
- /**
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.
131
- */ const findRichTextField = (fields, path) => findFieldAt(fields, path, "richText");
147
+ const longestMatch = (descriptors, segments, eq) => descriptors.map((descriptor) => ({
148
+ descriptor,
149
+ parts: splitPath(descriptor.path)
150
+ })).filter(({ parts }) => parts.every((part, offset) => eq(part, segments[offset]))).sort((left, right) => right.parts.length - left.parts.length)[0];
151
+ const classifyKey = (relative, prefix, key) => {
152
+ const candidates = relative.filter(({ parts }) => parts[0] === key);
153
+ const exact = candidates.find(({ parts }) => parts.length === 1);
154
+ if (exact) return {
155
+ descriptor: exact.descriptor,
156
+ kind: "leaf"
157
+ };
158
+ if (candidates.length === 0) return { kind: "none" };
159
+ return candidates.some(({ parts }) => parts[1] === "*") ? {
160
+ kind: "rows",
161
+ prefix: [
162
+ ...prefix,
163
+ key,
164
+ "*"
165
+ ]
166
+ } : {
167
+ kind: "group",
168
+ prefix: [...prefix, key]
169
+ };
170
+ };
132
171
  /**
133
172
  * Looks up the sanitized config for a collection or global as an
134
173
  * {@link EntitySchema}. Throws on an unknown slug instead of returning
@@ -140,4 +179,4 @@ const findBlocksField = (fields, path) => findFieldAt(fields, path, "blocks");
140
179
  return found;
141
180
  };
142
181
  //#endregion
143
- export { RESERVED_FIELD_NAMES, ROW_KEYS, blockOf, blockSlugsOf, describeAddressableFields, describeFields, descriptorsUnder, findBlocksField, findRichTextField, isAdminHidden, schemaOf };
182
+ export { RESERVED_FIELD_NAMES, ROW_KEYS, adminFlag, blockForRow, blockOf, blockSlugsOf, classifyKey, describeAddressableFields, describeFields, descriptorsUnder, findFieldAt, longestMatch, requireBlock, schemaOf };
@@ -1,11 +1,14 @@
1
1
  import { createDocument } from "./create-document.mjs";
2
+ import { deleteDocument } from "./delete-document.mjs";
2
3
  import { describeSchema } from "./describe-schema.mjs";
4
+ import { duplicateDocument } from "./duplicate-document.mjs";
3
5
  import { findDocuments } from "./find-documents.mjs";
4
6
  import { findVersions } from "./find-versions.mjs";
5
7
  import { getDocument } from "./get-document.mjs";
6
8
  import { listCapabilities } from "./list-capabilities.mjs";
7
9
  import { patchDocument } from "./patch-document.mjs";
8
10
  import { publishDocument } from "./publish-document.mjs";
11
+ import { createRunConfirmed } from "./run-confirmed.mjs";
9
12
  import { validateDocument } from "./validate-document.mjs";
10
13
  //#region src/tools/builtin.ts
11
14
  /**
@@ -21,8 +24,11 @@ import { validateDocument } from "./validate-document.mjs";
21
24
  findVersions,
22
25
  patchDocument,
23
26
  createDocument,
27
+ duplicateDocument,
24
28
  validateDocument,
25
- publishDocument
29
+ publishDocument,
30
+ deleteDocument,
31
+ createRunConfirmed(() => BUILTIN_TOOLS)
26
32
  ];
27
33
  //#endregion
28
34
  export { BUILTIN_TOOLS };
@@ -1,23 +1,26 @@
1
1
  import { defineMcpxTool } from "../define-tool.mjs";
2
- import { errorResult, jsonResult } from "../result.mjs";
2
+ import { definedProps } from "../guards.mjs";
3
+ import { errorResult } from "../result.mjs";
3
4
  import { validateWriteValue } from "../schema/shape.mjs";
4
5
  import "../schema/index.mjs";
5
- import { liveWriteSentence, localeOf, localeShape, patchOnlySlugs, slugEnum, slugsFor } from "./shared.mjs";
6
- import { collectPublishBlockers } from "../write/publish-blockers.mjs";
7
- import { readDraft, resolveEntity } from "./document.mjs";
6
+ import { assertAcceptsFile, fileShape, fileWriteRefusal, uploadHandoff, uploadedFile } from "../upload/file.mjs";
7
+ import { liveWriteSentence, localeOf, localeShape, patchOnlySlugs, slugsFor, stringEnum } from "./shared.mjs";
8
+ import { resolveCollection, writtenResult } from "./document.mjs";
8
9
  import { stripRowIds } from "../write/row-ids.mjs";
9
10
  import { z } from "zod";
11
+ import { APIError } from "payload";
10
12
  //#region src/tools/create-document.ts
11
13
  const uploadSentence = (scope) => {
12
14
  const slugs = patchOnlySlugs(scope);
13
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 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.
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. The response links the document with "adminUrl" and "previewUrl".
16
18
 
17
19
  ${liveWriteSentence(scope, "create")}${uploadSentence(scope)}`;
18
20
  /**
19
- * Collection-only, because a global always exists. Upload collections are
20
- * excluded because a create there would need 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
25
  * The seed is checked against the collection's fields first, so an unknown key
23
26
  * is refused along with its valid siblings. Row ids in the seed are stripped
@@ -35,18 +38,21 @@ ${liveWriteSentence(scope, "create")}${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: stringEnum(slugsFor(scope, "create").collections),
39
42
  ...localeShape(scope, {
40
43
  required: true,
41
44
  description: "Locale of the localized fields in \"data\"."
42
45
  }),
43
- data: z.record(z.string(), z.unknown()).describe("Field values by name, e.g. {\"title\":\"Home\",\"slug\":\"home\"}.")
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 = resolveEntity(scope, { collection: args.collection }, "create");
47
- if (target.kind === "global") throw new Error("createDocument resolved a global.");
50
+ const target = resolveCollection(scope, args.collection, "create");
48
51
  const { payload } = scope.req;
49
52
  const locale = localeOf(scope, args.locale);
53
+ const { file } = args;
54
+ if (file !== void 0) assertAcceptsFile(scope, target, file);
55
+ 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);
50
56
  if (Object.hasOwn(args.data, "id")) return errorResult("Nothing was created.", { problems: ["/id: Payload assigns the id; it cannot be supplied."] });
51
57
  const problems = validateWriteValue(payload.config, {
52
58
  pointer: "",
@@ -56,6 +62,21 @@ ${liveWriteSentence(scope, "create")}${uploadSentence(scope)}`;
56
62
  }
57
63
  }, args.data);
58
64
  if (problems.length > 0) return errorResult("Nothing was created.", { problems });
65
+ const bytes = uploadedFile(scope.req);
66
+ if (file !== void 0) {
67
+ const refusal = await fileWriteRefusal(scope, {
68
+ entity: target,
69
+ data: args.data
70
+ });
71
+ if (refusal) return refusal;
72
+ if (!bytes) return await uploadHandoff(scope, {
73
+ tool: "createDocument",
74
+ args: {
75
+ ...args,
76
+ file
77
+ }
78
+ });
79
+ }
59
80
  const created = await payload.create({
60
81
  collection: args.collection,
61
82
  data: stripRowIds(args.data),
@@ -63,26 +84,17 @@ ${liveWriteSentence(scope, "create")}${uploadSentence(scope)}`;
63
84
  draft: true,
64
85
  overrideAccess: false,
65
86
  req: scope.req,
66
- ...locale === void 0 ? {} : { locale }
87
+ ...definedProps({
88
+ locale,
89
+ file: bytes
90
+ })
67
91
  });
68
- const saved = await readDraft(scope, {
92
+ return await writtenResult(scope, {
69
93
  target: {
70
94
  ...target,
71
95
  id: created["id"]
72
96
  },
73
- locale,
74
- privileged: true
75
- });
76
- const validation = await collectPublishBlockers(scope.req, {
77
- doc: saved,
78
- entity: target
79
- });
80
- return jsonResult({
81
- id: saved["id"],
82
- status: saved["_status"],
83
- updatedAt: saved["updatedAt"],
84
- ...validation.blockers.length > 0 ? { publishBlockers: validation.blockers } : {},
85
- ...validation.unavailable ? { publishBlockersUnavailable: true } : {}
97
+ locale
86
98
  });
87
99
  }
88
100
  });