@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.
- package/README.md +140 -470
- package/dist/admin/capability-matrix.d.mts +33 -0
- package/dist/admin/capability-matrix.mjs +354 -0
- package/dist/admin/capability-toggles.mjs +73 -0
- package/dist/{client → admin}/setup-guide.d.mts +4 -2
- package/dist/{client → admin}/setup-guide.mjs +3 -10
- package/dist/api-keys/capability-matrix.d.mts +12 -8
- package/dist/api-keys/capability-matrix.mjs +17 -85
- package/dist/api-keys/collection.mjs +2 -1
- package/dist/api-keys/fields.mjs +36 -24
- package/dist/auth/resolve.mjs +44 -6
- package/dist/{tools/names.mjs → builtin-tool-names.mjs} +1 -1
- package/dist/capabilities.mjs +41 -43
- package/dist/define-tool.d.mts +23 -0
- package/dist/{types.mjs → define-tool.mjs} +1 -1
- package/dist/endpoint/errors.mjs +5 -6
- package/dist/endpoint/handler.mjs +49 -19
- package/dist/endpoint/server.mjs +72 -20
- package/dist/entity.d.mts +9 -0
- package/dist/guards.mjs +7 -0
- package/dist/i18n.mjs +10 -8
- package/dist/index.d.mts +5 -2
- package/dist/index.mjs +3 -2
- package/dist/options.mjs +52 -46
- package/dist/plugin.mjs +0 -1
- package/dist/request.d.mts +2 -3
- package/dist/request.mjs +2 -3
- package/dist/result.d.mts +2 -3
- package/dist/result.mjs +3 -3
- package/dist/schema/admin-hidden.mjs +91 -0
- package/dist/schema/describe.mjs +17 -33
- package/dist/schema/errors.mjs +8 -0
- package/dist/schema/index.mjs +5 -2
- package/dist/schema/lexical-pointer.mjs +18 -20
- package/dist/schema/lexical.mjs +44 -93
- package/dist/schema/outline.mjs +5 -15
- package/dist/schema/path.mjs +28 -0
- package/dist/schema/pointer.mjs +18 -25
- package/dist/schema/shape.mjs +25 -71
- package/dist/schema/walk.mjs +43 -45
- package/dist/tools/builtin.mjs +4 -4
- package/dist/tools/create-document.mjs +26 -23
- package/dist/tools/describe-schema.mjs +38 -34
- package/dist/tools/document.mjs +124 -0
- package/dist/tools/find-documents.mjs +32 -19
- package/dist/tools/find-versions.mjs +22 -18
- package/dist/tools/get-document.mjs +39 -32
- package/dist/tools/list-capabilities.mjs +23 -16
- package/dist/tools/patch-document.mjs +75 -49
- package/dist/tools/publish-document.mjs +26 -32
- package/dist/tools/query-paths.mjs +57 -0
- package/dist/tools/read-request.d.mts +10 -0
- package/dist/tools/read-request.mjs +43 -0
- package/dist/tools/shared.mjs +67 -99
- package/dist/tools/validate-document.mjs +18 -30
- package/dist/tools/versions.mjs +37 -20
- package/dist/types.d.mts +118 -74
- package/dist/write/draft-guard.mjs +23 -41
- package/dist/write/patch.mjs +27 -183
- package/dist/write/publish-blockers.mjs +7 -14
- package/dist/write/publish-intent.mjs +6 -4
- package/dist/write/row-ids.mjs +46 -0
- package/dist/write/transaction.mjs +5 -8
- package/dist/write/write-data.mjs +64 -0
- package/package.json +6 -5
- package/dist/client/capability-matrix.d.mts +0 -26
- package/dist/client/capability-matrix.mjs +0 -349
- package/dist/endpoint/index.mjs +0 -4
- package/dist/tools/target.mjs +0 -45
- /package/dist/{client → admin}/index.d.mts +0 -0
- /package/dist/{client → admin}/index.mjs +0 -0
package/dist/write/patch.mjs
CHANGED
|
@@ -1,98 +1,21 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
7
|
+
import { reconcileRowIds } from "./row-ids.mjs";
|
|
6
8
|
import { Pointer, applyPatch } from "rfc6902";
|
|
7
9
|
//#region src/write/patch.ts
|
|
8
|
-
const
|
|
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
|
|
16
|
+
return isIndexSegment(splitPath(pointer).at(-1) ?? "");
|
|
47
17
|
};
|
|
48
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
32
|
+
const resolvesReadOnly = (resolution) => {
|
|
110
33
|
if (resolution.readOnly) return true;
|
|
111
34
|
if (resolution.descriptor) return resolution.descriptor.readOnly === true;
|
|
112
|
-
const below =
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
120
|
+
if (!(error instanceof SchemaError)) throw error;
|
|
121
|
+
return [error.message];
|
|
208
122
|
}
|
|
209
123
|
};
|
|
210
124
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
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 {
|
|
145
|
+
export { applyPatchOperations, droppedPointer, isElementPointer, isReservedPointer };
|
|
@@ -1,21 +1,14 @@
|
|
|
1
|
-
import { pointerFromPayloadPath } from "../schema/
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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,
|
|
5
|
-
* `fn` as is.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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": "
|
|
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
|
-
"./
|
|
31
|
-
"types": "./dist/
|
|
32
|
-
"default": "./dist/
|
|
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
|