@abinnovision/payloadcms-mcpx 2.0.0-beta.9 → 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 +5 -3
- 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 +3 -3
- package/dist/admin/setup-guide.mjs +51 -33
- package/dist/api-keys/capability-matrix.d.mts +7 -9
- package/dist/api-keys/capability-matrix.mjs +15 -37
- package/dist/api-keys/collection.mjs +16 -18
- package/dist/api-keys/confirmation-view.mjs +7 -0
- package/dist/api-keys/fields.mjs +50 -57
- package/dist/auth/resolve.mjs +69 -40
- package/dist/capabilities.d.mts +2 -0
- package/dist/capabilities.mjs +64 -16
- package/dist/define-tool.d.mts +4 -4
- 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 +48 -29
- 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 +8 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/options.mjs +68 -61
- 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 +15 -13
- package/dist/schema/outline.mjs +4 -5
- package/dist/schema/path.mjs +4 -1
- package/dist/schema/pointer.mjs +11 -17
- package/dist/schema/shape.mjs +38 -50
- package/dist/schema/walk.mjs +57 -18
- 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 +131 -45
- 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 +57 -53
- package/dist/tools/publish-document.mjs +17 -29
- package/dist/tools/query-paths.mjs +1 -1
- package/dist/tools/run-confirmed.mjs +114 -0
- package/dist/tools/shared.mjs +53 -46
- package/dist/tools/validate-document.mjs +6 -8
- package/dist/tools/versions.mjs +11 -6
- package/dist/types.d.mts +158 -43
- package/dist/upload/file.mjs +177 -0
- package/dist/version.mjs +4 -2
- package/dist/write/draft-guard.mjs +35 -28
- package/dist/write/patch.mjs +4 -4
- package/dist/write/publish-intent.mjs +23 -11
- package/dist/write/row-ids.mjs +3 -1
- package/dist/write/write-data.mjs +18 -31
- package/package.json +14 -14
- package/dist/builtin-tool-names.mjs +0 -16
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { defineMcpxTool } from "../define-tool.mjs";
|
|
2
|
+
import { errorResult, jsonResult } from "../result.mjs";
|
|
3
|
+
import { translateStatic } from "../i18n.mjs";
|
|
4
|
+
import { expectedUpdatedAtShape, idSchema, slugsFor, stringEnum } from "./shared.mjs";
|
|
5
|
+
import { readDraft, resolveCollection, staleReadResult } from "./document.mjs";
|
|
6
|
+
import { withTrashIntent } from "../write/publish-intent.mjs";
|
|
7
|
+
import { withTransaction } from "../write/transaction.mjs";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { Forbidden, combineQueries, executeAccess } from "payload";
|
|
10
|
+
import { formatAdminURL } from "payload/shared";
|
|
11
|
+
//#region src/tools/delete-document.ts
|
|
12
|
+
const DESCRIPTION = `Asks to delete one document. This call deletes nothing: it returns a "confirmation" with an "id" and a "url". Give the url to the user, who approves or rejects in the admin panel, then pass the id to runConfirmed, which deletes. Refused when the document is unknown, already in trash, changed since "expectedUpdatedAt" or not deletable by the user.`;
|
|
13
|
+
const permanenceSentence = (scope) => {
|
|
14
|
+
const permanent = slugsFor(scope, "delete").collections.filter((slug) => scope.req.payload.collections[slug]?.config.trash !== true);
|
|
15
|
+
if (permanent.length === 0) return " Documents move to trash.";
|
|
16
|
+
const sentence = ` Deletes in ${permanent.join(", ")} are permanent.`;
|
|
17
|
+
return permanent.length === slugsFor(scope, "delete").collections.length ? sentence : `${sentence} Other documents move to trash.`;
|
|
18
|
+
};
|
|
19
|
+
const unattendedSentence = (scope) => {
|
|
20
|
+
const unattended = slugsFor(scope, "delete").collections.filter((slug) => scope.capabilities.collections[slug]?.deleteUnattended === true);
|
|
21
|
+
return unattended.length === 0 ? "" : ` Exception: in ${unattended.join(", ")} this call moves the document to trash at once and returns the delete result, with no confirmation.`;
|
|
22
|
+
};
|
|
23
|
+
const mayDelete = async (req, target) => {
|
|
24
|
+
const { config } = target;
|
|
25
|
+
const data = config.trash ? { deletedAt: (/* @__PURE__ */ new Date()).toISOString() } : {};
|
|
26
|
+
const rules = config.trash ? [config.access.delete, config.access.update] : [config.access.delete];
|
|
27
|
+
let where = { id: { equals: target.id } };
|
|
28
|
+
for (const rule of rules) {
|
|
29
|
+
const result = await executeAccess({
|
|
30
|
+
id: target.id,
|
|
31
|
+
data,
|
|
32
|
+
disableErrors: true,
|
|
33
|
+
req
|
|
34
|
+
}, rule);
|
|
35
|
+
if (result === false) return false;
|
|
36
|
+
if (typeof result === "object") where = combineQueries(where, result);
|
|
37
|
+
}
|
|
38
|
+
const { totalDocs } = await req.payload.count({
|
|
39
|
+
collection: target.slug,
|
|
40
|
+
where,
|
|
41
|
+
overrideAccess: true,
|
|
42
|
+
trash: true,
|
|
43
|
+
req
|
|
44
|
+
});
|
|
45
|
+
return totalDocs > 0;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Deletes one collection document once the key's user approved the call. The
|
|
49
|
+
* first call only checks and stores it; `runConfirmed` runs it again with the
|
|
50
|
+
* stored arguments, pinned to the `updatedAt` read here. Available where the
|
|
51
|
+
* config exposes `delete` and the key has the `delete` checkbox.
|
|
52
|
+
*/ const deleteDocument = defineMcpxTool({
|
|
53
|
+
name: "deleteDocument",
|
|
54
|
+
description: (scope) => `${DESCRIPTION}${permanenceSentence(scope)}${unattendedSentence(scope)}`,
|
|
55
|
+
annotations: {
|
|
56
|
+
destructiveHint: true,
|
|
57
|
+
openWorldHint: false
|
|
58
|
+
},
|
|
59
|
+
isEnabled: (scope) => scope.uploads && scope.collections.deletable.length > 0,
|
|
60
|
+
inputSchema: (scope) => ({
|
|
61
|
+
collection: stringEnum(scope.collections.deletable),
|
|
62
|
+
id: idSchema,
|
|
63
|
+
...expectedUpdatedAtShape,
|
|
64
|
+
reason: z.string().max(500).optional().describe("Why, shown to the user who approves.")
|
|
65
|
+
}),
|
|
66
|
+
confirm: {
|
|
67
|
+
label: "Delete documents",
|
|
68
|
+
summarize: async ({ args }, req) => {
|
|
69
|
+
const slug = String(args["collection"]);
|
|
70
|
+
const id = args["id"];
|
|
71
|
+
const config = req.payload.collections[slug]?.config;
|
|
72
|
+
const titleField = config?.admin.useAsTitle;
|
|
73
|
+
const read = async (draft) => config ? await req.payload.findByID({
|
|
74
|
+
collection: slug,
|
|
75
|
+
id,
|
|
76
|
+
depth: 0,
|
|
77
|
+
draft,
|
|
78
|
+
overrideAccess: false,
|
|
79
|
+
disableErrors: true,
|
|
80
|
+
req
|
|
81
|
+
}) : null;
|
|
82
|
+
const titleOf = (doc) => {
|
|
83
|
+
const value = titleField === void 0 ? void 0 : doc?.[titleField];
|
|
84
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
85
|
+
};
|
|
86
|
+
const published = await read(false);
|
|
87
|
+
const live = titleOf(published);
|
|
88
|
+
const latest = titleOf(await read(true));
|
|
89
|
+
const title = live === void 0 ? latest : latest !== void 0 && latest !== live ? `${live} (draft: ${latest})` : live;
|
|
90
|
+
const status = published?.["_status"];
|
|
91
|
+
return {
|
|
92
|
+
label: (typeof config?.labels.singular === "function" ? void 0 : translateStatic(config?.labels.singular, req.i18n)) ?? slug,
|
|
93
|
+
...title === void 0 ? {} : { title },
|
|
94
|
+
...typeof status === "string" ? { status } : {},
|
|
95
|
+
id: String(id),
|
|
96
|
+
href: formatAdminURL({
|
|
97
|
+
adminRoute: req.payload.config.routes.admin,
|
|
98
|
+
path: `/collections/${slug}/${String(id)}`
|
|
99
|
+
}),
|
|
100
|
+
permanent: config?.trash !== true,
|
|
101
|
+
...typeof args["reason"] === "string" ? { reason: args["reason"] } : {}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
handler: async ({ args, scope, confirmation }) => {
|
|
106
|
+
const target = {
|
|
107
|
+
...resolveCollection(scope, args.collection, "delete"),
|
|
108
|
+
id: args.id
|
|
109
|
+
};
|
|
110
|
+
const { req } = scope;
|
|
111
|
+
const { payload } = req;
|
|
112
|
+
const trash = target.config.trash ?? false;
|
|
113
|
+
return await withTransaction(req, async () => {
|
|
114
|
+
const doc = await readDraft(scope, {
|
|
115
|
+
target,
|
|
116
|
+
locale: void 0,
|
|
117
|
+
trash: true
|
|
118
|
+
});
|
|
119
|
+
if (doc["deletedAt"]) return errorResult("The document is already in the trash. Emptying the trash is not available.");
|
|
120
|
+
const stale = staleReadResult(doc, args.expectedUpdatedAt, "The document changed since you read it. Read it again before deleting.");
|
|
121
|
+
if (stale) return stale;
|
|
122
|
+
if (!await mayDelete(req, target)) throw new Forbidden(req.t);
|
|
123
|
+
const unattended = trash && scope.capabilities.collections[target.slug]?.deleteUnattended === true;
|
|
124
|
+
if (!confirmation.confirmed && !unattended) return await confirmation.request({
|
|
125
|
+
...args,
|
|
126
|
+
expectedUpdatedAt: new Date(doc["updatedAt"]).toISOString()
|
|
127
|
+
});
|
|
128
|
+
if (trash) await payload.update({
|
|
129
|
+
collection: target.slug,
|
|
130
|
+
id: target.id,
|
|
131
|
+
data: withTrashIntent({ deletedAt: (/* @__PURE__ */ new Date()).toISOString() }),
|
|
132
|
+
depth: 0,
|
|
133
|
+
overrideAccess: false,
|
|
134
|
+
overrideLock: false,
|
|
135
|
+
req
|
|
136
|
+
});
|
|
137
|
+
else await payload.delete({
|
|
138
|
+
collection: target.slug,
|
|
139
|
+
id: target.id,
|
|
140
|
+
depth: 0,
|
|
141
|
+
overrideAccess: false,
|
|
142
|
+
overrideLock: false,
|
|
143
|
+
req
|
|
144
|
+
});
|
|
145
|
+
return jsonResult({
|
|
146
|
+
collection: target.slug,
|
|
147
|
+
id: target.id,
|
|
148
|
+
movedToTrash: trash
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
//#endregion
|
|
154
|
+
export { deleteDocument };
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { defineMcpxTool } from "../define-tool.mjs";
|
|
2
2
|
import { jsonResult } from "../result.mjs";
|
|
3
|
+
import { SchemaError } from "../schema/errors.mjs";
|
|
3
4
|
import { nodePropertiesFor } from "../schema/lexical.mjs";
|
|
4
5
|
import { translatorFor } from "../i18n.mjs";
|
|
5
|
-
import { SchemaError } from "../schema/errors.mjs";
|
|
6
6
|
import { nodeDescriber, reachableSchemaPaths } from "../schema/describe.mjs";
|
|
7
7
|
import "../schema/index.mjs";
|
|
8
|
-
import { entityShape } from "./shared.mjs";
|
|
8
|
+
import { entityShape, reaches } from "./shared.mjs";
|
|
9
9
|
import { resolveEntity } from "./document.mjs";
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
//#region src/tools/describe-schema.ts
|
|
@@ -30,7 +30,7 @@ const PATHS_LIMIT = 400;
|
|
|
30
30
|
readOnlyHint: true,
|
|
31
31
|
openWorldHint: false
|
|
32
32
|
},
|
|
33
|
-
isEnabled: (scope) => scope
|
|
33
|
+
isEnabled: (scope) => reaches(scope, "read"),
|
|
34
34
|
inputSchema: (scope) => ({
|
|
35
35
|
...entityShape(scope, "read", "Instead of \"collection\"."),
|
|
36
36
|
paths: z.array(z.string()).max(PATHS_LIMIT).optional().describe("Schema paths from \"next\". Omit for the top-level node."),
|
|
@@ -43,9 +43,12 @@ const PATHS_LIMIT = 400;
|
|
|
43
43
|
const expanded = args.expand === true ? reachableSchemaPaths(config, ref) : void 0;
|
|
44
44
|
const requested = expanded?.paths ?? (args.paths && args.paths.length > 0 ? args.paths : [""]);
|
|
45
45
|
const failures = [];
|
|
46
|
+
const nodeTypes = [];
|
|
46
47
|
const nodes = requested.map((schemaPath) => {
|
|
47
48
|
try {
|
|
48
|
-
|
|
49
|
+
const node = describeNode(config, ref, schemaPath);
|
|
50
|
+
nodeTypes.push(...node.fields.flatMap((field) => field.nodes ?? []));
|
|
51
|
+
return node;
|
|
49
52
|
} catch (error) {
|
|
50
53
|
if (error instanceof SchemaError) return {
|
|
51
54
|
error: error.message,
|
|
@@ -62,7 +65,6 @@ const PATHS_LIMIT = 400;
|
|
|
62
65
|
err: failures[0],
|
|
63
66
|
msg: `[payloadcms-mcpx] Describing ${String(failures.length)} schema paths failed.`
|
|
64
67
|
});
|
|
65
|
-
const nodeTypes = nodes.flatMap((node) => (node.fields ?? []).flatMap((field) => field.nodes ?? []));
|
|
66
68
|
if (nodeTypes.length > 0) nodes.push({ nodeProperties: nodePropertiesFor(nodeTypes) });
|
|
67
69
|
if (expanded?.truncated) nodes.push({ error: `Result truncated after ${String(400)} nodes. Request explicit paths instead.` });
|
|
68
70
|
return jsonResult(nodes);
|
package/dist/tools/document.mjs
CHANGED
|
@@ -1,34 +1,44 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { definedProps } from "../guards.mjs";
|
|
2
|
+
import { errorResult, jsonResult } from "../result.mjs";
|
|
3
|
+
import { documentLinks } from "./links.mjs";
|
|
3
4
|
import { collectPublishBlockers } from "../write/publish-blockers.mjs";
|
|
5
|
+
import { slugsFor } from "./shared.mjs";
|
|
4
6
|
import { APIError, Forbidden, NotFound } from "payload";
|
|
5
7
|
//#region src/tools/document.ts
|
|
6
8
|
/**
|
|
9
|
+
* `payload.globals` is `{ config: SanitizedGlobalConfig[] }`, an array, not the
|
|
10
|
+
* slug-keyed map `payload.collections` is.
|
|
11
|
+
*/ const globalConfig = (payload, slug) => payload.globals.config.find((candidate) => candidate.slug === slug);
|
|
12
|
+
/**
|
|
13
|
+
* For a tool that takes a collection and no global, so the result needs no
|
|
14
|
+
* check for one.
|
|
15
|
+
*/ const resolveCollection = (scope, slug, operation) => {
|
|
16
|
+
const found = scope.req.payload.collections[slug]?.config;
|
|
17
|
+
if (!slugsFor(scope, operation).collections.includes(slug) || !found) throw new Forbidden(scope.req.t);
|
|
18
|
+
return {
|
|
19
|
+
kind: "collection",
|
|
20
|
+
slug,
|
|
21
|
+
config: found
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
7
25
|
* A raw input shape leaves no top-level `.refine` to express "exactly one of
|
|
8
26
|
* collection and global", so the rule is enforced here, with a message naming
|
|
9
27
|
* the offending arguments.
|
|
10
28
|
*/ const resolveEntity = (scope, args, operation) => {
|
|
11
29
|
const { collection, global } = args;
|
|
12
|
-
const allowedSlugs = slugsFor(scope, operation);
|
|
13
30
|
if (collection !== void 0 && global !== void 0) throw new APIError("Pass either \"collection\" or \"global\", not both.", 400);
|
|
14
|
-
if (collection
|
|
15
|
-
if (
|
|
16
|
-
const found = scope.req.payload
|
|
17
|
-
if (!
|
|
31
|
+
if (collection !== void 0) return resolveCollection(scope, collection, operation);
|
|
32
|
+
if (global !== void 0) {
|
|
33
|
+
const found = globalConfig(scope.req.payload, global);
|
|
34
|
+
if (!slugsFor(scope, operation).globals.includes(global) || !found) throw new Forbidden(scope.req.t);
|
|
18
35
|
return {
|
|
19
|
-
kind: "
|
|
20
|
-
slug:
|
|
21
|
-
config: found
|
|
36
|
+
kind: "global",
|
|
37
|
+
slug: global,
|
|
38
|
+
config: found
|
|
22
39
|
};
|
|
23
40
|
}
|
|
24
|
-
|
|
25
|
-
const found = scope.req.payload.globals.config.find((candidate) => candidate.slug === slug);
|
|
26
|
-
if (!allowedSlugs.globals.includes(slug) || !found) throw new Forbidden(scope.req.t);
|
|
27
|
-
return {
|
|
28
|
-
kind: "global",
|
|
29
|
-
slug,
|
|
30
|
-
config: found
|
|
31
|
-
};
|
|
41
|
+
throw new APIError("One of \"collection\" or \"global\" is required. Call listCapabilities to see which slugs are available.", 400);
|
|
32
42
|
};
|
|
33
43
|
/**
|
|
34
44
|
* Resolves the entity and checks `id` against it. A collection document needs
|
|
@@ -48,7 +58,45 @@ import { APIError, Forbidden, NotFound } from "payload";
|
|
|
48
58
|
};
|
|
49
59
|
/**
|
|
50
60
|
* What a response names its subject by: a document's id, or a global's slug.
|
|
51
|
-
*/ const identityOf = (target
|
|
61
|
+
*/ const identityOf = (target) => target.kind === "collection" ? { id: target.id } : { global: target.slug };
|
|
62
|
+
/**
|
|
63
|
+
* The fields every write result starts with, read from the saved `doc`.
|
|
64
|
+
*/ const documentSummary = (target, doc) => ({
|
|
65
|
+
...target.kind === "collection" ? { id: doc["id"] } : { global: target.slug },
|
|
66
|
+
status: doc["_status"],
|
|
67
|
+
updatedAt: doc["updatedAt"]
|
|
68
|
+
});
|
|
69
|
+
/**
|
|
70
|
+
* Names the blockers of a check, and whether the check itself failed.
|
|
71
|
+
*/ const blockerFields = (result, key) => ({
|
|
72
|
+
...result.blockers.length > 0 ? { [key]: result.blockers } : {},
|
|
73
|
+
...result.unavailable ? { [`${key}Unavailable`]: true } : {}
|
|
74
|
+
});
|
|
75
|
+
/**
|
|
76
|
+
* What a tool that wrote a document returns: the document, re-read privileged
|
|
77
|
+
* in `locale`, with its links and publish blockers. `extra` follows them.
|
|
78
|
+
*/ const writtenResult = async (scope, args) => {
|
|
79
|
+
const { target, locale } = args;
|
|
80
|
+
const saved = await readDraft(scope, {
|
|
81
|
+
target,
|
|
82
|
+
locale,
|
|
83
|
+
privileged: true
|
|
84
|
+
});
|
|
85
|
+
const validation = await collectPublishBlockers(scope.req, {
|
|
86
|
+
doc: saved,
|
|
87
|
+
entity: target
|
|
88
|
+
});
|
|
89
|
+
return jsonResult({
|
|
90
|
+
...documentSummary(target, saved),
|
|
91
|
+
...await documentLinks(scope.req, {
|
|
92
|
+
target,
|
|
93
|
+
doc: saved,
|
|
94
|
+
locale
|
|
95
|
+
}),
|
|
96
|
+
...blockerFields(validation, "publishBlockers"),
|
|
97
|
+
...args.extra
|
|
98
|
+
});
|
|
99
|
+
};
|
|
52
100
|
const sameInstant = (left, right) => (typeof left === "string" || left instanceof Date) && new Date(left).getTime() === new Date(right).getTime();
|
|
53
101
|
/**
|
|
54
102
|
* Refuses a write when the document changed since the client read it.
|
|
@@ -75,7 +123,8 @@ const sameInstant = (left, right) => (typeof left === "string" || left instanceo
|
|
|
75
123
|
...shared,
|
|
76
124
|
collection: args.target.slug,
|
|
77
125
|
id: args.target.id,
|
|
78
|
-
disableErrors: true
|
|
126
|
+
disableErrors: true,
|
|
127
|
+
...args.trash ? { trash: true } : {}
|
|
79
128
|
});
|
|
80
129
|
if (!doc) throw new NotFound(scope.req.t);
|
|
81
130
|
return doc;
|
|
@@ -86,39 +135,76 @@ const sameInstant = (left, right) => (typeof left === "string" || left instanceo
|
|
|
86
135
|
});
|
|
87
136
|
};
|
|
88
137
|
/**
|
|
138
|
+
* Updates a collection document or a global with no fallback locale, so a value
|
|
139
|
+
* missing in the written locale is not backfilled from another and persisted.
|
|
140
|
+
*/ const updateTarget = async (scope, target, write) => {
|
|
141
|
+
const { payload } = scope.req;
|
|
142
|
+
const { data, draft, file, ...optional } = write;
|
|
143
|
+
const shared = {
|
|
144
|
+
data,
|
|
145
|
+
depth: 0,
|
|
146
|
+
draft,
|
|
147
|
+
fallbackLocale: false,
|
|
148
|
+
overrideAccess: false,
|
|
149
|
+
req: scope.req,
|
|
150
|
+
...definedProps(optional)
|
|
151
|
+
};
|
|
152
|
+
if (target.kind === "collection") await payload.update({
|
|
153
|
+
...shared,
|
|
154
|
+
...definedProps({ file }),
|
|
155
|
+
collection: target.slug,
|
|
156
|
+
id: target.id
|
|
157
|
+
});
|
|
158
|
+
else await payload.updateGlobal({
|
|
159
|
+
...shared,
|
|
160
|
+
slug: target.slug
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Runs `run`, then puts back the request's locale and fallback locale, which
|
|
165
|
+
* Payload's local API assigns in place.
|
|
166
|
+
*/ const withRequestLocale = async (req, run) => {
|
|
167
|
+
const { fallbackLocale, locale } = req;
|
|
168
|
+
try {
|
|
169
|
+
return await run();
|
|
170
|
+
} finally {
|
|
171
|
+
if (locale !== void 0) req.locale = locale;
|
|
172
|
+
if (fallbackLocale !== void 0) req.fallbackLocale = fallbackLocale;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
89
176
|
* {@link collectPublishBlockers} over the draft of each locale, each blocker
|
|
90
177
|
* tagged with its locale. Locales run in turn because hooks share `req`, and
|
|
91
178
|
* the request's locale and fallback locale are restored afterwards.
|
|
92
179
|
*/ const collectLocaleBlockers = async (scope, target, locales) => {
|
|
93
180
|
const { req } = scope;
|
|
94
|
-
const { fallbackLocale, locale: requestLocale } = req;
|
|
95
181
|
const blockers = [];
|
|
96
|
-
let unavailable = false;
|
|
97
|
-
for (const locale of locales) try {
|
|
98
|
-
const doc = await readDraft(scope, {
|
|
99
|
-
target,
|
|
100
|
-
locale,
|
|
101
|
-
privileged: true
|
|
102
|
-
});
|
|
103
|
-
const result = await collectPublishBlockers(req, {
|
|
104
|
-
doc,
|
|
105
|
-
entity: target
|
|
106
|
-
});
|
|
107
|
-
unavailable ||= result.unavailable === true;
|
|
108
|
-
blockers.push(...result.blockers.map((entry) => ({
|
|
109
|
-
...entry,
|
|
110
|
-
locale
|
|
111
|
-
})));
|
|
112
|
-
} catch (error) {
|
|
113
|
-
req.payload.logger.warn(`[payloadcms-mcpx] Could not read the ${target.slug} draft in ${locale}: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
114
|
-
unavailable = true;
|
|
115
|
-
}
|
|
116
|
-
if (requestLocale !== void 0) req.locale = requestLocale;
|
|
117
|
-
if (fallbackLocale !== void 0) req.fallbackLocale = fallbackLocale;
|
|
118
182
|
return {
|
|
119
183
|
blockers,
|
|
120
|
-
...
|
|
184
|
+
...await withRequestLocale(req, async () => {
|
|
185
|
+
let failed = false;
|
|
186
|
+
for (const locale of locales) try {
|
|
187
|
+
const doc = await readDraft(scope, {
|
|
188
|
+
target,
|
|
189
|
+
locale,
|
|
190
|
+
privileged: true
|
|
191
|
+
});
|
|
192
|
+
const result = await collectPublishBlockers(req, {
|
|
193
|
+
doc,
|
|
194
|
+
entity: target
|
|
195
|
+
});
|
|
196
|
+
failed ||= result.unavailable === true;
|
|
197
|
+
blockers.push(...result.blockers.map((entry) => ({
|
|
198
|
+
...entry,
|
|
199
|
+
locale
|
|
200
|
+
})));
|
|
201
|
+
} catch (error) {
|
|
202
|
+
req.payload.logger.warn(`[payloadcms-mcpx] Could not read the ${target.slug} draft in ${locale}: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
203
|
+
failed = true;
|
|
204
|
+
}
|
|
205
|
+
return failed;
|
|
206
|
+
}) ? { unavailable: true } : {}
|
|
121
207
|
};
|
|
122
208
|
};
|
|
123
209
|
//#endregion
|
|
124
|
-
export { collectLocaleBlockers, identityOf, readDraft, resolveDocument, resolveEntity, staleReadResult };
|
|
210
|
+
export { blockerFields, collectLocaleBlockers, documentSummary, globalConfig, identityOf, readDraft, resolveCollection, resolveDocument, resolveEntity, staleReadResult, updateTarget, withRequestLocale, writtenResult };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { defineMcpxTool } from "../define-tool.mjs";
|
|
2
|
+
import { idSchema, liveWriteSentence, localeOf, slugsFor, stringEnum } from "./shared.mjs";
|
|
3
|
+
import { resolveCollection, writtenResult } from "./document.mjs";
|
|
4
|
+
import { withDuplicateIntent } from "../write/publish-intent.mjs";
|
|
5
|
+
import { Forbidden } from "payload";
|
|
6
|
+
//#region src/tools/duplicate-document.ts
|
|
7
|
+
const duplicableSlugs = (scope) => slugsFor(scope, "create").collections.filter((slug) => scope.req.payload.collections[slug]?.config.disableDuplicate !== true && !scope.exposure.collections.some((entity) => entity.slug === slug && entity.isUpload));
|
|
8
|
+
const DESCRIPTION = (scope) => `Copies a document into a new one in the same collection. Use it instead of getDocument and createDocument to copy a document. It copies the latest version, the draft if there is one, in every locale. Unique text fields get " - Copy" appended. Returns the same fields as createDocument. To rename the copy, call patchDocument with the returned "updatedAt".
|
|
9
|
+
|
|
10
|
+
${liveWriteSentence(scope, "create")}`;
|
|
11
|
+
/**
|
|
12
|
+
* Wraps Payload's duplicate, which checks read access on the source and create
|
|
13
|
+
* access on the copy, and runs the fields' `beforeDuplicate` hooks.
|
|
14
|
+
*/ const duplicateDocument = defineMcpxTool({
|
|
15
|
+
name: "duplicateDocument",
|
|
16
|
+
description: DESCRIPTION,
|
|
17
|
+
annotations: {
|
|
18
|
+
readOnlyHint: false,
|
|
19
|
+
destructiveHint: false,
|
|
20
|
+
idempotentHint: false,
|
|
21
|
+
openWorldHint: false
|
|
22
|
+
},
|
|
23
|
+
isEnabled: (scope) => duplicableSlugs(scope).length > 0,
|
|
24
|
+
inputSchema: (scope) => ({
|
|
25
|
+
collection: stringEnum(duplicableSlugs(scope)),
|
|
26
|
+
id: idSchema
|
|
27
|
+
}),
|
|
28
|
+
handler: async ({ args, scope }) => {
|
|
29
|
+
const target = resolveCollection(scope, args.collection, "create");
|
|
30
|
+
if (!duplicableSlugs(scope).includes(target.slug)) throw new Forbidden(scope.req.t);
|
|
31
|
+
const created = await scope.req.payload.duplicate({
|
|
32
|
+
collection: target.slug,
|
|
33
|
+
id: args.id,
|
|
34
|
+
data: withDuplicateIntent({}),
|
|
35
|
+
depth: 0,
|
|
36
|
+
draft: true,
|
|
37
|
+
overrideAccess: false,
|
|
38
|
+
req: scope.req
|
|
39
|
+
});
|
|
40
|
+
return await writtenResult(scope, {
|
|
41
|
+
target: {
|
|
42
|
+
...target,
|
|
43
|
+
id: created["id"]
|
|
44
|
+
},
|
|
45
|
+
locale: localeOf(scope, void 0)
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
//#endregion
|
|
50
|
+
export { duplicateDocument };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { defineMcpxTool } from "../define-tool.mjs";
|
|
2
|
+
import { definedProps } from "../guards.mjs";
|
|
2
3
|
import { jsonResult } from "../result.mjs";
|
|
3
4
|
import { stripAdminHidden } from "../schema/admin-hidden.mjs";
|
|
4
5
|
import "../schema/index.mjs";
|
|
5
|
-
import { READ_LOCALE_DESCRIPTION, depthShape, localeOf, localeShape,
|
|
6
|
-
import {
|
|
6
|
+
import { READ_LOCALE_DESCRIPTION, depthShape, localeOf, localeShape, pageFields, pageShape, stringEnum } from "./shared.mjs";
|
|
7
|
+
import { resolveCollection } from "./document.mjs";
|
|
7
8
|
import { assertQueryable } from "./query-paths.mjs";
|
|
8
9
|
import { mcpxReadRequest } from "./read-request.mjs";
|
|
9
10
|
import { z } from "zod";
|
|
@@ -23,11 +24,10 @@ import { z } from "zod";
|
|
|
23
24
|
},
|
|
24
25
|
isEnabled: (scope) => scope.collections.readable.length > 0,
|
|
25
26
|
inputSchema: (scope) => ({
|
|
26
|
-
collection:
|
|
27
|
+
collection: stringEnum(scope.collections.readable),
|
|
27
28
|
where: z.record(z.string(), z.unknown()).optional(),
|
|
28
29
|
sort: z.string().optional().describe("e.g. \"-updatedAt\" for descending."),
|
|
29
|
-
|
|
30
|
-
page: z.number().int().min(1).optional(),
|
|
30
|
+
...pageShape(scope),
|
|
31
31
|
...depthShape(scope),
|
|
32
32
|
select: z.record(z.string(), z.unknown()).optional().describe("Fields to return, e.g. {\"title\":true}."),
|
|
33
33
|
...localeShape(scope, {
|
|
@@ -37,7 +37,7 @@ import { z } from "zod";
|
|
|
37
37
|
draft: z.boolean().optional().describe("Default true: latest drafts.")
|
|
38
38
|
}),
|
|
39
39
|
handler: async ({ args, scope }) => {
|
|
40
|
-
|
|
40
|
+
const target = resolveCollection(scope, args.collection, "read");
|
|
41
41
|
const locale = localeOf(scope, args.locale);
|
|
42
42
|
assertQueryable(scope, {
|
|
43
43
|
collection: args.collection,
|
|
@@ -52,22 +52,17 @@ import { z } from "zod";
|
|
|
52
52
|
limit: args.limit ?? 10,
|
|
53
53
|
overrideAccess: false,
|
|
54
54
|
req: mcpxReadRequest(scope),
|
|
55
|
-
...
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
...definedProps({
|
|
56
|
+
page: args.page,
|
|
57
|
+
sort: args.sort,
|
|
58
|
+
where: args.where,
|
|
59
|
+
select: args.select,
|
|
60
|
+
locale
|
|
61
|
+
})
|
|
60
62
|
});
|
|
61
63
|
return jsonResult({
|
|
62
|
-
docs: result.docs.map((doc) => stripAdminHidden(scope.req.payload.config,
|
|
63
|
-
|
|
64
|
-
slug: args.collection
|
|
65
|
-
}, doc)),
|
|
66
|
-
totalDocs: result.totalDocs,
|
|
67
|
-
page: result.page,
|
|
68
|
-
totalPages: result.totalPages,
|
|
69
|
-
limit: result.limit,
|
|
70
|
-
hasNextPage: result.hasNextPage
|
|
64
|
+
docs: result.docs.map((doc) => stripAdminHidden(scope.req.payload.config, target, doc)),
|
|
65
|
+
...pageFields(result)
|
|
71
66
|
});
|
|
72
67
|
}
|
|
73
68
|
});
|
|
@@ -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
|
});
|