@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.
- package/LICENSE +201 -0
- package/README.md +353 -0
- package/dist/api-keys/collection.mjs +58 -0
- package/dist/api-keys/fields.mjs +137 -0
- package/dist/api-keys/key.mjs +10 -0
- package/dist/api-keys/setup-guide.mjs +54 -0
- package/dist/auth/resolve.mjs +62 -0
- package/dist/capabilities.mjs +43 -0
- package/dist/client/index.d.mts +2 -0
- package/dist/client/index.mjs +2 -0
- package/dist/client/setup-guide.d.mts +14 -0
- package/dist/client/setup-guide.mjs +87 -0
- package/dist/endpoint/handler.mjs +86 -0
- package/dist/endpoint/result.mjs +65 -0
- package/dist/endpoint/server.mjs +52 -0
- package/dist/i18n.d.mts +1 -0
- package/dist/i18n.mjs +40 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +4 -0
- package/dist/options.d.mts +2 -0
- package/dist/options.mjs +146 -0
- package/dist/plugin.d.mts +9 -0
- package/dist/plugin.mjs +43 -0
- package/dist/schema/describe.mjs +160 -0
- package/dist/schema/lexical.d.mts +1 -0
- package/dist/schema/lexical.mjs +117 -0
- package/dist/schema/pointer.mjs +80 -0
- package/dist/schema/shape.mjs +208 -0
- package/dist/schema/walk.d.mts +3 -0
- package/dist/schema/walk.mjs +165 -0
- package/dist/tools/create-document.mjs +68 -0
- package/dist/tools/describe-schema.mjs +54 -0
- package/dist/tools/find-documents.mjs +55 -0
- package/dist/tools/get-document.mjs +68 -0
- package/dist/tools/index.mjs +23 -0
- package/dist/tools/list-capabilities.mjs +68 -0
- package/dist/tools/names.mjs +14 -0
- package/dist/tools/patch-document.mjs +127 -0
- package/dist/tools/shared.mjs +97 -0
- 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 +55 -0
- package/dist/types.d.mts +143 -0
- package/dist/types.mjs +7 -0
- package/dist/version.mjs +6 -0
- package/dist/write/draft-guard.d.mts +10 -0
- package/dist/write/draft-guard.mjs +113 -0
- package/dist/write/patch.mjs +219 -0
- package/dist/write/publish-blockers.d.mts +15 -0
- package/dist/write/publish-blockers.mjs +74 -0
- package/dist/write/transaction.mjs +19 -0
- 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,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 };
|