@abinnovision/payloadcms-mcpx 1.1.0-beta.6 → 2.0.0-beta.10

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 (71) hide show
  1. package/README.md +140 -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 +17 -85
  9. package/dist/api-keys/collection.mjs +2 -1
  10. package/dist/api-keys/fields.mjs +36 -24
  11. package/dist/auth/resolve.mjs +44 -6
  12. package/dist/{tools/names.mjs → builtin-tool-names.mjs} +1 -1
  13. package/dist/capabilities.mjs +41 -43
  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 +49 -19
  18. package/dist/endpoint/server.mjs +72 -20
  19. package/dist/entity.d.mts +9 -0
  20. package/dist/guards.mjs +7 -0
  21. package/dist/i18n.mjs +10 -8
  22. package/dist/index.d.mts +5 -2
  23. package/dist/index.mjs +3 -2
  24. package/dist/options.mjs +52 -46
  25. package/dist/plugin.mjs +0 -1
  26. package/dist/request.d.mts +2 -3
  27. package/dist/request.mjs +2 -3
  28. package/dist/result.d.mts +2 -3
  29. package/dist/result.mjs +3 -3
  30. package/dist/schema/admin-hidden.mjs +91 -0
  31. package/dist/schema/describe.mjs +17 -33
  32. package/dist/schema/errors.mjs +8 -0
  33. package/dist/schema/index.mjs +5 -2
  34. package/dist/schema/lexical-pointer.mjs +18 -20
  35. package/dist/schema/lexical.mjs +44 -93
  36. package/dist/schema/outline.mjs +5 -15
  37. package/dist/schema/path.mjs +28 -0
  38. package/dist/schema/pointer.mjs +18 -25
  39. package/dist/schema/shape.mjs +25 -71
  40. package/dist/schema/walk.mjs +43 -45
  41. package/dist/tools/builtin.mjs +4 -4
  42. package/dist/tools/create-document.mjs +26 -23
  43. package/dist/tools/describe-schema.mjs +38 -34
  44. package/dist/tools/document.mjs +124 -0
  45. package/dist/tools/find-documents.mjs +32 -19
  46. package/dist/tools/find-versions.mjs +22 -18
  47. package/dist/tools/get-document.mjs +39 -32
  48. package/dist/tools/list-capabilities.mjs +23 -16
  49. package/dist/tools/patch-document.mjs +75 -49
  50. package/dist/tools/publish-document.mjs +26 -32
  51. package/dist/tools/query-paths.mjs +57 -0
  52. package/dist/tools/read-request.d.mts +10 -0
  53. package/dist/tools/read-request.mjs +43 -0
  54. package/dist/tools/shared.mjs +67 -99
  55. package/dist/tools/validate-document.mjs +18 -30
  56. package/dist/tools/versions.mjs +37 -20
  57. package/dist/types.d.mts +118 -74
  58. package/dist/write/draft-guard.mjs +23 -41
  59. package/dist/write/patch.mjs +27 -183
  60. package/dist/write/publish-blockers.mjs +7 -14
  61. package/dist/write/publish-intent.mjs +6 -4
  62. package/dist/write/row-ids.mjs +46 -0
  63. package/dist/write/transaction.mjs +5 -8
  64. package/dist/write/write-data.mjs +64 -0
  65. package/package.json +6 -5
  66. package/dist/client/capability-matrix.d.mts +0 -26
  67. package/dist/client/capability-matrix.mjs +0 -349
  68. package/dist/endpoint/index.mjs +0 -4
  69. package/dist/tools/target.mjs +0 -45
  70. /package/dist/{client → admin}/index.d.mts +0 -0
  71. /package/dist/{client → admin}/index.mjs +0 -0
@@ -1,98 +1,21 @@
1
- import { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, describeAddressableFields, findBlocksField, isIndexSegment, joinPath, splitPath } from "../schema/walk.mjs";
1
+ import { isIndexSegment, joinPath, prototypeSegmentProblem, splitPath } from "../schema/path.mjs";
2
+ import { RESERVED_FIELD_NAMES, descriptorsUnder } from "../schema/walk.mjs";
3
+ import { SchemaError } from "../schema/errors.mjs";
2
4
  import { resolveDataPointer } from "../schema/pointer.mjs";
