@abinnovision/payloadcms-mcpx 1.0.0-beta.9 → 1.0.0
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/CHANGELOG.md +33 -0
- package/README.md +328 -211
- package/dist/api-keys/fields.mjs +22 -5
- package/dist/api-keys/setup-guide.mjs +6 -4
- package/dist/auth/resolve.mjs +5 -7
- package/dist/capabilities.mjs +23 -4
- package/dist/client/index.d.mts +2 -2
- package/dist/client/setup-guide.d.mts +1 -1
- package/dist/endpoint/{result.mjs → errors.mjs} +4 -23
- package/dist/endpoint/handler.mjs +11 -5
- package/dist/endpoint/index.mjs +4 -0
- package/dist/endpoint/server.mjs +18 -26
- package/dist/i18n.mjs +4 -15
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +4 -3
- package/dist/options.mjs +30 -26
- package/dist/plugin.mjs +1 -0
- package/dist/{write/draft-guard.d.mts → request.d.mts} +2 -2
- package/dist/request.mjs +8 -0
- package/dist/result.d.mts +11 -0
- package/dist/result.mjs +20 -0
- package/dist/schema/describe.mjs +3 -15
- package/dist/schema/index.mjs +8 -0
- package/dist/schema/lexical-pointer.mjs +125 -0
- package/dist/schema/lexical.mjs +195 -27
- package/dist/schema/outline.mjs +67 -0
- package/dist/schema/pointer.mjs +77 -30
- package/dist/schema/shape.mjs +133 -51
- package/dist/schema/walk.mjs +44 -64
- package/dist/tools/{index.mjs → builtin.mjs} +8 -5
- package/dist/tools/create-document.mjs +34 -15
- package/dist/tools/describe-schema.mjs +21 -7
- package/dist/tools/find-documents.mjs +13 -6
- package/dist/tools/get-document.mjs +45 -11
- package/dist/tools/list-capabilities.mjs +19 -9
- package/dist/tools/names.mjs +2 -1
- package/dist/tools/patch-document.mjs +32 -21
- package/dist/tools/publish-document.mjs +79 -0
- package/dist/tools/shared.mjs +84 -32
- package/dist/tools/target.mjs +7 -11
- package/dist/tools/validate-document.mjs +20 -12
- package/dist/types.d.mts +110 -42
- package/dist/types.mjs +3 -4
- package/dist/version.mjs +1 -1
- package/dist/write/draft-guard.mjs +47 -44
- package/dist/write/patch.mjs +174 -92
- package/dist/write/publish-blockers.mjs +13 -12
- package/dist/write/publish-intent.mjs +17 -0
- package/dist/write/transaction.mjs +8 -3
- package/package.json +3 -3
- package/dist/i18n.d.mts +0 -1
- package/dist/options.d.mts +0 -2
- package/dist/schema/lexical.d.mts +0 -1
- package/dist/schema/walk.d.mts +0 -3
- package/dist/tools/target.d.mts +0 -3
- package/dist/tools/types.d.mts +0 -5
- package/dist/write/publish-blockers.d.mts +0 -15
package/dist/write/patch.mjs
CHANGED
|
@@ -1,49 +1,53 @@
|
|
|
1
|
-
import { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, describeAddressableFields, findBlocksField, splitPath } from "../schema/walk.mjs";
|
|
2
|
-
import { validateWriteValue } from "../schema/shape.mjs";
|
|
1
|
+
import { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, describeAddressableFields, findBlocksField, isIndexSegment, joinPath, splitPath } from "../schema/walk.mjs";
|
|
3
2
|
import { resolveDataPointer } from "../schema/pointer.mjs";
|
|
3
|
+
import { EMPTY_ROOT, validateWriteValue } from "../schema/shape.mjs";
|
|
4
|
+
import "../schema/index.mjs";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
import { Pointer, applyPatch } from "rfc6902";
|
|
6
7
|
//#region src/write/patch.ts
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"remove",
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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));
|
|
41
|
+
const droppedPointer = (operation) => {
|
|
28
42
|
if (operation.op === "remove") return operation.path;
|
|
29
43
|
return operation.op === "move" ? operation.from : void 0;
|
|
30
44
|
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
*/ const isElementPointer = (pointer) => {
|
|
34
|
-
const last = pointer.split("/").pop() ?? "";
|
|
35
|
-
return last === "-" || /^\d+$/.test(last);
|
|
45
|
+
const isElementPointer = (pointer) => {
|
|
46
|
+
return isIndexSegment(pointer.split("/").pop() ?? "");
|
|
36
47
|
};
|
|
37
48
|
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
* descended into.
|
|
41
|
-
*/ const isRichTextState = (value) => isPlainObject(value["root"]) && Array.isArray(value["root"]["children"]);
|
|
42
|
-
/**
|
|
43
|
-
* Visits every row in a value: a plain object that carries `blockType` or
|
|
44
|
-
* sits directly inside an array. Rich text states manage their own nodes and
|
|
45
|
-
* are never descended into.
|
|
46
|
-
*/ const walkRows = (value, visit, isRow = false) => {
|
|
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) => {
|
|
47
51
|
if (Array.isArray(value)) {
|
|
48
52
|
for (const entry of value) walkRows(entry, visit, true);
|
|
49
53
|
return;
|
|
@@ -76,10 +80,7 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
|
|
|
76
80
|
delete row["id"];
|
|
77
81
|
});
|
|
78
82
|
};
|
|
79
|
-
/**
|
|
80
|
-
* Drops every row id from a copy of `value`. Used on create, where no stored
|
|
81
|
-
* row exists and any incoming id is client-invented.
|
|
82
|
-
*/ const stripRowIds = (value) => {
|
|
83
|
+
/** For create, where no stored row exists and any incoming id is invented. */ const stripRowIds = (value) => {
|
|
83
84
|
const next = structuredClone(value);
|
|
84
85
|
walkRows(next, (row) => {
|
|
85
86
|
delete row["id"];
|
|
@@ -87,71 +88,155 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
|
|
|
87
88
|
return next;
|
|
88
89
|
};
|
|
89
90
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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) => {
|
|
96
|
+
const cloned = "value" in operation ? {
|
|
97
|
+
...operation,
|
|
98
|
+
value: structuredClone(operation.value)
|
|
99
|
+
} : operation;
|
|
100
|
+
return cloned.op === "replace" && !isElementPointer(cloned.path) && Pointer.fromJSON(cloned.path).get(doc) === void 0 ? {
|
|
101
|
+
...cloned,
|
|
102
|
+
op: "add"
|
|
103
|
+
} : cloned;
|
|
104
|
+
};
|
|
105
|
+
/** The one it carries, or the one it takes from `from`. `remove` writes nothing. */ const effectiveValue = (operation, doc) => {
|
|
106
|
+
if ("value" in operation) return operation.value;
|
|
107
|
+
return "from" in operation ? Pointer.fromJSON(operation.from).get(doc) : void 0;
|
|
108
|
+
};
|
|
109
|
+
/** A pointer stopping short addresses a subtree; the fields beneath decide. */ const resolvesReadOnly = (resolution) => {
|
|
110
|
+
if (resolution.readOnly) return true;
|
|
111
|
+
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);
|
|
114
|
+
};
|
|
115
|
+
/** An element has no descriptor, so its field is read one segment up. */ const isReadOnlyPointer = (config, target) => resolvesReadOnly(resolveDataPointer(config, {
|
|
116
|
+
doc: target.doc,
|
|
117
|
+
pointer: isElementPointer(target.pointer) ? joinPath(splitPath(target.pointer).slice(0, -1)) : target.pointer,
|
|
118
|
+
ref: target.ref
|
|
119
|
+
}));
|
|
120
|
+
/** Unresolvable pointers say nothing here; the caller reports them anyway. */ const resolutionAt = (config, target) => {
|
|
121
|
+
try {
|
|
122
|
+
return resolveDataPointer(config, {
|
|
123
|
+
doc: target.doc,
|
|
124
|
+
pointer: target.pointer,
|
|
125
|
+
ref: target.ref
|
|
126
|
+
});
|
|
127
|
+
} catch {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
109
130
|
};
|
|
110
131
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*/ const
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
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) => {
|
|
137
|
+
const resolution = resolutionAt(config, target);
|
|
138
|
+
const lexical = resolution?.lexical;
|
|
139
|
+
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
|
+
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
|
+
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
|
+
};
|
|
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) => {
|
|
148
|
+
const list = joinPath(splitPath(target.pointer).slice(0, -1));
|
|
149
|
+
const owner = resolutionAt(config, {
|
|
150
|
+
...target,
|
|
151
|
+
pointer: list
|
|
152
|
+
})?.lexical;
|
|
153
|
+
if (owner?.kind !== "nodes" || owner.isRoot !== true) return false;
|
|
154
|
+
const nodes = Pointer.fromJSON(list).get(target.doc);
|
|
155
|
+
return Array.isArray(nodes) && nodes.length === 1;
|
|
156
|
+
};
|
|
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) => {
|
|
162
|
+
const { doc, operation, ref } = target;
|
|
163
|
+
const pointers = [operation.path, ..."from" in operation ? [operation.from] : []];
|
|
164
|
+
if (pointers.includes("")) return ["an empty pointer addresses the whole document. Address a field instead."];
|
|
119
165
|
const reserved = pointers.find(isReservedPointer);
|
|
120
|
-
if (reserved !== void 0) return [
|
|
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.`];
|
|
121
167
|
const dropped = droppedPointer(operation);
|
|
122
|
-
if (dropped !== void 0 && !isElementPointer(dropped)) return [
|
|
123
|
-
|
|
168
|
+
if (dropped !== void 0 && !isElementPointer(dropped)) return [droppedFieldProblem(config, {
|
|
169
|
+
doc,
|
|
170
|
+
pointer: dropped,
|
|
171
|
+
ref
|
|
172
|
+
})];
|
|
124
173
|
try {
|
|
125
|
-
const
|
|
174
|
+
const value = effectiveValue(operation, doc);
|
|
175
|
+
if (value !== void 0 && operation.op !== "test" && isReadOnlyPointer(config, {
|
|
176
|
+
doc,
|
|
177
|
+
pointer: operation.path,
|
|
178
|
+
ref
|
|
179
|
+
})) return [`"${operation.path}" is read-only and cannot be written.`];
|
|
180
|
+
if (dropped !== void 0 && isReadOnlyPointer(config, {
|
|
181
|
+
doc,
|
|
182
|
+
pointer: dropped,
|
|
183
|
+
ref
|
|
184
|
+
})) return [`"${dropped}" sits in a read-only field and cannot be removed.`];
|
|
185
|
+
if (dropped !== void 0 && emptiesTheRoot(config, {
|
|
186
|
+
doc,
|
|
187
|
+
pointer: dropped,
|
|
188
|
+
ref
|
|
189
|
+
})) return [`"${dropped}": ${EMPTY_ROOT}`];
|
|
126
190
|
for (const pointer of pointers) {
|
|
127
191
|
const resolution = resolveDataPointer(config, {
|
|
128
|
-
addedValue: value
|
|
129
|
-
doc
|
|
192
|
+
addedValue: value,
|
|
193
|
+
doc,
|
|
130
194
|
pointer,
|
|
131
|
-
ref
|
|
195
|
+
ref
|
|
132
196
|
});
|
|
133
|
-
if (pointer === operation.path && value !== void 0)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
197
|
+
if (pointer === operation.path && value !== void 0) {
|
|
198
|
+
const problems = validateWriteValue(config, {
|
|
199
|
+
pointer,
|
|
200
|
+
resolution
|
|
201
|
+
}, value);
|
|
202
|
+
if (problems.length > 0) return problems;
|
|
203
|
+
}
|
|
137
204
|
}
|
|
138
205
|
return [];
|
|
139
206
|
} catch (error) {
|
|
140
|
-
return [
|
|
207
|
+
return [error instanceof Error ? error.message : "invalid"];
|
|
141
208
|
}
|
|
142
|
-
}
|
|
209
|
+
};
|
|
143
210
|
/**
|
|
144
|
-
*
|
|
145
|
-
|
|
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.
|
|
215
|
+
*/ const applyPatchOperations = (config, target) => {
|
|
216
|
+
const next = structuredClone(target.doc);
|
|
217
|
+
for (const [index, operation] of target.patches.entries()) {
|
|
218
|
+
const at = `patches[${String(index)}]`;
|
|
219
|
+
const problems = findOperationProblems(config, {
|
|
220
|
+
doc: next,
|
|
221
|
+
operation,
|
|
222
|
+
ref: target.ref
|
|
223
|
+
});
|
|
224
|
+
if (problems.length > 0) return { problems: problems.map((problem) => `${at}: ${problem}`) };
|
|
225
|
+
const [error] = applyPatch(next, [prepare(operation, next)]);
|
|
226
|
+
if (error) return { problems: [`${at}: ${error.message}`] };
|
|
227
|
+
}
|
|
228
|
+
reconcileRowIds(next, target.doc);
|
|
229
|
+
return { next };
|
|
230
|
+
};
|
|
231
|
+
const ROW_KEYS = /* @__PURE__ */ new Set([
|
|
146
232
|
"blockName",
|
|
147
233
|
"blockType",
|
|
148
234
|
"id"
|
|
149
235
|
]);
|
|
150
236
|
/**
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* is left out, so the write-back carries only what a client could have set.
|
|
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.
|
|
155
240
|
*/ const pickDescribed = (config, value, at) => {
|
|
156
241
|
const { fields, prefix, isRow } = at;
|
|
157
242
|
const relative = describeAddressableFields(fields).flatMap((descriptor) => {
|
|
@@ -205,10 +290,7 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
|
|
|
205
290
|
}
|
|
206
291
|
return result;
|
|
207
292
|
};
|
|
208
|
-
/**
|
|
209
|
-
* The data handed to `payload.update` after a patch: the patched document
|
|
210
|
-
* reduced to the fields the client may write, plus row identity keys.
|
|
211
|
-
*/ const buildWriteData = (config, target, doc) => {
|
|
293
|
+
/** The patched document reduced to writable fields, plus row identity keys. */ const buildWriteData = (config, target, doc) => {
|
|
212
294
|
return pickDescribed(config, doc, {
|
|
213
295
|
fields: target.flattenedFields,
|
|
214
296
|
prefix: [],
|
|
@@ -216,4 +298,4 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
|
|
|
216
298
|
});
|
|
217
299
|
};
|
|
218
300
|
//#endregion
|
|
219
|
-
export { PATCH_OPERATION_SCHEMA,
|
|
301
|
+
export { PATCH_OPERATION_SCHEMA, applyPatchOperations, buildWriteData, droppedPointer, isElementPointer, isReservedPointer, stripRowIds };
|
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
import { pointerFromPayloadPath } from "../schema/walk.mjs";
|
|
2
|
+
import "../schema/index.mjs";
|
|
2
3
|
import { beforeChangeTraverseFields, beforeValidateTraverseFields } from "payload";
|
|
3
4
|
//#region src/write/publish-blockers.ts
|
|
4
5
|
/**
|
|
5
6
|
* Runs Payload's own field validation over a draft without saving anything.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `
|
|
11
|
-
* instead of throwing. The `beforeValidate` pass runs first because some field
|
|
12
|
-
* hooks (Lexical's) prepare state in `context` that their `beforeChange`
|
|
13
|
-
* counterpart depends on.
|
|
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.
|
|
14
12
|
*
|
|
15
13
|
* Nothing is written: `data` is a copy, the context is a scratch copy and the
|
|
16
14
|
* locale merge actions are discarded. `overrideAccess` is true because the
|
|
17
15
|
* question is "could this be published", not "may this client write it".
|
|
18
16
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* `unavailable` marks a traversal that threw, which is not the same answer as a
|
|
18
|
+
* document with nothing wrong with it.
|
|
21
19
|
*/ const collectPublishBlockers = async (req, target) => {
|
|
22
20
|
const { doc, entity } = target;
|
|
23
21
|
const id = doc["id"];
|
|
@@ -62,13 +60,16 @@ import { beforeChangeTraverseFields, beforeValidateTraverseFields } from "payloa
|
|
|
62
60
|
});
|
|
63
61
|
} catch (error) {
|
|
64
62
|
req.payload.logger.warn(`[payloadcms-mcpx] Could not validate the ${entity.slug} draft: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
65
|
-
return
|
|
63
|
+
return {
|
|
64
|
+
blockers: [],
|
|
65
|
+
unavailable: true
|
|
66
|
+
};
|
|
66
67
|
}
|
|
67
|
-
return errors.map((error) => ({
|
|
68
|
+
return { blockers: errors.map((error) => ({
|
|
68
69
|
message: error.message,
|
|
69
70
|
path: pointerFromPayloadPath(error.path),
|
|
70
71
|
...typeof error.label === "string" ? { field: error.label } : {}
|
|
71
|
-
}));
|
|
72
|
+
})) };
|
|
72
73
|
};
|
|
73
74
|
//#endregion
|
|
74
75
|
export { collectPublishBlockers };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
//#region src/write/publish-intent.ts
|
|
3
|
+
const PUBLISH_INTENT = "__mcpxPublishIntent";
|
|
4
|
+
const TOKEN = randomUUID();
|
|
5
|
+
const withPublishIntent = (data) => ({
|
|
6
|
+
...data,
|
|
7
|
+
[PUBLISH_INTENT]: TOKEN
|
|
8
|
+
});
|
|
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;
|
|
13
|
+
delete data[PUBLISH_INTENT];
|
|
14
|
+
return true;
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { hasPublishIntent, takePublishIntent, withPublishIntent };
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { commitTransaction, initTransaction, killTransaction } from "payload";
|
|
2
2
|
//#region src/write/transaction.ts
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|
|
7
12
|
*/ const withTransaction = async (req, fn) => {
|
|
8
13
|
if (!await initTransaction(req)) return await fn();
|
|
9
14
|
try {
|
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.0.0
|
|
4
|
+
"version": "1.0.0",
|
|
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",
|
|
@@ -69,10 +69,10 @@
|
|
|
69
69
|
"@payloadcms/richtext-lexical": "3.88.0",
|
|
70
70
|
"@payloadcms/ui": "3.88.0",
|
|
71
71
|
"@swc/core": "^1.16.1",
|
|
72
|
-
"@types/node": "^26.
|
|
72
|
+
"@types/node": "^26.3.0",
|
|
73
73
|
"@types/react": "^19.2.18",
|
|
74
74
|
"@vitest/coverage-v8": "^4.1.10",
|
|
75
|
-
"eslint": "^10.
|
|
75
|
+
"eslint": "^10.9.1",
|
|
76
76
|
"payload": "3.88.0",
|
|
77
77
|
"prettier": "^3.9.5",
|
|
78
78
|
"publint": "^0.3.24",
|
package/dist/i18n.d.mts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import { PayloadRequest } from "payload";
|
package/dist/options.d.mts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import "payload";
|
package/dist/schema/walk.d.mts
DELETED
package/dist/tools/target.d.mts
DELETED
package/dist/tools/types.d.mts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import "../tools/target.mjs";
|
|
2
|
-
import { PayloadRequest } from "payload";
|
|
3
|
-
//#region src/write/publish-blockers.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* One reason a human could not publish the draft as it stands.
|
|
6
|
-
*/
|
|
7
|
-
interface PublishBlocker {
|
|
8
|
-
/** Resolved field label path, e.g. "Layout > Block 2 (Hero) > Title". */
|
|
9
|
-
field?: string;
|
|
10
|
-
message: string;
|
|
11
|
-
/** JSON Pointer to the offending value, e.g. "/layout/2/title". */
|
|
12
|
-
path: string;
|
|
13
|
-
}
|
|
14
|
-
//#endregion
|
|
15
|
-
export type { PublishBlocker };
|