@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 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. Strings and locale-keyed records pass through;
157
- functions and components are dropped.
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
@@ -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 };
@@ -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
- */ const describeNode = (config, ref, schemaPath = "") => {
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 { describeNode, reachableSchemaPaths };
160
+ export { nodeDescriber, reachableSchemaPaths };
@@ -0,0 +1 @@
1
+ import "payload";
@@ -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 };
@@ -1,4 +1,4 @@
1
- import { blockOf, blockSlugsOf, describeFields, findBlocksField, joinPath, splitPath, targetOf } from "./walk.mjs";
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 = describeFields(fields);
40
+ const descriptors = describeAddressableFields(fields);
41
41
  const match = longestMatch(descriptors, segments);
42
42
  if (!match) {
43
43
  if (isSubtreePrefix(descriptors, segments)) return {
@@ -1,5 +1,5 @@
1
1
  import { lexicalSubSchema } from "./lexical.mjs";
2
- import { blockOf, blockSlugsOf, describeFields, findBlocksField, findRichTextField, splitPath } from "./walk.mjs";
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
- * unusable rich text nodes. Required-ness, row counts, enum membership and
127
- * relationship existence stay with Payload, which already checks them and
128
- * reports them per field. Without this pass a misspelled field inside a new
129
- * block would be stripped in silence.
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 = describeFields(scope.fields).flatMap((descriptor) => {
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,
@@ -1 +1,3 @@
1
+ import "./lexical.mjs";
2
+ import "../i18n.mjs";
1
3
  import "payload";
@@ -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
- * The `admin.description` of a field or collection, when it is serializable:
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, path, readOnly) => {
69
- const descriptor = describeBase(field, path, readOnly);
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 === "richText") descriptor.nodes = allowedNodeTypes(field);
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. The walk stops at every blocks field and
88
- * names the slugs instead of descending, which keeps a node proportional to
89
- * the number of blocks it allows rather than to the size of their definitions.
90
- */ const describeFields = (fields, prefix = [], parentReadOnly = false) => fields.flatMap((field) => {
91
- if (isSkipped(field)) return [];
92
- const readOnly = parentReadOnly || isReadOnly(field);
93
- const path = [...prefix, field.name];
94
- if (field.type === "tab" || field.type === "group") return describeFields(field.flattenedFields, path, readOnly);
95
- if (field.type === "array") return describeFields(field.flattenedFields, [...path, "*"], readOnly);
96
- if (field.type === "blocks") return [withRows({
97
- ...describeBase(field, joinPath(path), readOnly),
98
- blocks: blockSlugsOf(field)
99
- }, field)];
100
- return [describeLeaf(field, joinPath(path), readOnly)];
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, staticDescription, targetOf };
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 { describeNode, reachableSchemaPaths } from "../schema/describe.mjs";
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 { staticDescription } from "../schema/walk.mjs";
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 = staticDescription(config.admin.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 = staticDescription(config.admin.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),
@@ -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
- if (typeof resolved === "string") return resolved;
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 };
@@ -1,4 +1,4 @@
1
- import { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, describeFields, findBlocksField, splitPath } from "../schema/walk.mjs";
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 = describeFields(fields).flatMap((descriptor) => {
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.6",
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",