@abinnovision/payloadcms-mcpx 1.0.0-beta.13 → 1.0.0-beta.14
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 +216 -166
- package/dist/api-keys/fields.mjs +7 -3
- package/dist/api-keys/setup-guide.mjs +6 -4
- package/dist/auth/resolve.mjs +1 -3
- package/dist/capabilities.mjs +8 -8
- package/dist/endpoint/errors.mjs +0 -2
- package/dist/endpoint/server.mjs +6 -20
- package/dist/i18n.mjs +4 -15
- package/dist/options.mjs +7 -20
- package/dist/result.d.mts +3 -5
- package/dist/result.mjs +3 -5
- package/dist/schema/describe.mjs +3 -15
- package/dist/schema/lexical.mjs +9 -28
- package/dist/schema/pointer.mjs +5 -11
- package/dist/schema/shape.mjs +5 -14
- package/dist/schema/walk.mjs +36 -54
- package/dist/tools/builtin.mjs +4 -6
- package/dist/tools/create-document.mjs +19 -6
- package/dist/tools/describe-schema.mjs +7 -1
- package/dist/tools/find-documents.mjs +8 -1
- package/dist/tools/get-document.mjs +5 -1
- package/dist/tools/list-capabilities.mjs +10 -2
- package/dist/tools/patch-document.mjs +7 -1
- package/dist/tools/publish-document.mjs +13 -15
- package/dist/tools/shared.mjs +39 -37
- package/dist/tools/target.mjs +3 -7
- package/dist/tools/validate-document.mjs +9 -1
- package/dist/types.d.mts +38 -77
- package/dist/write/draft-guard.mjs +33 -65
- package/dist/write/patch.mjs +22 -60
- package/dist/write/publish-blockers.mjs +6 -12
- package/dist/write/publish-intent.mjs +13 -35
- package/dist/write/transaction.mjs +2 -3
- package/package.json +1 -1
package/dist/write/patch.mjs
CHANGED
|
@@ -6,10 +6,7 @@ import { z } from "zod";
|
|
|
6
6
|
import { Pointer, applyPatch } from "rfc6902";
|
|
7
7
|
//#region src/write/patch.ts
|
|
8
8
|
const POINTER = z.string().regex(JSON_POINTER_PATTERN);
|
|
9
|
-
/**
|
|
10
|
-
* One RFC 6902 operation as accepted by `patchDocument`. Discriminated on `op`
|
|
11
|
-
* so an operation carries only the members RFC 6902 defines for it.
|
|
12
|
-
*/ const PATCH_OPERATION_SCHEMA = z.discriminatedUnion("op", [
|
|
9
|
+
/** Discriminated on `op`, so an operation carries only its own members. */ const PATCH_OPERATION_SCHEMA = z.discriminatedUnion("op", [
|
|
13
10
|
z.strictObject({
|
|
14
11
|
op: z.literal("add"),
|
|
15
12
|
path: POINTER,
|
|
@@ -40,31 +37,18 @@ const POINTER = z.string().regex(JSON_POINTER_PATTERN);
|
|
|
40
37
|
value: z.unknown()
|
|
41
38
|
})
|
|
42
39
|
]).describe("An RFC 6902 operation.");
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
*/ const isReservedPointer = (pointer) => pointer.split("/").slice(1).some((segment) => RESERVED_FIELD_NAMES.has(segment));
|
|
46
|
-
/**
|
|
47
|
-
* The pointer an operation removes a value from, if it removes one at all.
|
|
48
|
-
*/ const droppedPointer = (operation) => {
|
|
40
|
+
const isReservedPointer = (pointer) => pointer.split("/").slice(1).some((segment) => RESERVED_FIELD_NAMES.has(segment));
|
|
41
|
+
const droppedPointer = (operation) => {
|
|
49
42
|
if (operation.op === "remove") return operation.path;
|
|
50
43
|
return operation.op === "move" ? operation.from : void 0;
|
|
51
44
|
};
|
|
52
|
-
|
|
53
|
-
* Whether a pointer addresses a list element rather than a field.
|
|
54
|
-
*/ const isElementPointer = (pointer) => {
|
|
45
|
+
const isElementPointer = (pointer) => {
|
|
55
46
|
const last = pointer.split("/").pop() ?? "";
|
|
56
47
|
return last === "-" || /^\d+$/.test(last);
|
|
57
48
|
};
|
|
58
49
|
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
* descended into.
|
|
62
|
-
*/ const isRichTextState = (value) => isPlainObject(value["root"]) && Array.isArray(value["root"]["children"]);
|
|
63
|
-
/**
|
|
64
|
-
* Visits every row in a value: a plain object that carries `blockType` or
|
|
65
|
-
* sits directly inside an array. Rich text states manage their own nodes and
|
|
66
|
-
* are never descended into.
|
|
67
|
-
*/ const walkRows = (value, visit, isRow = false) => {
|
|
50
|
+
/** Its nodes manage their own ids, so it is never descended into. */ const isRichTextState = (value) => isPlainObject(value["root"]) && Array.isArray(value["root"]["children"]);
|
|
51
|
+
/** A row is a plain object carrying `blockType`, or one sitting in an array. */ const walkRows = (value, visit, isRow = false) => {
|
|
68
52
|
if (Array.isArray(value)) {
|
|
69
53
|
for (const entry of value) walkRows(entry, visit, true);
|
|
70
54
|
return;
|
|
@@ -97,10 +81,7 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
|
|
|
97
81
|
delete row["id"];
|
|
98
82
|
});
|
|
99
83
|
};
|
|
100
|
-
/**
|
|
101
|
-
* Drops every row id from a copy of `value`. Used on create, where no stored
|
|
102
|
-
* row exists and any incoming id is client-invented.
|
|
103
|
-
*/ const stripRowIds = (value) => {
|
|
84
|
+
/** For create, where no stored row exists and any incoming id is invented. */ const stripRowIds = (value) => {
|
|
104
85
|
const next = structuredClone(value);
|
|
105
86
|
walkRows(next, (row) => {
|
|
106
87
|
delete row["id"];
|
|
@@ -122,34 +103,24 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
|
|
|
122
103
|
op: "add"
|
|
123
104
|
} : cloned;
|
|
124
105
|
};
|
|
125
|
-
/**
|
|
126
|
-
* The value an operation writes at its path: the one it carries, or the one it
|
|
127
|
-
* takes from `from`. A `remove` writes nothing.
|
|
128
|
-
*/ const effectiveValue = (operation, doc) => {
|
|
106
|
+
/** The one it carries, or the one it takes from `from`. `remove` writes nothing. */ const effectiveValue = (operation, doc) => {
|
|
129
107
|
if ("value" in operation) return operation.value;
|
|
130
108
|
return "from" in operation ? Pointer.fromJSON(operation.from).get(doc) : void 0;
|
|
131
109
|
};
|
|
132
|
-
/**
|
|
133
|
-
* Whether a resolved pointer lands in a read-only field. A pointer that stops
|
|
134
|
-
* short of one addresses a subtree, and the fields beneath it decide.
|
|
135
|
-
*/ const resolvesReadOnly = (resolution) => {
|
|
110
|
+
/** A pointer stopping short addresses a subtree; the fields beneath decide. */ const resolvesReadOnly = (resolution) => {
|
|
136
111
|
if (resolution.descriptor) return resolution.descriptor.readOnly === true;
|
|
137
112
|
const below = describeAddressableFields(resolution.fields).filter((descriptor) => resolution.prefix.every((part, offset) => part === splitPath(descriptor.path)[offset]));
|
|
138
113
|
return below.length > 0 && below.every((descriptor) => descriptor.readOnly);
|
|
139
114
|
};
|
|
140
|
-
/**
|
|
141
|
-
* Whether the pointer addresses something read-only. An element carries no
|
|
142
|
-
* descriptor of its own, so the field it belongs to is read one segment up.
|
|
143
|
-
*/ const isReadOnlyPointer = (config, target) => resolvesReadOnly(resolveDataPointer(config, {
|
|
115
|
+
/** An element has no descriptor, so its field is read one segment up. */ const isReadOnlyPointer = (config, target) => resolvesReadOnly(resolveDataPointer(config, {
|
|
144
116
|
doc: target.doc,
|
|
145
117
|
pointer: isElementPointer(target.pointer) ? joinPath(splitPath(target.pointer).slice(0, -1)) : target.pointer,
|
|
146
118
|
ref: target.ref
|
|
147
119
|
}));
|
|
148
120
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* in a read-only field.
|
|
121
|
+
* Checked against the document as it stands when this operation runs: both
|
|
122
|
+
* pointers must resolve, the written value must pass write validation, and what
|
|
123
|
+
* it drops must not sit in a read-only field.
|
|
153
124
|
*/ const findOperationProblems = (config, target) => {
|
|
154
125
|
const { doc, operation, ref } = target;
|
|
155
126
|
const pointers = [operation.path, ..."from" in operation ? [operation.from] : []];
|
|
@@ -191,13 +162,10 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
|
|
|
191
162
|
}
|
|
192
163
|
};
|
|
193
164
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* the
|
|
197
|
-
*
|
|
198
|
-
* The copy means a failing operation leaves the original untouched, and the
|
|
199
|
-
* caller writes nothing unless the whole batch came back applied, so a
|
|
200
|
-
* partially applied batch is never persisted.
|
|
165
|
+
* One evolving copy, so an operation depending on an earlier one resolves
|
|
166
|
+
* against the shape it actually modifies, and a failure leaves the original
|
|
167
|
+
* untouched. The caller writes nothing unless the whole batch came back
|
|
168
|
+
* applied, so a partial batch is never persisted.
|
|
201
169
|
*/ const applyPatchOperations = (config, target) => {
|
|
202
170
|
const next = structuredClone(target.doc);
|
|
203
171
|
for (const [index, operation] of target.patches.entries()) {
|
|
@@ -214,18 +182,15 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
|
|
|
214
182
|
reconcileRowIds(next, target.doc);
|
|
215
183
|
return { next };
|
|
216
184
|
};
|
|
217
|
-
|
|
218
|
-
* Keys Payload manages on a row that travel back into the write unchanged.
|
|
219
|
-
*/ const ROW_KEYS = /* @__PURE__ */ new Set([
|
|
185
|
+
const ROW_KEYS = /* @__PURE__ */ new Set([
|
|
220
186
|
"blockName",
|
|
221
187
|
"blockType",
|
|
222
188
|
"id"
|
|
223
189
|
]);
|
|
224
190
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* is left out, so the write-back carries only what a client could have set.
|
|
191
|
+
* Everything Payload maintains or derives (`_status`, timestamps, join and
|
|
192
|
+
* virtual fields, upload base fields) is left out, so the write-back carries
|
|
193
|
+
* only what a client could have set.
|
|
229
194
|
*/ const pickDescribed = (config, value, at) => {
|
|
230
195
|
const { fields, prefix, isRow } = at;
|
|
231
196
|
const relative = describeAddressableFields(fields).flatMap((descriptor) => {
|
|
@@ -279,10 +244,7 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
|
|
|
279
244
|
}
|
|
280
245
|
return result;
|
|
281
246
|
};
|
|
282
|
-
/**
|
|
283
|
-
* The data handed to `payload.update` after a patch: the patched document
|
|
284
|
-
* reduced to the fields the client may write, plus row identity keys.
|
|
285
|
-
*/ const buildWriteData = (config, target, doc) => {
|
|
247
|
+
/** The patched document reduced to writable fields, plus row identity keys. */ const buildWriteData = (config, target, doc) => {
|
|
286
248
|
return pickDescribed(config, doc, {
|
|
287
249
|
fields: target.flattenedFields,
|
|
288
250
|
prefix: [],
|
|
@@ -5,23 +5,17 @@ import { beforeChangeTraverseFields, beforeValidateTraverseFields } from "payloa
|
|
|
5
5
|
/**
|
|
6
6
|
* Runs Payload's own field validation over a draft without saving anything.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `
|
|
12
|
-
* instead of throwing. The `beforeValidate` pass runs first because some field
|
|
13
|
-
* hooks (Lexical's) prepare state in `context` that their `beforeChange`
|
|
14
|
-
* 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.
|
|
15
12
|
*
|
|
16
13
|
* Nothing is written: `data` is a copy, the context is a scratch copy and the
|
|
17
14
|
* locale merge actions are discarded. `overrideAccess` is true because the
|
|
18
15
|
* question is "could this be published", not "may this client write it".
|
|
19
16
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* `unavailable` marks a traversal that threw, which is not the same answer as
|
|
24
|
-
* a document with nothing wrong with it.
|
|
17
|
+
* `unavailable` marks a traversal that threw, which is not the same answer as a
|
|
18
|
+
* document with nothing wrong with it.
|
|
25
19
|
*/ const collectPublishBlockers = async (req, target) => {
|
|
26
20
|
const { doc, entity } = target;
|
|
27
21
|
const id = doc["id"];
|
|
@@ -1,39 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
2
|
//#region src/write/publish-intent.ts
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* re-entrant write to the same document — an `afterChange` hook calling
|
|
15
|
-
* `payload.update`, say — cannot ride along on it. Only the first operation to
|
|
16
|
-
* ask gets it.
|
|
17
|
-
*/ const claimPublishIntent = (target) => {
|
|
18
|
-
const active = activeFor(target);
|
|
19
|
-
if (!active || active.claimed) return false;
|
|
20
|
-
active.claimed = true;
|
|
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];
|
|
21
14
|
return true;
|
|
22
15
|
};
|
|
23
|
-
/**
|
|
24
|
-
* Whether this change belongs to the operation that claimed the intent, which
|
|
25
|
-
* is what lets the `beforeChange` alarm accept a published status.
|
|
26
|
-
*
|
|
27
|
-
* The id is deliberately not compared here. A `beforeChange` hook reads it from
|
|
28
|
-
* the loaded document, where Payload has already coerced it to the collection's
|
|
29
|
-
* id type, while the claim above saw the raw tool argument; comparing the two
|
|
30
|
-
* would refuse a legitimate publish over `1` versus `"1"`. Nothing is lost: a
|
|
31
|
-
* nested write to another document of the same collection during the publish
|
|
32
|
-
* cannot claim the intent, so `forceDraftWrite` has already stripped its
|
|
33
|
-
* `_status` and it fails the alarm on that.
|
|
34
|
-
*/ const isClaimedPublish = (kind, slug) => {
|
|
35
|
-
const active = store.getStore();
|
|
36
|
-
return active?.kind === kind && active.slug === slug && active.claimed;
|
|
37
|
-
};
|
|
38
16
|
//#endregion
|
|
39
|
-
export {
|
|
17
|
+
export { hasPublishIntent, takePublishIntent, withPublishIntent };
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { commitTransaction, initTransaction, killTransaction } from "payload";
|
|
2
2
|
//#region src/write/transaction.ts
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* support, or a request that already owns one, run `fn` as is.
|
|
4
|
+
* Adapters without transaction support, or a request that already owns one, run
|
|
5
|
+
* `fn` as is.
|
|
7
6
|
*
|
|
8
7
|
* Atomicity, not isolation: neither SQLite nor Postgres at read committed locks
|
|
9
8
|
* the row on the read, so an `expectedUpdatedAt` check remains best effort. Nor
|
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-beta.
|
|
4
|
+
"version": "1.0.0-beta.14",
|
|
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",
|