@abinnovision/payloadcms-mcpx 1.0.0-beta.10

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.
Files changed (53) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +353 -0
  3. package/dist/api-keys/collection.mjs +58 -0
  4. package/dist/api-keys/fields.mjs +137 -0
  5. package/dist/api-keys/key.mjs +10 -0
  6. package/dist/api-keys/setup-guide.mjs +54 -0
  7. package/dist/auth/resolve.mjs +62 -0
  8. package/dist/capabilities.mjs +43 -0
  9. package/dist/client/index.d.mts +2 -0
  10. package/dist/client/index.mjs +2 -0
  11. package/dist/client/setup-guide.d.mts +14 -0
  12. package/dist/client/setup-guide.mjs +87 -0
  13. package/dist/endpoint/handler.mjs +86 -0
  14. package/dist/endpoint/result.mjs +65 -0
  15. package/dist/endpoint/server.mjs +52 -0
  16. package/dist/i18n.d.mts +1 -0
  17. package/dist/i18n.mjs +40 -0
  18. package/dist/index.d.mts +5 -0
  19. package/dist/index.mjs +4 -0
  20. package/dist/options.d.mts +2 -0
  21. package/dist/options.mjs +146 -0
  22. package/dist/plugin.d.mts +9 -0
  23. package/dist/plugin.mjs +43 -0
  24. package/dist/schema/describe.mjs +160 -0
  25. package/dist/schema/lexical.d.mts +1 -0
  26. package/dist/schema/lexical.mjs +117 -0
  27. package/dist/schema/pointer.mjs +80 -0
  28. package/dist/schema/shape.mjs +208 -0
  29. package/dist/schema/walk.d.mts +3 -0
  30. package/dist/schema/walk.mjs +165 -0
  31. package/dist/tools/create-document.mjs +68 -0
  32. package/dist/tools/describe-schema.mjs +54 -0
  33. package/dist/tools/find-documents.mjs +55 -0
  34. package/dist/tools/get-document.mjs +68 -0
  35. package/dist/tools/index.mjs +23 -0
  36. package/dist/tools/list-capabilities.mjs +68 -0
  37. package/dist/tools/names.mjs +14 -0
  38. package/dist/tools/patch-document.mjs +127 -0
  39. package/dist/tools/shared.mjs +97 -0
  40. package/dist/tools/target.d.mts +3 -0
  41. package/dist/tools/target.mjs +49 -0
  42. package/dist/tools/types.d.mts +5 -0
  43. package/dist/tools/validate-document.mjs +55 -0
  44. package/dist/types.d.mts +143 -0
  45. package/dist/types.mjs +7 -0
  46. package/dist/version.mjs +6 -0
  47. package/dist/write/draft-guard.d.mts +10 -0
  48. package/dist/write/draft-guard.mjs +113 -0
  49. package/dist/write/patch.mjs +219 -0
  50. package/dist/write/publish-blockers.d.mts +15 -0
  51. package/dist/write/publish-blockers.mjs +74 -0
  52. package/dist/write/transaction.mjs +19 -0
  53. package/package.json +104 -0
