@abinnovision/payloadcms-mcpx 1.0.0-beta.6 → 1.0.0-beta.8
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 +13 -2
- package/dist/i18n.d.mts +1 -0
- package/dist/i18n.mjs +40 -0
- package/dist/schema/describe.mjs +7 -3
- package/dist/schema/lexical.d.mts +1 -0
- package/dist/schema/lexical.mjs +45 -1
- package/dist/schema/pointer.mjs +2 -2
- package/dist/schema/shape.mjs +17 -9
- package/dist/schema/walk.d.mts +2 -0
- package/dist/schema/walk.mjs +66 -30
- package/dist/tools/describe-schema.mjs +3 -1
- package/dist/tools/list-capabilities.mjs +4 -3
- package/dist/tools/shared.mjs +2 -3
- package/dist/write/patch.mjs +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -151,10 +151,21 @@ Rules the tools enforce and explain in their own descriptions:
|
|
|
151
151
|
guessed. Any feature declaring `getSubFields` is picked up, custom ones
|
|
152
152
|
included. `upload` nodes are the exception: their fields depend on the
|
|
153
153
|
collection the node points at, so they are not addressable.
|
|
154
|
+
- Constraints a field declares travel with it: `minRows`/`maxRows` on arrays
|
|
155
|
+
and blocks fields, `maxLength`/`minLength` on text, `min`/`max` on numbers.
|
|
156
|
+
An array is described in its own right, so the `*` in `/items/*/title` has
|
|
157
|
+
something to read; a group or named tab only when it declares a description
|
|
158
|
+
or a constraint of its own.
|
|
159
|
+
- A `richText` field also reports `nodeOptions`, the node properties its editor
|
|
160
|
+
narrows. An editor built with `HeadingFeature({ enabledHeadingSizes: ["h4"] })`
|
|
161
|
+
answers `{ "heading": { "tag": ["h4"] } }`, and a write carrying any other
|
|
162
|
+
heading tag is refused. Lexical stores whatever tag it is given, so this is
|
|
163
|
+
the only place the restriction is checked.
|
|
154
164
|
- Field and collection `admin.description` values are included in
|
|
155
165
|
`describeSchema` and `listCapabilities`, so intent written for the admin
|
|
156
|
-
panel reaches the client.
|
|
157
|
-
|
|
166
|
+
panel reaches the client. A locale-keyed record is resolved to one string for
|
|
167
|
+
the request's language, falling back to the deployment's fallback language and
|
|
168
|
+
then to the record's first entry; functions and components are dropped.
|
|
158
169
|
- Builtin tools reject unknown arguments by name instead of silently ignoring
|
|
159
170
|
them.
|
|
160
171
|
- Every path this plugin accepts or reports is a JSON Pointer. A schema path
|
package/dist/i18n.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { PayloadRequest } from "payload";
|
package/dist/i18n.mjs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/i18n.ts
|
|
2
|
+
/**
|
|
3
|
+
* A locale-keyed record, once it is known to hold nothing but strings.
|
|
4
|
+
*/ const stringRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string") ? value : void 0;
|
|
5
|
+
/**
|
|
6
|
+
* Picks the entry a language addresses, treating an empty value as absent so
|
|
7
|
+
* the chain continues rather than yielding a useless string.
|
|
8
|
+
*/ const pick = (record, language) => {
|
|
9
|
+
for (const code of Array.isArray(language) ? language : [language]) {
|
|
10
|
+
const entry = record[code];
|
|
11
|
+
if (entry !== void 0 && entry.trim() !== "") return entry;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Resolves a static label or `admin.description` to the one string a client
|
|
16
|
+
* can use: the request's language, then the fallback language configured for
|
|
17
|
+
* the deployment, then whichever entry the record declares first.
|
|
18
|
+
*
|
|
19
|
+
* Anything that is not a string or a string-valued record is dropped. A
|
|
20
|
+
* description written as a function or a React component is an admin-UI
|
|
21
|
+
* construct that may reach client-only i18n, so it is never invoked here.
|
|
22
|
+
*/ const translateStatic = (value, language) => {
|
|
23
|
+
if (typeof value === "string") return value.trim() === "" ? void 0 : value;
|
|
24
|
+
const record = stringRecord(value);
|
|
25
|
+
if (!record) return;
|
|
26
|
+
return pick(record, language.language) ?? pick(record, language.fallbackLanguage) ?? Object.values(record).find((entry) => entry.trim() !== "");
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Binds {@link translateStatic} to a request's language, so a walk that
|
|
30
|
+
* resolves many descriptions carries no request of its own.
|
|
31
|
+
*/ const translatorFor = (i18n) => (value) => translateStatic(value, i18n);
|
|
32
|
+
/**
|
|
33
|
+
* Translator for callers with no request in hand. Both language keys miss, so
|
|
34
|
+
* the chain degrades to the record's first entry.
|
|
35
|
+
*/ const translateAny = translatorFor({
|
|
36
|
+
fallbackLanguage: "",
|
|
37
|
+
language: ""
|
|
38
|
+
});
|
|
39
|
+
//#endregion
|
|
40
|
+
export { translateAny, translateStatic, translatorFor };
|
package/dist/schema/describe.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { lexicalSubSchema, subSchemaNodeTypes } from "./lexical.mjs";
|
|
2
|
+
import { translateAny } from "../i18n.mjs";
|
|
2
3
|
import { blockOf, blockSlugsOf, describeFields, findBlocksField, findRichTextField, joinPath, splitPath, targetOf } from "./walk.mjs";
|
|
3
4
|
//#region src/schema/describe.ts
|
|
4
5
|
/**
|
|
@@ -115,9 +116,12 @@ import { blockOf, blockSlugsOf, describeFields, findBlocksField, findRichTextFie
|
|
|
115
116
|
/**
|
|
116
117
|
* Describes a collection or global root, one block reached through a schema
|
|
117
118
|
* path, or the fields a Lexical node carries.
|
|
118
|
-
|
|
119
|
+
*
|
|
120
|
+
* Curried on the translator that resolves each `admin.description`, so a
|
|
121
|
+
* request binds its language once and the walk itself stays request-free.
|
|
122
|
+
*/ const nodeDescriber = (translate = translateAny) => (config, ref, schemaPath = "") => {
|
|
119
123
|
const { blockType, fields } = fieldsAtSchemaPath(config, targetOf(config, ref), schemaPath);
|
|
120
|
-
const descriptors = describeFields(fields);
|
|
124
|
+
const descriptors = describeFields(fields, translate);
|
|
121
125
|
const next = descriptors.flatMap((descriptor) => branchesOf(fields, descriptor, schemaPath).map((branch) => branch.path));
|
|
122
126
|
return {
|
|
123
127
|
...blockType === void 0 ? {} : { blockType },
|
|
@@ -153,4 +157,4 @@ import { blockOf, blockSlugsOf, describeFields, findBlocksField, findRichTextFie
|
|
|
153
157
|
};
|
|
154
158
|
};
|
|
155
159
|
//#endregion
|
|
156
|
-
export {
|
|
160
|
+
export { nodeDescriber, reachableSchemaPaths };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "payload";
|
package/dist/schema/lexical.mjs
CHANGED
|
@@ -69,5 +69,49 @@ const resolveSubSchema = (field, nodeType) => {
|
|
|
69
69
|
* Node types of a rich text field that have a sub-schema, in the order their
|
|
70
70
|
* features registered them.
|
|
71
71
|
*/ const subSchemaNodeTypes = (field) => [...featuresOf(field)?.getSubFields?.keys() ?? []].filter((nodeType) => lexicalSubSchema(field, nodeType) !== void 0);
|
|
72
|
+
/**
|
|
73
|
+
* Node properties worth reporting and enforcing.
|
|
74
|
+
*
|
|
75
|
+
* Only properties a feature narrows and Lexical does not check on its own
|
|
76
|
+
* belong here. Everything else a feature restricts is already visible: a
|
|
77
|
+
* link's targets through its sub-schema, a block node's choices through the
|
|
78
|
+
* slugs it accepts.
|
|
79
|
+
*/ const NODE_OPTION_SOURCES = [{
|
|
80
|
+
defaults: [
|
|
81
|
+
"h1",
|
|
82
|
+
"h2",
|
|
83
|
+
"h3",
|
|
84
|
+
"h4",
|
|
85
|
+
"h5",
|
|
86
|
+
"h6"
|
|
87
|
+
],
|
|
88
|
+
featureKey: "heading",
|
|
89
|
+
featureProp: "enabledHeadingSizes",
|
|
90
|
+
nodeProp: "tag",
|
|
91
|
+
nodeType: "heading"
|
|
92
|
+
}];
|
|
93
|
+
/**
|
|
94
|
+
* The props a feature was resolved with.
|
|
95
|
+
*
|
|
96
|
+
* Sanitizing the editor drops every feature's props from `editorConfig.features`
|
|
97
|
+
* but leaves them on `resolvedFeatureMap`. A feature that declares no server
|
|
98
|
+
* props keeps only the client ones, so both are tried.
|
|
99
|
+
*/ const featurePropsOf = (field, featureKey) => {
|
|
100
|
+
const resolved = field.editor?.editorConfig?.resolvedFeatureMap?.get(featureKey);
|
|
101
|
+
const props = resolved?.sanitizedServerFeatureProps ?? resolved?.clientFeatureProps;
|
|
102
|
+
return typeof props === "object" && props !== null ? props : void 0;
|
|
103
|
+
};
|
|
104
|
+
const stringList = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : void 0;
|
|
105
|
+
/**
|
|
106
|
+
* The narrowed node properties of a rich text field, for the node types it
|
|
107
|
+
* actually accepts.
|
|
108
|
+
*/ const nodeOptions = (field, allowed) => {
|
|
109
|
+
const entries = NODE_OPTION_SOURCES.flatMap((source) => {
|
|
110
|
+
if (!allowed.includes(source.nodeType)) return [];
|
|
111
|
+
const values = stringList(featurePropsOf(field, source.featureKey)?.[source.featureProp]) ?? [...source.defaults];
|
|
112
|
+
return [[source.nodeType, { [source.nodeProp]: values }]];
|
|
113
|
+
});
|
|
114
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
115
|
+
};
|
|
72
116
|
//#endregion
|
|
73
|
-
export { allowedNodeTypes, lexicalSubSchema, subSchemaNodeTypes };
|
|
117
|
+
export { allowedNodeTypes, lexicalSubSchema, nodeOptions, subSchemaNodeTypes };
|
package/dist/schema/pointer.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { blockOf, blockSlugsOf,
|
|
1
|
+
import { blockOf, blockSlugsOf, describeAddressableFields, 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);
|
|
@@ -37,7 +37,7 @@ const partMatches = (part, segment) => segment !== void 0 && (part === "*" ? isI
|
|
|
37
37
|
let blockType;
|
|
38
38
|
let segments = splitPath(target.pointer);
|
|
39
39
|
while (segments.length > 0) {
|
|
40
|
-
const descriptors =
|
|
40
|
+
const descriptors = describeAddressableFields(fields);
|
|
41
41
|
const match = longestMatch(descriptors, segments);
|
|
42
42
|
if (!match) {
|
|
43
43
|
if (isSubtreePrefix(descriptors, segments)) return {
|
package/dist/schema/shape.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { lexicalSubSchema } from "./lexical.mjs";
|
|
2
|
-
import { blockOf, blockSlugsOf,
|
|
2
|
+
import { blockOf, blockSlugsOf, describeAddressableFields, findBlocksField, findRichTextField, splitPath } from "./walk.mjs";
|
|
3
3
|
//#region src/schema/shape.ts
|
|
4
4
|
/**
|
|
5
5
|
* Keys Payload manages on a row that a client may echo back harmlessly.
|
|
@@ -54,7 +54,9 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
|
|
|
54
54
|
* few node types that register one, so a `heading` inside a field whose
|
|
55
55
|
* editor has no heading feature is stored without complaint and only fails
|
|
56
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.
|
|
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`.
|
|
58
60
|
*/ const checkRichText = (scope, editor, value) => {
|
|
59
61
|
if (!isPlainObject(value) || !isPlainObject(value["root"])) {
|
|
60
62
|
scope.problems.push(`${scope.pointer}: expected a Lexical editor state with a "root".`);
|
|
@@ -72,6 +74,10 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
|
|
|
72
74
|
scope.problems.push(`${at}: "${node["type"]}" is not available in this field's editor. Allowed: ${editor.allowed.join(", ")}`);
|
|
73
75
|
return;
|
|
74
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
|
+
}
|
|
75
81
|
if (editor.field) checkNodeFields({
|
|
76
82
|
...scope,
|
|
77
83
|
pointer: at
|
|
@@ -92,7 +98,8 @@ const checkLeafValue = (scope, descriptor, value) => {
|
|
|
92
98
|
if (descriptor.type === "richText") {
|
|
93
99
|
checkRichText(scope, {
|
|
94
100
|
allowed: descriptor.nodes ?? [],
|
|
95
|
-
field: findRichTextField(scope.fields, splitPath(descriptor.path))
|
|
101
|
+
field: findRichTextField(scope.fields, splitPath(descriptor.path)),
|
|
102
|
+
nodeOptions: descriptor.nodeOptions
|
|
96
103
|
}, value);
|
|
97
104
|
return;
|
|
98
105
|
}
|
|
@@ -122,15 +129,16 @@ const checkLeafValue = (scope, descriptor, value) => {
|
|
|
122
129
|
* Walks an incoming value against the schema, reporting every shape problem
|
|
123
130
|
* rather than the first.
|
|
124
131
|
*
|
|
125
|
-
* Shape only: unknown field names, unknown block slugs, read-only fields and
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* block would be
|
|
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.
|
|
130
138
|
*/ const checkValue = (scope, value) => {
|
|
131
139
|
if (!isPlainObject(value)) return;
|
|
132
140
|
const prefixParts = scope.prefix;
|
|
133
|
-
const relative =
|
|
141
|
+
const relative = describeAddressableFields(scope.fields).flatMap((descriptor) => {
|
|
134
142
|
const parts = splitPath(descriptor.path);
|
|
135
143
|
return prefixParts.every((part, offset) => part === parts[offset]) ? [{
|
|
136
144
|
descriptor,
|
package/dist/schema/walk.d.mts
CHANGED
package/dist/schema/walk.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { allowedNodeTypes } from "./lexical.mjs";
|
|
1
|
+
import { allowedNodeTypes, nodeOptions } from "./lexical.mjs";
|
|
2
|
+
import { translateAny } from "../i18n.mjs";
|
|
2
3
|
import { fieldIsHiddenOrDisabled, fieldIsVirtual } from "payload/shared";
|
|
3
4
|
//#region src/schema/walk.ts
|
|
4
5
|
/**
|
|
@@ -46,16 +47,8 @@ import { fieldIsHiddenOrDisabled, fieldIsVirtual } from "payload/shared";
|
|
|
46
47
|
};
|
|
47
48
|
const isSkipped = (field) => !("name" in field) || field.type === "join" || RESERVED_FIELD_NAMES.has(field.name) || fieldIsVirtual(field) || fieldIsHiddenOrDisabled(field);
|
|
48
49
|
const isReadOnly = (field) => "admin" in field && field.admin.readOnly === true;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
* a string or a locale-keyed record. Functions and components are admin-UI
|
|
52
|
-
* constructs and are dropped.
|
|
53
|
-
*/ const staticDescription = (description) => {
|
|
54
|
-
if (typeof description === "string") return description;
|
|
55
|
-
return typeof description === "object" && description !== null && Object.values(description).every((entry) => typeof entry === "string") ? description : void 0;
|
|
56
|
-
};
|
|
57
|
-
const describeBase = (field, path, readOnly) => {
|
|
58
|
-
const description = staticDescription("admin" in field ? field.admin.description : void 0);
|
|
50
|
+
const describeBase = (field, { path, readOnly, translate }) => {
|
|
51
|
+
const description = translate("admin" in field ? field.admin.description : void 0);
|
|
59
52
|
return {
|
|
60
53
|
path,
|
|
61
54
|
type: field.type,
|
|
@@ -65,12 +58,20 @@ const describeBase = (field, path, readOnly) => {
|
|
|
65
58
|
...readOnly ? { readOnly: true } : {}
|
|
66
59
|
};
|
|
67
60
|
};
|
|
68
|
-
const describeLeaf = (field,
|
|
69
|
-
const descriptor = describeBase(field,
|
|
61
|
+
const describeLeaf = (field, at) => {
|
|
62
|
+
const descriptor = describeBase(field, at);
|
|
70
63
|
if (field.type === "select" || field.type === "radio") descriptor.options = field.options.map((option) => typeof option === "string" ? option : option.value);
|
|
71
64
|
if (field.type === "relationship" || field.type === "upload") descriptor.relationTo = field.relationTo;
|
|
72
65
|
if ((field.type === "select" || field.type === "relationship" || field.type === "upload") && field.hasMany === true) descriptor.hasMany = true;
|
|
73
|
-
if (field.type === "
|
|
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
|
+
}
|
|
74
75
|
return descriptor;
|
|
75
76
|
};
|
|
76
77
|
const withRows = (descriptor, field) => ({
|
|
@@ -79,26 +80,61 @@ const withRows = (descriptor, field) => ({
|
|
|
79
80
|
...field.maxRows === void 0 ? {} : { maxRows: field.maxRows }
|
|
80
81
|
});
|
|
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
|
+
/**
|
|
82
94
|
* Flattens a field list into descriptors addressed relative to the node.
|
|
83
95
|
*
|
|
84
96
|
* The input is Payload's own flattened shape, which has already merged every
|
|
85
97
|
* construct that exists only in the admin UI (unnamed tabs, unnamed groups,
|
|
86
98
|
* `row`, `collapsible`) and dropped `ui` fields. Named tabs, groups and
|
|
87
|
-
* arrays contribute a path segment
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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));
|
|
102
138
|
/**
|
|
103
139
|
* Locates the blocks field that a resolved descriptor path refers to.
|
|
104
140
|
*/ const findBlocksField = (fields, path) => {
|
|
@@ -126,4 +162,4 @@ const targetOf = (config, ref) => {
|
|
|
126
162
|
return found;
|
|
127
163
|
};
|
|
128
164
|
//#endregion
|
|
129
|
-
export { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, blockSlugsOf, describeFields, findBlocksField, findRichTextField, joinPath, pointerFromPayloadPath, splitPath,
|
|
165
|
+
export { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, blockSlugsOf, describeAddressableFields, describeFields, findBlocksField, findRichTextField, joinPath, pointerFromPayloadPath, splitPath, targetOf };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { translatorFor } from "../i18n.mjs";
|
|
1
2
|
import { jsonResult } from "../endpoint/result.mjs";
|
|
2
3
|
import { targetShape } from "./shared.mjs";
|
|
3
4
|
import { refOf, resolveTarget } from "./target.mjs";
|
|
4
|
-
import {
|
|
5
|
+
import { nodeDescriber, reachableSchemaPaths } from "../schema/describe.mjs";
|
|
5
6
|
import { z } from "zod";
|
|
6
7
|
//#region src/tools/describe-schema.ts
|
|
7
8
|
const describeSchema = {
|
|
@@ -33,6 +34,7 @@ Fields Payload maintains (id, _status, createdAt, updatedAt) are never listed an
|
|
|
33
34
|
handler: (args, scope) => {
|
|
34
35
|
const ref = refOf(resolveTarget(scope, args, "read"));
|
|
35
36
|
const { config } = scope.req.payload;
|
|
37
|
+
const describeNode = nodeDescriber(translatorFor(scope.req.i18n));
|
|
36
38
|
const expanded = args.expand === true ? reachableSchemaPaths(config, ref) : void 0;
|
|
37
39
|
const nodes = (expanded?.paths ?? (args.paths && args.paths.length > 0 ? args.paths : [""])).map((schemaPath) => {
|
|
38
40
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { translatorFor } from "../i18n.mjs";
|
|
2
2
|
import { jsonResult } from "../endpoint/result.mjs";
|
|
3
3
|
import { translateLabel } from "./shared.mjs";
|
|
4
4
|
import { hasDraftValidationEnabled } from "payload/shared";
|
|
@@ -16,12 +16,13 @@ A global is a singleton: it has no id, is not listed by findDocuments and cannot
|
|
|
16
16
|
inputSchema: () => ({}),
|
|
17
17
|
handler: (_args, scope) => {
|
|
18
18
|
const { payload } = scope.req;
|
|
19
|
+
const translate = translatorFor(scope.req.i18n);
|
|
19
20
|
const collections = scope.options.collections.flatMap((entry) => {
|
|
20
21
|
const capability = scope.capabilities.collections[entry.slug];
|
|
21
22
|
const collection = payload.collections[entry.slug];
|
|
22
23
|
if (!capability || !collection || !(capability.read || capability.write)) return [];
|
|
23
24
|
const { config } = collection;
|
|
24
|
-
const description =
|
|
25
|
+
const description = translate(config.admin.description);
|
|
25
26
|
return [{
|
|
26
27
|
slug: entry.slug,
|
|
27
28
|
labels: {
|
|
@@ -40,7 +41,7 @@ A global is a singleton: it has no id, is not listed by findDocuments and cannot
|
|
|
40
41
|
const capability = scope.capabilities.globals[entry.slug];
|
|
41
42
|
const config = payload.globals.config.find((candidate) => candidate.slug === entry.slug);
|
|
42
43
|
if (!capability || !config || !(capability.read || capability.write)) return [];
|
|
43
|
-
const description =
|
|
44
|
+
const description = translate(config.admin.description);
|
|
44
45
|
return [{
|
|
45
46
|
slug: entry.slug,
|
|
46
47
|
label: translateLabel(scope, config.label, entry.slug),
|
package/dist/tools/shared.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { translateStatic } from "../i18n.mjs";
|
|
1
2
|
import { NotFound } from "payload";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
//#region src/tools/shared.ts
|
|
@@ -90,9 +91,7 @@ const depthShape = (scope) => ({ depth: z.number().int().min(0).max(scope.option
|
|
|
90
91
|
i18n,
|
|
91
92
|
t
|
|
92
93
|
}) : label;
|
|
93
|
-
|
|
94
|
-
if (resolved && typeof resolved === "object") return resolved[i18n.language] ?? Object.values(resolved)[0] ?? fallback;
|
|
95
|
-
return fallback;
|
|
94
|
+
return translateStatic(resolved, i18n) ?? fallback;
|
|
96
95
|
};
|
|
97
96
|
//#endregion
|
|
98
97
|
export { depthShape, idSchema, idShape, localeOf, localeShape, readTarget, slugEnum, targetShape, translateLabel };
|
package/dist/write/patch.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf,
|
|
1
|
+
import { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, describeAddressableFields, findBlocksField, splitPath } from "../schema/walk.mjs";
|
|
2
2
|
import { validateWriteValue } from "../schema/shape.mjs";
|
|
3
3
|
import { resolveDataPointer } from "../schema/pointer.mjs";
|
|
4
4
|
import { z } from "zod";
|
|
@@ -154,7 +154,7 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
|
|
|
154
154
|
* is left out, so the write-back carries only what a client could have set.
|
|
155
155
|
*/ const pickDescribed = (config, value, at) => {
|
|
156
156
|
const { fields, prefix, isRow } = at;
|
|
157
|
-
const relative =
|
|
157
|
+
const relative = describeAddressableFields(fields).flatMap((descriptor) => {
|
|
158
158
|
const parts = splitPath(descriptor.path);
|
|
159
159
|
return prefix.every((part, offset) => part === parts[offset]) ? [{
|
|
160
160
|
descriptor,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@abinnovision/payloadcms-mcpx",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.8",
|
|
5
5
|
"description": "Payload CMS plugin exposing a fixed, schema-aware MCP tool surface with draft-only writes and per-API-key capabilities.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"payload",
|