@abinnovision/payloadcms-mcpx 1.1.0-beta.6 → 2.0.0-beta.12
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 +144 -470
- package/dist/admin/capability-matrix.d.mts +33 -0
- package/dist/admin/capability-matrix.mjs +354 -0
- package/dist/admin/capability-toggles.mjs +73 -0
- package/dist/{client → admin}/setup-guide.d.mts +4 -2
- package/dist/{client → admin}/setup-guide.mjs +3 -10
- package/dist/api-keys/capability-matrix.d.mts +12 -8
- package/dist/api-keys/capability-matrix.mjs +14 -86
- package/dist/api-keys/collection.mjs +2 -1
- package/dist/api-keys/fields.mjs +36 -24
- package/dist/auth/resolve.mjs +71 -23
- package/dist/{tools/names.mjs → builtin-tool-names.mjs} +1 -1
- package/dist/capabilities.mjs +44 -46
- package/dist/define-tool.d.mts +23 -0
- package/dist/{types.mjs → define-tool.mjs} +1 -1
- package/dist/endpoint/errors.mjs +5 -6
- package/dist/endpoint/handler.mjs +73 -29
- package/dist/endpoint/server.mjs +85 -22
- package/dist/endpoint/upload.mjs +135 -0
- package/dist/entity.d.mts +9 -0
- package/dist/guards.mjs +7 -0
- package/dist/i18n.mjs +10 -8
- package/dist/index.d.mts +5 -2
- package/dist/index.mjs +3 -2
- package/dist/options.mjs +54 -47
- package/dist/plugin.d.mts +2 -2
- package/dist/plugin.mjs +13 -3
- package/dist/request.d.mts +2 -3
- package/dist/request.mjs +2 -3
- package/dist/result.d.mts +2 -3
- package/dist/result.mjs +3 -3
- package/dist/schema/admin-hidden.mjs +91 -0
- package/dist/schema/describe.mjs +17 -33
- package/dist/schema/errors.mjs +8 -0
- package/dist/schema/index.mjs +5 -2
- package/dist/schema/lexical-pointer.mjs +18 -20
- package/dist/schema/lexical.mjs +44 -93
- package/dist/schema/outline.mjs +5 -15
- package/dist/schema/path.mjs +28 -0
- package/dist/schema/pointer.mjs +18 -25
- package/dist/schema/shape.mjs +25 -71
- package/dist/schema/walk.mjs +43 -45
- package/dist/tools/builtin.mjs +4 -4
- package/dist/tools/create-document.mjs +50 -24
- package/dist/tools/describe-schema.mjs +38 -34
- package/dist/tools/document.mjs +124 -0
- package/dist/tools/find-documents.mjs +32 -19
- package/dist/tools/find-versions.mjs +22 -18
- package/dist/tools/get-document.mjs +60 -33
- package/dist/tools/list-capabilities.mjs +35 -19
- package/dist/tools/patch-document.mjs +122 -66
- package/dist/tools/publish-document.mjs +26 -32
- package/dist/tools/query-paths.mjs +57 -0
- package/dist/tools/read-request.d.mts +10 -0
- package/dist/tools/read-request.mjs +43 -0
- package/dist/tools/shared.mjs +74 -105
- package/dist/tools/validate-document.mjs +18 -30
- package/dist/tools/versions.mjs +37 -20
- package/dist/types.d.mts +135 -75
- package/dist/upload/file.mjs +166 -0
- package/dist/upload/grant.mjs +96 -0
- package/dist/version.mjs +3 -1
- package/dist/write/draft-guard.mjs +23 -41
- package/dist/write/patch.mjs +27 -183
- package/dist/write/publish-blockers.mjs +7 -14
- package/dist/write/publish-intent.mjs +6 -4
- package/dist/write/row-ids.mjs +46 -0
- package/dist/write/transaction.mjs +5 -8
- package/dist/write/write-data.mjs +64 -0
- package/package.json +11 -10
- package/dist/client/capability-matrix.d.mts +0 -26
- package/dist/client/capability-matrix.mjs +0 -349
- package/dist/endpoint/index.mjs +0 -4
- package/dist/tools/target.mjs +0 -45
- /package/dist/{client → admin}/index.d.mts +0 -0
- /package/dist/{client → admin}/index.mjs +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { isPlainObject } from "../guards.mjs";
|
|
2
|
+
import { RESERVED_FIELD_NAMES, ROW_KEYS, blockOf, blockSlugsOf, isAdminHidden, schemaOf } from "./walk.mjs";
|
|
3
|
+
//#region src/schema/admin-hidden.ts
|
|
4
|
+
const UPLOAD_BASE_FIELDS = /* @__PURE__ */ new Set([
|
|
5
|
+
"filename",
|
|
6
|
+
"filesize",
|
|
7
|
+
"focalX",
|
|
8
|
+
"focalY",
|
|
9
|
+
"height",
|
|
10
|
+
"mimeType",
|
|
11
|
+
"sizes",
|
|
12
|
+
"thumbnailURL",
|
|
13
|
+
"url",
|
|
14
|
+
"width"
|
|
15
|
+
]);
|
|
16
|
+
const NONE = /* @__PURE__ */ new Set();
|
|
17
|
+
const isPayloadKey = (name) => RESERVED_FIELD_NAMES.has(name) || ROW_KEYS.has(name);
|
|
18
|
+
const collectionOf = (config, slug) => config.collections.find((collection) => collection.slug === slug);
|
|
19
|
+
const exemptOf = (collection) => collection?.upload ? UPLOAD_BASE_FIELDS : NONE;
|
|
20
|
+
const mapEach = (value, one) => Array.isArray(value) ? value.map(one) : one(value);
|
|
21
|
+
const stripFields = (config, fields, doc, exempt = NONE) => {
|
|
22
|
+
const result = { ...doc };
|
|
23
|
+
for (const field of fields) {
|
|
24
|
+
if (!("name" in field) || exempt.has(field.name) || isPayloadKey(field.name) || !Object.hasOwn(result, field.name)) continue;
|
|
25
|
+
if (isAdminHidden(field)) Reflect.deleteProperty(result, field.name);
|
|
26
|
+
else result[field.name] = stripValue(config, field, result[field.name]);
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
};
|
|
30
|
+
const stripRelated = (config, relationTo, value) => {
|
|
31
|
+
if (!isPlainObject(value)) return value;
|
|
32
|
+
if (Array.isArray(relationTo)) {
|
|
33
|
+
const slug = value["relationTo"];
|
|
34
|
+
return typeof slug === "string" && isPlainObject(value["value"]) ? {
|
|
35
|
+
...value,
|
|
36
|
+
value: stripRelated(config, slug, value["value"])
|
|
37
|
+
} : value;
|
|
38
|
+
}
|
|
39
|
+
const collection = collectionOf(config, relationTo);
|
|
40
|
+
return collection ? stripFields(config, collection.flattenedFields, value, exemptOf(collection)) : value;
|
|
41
|
+
};
|
|
42
|
+
const stripValue = (config, field, value) => {
|
|
43
|
+
switch (field.type) {
|
|
44
|
+
case "group":
|
|
45
|
+
case "tab": return isPlainObject(value) ? stripFields(config, field.flattenedFields, value) : value;
|
|
46
|
+
case "array": return mapEach(value, (row) => isPlainObject(row) ? stripFields(config, field.flattenedFields, row) : row);
|
|
47
|
+
case "blocks": return mapEach(value, (row) => {
|
|
48
|
+
const block = isPlainObject(row) && typeof row["blockType"] === "string" ? blockOf(config, field, row["blockType"]) : void 0;
|
|
49
|
+
return block && isPlainObject(row) ? stripFields(config, block.flattenedFields, row) : row;
|
|
50
|
+
});
|
|
51
|
+
case "relationship":
|
|
52
|
+
case "upload": return mapEach(value, (item) => stripRelated(config, field.relationTo, item));
|
|
53
|
+
case "join": return isPlainObject(value) && Array.isArray(value["docs"]) ? {
|
|
54
|
+
...value,
|
|
55
|
+
docs: value["docs"].map((item) => stripRelated(config, field.collection, item))
|
|
56
|
+
} : value;
|
|
57
|
+
default: return value;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The document without its `admin.hidden` fields, at every depth, including
|
|
62
|
+
* populated documents, which are read against their own collection. Payload's
|
|
63
|
+
* upload fields stay. Rich text is not walked.
|
|
64
|
+
*/ const stripAdminHidden = (config, ref, doc) => stripFields(config, schemaOf(config, ref).flattenedFields, doc, ref.kind === "collection" ? exemptOf(collectionOf(config, ref.slug)) : NONE);
|
|
65
|
+
/**
|
|
66
|
+
* Whether a query path inside one collection names an `admin.hidden` field or
|
|
67
|
+
* a field under one. The path is dotted, with a locale segment allowed after a
|
|
68
|
+
* localized field. Hops into other collections are the caller's to follow.
|
|
69
|
+
*/ const addressesAdminHidden = (config, collectionSlug, path) => {
|
|
70
|
+
const collection = collectionOf(config, collectionSlug);
|
|
71
|
+
if (!collection) return false;
|
|
72
|
+
const locales = config.localization ? config.localization.localeCodes : [];
|
|
73
|
+
const walk = (fields, segments) => {
|
|
74
|
+
const [segment, ...rest] = segments;
|
|
75
|
+
if (segment === void 0 || isPayloadKey(segment)) return false;
|
|
76
|
+
const matches = fields.filter((field) => "name" in field && field.name === segment);
|
|
77
|
+
if (matches.length === 0) return locales.includes(segment) ? walk(fields, rest) : false;
|
|
78
|
+
return matches.some((field) => {
|
|
79
|
+
if (isAdminHidden(field)) return true;
|
|
80
|
+
if (field.type === "group" || field.type === "tab" || field.type === "array") return walk(field.flattenedFields, rest);
|
|
81
|
+
return field.type === "blocks" && blockSlugsOf(field).some((slug) => {
|
|
82
|
+
const block = blockOf(config, field, slug);
|
|
83
|
+
return block !== void 0 && walk(block.flattenedFields, rest);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
const segments = path.split(".");
|
|
88
|
+
return !exemptOf(collection).has(segments[0] ?? "") && walk(collection.flattenedFields, segments);
|
|
89
|
+
};
|
|
90
|
+
//#endregion
|
|
91
|
+
export { addressesAdminHidden, stripAdminHidden };
|
package/dist/schema/describe.mjs
CHANGED
|
@@ -1,68 +1,55 @@
|
|
|
1
1
|
import { lexicalSubSchema, subSchemaNodeTypes } from "./lexical.mjs";
|
|
2
|
+
import { joinPath, splitPath } from "./path.mjs";
|
|
2
3
|
import { translateAny } from "../i18n.mjs";
|
|
3
|
-
import { blockOf, blockSlugsOf, describeFields, findBlocksField, findRichTextField,
|
|
4
|
+
import { blockOf, blockSlugsOf, describeFields, findBlocksField, findRichTextField, schemaOf } from "./walk.mjs";
|
|
5
|
+
import { SchemaError } from "./errors.mjs";
|
|
4
6
|
//#region src/schema/describe.ts
|
|
5
|
-
|
|
7
|
+
const matchSchemaSegments = (descriptors, remaining) => descriptors.map((descriptor) => splitPath(descriptor.path)).filter((parts) => parts.every((part, offset) => part === remaining[offset])).sort((left, right) => right.length - left.length)[0];
|
|
6
8
|
const stepThroughBlocks = ({ config, fields, match, remaining }) => {
|
|
7
9
|
const field = findBlocksField(fields, match);
|
|
8
10
|
if (!field) throw new Error(`"${joinPath(match)}" could not be resolved.`);
|
|
9
11
|
const slug = remaining.at(match.length);
|
|
10
|
-
if (slug === void 0) throw new
|
|
12
|
+
if (slug === void 0) throw new SchemaError(`"${joinPath(match)}" is a blocks field; append one of: ${blockSlugsOf(field).join(", ")}`);
|
|
11
13
|
const block = blockOf(config, field, slug);
|
|
12
|
-
if (!block) throw new
|
|
14
|
+
if (!block) throw new SchemaError(`"${slug}" is not allowed at "${joinPath(match)}". Allowed: ${blockSlugsOf(field).join(", ")}`);
|
|
13
15
|
return {
|
|
14
16
|
blockType: slug,
|
|
15
17
|
fields: block.flattenedFields,
|
|
16
18
|
rest: remaining.slice(match.length + 1)
|
|
17
19
|
};
|
|
18
20
|
};
|
|
19
|
-
|
|
20
|
-
* A node that picks a block by slug takes one segment more, so `/content/block`
|
|
21
|
-
* addresses the choice and `/content/block/callout` the definition. Everything
|
|
22
|
-
* else, a link node being the usual case, resolves in a single segment.
|
|
23
|
-
*/ const stepThroughLexical = ({ config, fields, match, remaining }) => {
|
|
21
|
+
const stepThroughLexical = ({ config, fields, match, remaining }) => {
|
|
24
22
|
const field = findRichTextField(fields, match);
|
|
25
23
|
if (!field) throw new Error(`"${joinPath(match)}" could not be resolved.`);
|
|
26
24
|
const available = subSchemaNodeTypes(field).join(", ") || "none";
|
|
27
25
|
const nodeType = remaining.at(match.length);
|
|
28
|
-
if (nodeType === void 0) throw new
|
|
26
|
+
if (nodeType === void 0) throw new SchemaError(`"${joinPath(match)}" is a rich text field; append one of: ${available}`);
|
|
29
27
|
const sub = lexicalSubSchema(field, nodeType);
|
|
30
28
|
const reached = joinPath([...match, nodeType]);
|
|
31
|
-
if (!sub) throw new
|
|
29
|
+
if (!sub) throw new SchemaError(`"${nodeType}" carries no fields in this field's editor. Node types with fields here: ${available}`);
|
|
32
30
|
if (sub.kind === "fields") return {
|
|
33
31
|
fields: sub.fields,
|
|
34
32
|
rest: remaining.slice(match.length + 1)
|
|
35
33
|
};
|
|
36
34
|
const slug = remaining.at(match.length + 1);
|
|
37
35
|
const slugs = blockSlugsOf(sub.blocksField).join(", ");
|
|
38
|
-
if (slug === void 0) throw new
|
|
36
|
+
if (slug === void 0) throw new SchemaError(`"${reached}" selects a block; append one of: ${slugs}`);
|
|
39
37
|
const block = blockOf(config, sub.blocksField, slug);
|
|
40
|
-
if (!block) throw new
|
|
38
|
+
if (!block) throw new SchemaError(`"${slug}" is not allowed at "${reached}". Allowed: ${slugs}`);
|
|
41
39
|
return {
|
|
42
40
|
blockType: slug,
|
|
43
41
|
fields: block.flattenedFields,
|
|
44
42
|
rest: remaining.slice(match.length + 2)
|
|
45
43
|
};
|
|
46
44
|
};
|
|
47
|
-
|
|
48
|
-
* A schema path alternates a blocks field's own path with the slug of one of
|
|
49
|
-
* the blocks it accepts, so `/layout/sections/sectionWrapper/modules/hero`
|
|
50
|
-
* reaches `hero` as it exists under `pages` specifically. The slug sits where
|
|
51
|
-
* a pointer into a document would carry the element's index. A rich text
|
|
52
|
-
* field's path continues the same way, naming a Lexical node type and, for the
|
|
53
|
-
* block nodes, the slug it holds.
|
|
54
|
-
*/ const fieldsAtSchemaPath = (config, target, schemaPath) => {
|
|
45
|
+
const fieldsAtSchemaPath = (config, target, schemaPath) => {
|
|
55
46
|
let fields = target.flattenedFields;
|
|
56
47
|
let blockType;
|
|
57
48
|
let remaining = splitPath(schemaPath);
|
|
58
49
|
while (remaining.length > 0) {
|
|
59
50
|
const descendable = describeFields(fields).filter((descriptor) => descriptor.type === "blocks" || descriptor.type === "richText");
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
* the longest matching one is taken. Blocks and rich text fields are both
|
|
63
|
-
* leaves of the walk, so no two of these paths overlap.
|
|
64
|
-
*/ const match = longestMatch(descendable, remaining);
|
|
65
|
-
if (!match) throw new Error(`"${joinPath(remaining)}" does not address a blocks or rich text field. Available here: ${descendable.map((descriptor) => descriptor.path).join(", ") || "none"}`);
|
|
51
|
+
const match = matchSchemaSegments(descendable, remaining);
|
|
52
|
+
if (!match) throw new SchemaError(`"${joinPath(remaining)}" does not address a blocks or rich text field. Available here: ${descendable.map((descriptor) => descriptor.path).join(", ") || "none"}`);
|
|
66
53
|
const at = {
|
|
67
54
|
config,
|
|
68
55
|
fields,
|
|
@@ -79,10 +66,7 @@ const stepThroughBlocks = ({ config, fields, match, remaining }) => {
|
|
|
79
66
|
fields
|
|
80
67
|
};
|
|
81
68
|
};
|
|
82
|
-
|
|
83
|
-
* Where a descriptor can be drilled into: one branch per block a blocks field
|
|
84
|
-
* accepts, and one per Lexical node type that carries fields.
|
|
85
|
-
*/ const branchesOf = (fields, descriptor, schemaPath) => {
|
|
69
|
+
const branchesOf = (fields, descriptor, schemaPath) => {
|
|
86
70
|
const base = `${schemaPath}${descriptor.path}`;
|
|
87
71
|
if (descriptor.type === "richText") {
|
|
88
72
|
const field = findRichTextField(fields, splitPath(descriptor.path));
|
|
@@ -108,7 +92,7 @@ const stepThroughBlocks = ({ config, fields, match, remaining }) => {
|
|
|
108
92
|
* Curried on the translator that resolves each `admin.description`, so a
|
|
109
93
|
* request binds its language once and the walk stays request-free.
|
|
110
94
|
*/ const nodeDescriber = (translate = translateAny) => (config, ref, schemaPath = "") => {
|
|
111
|
-
const { blockType, fields } = fieldsAtSchemaPath(config,
|
|
95
|
+
const { blockType, fields } = fieldsAtSchemaPath(config, schemaOf(config, ref), schemaPath);
|
|
112
96
|
const descriptors = describeFields(fields, translate);
|
|
113
97
|
const next = descriptors.flatMap((descriptor) => branchesOf(fields, descriptor, schemaPath).map((branch) => branch.path));
|
|
114
98
|
return {
|
|
@@ -132,7 +116,7 @@ const stepThroughBlocks = ({ config, fields, match, remaining }) => {
|
|
|
132
116
|
return;
|
|
133
117
|
}
|
|
134
118
|
seen.push(schemaPath);
|
|
135
|
-
const { fields } = fieldsAtSchemaPath(config,
|
|
119
|
+
const { fields } = fieldsAtSchemaPath(config, schemaOf(config, ref), schemaPath);
|
|
136
120
|
for (const descriptor of describeFields(fields)) for (const branch of branchesOf(fields, descriptor, schemaPath)) {
|
|
137
121
|
if (visited.includes(branch.token)) continue;
|
|
138
122
|
walk(branch.path, [...visited, branch.token]);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/schema/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* A schema lookup the client got wrong, such as a path no field answers to.
|
|
4
|
+
* The message is written for the client and names what to send instead.
|
|
5
|
+
*/ var SchemaError = class extends Error {};
|
|
6
|
+
SchemaError.prototype.name = "SchemaError";
|
|
7
|
+
//#endregion
|
|
8
|
+
export { SchemaError };
|
package/dist/schema/index.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { REQUIRED_NODE_PROPERTIES, ROOT_PROPERTIES, allowedNodeTypes, constrainsFields, lexicalSubSchema, nodeOptions, nodeProblems, nodePropertiesFor, propertyProblem, rootProblems, subSchemaNodeTypes } from "./lexical.mjs";
|
|
2
|
-
import { JSON_POINTER_PATTERN,
|
|
2
|
+
import { JSON_POINTER_PATTERN, isIndexSegment, joinPath, pointerFromPayloadPath, prototypeSegmentProblem, splitPath } from "./path.mjs";
|
|
3
|
+
import { RESERVED_FIELD_NAMES, ROW_KEYS, blockOf, blockSlugsOf, describeAddressableFields, describeFields, descriptorsUnder, findBlocksField, findRichTextField, isAdminHidden, schemaOf } from "./walk.mjs";
|
|
4
|
+
import { addressesAdminHidden, stripAdminHidden } from "./admin-hidden.mjs";
|
|
5
|
+
import { SchemaError } from "./errors.mjs";
|
|
3
6
|
import { nodeDescriber, reachableSchemaPaths } from "./describe.mjs";
|
|
4
7
|
import { resolveLexicalPointer } from "./lexical-pointer.mjs";
|
|
5
8
|
import { lexicalOutline } from "./outline.mjs";
|
|
6
9
|
import { resolveDataPointer } from "./pointer.mjs";
|
|
7
10
|
import { EMPTY_ROOT, validateWriteValue } from "./shape.mjs";
|
|
8
|
-
export { EMPTY_ROOT, JSON_POINTER_PATTERN, REQUIRED_NODE_PROPERTIES, RESERVED_FIELD_NAMES, ROOT_PROPERTIES, allowedNodeTypes, blockOf, blockSlugsOf, constrainsFields, describeAddressableFields, describeFields, findBlocksField, findRichTextField,
|
|
11
|
+
export { EMPTY_ROOT, JSON_POINTER_PATTERN, REQUIRED_NODE_PROPERTIES, RESERVED_FIELD_NAMES, ROOT_PROPERTIES, ROW_KEYS, SchemaError, addressesAdminHidden, allowedNodeTypes, blockOf, blockSlugsOf, constrainsFields, describeAddressableFields, describeFields, descriptorsUnder, findBlocksField, findRichTextField, isAdminHidden, isIndexSegment, joinPath, lexicalOutline, lexicalSubSchema, nodeDescriber, nodeOptions, nodeProblems, nodePropertiesFor, pointerFromPayloadPath, propertyProblem, prototypeSegmentProblem, reachableSchemaPaths, resolveDataPointer, resolveLexicalPointer, rootProblems, schemaOf, splitPath, stripAdminHidden, subSchemaNodeTypes, validateWriteValue };
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
+
import { isPlainObject } from "../guards.mjs";
|
|
1
2
|
import { lexicalSubSchema, subSchemaNodeTypes } from "./lexical.mjs";
|
|
2
|
-
import {
|
|
3
|
+
import { isIndexSegment, joinPath } from "./path.mjs";
|
|
4
|
+
import { blockOf, blockSlugsOf } from "./walk.mjs";
|
|
5
|
+
import { SchemaError } from "./errors.mjs";
|
|
3
6
|
//#region src/schema/lexical-pointer.ts
|
|
4
|
-
|
|
5
|
-
* A node's `fields` is ordinary Payload field-land, reached either through the
|
|
6
|
-
* schema a feature declares for the node or, where the node picks a block by
|
|
7
|
-
* slug, through that block.
|
|
8
|
-
*/ const stepIntoFields = (at) => {
|
|
7
|
+
const stepIntoFields = (at) => {
|
|
9
8
|
const { addedValue, config, field, node, nodeType, rest } = at;
|
|
10
9
|
const sub = lexicalSubSchema(field, nodeType);
|
|
11
|
-
if (!sub) throw new
|
|
10
|
+
if (!sub) throw new SchemaError(`"${nodeType}" nodes carry no addressable fields in this field's editor. Node types with fields here: ${subSchemaNodeTypes(field).join(", ")}`);
|
|
12
11
|
const data = node?.["fields"];
|
|
13
12
|
if (sub.kind === "fields") return {
|
|
14
13
|
data,
|
|
@@ -18,9 +17,10 @@ import { blockOf, blockSlugsOf, isIndexSegment, isPlainObject, joinPath } from "
|
|
|
18
17
|
};
|
|
19
18
|
const added = addedValue?.fields;
|
|
20
19
|
const slug = (isPlainObject(data) ? data["blockType"] : void 0) ?? (isPlainObject(added) ? added["blockType"] : void 0);
|
|
21
|
-
if (
|
|
20
|
+
if (slug === void 0) throw new SchemaError(`Cannot tell which block a "${nodeType}" node holds. Supply a "blockType" on the value, one of: ${blockSlugsOf(sub.blocksField).join(", ")}`);
|
|
21
|
+
if (typeof slug !== "string") throw new SchemaError(`"blockType" must be a string naming a block in a "${nodeType}" node here. Allowed: ${blockSlugsOf(sub.blocksField).join(", ")}`);
|
|
22
22
|
const block = blockOf(config, sub.blocksField, slug);
|
|
23
|
-
if (!block) throw new
|
|
23
|
+
if (!block) throw new SchemaError(`"${slug}" is not allowed in a "${nodeType}" node here. Allowed: ${blockSlugsOf(sub.blocksField).join(", ")}`);
|
|
24
24
|
return {
|
|
25
25
|
blockType: slug,
|
|
26
26
|
data,
|
|
@@ -31,21 +31,19 @@ import { blockOf, blockSlugsOf, isIndexSegment, isPlainObject, joinPath } from "
|
|
|
31
31
|
};
|
|
32
32
|
/**
|
|
33
33
|
* Walks the segments left over once a pointer has reached a rich text field.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* A position the document does not have yet takes its type from the value
|
|
39
|
-
* being added, and is addressable no further.
|
|
34
|
+
* The stored state chooses the branch at every index, as the stored document
|
|
35
|
+
* does at a blocks element: an editor state admits many node shapes at the same
|
|
36
|
+
* position. A position the document does not have yet takes its type from the
|
|
37
|
+
* value being added and is addressable no further.
|
|
40
38
|
*/ const resolveLexicalPointer = (at) => {
|
|
41
39
|
const { addedValue, config, descriptor, field, state } = at;
|
|
42
40
|
const base = {
|
|
43
41
|
descriptor,
|
|
44
42
|
field
|
|
45
43
|
};
|
|
46
|
-
if (!isPlainObject(state) || !isPlainObject(state["root"])) throw new
|
|
44
|
+
if (!isPlainObject(state) || !isPlainObject(state["root"])) throw new SchemaError(`"${descriptor.path}" holds no editor state yet. Write the whole field once, then address positions inside it.`);
|
|
47
45
|
const [entry, ...rest] = at.segments;
|
|
48
|
-
if (entry !== "root") throw new
|
|
46
|
+
if (entry !== "root") throw new SchemaError(`"${String(entry)}" is not a position in a rich text field. An editor state is entered at "root", e.g. "${descriptor.path}/root/children/0". getDocument with "outline" lists every position this field holds.`);
|
|
49
47
|
let node = state["root"];
|
|
50
48
|
let nodeType = "root";
|
|
51
49
|
let segments = rest;
|
|
@@ -72,7 +70,7 @@ import { blockOf, blockSlugsOf, isIndexSegment, isPlainObject, joinPath } from "
|
|
|
72
70
|
}
|
|
73
71
|
};
|
|
74
72
|
const [index, ...beyond] = remaining;
|
|
75
|
-
if (!isIndexSegment(index)) throw new
|
|
73
|
+
if (!isIndexSegment(index)) throw new SchemaError(`"${descriptor.path}${joinPath([...walked, "children"])}" is a list; "${index}" is not an index. getDocument with "outline" reports the pointer of each node in it.`);
|
|
76
74
|
const children = node?.["children"];
|
|
77
75
|
const child = Array.isArray(children) && index !== "-" ? children[Number(index)] : void 0;
|
|
78
76
|
const type = isPlainObject(child) ? child["type"] : addedValue?.type;
|
|
@@ -84,7 +82,7 @@ import { blockOf, blockSlugsOf, isIndexSegment, isPlainObject, joinPath } from "
|
|
|
84
82
|
kind: "node"
|
|
85
83
|
}
|
|
86
84
|
};
|
|
87
|
-
throw new
|
|
85
|
+
throw new SchemaError(`Cannot tell which node "${descriptor.path}${joinPath([
|
|
88
86
|
...walked,
|
|
89
87
|
"children",
|
|
90
88
|
index
|
|
@@ -108,7 +106,7 @@ import { blockOf, blockSlugsOf, isIndexSegment, isPlainObject, joinPath } from "
|
|
|
108
106
|
nodeType,
|
|
109
107
|
rest: remaining
|
|
110
108
|
});
|
|
111
|
-
if (remaining.length > 0) throw new
|
|
109
|
+
if (remaining.length > 0) throw new SchemaError(`"${segment}" is a property of a "${nodeType}" node and nothing beneath it can be addressed.`);
|
|
112
110
|
return {
|
|
113
111
|
kind: "position",
|
|
114
112
|
position: {
|
package/dist/schema/lexical.mjs
CHANGED
|
@@ -1,27 +1,19 @@
|
|
|
1
|
+
import { isPlainObject, ownValue } from "../guards.mjs";
|
|
1
2
|
import { flattenAllFields } from "payload";
|
|
2
3
|
//#region src/schema/lexical.ts
|
|
3
|
-
|
|
4
|
-
* `editorConfig.features.nodes` lists only what a feature contributed, so a
|
|
5
|
-
* field whose editor enables nothing but text formatting reports none at all.
|
|
6
|
-
*/ const LEXICAL_CORE_NODES = [
|
|
4
|
+
const LEXICAL_CORE_NODES = [
|
|
7
5
|
"root",
|
|
8
6
|
"paragraph",
|
|
9
7
|
"text",
|
|
10
8
|
"linebreak",
|
|
11
9
|
"tab"
|
|
12
10
|
];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* node, `upload` answers with every enabled collection's upload fields
|
|
16
|
-
* concatenated, so the result describes no single position.
|
|
17
|
-
*/ const OPAQUE_NODE_TYPES = /* @__PURE__ */ new Set(["upload"]);
|
|
18
|
-
/**
|
|
19
|
-
* `null` records a node type that was asked and has nothing to describe, so it
|
|
20
|
-
* is asked only once. Worth caching because describe and validate resolve the
|
|
21
|
-
* same field repeatedly and the block features rebuild their answer every call.
|
|
22
|
-
*/ const subSchemaCache = /* @__PURE__ */ new WeakMap();
|
|
11
|
+
const OPAQUE_NODE_TYPES = /* @__PURE__ */ new Set(["upload"]);
|
|
12
|
+
const subSchemaCache = /* @__PURE__ */ new WeakMap();
|
|
23
13
|
const featuresOf = (field) => field.editor?.editorConfig?.features;
|
|
24
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Editors other than Lexical report only the core nodes.
|
|
16
|
+
*/ const allowedNodeTypes = (field) => {
|
|
25
17
|
const registered = (featuresOf(field)?.nodes ?? []).flatMap((entry) => {
|
|
26
18
|
const type = entry.node?.getType?.();
|
|
27
19
|
return type ? [type] : [];
|
|
@@ -51,11 +43,10 @@ const lexicalSubSchema = (field, nodeType) => {
|
|
|
51
43
|
if (!cached.has(nodeType)) cached.set(nodeType, resolveSubSchema(field, nodeType));
|
|
52
44
|
return cached.get(nodeType) ?? void 0;
|
|
53
45
|
};
|
|
54
|
-
/** In the order their features registered them. */ const subSchemaNodeTypes = (field) => [...featuresOf(field)?.getSubFields?.keys() ?? []].filter((nodeType) => lexicalSubSchema(field, nodeType) !== void 0);
|
|
55
46
|
/**
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
* In the order their features registered them.
|
|
48
|
+
*/ const subSchemaNodeTypes = (field) => [...featuresOf(field)?.getSubFields?.keys() ?? []].filter((nodeType) => lexicalSubSchema(field, nodeType) !== void 0);
|
|
49
|
+
const KINDS = {
|
|
59
50
|
array: {
|
|
60
51
|
accepts: (value) => Array.isArray(value),
|
|
61
52
|
needs: "an array"
|
|
@@ -69,11 +60,11 @@ const lexicalSubSchema = (field, nodeType) => {
|
|
|
69
60
|
needs: "a number"
|
|
70
61
|
},
|
|
71
62
|
object: {
|
|
72
|
-
accepts:
|
|
63
|
+
accepts: isPlainObject,
|
|
73
64
|
needs: "an object"
|
|
74
65
|
},
|
|
75
66
|
optionalObject: {
|
|
76
|
-
accepts: (value) => value === null ||
|
|
67
|
+
accepts: (value) => value === null || isPlainObject(value),
|
|
77
68
|
needs: "an object or null"
|
|
78
69
|
},
|
|
79
70
|
string: {
|
|
@@ -88,26 +79,17 @@ const ELEMENT_PROPERTIES = {
|
|
|
88
79
|
direction: "direction",
|
|
89
80
|
indent: "number"
|
|
90
81
|
};
|
|
91
|
-
|
|
82
|
+
const TEXT_PROPERTIES = {
|
|
92
83
|
detail: "number",
|
|
93
84
|
format: "number",
|
|
94
85
|
mode: "string",
|
|
95
86
|
style: "string",
|
|
96
87
|
text: "string"
|
|
97
88
|
};
|
|
89
|
+
const UNIVERSAL_PROPERTIES = { version: "number" };
|
|
98
90
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* Aligned with Payload rather than measured: its `outputSchema` declares `type`
|
|
102
|
-
* and `version` required for every node in the tree, and that declaration is
|
|
103
|
-
* what types the field in `payload-types.ts`. Lexical itself hydrates a node
|
|
104
|
-
* without a `version`, or with the wrong kind of one, unchanged - but a
|
|
105
|
-
* consumer reading the document through the generated types has been promised
|
|
106
|
-
* an integer, and `BlockNode.importJSON` migrates on it.
|
|
107
|
-
*/ const UNIVERSAL_PROPERTIES = { version: "number" };
|
|
108
|
-
/**
|
|
109
|
-
* The root, as Payload declares it and as an editor exports it: these six
|
|
110
|
-
* properties, these kinds, and nothing else.
|
|
91
|
+
* The root as Payload declares it and an editor exports it: these six
|
|
92
|
+
* properties, these kinds, nothing else.
|
|
111
93
|
*/ const ROOT_PROPERTIES = {
|
|
112
94
|
children: "array",
|
|
113
95
|
direction: "direction",
|
|
@@ -118,20 +100,11 @@ const ELEMENT_PROPERTIES = {
|
|
|
118
100
|
};
|
|
119
101
|
/**
|
|
120
102
|
* What a serialized node must carry beyond {@link UNIVERSAL_PROPERTIES}, keyed
|
|
121
|
-
* by node type.
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* table is measured instead: an entry belongs here only if breaking it makes
|
|
127
|
-
* Lexical throw, or changes what the editor reads back. An element's `format`
|
|
128
|
-
* and a paragraph's text defaults are absent for that reason.
|
|
129
|
-
* `lexical.spec.ts` holds every entry to the rule against the node classes
|
|
130
|
-
* `@payloadcms/richtext-lexical` ships, so extend that test first.
|
|
131
|
-
*
|
|
132
|
-
* A node type with no entry is checked for the universal properties only.
|
|
133
|
-
* Guessing at the requirements of a project's own nodes would reject content
|
|
134
|
-
* that works.
|
|
103
|
+
* by node type. Payload stores a state without hydrating it, so a node missing
|
|
104
|
+
* these fails only later, in the admin editor. Payload declares nothing per
|
|
105
|
+
* node type, so the table is measured: an entry belongs only if breaking it
|
|
106
|
+
* makes Lexical throw or changes what the editor reads back. A type with no
|
|
107
|
+
* entry is not guessed at, since that would reject a project's working nodes.
|
|
135
108
|
*/ const REQUIRED_NODE_PROPERTIES = {
|
|
136
109
|
autolink: {
|
|
137
110
|
...ELEMENT_PROPERTIES,
|
|
@@ -175,31 +148,22 @@ const ELEMENT_PROPERTIES = {
|
|
|
175
148
|
}
|
|
176
149
|
};
|
|
177
150
|
/**
|
|
178
|
-
* Whether the table already says what a node type's `fields`
|
|
179
|
-
* sub-field walk does not report the same problem
|
|
180
|
-
*/ const constrainsFields = (type) => "fields" in (REQUIRED_NODE_PROPERTIES
|
|
151
|
+
* Whether the table already says what a node type's `fields` must be, so the
|
|
152
|
+
* sub-field walk does not report the same problem twice.
|
|
153
|
+
*/ const constrainsFields = (type) => "fields" in (ownValue(REQUIRED_NODE_PROPERTIES, type) ?? {});
|
|
181
154
|
const describeConstraints = (constraints) => Object.fromEntries(Object.entries(constraints).map(([property, constraint]) => [property, needs(constraint)]).sort((left, right) => left[0].localeCompare(right[0])));
|
|
182
155
|
/**
|
|
183
|
-
* What each
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
* Keyed by node type rather than reported per field, because that is what it
|
|
189
|
-
* depends on. A field says which types it allows, in its `nodes`; what a `text`
|
|
190
|
-
* node has to carry is the same wherever one is written, so a response that
|
|
191
|
-
* describes twenty rich text fields still states it once.
|
|
192
|
-
*
|
|
193
|
-
* `type` is listed although {@link UNIVERSAL_PROPERTIES} omits it. The
|
|
194
|
-
* validator never reports it missing, because a node's `type` is how it finds
|
|
195
|
-
* the entry to check against, but a client assembling a node from this listing
|
|
196
|
-
* still has to write one.
|
|
156
|
+
* What each node type must carry, phrased as the write side phrases its
|
|
157
|
+
* refusals so the two never disagree. Keyed by node type, since a `text` node
|
|
158
|
+
* needs the same wherever it is written. `type` is listed although
|
|
159
|
+
* {@link UNIVERSAL_PROPERTIES} omits it: the validator finds the entry by
|
|
160
|
+
* `type` and so never reports it missing, but a client must still write one.
|
|
197
161
|
*/ const nodePropertiesFor = (types) => Object.fromEntries([...new Set(types)].sort().map((type) => [type, describeConstraints(type === "root" ? ROOT_PROPERTIES : {
|
|
198
|
-
...REQUIRED_NODE_PROPERTIES
|
|
162
|
+
...ownValue(REQUIRED_NODE_PROPERTIES, type),
|
|
199
163
|
...UNIVERSAL_PROPERTIES,
|
|
200
164
|
type: "string"
|
|
201
165
|
})]));
|
|
202
|
-
|
|
166
|
+
const check = (node, constraints) => {
|
|
203
167
|
const problems = {
|
|
204
168
|
missing: [],
|
|
205
169
|
rejected: []
|
|
@@ -215,36 +179,29 @@ const describeConstraints = (constraints) => Object.fromEntries(Object.entries(c
|
|
|
215
179
|
};
|
|
216
180
|
const nodeProblems = (node) => check(node, {
|
|
217
181
|
...UNIVERSAL_PROPERTIES,
|
|
218
|
-
...REQUIRED_NODE_PROPERTIES
|
|
182
|
+
...ownValue(REQUIRED_NODE_PROPERTIES, node["type"])
|
|
219
183
|
});
|
|
220
184
|
/**
|
|
221
185
|
* The root is the one node Payload describes itself, down to refusing an
|
|
222
|
-
* unknown property, so it is checked against that description
|
|
223
|
-
*
|
|
186
|
+
* unknown property, so it is checked against that description instead of the
|
|
187
|
+
* walk's table.
|
|
224
188
|
*/ const rootProblems = (root) => ({
|
|
225
189
|
...check(root, ROOT_PROPERTIES),
|
|
226
|
-
unexpected: Object.keys(root).filter((property) => !(property
|
|
190
|
+
unexpected: Object.keys(root).filter((property) => !Object.hasOwn(ROOT_PROPERTIES, property))
|
|
227
191
|
});
|
|
228
192
|
/**
|
|
229
|
-
* What one serialized property
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* Absent where the table says nothing, which is the same tolerance the node
|
|
233
|
-
* walk shows: a project's own node may carry any property, and guessing at one
|
|
234
|
-
* would reject content that works.
|
|
193
|
+
* What one serialized property must be, for a write addressing a property
|
|
194
|
+
* instead of a whole node. Absent where the table says nothing, matching the
|
|
195
|
+
* node walk: a project's own node may carry any property.
|
|
235
196
|
*/ const propertyProblem = (nodeType, property, value) => {
|
|
236
|
-
const
|
|
197
|
+
const constraints = nodeType === "root" ? ROOT_PROPERTIES : {
|
|
237
198
|
...UNIVERSAL_PROPERTIES,
|
|
238
|
-
...REQUIRED_NODE_PROPERTIES
|
|
239
|
-
}
|
|
199
|
+
...ownValue(REQUIRED_NODE_PROPERTIES, nodeType)
|
|
200
|
+
};
|
|
201
|
+
const constraint = ownValue(constraints, property);
|
|
240
202
|
return constraint === void 0 || accepts(constraint, value) ? void 0 : { needs: needs(constraint) };
|
|
241
203
|
};
|
|
242
|
-
|
|
243
|
-
* Only properties a feature narrows and Lexical does not check on its own
|
|
244
|
-
* belong here. Everything else a feature restricts is already visible: a
|
|
245
|
-
* link's targets through its sub-schema, a block node's choices through the
|
|
246
|
-
* slugs it accepts.
|
|
247
|
-
*/ const NODE_OPTION_SOURCES = [{
|
|
204
|
+
const NODE_OPTION_SOURCES = [{
|
|
248
205
|
defaults: [
|
|
249
206
|
"h1",
|
|
250
207
|
"h2",
|
|
@@ -258,13 +215,7 @@ const nodeProblems = (node) => check(node, {
|
|
|
258
215
|
nodeProp: "tag",
|
|
259
216
|
nodeType: "heading"
|
|
260
217
|
}];
|
|
261
|
-
|
|
262
|
-
* The props a feature was resolved with.
|
|
263
|
-
*
|
|
264
|
-
* Sanitizing the editor drops every feature's props from `editorConfig.features`
|
|
265
|
-
* but leaves them on `resolvedFeatureMap`. A feature that declares no server
|
|
266
|
-
* props keeps only the client ones, so both are tried.
|
|
267
|
-
*/ const featurePropsOf = (field, featureKey) => {
|
|
218
|
+
const featurePropsOf = (field, featureKey) => {
|
|
268
219
|
const resolved = field.editor?.editorConfig?.resolvedFeatureMap?.get(featureKey);
|
|
269
220
|
const props = resolved?.sanitizedServerFeatureProps ?? resolved?.clientFeatureProps;
|
|
270
221
|
return typeof props === "object" && props !== null ? props : void 0;
|
package/dist/schema/outline.mjs
CHANGED
|
@@ -1,24 +1,14 @@
|
|
|
1
|
+
import { isPlainObject, ownValue } from "../guards.mjs";
|
|
1
2
|
import { allowedNodeTypes, nodeOptions } from "./lexical.mjs";
|
|
2
|
-
import { isPlainObject } from "./walk.mjs";
|
|
3
3
|
//#region src/schema/outline.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* document stays a fraction of the size of its editor state.
|
|
7
|
-
*/ const TEXT_PREVIEW_LENGTH = 80;
|
|
8
|
-
/**
|
|
9
|
-
* Every descendant text node contributes, not only direct children, since a
|
|
10
|
-
* link or a formatting mark nests the text a level deeper.
|
|
11
|
-
*/ const collectText = (node) => {
|
|
4
|
+
const TEXT_PREVIEW_LENGTH = 80;
|
|
5
|
+
const collectText = (node) => {
|
|
12
6
|
if (node["type"] === "text") return typeof node["text"] === "string" ? node["text"] : "";
|
|
13
7
|
const children = node["children"];
|
|
14
8
|
return Array.isArray(children) ? children.filter(isPlainObject).map((child) => collectText(child)).join("") : "";
|
|
15
9
|
};
|
|
16
10
|
const preview = (text) => text.length > TEXT_PREVIEW_LENGTH ? `${text.slice(0, TEXT_PREVIEW_LENGTH)}…` : text;
|
|
17
|
-
|
|
18
|
-
* Only the properties a feature actually narrows for this node type, and only
|
|
19
|
-
* where the node carries a string for one. A node missing the property, or
|
|
20
|
-
* carrying something the feature never produces, says nothing worth reporting.
|
|
21
|
-
*/ const narrowedOptions = (node, narrowed) => {
|
|
11
|
+
const narrowedOptions = (node, narrowed) => {
|
|
22
12
|
if (!narrowed) return;
|
|
23
13
|
const set = Object.keys(narrowed).flatMap((property) => {
|
|
24
14
|
const value = node[property];
|
|
@@ -31,7 +21,7 @@ const walk = (node, pointer, options, entries) => {
|
|
|
31
21
|
if (typeof type !== "string") return;
|
|
32
22
|
const version = node["version"];
|
|
33
23
|
const text = preview(collectText(node));
|
|
34
|
-
const nodeOptionsFound = narrowedOptions(node, options
|
|
24
|
+
const nodeOptionsFound = narrowedOptions(node, ownValue(options, type));
|
|
35
25
|
const children = node["children"];
|
|
36
26
|
const childCount = Array.isArray(children) ? children.length : 0;
|
|
37
27
|
entries.push({
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/schema/path.ts
|
|
2
|
+
const JSON_POINTER_PATTERN = /^(\/([^~/]|~[01])*)*$/;
|
|
3
|
+
/**
|
|
4
|
+
* No segments is the root pointer, `""`.
|
|
5
|
+
*/ const joinPath = (parts) => parts.map((part) => `/${part.replace(/~/g, "~0").replace(/\//g, "~1")}`).join("");
|
|
6
|
+
/**
|
|
7
|
+
* Unescapes `~1` and `~0`. The root pointer yields no segments.
|
|
8
|
+
*/ const splitPath = (path) => path.split("/").slice(1).map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
9
|
+
const PROTOTYPE_SEGMENTS = /* @__PURE__ */ new Set([
|
|
10
|
+
"__proto__",
|
|
11
|
+
"constructor",
|
|
12
|
+
"prototype"
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* No field or node property has one of these names. rfc6902 skips them as
|
|
16
|
+
* intermediate tokens but writes them as a final key, so a pointer containing
|
|
17
|
+
* one is refused before anything resolves it.
|
|
18
|
+
*/ const prototypeSegmentProblem = (pointer) => splitPath(pointer).some((segment) => PROTOTYPE_SEGMENTS.has(segment)) ? `"${pointer}" contains a segment named __proto__, constructor or prototype, which no field or node property uses.` : void 0;
|
|
19
|
+
/**
|
|
20
|
+
* `-` included, since RFC 6901 reads it as the position after the last.
|
|
21
|
+
*/ const isIndexSegment = (segment) => segment === "-" || /^\d+$/.test(segment);
|
|
22
|
+
/**
|
|
23
|
+
* A path Payload reports on a validation error (`layout.0.title`) as a JSON
|
|
24
|
+
* Pointer, so everything handed back addresses documents the same way. The
|
|
25
|
+
* path already carries real indices, so it maps directly.
|
|
26
|
+
*/ const pointerFromPayloadPath = (path) => path ? joinPath(path.split(".")) : "";
|
|
27
|
+
//#endregion
|
|
28
|
+
export { JSON_POINTER_PATTERN, isIndexSegment, joinPath, pointerFromPayloadPath, prototypeSegmentProblem, splitPath };
|