3
5
  import { EMPTY_ROOT, validateWriteValue } from "../schema/shape.mjs";
4
6
  import "../schema/index.mjs";
5
- import { z } from "zod";
7
+ import { reconcileRowIds } from "./row-ids.mjs";
6
8
  import { Pointer, applyPatch } from "rfc6902";
7
9
  //#region src/write/patch.ts
8
- const POINTER = z.string().regex(JSON_POINTER_PATTERN);
9
- /** Discriminated on `op`, so an operation carries only its own members. */ const PATCH_OPERATION_SCHEMA = z.discriminatedUnion("op", [
10
- z.strictObject({
11
- op: z.literal("add"),
12
- path: POINTER,
13
- value: z.unknown()
14
- }),
15
- z.strictObject({
16
- op: z.literal("remove"),
17
- path: POINTER
18
- }),
19
- z.strictObject({
20
- op: z.literal("replace"),
21
- path: POINTER,
22
- value: z.unknown()
23
- }),
24
- z.strictObject({
25
- from: POINTER,
26
- op: z.literal("move"),
27
- path: POINTER
28
- }),
29
- z.strictObject({
30
- from: POINTER,
31
- op: z.literal("copy"),
32
- path: POINTER
33
- }),
34
- z.strictObject({
35
- op: z.literal("test"),
36
- path: POINTER,
37
- value: z.unknown()
38
- })
39
- ]).describe("An RFC 6902 operation.");
40
- const isReservedPointer = (pointer) => pointer.split("/").slice(1).some((segment) => RESERVED_FIELD_NAMES.has(segment));
10
+ const isReservedPointer = (pointer) => splitPath(pointer).some((segment) => RESERVED_FIELD_NAMES.has(segment));
41
11
  const droppedPointer = (operation) => {
42
12
  if (operation.op === "remove") return operation.path;
43
13
  return operation.op === "move" ? operation.from : void 0;
44
14
  };
45
15
  const isElementPointer = (pointer) => {
46
- return isIndexSegment(pointer.split("/").pop() ?? "");
16
+ return isIndexSegment(splitPath(pointer).at(-1) ?? "");
47
17
  };
