@abinnovision/payloadcms-mcpx 1.0.0-beta.3 → 1.0.0-beta.5
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 +77 -24
- package/dist/api-keys/fields.mjs +27 -12
- package/dist/capabilities.mjs +16 -3
- package/dist/endpoint/handler.mjs +3 -1
- package/dist/endpoint/result.mjs +9 -3
- package/dist/endpoint/server.mjs +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/options.d.mts +2 -0
- package/dist/options.mjs +39 -1
- package/dist/plugin.d.mts +1 -1
- package/dist/plugin.mjs +3 -2
- package/dist/schema/describe.mjs +19 -25
- package/dist/schema/pointer.mjs +9 -12
- package/dist/schema/shape.mjs +9 -9
- package/dist/schema/walk.d.mts +1 -0
- package/dist/schema/walk.mjs +21 -12
- package/dist/tools/create-document.mjs +10 -9
- package/dist/tools/describe-schema.mjs +15 -9
- package/dist/tools/find-documents.mjs +4 -3
- package/dist/tools/get-document.mjs +26 -12
- package/dist/tools/list-capabilities.mjs +20 -2
- package/dist/tools/patch-document.mjs +35 -21
- package/dist/tools/shared.mjs +54 -21
- package/dist/tools/target.d.mts +3 -0
- package/dist/tools/target.mjs +49 -0
- package/dist/tools/types.d.mts +5 -0
- package/dist/tools/validate-document.mjs +22 -15
- package/dist/types.d.mts +25 -2
- package/dist/version.mjs +1 -1
- package/dist/write/draft-guard.mjs +51 -8
- package/dist/write/patch.mjs +8 -9
- package/dist/write/publish-blockers.d.mts +2 -0
- package/dist/write/publish-blockers.mjs +10 -8
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "payload";
|
package/dist/schema/walk.mjs
CHANGED
|
@@ -11,13 +11,22 @@ import { fieldIsHiddenOrDisabled, fieldIsVirtual } from "payload/shared";
|
|
|
11
11
|
"updatedAt"
|
|
12
12
|
]);
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
*/ const joinPath = (parts) => parts.reduce((path, part) => part === "[]" ? `${path}[]` : path ? `${path}.${part}` : part, "");
|
|
14
|
+
* Shape a JSON Pointer must have to be parseable at all.
|
|
15
|
+
*/ const JSON_POINTER_PATTERN = /^(\/([^~/]|~[01])*)*$/;
|
|
17
16
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
17
|
+
* Joins segments into a JSON Pointer, so the segments `items`, `*`, `title`
|
|
18
|
+
* read as one path to a subfield of every element of `items`. No segments is
|
|
19
|
+
* the root pointer, `""`.
|
|
20
|
+
*/ const joinPath = (parts) => parts.map((part) => `/${part.replace(/~/g, "~0").replace(/\//g, "~1")}`).join("");
|
|
21
|
+
/**
|
|
22
|
+
* Splits a JSON Pointer into its segments, unescaping `~1` and `~0`. The root
|
|
23
|
+
* pointer yields no segments.
|
|
24
|
+
*/ const splitPath = (path) => path.split("/").slice(1).map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
25
|
+
/**
|
|
26
|
+
* Restates a path Payload reports on a validation error (`layout.0.title`) as
|
|
27
|
+
* a JSON Pointer, so everything this plugin hands back addresses documents the
|
|
28
|
+
* same way. Payload's path already carries real indices, so it maps directly.
|
|
29
|
+
*/ const pointerFromPayloadPath = (path) => path ? joinPath(path.split(".")) : "";
|
|
21
30
|
/**
|
|
22
31
|
* Blocks a blocks field accepts, by slug. On a flattened field, whichever of
|
|
23
32
|
* `blockReferences` and `blocks` was declared carries the definitions.
|
|
@@ -83,7 +92,7 @@ const withRows = (descriptor, field) => ({
|
|
|
83
92
|
const readOnly = parentReadOnly || isReadOnly(field);
|
|
84
93
|
const path = [...prefix, field.name];
|
|
85
94
|
if (field.type === "tab" || field.type === "group") return describeFields(field.flattenedFields, path, readOnly);
|
|
86
|
-
if (field.type === "array") return describeFields(field.flattenedFields, [...path, "
|
|
95
|
+
if (field.type === "array") return describeFields(field.flattenedFields, [...path, "*"], readOnly);
|
|
87
96
|
if (field.type === "blocks") return [withRows({
|
|
88
97
|
...describeBase(field, joinPath(path), readOnly),
|
|
89
98
|
blocks: blockSlugsOf(field)
|
|
@@ -97,13 +106,13 @@ const withRows = (descriptor, field) => ({
|
|
|
97
106
|
if (!("name" in field) || field.name !== path[0]) continue;
|
|
98
107
|
if (field.type === "blocks" && path.length === 1) return field;
|
|
99
108
|
if (field.type === "tab" || field.type === "group") return findBlocksField(field.flattenedFields, path.slice(1));
|
|
100
|
-
if (field.type === "array" && path[1] === "
|
|
109
|
+
if (field.type === "array" && path[1] === "*") return findBlocksField(field.flattenedFields, path.slice(2));
|
|
101
110
|
}
|
|
102
111
|
};
|
|
103
|
-
const
|
|
104
|
-
const found = config.collections.find((candidate) => candidate.slug ===
|
|
105
|
-
if (!found) throw new Error(`Unknown
|
|
112
|
+
const targetOf = (config, ref) => {
|
|
113
|
+
const found = ref.kind === "collection" ? config.collections.find((candidate) => candidate.slug === ref.slug) : config.globals.find((candidate) => candidate.slug === ref.slug);
|
|
114
|
+
if (!found) throw new Error(`Unknown ${ref.kind} "${ref.slug}".`);
|
|
106
115
|
return found;
|
|
107
116
|
};
|
|
108
117
|
//#endregion
|
|
109
|
-
export { RESERVED_FIELD_NAMES, blockOf, blockSlugsOf,
|
|
118
|
+
export { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, blockSlugsOf, describeFields, findBlocksField, joinPath, pointerFromPayloadPath, splitPath, staticDescription, targetOf };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { errorResult, jsonResult } from "../endpoint/result.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { localeOf, localeShape, readTarget, slugEnum } from "./shared.mjs";
|
|
3
|
+
import { resolveTarget } from "./target.mjs";
|
|
3
4
|
import { validateWriteValue } from "../schema/shape.mjs";
|
|
4
5
|
import { stripRowIds } from "../write/patch.mjs";
|
|
5
6
|
import { collectPublishBlockers } from "../write/publish-blockers.mjs";
|
|
@@ -16,7 +17,7 @@ const createDocument = {
|
|
|
16
17
|
},
|
|
17
18
|
isEnabled: (scope) => scope.writable.length > 0,
|
|
18
19
|
inputSchema: (scope) => ({
|
|
19
|
-
collection:
|
|
20
|
+
collection: slugEnum(scope.writable).describe("Collection to create the document in."),
|
|
20
21
|
...localeShape(scope, {
|
|
21
22
|
required: true,
|
|
22
23
|
description: "Locale the localized fields of the seed belong to."
|
|
@@ -24,15 +25,15 @@ const createDocument = {
|
|
|
24
25
|
data: z.record(z.string(), z.unknown()).describe("Initial field values, as describeSchema lists them.")
|
|
25
26
|
}),
|
|
26
27
|
handler: async (args, scope) => {
|
|
27
|
-
const
|
|
28
|
+
const target = resolveTarget(scope, { collection: args.collection }, "write");
|
|
28
29
|
const { payload } = scope.req;
|
|
29
30
|
const locale = localeOf(scope, args.locale);
|
|
30
31
|
const { id: _ignored, ...seed } = args.data;
|
|
31
32
|
const problems = validateWriteValue(payload.config, {
|
|
32
33
|
pointer: "",
|
|
33
34
|
resolution: {
|
|
34
|
-
fields:
|
|
35
|
-
prefix:
|
|
35
|
+
fields: target.config.flattenedFields,
|
|
36
|
+
prefix: []
|
|
36
37
|
}
|
|
37
38
|
}, seed);
|
|
38
39
|
if (problems.length > 0) return errorResult("Nothing was created.", { problems });
|
|
@@ -45,15 +46,15 @@ const createDocument = {
|
|
|
45
46
|
req: scope.req,
|
|
46
47
|
...locale === void 0 ? {} : { locale }
|
|
47
48
|
});
|
|
48
|
-
const saved = await
|
|
49
|
-
|
|
49
|
+
const saved = await readTarget(scope, {
|
|
50
|
+
target,
|
|
50
51
|
id: created["id"],
|
|
51
52
|
locale,
|
|
52
53
|
privileged: true
|
|
53
54
|
});
|
|
54
55
|
const publishBlockers = await collectPublishBlockers(scope.req, {
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
doc: saved,
|
|
57
|
+
entity: target
|
|
57
58
|
});
|
|
58
59
|
return jsonResult({
|
|
59
60
|
id: saved["id"],
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsonResult } from "../endpoint/result.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { targetShape } from "./shared.mjs";
|
|
3
|
+
import { refOf, resolveTarget } from "./target.mjs";
|
|
3
4
|
import { describeNode, reachableSchemaPaths } from "../schema/describe.mjs";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
//#region src/tools/describe-schema.ts
|
|
@@ -7,28 +8,33 @@ const describeSchema = {
|
|
|
7
8
|
name: "describeSchema",
|
|
8
9
|
description: `Describes the writable shape of a document, one node at a time.
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
Pass exactly one of "collection" and "global". A global is a singleton: it has no id, is not listed by findDocuments and cannot be created.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
Call it with no "paths" to get a collection's own fields. Every "blocks" field stops there and lists the block slugs it accepts instead of nesting them; each node's "next" lists the ready-to-use paths for those blocks, so pass any entry of "next" as a "paths" element to descend, e.g. "/layout/sections/sectionWrapper" and then "/layout/sections/sectionWrapper/modules/hero". A block is described as it exists at that position, because the same block can accept different children elsewhere.
|
|
14
|
+
|
|
15
|
+
Paths here use the same JSON Pointer syntax as getDocument and patchDocument, and are already resolved through anything that does not nest in the stored document. The difference is only what stands in an element position: a path names an array element "*" and a block by its slug, where a pointer into a document carries a 0-based index. So "/items/*/title" is written at "/items/0/title", and "/layout/sections/hero" at "/layout/sections/0".
|
|
13
16
|
|
|
14
17
|
Fields Payload maintains (id, _status, createdAt, updatedAt) are never listed and cannot be written. Fields marked readOnly are listed but refused on write.`,
|
|
15
18
|
annotations: {
|
|
16
19
|
readOnlyHint: true,
|
|
17
20
|
openWorldHint: false
|
|
18
21
|
},
|
|
19
|
-
isEnabled: (scope) => scope.readable.length > 0,
|
|
22
|
+
isEnabled: (scope) => scope.readable.length + scope.readableGlobals.length > 0,
|
|
20
23
|
inputSchema: (scope) => ({
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
...targetShape(scope, "read", {
|
|
25
|
+
collection: "Collection to describe.",
|
|
26
|
+
global: "Global to describe."
|
|
27
|
+
}),
|
|
28
|
+
paths: z.array(z.string()).optional().describe("Schema paths to describe, e.g. \"/layout/sections/sectionWrapper\". Omit for the collection root."),
|
|
23
29
|
expand: z.boolean().optional().describe("Return every node reachable from the root in one response. Ignores paths.")
|
|
24
30
|
}),
|
|
25
31
|
handler: (args, scope) => {
|
|
26
|
-
|
|
32
|
+
const ref = refOf(resolveTarget(scope, args, "read"));
|
|
27
33
|
const { config } = scope.req.payload;
|
|
28
|
-
const expanded = args.expand === true ? reachableSchemaPaths(config,
|
|
34
|
+
const expanded = args.expand === true ? reachableSchemaPaths(config, ref) : void 0;
|
|
29
35
|
const nodes = (expanded?.paths ?? (args.paths && args.paths.length > 0 ? args.paths : [""])).map((schemaPath) => {
|
|
30
36
|
try {
|
|
31
|
-
return describeNode(config,
|
|
37
|
+
return describeNode(config, ref, schemaPath);
|
|
32
38
|
} catch (error) {
|
|
33
39
|
return {
|
|
34
40
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsonResult } from "../endpoint/result.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { depthShape, localeOf, localeShape, slugEnum } from "./shared.mjs";
|
|
3
|
+
import { resolveTarget } from "./target.mjs";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
//#region src/tools/find-documents.ts
|
|
5
6
|
const findDocuments = {
|
|
@@ -11,7 +12,7 @@ const findDocuments = {
|
|
|
11
12
|
},
|
|
12
13
|
isEnabled: (scope) => scope.readable.length > 0,
|
|
13
14
|
inputSchema: (scope) => ({
|
|
14
|
-
collection:
|
|
15
|
+
collection: slugEnum(scope.readable).describe("Collection to search."),
|
|
15
16
|
where: z.record(z.string(), z.unknown()).optional().describe("Payload where query."),
|
|
16
17
|
sort: z.string().optional().describe("Sort field, prefix with \"-\" for descending."),
|
|
17
18
|
limit: z.number().int().min(1).max(scope.options.limits.maxLimit).optional().describe(`Documents per page. Default 10, at most ${String(scope.options.limits.maxLimit)}.`),
|
|
@@ -25,7 +26,7 @@ const findDocuments = {
|
|
|
25
26
|
draft: z.boolean().optional().describe("Include the latest drafts. Default true.")
|
|
26
27
|
}),
|
|
27
28
|
handler: async (args, scope) => {
|
|
28
|
-
|
|
29
|
+
resolveTarget(scope, { collection: args.collection }, "read");
|
|
29
30
|
const locale = localeOf(scope, args.locale);
|
|
30
31
|
const result = await scope.req.payload.find({
|
|
31
32
|
collection: args.collection,
|
|
@@ -1,20 +1,27 @@
|
|
|
1
|
+
import { JSON_POINTER_PATTERN } from "../schema/walk.mjs";
|
|
1
2
|
import { errorResult, jsonResult } from "../endpoint/result.mjs";
|
|
2
|
-
import {
|
|
3
|
+
import { depthShape, idShape, localeOf, localeShape, targetShape } from "./shared.mjs";
|
|
4
|
+
import { requireIdFor, resolveTarget } from "./target.mjs";
|
|
3
5
|
import { z } from "zod";
|
|
4
6
|
import { Pointer } from "rfc6902";
|
|
5
7
|
//#region src/tools/get-document.ts
|
|
6
8
|
const getDocument = {
|
|
7
9
|
name: "getDocument",
|
|
8
|
-
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
|
|
10
|
+
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.
|
|
11
|
+
|
|
12
|
+
Pass exactly one of "collection" and "global". "id" is required with "collection" and must be omitted with "global", because a global is a singleton.`,
|
|
9
13
|
annotations: {
|
|
10
14
|
readOnlyHint: true,
|
|
11
15
|
openWorldHint: false
|
|
12
16
|
},
|
|
13
|
-
isEnabled: (scope) => scope.readable.length > 0,
|
|
17
|
+
isEnabled: (scope) => scope.readable.length + scope.readableGlobals.length > 0,
|
|
14
18
|
inputSchema: (scope) => ({
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
...targetShape(scope, "read", {
|
|
20
|
+
collection: "Collection holding the document.",
|
|
21
|
+
global: "Global to read."
|
|
22
|
+
}),
|
|
23
|
+
...idShape(scope, "read"),
|
|
24
|
+
path: z.string().regex(JSON_POINTER_PATTERN).optional().describe("JSON pointer to return only a subtree, e.g. \"/layout/sections/0\"."),
|
|
18
25
|
...depthShape(scope),
|
|
19
26
|
...localeShape(scope, {
|
|
20
27
|
required: false,
|
|
@@ -23,17 +30,24 @@ const getDocument = {
|
|
|
23
30
|
draft: z.boolean().optional().describe("Return the latest draft. Default true.")
|
|
24
31
|
}),
|
|
25
32
|
handler: async (args, scope) => {
|
|
26
|
-
|
|
33
|
+
const target = resolveTarget(scope, args, "read");
|
|
34
|
+
const id = requireIdFor(target, args.id);
|
|
27
35
|
const locale = localeOf(scope, args.locale);
|
|
28
|
-
const
|
|
29
|
-
collection: args.collection,
|
|
30
|
-
id: args.id,
|
|
36
|
+
const shared = {
|
|
31
37
|
depth: args.depth ?? 0,
|
|
32
38
|
draft: args.draft ?? true,
|
|
33
39
|
overrideAccess: false,
|
|
34
40
|
req: scope.req,
|
|
35
41
|
...locale === void 0 ? {} : { locale }
|
|
36
|
-
}
|
|
42
|
+
};
|
|
43
|
+
const doc = await (target.kind === "collection" ? scope.req.payload.findByID({
|
|
44
|
+
...shared,
|
|
45
|
+
collection: target.slug,
|
|
46
|
+
id
|
|
47
|
+
}) : scope.req.payload.findGlobal({
|
|
48
|
+
...shared,
|
|
49
|
+
slug: target.slug
|
|
50
|
+
}));
|
|
37
51
|
if (args.path === void 0 || args.path === "") return jsonResult(doc);
|
|
38
52
|
let value;
|
|
39
53
|
try {
|
|
@@ -42,7 +56,7 @@ const getDocument = {
|
|
|
42
56
|
return errorResult(`"${args.path}" is not a valid JSON pointer.`);
|
|
43
57
|
}
|
|
44
58
|
return jsonResult({
|
|
45
|
-
id: doc["id"],
|
|
59
|
+
...target.kind === "collection" ? { id: doc["id"] } : { global: target.slug },
|
|
46
60
|
status: doc["_status"],
|
|
47
61
|
updatedAt: doc["updatedAt"],
|
|
48
62
|
path: args.path,
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import { staticDescription } from "../schema/walk.mjs";
|
|
1
2
|
import { jsonResult } from "../endpoint/result.mjs";
|
|
2
3
|
import { translateLabel } from "./shared.mjs";
|
|
3
|
-
import { staticDescription } from "../schema/walk.mjs";
|
|
4
4
|
import { hasDraftValidationEnabled } from "payload/shared";
|
|
5
5
|
//#region src/tools/list-capabilities.ts
|
|
6
6
|
const listCapabilities = {
|
|
7
7
|
name: "listCapabilities",
|
|
8
|
-
description: `Lists what this key may do: the collections it can read or write, their draft behaviour and id type, the configured locales, the limits in force and the custom tools available. Call it first to orient; nothing here changes with the content model
|
|
8
|
+
description: `Lists what this key may do: the collections and globals it can read or write, their draft behaviour and id type, the configured locales, the limits in force and the custom tools available. Call it first to orient; nothing here changes with the content model.
|
|
9
|
+
|
|
10
|
+
A global is a singleton: it has no id, is not listed by findDocuments and cannot be created. Address one with the "global" argument where a collection document would take "collection" and "id".`,
|
|
9
11
|
annotations: {
|
|
10
12
|
readOnlyHint: true,
|
|
11
13
|
openWorldHint: false
|
|
@@ -34,8 +36,24 @@ const listCapabilities = {
|
|
|
34
36
|
idType: collection.customIDType ?? payload.db.defaultIDType
|
|
35
37
|
}];
|
|
36
38
|
});
|
|
39
|
+
const globals = scope.options.globals.flatMap((entry) => {
|
|
40
|
+
const capability = scope.capabilities.globals[entry.slug];
|
|
41
|
+
const config = payload.globals.config.find((candidate) => candidate.slug === entry.slug);
|
|
42
|
+
if (!capability || !config || !(capability.read || capability.write)) return [];
|
|
43
|
+
const description = staticDescription(config.admin.description);
|
|
44
|
+
return [{
|
|
45
|
+
slug: entry.slug,
|
|
46
|
+
label: translateLabel(scope, config.label, entry.slug),
|
|
47
|
+
...description === void 0 ? {} : { description },
|
|
48
|
+
read: capability.read,
|
|
49
|
+
write: capability.write,
|
|
50
|
+
drafts: entry.hasDrafts,
|
|
51
|
+
draftValidation: hasDraftValidationEnabled(config)
|
|
52
|
+
}];
|
|
53
|
+
});
|
|
37
54
|
return Promise.resolve(jsonResult({
|
|
38
55
|
collections,
|
|
56
|
+
...globals.length > 0 ? { globals } : {},
|
|
39
57
|
locales: scope.locales ? {
|
|
40
58
|
codes: scope.locales,
|
|
41
59
|
default: scope.defaultLocale
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { errorResult, jsonResult } from "../endpoint/result.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { idShape, localeOf, localeShape, readTarget, targetShape } from "./shared.mjs";
|
|
3
|
+
import { refOf, requireIdFor, resolveTarget } from "./target.mjs";
|
|
3
4
|
import { PATCH_OPERATION_SCHEMA, applyPatchToCopy, buildWriteData, findPatchProblems, isElementPointer } from "../write/patch.mjs";
|
|
4
5
|
import { collectPublishBlockers } from "../write/publish-blockers.mjs";
|
|
5
6
|
import { withTransaction } from "../write/transaction.mjs";
|
|
@@ -8,9 +9,11 @@ import { Pointer } from "rfc6902";
|
|
|
8
9
|
//#region src/tools/patch-document.ts
|
|
9
10
|
const DESCRIPTION = `Applies RFC 6902 JSON Patch operations to one document.
|
|
10
11
|
|
|
12
|
+
Pass exactly one of "collection" and "global". "id" is required with "collection" and must be omitted with "global", because a global is a singleton.
|
|
13
|
+
|
|
11
14
|
The write always lands as a draft and is never published, whatever it contains; publishing stays a human action in the admin panel.
|
|
12
15
|
|
|
13
|
-
Only the fields describeSchema lists can be addressed. A pointer that does not resolve is refused with the fields that are valid at that point, and nothing is applied unless every operation in the batch validates first.
|
|
16
|
+
Only the fields describeSchema lists can be addressed. A pointer that does not resolve is refused with the fields that are valid at that point, and nothing is applied unless every operation in the batch validates first. describeSchema reports field paths in this same pointer syntax; a path becomes a pointer into a document by replacing each "*" and each block slug with its 0-based index.
|
|
14
17
|
|
|
15
18
|
Adding a block requires "blockType" on the value. Append with "/-" as the last segment. To clear a field use "replace" with null; an array or blocks field refuses null and is emptied with [] instead. "remove" is only for list elements, because a field left out of a write is kept rather than cleared. Read the document first to learn the indices, and pass its "updatedAt" as expectedUpdatedAt so a concurrent edit is refused rather than overwritten.
|
|
16
19
|
|
|
@@ -48,10 +51,13 @@ const patchDocument = {
|
|
|
48
51
|
idempotentHint: false,
|
|
49
52
|
openWorldHint: false
|
|
50
53
|
},
|
|
51
|
-
isEnabled: (scope) => scope.writable.length > 0,
|
|
54
|
+
isEnabled: (scope) => scope.writable.length + scope.writableGlobals.length > 0,
|
|
52
55
|
inputSchema: (scope) => ({
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
...targetShape(scope, "write", {
|
|
57
|
+
collection: "Collection holding the document.",
|
|
58
|
+
global: "Global to patch."
|
|
59
|
+
}),
|
|
60
|
+
...idShape(scope, "write"),
|
|
55
61
|
...localeShape(scope, {
|
|
56
62
|
required: true,
|
|
57
63
|
description: "Locale the patch applies to. Localized fields write here only."
|
|
@@ -60,47 +66,55 @@ const patchDocument = {
|
|
|
60
66
|
expectedUpdatedAt: z.string().optional().describe("The updatedAt read before patching. The write is refused if the document has changed since.")
|
|
61
67
|
}),
|
|
62
68
|
handler: async (args, scope) => {
|
|
63
|
-
const
|
|
69
|
+
const target = resolveTarget(scope, args, "write");
|
|
70
|
+
const id = requireIdFor(target, args.id);
|
|
64
71
|
const { payload } = scope.req;
|
|
65
72
|
const locale = localeOf(scope, args.locale);
|
|
66
73
|
return await withTransaction(scope.req, async () => {
|
|
67
|
-
const doc = await
|
|
68
|
-
|
|
69
|
-
id
|
|
74
|
+
const doc = await readTarget(scope, {
|
|
75
|
+
target,
|
|
76
|
+
id,
|
|
70
77
|
locale
|
|
71
78
|
});
|
|
72
79
|
if (args.expectedUpdatedAt !== void 0 && !sameInstant(doc["updatedAt"], args.expectedUpdatedAt)) return errorResult("The document changed since you read it. Read it again and re-apply the patch.", { updatedAt: doc["updatedAt"] });
|
|
73
80
|
const problems = findPatchProblems(payload.config, {
|
|
74
|
-
collection: args.collection,
|
|
75
81
|
doc,
|
|
76
|
-
patches: args.patches
|
|
82
|
+
patches: args.patches,
|
|
83
|
+
ref: refOf(target)
|
|
77
84
|
});
|
|
78
85
|
if (problems.length > 0) return errorResult("No operation was applied.", { problems });
|
|
79
86
|
const applied = applyPatchToCopy(doc, args.patches);
|
|
80
87
|
if ("problems" in applied) return errorResult("No operation was applied.", { problems: applied.problems });
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
id: args.id,
|
|
84
|
-
data: buildWriteData(payload.config, collection, applied.next),
|
|
88
|
+
const write = {
|
|
89
|
+
data: buildWriteData(payload.config, target.config, applied.next),
|
|
85
90
|
depth: 0,
|
|
86
91
|
draft: true,
|
|
87
92
|
overrideAccess: false,
|
|
88
93
|
req: scope.req,
|
|
89
94
|
...locale === void 0 ? {} : { locale }
|
|
95
|
+
};
|
|
96
|
+
if (target.kind === "collection") await payload.update({
|
|
97
|
+
...write,
|
|
98
|
+
collection: target.slug,
|
|
99
|
+
id
|
|
100
|
+
});
|
|
101
|
+
else await payload.updateGlobal({
|
|
102
|
+
...write,
|
|
103
|
+
slug: target.slug
|
|
90
104
|
});
|
|
91
|
-
const saved = await
|
|
92
|
-
|
|
93
|
-
id
|
|
105
|
+
const saved = await readTarget(scope, {
|
|
106
|
+
target,
|
|
107
|
+
id,
|
|
94
108
|
locale,
|
|
95
109
|
privileged: true
|
|
96
110
|
});
|
|
97
111
|
const notApplied = notAppliedPointers(args.patches, applied.next, saved);
|
|
98
112
|
const publishBlockers = await collectPublishBlockers(scope.req, {
|
|
99
|
-
|
|
100
|
-
|
|
113
|
+
doc: saved,
|
|
114
|
+
entity: target
|
|
101
115
|
});
|
|
102
116
|
return jsonResult({
|
|
103
|
-
id: saved["id"],
|
|
117
|
+
...target.kind === "collection" ? { id: saved["id"] } : { global: target.slug },
|
|
104
118
|
status: saved["_status"],
|
|
105
119
|
updatedAt: saved["updatedAt"],
|
|
106
120
|
...publishBlockers.length > 0 ? { publishBlockers } : {},
|
package/dist/tools/shared.mjs
CHANGED
|
@@ -1,8 +1,39 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NotFound } from "payload";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
//#region src/tools/shared.ts
|
|
4
|
-
const
|
|
4
|
+
const slugEnum = (slugs) => z.enum(slugs);
|
|
5
5
|
const idSchema = z.union([z.string(), z.number()]).describe("Document id.");
|
|
6
|
+
const slugsFor = (scope, operation) => ({
|
|
7
|
+
collections: operation === "read" ? scope.readable : scope.writable,
|
|
8
|
+
globals: operation === "read" ? scope.readableGlobals : scope.writableGlobals
|
|
9
|
+
});
|
|
10
|
+
/**
|
|
11
|
+
* The `collection` and `global` arguments.
|
|
12
|
+
*
|
|
13
|
+
* When the key can reach no global, `global` is left out of the shape entirely
|
|
14
|
+
* and `collection` stays required, mirroring how {@link localeShape} omits
|
|
15
|
+
* `locale` when localization is off. A deployment without globals therefore
|
|
16
|
+
* sees exactly the schema it saw before. Only the mixed case makes either
|
|
17
|
+
* argument optional, and the handler enforces the exclusivity there.
|
|
18
|
+
*/ const targetShape = (scope, operation, descriptions) => {
|
|
19
|
+
const { collections, globals } = slugsFor(scope, operation);
|
|
20
|
+
if (globals.length === 0) return { collection: slugEnum(collections).describe(descriptions.collection) };
|
|
21
|
+
if (collections.length === 0) return { global: slugEnum(globals).describe(descriptions.global) };
|
|
22
|
+
return {
|
|
23
|
+
collection: slugEnum(collections).optional().describe(descriptions.collection),
|
|
24
|
+
global: slugEnum(globals).optional().describe(descriptions.global)
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The `id` argument, which only a collection document has. Omitted when the key
|
|
29
|
+
* can reach no collection, required when it can reach no global, and optional
|
|
30
|
+
* in between, where `requireIdFor` enforces the dependency.
|
|
31
|
+
*/ const idShape = (scope, operation) => {
|
|
32
|
+
const { collections, globals } = slugsFor(scope, operation);
|
|
33
|
+
if (collections.length === 0) return {};
|
|
34
|
+
if (globals.length === 0) return { id: idSchema };
|
|
35
|
+
return { id: idSchema.optional().describe("Document id. Required with \"collection\"; must be omitted with \"global\".") };
|
|
36
|
+
};
|
|
6
37
|
/**
|
|
7
38
|
* The `locale` argument, present only when localization is configured.
|
|
8
39
|
*/ const localeShape = (scope, options) => {
|
|
@@ -12,15 +43,6 @@ const idSchema = z.union([z.string(), z.number()]).describe("Document id.");
|
|
|
12
43
|
};
|
|
13
44
|
const depthShape = (scope) => ({ depth: z.number().int().min(0).max(scope.options.limits.maxDepth).optional().describe(`Relationship population depth. Default 0, at most ${String(scope.options.limits.maxDepth)}.`) });
|
|
14
45
|
/**
|
|
15
|
-
* Throws unless the key may perform `operation` on `slug`. The input schema
|
|
16
|
-
* already limits the enum, so this only guards against a stale tool list.
|
|
17
|
-
*/ const ensureAllowed = (scope, slug, operation) => {
|
|
18
|
-
const allowed = operation === "read" ? scope.readable : scope.writable;
|
|
19
|
-
const collection = scope.req.payload.collections[slug];
|
|
20
|
-
if (!allowed.includes(slug) || !collection) throw new Forbidden(scope.req.t);
|
|
21
|
-
return collection.config;
|
|
22
|
-
};
|
|
23
|
-
/**
|
|
24
46
|
* The locale to operate on: the explicit argument, else the request's, else
|
|
25
47
|
* the default. `undefined` when localization is off.
|
|
26
48
|
*/ const localeOf = (scope, locale) => {
|
|
@@ -31,23 +53,34 @@ const depthShape = (scope) => ({ depth: z.number().int().min(0).max(scope.option
|
|
|
31
53
|
/**
|
|
32
54
|
* Reads the current draft in a fixed locale with no fallback, which is the
|
|
33
55
|
* shape that may be written back or validated without mixing locales.
|
|
34
|
-
*/ const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
*/ const readTarget = async (scope, args) => {
|
|
57
|
+
const { payload } = scope.req;
|
|
58
|
+
const privileged = args.privileged === true;
|
|
59
|
+
const shared = {
|
|
38
60
|
depth: 0,
|
|
39
61
|
draft: true,
|
|
40
62
|
...args.locale === void 0 ? {} : {
|
|
41
63
|
locale: args.locale,
|
|
42
64
|
fallbackLocale: false
|
|
43
65
|
},
|
|
44
|
-
overrideAccess:
|
|
45
|
-
showHiddenFields:
|
|
46
|
-
disableErrors: true,
|
|
66
|
+
overrideAccess: privileged,
|
|
67
|
+
showHiddenFields: privileged,
|
|
47
68
|
req: scope.req
|
|
69
|
+
};
|
|
70
|
+
if (args.target.kind === "collection") {
|
|
71
|
+
const doc = await payload.findByID({
|
|
72
|
+
...shared,
|
|
73
|
+
collection: args.target.slug,
|
|
74
|
+
id: args.id,
|
|
75
|
+
disableErrors: true
|
|
76
|
+
});
|
|
77
|
+
if (!doc) throw new NotFound(scope.req.t);
|
|
78
|
+
return doc;
|
|
79
|
+
}
|
|
80
|
+
return await payload.findGlobal({
|
|
81
|
+
...shared,
|
|
82
|
+
slug: args.target.slug
|
|
48
83
|
});
|
|
49
|
-
if (!doc) throw new NotFound(scope.req.t);
|
|
50
|
-
return doc;
|
|
51
84
|
};
|
|
52
85
|
/**
|
|
53
86
|
* Resolves a collection label for the request's language.
|
|
@@ -62,4 +95,4 @@ const depthShape = (scope) => ({ depth: z.number().int().min(0).max(scope.option
|
|
|
62
95
|
return fallback;
|
|
63
96
|
};
|
|
64
97
|
//#endregion
|
|
65
|
-
export {
|
|
98
|
+
export { depthShape, idSchema, idShape, localeOf, localeShape, readTarget, slugEnum, targetShape, translateLabel };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { APIError, Forbidden } from "payload";
|
|
2
|
+
//#region src/tools/target.ts
|
|
3
|
+
const refOf = (target) => ({
|
|
4
|
+
kind: target.kind,
|
|
5
|
+
slug: target.slug
|
|
6
|
+
});
|
|
7
|
+
/**
|
|
8
|
+
* Resolves the `collection`/`global` arguments to one entity and checks the key
|
|
9
|
+
* may perform `operation` on it.
|
|
10
|
+
*
|
|
11
|
+
* A tool's `inputSchema` returns a raw shape, which leaves no top-level
|
|
12
|
+
* `.refine` to express "exactly one of collection and global". The rule is
|
|
13
|
+
* enforced here instead, with a message naming the offending arguments so one
|
|
14
|
+
* failed call teaches it.
|
|
15
|
+
*/ const resolveTarget = (scope, args, operation) => {
|
|
16
|
+
const { collection, global } = args;
|
|
17
|
+
if (collection !== void 0 && global !== void 0) throw new APIError("Pass either \"collection\" or \"global\", not both.", 400);
|
|
18
|
+
if (collection === void 0 && global === void 0) throw new APIError("One of \"collection\" or \"global\" is required. Call listCapabilities to see which slugs are available.", 400);
|
|
19
|
+
if (collection !== void 0) {
|
|
20
|
+
const allowed = operation === "read" ? scope.readable : scope.writable;
|
|
21
|
+
const found = scope.req.payload.collections[collection];
|
|
22
|
+
if (!allowed.includes(collection) || !found) throw new Forbidden(scope.req.t);
|
|
23
|
+
return {
|
|
24
|
+
kind: "collection",
|
|
25
|
+
slug: collection,
|
|
26
|
+
config: found.config
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const slug = global;
|
|
30
|
+
const allowed = operation === "read" ? scope.readableGlobals : scope.writableGlobals;
|
|
31
|
+
const found = scope.req.payload.globals.config.find((candidate) => candidate.slug === slug);
|
|
32
|
+
if (!allowed.includes(slug) || !found) throw new Forbidden(scope.req.t);
|
|
33
|
+
return {
|
|
34
|
+
kind: "global",
|
|
35
|
+
slug,
|
|
36
|
+
config: found
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Checks `id` against the resolved target. A collection document needs one; a
|
|
41
|
+
* global is a singleton and must not carry one. The schema cannot express the
|
|
42
|
+
* dependency, so it is stated here and in every affected tool description.
|
|
43
|
+
*/ const requireIdFor = (target, id) => {
|
|
44
|
+
if (target.kind === "collection" && id === void 0) throw new APIError(`"id" is required when "collection" is "${target.slug}".`, 400);
|
|
45
|
+
if (target.kind === "global" && id !== void 0) throw new APIError(`"id" must be omitted when "global" is "${target.slug}"; a global is a singleton.`, 400);
|
|
46
|
+
return target.kind === "collection" ? id : void 0;
|
|
47
|
+
};
|
|
48
|
+
//#endregion
|
|
49
|
+
export { refOf, requireIdFor, resolveTarget };
|