@abinnovision/payloadcms-mcpx 2.0.0-beta.8 → 2.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 +43 -0
- package/README.md +56 -36
- package/dist/admin/capability-matrix.d.mts +17 -7
- package/dist/admin/capability-matrix.mjs +374 -272
- package/dist/admin/capability-toggles.mjs +79 -42
- package/dist/admin/confirmations-panel.mjs +232 -0
- package/dist/admin/confirmations.d.mts +15 -0
- package/dist/admin/confirmations.mjs +71 -0
- package/dist/admin/endpoint-url.mjs +7 -0
- package/dist/admin/index.d.mts +2 -1
- package/dist/admin/index.mjs +2 -1
- package/dist/admin/setup-guide.d.mts +6 -4
- package/dist/admin/setup-guide.mjs +51 -33
- package/dist/api-keys/capability-matrix.d.mts +12 -10
- package/dist/api-keys/capability-matrix.mjs +17 -37
- package/dist/api-keys/collection.mjs +18 -19
- package/dist/api-keys/confirmation-view.mjs +7 -0
- package/dist/api-keys/fields.mjs +67 -53
- package/dist/auth/resolve.mjs +79 -30
- package/dist/capabilities.d.mts +2 -0
- package/dist/capabilities.mjs +69 -15
- package/dist/define-tool.d.mts +10 -6
- package/dist/define-tool.mjs +8 -1
- package/dist/endpoint/confirmations.mjs +124 -0
- package/dist/endpoint/handler.mjs +25 -27
- package/dist/endpoint/http.mjs +27 -0
- package/dist/endpoint/server.mjs +52 -27
- package/dist/endpoint/upload.mjs +104 -0
- package/dist/grants/confirmation.mjs +75 -0
- package/dist/grants/grant.mjs +137 -0
- package/dist/guards.mjs +11 -2
- package/dist/i18n.mjs +3 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/options.mjs +73 -62
- package/dist/plugin.d.mts +3 -2
- package/dist/plugin.mjs +19 -4
- package/dist/request.mjs +5 -1
- package/dist/result.mjs +11 -1
- package/dist/schema/admin-hidden.mjs +4 -4
- package/dist/schema/describe.mjs +12 -18
- package/dist/schema/index.mjs +5 -6
- package/dist/schema/lexical-pointer.mjs +8 -8
- package/dist/schema/lexical.mjs +21 -15
- package/dist/schema/outline.mjs +4 -5
- package/dist/schema/path.mjs +13 -4
- package/dist/schema/pointer.mjs +11 -17
- package/dist/schema/shape.mjs +39 -51
- package/dist/schema/walk.mjs +68 -21
- package/dist/tools/builtin.mjs +7 -1
- package/dist/tools/create-document.mjs +38 -26
- package/dist/tools/delete-document.mjs +154 -0
- package/dist/tools/describe-schema.mjs +7 -5
- package/dist/tools/document.mjs +147 -21
- package/dist/tools/duplicate-document.mjs +50 -0
- package/dist/tools/find-documents.mjs +15 -20
- package/dist/tools/find-versions.mjs +11 -15
- package/dist/tools/get-document.mjs +31 -15
- package/dist/tools/links.mjs +53 -0
- package/dist/tools/list-capabilities.mjs +17 -7
- package/dist/tools/patch-document.mjs +60 -54
- package/dist/tools/publish-document.mjs +23 -30
- package/dist/tools/query-paths.mjs +4 -2
- package/dist/tools/run-confirmed.mjs +114 -0
- package/dist/tools/shared.mjs +66 -50
- package/dist/tools/validate-document.mjs +10 -11
- package/dist/tools/versions.mjs +24 -10
- package/dist/types.d.mts +228 -60
- package/dist/upload/file.mjs +177 -0
- package/dist/version.mjs +4 -2
- package/dist/write/draft-guard.mjs +46 -37
- package/dist/write/patch.mjs +4 -4
- package/dist/write/publish-intent.mjs +25 -11
- package/dist/write/row-ids.mjs +6 -2
- package/dist/write/write-data.mjs +18 -31
- package/package.json +15 -14
- package/dist/builtin-tool-names.mjs +0 -16
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { defineMcpxTool } from "../define-tool.mjs";
|
|
2
|
+
import { definedProps } from "../guards.mjs";
|
|
2
3
|
import { jsonResult } from "../result.mjs";
|
|
3
|
-
import {
|
|
4
|
+
import { entityShape, idShape, localeOf, localeShape, pageFields, pageShape, reaches, slugsFor, slugsWhere, widen } from "./shared.mjs";
|
|
4
5
|
import { resolveDocument } from "./document.mjs";
|
|
5
6
|
import { queryVersions } from "./versions.mjs";
|
|
7
|
+
import { z } from "zod";
|
|
6
8
|
import { APIError } from "payload";
|
|
7
9
|
import { hasDraftsEnabled } from "payload/shared";
|
|
8
|
-
import { z } from "zod";
|
|
9
10
|
//#region src/tools/find-versions.ts
|
|
10
11
|
const DESCRIPTION = `Lists the versions of one document or global, newest first, autosaves included. Returns per version "versionId", "createdAt", "updatedAt", "status", "latest", "autosave" and, where set, "publishedLocale", without the content. Read a version with getDocument "versionId", or compare it with "diffFrom".`;
|
|
11
12
|
const statusShape = (scope) => {
|
|
12
|
-
const { collections, globals } =
|
|
13
|
+
const { collections, globals } = slugsWhere(scope, (entity) => entity.hasDrafts, slugsFor(scope, "versions"));
|
|
13
14
|
if (collections.length + globals.length === 0) return widen({});
|
|
14
15
|
return widen({ status: z.enum(["published", "draft"]).optional() });
|
|
15
16
|
};
|
|
@@ -25,15 +26,11 @@ const statusShape = (scope) => {
|
|
|
25
26
|
readOnlyHint: true,
|
|
26
27
|
openWorldHint: false
|
|
27
28
|
},
|
|
28
|
-
isEnabled: (scope) =>
|
|
29
|
-
const { collections, globals } = slugsFor(scope, "versions");
|
|
30
|
-
return collections.length + globals.length > 0;
|
|
31
|
-
},
|
|
29
|
+
isEnabled: (scope) => reaches(scope, "versions"),
|
|
32
30
|
inputSchema: (scope) => ({
|
|
33
31
|
...entityShape(scope, "versions"),
|
|
34
32
|
...idShape(scope, "versions"),
|
|
35
|
-
|
|
36
|
-
page: z.number().int().min(1).optional(),
|
|
33
|
+
...pageShape(scope),
|
|
37
34
|
...statusShape(scope),
|
|
38
35
|
...localeShape(scope, {
|
|
39
36
|
required: false,
|
|
@@ -49,8 +46,10 @@ const statusShape = (scope) => {
|
|
|
49
46
|
locale: localeOf(scope, args.locale)
|
|
50
47
|
}, {
|
|
51
48
|
limit: args.limit ?? 10,
|
|
52
|
-
...
|
|
53
|
-
|
|
49
|
+
...definedProps({
|
|
50
|
+
page: args.page,
|
|
51
|
+
status: args.status
|
|
52
|
+
})
|
|
54
53
|
});
|
|
55
54
|
const versions = result.docs.map((doc) => ({
|
|
56
55
|
versionId: doc.id,
|
|
@@ -63,10 +62,7 @@ const statusShape = (scope) => {
|
|
|
63
62
|
}));
|
|
64
63
|
return jsonResult({
|
|
65
64
|
versions,
|
|
66
|
-
|
|
67
|
-
page: result.page,
|
|
68
|
-
totalPages: result.totalPages,
|
|
69
|
-
hasNextPage: result.hasNextPage
|
|
65
|
+
...pageFields(result)
|
|
70
66
|
});
|
|
71
67
|
}
|
|
72
68
|
});
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { defineMcpxTool } from "../define-tool.mjs";
|
|
2
2
|
import { errorResult, jsonResult } from "../result.mjs";
|
|
3
|
-
import { JSON_POINTER_PATTERN, prototypeSegmentProblem, splitPath } from "../schema/path.mjs";
|
|
4
|
-
import { findRichTextField } from "../schema/walk.mjs";
|
|
5
3
|
import { SchemaError } from "../schema/errors.mjs";
|
|
4
|
+
import { JSON_POINTER_PATTERN, prototypeSegmentProblem, splitPath } from "../schema/path.mjs";
|
|
5
|
+
import { findFieldAt } from "../schema/walk.mjs";
|
|
6
6
|
import { lexicalOutline } from "../schema/outline.mjs";
|
|
7
7
|
import { resolveDataPointer } from "../schema/pointer.mjs";
|
|
8
8
|
import "../schema/index.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import { downloadHandoff, downloadSlugs } from "../upload/file.mjs";
|
|
10
|
+
import { READ_LOCALE_DESCRIPTION, depthShape, entityShape, idSchema, idShape, localeOf, localeShape, reaches, slugsFor, widen } from "./shared.mjs";
|
|
10
11
|
import { identityOf, resolveDocument } from "./document.mjs";
|
|
11
12
|
import { diffDocuments, loadPublished, loadVersion, readLive } from "./versions.mjs";
|
|
12
|
-
import { APIError } from "payload";
|
|
13
13
|
import { z } from "zod";
|
|
14
|
+
import { APIError } from "payload";
|
|
14
15
|
import { Pointer } from "rfc6902";
|
|
15
16
|
//#region src/tools/get-document.ts
|
|
16
17
|
const OUTLINE_ERROR = "\"outline\" applies to a rich text field; give \"path\" for one.";
|
|
@@ -18,10 +19,6 @@ const VERSION_PARAGRAPH = `
|
|
|
18
19
|
|
|
19
20
|
"versionId" and "diffFrom" work where findVersions does. To revert, call getDocument with the old version as "versionId", the version findVersions marks "latest" as "diffFrom" and the locale you will write, then apply the returned "patch" with patchDocument.`;
|
|
20
21
|
const DESCRIPTION = `Reads one document or global, or with "path" only the value at that pointer. Returns the latest draft by default, with the "updatedAt" a write takes as "expectedUpdatedAt".`;
|
|
21
|
-
const exposesVersions = (scope) => {
|
|
22
|
-
const { collections, globals } = slugsFor(scope, "versions");
|
|
23
|
-
return collections.length + globals.length > 0;
|
|
24
|
-
};
|
|
25
22
|
const assertVersionArgs = (scope, target, args) => {
|
|
26
23
|
if (args.versionId === void 0 && args.diffFrom === void 0) return;
|
|
27
24
|
const { collections, globals } = slugsFor(scope, "versions");
|
|
@@ -40,24 +37,36 @@ const diffResult = async (scope, read, diff, doc) => {
|
|
|
40
37
|
});
|
|
41
38
|
};
|
|
42
39
|
const versionShape = (scope) => {
|
|
43
|
-
if (!
|
|
40
|
+
if (!reaches(scope, "versions")) return widen({});
|
|
44
41
|
return widen({
|
|
45
42
|
versionId: idSchema.optional().describe("From findVersions. Not with \"draft\"."),
|
|
46
43
|
diffFrom: idSchema.optional().describe("Version id, or \"published\" for the newest version published in any locale. Returns {from, to, patch}, the JSON Patch from that version to what is read.")
|
|
47
44
|
});
|
|
48
45
|
};
|
|
46
|
+
const downloadShape = (scope) => {
|
|
47
|
+
const slugs = downloadSlugs(scope);
|
|
48
|
+
return widen(slugs.length === 0 ? {} : { download: z.literal(true).optional().describe(`Only for ${slugs.join(", ")}. Adds "download": GET its "url" with its "headers" once, within 5 minutes, for the file. Not with "path", "versionId" or "diffFrom".`) });
|
|
49
|
+
};
|
|
50
|
+
const assertDownloadArgs = (scope, target, args) => {
|
|
51
|
+
if (target.kind === "global" || !downloadSlugs(scope).includes(target.slug)) throw new APIError(`"${target.slug}" has no file to download.`, 400);
|
|
52
|
+
if (args.path !== void 0 || args.versionId !== void 0 || args.diffFrom !== void 0) throw new APIError("\"download\" cannot be combined with \"path\", \"versionId\" or \"diffFrom\".", 400);
|
|
53
|
+
};
|
|
54
|
+
const withDownload = async (scope, doc, target) => typeof doc["filename"] === "string" ? jsonResult({
|
|
55
|
+
...doc,
|
|
56
|
+
download: await downloadHandoff(scope, target)
|
|
57
|
+
}) : errorResult("This document has no file to download.");
|
|
49
58
|
/**
|
|
50
59
|
* With `path` the handler returns the subtree plus the `id`, `_status` and
|
|
51
60
|
* `updatedAt` a client needs to write back, so reading one branch still gives
|
|
52
61
|
* the `expectedUpdatedAt` timestamp without a second call.
|
|
53
62
|
*/ const getDocument = defineMcpxTool({
|
|
54
63
|
name: "getDocument",
|
|
55
|
-
description: (scope) =>
|
|
64
|
+
description: (scope) => reaches(scope, "versions") ? DESCRIPTION + VERSION_PARAGRAPH : DESCRIPTION,
|
|
56
65
|
annotations: {
|
|
57
66
|
readOnlyHint: true,
|
|
58
67
|
openWorldHint: false
|
|
59
68
|
},
|
|
60
|
-
isEnabled: (scope) => scope
|
|
69
|
+
isEnabled: (scope) => reaches(scope, "read"),
|
|
61
70
|
inputSchema: (scope) => ({
|
|
62
71
|
...entityShape(scope, "read"),
|
|
63
72
|
...idShape(scope, "read"),
|
|
@@ -69,11 +78,13 @@ const versionShape = (scope) => {
|
|
|
69
78
|
}),
|
|
70
79
|
draft: z.boolean().optional().describe("Default true. false reads the published version."),
|
|
71
80
|
outline: z.boolean().optional().describe("With \"path\" at a rich text field: list each node's pointer, type, \"version\" and text."),
|
|
72
|
-
...versionShape(scope)
|
|
81
|
+
...versionShape(scope),
|
|
82
|
+
...downloadShape(scope)
|
|
73
83
|
}),
|
|
74
84
|
handler: async ({ args, scope }) => {
|
|
75
85
|
const target = resolveDocument(scope, args, "read");
|
|
76
86
|
assertVersionArgs(scope, target, args);
|
|
87
|
+
if (args.download) assertDownloadArgs(scope, target, args);
|
|
77
88
|
const prototyped = args.path ? prototypeSegmentProblem(args.path) : void 0;
|
|
78
89
|
if (prototyped !== void 0) return errorResult(prototyped);
|
|
79
90
|
let pointer;
|
|
@@ -96,12 +107,17 @@ const versionShape = (scope) => {
|
|
|
96
107
|
}, doc);
|
|
97
108
|
if (pointer === void 0) {
|
|
98
109
|
if (args.outline) return errorResult(OUTLINE_ERROR);
|
|
99
|
-
return
|
|
110
|
+
return args.download && args.id !== void 0 ? await withDownload(scope, doc, {
|
|
111
|
+
collection: target.slug,
|
|
112
|
+
id: args.id,
|
|
113
|
+
draft: args.draft ?? true,
|
|
114
|
+
locale: read.locale ?? null
|
|
115
|
+
}) : jsonResult(doc);
|
|
100
116
|
}
|
|
101
117
|
const path = pointer.toString();
|
|
102
118
|
const value = pointer.get(doc);
|
|
103
119
|
const envelope = {
|
|
104
|
-
...identityOf(target
|
|
120
|
+
...identityOf(target),
|
|
105
121
|
status: doc["_status"],
|
|
106
122
|
updatedAt: doc["updatedAt"],
|
|
107
123
|
path
|
|
@@ -121,7 +137,7 @@ const versionShape = (scope) => {
|
|
|
121
137
|
if (!(error instanceof SchemaError)) throw error;
|
|
122
138
|
return errorResult(error.message);
|
|
123
139
|
}
|
|
124
|
-
const field = resolution.descriptor?.type === "richText" && !resolution.lexical ?
|
|
140
|
+
const field = resolution.descriptor?.type === "richText" && !resolution.lexical ? findFieldAt(resolution.fields, splitPath(resolution.descriptor.path), "richText") : void 0;
|
|
125
141
|
if (!field) return errorResult(OUTLINE_ERROR);
|
|
126
142
|
return jsonResult({
|
|
127
143
|
...envelope,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { publicOrigin } from "../request.mjs";
|
|
2
|
+
import { formatAdminURL } from "payload/shared";
|
|
3
|
+
//#region src/tools/links.ts
|
|
4
|
+
const livePreviewOf = (req, target) => {
|
|
5
|
+
if (target.config.admin.livePreview) return target.config.admin.livePreview;
|
|
6
|
+
const root = req.payload.config.admin.livePreview;
|
|
7
|
+
return (target.kind === "collection" ? root?.collections : root?.globals)?.includes(target.slug) ? root : void 0;
|
|
8
|
+
};
|
|
9
|
+
const previewOf = async (req, args) => {
|
|
10
|
+
const { target, doc, locale } = args;
|
|
11
|
+
const { preview } = target.config.admin;
|
|
12
|
+
if (preview) return await preview(doc, {
|
|
13
|
+
locale: locale ?? "",
|
|
14
|
+
req,
|
|
15
|
+
token: null
|
|
16
|
+
});
|
|
17
|
+
const url = livePreviewOf(req, target)?.url;
|
|
18
|
+
if (typeof url !== "function") return url;
|
|
19
|
+
const { localization } = req.payload.config;
|
|
20
|
+
const entry = localization ? localization.locales.find((item) => item.code === locale) : void 0;
|
|
21
|
+
return await url({
|
|
22
|
+
...target.kind === "collection" ? { collectionConfig: target.config } : { globalConfig: target.config },
|
|
23
|
+
data: doc,
|
|
24
|
+
locale: entry ?? {
|
|
25
|
+
code: locale ?? "",
|
|
26
|
+
label: locale ?? ""
|
|
27
|
+
},
|
|
28
|
+
payload: req.payload,
|
|
29
|
+
req
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The admin panel URL of a written document, and its preview URL where the
|
|
34
|
+
* entity configures one. Both sit at the origin of `serverURL` when set, else
|
|
35
|
+
* of the MCP request. A preview that fails is left out: the write has landed.
|
|
36
|
+
*/ const documentLinks = async (req, args) => {
|
|
37
|
+
const { target, doc, locale } = args;
|
|
38
|
+
const { config } = req.payload;
|
|
39
|
+
const origin = publicOrigin(req);
|
|
40
|
+
const path = target.kind === "collection" ? `/collections/${target.slug}/${String(doc["id"])}` : `/globals/${target.slug}`;
|
|
41
|
+
const adminUrl = formatAdminURL({
|
|
42
|
+
adminRoute: config.routes.admin,
|
|
43
|
+
path,
|
|
44
|
+
serverURL: origin
|
|
45
|
+
});
|
|
46
|
+
const preview = await previewOf(req, args).catch(() => void 0);
|
|
47
|
+
return {
|
|
48
|
+
adminUrl: locale ? `${adminUrl}?locale=${encodeURIComponent(locale)}` : adminUrl,
|
|
49
|
+
...preview ? { previewUrl: new URL(preview, origin).toString() } : {}
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
//#endregion
|
|
53
|
+
export { documentLinks };
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { defineMcpxTool } from "../define-tool.mjs";
|
|
2
|
-
import { canCreate } from "../capabilities.mjs";
|
|
3
2
|
import { jsonResult } from "../result.mjs";
|
|
4
3
|
import { translateStatic, translatorFor } from "../i18n.mjs";
|
|
4
|
+
import { slugsFor } from "./shared.mjs";
|
|
5
|
+
import { globalConfig } from "./document.mjs";
|
|
6
|
+
import { MCPX_ISSUES_URL, MCPX_REPOSITORY_URL } from "../version.mjs";
|
|
5
7
|
import { hasDraftValidationEnabled } from "payload/shared";
|
|
6
8
|
//#region src/tools/list-capabilities.ts
|
|
7
9
|
const DESCRIPTION = `Lists what this key may do. Call it first. Returns each collection and global the key can read or write, with whether it can create, publish, has drafts and exposes versions to findVersions, and a collection's "idType". Also returns the locales, the "limits" on page size and depth, and the enabled custom tools.`;
|
|
10
|
+
const DIAGNOSTICS = ` Its "server" block holds the server version with repository and issue links, for a bug report.`;
|
|
8
11
|
const translateLabel = (scope, label, fallback) => {
|
|
9
12
|
const { i18n, t } = scope.req;
|
|
10
13
|
const resolved = typeof label === "function" ? label({
|
|
@@ -21,7 +24,7 @@ const translateLabel = (scope, label, fallback) => {
|
|
|
21
24
|
* grows.
|
|
22
25
|
*/ const listCapabilities = defineMcpxTool({
|
|
23
26
|
name: "listCapabilities",
|
|
24
|
-
description: DESCRIPTION,
|
|
27
|
+
description: (scope) => scope.diagnostics ? `${DESCRIPTION}${DIAGNOSTICS}` : DESCRIPTION,
|
|
25
28
|
annotations: {
|
|
26
29
|
readOnlyHint: true,
|
|
27
30
|
openWorldHint: false
|
|
@@ -31,10 +34,11 @@ const translateLabel = (scope, label, fallback) => {
|
|
|
31
34
|
handler: ({ scope }) => {
|
|
32
35
|
const { payload } = scope.req;
|
|
33
36
|
const translate = translatorFor(scope.req.i18n);
|
|
37
|
+
const creatable = slugsFor(scope, "create").collections;
|
|
34
38
|
const collections = scope.exposure.collections.flatMap((entry) => {
|
|
35
39
|
const capability = scope.capabilities.collections[entry.slug];
|
|
36
40
|
const collection = payload.collections[entry.slug];
|
|
37
|
-
if (!capability || !collection || !
|
|
41
|
+
if (!capability || !collection || !capability.read) return [];
|
|
38
42
|
const { config } = collection;
|
|
39
43
|
const description = translate(config.admin.description);
|
|
40
44
|
return [{
|
|
@@ -46,7 +50,7 @@ const translateLabel = (scope, label, fallback) => {
|
|
|
46
50
|
...description === void 0 ? {} : { description },
|
|
47
51
|
read: capability.read,
|
|
48
52
|
write: capability.write,
|
|
49
|
-
create:
|
|
53
|
+
create: creatable.includes(entry.slug),
|
|
50
54
|
publish: capability.publish,
|
|
51
55
|
drafts: entry.hasDrafts,
|
|
52
56
|
versions: entry.hasVersions,
|
|
@@ -56,8 +60,8 @@ const translateLabel = (scope, label, fallback) => {
|
|
|
56
60
|
});
|
|
57
61
|
const globals = scope.exposure.globals.flatMap((entry) => {
|
|
58
62
|
const capability = scope.capabilities.globals[entry.slug];
|
|
59
|
-
const config = payload
|
|
60
|
-
if (!capability || !config || !
|
|
63
|
+
const config = globalConfig(payload, entry.slug);
|
|
64
|
+
if (!capability || !config || !capability.read) return [];
|
|
61
65
|
const description = translate(config.admin.description);
|
|
62
66
|
return [{
|
|
63
67
|
slug: entry.slug,
|
|
@@ -79,7 +83,13 @@ const translateLabel = (scope, label, fallback) => {
|
|
|
79
83
|
default: scope.localization.defaultLocale
|
|
80
84
|
} : null,
|
|
81
85
|
limits: scope.limits,
|
|
82
|
-
tools: Object.entries(scope.capabilities.tools).filter(([, enabled]) => enabled).map(([name]) => name)
|
|
86
|
+
tools: Object.entries(scope.capabilities.tools).filter(([, enabled]) => enabled).map(([name]) => name),
|
|
87
|
+
...scope.diagnostics ? { server: {
|
|
88
|
+
name: scope.diagnostics.name,
|
|
89
|
+
version: scope.diagnostics.version,
|
|
90
|
+
repository: MCPX_REPOSITORY_URL,
|
|
91
|
+
issues: MCPX_ISSUES_URL
|
|
92
|
+
} } : {}
|
|
83
93
|
});
|
|
84
94
|
}
|
|
85
95
|
});
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { defineMcpxTool } from "../define-tool.mjs";
|
|
2
2
|
import { isPlainObject } from "../guards.mjs";
|
|
3
|
-
import { errorResult
|
|
3
|
+
import { errorResult } from "../result.mjs";
|
|
4
4
|
import { JSON_POINTER_PATTERN } from "../schema/path.mjs";
|
|
5
5
|
import "../schema/index.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { applyPatchOperations, isElementPointer } from "../write/patch.mjs";
|
|
6
|
+
import { assertAcceptsFile, fileShape, fileSlugs, fileWriteRefusal, uploadHandoff, uploadedFile } from "../upload/file.mjs";
|
|
7
|
+
import { entityShape, expectedUpdatedAtShape, idShape, liveWriteSentence, localeOf, localeShape, reaches } from "./shared.mjs";
|
|
8
|
+
import { readDraft, resolveDocument, staleReadResult, updateTarget, writtenResult } from "./document.mjs";
|
|
10
9
|
import { withTransaction } from "../write/transaction.mjs";
|
|
10
|
+
import { applyPatchOperations, isElementPointer } from "../write/patch.mjs";
|
|
11
11
|
import { buildWriteData } from "../write/write-data.mjs";
|
|
12
|
-
import { Forbidden, NotFound } from "payload";
|
|
13
12
|
import { z } from "zod";
|
|
13
|
+
import { APIError, Forbidden, NotFound } from "payload";
|
|
14
14
|
import { Pointer } from "rfc6902";
|
|
15
15
|
//#region src/tools/patch-document.ts
|
|
16
16
|
const DESCRIPTION = (scope) => `Applies JSON Patch operations to one document or global, e.g. {"op":"replace","path":"/title","value":"Home"}. If any operation fails, nothing is written and the response lists the problems.
|
|
@@ -21,10 +21,12 @@ Read the document with getDocument right before patching. Append to a list with
|
|
|
21
21
|
|
|
22
22
|
In a rich text field a pointer continues into the rich text state: "/content/root/children/2" is a node, "/content/root/children/2/tag" one of its properties and "/content/root/children/3/fields/tone" a field of a block node. getDocument "outline" lists each node's pointer and "version". Build nodes from describeSchema "nodeProperties". Check a position before writing to it, e.g. {"op":"test","path":"/content/root/children/2/type","value":"heading"}.
|
|
23
23
|
|
|
24
|
-
The response carries "updatedAt" and, if any, "publishBlockers": what must be fixed before publishing. "publishBlockersUnavailable" means that check failed. The write stands either way. "notApplied" lists pointers whose value did not change or cannot be read back, for example where field access denies the update.`;
|
|
24
|
+
The response carries "updatedAt" and, if any, "publishBlockers": what must be fixed before publishing. "publishBlockersUnavailable" means that check failed. The write stands either way. "notApplied" lists pointers whose value did not change or cannot be read back, for example where field access denies the update. The response links the document with "adminUrl" and "previewUrl".`;
|
|
25
25
|
const POINTER = z.string().regex(JSON_POINTER_PATTERN);
|
|
26
26
|
const PATCHES_LIMIT = 500;
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Discriminated on `op`, so an operation carries only its own members.
|
|
29
|
+
*/ const PATCH_OPERATION_SCHEMA = z.discriminatedUnion("op", [
|
|
28
30
|
z.strictObject({
|
|
29
31
|
op: z.literal("add"),
|
|
30
32
|
path: POINTER,
|
|
@@ -73,7 +75,8 @@ const notAppliedPointers = (patches, intended, saved) => patches.flatMap((operat
|
|
|
73
75
|
* document before writing anything, runs the write in a transaction, then
|
|
74
76
|
* re-reads the saved document to report which pointers survived and what still
|
|
75
77
|
* blocks publishing. The draft guard, not this tool, decides where the write
|
|
76
|
-
* lands.
|
|
78
|
+
* lands. With `file`, the patches and the file land as one update, and the
|
|
79
|
+
* transaction commits right after it because Payload has moved files by then.
|
|
77
80
|
*/ const patchDocument = defineMcpxTool({
|
|
78
81
|
name: "patchDocument",
|
|
79
82
|
description: DESCRIPTION,
|
|
@@ -83,7 +86,7 @@ const notAppliedPointers = (patches, intended, saved) => patches.flatMap((operat
|
|
|
83
86
|
idempotentHint: false,
|
|
84
87
|
openWorldHint: false
|
|
85
88
|
},
|
|
86
|
-
isEnabled: (scope) => scope
|
|
89
|
+
isEnabled: (scope) => reaches(scope, "write"),
|
|
87
90
|
inputSchema: (scope) => ({
|
|
88
91
|
...entityShape(scope, "write"),
|
|
89
92
|
...idShape(scope, "write"),
|
|
@@ -91,71 +94,74 @@ const notAppliedPointers = (patches, intended, saved) => patches.flatMap((operat
|
|
|
91
94
|
required: true,
|
|
92
95
|
description: "Localized fields are written in this locale only."
|
|
93
96
|
}),
|
|
94
|
-
patches: z.array(PATCH_OPERATION_SCHEMA).min(1).max(PATCHES_LIMIT),
|
|
95
|
-
|
|
97
|
+
patches: fileSlugs(scope).length === 0 ? z.array(PATCH_OPERATION_SCHEMA).min(1).max(PATCHES_LIMIT) : z.array(PATCH_OPERATION_SCHEMA).max(PATCHES_LIMIT).describe("May be empty when \"file\" is given."),
|
|
98
|
+
...expectedUpdatedAtShape,
|
|
99
|
+
...fileShape(scope, (slugs) => `Replaces the file of a document in ${slugs}. The call returns an "upload" for the bytes instead of writing.`)
|
|
96
100
|
}),
|
|
97
101
|
handler: async ({ args, scope }) => {
|
|
98
102
|
const target = resolveDocument(scope, args, "write");
|
|
99
103
|
const { payload } = scope.req;
|
|
100
104
|
const locale = localeOf(scope, args.locale);
|
|
101
105
|
const patches = args.patches;
|
|
102
|
-
|
|
106
|
+
const { file } = args;
|
|
107
|
+
const bytes = uploadedFile(scope.req);
|
|
108
|
+
if (file !== void 0) assertAcceptsFile(scope, target, file);
|
|
109
|
+
if (patches.length === 0 && file === void 0) throw new APIError("\"patches\" may be empty only when \"file\" is given.", 400);
|
|
110
|
+
const report = async (next) => {
|
|
111
|
+
const readable = await readDraft(scope, {
|
|
112
|
+
target,
|
|
113
|
+
locale
|
|
114
|
+
}).catch((error) => {
|
|
115
|
+
if (error instanceof NotFound || error instanceof Forbidden) return;
|
|
116
|
+
throw error;
|
|
117
|
+
});
|
|
118
|
+
const notApplied = readable ? notAppliedPointers(patches, next, readable) : [];
|
|
119
|
+
return await writtenResult(scope, {
|
|
120
|
+
target,
|
|
121
|
+
locale,
|
|
122
|
+
extra: notApplied.length > 0 ? { notApplied } : {}
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
const outcome = await withTransaction(scope.req, async () => {
|
|
103
126
|
const doc = await readDraft(scope, {
|
|
104
127
|
target,
|
|
105
128
|
locale
|
|
106
129
|
});
|
|
107
130
|
const stale = staleReadResult(doc, args.expectedUpdatedAt, "The document changed since you read it. Read it again and re-apply the patch.");
|
|
108
|
-
if (stale) return stale;
|
|
131
|
+
if (stale) return { done: stale };
|
|
109
132
|
const applied = applyPatchOperations(payload.config, {
|
|
110
133
|
doc,
|
|
111
134
|
patches,
|
|
112
135
|
ref: target
|
|
113
136
|
});
|
|
114
|
-
if ("problems" in applied) return errorResult("No operation was applied.", { problems: applied.problems });
|
|
115
|
-
|
|
137
|
+
if ("problems" in applied) return { done: errorResult("No operation was applied.", { problems: applied.problems }) };
|
|
138
|
+
if (file !== void 0 && target.kind === "collection") {
|
|
139
|
+
const refusal = await fileWriteRefusal(scope, {
|
|
140
|
+
entity: target,
|
|
141
|
+
id: target.id,
|
|
142
|
+
data: applied.next
|
|
143
|
+
});
|
|
144
|
+
if (refusal) return { done: refusal };
|
|
145
|
+
if (!bytes) return { handoff: {
|
|
146
|
+
...args,
|
|
147
|
+
file,
|
|
148
|
+
expectedUpdatedAt: doc["updatedAt"]
|
|
149
|
+
} };
|
|
150
|
+
}
|
|
151
|
+
await updateTarget(scope, target, {
|
|
116
152
|
data: buildWriteData(payload.config, target.config, applied.next),
|
|
117
|
-
depth: 0,
|
|
118
153
|
draft: true,
|
|
119
|
-
overrideAccess: false,
|
|
120
|
-
req: scope.req,
|
|
121
|
-
...locale === void 0 ? {} : { locale }
|
|
122
|
-
};
|
|
123
|
-
if (target.kind === "collection") await payload.update({
|
|
124
|
-
...write,
|
|
125
|
-
collection: target.slug,
|
|
126
|
-
id: target.id
|
|
127
|
-
});
|
|
128
|
-
else await payload.updateGlobal({
|
|
129
|
-
...write,
|
|
130
|
-
fallbackLocale: false,
|
|
131
|
-
slug: target.slug
|
|
132
|
-
});
|
|
133
|
-
const saved = await readDraft(scope, {
|
|
134
|
-
target,
|
|
135
154
|
locale,
|
|
136
|
-
|
|
137
|
-
});
|
|
138
|
-
const readable = await readDraft(scope, {
|
|
139
|
-
target,
|
|
140
|
-
locale
|
|
141
|
-
}).catch((error) => {
|
|
142
|
-
if (error instanceof NotFound || error instanceof Forbidden) return;
|
|
143
|
-
throw error;
|
|
144
|
-
});
|
|
145
|
-
const notApplied = readable ? notAppliedPointers(patches, applied.next, readable) : [];
|
|
146
|
-
const validation = await collectPublishBlockers(scope.req, {
|
|
147
|
-
doc: saved,
|
|
148
|
-
entity: target
|
|
149
|
-
});
|
|
150
|
-
return jsonResult({
|
|
151
|
-
...identityOf(target, saved["id"]),
|
|
152
|
-
status: saved["_status"],
|
|
153
|
-
updatedAt: saved["updatedAt"],
|
|
154
|
-
...validation.blockers.length > 0 ? { publishBlockers: validation.blockers } : {},
|
|
155
|
-
...validation.unavailable ? { publishBlockersUnavailable: true } : {},
|
|
156
|
-
...notApplied.length > 0 ? { notApplied } : {}
|
|
155
|
+
file: bytes
|
|
157
156
|
});
|
|
157
|
+
return bytes === void 0 ? { done: await report(applied.next) } : { written: applied.next };
|
|
158
|
+
});
|
|
159
|
+
if ("done" in outcome) return outcome.done;
|
|
160
|
+
if ("handoff" in outcome) return await uploadHandoff(scope, {
|
|
161
|
+
tool: "patchDocument",
|
|
162
|
+
args: outcome.handoff
|
|
158
163
|
});
|
|
164
|
+
return await report(outcome.written);
|
|
159
165
|
}
|
|
160
166
|
});
|
|
161
167
|
//#endregion
|
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
import { defineMcpxTool } from "../define-tool.mjs";
|
|
2
2
|
import { jsonResult } from "../result.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { documentLinks } from "./links.mjs";
|
|
4
|
+
import { entityShape, expectedUpdatedAtShape, idShape, localeOf, localeShape, reaches } from "./shared.mjs";
|
|
5
|
+
import { blockerFields, collectLocaleBlockers, documentSummary, readDraft, resolveDocument, staleReadResult, updateTarget } from "./document.mjs";
|
|
6
6
|
import { withPublishIntent } from "../write/publish-intent.mjs";
|
|
7
|
-
import {
|
|
7
|
+
import { withTransaction } from "../write/transaction.mjs";
|
|
8
8
|
/**
|
|
9
|
-
* Publishes a draft. Available where the config exposes
|
|
10
|
-
*
|
|
11
|
-
* `publish` checkboxes.
|
|
9
|
+
* Publishes a draft. Available where the config exposes `publish` on an entity
|
|
10
|
+
* with drafts and the key has both the `write` and `publish` checkboxes.
|
|
12
11
|
*/ const publishDocument = defineMcpxTool({
|
|
13
12
|
name: "publishDocument",
|
|
14
|
-
description: `Makes the current draft of one document or global public. A draft with publish blockers is refused, so call validateDocument first.
|
|
13
|
+
description: `Makes the current draft of one document or global public, in every locale or only in "locale". A scoped publish keeps the other locales of localized fields as last published, and a never-published document goes live in full. A draft with publish blockers is refused, so call validateDocument first. Payload checks only the published locale, so "otherLocaleBlockers" lists what other locales still lack. There is no unpublish. The response links the document with "adminUrl" and "previewUrl".`,
|
|
15
14
|
annotations: {
|
|
16
15
|
destructiveHint: true,
|
|
17
16
|
openWorldHint: false
|
|
18
17
|
},
|
|
19
|
-
isEnabled: (scope) => scope
|
|
18
|
+
isEnabled: (scope) => reaches(scope, "publish"),
|
|
20
19
|
inputSchema: (scope) => ({
|
|
21
20
|
...entityShape(scope, "publish"),
|
|
22
21
|
...idShape(scope, "publish"),
|
|
23
|
-
|
|
22
|
+
...localeShape(scope, { required: false }),
|
|
23
|
+
...expectedUpdatedAtShape
|
|
24
24
|
}),
|
|
25
25
|
handler: async ({ args, scope }) => {
|
|
26
26
|
const target = resolveDocument(scope, args, "publish");
|
|
27
|
-
const
|
|
28
|
-
const
|
|
27
|
+
const locale = localeOf(scope, args.locale);
|
|
28
|
+
const { localization } = scope;
|
|
29
29
|
return await withTransaction(scope.req, async () => {
|
|
30
30
|
const doc = await readDraft(scope, {
|
|
31
31
|
target,
|
|
@@ -33,33 +33,26 @@ import { z } from "zod";
|
|
|
33
33
|
});
|
|
34
34
|
const stale = staleReadResult(doc, args.expectedUpdatedAt, "The document changed since you read it. Read it again before publishing.");
|
|
35
35
|
if (stale) return stale;
|
|
36
|
-
|
|
36
|
+
await updateTarget(scope, target, {
|
|
37
37
|
data: withPublishIntent({}),
|
|
38
|
-
depth: 0,
|
|
39
38
|
draft: false,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
req: scope.req,
|
|
43
|
-
...locale === void 0 ? {} : { locale }
|
|
44
|
-
};
|
|
45
|
-
if (target.kind === "collection") await payload.update({
|
|
46
|
-
...write,
|
|
47
|
-
collection: target.slug,
|
|
48
|
-
id: target.id
|
|
49
|
-
});
|
|
50
|
-
else await payload.updateGlobal({
|
|
51
|
-
...write,
|
|
52
|
-
slug: target.slug
|
|
39
|
+
locale,
|
|
40
|
+
publishSpecificLocale: args.locale
|
|
53
41
|
});
|
|
54
42
|
const saved = await readDraft(scope, {
|
|
55
43
|
target,
|
|
56
44
|
locale,
|
|
57
45
|
privileged: true
|
|
58
46
|
});
|
|
47
|
+
const others = await collectLocaleBlockers(scope, target, localization?.locales.filter((entry) => entry !== locale) ?? []);
|
|
59
48
|
return jsonResult({
|
|
60
|
-
...
|
|
61
|
-
|
|
62
|
-
|
|
49
|
+
...documentSummary(target, saved),
|
|
50
|
+
...await documentLinks(scope.req, {
|
|
51
|
+
target,
|
|
52
|
+
doc: saved,
|
|
53
|
+
locale
|
|
54
|
+
}),
|
|
55
|
+
...blockerFields(others, "otherLocaleBlockers")
|
|
63
56
|
});
|
|
64
57
|
});
|
|
65
58
|
}
|
|
@@ -8,7 +8,9 @@ import { APIError, getLocalizedPaths } from "payload";
|
|
|
8
8
|
* them: `and` and `or` (any case) hold arrays of clauses, every other key is a
|
|
9
9
|
* path.
|
|
10
10
|
*/ const wherePaths = (where) => isPlainObject(where) ? Object.entries(where).flatMap(([key, clause]) => ["and", "or"].includes(key.toLowerCase()) && Array.isArray(clause) ? clause.flatMap(wherePaths) : [key]) : [];
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* The paths of a `sort`: a field name, `-` for descending, comma separated.
|
|
13
|
+
*/ const sortPaths = (sort) => (sort ?? "").split(",").map((entry) => entry.trim().replace(/^-/, "")).filter(Boolean);
|
|
12
14
|
const normalise = (path) => path === "_id" ? "id" : path.replaceAll("__", ".");
|
|
13
15
|
const isRefusedPath = (scope, args, seen = /* @__PURE__ */ new Set()) => {
|
|
14
16
|
const key = `${args.collection}:${args.path}`;
|
|
@@ -46,7 +48,7 @@ const isRefusedPath = (scope, args, seen = /* @__PURE__ */ new Set()) => {
|
|
|
46
48
|
const reaches = (path) => isRefusedPath(scope, {
|
|
47
49
|
collection: args.collection,
|
|
48
50
|
path: normalise(path),
|
|
49
|
-
locale: args.locale
|
|
51
|
+
locale: args.locale
|
|
50
52
|
});
|
|
51
53
|
for (const path of wherePaths(args.where)) if (reaches(path)) throw new APIError(`This key cannot query through "${path}".`, 400);
|
|
52
54
|
for (const path of sortPaths(args.sort)) if (reaches(path) || normalise(path).split(".").some(reaches)) throw new APIError(`This key cannot query through "${path}".`, 400);
|