48
- const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
49
- /** Its nodes manage their own ids, so it is never descended into. */ const isRichTextState = (value) => isPlainObject(value["root"]) && Array.isArray(value["root"]["children"]);
50
- /** A row is a plain object carrying `blockType`, or one sitting in an array. */ const walkRows = (value, visit, isRow = false) => {
51
- if (Array.isArray(value)) {
52
- for (const entry of value) walkRows(entry, visit, true);
53
- return;
54
- }
55
- if (!isPlainObject(value) || isRichTextState(value)) return;
56
- if (isRow || typeof value["blockType"] === "string") visit(value);
57
- for (const entry of Object.values(value)) walkRows(entry, visit);
58
- };
59
- /**
60
- * Keeps a row id only when the stored document already has it and no earlier
61
- * row in the write claimed it; every other id is dropped so Payload assigns a
62
- * fresh one.
63
- *
64
- * A kept id makes Payload update the row in place, which preserves the other
65
- * locales of any localized field inside it. A duplicated id (a copied row) or
66
- * an id from elsewhere would violate a SQL primary key, so those never pass.
67
- */ const reconcileRowIds = (next, stored) => {
68
- const known = /* @__PURE__ */ new Set();
69
- walkRows(stored, (row) => {
70
- if (row["id"] !== void 0) known.add(row["id"]);
71
- });
72
- const seen = /* @__PURE__ */ new Set();
73
- walkRows(next, (row) => {
74
- const id = row["id"];
75
- if (id === void 0) return;
76
- if (known.has(id) && !seen.has(id)) {
77
- seen.add(id);
78
- return;
79
- }
80
- delete row["id"];
81
- });
82
- };
83
- /** For create, where no stored row exists and any incoming id is invented. */ const stripRowIds = (value) => {
84
- const next = structuredClone(value);
85
- walkRows(next, (row) => {
86
- delete row["id"];
87
- });
88
- return next;
89
- };
90
- /**
91
- * The operation as it is applied: values are cloned so the written document
92
- * never shares references with the caller's operations, and a `replace` of a
93
- * field the target locale has no value for becomes an `add`, which is what
94
- * RFC 6902 requires when nothing is there to replace.
95
- */ const prepare = (operation, doc) => {
18
+ const prepare = (operation, doc) => {
96
19
  const cloned = "value" in operation ? {
97
20
  ...operation,
98
21
  value: structuredClone(operation.value)
@@ -102,49 +25,41 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
102
25
  op: "add"
103
26
  } : cloned;
104
27
  };
105
- /** The one it carries, or the one it takes from `from`. `remove` writes nothing. */ const effectiveValue = (operation, doc) => {
28
+ const effectiveValue = (operation, doc) => {
106
29
  if ("value" in operation) return operation.value;
107
30
  return "from" in operation ? Pointer.fromJSON(operation.from).get(doc) : void 0;
108
31
  };
109
- /** A pointer stopping short addresses a subtree; the fields beneath decide. */ const resolvesReadOnly = (resolution) => {
32
+ const resolvesReadOnly = (resolution) => {
110
33
  if (resolution.readOnly) return true;
111
34
  if (resolution.descriptor) return resolution.descriptor.readOnly === true;
112
- const below = describeAddressableFields(resolution.fields).filter((descriptor) => resolution.prefix.every((part, offset) => part === splitPath(descriptor.path)[offset]));
113
- return below.length > 0 && below.every((descriptor) => descriptor.readOnly);
35
+ const below = descriptorsUnder(resolution.fields, resolution.prefix);
36
+ return below.length > 0 && below.every(({ descriptor }) => descriptor.readOnly);
114
37
  };
115
- /** An element has no descriptor, so its field is read one segment up. */ const isReadOnlyPointer = (config, target) => resolvesReadOnly(resolveDataPointer(config, {
38
+ const isReadOnlyPointer = (config, target) => resolvesReadOnly(resolveDataPointer(config, {
116
39
  doc: target.doc,
117
40
  pointer: isElementPointer(target.pointer) ? joinPath(splitPath(target.pointer).slice(0, -1)) : target.pointer,
118
41
  ref: target.ref
119
42
  }));
120
- /** Unresolvable pointers say nothing here; the caller reports them anyway. */ const resolutionAt = (config, target) => {
43
+ const resolutionAt = (config, target) => {
121
44
  try {
122
45
  return resolveDataPointer(config, {
123
46
  doc: target.doc,
124
47
  pointer: target.pointer,
125
48
  ref: target.ref
126
49
  });
127
- } catch {
50
+ } catch (error) {
51
+ if (!(error instanceof SchemaError)) throw error;
128
52
  return;
129
53
  }
130
54
  };
131
- /**
132
- * Removing a field does nothing, but removing part of an editor state does
133
- * something, and something worse: a node written without the properties its
134
- * class hydrates from throws when the admin editor opens it. Both are refused,
135
- * and only the reason differs.
136
- */ const droppedFieldProblem = (config, target) => {
55
+ const droppedFieldProblem = (config, target) => {
137
56
  const resolution = resolutionAt(config, target);
138
57
  const lexical = resolution?.lexical;
139
58
  if (lexical?.kind === "property") return `"${target.pointer}" is a node property, not a list element. A "${lexical.nodeType}" node needs it, so replace it rather than removing it.`;
140
59
  if (lexical ?? resolution?.inLexical) return `"${target.pointer}" sits inside an editor state, which is written whole, so removing it would take effect and leave a node the admin editor cannot open. Replace it instead, or remove the node that holds it.`;
141
60
  return `"${target.pointer}" is a field, not a list element, and removing it would do nothing. The patched document is written whole, and Payload keeps any field absent from a write rather than clearing it. Use "replace" with null to clear a field, or with [] to empty a list.`;
142
61
  };
143
- /**
144
- * Dropping the only node under a root empties the state, which Lexical refuses
145
- * to hydrate. Checked here rather than on the written value, because a
146
- * `remove` carries none.
147
- */ const emptiesTheRoot = (config, target) => {
62
+ const emptiesTheRoot = (config, target) => {
148
63
  const list = joinPath(splitPath(target.pointer).slice(0, -1));
149
64
  const owner = resolutionAt(config, {
150
65
  ...target,
@@ -154,16 +69,14 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
154
69
  const nodes = Pointer.fromJSON(list).get(target.doc);
155
70
  return Array.isArray(nodes) && nodes.length === 1;
156
71
  };
157
- /**
158
- * Checked against the document as it stands when this operation runs: both
159
- * pointers must resolve, the written value must pass write validation, and what
160
- * it drops must not sit in a read-only field.
161
- */ const findOperationProblems = (config, target) => {
72
+ const findOperationProblems = (config, target) => {
162
73
  const { doc, operation, ref } = target;
163
74
  const pointers = [operation.path, ..."from" in operation ? [operation.from] : []];
164
75
  if (pointers.includes("")) return ["an empty pointer addresses the whole document. Address a field instead."];
76
+ const prototyped = pointers.map(prototypeSegmentProblem).find((problem) => problem !== void 0);
77
+ if (prototyped !== void 0) return [prototyped];
165
78
  const reserved = pointers.find(isReservedPointer);
166
- if (reserved !== void 0) return [`"${reserved}" addresses a field Payload maintains. This tool only ever writes drafts, and id, _status, createdAt and updatedAt are not writable; use publishDocument to publish.`];
79
+ if (reserved !== void 0) return [`"${reserved}" addresses a field Payload maintains. id, _status, createdAt and updatedAt are not writable, and "_status" changes only when a draft is published.`];
167
80
  const dropped = droppedPointer(operation);
168
81
  if (dropped !== void 0 && !isElementPointer(dropped)) return [droppedFieldProblem(config, {
169
82
  doc,
@@ -204,14 +117,14 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
204
117
  }
205
118
  return [];
206
119
  } catch (error) {
207
- return [error instanceof Error ? error.message : "invalid"];
120
+ if (!(error instanceof SchemaError)) throw error;
121
+ return [error.message];
208
122
  }
209
123
  };
210
124
  /**
211
- * One evolving copy, so an operation depending on an earlier one resolves
212
- * against the shape it actually modifies, and a failure leaves the original
213
- * untouched. The caller writes nothing unless the whole batch came back
214
- * applied, so a partial batch is never persisted.
125
+ * Works on one evolving copy, so each operation resolves against the result of
126
+ * the earlier ones and a failure leaves the original untouched. The caller
127
+ * writes only when the whole batch applied, so a partial batch is never saved.
215
128
  */ const applyPatchOperations = (config, target) => {
216
129
  const next = structuredClone(target.doc);
217
130
  for (const [index, operation] of target.patches.entries()) {
@@ -228,74 +141,5 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
228
141
  reconcileRowIds(next, target.doc);
229
142
  return { next };
230
143
  };
231
- const ROW_KEYS = /* @__PURE__ */ new Set([
232
- "blockName",
233
- "blockType",
234
- "id"
235
- ]);
236
- /**
237
- * Everything Payload maintains or derives (`_status`, timestamps, join and
238
- * virtual fields, upload base fields) is left out, so the write-back carries
239
- * only what a client could have set.
240
- */ const pickDescribed = (config, value, at) => {
241
- const { fields, prefix, isRow } = at;
242
- const relative = describeAddressableFields(fields).flatMap((descriptor) => {
243
- const parts = splitPath(descriptor.path);
244
- return prefix.every((part, offset) => part === parts[offset]) ? [{
245
- descriptor,
246
- parts: parts.slice(prefix.length)
247
- }] : [];
248
- });
249
- const result = {};
250
- if (isRow) {
251
- for (const key of ROW_KEYS) if (key in value) result[key] = value[key];
252
- }
253
- for (const [key, entry] of Object.entries(value)) {
254
- const candidates = relative.filter(({ parts }) => parts[0] === key);
255
- if (candidates.length === 0) continue;
256
- const exact = candidates.find(({ parts }) => parts.length === 1);
257
- if (exact?.descriptor.type === "blocks" && Array.isArray(entry)) {
258
- const field = findBlocksField(fields, splitPath(exact.descriptor.path));
259
- result[key] = entry.map((row) => {
260
- const block = field && isPlainObject(row) && typeof row["blockType"] === "string" ? blockOf(config, field, row["blockType"]) : void 0;
261
- return block && isPlainObject(row) ? pickDescribed(config, row, {
262
- fields: block.flattenedFields,
263
- prefix: [],
264
- isRow: true
265
- }) : row;
266
- });
267
- continue;
268
- }
269
- if (exact) {
270
- result[key] = entry;
271
- continue;
272
- }
273
- if (candidates.some(({ parts }) => parts[1] === "*")) {
274
- result[key] = Array.isArray(entry) ? entry.map((row) => isPlainObject(row) ? pickDescribed(config, row, {
275
- fields,
276
- prefix: [
277
- ...prefix,
278
- key,
279
- "*"
280
- ],
281
- isRow: true
282
- }) : row) : entry;
283
- continue;
284
- }
285
- result[key] = isPlainObject(entry) ? pickDescribed(config, entry, {
286
- fields,
287
- prefix: [...prefix, key],
288
- isRow: false
289
- }) : entry;
290
- }
291
- return result;
292
- };
293
- /** The patched document reduced to writable fields, plus row identity keys. */ const buildWriteData = (config, target, doc) => {
294
- return pickDescribed(config, doc, {
295
- fields: target.flattenedFields,
296
- prefix: [],
297
- isRow: false
298
- });
299
- };
300
144
  //#endregion
301
- export { PATCH_OPERATION_SCHEMA, applyPatchOperations, buildWriteData, droppedPointer, isElementPointer, isReservedPointer, stripRowIds };
145
+ export { applyPatchOperations, droppedPointer, isElementPointer, isReservedPointer };
@@ -1,21 +1,14 @@
1
- import { pointerFromPayloadPath } from "../schema/walk.mjs";
1
+ import { pointerFromPayloadPath } from "../schema/path.mjs";
2
2
  import "../schema/index.mjs";
3
3
  import { beforeChangeTraverseFields, beforeValidateTraverseFields } from "payload";
4
4
  //#region src/write/publish-blockers.ts
5
5
  /**
6
- * Runs Payload's own field validation over a draft without saving anything.
7
- *
8
- * The same traversal a real save runs, exported from `payload` and called with
9
- * `skipValidation: false` so it collects into `errors` instead of throwing. The
10
- * `beforeValidate` pass runs first because some field hooks (Lexical's) prepare
11
- * state in `context` that their `beforeChange` counterpart depends on.
12
- *
13
- * Nothing is written: `data` is a copy, the context is a scratch copy and the
14
- * locale merge actions are discarded. `overrideAccess` is true because the
15
- * question is "could this be published", not "may this client write it".
16
- *
17
- * `unavailable` marks a traversal that threw, which is not the same answer as a
18
- * document with nothing wrong with it.
6
+ * Payload's field validation over a draft, without saving: `data` and the
7
+ * context are copies. `beforeValidate` runs first because some field hooks
8
+ * (Lexical's) prepare `context` state that `beforeChange` needs. Access is
9
+ * overridden because the question is whether the draft could be published, not
10
+ * whether this client may write it. `unavailable` means the traversal threw,
11
+ * which is not the same as a clean document.
19
12
  */ const collectPublishBlockers = async (req, target) => {
20
13
  const { doc, entity } = target;
21
14
  const id = doc["id"];
@@ -1,3 +1,4 @@
1
+ import { isPlainObject } from "../guards.mjs";
1
2
  import { randomUUID } from "node:crypto";
2
3
  //#region src/write/publish-intent.ts
3
4
  const PUBLISH_INTENT = "__mcpxPublishIntent";
@@ -6,10 +7,11 @@ const withPublishIntent = (data) => ({
6
7
  ...data,
7
8
  [PUBLISH_INTENT]: TOKEN
8
9
  });
9
- const carries = (data) => typeof data === "object" && data !== null && data[PUBLISH_INTENT] === TOKEN;
10
- const hasPublishIntent = (data) => carries(data);
11
- /** Asked by the last hook that needs it, so it takes the marker off. */ const takePublishIntent = (data) => {
12
- if (!carries(data)) return false;
10
+ const hasPublishIntent = (data) => isPlainObject(data) && data[PUBLISH_INTENT] === TOKEN;
11
+ /**
12
+ * Called by the last hook that needs the marker, which removes it.
13
+ */ const takePublishIntent = (data) => {
14
+ if (!hasPublishIntent(data)) return false;
13
15
  delete data[PUBLISH_INTENT];
14
16
  return true;
15
17
  };
@@ -0,0 +1,46 @@
1
+ import { isPlainObject } from "../guards.mjs";
2
+ //#region src/write/row-ids.ts
3
+ const isRichTextState = (value) => isPlainObject(value["root"]) && Array.isArray(value["root"]["children"]);
4
+ const walkRows = (value, visit, isRow = false) => {
5
+ if (Array.isArray(value)) {
6
+ for (const entry of value) walkRows(entry, visit, true);
7
+ return;
8
+ }
9
+ if (!isPlainObject(value) || isRichTextState(value)) return;
10
+ if (isRow || typeof value["blockType"] === "string") visit(value);
11
+ for (const entry of Object.values(value)) walkRows(entry, visit);
12
+ };
13
+ /**
14
+ * Keeps a row id only when the stored document has it and no earlier row in the
15
+ * write claimed it. Other ids are dropped so Payload assigns a fresh one.
16
+ *
17
+ * A kept id makes Payload update the row in place, which preserves the other
18
+ * locales of localized fields inside it. A duplicated id (a copied row) or a
19
+ * foreign id would violate a SQL primary key.
20
+ */ const reconcileRowIds = (next, stored) => {
21
+ const known = /* @__PURE__ */ new Set();
22
+ walkRows(stored, (row) => {
23
+ if (row["id"] !== void 0) known.add(row["id"]);
24
+ });
25
+ const seen = /* @__PURE__ */ new Set();
26
+ walkRows(next, (row) => {
27
+ const id = row["id"];
28
+ if (id === void 0) return;
29
+ if (known.has(id) && !seen.has(id)) {
30
+ seen.add(id);
31
+ return;
32
+ }
33
+ delete row["id"];
34
+ });
35
+ };
36
+ /**
37
+ * For create, where no stored row exists and any incoming id is invented.
38
+ */ const stripRowIds = (value) => {
39
+ const next = structuredClone(value);
40
+ walkRows(next, (row) => {
41
+ delete row["id"];
42
+ });
43
+ return next;
44
+ };
45
+ //#endregion
46
+ export { reconcileRowIds, stripRowIds };
@@ -1,14 +1,11 @@
1
1
  import { commitTransaction, initTransaction, killTransaction } from "payload";
2
2
  //#region src/write/transaction.ts
3
3
  /**
4
- * Adapters without transaction support, or a request that already owns one, run
5
- * `fn` as is.
6
- *
7
- * Atomicity, not isolation: neither SQLite nor Postgres at read committed locks
8
- * the row on the read, so an `expectedUpdatedAt` check remains best effort. Nor
9
- * is this safe across the tool calls of one JSON-RPC batch, which share a
10
- * request: the second caller joins the first's transaction, so one tool's
11
- * rollback takes the other's work with it.
4
+ * Adapters without transaction support, or a request that already owns one,
5
+ * run `fn` as is. This gives atomicity, not isolation: SQLite and Postgres at
6
+ * read committed do not lock the row on read, so an `expectedUpdatedAt` check
7
+ * stays best effort. Tool calls in one JSON-RPC batch run one after another, so
8
+ * each owns its transaction and a rollback never undoes another call.
12
9
  */ const withTransaction = async (req, fn) => {
13
10
  if (!await initTransaction(req)) return await fn();
14
11
  try {
@@ -0,0 +1,64 @@
1
+ import { isPlainObject } from "../guards.mjs";
2
+ import { splitPath } from "../schema/path.mjs";
3
+ import { ROW_KEYS, blockOf, descriptorsUnder, findBlocksField } from "../schema/walk.mjs";
4
+ import "../schema/index.mjs";
5
+ //#region src/write/write-data.ts
6
+ const pickDescribed = (config, value, at) => {
7
+ const { fields, prefix, isRow } = at;
8
+ const relative = descriptorsUnder(fields, prefix);
9
+ const result = {};
10
+ if (isRow) {
11
+ for (const key of ROW_KEYS) if (Object.hasOwn(value, key)) result[key] = value[key];
12
+ }
13
+ for (const [key, entry] of Object.entries(value)) {
14
+ const candidates = relative.filter(({ parts }) => parts[0] === key);
15
+ if (candidates.length === 0) continue;
16
+ const exact = candidates.find(({ parts }) => parts.length === 1);
17
+ if (exact?.descriptor.type === "blocks" && Array.isArray(entry)) {
18
+ const field = findBlocksField(fields, splitPath(exact.descriptor.path));
19
+ result[key] = entry.map((row) => {
20
+ const block = field && isPlainObject(row) && typeof row["blockType"] === "string" ? blockOf(config, field, row["blockType"]) : void 0;
21
+ return block && isPlainObject(row) ? pickDescribed(config, row, {
22
+ fields: block.flattenedFields,
23
+ prefix: [],
24
+ isRow: true
25
+ }) : row;
26
+ });
27
+ continue;
28
+ }
29
+ if (exact) {
30
+ result[key] = entry;
31
+ continue;
32
+ }
33
+ if (candidates.some(({ parts }) => parts[1] === "*")) {
34
+ result[key] = Array.isArray(entry) ? entry.map((row) => isPlainObject(row) ? pickDescribed(config, row, {
35
+ fields,
36
+ prefix: [
37
+ ...prefix,
38
+ key,
39
+ "*"
40
+ ],
41
+ isRow: true
42
+ }) : row) : entry;
43
+ continue;
44
+ }
45
+ result[key] = isPlainObject(entry) ? pickDescribed(config, entry, {
46
+ fields,
47
+ prefix: [...prefix, key],
48
+ isRow: false
49
+ }) : entry;
50
+ }
51
+ return result;
52
+ };
53
+ /**
54
+ * The patched document reduced to writable fields, plus row identity keys.
55
+ * `target` is a sanitized collection or global alike.
56
+ */ const buildWriteData = (config, target, doc) => {
57
+ return pickDescribed(config, doc, {
58
+ fields: target.flattenedFields,
59
+ prefix: [],
60
+ isRow: false
61
+ });
62
+ };
63
+ //#endregion
64
+ export { buildWriteData };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@abinnovision/payloadcms-mcpx",
4
- "version": "1.1.0-beta.6",
4
+ "version": "2.0.0-beta.10",
5
5
  "description": "Payload CMS plugin exposing a fixed, schema-aware MCP tool surface with draft-only writes and per-API-key capabilities.",
6
6
  "keywords": [
7
7
  "payload",
@@ -27,9 +27,9 @@
27
27
  "types": "./dist/index.d.mts",
28
28
  "default": "./dist/index.mjs"
29
29
  },
30
- "./client": {
31
- "types": "./dist/client/index.d.mts",
32
- "default": "./dist/client/index.mjs"
30
+ "./admin": {
31
+ "types": "./dist/admin/index.d.mts",
32
+ "default": "./dist/admin/index.mjs"
33
33
  }
34
34
  },
35
35
  "files": [
@@ -65,6 +65,7 @@
65
65
  "@abinnovision/eslint-config-base": "^3.4.2",
66
66
  "@abinnovision/prettier-config": "^2.2.0",
67
67
  "@arethetypeswrong/core": "^0.18.5",
68
+ "@internal/test-utils": "0.0.0",
68
69
  "@payloadcms/db-sqlite": "3.89.0",
69
70
  "@payloadcms/richtext-lexical": "3.89.0",
70
71
  "@payloadcms/ui": "3.89.0",
@@ -101,4 +102,4 @@
101
102
  "npm": true,
102
103
  "npmAccess": "public"
103
104
  }
104
- }
105
+ }
@@ -1,26 +0,0 @@
1
- import { CapabilityMatrix } from "../api-keys/capability-matrix.mjs";
2
- import React from "react";
3
- //#region src/client/capability-matrix.d.ts
4
- interface McpxCapabilityMatrixProps {
5
- /** Built from the plugin options at config time. */
6
- matrix: CapabilityMatrix;
7
- /** The group's own config, supplied by Payload. Carries the description. */
8
- field?: {
9
- admin?: {
10
- description?: unknown;
11
- };
12
- };
13
- /** The `capabilities` group's own path, supplied by Payload. */
14
- path?: string;
15
- readOnly?: boolean;
16
- /** Whether the key form is split into tabs, from the plugin options. */
17
- withinTab?: boolean;
18
- }
19
- /**
20
- * What an API key may do, as one table per namespace: a row per entity, a
21
- * column per operation, and a bulk toggle in each column header. The nested
22
- * checkbox fields this replaces still back every cell, so the stored document
23
- * is unchanged; only the rendering is.
24
- */
25
- export declare const McpxCapabilityMatrix: React.FC<McpxCapabilityMatrixProps>;
26
- //#endregion