@abinnovision/payloadcms-mcpx 1.1.0-beta.3 → 1.1.0-beta.6
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 +32 -10
- package/dist/options.mjs +2 -0
- package/dist/tools/builtin.mjs +2 -0
- package/dist/tools/find-versions.mjs +70 -0
- package/dist/tools/get-document.mjs +56 -32
- package/dist/tools/list-capabilities.mjs +2 -0
- package/dist/tools/names.mjs +1 -0
- package/dist/tools/shared.mjs +10 -0
- package/dist/tools/versions.mjs +119 -0
- package/dist/types.d.mts +2 -0
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -170,16 +170,17 @@ every `collection` and `global` enum contains only the slugs the key may touch.
|
|
|
170
170
|
Builtin tools reject unknown arguments by name instead of silently ignoring
|
|
171
171
|
them.
|
|
172
172
|
|
|
173
|
-
| Tool | Purpose | Key arguments
|
|
174
|
-
| ------------------ | ------------------------------------------------------------------------ |
|
|
175
|
-
| `listCapabilities` | What this key may do, `create` apart from `write`; call first to orient. | none
|
|
176
|
-
| `describeSchema` | Field shape of one node; `next` lists the drill-down paths. | `collection` \| `global`, `paths?`, `expand?`
|
|
177
|
-
| `findDocuments` | Query documents. | `collection`, `where?`, `sort?`, `limit?`, `page?`, `depth?`, `select?`, `locale?`, `draft?`
|
|
178
|
-
| `getDocument` | Read one document or a subtree of it.
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
181
|
-
| `
|
|
182
|
-
| `
|
|
173
|
+
| Tool | Purpose | Key arguments |
|
|
174
|
+
| ------------------ | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
|
|
175
|
+
| `listCapabilities` | What this key may do, `create` apart from `write`; call first to orient. | none |
|
|
176
|
+
| `describeSchema` | Field shape of one node; `next` lists the drill-down paths. | `collection` \| `global`, `paths?`, `expand?` |
|
|
177
|
+
| `findDocuments` | Query documents. | `collection`, `where?`, `sort?`, `limit?`, `page?`, `depth?`, `select?`, `locale?`, `draft?` |
|
|
178
|
+
| `getDocument` | Read one document or a subtree of it, an old version, or a diff. | `collection` + `id` \| `global`, `path?`, `depth?`, `locale?`, `draft?`, `outline?`, `versionId?`, `diffFrom?` |
|
|
179
|
+
| `findVersions` | Version history of one document or global, metadata only. | `collection` + `id` \| `global`, `limit?`, `page?`, `status?`, `locale?` |
|
|
180
|
+
| `patchDocument` | Apply RFC 6902 operations to the current draft. | `collection` + `id` \| `global`, `locale`, `patches`, `expectedUpdatedAt?` |
|
|
181
|
+
| `createDocument` | Create a draft from a minimal seed. Not for upload collections. | `collection`, `locale`, `data` |
|
|
182
|
+
| `validateDocument` | Publish blockers without saving anything. | `collection` + `id` \| `global`, `locale` |
|
|
183
|
+
| `publishDocument` | Publish the current draft. | `collection` + `id` \| `global`, `expectedUpdatedAt?` |
|
|
183
184
|
|
|
184
185
|
### Paths and pointers
|
|
185
186
|
|
|
@@ -416,6 +417,27 @@ privileged, so blocker paths and messages may name fields the key's user cannot
|
|
|
416
417
|
read, though values are never included. `validateDocument` runs the same
|
|
417
418
|
traversal without saving anything, which is why it carries no `readOnlyHint`.
|
|
418
419
|
|
|
420
|
+
## Version history
|
|
421
|
+
|
|
422
|
+
On a collection or global with `versions` (with or without drafts),
|
|
423
|
+
`findVersions` lists the history newest first: `versionId`, timestamps,
|
|
424
|
+
`status`, `latest` and `autosave`, but no bodies. It is a read, so it follows
|
|
425
|
+
the `read` capability. The document's own `read` access is checked first, then
|
|
426
|
+
Payload's `readVersions`.
|
|
427
|
+
|
|
428
|
+
`getDocument` takes `versionId` to read one of those versions in place of the
|
|
429
|
+
document, with `path`, `outline`, `locale` and `depth` working as usual. With
|
|
430
|
+
`diffFrom`, a `versionId` or `"published"` (the newest version with published
|
|
431
|
+
status, in any locale), it returns `{ from, to, patch }`: the RFC 6902
|
|
432
|
+
operations that turn `diffFrom` into the document read (the current draft, or
|
|
433
|
+
`versionId` when given). The ops use the pointers `patchDocument` takes, `path`
|
|
434
|
+
limits them to a subtree, and `id`, timestamps and `_status` are left out. Arrays diff by position, so a reordered block shows up as replaces.
|
|
435
|
+
|
|
436
|
+
There is no restore tool. To revert, pass the old version as `versionId` and the
|
|
437
|
+
latest one as `diffFrom`, then apply the patch with `patchDocument`. The revert
|
|
438
|
+
lands as a draft like any other write, with `expectedUpdatedAt` and publish
|
|
439
|
+
blockers in force.
|
|
440
|
+
|
|
419
441
|
## Custom tools
|
|
420
442
|
|
|
421
443
|
```ts
|
package/dist/options.mjs
CHANGED
|
@@ -66,6 +66,7 @@ const normalizeCollections = (config, options, apiKeysSlug) => {
|
|
|
66
66
|
read: settings.read ?? true,
|
|
67
67
|
write: normalizeWriteMode("Collection", slug, settings.write),
|
|
68
68
|
hasDrafts,
|
|
69
|
+
hasVersions: Boolean(collection.versions),
|
|
69
70
|
isUpload: Boolean(collection.upload),
|
|
70
71
|
fieldName: toCamelCase(slug)
|
|
71
72
|
};
|
|
@@ -90,6 +91,7 @@ const normalizeGlobals = (config, options) => {
|
|
|
90
91
|
read: settings.read ?? true,
|
|
91
92
|
write: normalizeWriteMode("Global", slug, settings.write),
|
|
92
93
|
hasDrafts,
|
|
94
|
+
hasVersions: Boolean(global.versions),
|
|
93
95
|
isUpload: false,
|
|
94
96
|
fieldName: toCamelCase(slug)
|
|
95
97
|
};
|
package/dist/tools/builtin.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createDocument } from "./create-document.mjs";
|
|
2
2
|
import { describeSchema } from "./describe-schema.mjs";
|
|
3
3
|
import { findDocuments } from "./find-documents.mjs";
|
|
4
|
+
import { findVersions } from "./find-versions.mjs";
|
|
4
5
|
import { getDocument } from "./get-document.mjs";
|
|
5
6
|
import { listCapabilities } from "./list-capabilities.mjs";
|
|
6
7
|
import { patchDocument } from "./patch-document.mjs";
|
|
@@ -17,6 +18,7 @@ import { validateDocument } from "./validate-document.mjs";
|
|
|
17
18
|
describeSchema,
|
|
18
19
|
findDocuments,
|
|
19
20
|
getDocument,
|
|
21
|
+
findVersions,
|
|
20
22
|
patchDocument,
|
|
21
23
|
createDocument,
|
|
22
24
|
validateDocument,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsonResult } from "../result.mjs";
|
|
2
|
+
import { idShape, localeOf, localeShape, slugsFor, targetShape } from "./shared.mjs";
|
|
3
|
+
import { requireIdFor, resolveTarget } from "./target.mjs";
|
|
4
|
+
import { defineMcpxTool } from "../types.mjs";
|
|
5
|
+
import { queryVersions } from "./versions.mjs";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
/**
|
|
8
|
+
* Bodies are left out so a long history stays small; `getDocument` reads one
|
|
9
|
+
* when it is needed. The document's `read` access is checked first, then
|
|
10
|
+
* Payload's `readVersions`.
|
|
11
|
+
*/ const findVersions = defineMcpxTool({
|
|
12
|
+
name: "findVersions",
|
|
13
|
+
description: `Lists the version history of one document or global, newest first. Returns metadata only; read a version's body with getDocument and "versionId", or what changed with "diffFrom".
|
|
14
|
+
|
|
15
|
+
Pass exactly one of "collection" and "global". "id" is required with "collection" and must be omitted with "global". Autosave versions are listed too, marked "autosave".`,
|
|
16
|
+
annotations: {
|
|
17
|
+
readOnlyHint: true,
|
|
18
|
+
openWorldHint: false
|
|
19
|
+
},
|
|
20
|
+
isEnabled: (scope) => {
|
|
21
|
+
const { collections, globals } = slugsFor(scope, "versions");
|
|
22
|
+
return collections.length + globals.length > 0;
|
|
23
|
+
},
|
|
24
|
+
inputSchema: (scope) => ({
|
|
25
|
+
...targetShape(scope, "versions", {
|
|
26
|
+
collection: "Collection holding the document.",
|
|
27
|
+
global: "Global whose history to list."
|
|
28
|
+
}),
|
|
29
|
+
...idShape(scope, "versions"),
|
|
30
|
+
limit: z.number().int().min(1).max(scope.limits.maxLimit).optional().describe(`Versions per page. Default 10, at most ${String(scope.limits.maxLimit)}.`),
|
|
31
|
+
page: z.number().int().min(1).optional().describe("Page number, from 1."),
|
|
32
|
+
status: z.enum(["published", "draft"]).optional().describe("Only versions with this status."),
|
|
33
|
+
...localeShape(scope, {
|
|
34
|
+
required: false,
|
|
35
|
+
description: "Locale whose status to report. Defaults to the default locale."
|
|
36
|
+
})
|
|
37
|
+
}),
|
|
38
|
+
handler: async ({ args, scope }) => {
|
|
39
|
+
const target = resolveTarget(scope, args, "versions");
|
|
40
|
+
const id = requireIdFor(target, args.id);
|
|
41
|
+
const result = await queryVersions(scope, {
|
|
42
|
+
target,
|
|
43
|
+
id,
|
|
44
|
+
depth: 0,
|
|
45
|
+
locale: localeOf(scope, args.locale)
|
|
46
|
+
}, {
|
|
47
|
+
limit: args.limit ?? 10,
|
|
48
|
+
...args.page === void 0 ? {} : { page: args.page },
|
|
49
|
+
...args.status === void 0 ? {} : { status: args.status }
|
|
50
|
+
});
|
|
51
|
+
const versions = result.docs.map((doc) => ({
|
|
52
|
+
versionId: doc.id,
|
|
53
|
+
createdAt: doc["createdAt"],
|
|
54
|
+
updatedAt: doc["updatedAt"],
|
|
55
|
+
status: doc.version["_status"] ?? null,
|
|
56
|
+
latest: doc["latest"] === true,
|
|
57
|
+
autosave: doc["autosave"] === true,
|
|
58
|
+
...doc["publishedLocale"] ? { publishedLocale: doc["publishedLocale"] } : {}
|
|
59
|
+
}));
|
|
60
|
+
return jsonResult({
|
|
61
|
+
versions,
|
|
62
|
+
totalDocs: result.totalDocs,
|
|
63
|
+
page: result.page,
|
|
64
|
+
totalPages: result.totalPages,
|
|
65
|
+
hasNextPage: result.hasNextPage
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
//#endregion
|
|
70
|
+
export { findVersions };
|
|
@@ -3,24 +3,46 @@ import { JSON_POINTER_PATTERN, findRichTextField, splitPath } from "../schema/wa
|
|
|
3
3
|
import { lexicalOutline } from "../schema/outline.mjs";
|
|
4
4
|
import { resolveDataPointer } from "../schema/pointer.mjs";
|
|
5
5
|
import "../schema/index.mjs";
|
|
6
|
-
import { depthShape, idShape, localeOf, localeShape, targetShape } from "./shared.mjs";
|
|
6
|
+
import { depthShape, idSchema, idShape, localeOf, localeShape, slugsFor, targetShape } from "./shared.mjs";
|
|
7
7
|
import { refOf, requireIdFor, resolveTarget } from "./target.mjs";
|
|
8
8
|
import { defineMcpxTool } from "../types.mjs";
|
|
9
|
+
import { diffDocuments, loadPublished, loadVersion, readLive } from "./versions.mjs";
|
|
10
|
+
import { APIError } from "payload";
|
|
9
11
|
import { z } from "zod";
|
|
10
12
|
import { Pointer } from "rfc6902";
|
|
11
13
|
//#region src/tools/get-document.ts
|
|
12
14
|
const OUTLINE_ERROR = "\"outline\" applies to a rich text field; give \"path\" for one.";
|
|
15
|
+
const DESCRIPTION = `Reads one document, or one subtree of it when "path" is given as a JSON pointer such as "/layout/sections/2". Returns the latest draft by default. Read before patching: the response carries "updatedAt" for expectedUpdatedAt and the indices pointers need.
|
|
16
|
+
|
|
17
|
+
Pass exactly one of "collection" and "global". "id" is required with "collection" and must be omitted with "global", because a global is a singleton.
|
|
18
|
+
|
|
19
|
+
Set "outline" on a rich text "path" to get a compact positional listing of its nodes instead of the raw editor state.
|
|
20
|
+
|
|
21
|
+
On an entity with versions, "versionId" (from findVersions) reads that version instead. "diffFrom" returns the RFC 6902 operations turning a version, or "published" (the newest version with published status, regardless of locale), into the document read, in the pointer syntax patchDocument takes and limited to "path" when given. To revert, pass the old version as "versionId" and the latest one from findVersions as "diffFrom", then apply the patch with patchDocument.`;
|
|
22
|
+
/** Refuses `versionId` and `diffFrom` where they cannot apply. */ const assertVersionArgs = (scope, target, args) => {
|
|
23
|
+
if (args.versionId === void 0 && args.diffFrom === void 0) return;
|
|
24
|
+
const { collections, globals } = slugsFor(scope, "versions");
|
|
25
|
+
if (!(target.kind === "collection" ? collections : globals).includes(target.slug)) throw new APIError(`"${target.slug}" keeps no versions.`, 400);
|
|
26
|
+
if (args.versionId !== void 0 && args.draft !== void 0) throw new APIError("Pass either \"versionId\" or \"draft\", not both.", 400);
|
|
27
|
+
if (args.diffFrom !== void 0 && args.outline) throw new APIError("\"outline\" cannot be combined with \"diffFrom\".", 400);
|
|
28
|
+
};
|
|
29
|
+
const diffResult = async (scope, read, diff, doc) => {
|
|
30
|
+
const from = diff.from === "published" ? await loadPublished(scope, read) : await loadVersion(scope, read, diff.from);
|
|
31
|
+
if (!from) return errorResult(diff.from === "published" ? `"${read.target.slug}" has no published version to diff from.` : `Version "${String(diff.from)}" not found.`);
|
|
32
|
+
return jsonResult({
|
|
33
|
+
from: from.id,
|
|
34
|
+
to: diff.to ?? "current",
|
|
35
|
+
...diff.pointer === void 0 ? {} : { path: diff.pointer.toString() },
|
|
36
|
+
patch: diffDocuments(from.version, doc, diff.pointer)
|
|
37
|
+
});
|
|
38
|
+
};
|
|
13
39
|
/**
|
|
14
40
|
* With `path` the handler returns the subtree plus the `id`, `_status` and
|
|
15
41
|
* `updatedAt` a client needs to write back, so a caller reading one branch
|
|
16
42
|
* still gets the timestamp `expectedUpdatedAt` wants without a second call.
|
|
17
43
|
*/ const getDocument = defineMcpxTool({
|
|
18
44
|
name: "getDocument",
|
|
19
|
-
description:
|
|
20
|
-
|
|
21
|
-
Pass exactly one of "collection" and "global". "id" is required with "collection" and must be omitted with "global", because a global is a singleton.
|
|
22
|
-
|
|
23
|
-
Set "outline" on a rich text "path" to get a compact positional listing of its nodes instead of the raw editor state.`,
|
|
45
|
+
description: DESCRIPTION,
|
|
24
46
|
annotations: {
|
|
25
47
|
readOnlyHint: true,
|
|
26
48
|
openWorldHint: false
|
|
@@ -39,42 +61,44 @@ Set "outline" on a rich text "path" to get a compact positional listing of its n
|
|
|
39
61
|
description: "Locale to read. Defaults to the default locale."
|
|
40
62
|
}),
|
|
41
63
|
draft: z.boolean().optional().describe("Return the latest draft. Default true."),
|
|
42
|
-
outline: z.boolean().optional().describe("For a rich text field, return a compact positional outline instead of the editor state. Requires \"path\".")
|
|
64
|
+
outline: z.boolean().optional().describe("For a rich text field, return a compact positional outline instead of the editor state. Requires \"path\"."),
|
|
65
|
+
versionId: idSchema.optional().describe("Version to read instead of the document, from findVersions. Only for entities with versions; not with \"draft\"."),
|
|
66
|
+
diffFrom: idSchema.optional().describe("Version id, or \"published\" for the newest published version in any locale, to diff from. Returns {from, to, patch} instead of the document. Only for entities with versions.")
|
|
43
67
|
}),
|
|
44
68
|
handler: async ({ args, scope }) => {
|
|
45
69
|
const target = resolveTarget(scope, args, "read");
|
|
46
70
|
const id = requireIdFor(target, args.id);
|
|
47
|
-
|
|
48
|
-
|
|
71
|
+
assertVersionArgs(scope, target, args);
|
|
72
|
+
let pointer;
|
|
73
|
+
try {
|
|
74
|
+
pointer = args.path ? Pointer.fromJSON(args.path) : void 0;
|
|
75
|
+
} catch {
|
|
76
|
+
return errorResult(`"${String(args.path)}" is not a valid JSON pointer.`);
|
|
77
|
+
}
|
|
78
|
+
const read = {
|
|
79
|
+
target,
|
|
80
|
+
id,
|
|
49
81
|
depth: args.depth ?? 0,
|
|
50
|
-
|
|
51
|
-
overrideAccess: false,
|
|
52
|
-
req: scope.req,
|
|
53
|
-
...locale === void 0 ? {} : { locale }
|
|
82
|
+
locale: localeOf(scope, args.locale)
|
|
54
83
|
};
|
|
55
|
-
const doc = await (
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (args.path === void 0 || args.path === "") {
|
|
84
|
+
const doc = args.versionId === void 0 ? await readLive(scope, read, { draft: args.draft ?? true }) : (await loadVersion(scope, read, args.versionId))?.version;
|
|
85
|
+
if (!doc) return errorResult(`Version "${String(args.versionId)}" not found.`);
|
|
86
|
+
if (args.diffFrom !== void 0) return await diffResult(scope, read, {
|
|
87
|
+
from: args.diffFrom,
|
|
88
|
+
to: args.versionId,
|
|
89
|
+
pointer
|
|
90
|
+
}, doc);
|
|
91
|
+
if (pointer === void 0) {
|
|
64
92
|
if (args.outline) return errorResult(OUTLINE_ERROR);
|
|
65
93
|
return jsonResult(doc);
|
|
66
94
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
value = Pointer.fromJSON(args.path).get(doc);
|
|
70
|
-
} catch {
|
|
71
|
-
return errorResult(`"${args.path}" is not a valid JSON pointer.`);
|
|
72
|
-
}
|
|
95
|
+
const path = pointer.toString();
|
|
96
|
+
const value = pointer.get(doc);
|
|
73
97
|
const envelope = {
|
|
74
|
-
...target.kind === "collection" ? { id
|
|
98
|
+
...target.kind === "collection" ? { id } : { global: target.slug },
|
|
75
99
|
status: doc["_status"],
|
|
76
100
|
updatedAt: doc["updatedAt"],
|
|
77
|
-
path
|
|
101
|
+
path
|
|
78
102
|
};
|
|
79
103
|
if (!args.outline) return jsonResult({
|
|
80
104
|
...envelope,
|
|
@@ -84,7 +108,7 @@ Set "outline" on a rich text "path" to get a compact positional listing of its n
|
|
|
84
108
|
try {
|
|
85
109
|
resolution = resolveDataPointer(scope.req.payload.config, {
|
|
86
110
|
doc,
|
|
87
|
-
pointer:
|
|
111
|
+
pointer: path,
|
|
88
112
|
ref: refOf(target)
|
|
89
113
|
});
|
|
90
114
|
} catch (error) {
|
|
@@ -94,7 +118,7 @@ Set "outline" on a rich text "path" to get a compact positional listing of its n
|
|
|
94
118
|
if (!field) return errorResult(OUTLINE_ERROR);
|
|
95
119
|
return jsonResult({
|
|
96
120
|
...envelope,
|
|
97
|
-
outline: lexicalOutline(value,
|
|
121
|
+
outline: lexicalOutline(value, path, field)
|
|
98
122
|
});
|
|
99
123
|
}
|
|
100
124
|
});
|
|
@@ -42,6 +42,7 @@ A global is a singleton: it has no id, is not listed by findDocuments and cannot
|
|
|
42
42
|
create: capability.write && canCreate(entry),
|
|
43
43
|
publish: capability.publish,
|
|
44
44
|
drafts: entry.hasDrafts,
|
|
45
|
+
versions: entry.hasVersions,
|
|
45
46
|
draftValidation: hasDraftValidationEnabled(config),
|
|
46
47
|
idType: collection.customIDType ?? payload.db.defaultIDType
|
|
47
48
|
}];
|
|
@@ -59,6 +60,7 @@ A global is a singleton: it has no id, is not listed by findDocuments and cannot
|
|
|
59
60
|
write: capability.write,
|
|
60
61
|
publish: capability.publish,
|
|
61
62
|
drafts: entry.hasDrafts,
|
|
63
|
+
versions: entry.hasVersions,
|
|
62
64
|
draftValidation: hasDraftValidationEnabled(config)
|
|
63
65
|
}];
|
|
64
66
|
});
|
package/dist/tools/names.mjs
CHANGED
package/dist/tools/shared.mjs
CHANGED
|
@@ -60,6 +60,16 @@ const slugsWhere = (scope, predicate, allowed) => {
|
|
|
60
60
|
collections: scope.readable,
|
|
61
61
|
globals: scope.readableGlobals
|
|
62
62
|
};
|
|
63
|
+
case "versions": return {
|
|
64
|
+
collections: slugsWhere(scope, (entity) => entity.hasVersions, {
|
|
65
|
+
collections: scope.readable,
|
|
66
|
+
globals: []
|
|
67
|
+
}),
|
|
68
|
+
globals: slugsWhere(scope, (entity) => entity.hasVersions, {
|
|
69
|
+
collections: [],
|
|
70
|
+
globals: scope.readableGlobals
|
|
71
|
+
})
|
|
72
|
+
};
|
|
63
73
|
case "write": return {
|
|
64
74
|
collections: scope.writable,
|
|
65
75
|
globals: scope.writableGlobals
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Pointer, createPatch } from "rfc6902";
|
|
2
|
+
//#region src/tools/versions.ts
|
|
3
|
+
/** Bookkeeping that differs between saves without saying anything about content. */ const NOISE = [
|
|
4
|
+
"id",
|
|
5
|
+
"globalType",
|
|
6
|
+
"createdAt",
|
|
7
|
+
"updatedAt",
|
|
8
|
+
"_status"
|
|
9
|
+
];
|
|
10
|
+
/** Newest first, with the id breaking ties between saves in the same instant. */ const NEWEST_FIRST = ["-updatedAt", "-id"];
|
|
11
|
+
/** The document or global as the key's user sees it. */ const readLive = async (scope, read, options) => {
|
|
12
|
+
const shared = {
|
|
13
|
+
depth: read.depth,
|
|
14
|
+
draft: options.draft,
|
|
15
|
+
overrideAccess: false,
|
|
16
|
+
req: scope.req,
|
|
17
|
+
...options.select === void 0 ? {} : { select: options.select },
|
|
18
|
+
...read.locale === void 0 ? {} : { locale: read.locale }
|
|
19
|
+
};
|
|
20
|
+
return await (read.target.kind === "collection" ? scope.req.payload.findByID({
|
|
21
|
+
...shared,
|
|
22
|
+
collection: read.target.slug,
|
|
23
|
+
id: read.id
|
|
24
|
+
}) : scope.req.payload.findGlobal({
|
|
25
|
+
...shared,
|
|
26
|
+
slug: read.target.slug
|
|
27
|
+
}));
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Payload checks only `readVersions` on a version read, never `read`, so the
|
|
31
|
+
* document itself is read first and throws when the key's user may not see it.
|
|
32
|
+
*/ const assertReadable = async (scope, read) => {
|
|
33
|
+
await readLive(scope, {
|
|
34
|
+
...read,
|
|
35
|
+
depth: 0
|
|
36
|
+
}, {
|
|
37
|
+
draft: true,
|
|
38
|
+
select: {}
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
/** `parent` comes back as a raw id or, populated, as the document itself. */ const isVersionOf = (version, id) => {
|
|
42
|
+
const parent = version["parent"];
|
|
43
|
+
const parentId = typeof parent === "object" && parent !== null ? parent["id"] : parent;
|
|
44
|
+
return (typeof parentId === "string" || typeof parentId === "number") && String(parentId) === String(id);
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Reads one version, or `null` when it does not exist, is not readable, or
|
|
48
|
+
* belongs to another document. `findVersionByID` knows nothing of the document
|
|
49
|
+
* the caller asked about, so the parent check is what keeps one readable
|
|
50
|
+
* document from opening the history of another.
|
|
51
|
+
*/ const loadVersion = async (scope, read, versionId) => {
|
|
52
|
+
await assertReadable(scope, read);
|
|
53
|
+
const { payload } = scope.req;
|
|
54
|
+
const shared = {
|
|
55
|
+
id: versionId,
|
|
56
|
+
depth: read.depth,
|
|
57
|
+
disableErrors: true,
|
|
58
|
+
overrideAccess: false,
|
|
59
|
+
req: scope.req,
|
|
60
|
+
...read.locale === void 0 ? {} : { locale: read.locale }
|
|
61
|
+
};
|
|
62
|
+
const version = await (read.target.kind === "collection" ? payload.findVersionByID({
|
|
63
|
+
...shared,
|
|
64
|
+
collection: read.target.slug
|
|
65
|
+
}) : payload.findGlobalVersionByID({
|
|
66
|
+
...shared,
|
|
67
|
+
slug: read.target.slug
|
|
68
|
+
}));
|
|
69
|
+
if (!version) return null;
|
|
70
|
+
if (read.id !== void 0 && !isVersionOf(version, read.id)) return null;
|
|
71
|
+
return version;
|
|
72
|
+
};
|
|
73
|
+
/** One page of a document's or global's history, newest first. */ const queryVersions = async (scope, read, options) => {
|
|
74
|
+
await assertReadable(scope, read);
|
|
75
|
+
const where = {
|
|
76
|
+
...read.id === void 0 ? {} : { parent: { equals: read.id } },
|
|
77
|
+
...options.status === void 0 ? {} : { "version._status": { equals: options.status } }
|
|
78
|
+
};
|
|
79
|
+
const shared = {
|
|
80
|
+
depth: read.depth,
|
|
81
|
+
limit: options.limit,
|
|
82
|
+
sort: NEWEST_FIRST,
|
|
83
|
+
overrideAccess: false,
|
|
84
|
+
req: scope.req,
|
|
85
|
+
where,
|
|
86
|
+
...options.page === void 0 ? {} : { page: options.page },
|
|
87
|
+
...read.locale === void 0 ? {} : { locale: read.locale }
|
|
88
|
+
};
|
|
89
|
+
return await (read.target.kind === "collection" ? scope.req.payload.findVersions({
|
|
90
|
+
...shared,
|
|
91
|
+
collection: read.target.slug
|
|
92
|
+
}) : scope.req.payload.findGlobalVersions({
|
|
93
|
+
...shared,
|
|
94
|
+
slug: read.target.slug
|
|
95
|
+
}));
|
|
96
|
+
};
|
|
97
|
+
/** The newest version whose status is published, or `null` if none is. */ const loadPublished = async (scope, read) => {
|
|
98
|
+
return (await queryVersions(scope, read, {
|
|
99
|
+
status: "published",
|
|
100
|
+
limit: 1
|
|
101
|
+
})).docs[0] ?? null;
|
|
102
|
+
};
|
|
103
|
+
const strip = (doc) => Object.fromEntries(Object.entries(doc).filter(([key]) => !NOISE.includes(key)));
|
|
104
|
+
/**
|
|
105
|
+
* The RFC 6902 operations turning `from` into `to`, limited to the subtree at
|
|
106
|
+
* `pointer`. Op paths stay absolute, so they apply as `patchDocument` ops.
|
|
107
|
+
*/ const diffDocuments = (from, to, pointer = Pointer.fromJSON("")) => {
|
|
108
|
+
const wrap = (doc) => {
|
|
109
|
+
const value = pointer.get(strip(doc));
|
|
110
|
+
return value === void 0 ? {} : { value };
|
|
111
|
+
};
|
|
112
|
+
const path = pointer.toString();
|
|
113
|
+
return createPatch(wrap(from), wrap(to)).map((op) => ({
|
|
114
|
+
...op,
|
|
115
|
+
path: `${path}${op.path.slice(6)}`
|
|
116
|
+
}));
|
|
117
|
+
};
|
|
118
|
+
//#endregion
|
|
119
|
+
export { diffDocuments, isVersionOf, loadPublished, loadVersion, queryVersions, readLive };
|
package/dist/types.d.mts
CHANGED
|
@@ -50,6 +50,8 @@ export interface McpxExposedEntity {
|
|
|
50
50
|
read: boolean;
|
|
51
51
|
write: McpxWriteMode;
|
|
52
52
|
hasDrafts: boolean;
|
|
53
|
+
/** Payload keeps a version history, with or without drafts. */
|
|
54
|
+
hasVersions: boolean;
|
|
53
55
|
/** An upload document is a file, and no tool here can supply one. */
|
|
54
56
|
isUpload: boolean;
|
|
55
57
|
/** Name of the capability group on the key document. */
|
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.
|
|
4
|
+
"version": "1.1.0-beta.6",
|
|
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",
|
|
@@ -59,25 +59,25 @@
|
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
61
61
|
"rfc6902": "^5.3.0",
|
|
62
|
-
"zod": "^4.5
|
|
62
|
+
"zod": "^4.6.5"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
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
|
-
"@payloadcms/db-sqlite": "3.
|
|
69
|
-
"@payloadcms/richtext-lexical": "3.
|
|
70
|
-
"@payloadcms/ui": "3.
|
|
68
|
+
"@payloadcms/db-sqlite": "3.89.0",
|
|
69
|
+
"@payloadcms/richtext-lexical": "3.89.0",
|
|
70
|
+
"@payloadcms/ui": "3.89.0",
|
|
71
71
|
"@swc/core": "^1.16.2",
|
|
72
72
|
"@types/node": "^26.5.1",
|
|
73
|
-
"@types/react": "^19.
|
|
73
|
+
"@types/react": "^19.3.0",
|
|
74
74
|
"@vitest/coverage-v8": "^5.0.0",
|
|
75
75
|
"eslint": "^10.10.0",
|
|
76
|
-
"payload": "3.
|
|
76
|
+
"payload": "3.89.0",
|
|
77
77
|
"prettier": "^3.9.5",
|
|
78
78
|
"publint": "^0.3.24",
|
|
79
|
-
"react": "^19.
|
|
80
|
-
"react-dom": "^19.
|
|
79
|
+
"react": "^19.3.0",
|
|
80
|
+
"react-dom": "^19.3.0",
|
|
81
81
|
"tsdown": "^0.23.0",
|
|
82
82
|
"typescript": "^6.0.3",
|
|
83
83
|
"unplugin-swc": "^1.6.0",
|