@abinnovision/payloadcms-mcpx 1.1.0-beta.6 → 2.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -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 +17 -85
- package/dist/api-keys/collection.mjs +2 -1
- package/dist/api-keys/fields.mjs +36 -24
- package/dist/auth/resolve.mjs +44 -6
- package/dist/{tools/names.mjs → builtin-tool-names.mjs} +1 -1
- package/dist/capabilities.mjs +41 -43
- 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 +49 -19
- package/dist/endpoint/server.mjs +72 -20
- 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 +52 -46
- package/dist/plugin.mjs +0 -1
- 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 +26 -23
- 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 +39 -32
- package/dist/tools/list-capabilities.mjs +23 -16
- package/dist/tools/patch-document.mjs +75 -49
- 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 +67 -99
- package/dist/tools/validate-document.mjs +18 -30
- package/dist/tools/versions.mjs +37 -20
- package/dist/types.d.mts +118 -74
- 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 +6 -5
- 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
package/dist/i18n.mjs
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
//#region src/i18n.ts
|
|
2
2
|
const stringRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string") ? value : void 0;
|
|
3
|
-
|
|
3
|
+
const pick = (record, language) => {
|
|
4
4
|
for (const code of Array.isArray(language) ? language : [language]) {
|
|
5
5
|
const entry = record[code];
|
|
6
6
|
if (entry !== void 0 && entry.trim() !== "") return entry;
|
|
7
7
|
}
|
|
8
8
|
};
|
|
9
9
|
/**
|
|
10
|
-
* Resolves a static label or `admin.description` to
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Resolves a static label or `admin.description` to one string: the request's
|
|
11
|
+
* language, then the configured fallback language, then the first entry the
|
|
12
|
+
* record declares.
|
|
13
13
|
*
|
|
14
14
|
* Anything that is not a string or a string-valued record is dropped. A
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* function or React component description may reach client-only i18n, so it is
|
|
16
|
+
* never invoked.
|
|
17
17
|
*/ const translateStatic = (value, language) => {
|
|
18
18
|
if (typeof value === "string") return value.trim() === "" ? void 0 : value;
|
|
19
19
|
const record = stringRecord(value);
|
|
20
20
|
if (!record) return;
|
|
21
21
|
return pick(record, language.language) ?? pick(record, language.fallbackLanguage) ?? Object.values(record).find((entry) => entry.trim() !== "");
|
|
22
22
|
};
|
|
23
|
-
|
|
24
|
-
/**
|
|
23
|
+
const translatorFor = (i18n) => (value) => translateStatic(value, i18n);
|
|
24
|
+
/**
|
|
25
|
+
* For callers with no request. Both languages miss, so the first entry wins.
|
|
26
|
+
*/ const translateAny = translatorFor({
|
|
25
27
|
fallbackLanguage: "",
|
|
26
28
|
language: ""
|
|
27
29
|
});
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DocumentId } from "./entity.mjs";
|
|
2
|
+
import { McpxAnyTool, McpxAuthResult, McpxCollectionCapabilities, McpxCollectionOptions, McpxExposedEntity, McpxGlobalOptions, McpxPluginOptions, McpxRequestContext, McpxResolvedCapabilities, McpxScopeSlugs, McpxTool, McpxToolExtra, McpxToolScope, McpxWriteMode } from "./types.mjs";
|
|
3
|
+
import { defineMcpxTool } from "./define-tool.mjs";
|
|
2
4
|
import { mcpxPlugin } from "./plugin.mjs";
|
|
3
5
|
import { isMcpxRequest } from "./request.mjs";
|
|
4
6
|
import { errorResult, jsonResult } from "./result.mjs";
|
|
5
|
-
|
|
7
|
+
import { mcpxReadRequest } from "./tools/read-request.mjs";
|
|
8
|
+
export { type McpxAnyTool, type McpxAuthResult, type McpxCollectionCapabilities, type McpxCollectionOptions, type DocumentId as McpxDocumentId, type McpxExposedEntity, type McpxGlobalOptions, type McpxPluginOptions, type McpxRequestContext, type McpxResolvedCapabilities, type McpxScopeSlugs, type McpxTool, type McpxToolExtra, type McpxToolScope, type McpxWriteMode, defineMcpxTool, errorResult, isMcpxRequest, jsonResult, mcpxPlugin, mcpxReadRequest };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { defineMcpxTool } from "./define-tool.mjs";
|
|
1
2
|
import { errorResult, jsonResult } from "./result.mjs";
|
|
2
|
-
import {
|
|
3
|
+
import { mcpxReadRequest } from "./tools/read-request.mjs";
|
|
3
4
|
import { isMcpxRequest } from "./request.mjs";
|
|
4
5
|
import { mcpxPlugin } from "./plugin.mjs";
|
|
5
|
-
export { defineMcpxTool, errorResult, isMcpxRequest, jsonResult, mcpxPlugin };
|
|
6
|
+
export { defineMcpxTool, errorResult, isMcpxRequest, jsonResult, mcpxPlugin, mcpxReadRequest };
|
package/dist/options.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isPlainObject } from "./guards.mjs";
|
|
2
|
+
import { BUILTIN_TOOL_NAMES } from "./builtin-tool-names.mjs";
|
|
2
3
|
import "./version.mjs";
|
|
3
4
|
import { InvalidConfiguration } from "payload";
|
|
4
5
|
import { hasDraftsEnabled, hasLocalizeStatusEnabled } from "payload/shared";
|
|
@@ -11,45 +12,55 @@ const TOOL_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9]*$/;
|
|
|
11
12
|
const fail = (message) => {
|
|
12
13
|
throw new InvalidConfiguration(`[payloadcms-mcpx] ${message}`);
|
|
13
14
|
};
|
|
14
|
-
/** The same transform the stock MCP plugin uses to derive field names. */ const toCamelCase = (value) => value.replace(/[-_\s]+(.)?/g, (_, char) => char ? char.toUpperCase() : "").replace(/^(.)/, (_, char) => char.toLowerCase());
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
* The same transform the stock MCP plugin uses to derive field names.
|
|
17
|
+
*/ const toCamelCase = (value) => value.replace(/[-_\s]+(.)?/g, (_, char) => char ? char.toUpperCase() : "").replace(/^(.)/, (_, char) => char.toLowerCase());
|
|
18
|
+
const assertExposable = (collection, apiKeysSlug) => {
|
|
19
19
|
const { slug } = collection;
|
|
20
20
|
if (slug === apiKeysSlug || slug.startsWith("payload-")) fail(`Collection "${slug}" cannot be exposed.`);
|
|
21
21
|
if (collection.auth) fail(`Auth collection "${slug}" cannot be exposed. Its documents carry credentials.`);
|
|
22
22
|
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const OPTION_NAMES = [
|
|
24
|
+
"read",
|
|
25
|
+
"write",
|
|
26
|
+
"publish"
|
|
27
|
+
];
|
|
28
|
+
const REMOVED_WRITE_MODES = {
|
|
29
|
+
draft: "Use { publish: false } instead.",
|
|
30
|
+
live: "Writes are on by default, so remove it."
|
|
27
31
|
};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
* the tool responses model `_status` as a string. Refused until both are
|
|
33
|
-
* handled.
|
|
34
|
-
*/ const assertPublishable = (kind, config) => {
|
|
35
|
-
if (hasLocalizeStatusEnabled(config)) fail(`${kind} "${config.slug}" has versions.drafts.localizeStatus enabled, which write: "live" does not support yet.`);
|
|
32
|
+
const readFlag = (kind, slug, name, value) => {
|
|
33
|
+
if (value === void 0 || typeof value === "boolean") return value;
|
|
34
|
+
const moved = name === "write" && typeof value === "string" ? REMOVED_WRITE_MODES[value] : void 0;
|
|
35
|
+
return fail(`${kind} "${slug}" has ${name}: ${JSON.stringify(value)}. ${moved ?? "Use true or false."}`);
|
|
36
36
|
};
|
|
37
|
-
const assertWritable = (
|
|
38
|
-
|
|
39
|
-
if (collection.timestamps === false) fail(`Collection "${slug}" has timestamps disabled, which write tools need for concurrency checks.`);
|
|
40
|
-
if (options.write === "draft" && !options.hasDrafts) fail(`Collection "${slug}" has no drafts. Enable versions.drafts or set write: "live".`);
|
|
41
|
-
if (options.write === "live") assertPublishable("Collection", collection);
|
|
37
|
+
const assertWritable = (kind, config, write) => {
|
|
38
|
+
if (write === "live" && hasLocalizeStatusEnabled(config)) fail(`${kind} "${config.slug}" has versions.drafts.localizeStatus enabled, which live writes do not support yet. Set publish: false or write: false.`);
|
|
42
39
|
};
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
const normalizeCapabilities = (kind, config, raw) => {
|
|
41
|
+
const { slug } = config;
|
|
42
|
+
if (raw === false) fail(`${kind} "${slug}" is set to false. Remove the entry to hide the ${kind.toLowerCase()}.`);
|
|
43
|
+
if (raw !== true && !isPlainObject(raw)) fail(`${kind} "${slug}" has ${JSON.stringify(raw)}. Use true or an object of ${OPTION_NAMES.join(", ")}.`);
|
|
44
|
+
const settings = isPlainObject(raw) ? raw : {};
|
|
45
|
+
const unknownKey = Object.keys(settings).find((key) => !OPTION_NAMES.includes(key));
|
|
46
|
+
if (unknownKey !== void 0) fail(`${kind} "${slug}" has the unknown option "${unknownKey}". The options are ${OPTION_NAMES.join(", ")}.`);
|
|
47
|
+
const flag = (name) => readFlag(kind, slug, name, settings[name]);
|
|
48
|
+
const read = flag("read") ?? true;
|
|
49
|
+
const write = flag("write") ?? true;
|
|
50
|
+
const publish = flag("publish");
|
|
51
|
+
const hasDrafts = hasDraftsEnabled(config);
|
|
52
|
+
if (publish === false && write && !hasDrafts) fail(`${kind} "${slug}" has no drafts, so every write goes live. Set write: false or remove publish: false.`);
|
|
53
|
+
if (publish === true && !hasDrafts) fail(`${kind} "${slug}" has no drafts, so there is nothing to publish. Enable versions.drafts or remove publish: true.`);
|
|
54
|
+
if (publish === true && !write) fail(`${kind} "${slug}" has publish: true but write: false. Publishing follows write.`);
|
|
55
|
+
return {
|
|
56
|
+
read,
|
|
57
|
+
write: write ? publish === false ? "draft" : "live" : false,
|
|
58
|
+
hasDrafts,
|
|
59
|
+
hasVersions: read && Boolean(config.versions)
|
|
60
|
+
};
|
|
45
61
|
};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
* `createdAt`/`updatedAt`, so the concurrency check the collection path guards
|
|
49
|
-
* for is always available here. Drafts are the only requirement left.
|
|
50
|
-
*/ const assertGlobalWritable = (global, options) => {
|
|
51
|
-
if (options.write === "draft" && !options.hasDrafts) fail(`Global "${global.slug}" has no drafts. Enable versions.drafts or set write: "live".`);
|
|
52
|
-
if (options.write === "live") assertPublishable("Global", global);
|
|
62
|
+
const assertGlobalExposable = (global) => {
|
|
63
|
+
if (global.slug.startsWith("payload-")) fail(`Global "${global.slug}" cannot be exposed.`);
|
|
53
64
|
};
|
|
54
65
|
const normalizeCollections = (config, options, apiKeysSlug) => {
|
|
55
66
|
const collections = config.collections ?? [];
|
|
@@ -59,18 +70,16 @@ const normalizeCollections = (config, options, apiKeysSlug) => {
|
|
|
59
70
|
const collection = collections.find((candidate) => candidate.slug === slug);
|
|
60
71
|
if (!collection) return fail(`Exposed collection "${slug}" does not exist.`);
|
|
61
72
|
assertExposable(collection, apiKeysSlug);
|
|
62
|
-
const settings = raw === true ? {} : raw;
|
|
63
|
-
const hasDrafts = hasDraftsEnabled(collection);
|
|
64
73
|
const normalized = {
|
|
65
74
|
slug,
|
|
66
|
-
|
|
67
|
-
write: normalizeWriteMode("Collection", slug, settings.write),
|
|
68
|
-
hasDrafts,
|
|
69
|
-
hasVersions: Boolean(collection.versions),
|
|
75
|
+
...normalizeCapabilities("Collection", collection, raw),
|
|
70
76
|
isUpload: Boolean(collection.upload),
|
|
71
77
|
fieldName: toCamelCase(slug)
|
|
72
78
|
};
|
|
73
|
-
if (normalized.write !== false)
|
|
79
|
+
if (normalized.write !== false) {
|
|
80
|
+
if (collection.timestamps === false) fail(`Collection "${slug}" has timestamps disabled, which write tools need for concurrency checks.`);
|
|
81
|
+
assertWritable("Collection", collection, normalized.write);
|
|
82
|
+
}
|
|
74
83
|
if (fieldNames.has(normalized.fieldName)) fail(`Collection "${slug}" maps to capability field "${normalized.fieldName}", which another exposed collection already uses.`);
|
|
75
84
|
fieldNames.add(normalized.fieldName);
|
|
76
85
|
return [normalized];
|
|
@@ -84,18 +93,13 @@ const normalizeGlobals = (config, options) => {
|
|
|
84
93
|
const global = globals.find((candidate) => candidate.slug === slug);
|
|
85
94
|
if (!global) return fail(`Exposed global "${slug}" does not exist.`);
|
|
86
95
|
assertGlobalExposable(global);
|
|
87
|
-
const settings = raw === true ? {} : raw;
|
|
88
|
-
const hasDrafts = hasDraftsEnabled(global);
|
|
89
96
|
const normalized = {
|
|
90
97
|
slug,
|
|
91
|
-
|
|
92
|
-
write: normalizeWriteMode("Global", slug, settings.write),
|
|
93
|
-
hasDrafts,
|
|
94
|
-
hasVersions: Boolean(global.versions),
|
|
98
|
+
...normalizeCapabilities("Global", global, raw),
|
|
95
99
|
isUpload: false,
|
|
96
100
|
fieldName: toCamelCase(slug)
|
|
97
101
|
};
|
|
98
|
-
if (normalized.write !== false)
|
|
102
|
+
if (normalized.write !== false) assertWritable("Global", global, normalized.write);
|
|
99
103
|
if (fieldNames.has(normalized.fieldName)) fail(`Global "${slug}" maps to capability field "${normalized.fieldName}", which another exposed global already uses.`);
|
|
100
104
|
fieldNames.add(normalized.fieldName);
|
|
101
105
|
return [normalized];
|
|
@@ -125,7 +129,9 @@ const normalizeLimits = (limits) => {
|
|
|
125
129
|
maxDepth
|
|
126
130
|
};
|
|
127
131
|
};
|
|
128
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* Every problem is an `InvalidConfiguration`, so it fails at startup.
|
|
134
|
+
*/ const normalizeOptions = (config, options) => {
|
|
129
135
|
const apiKeysSlug = options.apiKeys?.slug ?? DEFAULT_API_KEYS_SLUG;
|
|
130
136
|
const userCollection = options.userCollection ?? config.admin?.user ?? "users";
|
|
131
137
|
if ((config.collections ?? []).some((c) => c.slug === apiKeysSlug)) fail(`API key collection slug "${apiKeysSlug}" is already taken.`);
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createApiKeysCollection } from "./api-keys/collection.mjs";
|
|
2
2
|
import { createMcpxHandler, methodNotAllowed } from "./endpoint/handler.mjs";
|
|
3
|
-
import "./endpoint/index.mjs";
|
|
4
3
|
import { normalizeOptions } from "./options.mjs";
|
|
5
4
|
import { installDraftGuards, installGlobalDraftGuards } from "./write/draft-guard.mjs";
|
|
6
5
|
import { definePlugin } from "payload";
|
package/dist/request.d.mts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { PayloadRequest } from "payload";
|
|
2
2
|
//#region src/request.d.ts
|
|
3
3
|
/**
|
|
4
|
-
* Whether
|
|
5
|
-
* `req
|
|
6
|
-
* same `req`, including those made by custom tools.
|
|
4
|
+
* Whether `req` came through the MCP endpoint, which stamps `req.context.mcpx`.
|
|
5
|
+
* The stamp follows `req` into every local API call, including custom tools.
|
|
7
6
|
*/
|
|
8
7
|
export declare const isMcpxRequest: (req: PayloadRequest) => boolean;
|
|
9
8
|
//#endregion
|
package/dist/request.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
//#region src/request.ts
|
|
2
2
|
/**
|
|
3
|
-
* Whether
|
|
4
|
-
* `req
|
|
5
|
-
* same `req`, including those made by custom tools.
|
|
3
|
+
* Whether `req` came through the MCP endpoint, which stamps `req.context.mcpx`.
|
|
4
|
+
* The stamp follows `req` into every local API call, including custom tools.
|
|
6
5
|
*/ const isMcpxRequest = (req) => req.context.mcpx !== void 0;
|
|
7
6
|
//#endregion
|
|
8
7
|
export { isMcpxRequest };
|
package/dist/result.d.mts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
//#region src/result.d.ts
|
|
3
|
-
/** `value` as JSON text. */
|
|
4
3
|
export declare const jsonResult: (value: unknown) => CallToolResult;
|
|
5
4
|
/**
|
|
6
|
-
* `extras`
|
|
7
|
-
*
|
|
5
|
+
* `extras` carries data the client can act on, such as problems, validation
|
|
6
|
+
* errors or the current `updatedAt`.
|
|
8
7
|
*/
|
|
9
8
|
export declare const errorResult: (message: string, extras?: Record<string, unknown>) => CallToolResult;
|
|
10
9
|
//#endregion
|
package/dist/result.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
//#region src/result.ts
|
|
2
|
-
|
|
2
|
+
const jsonResult = (value) => ({ content: [{
|
|
3
3
|
type: "text",
|
|
4
4
|
text: JSON.stringify(value)
|
|
5
5
|
}] });
|
|
6
6
|
/**
|
|
7
|
-
* `extras`
|
|
8
|
-
*
|
|
7
|
+
* `extras` carries data the client can act on, such as problems, validation
|
|
8
|
+
* errors or the current `updatedAt`.
|
|
9
9
|
*/ const errorResult = (message, extras = {}) => ({
|
|
10
10
|
content: [{
|
|
11
11
|
type: "text",
|
|
@@ -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: {
|