@abinnovision/payloadcms-mcpx 1.0.0-beta.6 → 1.0.0-beta.7
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 +10 -0
- 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 +1 -0
- package/dist/schema/walk.mjs +38 -8
- package/dist/write/patch.mjs +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -151,6 +151,16 @@ 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
166
|
panel reaches the client. Strings and locale-keyed records pass through;
|
|
@@ -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,4 @@
|
|
|
1
|
-
import { allowedNodeTypes } from "./lexical.mjs";
|
|
1
|
+
import { allowedNodeTypes, nodeOptions } from "./lexical.mjs";
|
|
2
2
|
import { fieldIsHiddenOrDisabled, fieldIsVirtual } from "payload/shared";
|
|
3
3
|
//#region src/schema/walk.ts
|
|
4
4
|
/**
|
|
@@ -70,7 +70,15 @@ const describeLeaf = (field, path, readOnly) => {
|
|
|
70
70
|
if (field.type === "select" || field.type === "radio") descriptor.options = field.options.map((option) => typeof option === "string" ? option : option.value);
|
|
71
71
|
if (field.type === "relationship" || field.type === "upload") descriptor.relationTo = field.relationTo;
|
|
72
72
|
if ((field.type === "select" || field.type === "relationship" || field.type === "upload") && field.hasMany === true) descriptor.hasMany = true;
|
|
73
|
-
if (field.type === "
|
|
73
|
+
if ((field.type === "text" || field.type === "textarea") && field.maxLength !== void 0) descriptor.maxLength = field.maxLength;
|
|
74
|
+
if ((field.type === "text" || field.type === "textarea") && field.minLength !== void 0) descriptor.minLength = field.minLength;
|
|
75
|
+
if (field.type === "number" && field.max !== void 0) descriptor.max = field.max;
|
|
76
|
+
if (field.type === "number" && field.min !== void 0) descriptor.min = field.min;
|
|
77
|
+
if (field.type === "richText") {
|
|
78
|
+
descriptor.nodes = allowedNodeTypes(field);
|
|
79
|
+
const options = nodeOptions(field, descriptor.nodes);
|
|
80
|
+
if (options) descriptor.nodeOptions = options;
|
|
81
|
+
}
|
|
74
82
|
return descriptor;
|
|
75
83
|
};
|
|
76
84
|
const withRows = (descriptor, field) => ({
|
|
@@ -79,20 +87,37 @@ const withRows = (descriptor, field) => ({
|
|
|
79
87
|
...field.maxRows === void 0 ? {} : { maxRows: field.maxRows }
|
|
80
88
|
});
|
|
81
89
|
/**
|
|
90
|
+
* Whether a descriptor stands for a construct that only holds other fields.
|
|
91
|
+
*
|
|
92
|
+
* These describe a position rather than a value, so everything that resolves a
|
|
93
|
+
* path to something writable skips them; only {@link describeNode} reports
|
|
94
|
+
* them, to carry what the container itself declares.
|
|
95
|
+
*/ const isContainer = (descriptor) => descriptor.type === "array" || descriptor.type === "group" || descriptor.type === "tab";
|
|
96
|
+
/**
|
|
97
|
+
* Whether a container declares anything a client could not infer from the
|
|
98
|
+
* fields beneath it. A group that exists only to nest is not worth reporting.
|
|
99
|
+
*/ const isInformative = (descriptor) => descriptor.description !== void 0 || descriptor.required === true || descriptor.localized === true;
|
|
100
|
+
/**
|
|
82
101
|
* Flattens a field list into descriptors addressed relative to the node.
|
|
83
102
|
*
|
|
84
103
|
* The input is Payload's own flattened shape, which has already merged every
|
|
85
104
|
* construct that exists only in the admin UI (unnamed tabs, unnamed groups,
|
|
86
105
|
* `row`, `collapsible`) and dropped `ui` fields. Named tabs, groups and
|
|
87
|
-
* arrays contribute a path segment
|
|
88
|
-
*
|
|
89
|
-
*
|
|
106
|
+
* arrays contribute a path segment, and are described in their own right when
|
|
107
|
+
* they declare something of their own: an array always, since its row counts
|
|
108
|
+
* live nowhere else, a group or tab only when it carries a description or a
|
|
109
|
+
* constraint. The walk stops at every blocks field and names the slugs instead
|
|
110
|
+
* of descending, which keeps a node proportional to the number of blocks it
|
|
111
|
+
* allows rather than to the size of their definitions.
|
|
90
112
|
*/ const describeFields = (fields, prefix = [], parentReadOnly = false) => fields.flatMap((field) => {
|
|
91
113
|
if (isSkipped(field)) return [];
|
|
92
114
|
const readOnly = parentReadOnly || isReadOnly(field);
|
|
93
115
|
const path = [...prefix, field.name];
|
|
94
|
-
if (field.type === "tab" || field.type === "group")
|
|
95
|
-
|
|
116
|
+
if (field.type === "tab" || field.type === "group") {
|
|
117
|
+
const own = describeBase(field, joinPath(path), readOnly);
|
|
118
|
+
return [...isInformative(own) ? [own] : [], ...describeFields(field.flattenedFields, path, readOnly)];
|
|
119
|
+
}
|
|
120
|
+
if (field.type === "array") return [withRows(describeBase(field, joinPath(path), readOnly), field), ...describeFields(field.flattenedFields, [...path, "*"], readOnly)];
|
|
96
121
|
if (field.type === "blocks") return [withRows({
|
|
97
122
|
...describeBase(field, joinPath(path), readOnly),
|
|
98
123
|
blocks: blockSlugsOf(field)
|
|
@@ -100,6 +125,11 @@ const withRows = (descriptor, field) => ({
|
|
|
100
125
|
return [describeLeaf(field, joinPath(path), readOnly)];
|
|
101
126
|
});
|
|
102
127
|
/**
|
|
128
|
+
* The descriptors that address a value, which is what every walk resolving a
|
|
129
|
+
* path against a document needs. A container describes a position rather than
|
|
130
|
+
* a value, so only {@link describeNode} reports one.
|
|
131
|
+
*/ const describeAddressableFields = (fields) => describeFields(fields).filter((descriptor) => !isContainer(descriptor));
|
|
132
|
+
/**
|
|
103
133
|
* Locates the blocks field that a resolved descriptor path refers to.
|
|
104
134
|
*/ const findBlocksField = (fields, path) => {
|
|
105
135
|
for (const field of fields) {
|
|
@@ -126,4 +156,4 @@ const targetOf = (config, ref) => {
|
|
|
126
156
|
return found;
|
|
127
157
|
};
|
|
128
158
|
//#endregion
|
|
129
|
-
export { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, blockSlugsOf, describeFields, findBlocksField, findRichTextField, joinPath, pointerFromPayloadPath, splitPath, staticDescription, targetOf };
|
|
159
|
+
export { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, blockSlugsOf, describeAddressableFields, describeFields, findBlocksField, findRichTextField, joinPath, pointerFromPayloadPath, splitPath, staticDescription, targetOf };
|
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.7",
|
|
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",
|