@abinnovision/payloadcms-mcpx 1.0.0-beta.4 → 1.0.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -17
- package/dist/api-keys/fields.mjs +27 -12
- package/dist/capabilities.mjs +16 -3
- package/dist/endpoint/handler.mjs +3 -1
- 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 +38 -0
- package/dist/plugin.d.mts +1 -1
- package/dist/plugin.mjs +3 -2
- package/dist/schema/describe.mjs +109 -28
- package/dist/schema/lexical.mjs +50 -2
- package/dist/schema/pointer.mjs +2 -2
- package/dist/schema/shape.mjs +67 -14
- package/dist/schema/walk.d.mts +1 -0
- package/dist/schema/walk.mjs +15 -4
- package/dist/tools/create-document.mjs +9 -8
- package/dist/tools/describe-schema.mjs +14 -6
- package/dist/tools/find-documents.mjs +4 -3
- package/dist/tools/get-document.mjs +24 -11
- package/dist/tools/list-capabilities.mjs +19 -1
- package/dist/tools/patch-document.mjs +34 -20
- 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/write/draft-guard.mjs +51 -8
- package/dist/write/patch.mjs +4 -4
- package/dist/write/publish-blockers.d.mts +1 -0
- package/dist/write/publish-blockers.mjs +8 -7
- package/package.json +1 -1
package/dist/schema/pointer.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { blockOf, blockSlugsOf,
|
|
1
|
+
import { blockOf, blockSlugsOf, describeFields, findBlocksField, joinPath, splitPath, targetOf } from "./walk.mjs";
|
|
2
2
|
//#region src/schema/pointer.ts
|
|
3
3
|
const isIndexSegment = (segment) => segment === "-" || /^\d+$/.test(segment);
|
|
4
4
|
const partMatches = (part, segment) => segment !== void 0 && (part === "*" ? isIndexSegment(segment) : part === segment);
|
|
@@ -32,7 +32,7 @@ const partMatches = (part, segment) => segment !== void 0 && (part === "*" ? isI
|
|
|
32
32
|
* can only be resolved by reading `blockType` off `sections[3]`, since a blocks
|
|
33
33
|
* field admits many shapes at the same index.
|
|
34
34
|
*/ const resolveDataPointer = (config, target) => {
|
|
35
|
-
let fields =
|
|
35
|
+
let fields = targetOf(config, target.ref).flattenedFields;
|
|
36
36
|
let data = target.doc;
|
|
37
37
|
let blockType;
|
|
38
38
|
let segments = splitPath(target.pointer);
|
package/dist/schema/shape.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { lexicalSubSchema } from "./lexical.mjs";
|
|
2
|
+
import { blockOf, blockSlugsOf, describeFields, findBlocksField, findRichTextField, splitPath } from "./walk.mjs";
|
|
2
3
|
//#region src/schema/shape.ts
|
|
3
4
|
/**
|
|
4
5
|
* Keys Payload manages on a row that a client may echo back harmlessly.
|
|
@@ -9,30 +10,79 @@ import { blockOf, blockSlugsOf, describeFields, findBlocksField, splitPath } fro
|
|
|
9
10
|
]);
|
|
10
11
|
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
12
|
/**
|
|
12
|
-
* Checks
|
|
13
|
-
*
|
|
13
|
+
* Checks the fields a single Lexical node carries against the schema its
|
|
14
|
+
* feature declares for that node type.
|
|
15
|
+
*
|
|
16
|
+
* A node with nothing to declare, and one whose sub-fields cannot be named at
|
|
17
|
+
* a position, are both left alone.
|
|
18
|
+
*/ const checkNodeFields = (scope, field, node) => {
|
|
19
|
+
const sub = lexicalSubSchema(field, node.type);
|
|
20
|
+
if (!sub) return;
|
|
21
|
+
const data = node.fields;
|
|
22
|
+
if (!isPlainObject(data)) {
|
|
23
|
+
scope.problems.push(`${scope.pointer}: a "${node.type}" node carries a "fields" object.`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const nested = {
|
|
27
|
+
...scope,
|
|
28
|
+
pointer: `${scope.pointer}/fields`,
|
|
29
|
+
prefix: []
|
|
30
|
+
};
|
|
31
|
+
if (sub.kind === "fields") {
|
|
32
|
+
checkValue({
|
|
33
|
+
...nested,
|
|
34
|
+
fields: sub.fields
|
|
35
|
+
}, data);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const slug = data["blockType"];
|
|
39
|
+
const block = typeof slug === "string" ? blockOf(scope.config, sub.blocksField, slug) : void 0;
|
|
40
|
+
if (!block) {
|
|
41
|
+
scope.problems.push(`${scope.pointer}/fields: "${String(slug)}" is not allowed here. Allowed: ${blockSlugsOf(sub.blocksField).join(", ")}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
checkValue({
|
|
45
|
+
...nested,
|
|
46
|
+
fields: block.flattenedFields
|
|
47
|
+
}, data);
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Checks an editor state against what the field's editor can actually
|
|
51
|
+
* produce: every node type, and the fields each node carries.
|
|
14
52
|
*
|
|
15
53
|
* Payload does not: the Lexical validator runs node validations only for the
|
|
16
54
|
* few node types that register one, so a `heading` inside a field whose
|
|
17
55
|
* editor has no heading feature is stored without complaint and only fails
|
|
18
|
-
* later, at render or when the document is reopened in the admin editor.
|
|
19
|
-
|
|
56
|
+
* later, at render or when the document is reopened in the admin editor. A key
|
|
57
|
+
* a node's fields do not declare is dropped just as silently.
|
|
58
|
+
*/ const checkRichText = (scope, editor, value) => {
|
|
20
59
|
if (!isPlainObject(value) || !isPlainObject(value["root"])) {
|
|
21
60
|
scope.problems.push(`${scope.pointer}: expected a Lexical editor state with a "root".`);
|
|
22
61
|
return;
|
|
23
62
|
}
|
|
24
|
-
const walk = (nodes) => {
|
|
63
|
+
const walk = (nodes, pointer) => {
|
|
25
64
|
if (!Array.isArray(nodes)) return;
|
|
26
|
-
|
|
65
|
+
nodes.forEach((node, index) => {
|
|
66
|
+
const at = `${pointer}/${String(index)}`;
|
|
27
67
|
if (!isPlainObject(node) || typeof node["type"] !== "string") {
|
|
28
|
-
scope.problems.push(`${
|
|
29
|
-
|
|
68
|
+
scope.problems.push(`${at}: every node needs a "type".`);
|
|
69
|
+
return;
|
|
30
70
|
}
|
|
31
|
-
if (!allowed.includes(node["type"]))
|
|
32
|
-
|
|
33
|
-
|
|
71
|
+
if (!editor.allowed.includes(node["type"])) {
|
|
72
|
+
scope.problems.push(`${at}: "${node["type"]}" is not available in this field's editor. Allowed: ${editor.allowed.join(", ")}`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (editor.field) checkNodeFields({
|
|
76
|
+
...scope,
|
|
77
|
+
pointer: at
|
|
78
|
+
}, editor.field, {
|
|
79
|
+
fields: node["fields"],
|
|
80
|
+
type: node["type"]
|
|
81
|
+
});
|
|
82
|
+
walk(node["children"], `${at}/children`);
|
|
83
|
+
});
|
|
34
84
|
};
|
|
35
|
-
walk(value["root"]["children"]);
|
|
85
|
+
walk(value["root"]["children"], `${scope.pointer}/root/children`);
|
|
36
86
|
};
|
|
37
87
|
const checkLeafValue = (scope, descriptor, value) => {
|
|
38
88
|
if (descriptor.readOnly) {
|
|
@@ -40,7 +90,10 @@ const checkLeafValue = (scope, descriptor, value) => {
|
|
|
40
90
|
return;
|
|
41
91
|
}
|
|
42
92
|
if (descriptor.type === "richText") {
|
|
43
|
-
checkRichText(scope,
|
|
93
|
+
checkRichText(scope, {
|
|
94
|
+
allowed: descriptor.nodes ?? [],
|
|
95
|
+
field: findRichTextField(scope.fields, splitPath(descriptor.path))
|
|
96
|
+
}, value);
|
|
44
97
|
return;
|
|
45
98
|
}
|
|
46
99
|
if (descriptor.type !== "blocks") return;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "payload";
|
package/dist/schema/walk.mjs
CHANGED
|
@@ -109,10 +109,21 @@ const withRows = (descriptor, field) => ({
|
|
|
109
109
|
if (field.type === "array" && path[1] === "*") return findBlocksField(field.flattenedFields, path.slice(2));
|
|
110
110
|
}
|
|
111
111
|
};
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Locates the rich text field that a resolved descriptor path refers to, so
|
|
114
|
+
* its editor can be introspected for the fields its nodes carry.
|
|
115
|
+
*/ const findRichTextField = (fields, path) => {
|
|
116
|
+
for (const field of fields) {
|
|
117
|
+
if (!("name" in field) || field.name !== path[0]) continue;
|
|
118
|
+
if (field.type === "richText" && path.length === 1) return field;
|
|
119
|
+
if (field.type === "tab" || field.type === "group") return findRichTextField(field.flattenedFields, path.slice(1));
|
|
120
|
+
if (field.type === "array" && path[1] === "*") return findRichTextField(field.flattenedFields, path.slice(2));
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const targetOf = (config, ref) => {
|
|
124
|
+
const found = ref.kind === "collection" ? config.collections.find((candidate) => candidate.slug === ref.slug) : config.globals.find((candidate) => candidate.slug === ref.slug);
|
|
125
|
+
if (!found) throw new Error(`Unknown ${ref.kind} "${ref.slug}".`);
|
|
115
126
|
return found;
|
|
116
127
|
};
|
|
117
128
|
//#endregion
|
|
118
|
-
export { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, blockSlugsOf,
|
|
129
|
+
export { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, blockSlugsOf, describeFields, findBlocksField, findRichTextField, 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,14 +25,14 @@ 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
|
+
fields: target.config.flattenedFields,
|
|
35
36
|
prefix: []
|
|
36
37
|
}
|
|
37
38
|
}, seed);
|
|
@@ -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,8 +8,12 @@ const describeSchema = {
|
|
|
7
8
|
name: "describeSchema",
|
|
8
9
|
description: `Describes the writable shape of a document, one node at a time.
|
|
9
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.
|
|
12
|
+
|
|
10
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.
|
|
11
14
|
|
|
15
|
+
A "richText" field stops there too. It lists the Lexical node types it accepts in "nodes", and "next" carries a path for every node type that holds fields of its own: "/content/link" for a link node, "/content/block/callout" and "/content/inlineBlock/badge" for the block nodes. Descend to get the real field list instead of guessing what a node carries. Upload nodes are not addressable, because their fields depend on the collection the node points at.
|
|
16
|
+
|
|
12
17
|
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
18
|
|
|
14
19
|
Fields Payload maintains (id, _status, createdAt, updatedAt) are never listed and cannot be written. Fields marked readOnly are listed but refused on write.`,
|
|
@@ -16,19 +21,22 @@ Fields Payload maintains (id, _status, createdAt, updatedAt) are never listed an
|
|
|
16
21
|
readOnlyHint: true,
|
|
17
22
|
openWorldHint: false
|
|
18
23
|
},
|
|
19
|
-
isEnabled: (scope) => scope.readable.length > 0,
|
|
24
|
+
isEnabled: (scope) => scope.readable.length + scope.readableGlobals.length > 0,
|
|
20
25
|
inputSchema: (scope) => ({
|
|
21
|
-
|
|
26
|
+
...targetShape(scope, "read", {
|
|
27
|
+
collection: "Collection to describe.",
|
|
28
|
+
global: "Global to describe."
|
|
29
|
+
}),
|
|
22
30
|
paths: z.array(z.string()).optional().describe("Schema paths to describe, e.g. \"/layout/sections/sectionWrapper\". Omit for the collection root."),
|
|
23
31
|
expand: z.boolean().optional().describe("Return every node reachable from the root in one response. Ignores paths.")
|
|
24
32
|
}),
|
|
25
33
|
handler: (args, scope) => {
|
|
26
|
-
|
|
34
|
+
const ref = refOf(resolveTarget(scope, args, "read"));
|
|
27
35
|
const { config } = scope.req.payload;
|
|
28
|
-
const expanded = args.expand === true ? reachableSchemaPaths(config,
|
|
36
|
+
const expanded = args.expand === true ? reachableSchemaPaths(config, ref) : void 0;
|
|
29
37
|
const nodes = (expanded?.paths ?? (args.paths && args.paths.length > 0 ? args.paths : [""])).map((schemaPath) => {
|
|
30
38
|
try {
|
|
31
|
-
return describeNode(config,
|
|
39
|
+
return describeNode(config, ref, schemaPath);
|
|
32
40
|
} catch (error) {
|
|
33
41
|
return {
|
|
34
42
|
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,26 @@
|
|
|
1
1
|
import { JSON_POINTER_PATTERN } from "../schema/walk.mjs";
|
|
2
2
|
import { errorResult, jsonResult } from "../endpoint/result.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { depthShape, idShape, localeOf, localeShape, targetShape } from "./shared.mjs";
|
|
4
|
+
import { requireIdFor, resolveTarget } from "./target.mjs";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
import { Pointer } from "rfc6902";
|
|
6
7
|
//#region src/tools/get-document.ts
|
|
7
8
|
const getDocument = {
|
|
8
9
|
name: "getDocument",
|
|
9
|
-
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.`,
|
|
10
13
|
annotations: {
|
|
11
14
|
readOnlyHint: true,
|
|
12
15
|
openWorldHint: false
|
|
13
16
|
},
|
|
14
|
-
isEnabled: (scope) => scope.readable.length > 0,
|
|
17
|
+
isEnabled: (scope) => scope.readable.length + scope.readableGlobals.length > 0,
|
|
15
18
|
inputSchema: (scope) => ({
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
...targetShape(scope, "read", {
|
|
20
|
+
collection: "Collection holding the document.",
|
|
21
|
+
global: "Global to read."
|
|
22
|
+
}),
|
|
23
|
+
...idShape(scope, "read"),
|
|
18
24
|
path: z.string().regex(JSON_POINTER_PATTERN).optional().describe("JSON pointer to return only a subtree, e.g. \"/layout/sections/0\"."),
|
|
19
25
|
...depthShape(scope),
|
|
20
26
|
...localeShape(scope, {
|
|
@@ -24,17 +30,24 @@ const getDocument = {
|
|
|
24
30
|
draft: z.boolean().optional().describe("Return the latest draft. Default true.")
|
|
25
31
|
}),
|
|
26
32
|
handler: async (args, scope) => {
|
|
27
|
-
|
|
33
|
+
const target = resolveTarget(scope, args, "read");
|
|
34
|
+
const id = requireIdFor(target, args.id);
|
|
28
35
|
const locale = localeOf(scope, args.locale);
|
|
29
|
-
const
|
|
30
|
-
collection: args.collection,
|
|
31
|
-
id: args.id,
|
|
36
|
+
const shared = {
|
|
32
37
|
depth: args.depth ?? 0,
|
|
33
38
|
draft: args.draft ?? true,
|
|
34
39
|
overrideAccess: false,
|
|
35
40
|
req: scope.req,
|
|
36
41
|
...locale === void 0 ? {} : { locale }
|
|
37
|
-
}
|
|
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
|
+
}));
|
|
38
51
|
if (args.path === void 0 || args.path === "") return jsonResult(doc);
|
|
39
52
|
let value;
|
|
40
53
|
try {
|
|
@@ -43,7 +56,7 @@ const getDocument = {
|
|
|
43
56
|
return errorResult(`"${args.path}" is not a valid JSON pointer.`);
|
|
44
57
|
}
|
|
45
58
|
return jsonResult({
|
|
46
|
-
id: doc["id"],
|
|
59
|
+
...target.kind === "collection" ? { id: doc["id"] } : { global: target.slug },
|
|
47
60
|
status: doc["_status"],
|
|
48
61
|
updatedAt: doc["updatedAt"],
|
|
49
62
|
path: args.path,
|
|
@@ -5,7 +5,9 @@ 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,6 +9,8 @@ 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
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.
|
|
@@ -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 };
|