@@ -0,0 +1,208 @@
1
+ import { lexicalSubSchema } from "./lexical.mjs";
2
+ import { blockOf, blockSlugsOf, describeAddressableFields, findBlocksField, findRichTextField, splitPath } from "./walk.mjs";
3
+ //#region src/schema/shape.ts
4
+ /**
5
+ * Keys Payload manages on a row that a client may echo back harmlessly.
6
+ */ const TOLERATED_VALUE_KEYS = /* @__PURE__ */ new Set([
7
+ "blockName",
8
+ "blockType",
9
+ "id"
10
+ ]);
11
+ const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
12
+ /**
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.
52
+ *
53
+ * Payload does not: the Lexical validator runs node validations only for the
54
+ * few node types that register one, so a `heading` inside a field whose
55
+ * editor has no heading feature is stored without complaint and only fails
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. The same holds
58
+ * one level down, for the node properties a feature narrows: an `h3` in an
59
+ * editor restricted to `h4` is stored as readily as an `h4`.
60
+ */ const checkRichText = (scope, editor, value) => {
61
+ if (!isPlainObject(value) || !isPlainObject(value["root"])) {
62
+ scope.problems.push(`${scope.pointer}: expected a Lexical editor state with a "root".`);
63
+ return;
64
+ }
65
+ const walk = (nodes, pointer) => {
66
+ if (!Array.isArray(nodes)) return;
67
+ nodes.forEach((node, index) => {
68
+ const at = `${pointer}/${String(index)}`;
69
+ if (!isPlainObject(node) || typeof node["type"] !== "string") {
70
+ scope.problems.push(`${at}: every node needs a "type".`);
71
+ return;
72
+ }
73
+ if (!editor.allowed.includes(node["type"])) {
74
+ scope.problems.push(`${at}: "${node["type"]}" is not available in this field's editor. Allowed: ${editor.allowed.join(", ")}`);
75
+ return;
76
+ }
77
+ for (const [property, values] of Object.entries(editor.nodeOptions?.[node["type"]] ?? {})) {
78
+ const value = node[property];
79
+ if (typeof value === "string" && !values.includes(value)) scope.problems.push(`${at}/${property}: "${value}" is not available for a "${node["type"]}" node in this field's editor. Allowed: ${values.join(", ")}`);
80
+ }
81
+ if (editor.field) checkNodeFields({
82
+ ...scope,
83
+ pointer: at
84
+ }, editor.field, {
85
+ fields: node["fields"],
86
+ type: node["type"]
87
+ });
88
+ walk(node["children"], `${at}/children`);
89
+ });
90
+ };
91
+ walk(value["root"]["children"], `${scope.pointer}/root/children`);
92
+ };
93
+ const checkLeafValue = (scope, descriptor, value) => {
94
+ if (descriptor.readOnly) {
95
+ scope.problems.push(`${scope.pointer}: this field is read-only and cannot be written.`);
96
+ return;
97
+ }
98
+ if (descriptor.type === "richText") {
99
+ checkRichText(scope, {
100
+ allowed: descriptor.nodes ?? [],
101
+ field: findRichTextField(scope.fields, splitPath(descriptor.path)),
102
+ nodeOptions: descriptor.nodeOptions
103
+ }, value);
104
+ return;
105
+ }
106
+ if (descriptor.type !== "blocks") return;
107
+ if (!Array.isArray(value)) {
108
+ scope.problems.push(`${scope.pointer}: expected an array of blocks.`);
109
+ return;
110
+ }
111
+ const field = findBlocksField(scope.fields, splitPath(descriptor.path));
112
+ if (!field) return;
113
+ value.forEach((row, index) => {
114
+ const slug = isPlainObject(row) ? row["blockType"] : void 0;
115
+ const block = typeof slug === "string" ? blockOf(scope.config, field, slug) : void 0;
116
+ if (!block) {
117
+ scope.problems.push(`${scope.pointer}/${String(index)}: "${String(slug)}" is not allowed here. Allowed: ${blockSlugsOf(field).join(", ")}`);
118
+ return;
119
+ }
120
+ checkValue({
121
+ ...scope,
122
+ fields: block.flattenedFields,
123
+ pointer: `${scope.pointer}/${String(index)}`,
124
+ prefix: []
125
+ }, row);
126
+ });
127
+ };
128
+ /**
129
+ * Walks an incoming value against the schema, reporting every shape problem
130
+ * rather than the first.
131
+ *
132
+ * Shape only: unknown field names, unknown block slugs, read-only fields, and
133
+ * rich text nodes or node properties the field's editor cannot produce.
134
+ * Required-ness, row counts, lengths, enum membership and relationship
135
+ * existence stay with Payload, which already checks them and reports them per
136
+ * field. Without this pass a misspelled field inside a new block would be
137
+ * stripped in silence.
138
+ */ const checkValue = (scope, value) => {
139
+ if (!isPlainObject(value)) return;
140
+ const prefixParts = scope.prefix;
141
+ const relative = describeAddressableFields(scope.fields).flatMap((descriptor) => {
142
+ const parts = splitPath(descriptor.path);
143
+ return prefixParts.every((part, offset) => part === parts[offset]) ? [{
144
+ descriptor,
145
+ parts: parts.slice(prefixParts.length)
146
+ }] : [];
147
+ });
148
+ for (const [key, entry] of Object.entries(value)) {
149
+ if (TOLERATED_VALUE_KEYS.has(key)) continue;
150
+ const candidates = relative.filter(({ parts }) => parts[0] === key);
151
+ const pointer = `${scope.pointer}/${key}`;
152
+ if (candidates.length === 0) {
153
+ scope.problems.push(`${pointer}: no such field. Available: ${[...new Set(relative.map(({ parts }) => parts[0]))].join(", ")}`);
154
+ continue;
155
+ }
156
+ const exact = candidates.find(({ parts }) => parts.length === 1);
157
+ if (exact) {
158
+ checkLeafValue({
159
+ ...scope,
160
+ pointer
161
+ }, exact.descriptor, entry);
162
+ continue;
163
+ }
164
+ if (candidates.some(({ parts }) => parts[1] === "*")) {
165
+ if (!Array.isArray(entry)) {
166
+ scope.problems.push(`${pointer}: expected an array.`);
167
+ continue;
168
+ }
169
+ entry.forEach((row, index) => {
170
+ checkValue({
171
+ ...scope,
172
+ pointer: `${pointer}/${String(index)}`,
173
+ prefix: [
174
+ ...prefixParts,
175
+ key,
176
+ "*"
177
+ ]
178
+ }, row);
179
+ });
180
+ continue;
181
+ }
182
+ checkValue({
183
+ ...scope,
184
+ pointer,
185
+ prefix: [...prefixParts, key]
186
+ }, entry);
187
+ }
188
+ };
189
+ /**
190
+ * Shape problems with a value about to be written at a resolved pointer.
191
+ */ const validateWriteValue = (config, target, value) => {
192
+ const problems = [];
193
+ const scope = {
194
+ config,
195
+ fields: target.resolution.fields,
196
+ pointer: target.pointer,
197
+ prefix: target.resolution.prefix,
198
+ problems
199
+ };
200
+ if (target.resolution.descriptor) {
201
+ checkLeafValue(scope, target.resolution.descriptor, value);
202
+ return problems;
203
+ }
204
+ checkValue(scope, value);
205
+ return problems;
206
+ };
207
+ //#endregion
208
+ export { validateWriteValue };
@@ -0,0 +1,3 @@
1
+ import "./lexical.mjs";
2
+ import "../i18n.mjs";
3
+ import "payload";
@@ -0,0 +1,165 @@
1
+ import { allowedNodeTypes, nodeOptions } from "./lexical.mjs";
2
+ import { translateAny } from "../i18n.mjs";
3
+ import { fieldIsHiddenOrDisabled, fieldIsVirtual } from "payload/shared";
4
+ //#region src/schema/walk.ts
5
+ /**
6
+ * Fields Payload maintains, which a client may neither address nor supply.
7
+ */ const RESERVED_FIELD_NAMES = /* @__PURE__ */ new Set([
8
+ "_status",
9
+ "createdAt",
10
+ "deletedAt",
11
+ "id",
12
+ "updatedAt"
13
+ ]);
14
+ /**
15
+ * Shape a JSON Pointer must have to be parseable at all.
16
+ */ const JSON_POINTER_PATTERN = /^(\/([^~/]|~[01])*)*$/;
17
+ /**
18
+ * Joins segments into a JSON Pointer, so the segments `items`, `*`, `title`
19
+ * read as one path to a subfield of every element of `items`. No segments is
20
+ * the root pointer, `""`.
21
+ */ const joinPath = (parts) => parts.map((part) => `/${part.replace(/~/g, "~0").replace(/\//g, "~1")}`).join("");
22
+ /**
23
+ * Splits a JSON Pointer into its segments, unescaping `~1` and `~0`. The root
24
+ * pointer yields no segments.
25
+ */ const splitPath = (path) => path.split("/").slice(1).map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
26
+ /**
27
+ * Restates a path Payload reports on a validation error (`layout.0.title`) as
28
+ * a JSON Pointer, so everything this plugin hands back addresses documents the
29
+ * same way. Payload's path already carries real indices, so it maps directly.
30
+ */ const pointerFromPayloadPath = (path) => path ? joinPath(path.split(".")) : "";
31
+ /**
32
+ * Blocks a blocks field accepts, by slug. On a flattened field, whichever of
33
+ * `blockReferences` and `blocks` was declared carries the definitions.
34
+ */ const blockSlugsOf = (field) => [...new Set((field.blockReferences ?? field.blocks).map((block) => typeof block === "string" ? block : block.slug))];
35
+ /**
36
+ * Resolves one of a blocks field's slugs to its definition.
37
+ *
38
+ * A definition inlined on the field wins over the shared registry. A block's
39
+ * own fields are identical wherever it appears, but the blocks its children
40
+ * accept are not, so an inline definition has to be read at its position.
41
+ * The registry (`config.blocks`) is the fallback for slugs referenced by name.
42
+ */ const blockOf = (config, field, slug) => {
43
+ const declared = field.blockReferences ?? field.blocks;
44
+ const inline = declared.find((block) => typeof block !== "string" && block.slug === slug);
45
+ if (inline) return inline;
46
+ return declared.includes(slug) ? config.blocks?.find((block) => block.slug === slug) : void 0;
47
+ };
48
+ const isSkipped = (field) => !("name" in field) || field.type === "join" || RESERVED_FIELD_NAMES.has(field.name) || fieldIsVirtual(field) || fieldIsHiddenOrDisabled(field);
49
+ const isReadOnly = (field) => "admin" in field && field.admin.readOnly === true;
50
+ const describeBase = (field, { path, readOnly, translate }) => {
51
+ const description = translate("admin" in field ? field.admin.description : void 0);
52
+ return {
53
+ path,
54
+ type: field.type,
55
+ ...description === void 0 ? {} : { description },
56
+ ..."required" in field && field.required ? { required: true } : {},
57
+ ..."localized" in field && field.localized ? { localized: true } : {},
58
+ ...readOnly ? { readOnly: true } : {}
59
+ };
60
+ };
61
+ const describeLeaf = (field, at) => {
62
+ const descriptor = describeBase(field, at);
63
+ if (field.type === "select" || field.type === "radio") descriptor.options = field.options.map((option) => typeof option === "string" ? option : option.value);
64
+ if (field.type === "relationship" || field.type === "upload") descriptor.relationTo = field.relationTo;
65
+ if ((field.type === "select" || field.type === "relationship" || field.type === "upload") && field.hasMany === true) descriptor.hasMany = true;
66
+ if ((field.type === "text" || field.type === "textarea") && field.maxLength !== void 0) descriptor.maxLength = field.maxLength;
67
+ if ((field.type === "text" || field.type === "textarea") && field.minLength !== void 0) descriptor.minLength = field.minLength;
68
+ if (field.type === "number" && field.max !== void 0) descriptor.max = field.max;
69
+ if (field.type === "number" && field.min !== void 0) descriptor.min = field.min;
70
+ if (field.type === "richText") {
71
+ descriptor.nodes = allowedNodeTypes(field);
72
+ const options = nodeOptions(field, descriptor.nodes);
73
+ if (options) descriptor.nodeOptions = options;
74
+ }
75
+ return descriptor;
76
+ };
77
+ const withRows = (descriptor, field) => ({
78
+ ...descriptor,
79
+ ...field.minRows === void 0 ? {} : { minRows: field.minRows },
80
+ ...field.maxRows === void 0 ? {} : { maxRows: field.maxRows }
81
+ });
82
+ /**
83
+ * Whether a descriptor stands for a construct that only holds other fields.
84
+ *
85
+ * These describe a position rather than a value, so everything that resolves a
86
+ * path to something writable skips them; only {@link describeNode} reports
87
+ * them, to carry what the container itself declares.
88
+ */ const isContainer = (descriptor) => descriptor.type === "array" || descriptor.type === "group" || descriptor.type === "tab";
89
+ /**
90
+ * Whether a container declares anything a client could not infer from the
91
+ * fields beneath it. A group that exists only to nest is not worth reporting.
92
+ */ const isInformative = (descriptor) => descriptor.description !== void 0 || descriptor.required === true || descriptor.localized === true;
93
+ /**
94
+ * Flattens a field list into descriptors addressed relative to the node.
95
+ *
96
+ * The input is Payload's own flattened shape, which has already merged every
97
+ * construct that exists only in the admin UI (unnamed tabs, unnamed groups,
98
+ * `row`, `collapsible`) and dropped `ui` fields. Named tabs, groups and
99
+ * arrays contribute a path segment, and are described in their own right when
100
+ * they declare something of their own: an array always, since its row counts
101
+ * live nowhere else, a group or tab only when it carries a description or a
102
+ * constraint. The walk stops at every blocks field and names the slugs instead
103
+ * of descending, which keeps a node proportional to the number of blocks it
104
+ * allows rather than to the size of their definitions.
105
+ *
106
+ * `translate` resolves each `admin.description` to the request's language.
107
+ * Callers that walk for paths alone leave it out and get the language-agnostic
108
+ * default, so a missing argument costs language selection, never the
109
+ * description itself.
110
+ */ const describeFields = (fields, translate = translateAny) => {
111
+ const walk = (current, prefix, parentReadOnly) => current.flatMap((field) => {
112
+ if (isSkipped(field)) return [];
113
+ const readOnly = parentReadOnly || isReadOnly(field);
114
+ const path = [...prefix, field.name];
115
+ const at = {
116
+ path: joinPath(path),
117
+ readOnly,
118
+ translate
119
+ };
120
+ if (field.type === "tab" || field.type === "group") {
121
+ const own = describeBase(field, at);
122
+ return [...isInformative(own) ? [own] : [], ...walk(field.flattenedFields, path, readOnly)];
123
+ }
124
+ if (field.type === "array") return [withRows(describeBase(field, at), field), ...walk(field.flattenedFields, [...path, "*"], readOnly)];
125
+ if (field.type === "blocks") return [withRows({
126
+ ...describeBase(field, at),
127
+ blocks: blockSlugsOf(field)
128
+ }, field)];
129
+ return [describeLeaf(field, at)];
130
+ });
131
+ return walk(fields, [], false);
132
+ };
133
+ /**
134
+ * The descriptors that address a value, which is what every walk resolving a
135
+ * path against a document needs. A container describes a position rather than
136
+ * a value, so only {@link describeNode} reports one.
137
+ */ const describeAddressableFields = (fields) => describeFields(fields).filter((descriptor) => !isContainer(descriptor));
138
+ /**
139
+ * Locates the blocks field that a resolved descriptor path refers to.
140
+ */ const findBlocksField = (fields, path) => {
141
+ for (const field of fields) {
142
+ if (!("name" in field) || field.name !== path[0]) continue;
143
+ if (field.type === "blocks" && path.length === 1) return field;
144
+ if (field.type === "tab" || field.type === "group") return findBlocksField(field.flattenedFields, path.slice(1));
145
+ if (field.type === "array" && path[1] === "*") return findBlocksField(field.flattenedFields, path.slice(2));
146
+ }
147
+ };
148
+ /**
149
+ * Locates the rich text field that a resolved descriptor path refers to, so
150
+ * its editor can be introspected for the fields its nodes carry.
151
+ */ const findRichTextField = (fields, path) => {
152
+ for (const field of fields) {
153
+ if (!("name" in field) || field.name !== path[0]) continue;
154
+ if (field.type === "richText" && path.length === 1) return field;
155
+ if (field.type === "tab" || field.type === "group") return findRichTextField(field.flattenedFields, path.slice(1));
156
+ if (field.type === "array" && path[1] === "*") return findRichTextField(field.flattenedFields, path.slice(2));
157
+ }
158
+ };
159
+ const targetOf = (config, ref) => {
160
+ const found = ref.kind === "collection" ? config.collections.find((candidate) => candidate.slug === ref.slug) : config.globals.find((candidate) => candidate.slug === ref.slug);
161
+ if (!found) throw new Error(`Unknown ${ref.kind} "${ref.slug}".`);
162
+ return found;
163
+ };
164
+ //#endregion
165
+ export { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, blockSlugsOf, describeAddressableFields, describeFields, findBlocksField, findRichTextField, joinPath, pointerFromPayloadPath, splitPath, targetOf };
@@ -0,0 +1,68 @@
1
+ import { errorResult, jsonResult } from "../endpoint/result.mjs";
2
+ import { localeOf, localeShape, readTarget, slugEnum } from "./shared.mjs";
3
+ import { resolveTarget } from "./target.mjs";
4
+ import { validateWriteValue } from "../schema/shape.mjs";
5
+ import { stripRowIds } from "../write/patch.mjs";
6
+ import { collectPublishBlockers } from "../write/publish-blockers.mjs";
7
+ import { z } from "zod";
8
+ //#region src/tools/create-document.ts
9
+ const createDocument = {
10
+ name: "createDocument",
11
+ description: `Creates a new document as a draft from a minimal seed. Only the fields describeSchema lists may appear in "data"; unknown keys are refused with the valid siblings. The draft may be incomplete: the response lists "publishBlockers", which patchDocument can then work through. Use this when no document exists yet; prefer patching an existing draft otherwise.`,
12
+ annotations: {
13
+ readOnlyHint: false,
14
+ destructiveHint: false,
15
+ idempotentHint: false,
16
+ openWorldHint: false
17
+ },
18
+ isEnabled: (scope) => scope.writable.length > 0,
19
+ inputSchema: (scope) => ({
20
+ collection: slugEnum(scope.writable).describe("Collection to create the document in."),
21
+ ...localeShape(scope, {
22
+ required: true,
23
+ description: "Locale the localized fields of the seed belong to."
24
+ }),
25
+ data: z.record(z.string(), z.unknown()).describe("Initial field values, as describeSchema lists them.")
26
+ }),
27
+ handler: async (args, scope) => {
28
+ const target = resolveTarget(scope, { collection: args.collection }, "write");
29
+ const { payload } = scope.req;
30
+ const locale = localeOf(scope, args.locale);
31
+ const { id: _ignored, ...seed } = args.data;
32
+ const problems = validateWriteValue(payload.config, {
33
+ pointer: "",
34
+ resolution: {
35
+ fields: target.config.flattenedFields,
36
+ prefix: []
37
+ }
38
+ }, seed);
39
+ if (problems.length > 0) return errorResult("Nothing was created.", { problems });
40
+ const created = await payload.create({
41
+ collection: args.collection,
42
+ data: stripRowIds(seed),
43
+ depth: 0,
44
+ draft: true,
45
+ overrideAccess: false,
46
+ req: scope.req,
47
+ ...locale === void 0 ? {} : { locale }
48
+ });
49
+ const saved = await readTarget(scope, {
50
+ target,
51
+ id: created["id"],
52
+ locale,
53
+ privileged: true
54
+ });
55
+ const publishBlockers = await collectPublishBlockers(scope.req, {
56
+ doc: saved,
57
+ entity: target
58
+ });
59
+ return jsonResult({
60
+ id: saved["id"],
61
+ status: saved["_status"],
62
+ updatedAt: saved["updatedAt"],
63
+ ...publishBlockers.length > 0 ? { publishBlockers } : {}
64
+ });
65
+ }
66
+ };
67
+ //#endregion
68
+ export { createDocument };
@@ -0,0 +1,54 @@
1
+ import { translatorFor } from "../i18n.mjs";
2
+ import { jsonResult } from "../endpoint/result.mjs";
3
+ import { targetShape } from "./shared.mjs";
4
+ import { refOf, resolveTarget } from "./target.mjs";
5
+ import { nodeDescriber, reachableSchemaPaths } from "../schema/describe.mjs";
6
+ import { z } from "zod";
7
+ //#region src/tools/describe-schema.ts
8
+ const describeSchema = {
9
+ name: "describeSchema",
10
+ description: `Describes the writable shape of a document, one node at a time.
11
+
12
+ 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.
13
+
14
+ 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.
15
+
16
+ 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.
17
+
18
+ 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".
19
+
20
+ Fields Payload maintains (id, _status, createdAt, updatedAt) are never listed and cannot be written. Fields marked readOnly are listed but refused on write.`,
21
+ annotations: {
22
+ readOnlyHint: true,
23
+ openWorldHint: false
24
+ },
25
+ isEnabled: (scope) => scope.readable.length + scope.readableGlobals.length > 0,
26
+ inputSchema: (scope) => ({
27
+ ...targetShape(scope, "read", {
28
+ collection: "Collection to describe.",
29
+ global: "Global to describe."
30
+ }),
31
+ paths: z.array(z.string()).optional().describe("Schema paths to describe, e.g. \"/layout/sections/sectionWrapper\". Omit for the collection root."),
32
+ expand: z.boolean().optional().describe("Return every node reachable from the root in one response. Ignores paths.")
33
+ }),
34
+ handler: (args, scope) => {
35
+ const ref = refOf(resolveTarget(scope, args, "read"));
36
+ const { config } = scope.req.payload;
37
+ const describeNode = nodeDescriber(translatorFor(scope.req.i18n));
38
+ const expanded = args.expand === true ? reachableSchemaPaths(config, ref) : void 0;
39
+ const nodes = (expanded?.paths ?? (args.paths && args.paths.length > 0 ? args.paths : [""])).map((schemaPath) => {
40
+ try {
41
+ return describeNode(config, ref, schemaPath);
42
+ } catch (error) {
43
+ return {
44
+ error: error instanceof Error ? error.message : "Unknown error",
45
+ schemaPath
46
+ };
47
+ }
48
+ });
49
+ if (expanded?.truncated) nodes.push({ error: `Result truncated after ${String(400)} nodes. Request explicit paths instead.` });
50
+ return Promise.resolve(jsonResult(nodes));
51
+ }
52
+ };
53
+ //#endregion
54
+ export { describeSchema };
@@ -0,0 +1,55 @@
1
+ import { jsonResult } from "../endpoint/result.mjs";
2
+ import { depthShape, localeOf, localeShape, slugEnum } from "./shared.mjs";
3
+ import { resolveTarget } from "./target.mjs";
4
+ import { z } from "zod";
5
+ //#region src/tools/find-documents.ts
6
+ const findDocuments = {
7
+ name: "findDocuments",
8
+ description: `Finds documents in a collection. "where" is a Payload query object, e.g. {"title":{"contains":"home"}} or {"and":[...]}; "select" picks fields, e.g. {"title":true}. Drafts are included by default so unpublished work is visible. Keep depth at 0 unless populated relationships are needed; ids are enough for writes.`,
9
+ annotations: {
10
+ readOnlyHint: true,
11
+ openWorldHint: false
12
+ },
13
+ isEnabled: (scope) => scope.readable.length > 0,
14
+ inputSchema: (scope) => ({
15
+ collection: slugEnum(scope.readable).describe("Collection to search."),
16
+ where: z.record(z.string(), z.unknown()).optional().describe("Payload where query."),
17
+ sort: z.string().optional().describe("Sort field, prefix with \"-\" for descending."),
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)}.`),
19
+ page: z.number().int().min(1).optional().describe("Page number, from 1."),
20
+ ...depthShape(scope),
21
+ select: z.record(z.string(), z.unknown()).optional().describe("Fields to return, e.g. {\"title\":true}."),
22
+ ...localeShape(scope, {
23
+ required: false,
24
+ description: "Locale to read. Defaults to the default locale."
25
+ }),
26
+ draft: z.boolean().optional().describe("Include the latest drafts. Default true.")
27
+ }),
28
+ handler: async (args, scope) => {
29
+ resolveTarget(scope, { collection: args.collection }, "read");
30
+ const locale = localeOf(scope, args.locale);
31
+ const result = await scope.req.payload.find({
32
+ collection: args.collection,
33
+ depth: args.depth ?? 0,
34
+ draft: args.draft ?? true,
35
+ limit: args.limit ?? 10,
36
+ overrideAccess: false,
37
+ req: scope.req,
38
+ ...args.page === void 0 ? {} : { page: args.page },
39
+ ...args.sort === void 0 ? {} : { sort: args.sort },
40
+ ...args.where === void 0 ? {} : { where: args.where },
41
+ ...args.select === void 0 ? {} : { select: args.select },
42
+ ...locale === void 0 ? {} : { locale }
43
+ });
44
+ return jsonResult({
45
+ docs: result.docs,
46
+ totalDocs: result.totalDocs,
47
+ page: result.page,
48
+ totalPages: result.totalPages,
49
+ limit: result.limit,
50
+ hasNextPage: result.hasNextPage
51
+ });
52
+ }
53
+ };
54
+ //#endregion
55
+ export { findDocuments };
@@ -0,0 +1,68 @@
1
+ import { JSON_POINTER_PATTERN } from "../schema/walk.mjs";
2
+ import { errorResult, jsonResult } from "../endpoint/result.mjs";
3
+ import { depthShape, idShape, localeOf, localeShape, targetShape } from "./shared.mjs";
4
+ import { requireIdFor, resolveTarget } from "./target.mjs";
5
+ import { z } from "zod";
6
+ import { Pointer } from "rfc6902";
7
+ //#region src/tools/get-document.ts
8
+ const getDocument = {
9
+ name: "getDocument",
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.`,
13
+ annotations: {
14
+ readOnlyHint: true,
15
+ openWorldHint: false
16
+ },
17
+ isEnabled: (scope) => scope.readable.length + scope.readableGlobals.length > 0,
18
+ inputSchema: (scope) => ({
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\"."),
25
+ ...depthShape(scope),
26
+ ...localeShape(scope, {
27
+ required: false,
28
+ description: "Locale to read. Defaults to the default locale."
29
+ }),
30
+ draft: z.boolean().optional().describe("Return the latest draft. Default true.")
31
+ }),
32
+ handler: async (args, scope) => {
33
+ const target = resolveTarget(scope, args, "read");
34
+ const id = requireIdFor(target, args.id);
35
+ const locale = localeOf(scope, args.locale);
36
+ const shared = {
37
+ depth: args.depth ?? 0,
38
+ draft: args.draft ?? true,
39
+ overrideAccess: false,
40
+ req: scope.req,
41
+ ...locale === void 0 ? {} : { locale }
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
+ }));
51
+ if (args.path === void 0 || args.path === "") return jsonResult(doc);
52
+ let value;
53
+ try {
54
+ value = Pointer.fromJSON(args.path).get(doc);
55
+ } catch {
56
+ return errorResult(`"${args.path}" is not a valid JSON pointer.`);
57
+ }
58
+ return jsonResult({
59
+ ...target.kind === "collection" ? { id: doc["id"] } : { global: target.slug },
60
+ status: doc["_status"],
61
+ updatedAt: doc["updatedAt"],
62
+ path: args.path,
63
+ value
64
+ });
65
+ }
66
+ };
67
+ //#endregion
68
+ export { getDocument };
@@ -0,0 +1,23 @@
1
+ import { createDocument } from "./create-document.mjs";
2
+ import { describeSchema } from "./describe-schema.mjs";
3
+ import { findDocuments } from "./find-documents.mjs";
4
+ import { getDocument } from "./get-document.mjs";
5
+ import { listCapabilities } from "./list-capabilities.mjs";
6
+ import { patchDocument } from "./patch-document.mjs";
7
+ import { validateDocument } from "./validate-document.mjs";
8
+ //#region src/tools/index.ts
9
+ /**
10
+ * The builtin tools in registration order. The surface is fixed: adding a
11
+ * collection, block or field never changes it. Typed over `never` because
12
+ * each tool validates its own arguments through its input schema.
13
+ */ const BUILTIN_TOOLS = [
14
+ listCapabilities,
15
+ describeSchema,
16
+ findDocuments,
17
+ getDocument,
18
+ patchDocument,
19
+ createDocument,
20
+ validateDocument
21
+ ];
22
+ //#endregion
23
+ export { BUILTIN_TOOLS };