@abinnovision/payloadcms-mcpx 2.0.0-beta.9 → 2.1.0-beta.1
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/CHANGELOG.md +43 -0
- package/README.md +56 -36
- package/dist/admin/capability-matrix.d.mts +5 -3
- package/dist/admin/capability-matrix.mjs +374 -272
- package/dist/admin/capability-toggles.mjs +79 -42
- package/dist/admin/confirmations-panel.mjs +232 -0
- package/dist/admin/confirmations.d.mts +15 -0
- package/dist/admin/confirmations.mjs +71 -0
- package/dist/admin/endpoint-url.mjs +7 -0
- package/dist/admin/index.d.mts +2 -1
- package/dist/admin/index.mjs +2 -1
- package/dist/admin/setup-guide.d.mts +3 -3
- package/dist/admin/setup-guide.mjs +51 -33
- package/dist/api-keys/capability-matrix.d.mts +7 -9
- package/dist/api-keys/capability-matrix.mjs +15 -37
- package/dist/api-keys/collection.mjs +16 -18
- package/dist/api-keys/confirmation-view.mjs +7 -0
- package/dist/api-keys/fields.mjs +50 -57
- package/dist/auth/resolve.mjs +69 -40
- package/dist/capabilities.d.mts +2 -0
- package/dist/capabilities.mjs +64 -16
- package/dist/define-tool.d.mts +4 -4
- package/dist/define-tool.mjs +8 -1
- package/dist/endpoint/confirmations.mjs +124 -0
- package/dist/endpoint/handler.mjs +25 -27
- package/dist/endpoint/http.mjs +27 -0
- package/dist/endpoint/server.mjs +48 -29
- package/dist/endpoint/upload.mjs +104 -0
- package/dist/guards.mjs +8 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/leases/confirmation.mjs +74 -0
- package/dist/leases/grant.mjs +29 -0
- package/dist/leases/lease.mjs +115 -0
- package/dist/options.mjs +68 -61
- package/dist/plugin.d.mts +3 -2
- package/dist/plugin.mjs +19 -4
- package/dist/request.mjs +5 -1
- package/dist/result.mjs +11 -1
- package/dist/schema/admin-hidden.mjs +4 -4
- package/dist/schema/describe.mjs +12 -18
- package/dist/schema/index.mjs +5 -6
- package/dist/schema/lexical-pointer.mjs +8 -8
- package/dist/schema/lexical.mjs +15 -13
- package/dist/schema/outline.mjs +4 -5
- package/dist/schema/path.mjs +4 -1
- package/dist/schema/pointer.mjs +11 -17
- package/dist/schema/shape.mjs +38 -50
- package/dist/schema/walk.mjs +57 -18
- package/dist/tools/builtin.mjs +7 -1
- package/dist/tools/create-document.mjs +38 -26
- package/dist/tools/delete-document.mjs +154 -0
- package/dist/tools/describe-schema.mjs +7 -5
- package/dist/tools/document.mjs +131 -45
- package/dist/tools/duplicate-document.mjs +50 -0
- package/dist/tools/find-documents.mjs +15 -20
- package/dist/tools/find-versions.mjs +11 -15
- package/dist/tools/get-document.mjs +31 -15
- package/dist/tools/links.mjs +53 -0
- package/dist/tools/list-capabilities.mjs +17 -7
- package/dist/tools/patch-document.mjs +57 -53
- package/dist/tools/publish-document.mjs +17 -29
- package/dist/tools/query-paths.mjs +1 -1
- package/dist/tools/run-confirmed.mjs +114 -0
- package/dist/tools/shared.mjs +53 -46
- package/dist/tools/validate-document.mjs +6 -8
- package/dist/tools/versions.mjs +11 -6
- package/dist/types.d.mts +158 -43
- package/dist/upload/file.mjs +178 -0
- package/dist/version.mjs +4 -2
- package/dist/write/draft-guard.mjs +35 -28
- package/dist/write/patch.mjs +4 -4
- package/dist/write/publish-intent.mjs +23 -11
- package/dist/write/row-ids.mjs +3 -1
- package/dist/write/write-data.mjs +18 -31
- package/package.json +14 -14
- package/dist/builtin-tool-names.mjs +0 -16
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { isPlainObject } from "../guards.mjs";
|
|
2
|
+
import { generateApiKey, hashApiKey } from "../api-keys/key.mjs";
|
|
3
|
+
import { APIError, ValidationError } from "payload";
|
|
4
|
+
//#region src/leases/lease.ts
|
|
5
|
+
const LEASE_ID = /^[\w-]{43}$/;
|
|
6
|
+
const NO_REQ = {};
|
|
7
|
+
/**
|
|
8
|
+
* The key prefixes of a kind of lease: waiting leases and the markers of used
|
|
9
|
+
* ones.
|
|
10
|
+
*/ const leasePrefixes = (kind, apiKeyId) => ({
|
|
11
|
+
prefix: `mcpx-${kind}:${String(apiKeyId)}:`,
|
|
12
|
+
usedPrefix: `mcpx-${kind}-used:${String(apiKeyId)}:`
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* What a lease is addressed by: the HMAC of its id, which the row key ends in.
|
|
16
|
+
*/ const leaseHandle = (payload, id) => hashApiKey(payload.secret, id);
|
|
17
|
+
const expOf = (data) => isPlainObject(data) && typeof data["exp"] === "number" ? data["exp"] : 0;
|
|
18
|
+
/**
|
|
19
|
+
* The KV collection slug, when Payload's database KV adapter backs it with a
|
|
20
|
+
* unique `key`, which the single-use claim relies on. `undefined` otherwise.
|
|
21
|
+
*/ const leaseKvSlug = (config) => {
|
|
22
|
+
const kv = config.kv;
|
|
23
|
+
const key = kv?.kvCollection?.fields.find((field) => "name" in field && field.name === "key");
|
|
24
|
+
return key && "unique" in key && key.unique ? kv?.kvCollection?.slug : void 0;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* KV rows whose key contains one of `fragments`.
|
|
28
|
+
*/ const leasesMatching = async (payload, slug, fragments) => {
|
|
29
|
+
const { docs } = await payload.db.find({
|
|
30
|
+
collection: slug,
|
|
31
|
+
where: { or: fragments.map((fragment) => ({ key: { like: fragment } })) },
|
|
32
|
+
limit: 0,
|
|
33
|
+
pagination: false,
|
|
34
|
+
req: NO_REQ
|
|
35
|
+
});
|
|
36
|
+
return docs;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* KV rows whose key starts with one of `prefixes`. `like` matches a
|
|
40
|
+
* substring, so the prefix is checked again here.
|
|
41
|
+
*/ const leasesWith = async (payload, slug, prefixes) => (await leasesMatching(payload, slug, prefixes)).filter((row) => prefixes.some((prefix) => row.key.startsWith(prefix)));
|
|
42
|
+
/**
|
|
43
|
+
* Stores `data` under `prefix` and the HMAC of a fresh id, and returns the id,
|
|
44
|
+
* the only copy of which goes to the client. Expired rows under `prefix` and
|
|
45
|
+
* `usedPrefix` are purged first, and at most `cap` unexpired rows may wait
|
|
46
|
+
* under `prefix`; past that, `capMessage` is thrown with 429.
|
|
47
|
+
*/ const issueLease = async (payload, slug, args) => {
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
const rows = await leasesWith(payload, slug, [args.prefix, args.usedPrefix]);
|
|
50
|
+
const expired = rows.filter((row) => expOf(row.data) <= now);
|
|
51
|
+
if (expired.length > 0) await payload.db.deleteMany({
|
|
52
|
+
collection: slug,
|
|
53
|
+
where: { key: { in: expired.map((row) => row.key) } },
|
|
54
|
+
req: NO_REQ
|
|
55
|
+
});
|
|
56
|
+
if (rows.filter((row) => row.key.startsWith(args.prefix) && expOf(row.data) > now).length >= args.cap) throw new APIError(args.capMessage, 429);
|
|
57
|
+
const id = generateApiKey();
|
|
58
|
+
const exp = now + args.ttlMs;
|
|
59
|
+
await payload.db.create({
|
|
60
|
+
collection: slug,
|
|
61
|
+
data: {
|
|
62
|
+
key: `${args.prefix}${leaseHandle(payload, id)}`,
|
|
63
|
+
data: {
|
|
64
|
+
...args.data,
|
|
65
|
+
exp
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
req: NO_REQ
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
id,
|
|
72
|
+
exp
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Marks the row under `handle` used and removes it. The marker is a
|
|
77
|
+
* unique-key insert, so of two concurrent claims exactly one returns `true`.
|
|
78
|
+
*/ const claimLease = async (payload, slug, prefixes, handle, exp) => {
|
|
79
|
+
try {
|
|
80
|
+
await payload.db.create({
|
|
81
|
+
collection: slug,
|
|
82
|
+
data: {
|
|
83
|
+
key: `${prefixes.usedPrefix}${handle}`,
|
|
84
|
+
data: { exp }
|
|
85
|
+
},
|
|
86
|
+
req: NO_REQ
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error instanceof ValidationError) return false;
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
await payload.db.deleteOne({
|
|
93
|
+
collection: slug,
|
|
94
|
+
where: { key: { equals: `${prefixes.prefix}${handle}` } },
|
|
95
|
+
req: NO_REQ
|
|
96
|
+
});
|
|
97
|
+
return true;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* The KV slug and key id of the request, which a call that stores a lease
|
|
101
|
+
* needs. Throws where the endpoint would not offer one.
|
|
102
|
+
*/ const leaseContext = (req) => {
|
|
103
|
+
const slug = leaseKvSlug(req.payload.config);
|
|
104
|
+
const apiKeyId = req.context.mcpx?.apiKeyId;
|
|
105
|
+
if (slug === void 0 || apiKeyId === void 0) throw new Error("A lease needs a database KV and a key.");
|
|
106
|
+
return {
|
|
107
|
+
slug,
|
|
108
|
+
apiKeyId
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* The data of `row` when it has not expired at `now`, otherwise `undefined`.
|
|
113
|
+
*/ const unexpired = (row, now) => row && expOf(row.data) > now ? row.data : void 0;
|
|
114
|
+
//#endregion
|
|
115
|
+
export { LEASE_ID, NO_REQ, claimLease, issueLease, leaseContext, leaseHandle, leaseKvSlug, leasePrefixes, leasesMatching, leasesWith, unexpired };
|
package/dist/options.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isPlainObject } from "./guards.mjs";
|
|
2
|
-
import { BUILTIN_TOOL_NAMES } from "./builtin-tool-names.mjs";
|
|
3
2
|
import "./version.mjs";
|
|
4
3
|
import { InvalidConfiguration } from "payload";
|
|
5
4
|
import { hasDraftsEnabled, hasLocalizeStatusEnabled } from "payload/shared";
|
|
@@ -15,12 +14,18 @@ const fail = (message) => {
|
|
|
15
14
|
/**
|
|
16
15
|
* The same transform the stock MCP plugin uses to derive field names.
|
|
17
16
|
*/ const toCamelCase = (value) => value.replace(/[-_\s]+(.)?/g, (_, char) => char ? char.toUpperCase() : "").replace(/^(.)/, (_, char) => char.toLowerCase());
|
|
18
|
-
const assertExposable = (
|
|
19
|
-
const { slug } =
|
|
20
|
-
if (slug
|
|
21
|
-
if (
|
|
17
|
+
const assertExposable = (kind, config, apiKeysSlug) => {
|
|
18
|
+
const { slug } = config;
|
|
19
|
+
if (slug.startsWith("payload-") || kind === "Collection" && slug === apiKeysSlug) fail(`${kind} "${slug}" cannot be exposed.`);
|
|
20
|
+
if ("auth" in config && config.auth) fail(`Auth collection "${slug}" cannot be exposed. Its documents carry credentials.`);
|
|
22
21
|
};
|
|
23
22
|
const OPTION_NAMES = [
|
|
23
|
+
"read",
|
|
24
|
+
"write",
|
|
25
|
+
"publish",
|
|
26
|
+
"delete"
|
|
27
|
+
];
|
|
28
|
+
const GLOBAL_OPTION_NAMES = [
|
|
24
29
|
"read",
|
|
25
30
|
"write",
|
|
26
31
|
"publish"
|
|
@@ -34,87 +39,82 @@ const readFlag = (kind, slug, name, value) => {
|
|
|
34
39
|
const moved = name === "write" && typeof value === "string" ? REMOVED_WRITE_MODES[value] : void 0;
|
|
35
40
|
return fail(`${kind} "${slug}" has ${name}: ${JSON.stringify(value)}. ${moved ?? "Use true or false."}`);
|
|
36
41
|
};
|
|
37
|
-
const assertWritable = (kind, config,
|
|
38
|
-
if (
|
|
42
|
+
const assertWritable = (kind, config, publish) => {
|
|
43
|
+
if (publish && hasLocalizeStatusEnabled(config)) fail(`${kind} "${config.slug}" has versions.drafts.localizeStatus enabled, which live writes do not support yet. Set publish: false or write: false.`);
|
|
44
|
+
};
|
|
45
|
+
const normalizeDelete = (kind, config, value) => {
|
|
46
|
+
if (value !== "unattended") return {
|
|
47
|
+
delete: readFlag(kind, config.slug, "delete", value) ?? false,
|
|
48
|
+
deleteUnattended: false
|
|
49
|
+
};
|
|
50
|
+
if (!("trash" in config) || !config.trash) fail(`${kind} "${config.slug}" has delete: "unattended" but no trash, so a delete without approval would be permanent. Enable trash or set delete: true.`);
|
|
51
|
+
return {
|
|
52
|
+
delete: true,
|
|
53
|
+
deleteUnattended: true
|
|
54
|
+
};
|
|
39
55
|
};
|
|
40
56
|
const normalizeCapabilities = (kind, config, raw) => {
|
|
41
57
|
const { slug } = config;
|
|
58
|
+
const names = kind === "Global" ? GLOBAL_OPTION_NAMES : OPTION_NAMES;
|
|
42
59
|
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 ${
|
|
60
|
+
if (raw !== true && !isPlainObject(raw)) fail(`${kind} "${slug}" has ${JSON.stringify(raw)}. Use true or an object of ${names.join(", ")}.`);
|
|
44
61
|
const settings = isPlainObject(raw) ? raw : {};
|
|
45
|
-
const unknownKey = Object.keys(settings).find((key) => !
|
|
46
|
-
if (unknownKey !== void 0) fail(`${kind} "${slug}" has the unknown option "${unknownKey}". The options are ${
|
|
62
|
+
const unknownKey = Object.keys(settings).find((key) => !names.includes(key));
|
|
63
|
+
if (unknownKey !== void 0) fail(`${kind} "${slug}" has the unknown option "${unknownKey}". The options are ${names.join(", ")}.`);
|
|
47
64
|
const flag = (name) => readFlag(kind, slug, name, settings[name]);
|
|
48
65
|
const read = flag("read") ?? true;
|
|
49
|
-
const write = flag("write") ??
|
|
66
|
+
const write = flag("write") ?? read;
|
|
50
67
|
const publish = flag("publish");
|
|
51
68
|
const hasDrafts = hasDraftsEnabled(config);
|
|
52
69
|
if (publish === false && write && !hasDrafts) fail(`${kind} "${slug}" has no drafts, so every write goes live. Set write: false or remove publish: false.`);
|
|
53
70
|
if (publish === true && !hasDrafts) fail(`${kind} "${slug}" has no drafts, so there is nothing to publish. Enable versions.drafts or remove publish: true.`);
|
|
71
|
+
if (!read && (flag("write") === true || publish === true)) fail(`${kind} "${slug}" has read: false but write or publish on. Writing and publishing need read.`);
|
|
54
72
|
if (publish === true && !write) fail(`${kind} "${slug}" has publish: true but write: false. Publishing follows write.`);
|
|
73
|
+
const deleting = normalizeDelete(kind, config, settings["delete"]);
|
|
74
|
+
if (!read && deleting.delete) fail(`${kind} "${slug}" has read: false but delete on. Deleting needs read.`);
|
|
55
75
|
return {
|
|
56
76
|
read,
|
|
57
|
-
write
|
|
77
|
+
write,
|
|
78
|
+
publish: write && publish !== false && hasDrafts,
|
|
79
|
+
liveWrite: write && !hasDrafts,
|
|
58
80
|
hasDrafts,
|
|
59
|
-
hasVersions: read && Boolean(config.versions)
|
|
81
|
+
hasVersions: read && Boolean(config.versions),
|
|
82
|
+
...deleting
|
|
60
83
|
};
|
|
61
84
|
};
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
};
|
|
65
|
-
const normalizeCollections = (config, options, apiKeysSlug) => {
|
|
66
|
-
const collections = config.collections ?? [];
|
|
85
|
+
const bySlug = (configs) => new Map(configs.map((config) => [config.slug, config]));
|
|
86
|
+
const normalizeEntities = (kind, configs, entries, apiKeysSlug) => {
|
|
67
87
|
const fieldNames = /* @__PURE__ */ new Set();
|
|
68
|
-
return Object.entries(
|
|
88
|
+
return Object.entries(entries).flatMap(([slug, raw]) => {
|
|
69
89
|
if (raw === void 0) return [];
|
|
70
|
-
const
|
|
71
|
-
if (!
|
|
72
|
-
assertExposable(
|
|
90
|
+
const config = configs.get(slug);
|
|
91
|
+
if (!config) return fail(`Exposed ${kind.toLowerCase()} "${slug}" does not exist.`);
|
|
92
|
+
assertExposable(kind, config, apiKeysSlug);
|
|
73
93
|
const normalized = {
|
|
74
94
|
slug,
|
|
75
|
-
...normalizeCapabilities(
|
|
76
|
-
isUpload: Boolean(
|
|
95
|
+
...normalizeCapabilities(kind, config, raw),
|
|
96
|
+
isUpload: "upload" in config && Boolean(config.upload),
|
|
77
97
|
fieldName: toCamelCase(slug)
|
|
78
98
|
};
|
|
79
|
-
if (normalized.write
|
|
80
|
-
if (
|
|
81
|
-
assertWritable(
|
|
99
|
+
if (normalized.write) {
|
|
100
|
+
if ("timestamps" in config && config.timestamps === false) fail(`Collection "${slug}" has timestamps disabled, which write tools need for concurrency checks.`);
|
|
101
|
+
assertWritable(kind, config, normalized.publish);
|
|
82
102
|
}
|
|
83
|
-
if (fieldNames.has(normalized.fieldName)) fail(
|
|
84
|
-
fieldNames.add(normalized.fieldName);
|
|
85
|
-
return [normalized];
|
|
86
|
-
});
|
|
87
|
-
};
|
|
88
|
-
const normalizeGlobals = (config, options) => {
|
|
89
|
-
const globals = config.globals ?? [];
|
|
90
|
-
const fieldNames = /* @__PURE__ */ new Set();
|
|
91
|
-
return Object.entries(options.globals ?? {}).flatMap(([slug, raw]) => {
|
|
92
|
-
if (raw === void 0) return [];
|
|
93
|
-
const global = globals.find((candidate) => candidate.slug === slug);
|
|
94
|
-
if (!global) return fail(`Exposed global "${slug}" does not exist.`);
|
|
95
|
-
assertGlobalExposable(global);
|
|
96
|
-
const normalized = {
|
|
97
|
-
slug,
|
|
98
|
-
...normalizeCapabilities("Global", global, raw),
|
|
99
|
-
isUpload: false,
|
|
100
|
-
fieldName: toCamelCase(slug)
|
|
101
|
-
};
|
|
102
|
-
if (normalized.write !== false) assertWritable("Global", global, normalized.write);
|
|
103
|
-
if (fieldNames.has(normalized.fieldName)) fail(`Global "${slug}" maps to capability field "${normalized.fieldName}", which another exposed global already uses.`);
|
|
103
|
+
if (fieldNames.has(normalized.fieldName)) fail(`${kind} "${slug}" maps to capability field "${normalized.fieldName}", which another exposed ${kind.toLowerCase()} already uses.`);
|
|
104
104
|
fieldNames.add(normalized.fieldName);
|
|
105
105
|
return [normalized];
|
|
106
106
|
});
|
|
107
107
|
};
|
|
108
|
-
const assertUserCollection = (
|
|
109
|
-
const collection =
|
|
108
|
+
const assertUserCollection = (collections, slug) => {
|
|
109
|
+
const collection = collections.get(slug);
|
|
110
110
|
if (!collection) fail(`User collection "${slug}" does not exist.`);
|
|
111
|
-
else if (!collection.auth) fail(`User collection "${slug}" is not an auth collection.`);
|
|
111
|
+
else if (!("auth" in collection && collection.auth)) fail(`User collection "${slug}" is not an auth collection.`);
|
|
112
112
|
};
|
|
113
|
-
const assertTools = (tools) => {
|
|
113
|
+
const assertTools = (tools, reserved) => {
|
|
114
114
|
const names = /* @__PURE__ */ new Set();
|
|
115
115
|
for (const tool of tools) {
|
|
116
116
|
if (!TOOL_NAME_PATTERN.test(tool.name)) fail(`Tool name "${tool.name}" must match ${String(TOOL_NAME_PATTERN)}.`);
|
|
117
|
-
if (
|
|
117
|
+
if (reserved.includes(tool.name)) fail(`Tool name "${tool.name}" is reserved for a builtin tool.`);
|
|
118
118
|
if (names.has(tool.name)) fail(`Tool name "${tool.name}" is used twice.`);
|
|
119
119
|
names.add(tool.name);
|
|
120
120
|
}
|
|
@@ -131,27 +131,34 @@ const normalizeLimits = (limits) => {
|
|
|
131
131
|
};
|
|
132
132
|
/**
|
|
133
133
|
* Every problem is an `InvalidConfiguration`, so it fails at startup.
|
|
134
|
-
|
|
134
|
+
* `reservedToolNames` are the builtin tool names custom tools must not reuse.
|
|
135
|
+
*/ const normalizeOptions = (config, options, reservedToolNames) => {
|
|
135
136
|
const apiKeysSlug = options.apiKeys?.slug ?? DEFAULT_API_KEYS_SLUG;
|
|
136
137
|
const userCollection = options.userCollection ?? config.admin?.user ?? "users";
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
const collectionConfigs = bySlug(config.collections ?? []);
|
|
139
|
+
if (collectionConfigs.has(apiKeysSlug)) fail(`API key collection slug "${apiKeysSlug}" is already taken.`);
|
|
140
|
+
assertUserCollection(collectionConfigs, userCollection);
|
|
139
141
|
const tools = options.tools ?? [];
|
|
140
|
-
assertTools(tools);
|
|
142
|
+
assertTools(tools, reservedToolNames);
|
|
143
|
+
const collections = normalizeEntities("Collection", collectionConfigs, options.collections, apiKeysSlug);
|
|
144
|
+
const confirmations = collections.some((entity) => entity.delete);
|
|
145
|
+
if (confirmations && options.auth?.resolve) fail("delete needs approval on the API key document, which a custom auth.resolve does not support. Remove delete: true or auth.resolve.");
|
|
141
146
|
return {
|
|
142
|
-
collections
|
|
143
|
-
globals:
|
|
147
|
+
collections,
|
|
148
|
+
globals: normalizeEntities("Global", bySlug(config.globals ?? []), options.globals ?? {}, apiKeysSlug),
|
|
144
149
|
userCollection,
|
|
145
150
|
apiKeysSlug,
|
|
146
151
|
endpointPath: options.endpoint?.path ?? DEFAULT_ENDPOINT_PATH,
|
|
147
152
|
setupGuide: options.apiKeys?.setupGuide ?? true,
|
|
148
153
|
limits: normalizeLimits(options.limits),
|
|
149
154
|
tools,
|
|
155
|
+
confirmations,
|
|
150
156
|
auth: options.auth,
|
|
151
157
|
serverInfo: {
|
|
152
158
|
name: options.serverInfo?.name ?? "payloadcms-mcpx",
|
|
153
|
-
version: options.serverInfo?.version ?? "
|
|
154
|
-
}
|
|
159
|
+
version: options.serverInfo?.version ?? "2.0.0"
|
|
160
|
+
},
|
|
161
|
+
diagnostics: options.diagnostics ?? true
|
|
155
162
|
};
|
|
156
163
|
};
|
|
157
164
|
//#endregion
|
package/dist/plugin.d.mts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { McpxPluginOptions } from "./types.mjs";
|
|
2
2
|
//#region src/plugin.d.ts
|
|
3
3
|
/**
|
|
4
|
-
* Mounts the MCP
|
|
5
|
-
*
|
|
4
|
+
* Mounts the MCP, upload and download endpoints, and the confirmation
|
|
5
|
+
* endpoints where a collection exposes `delete`. Adds the API key collection
|
|
6
|
+
* and installs the draft guard on every collection and global.
|
|
6
7
|
*/
|
|
7
8
|
export declare const mcpxPlugin: (options: McpxPluginOptions) => import("payload").Plugin;
|
|
8
9
|
//#endregion
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { createApiKeysCollection } from "./api-keys/collection.mjs";
|
|
2
|
+
import { BUILTIN_TOOLS } from "./tools/builtin.mjs";
|
|
3
|
+
import { confirmationEndpoints } from "./endpoint/confirmations.mjs";
|
|
2
4
|
import { createMcpxHandler, methodNotAllowed } from "./endpoint/handler.mjs";
|
|
5
|
+
import { createDownloadHandler, createUploadHandler } from "./endpoint/upload.mjs";
|
|
3
6
|
import { normalizeOptions } from "./options.mjs";
|
|
4
7
|
import { installDraftGuards, installGlobalDraftGuards } from "./write/draft-guard.mjs";
|
|
5
8
|
import { definePlugin } from "payload";
|
|
6
9
|
//#region src/plugin.ts
|
|
7
10
|
/**
|
|
8
|
-
* Mounts the MCP
|
|
9
|
-
*
|
|
11
|
+
* Mounts the MCP, upload and download endpoints, and the confirmation
|
|
12
|
+
* endpoints where a collection exposes `delete`. Adds the API key collection
|
|
13
|
+
* and installs the draft guard on every collection and global.
|
|
10
14
|
*/ const mcpxPlugin = definePlugin({
|
|
11
15
|
slug: "@abinnovision/payloadcms-mcpx",
|
|
12
16
|
order: 100,
|
|
13
17
|
plugin: ({ config, plugins: _plugins, ...options }) => {
|
|
14
|
-
const normalized = normalizeOptions(config, options);
|
|
18
|
+
const normalized = normalizeOptions(config, options, BUILTIN_TOOLS.map((tool) => tool.name));
|
|
15
19
|
const apiKeys = createApiKeysCollection(normalized);
|
|
16
20
|
const apiKeysCollection = options.apiKeys?.overrideCollection?.(apiKeys) ?? apiKeys;
|
|
17
21
|
return {
|
|
@@ -34,7 +38,18 @@ import { definePlugin } from "payload";
|
|
|
34
38
|
path: normalized.endpointPath,
|
|
35
39
|
method: "delete",
|
|
36
40
|
handler: methodNotAllowed
|
|
37
|
-
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
path: `${normalized.endpointPath}/upload`,
|
|
44
|
+
method: "put",
|
|
45
|
+
handler: createUploadHandler(normalized)
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
path: `${normalized.endpointPath}/file`,
|
|
49
|
+
method: "get",
|
|
50
|
+
handler: createDownloadHandler(normalized)
|
|
51
|
+
},
|
|
52
|
+
...normalized.confirmations ? confirmationEndpoints(normalized) : []
|
|
38
53
|
]
|
|
39
54
|
};
|
|
40
55
|
}
|
package/dist/request.mjs
CHANGED
|
@@ -3,5 +3,9 @@
|
|
|
3
3
|
* Whether `req` came through the MCP endpoint, which stamps `req.context.mcpx`.
|
|
4
4
|
* The stamp follows `req` into every local API call, including custom tools.
|
|
5
5
|
*/ const isMcpxRequest = (req) => req.context.mcpx !== void 0;
|
|
6
|
+
/**
|
|
7
|
+
* The origin links to this server use: `serverURL` when set, else that of the
|
|
8
|
+
* MCP request.
|
|
9
|
+
*/ const publicOrigin = (req) => req.payload.config.serverURL || new URL(req.url ?? "").origin;
|
|
6
10
|
//#endregion
|
|
7
|
-
export { isMcpxRequest };
|
|
11
|
+
export { isMcpxRequest, publicOrigin };
|
package/dist/result.mjs
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
//#region src/result.ts
|
|
2
|
+
/**
|
|
3
|
+
* The JSON of a result's first text block. Other text comes back as `{ text }`.
|
|
4
|
+
*/ const parseResult = (result) => {
|
|
5
|
+
const [first] = result.content;
|
|
6
|
+
try {
|
|
7
|
+
return first?.type === "text" ? JSON.parse(first.text) : {};
|
|
8
|
+
} catch {
|
|
9
|
+
return { text: first?.type === "text" ? first.text : void 0 };
|
|
10
|
+
}
|
|
11
|
+
};
|
|
2
12
|
const jsonResult = (value) => ({ content: [{
|
|
3
13
|
type: "text",
|
|
4
14
|
text: JSON.stringify(value)
|
|
@@ -17,4 +27,4 @@ const jsonResult = (value) => ({ content: [{
|
|
|
17
27
|
isError: true
|
|
18
28
|
});
|
|
19
29
|
//#endregion
|
|
20
|
-
export { errorResult, jsonResult };
|
|
30
|
+
export { errorResult, jsonResult, parseResult };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isPlainObject } from "../guards.mjs";
|
|
2
|
-
import { RESERVED_FIELD_NAMES, ROW_KEYS, blockOf, blockSlugsOf,
|
|
2
|
+
import { RESERVED_FIELD_NAMES, ROW_KEYS, adminFlag, blockForRow, blockOf, blockSlugsOf, schemaOf } from "./walk.mjs";
|
|
3
3
|
//#region src/schema/admin-hidden.ts
|
|
4
4
|
const UPLOAD_BASE_FIELDS = /* @__PURE__ */ new Set([
|
|
5
5
|
"filename",
|
|
@@ -22,7 +22,7 @@ const stripFields = (config, fields, doc, exempt = NONE) => {
|
|
|
22
22
|
const result = { ...doc };
|
|
23
23
|
for (const field of fields) {
|
|
24
24
|
if (!("name" in field) || exempt.has(field.name) || isPayloadKey(field.name) || !Object.hasOwn(result, field.name)) continue;
|
|
25
|
-
if (
|
|
25
|
+
if (adminFlag(field, "hidden")) Reflect.deleteProperty(result, field.name);
|
|
26
26
|
else result[field.name] = stripValue(config, field, result[field.name]);
|
|
27
27
|
}
|
|
28
28
|
return result;
|
|
@@ -45,7 +45,7 @@ const stripValue = (config, field, value) => {
|
|
|
45
45
|
case "tab": return isPlainObject(value) ? stripFields(config, field.flattenedFields, value) : value;
|
|
46
46
|
case "array": return mapEach(value, (row) => isPlainObject(row) ? stripFields(config, field.flattenedFields, row) : row);
|
|
47
47
|
case "blocks": return mapEach(value, (row) => {
|
|
48
|
-
const block =
|
|
48
|
+
const block = blockForRow(config, field, row);
|
|
49
49
|
return block && isPlainObject(row) ? stripFields(config, block.flattenedFields, row) : row;
|
|
50
50
|
});
|
|
51
51
|
case "relationship":
|
|
@@ -76,7 +76,7 @@ const stripValue = (config, field, value) => {
|
|
|
76
76
|
const matches = fields.filter((field) => "name" in field && field.name === segment);
|
|
77
77
|
if (matches.length === 0) return locales.includes(segment) ? walk(fields, rest) : false;
|
|
78
78
|
return matches.some((field) => {
|
|
79
|
-
if (
|
|
79
|
+
if (adminFlag(field, "hidden")) return true;
|
|
80
80
|
if (field.type === "group" || field.type === "tab" || field.type === "array") return walk(field.flattenedFields, rest);
|
|
81
81
|
return field.type === "blocks" && blockSlugsOf(field).some((slug) => {
|
|
82
82
|
const block = blockOf(config, field, slug);
|
package/dist/schema/describe.mjs
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
|
+
import { SchemaError } from "./errors.mjs";
|
|
1
2
|
import { lexicalSubSchema, subSchemaNodeTypes } from "./lexical.mjs";
|
|
2
3
|
import { joinPath, splitPath } from "./path.mjs";
|
|
3
4
|
import { translateAny } from "../i18n.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import { SchemaError } from "./errors.mjs";
|
|
5
|
+
import { blockSlugsOf, describeFields, findFieldAt, longestMatch, requireBlock, schemaOf } from "./walk.mjs";
|
|
6
6
|
//#region src/schema/describe.ts
|
|
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];
|
|
8
7
|
const stepThroughBlocks = ({ config, fields, match, remaining }) => {
|
|
9
|
-
const field =
|
|
8
|
+
const field = findFieldAt(fields, match, "blocks");
|
|
10
9
|
if (!field) throw new Error(`"${joinPath(match)}" could not be resolved.`);
|
|
11
10
|
const slug = remaining.at(match.length);
|
|
12
11
|
if (slug === void 0) throw new SchemaError(`"${joinPath(match)}" is a blocks field; append one of: ${blockSlugsOf(field).join(", ")}`);
|
|
13
|
-
const block = blockOf(config, field, slug);
|
|
14
|
-
if (!block) throw new SchemaError(`"${slug}" is not allowed at "${joinPath(match)}". Allowed: ${blockSlugsOf(field).join(", ")}`);
|
|
15
12
|
return {
|
|
16
13
|
blockType: slug,
|
|
17
|
-
fields:
|
|
14
|
+
fields: requireBlock(config, field, slug, joinPath(match)).flattenedFields,
|
|
18
15
|
rest: remaining.slice(match.length + 1)
|
|
19
16
|
};
|
|
20
17
|
};
|
|
21
18
|
const stepThroughLexical = ({ config, fields, match, remaining }) => {
|
|
22
|
-
const field =
|
|
19
|
+
const field = findFieldAt(fields, match, "richText");
|
|
23
20
|
if (!field) throw new Error(`"${joinPath(match)}" could not be resolved.`);
|
|
24
21
|
const available = subSchemaNodeTypes(field).join(", ") || "none";
|
|
25
22
|
const nodeType = remaining.at(match.length);
|
|
@@ -32,13 +29,10 @@ const stepThroughLexical = ({ config, fields, match, remaining }) => {
|
|
|
32
29
|
rest: remaining.slice(match.length + 1)
|
|
33
30
|
};
|
|
34
31
|
const slug = remaining.at(match.length + 1);
|
|
35
|
-
|
|
36
|
-
if (slug === void 0) throw new SchemaError(`"${reached}" selects a block; append one of: ${slugs}`);
|
|
37
|
-
const block = blockOf(config, sub.blocksField, slug);
|
|
38
|
-
if (!block) throw new SchemaError(`"${slug}" is not allowed at "${reached}". Allowed: ${slugs}`);
|
|
32
|
+
if (slug === void 0) throw new SchemaError(`"${reached}" selects a block; append one of: ${blockSlugsOf(sub.blocksField).join(", ")}`);
|
|
39
33
|
return {
|
|
40
34
|
blockType: slug,
|
|
41
|
-
fields:
|
|
35
|
+
fields: requireBlock(config, sub.blocksField, slug, reached).flattenedFields,
|
|
42
36
|
rest: remaining.slice(match.length + 2)
|
|
43
37
|
};
|
|
44
38
|
};
|
|
@@ -48,15 +42,15 @@ const fieldsAtSchemaPath = (config, target, schemaPath) => {
|
|
|
48
42
|
let remaining = splitPath(schemaPath);
|
|
49
43
|
while (remaining.length > 0) {
|
|
50
44
|
const descendable = describeFields(fields).filter((descriptor) => descriptor.type === "blocks" || descriptor.type === "richText");
|
|
51
|
-
const
|
|
52
|
-
if (!
|
|
45
|
+
const found = longestMatch(descendable, remaining, (part, segment) => part === segment);
|
|
46
|
+
if (!found) throw new SchemaError(`"${joinPath(remaining)}" does not address a blocks or rich text field. Available here: ${descendable.map((descriptor) => descriptor.path).join(", ") || "none"}`);
|
|
53
47
|
const at = {
|
|
54
48
|
config,
|
|
55
49
|
fields,
|
|
56
|
-
match,
|
|
50
|
+
match: found.parts,
|
|
57
51
|
remaining
|
|
58
52
|
};
|
|
59
|
-
const step =
|
|
53
|
+
const step = found.descriptor.type === "blocks" ? stepThroughBlocks(at) : stepThroughLexical(at);
|
|
60
54
|
blockType = step.blockType;
|
|
61
55
|
fields = step.fields;
|
|
62
56
|
remaining = step.rest;
|
|
@@ -69,7 +63,7 @@ const fieldsAtSchemaPath = (config, target, schemaPath) => {
|
|
|
69
63
|
const branchesOf = (fields, descriptor, schemaPath) => {
|
|
70
64
|
const base = `${schemaPath}${descriptor.path}`;
|
|
71
65
|
if (descriptor.type === "richText") {
|
|
72
|
-
const field =
|
|
66
|
+
const field = findFieldAt(fields, splitPath(descriptor.path), "richText");
|
|
73
67
|
if (!field) return [];
|
|
74
68
|
return subSchemaNodeTypes(field).flatMap((nodeType) => {
|
|
75
69
|
const sub = lexicalSubSchema(field, nodeType);
|
package/dist/schema/index.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { REQUIRED_NODE_PROPERTIES, ROOT_PROPERTIES, allowedNodeTypes, constrainsFields, lexicalSubSchema, nodeOptions, nodeProblems, nodePropertiesFor, propertyProblem, rootProblems, subSchemaNodeTypes } from "./lexical.mjs";
|
|
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
1
|
import { SchemaError } from "./errors.mjs";
|
|
2
|
+
import { editorRoot, nodePropertiesFor } from "./lexical.mjs";
|
|
3
|
+
import { JSON_POINTER_PATTERN, isIndexSegment, parentPointer, pointerFromPayloadPath, prototypeSegmentProblem, splitPath } from "./path.mjs";
|
|
4
|
+
import { RESERVED_FIELD_NAMES, ROW_KEYS, blockForRow, classifyKey, descriptorsUnder, findFieldAt } from "./walk.mjs";
|
|
5
|
+
import { addressesAdminHidden, stripAdminHidden } from "./admin-hidden.mjs";
|
|
6
6
|
import { nodeDescriber, reachableSchemaPaths } from "./describe.mjs";
|
|
7
|
-
import { resolveLexicalPointer } from "./lexical-pointer.mjs";
|
|
8
7
|
import { lexicalOutline } from "./outline.mjs";
|
|
9
8
|
import { resolveDataPointer } from "./pointer.mjs";
|
|
10
9
|
import { EMPTY_ROOT, validateWriteValue } from "./shape.mjs";
|
|
11
|
-
export { EMPTY_ROOT, JSON_POINTER_PATTERN,
|
|
10
|
+
export { EMPTY_ROOT, JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, ROW_KEYS, SchemaError, addressesAdminHidden, blockForRow, classifyKey, descriptorsUnder, editorRoot, findFieldAt, isIndexSegment, lexicalOutline, nodeDescriber, nodePropertiesFor, parentPointer, pointerFromPayloadPath, prototypeSegmentProblem, reachableSchemaPaths, resolveDataPointer, splitPath, stripAdminHidden, validateWriteValue };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { isPlainObject } from "../guards.mjs";
|
|
2
|
-
import {
|
|
1
|
+
import { isPlainObject, propOf } from "../guards.mjs";
|
|
2
|
+
import { SchemaError } from "./errors.mjs";
|
|
3
|
+
import { editorRoot, lexicalSubSchema, subSchemaNodeTypes } from "./lexical.mjs";
|
|
3
4
|
import { isIndexSegment, joinPath } from "./path.mjs";
|
|
4
5
|
import { blockOf, blockSlugsOf } from "./walk.mjs";
|
|
5
|
-
import { SchemaError } from "./errors.mjs";
|
|
6
6
|
//#region src/schema/lexical-pointer.ts
|
|
7
7
|
const stepIntoFields = (at) => {
|
|
8
8
|
const { addedValue, config, field, node, nodeType, rest } = at;
|
|
@@ -15,8 +15,7 @@ const stepIntoFields = (at) => {
|
|
|
15
15
|
kind: "fields",
|
|
16
16
|
rest
|
|
17
17
|
};
|
|
18
|
-
const
|
|
19
|
-
const slug = (isPlainObject(data) ? data["blockType"] : void 0) ?? (isPlainObject(added) ? added["blockType"] : void 0);
|
|
18
|
+
const slug = propOf(data, "blockType") ?? propOf(propOf(addedValue, "fields"), "blockType");
|
|
20
19
|
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
20
|
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
21
|
const block = blockOf(config, sub.blocksField, slug);
|
|
@@ -41,10 +40,11 @@ const stepIntoFields = (at) => {
|
|
|
41
40
|
descriptor,
|
|
42
41
|
field
|
|
43
42
|
};
|
|
44
|
-
|
|
43
|
+
const root = editorRoot(state);
|
|
44
|
+
if (!root) throw new SchemaError(`"${descriptor.path}" holds no editor state yet. Write the whole field once, then address positions inside it.`);
|
|
45
45
|
const [entry, ...rest] = at.segments;
|
|
46
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.`);
|
|
47
|
-
let node =
|
|
47
|
+
let node = root;
|
|
48
48
|
let nodeType = "root";
|
|
49
49
|
let segments = rest;
|
|
50
50
|
let walked = ["root"];
|
|
@@ -73,7 +73,7 @@ const stepIntoFields = (at) => {
|
|
|
73
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.`);
|
|
74
74
|
const children = node?.["children"];
|
|
75
75
|
const child = Array.isArray(children) && index !== "-" ? children[Number(index)] : void 0;
|
|
76
|
-
const type = isPlainObject(child) ? child["type"] : addedValue
|
|
76
|
+
const type = isPlainObject(child) ? child["type"] : propOf(addedValue, "type");
|
|
77
77
|
if (typeof type !== "string") {
|
|
78
78
|
if (beyond.length === 0) return {
|
|
79
79
|
kind: "position",
|
package/dist/schema/lexical.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isPlainObject, ownValue } from "../guards.mjs";
|
|
1
|
+
import { isPlainObject, ownValue, propOf } from "../guards.mjs";
|
|
2
2
|
import { flattenAllFields } from "payload";
|
|
3
3
|
//#region src/schema/lexical.ts
|
|
4
4
|
const LEXICAL_CORE_NODES = [
|
|
@@ -148,6 +148,16 @@ const UNIVERSAL_PROPERTIES = { version: "number" };
|
|
|
148
148
|
}
|
|
149
149
|
};
|
|
150
150
|
/**
|
|
151
|
+
* The `root` node of an editor state, if it has one.
|
|
152
|
+
*/ const editorRoot = (state) => {
|
|
153
|
+
const root = propOf(state, "root");
|
|
154
|
+
return isPlainObject(root) ? root : void 0;
|
|
155
|
+
};
|
|
156
|
+
const constraintsFor = (type) => ({
|
|
157
|
+
...UNIVERSAL_PROPERTIES,
|
|
158
|
+
...ownValue(REQUIRED_NODE_PROPERTIES, type)
|
|
159
|
+
});
|
|
160
|
+
/**
|
|
151
161
|
* Whether the table already says what a node type's `fields` must be, so the
|
|
152
162
|
* sub-field walk does not report the same problem twice.
|
|
153
163
|
*/ const constrainsFields = (type) => "fields" in (ownValue(REQUIRED_NODE_PROPERTIES, type) ?? {});
|
|
@@ -159,8 +169,7 @@ const describeConstraints = (constraints) => Object.fromEntries(Object.entries(c
|
|
|
159
169
|
* {@link UNIVERSAL_PROPERTIES} omits it: the validator finds the entry by
|
|
160
170
|
* `type` and so never reports it missing, but a client must still write one.
|
|
161
171
|
*/ const nodePropertiesFor = (types) => Object.fromEntries([...new Set(types)].sort().map((type) => [type, describeConstraints(type === "root" ? ROOT_PROPERTIES : {
|
|
162
|
-
...
|
|
163
|
-
...UNIVERSAL_PROPERTIES,
|
|
172
|
+
...constraintsFor(type),
|
|
164
173
|
type: "string"
|
|
165
174
|
})]));
|
|
166
175
|
const check = (node, constraints) => {
|
|
@@ -177,10 +186,7 @@ const check = (node, constraints) => {
|
|
|
177
186
|
problems.rejected.sort((left, right) => left.property.localeCompare(right.property));
|
|
178
187
|
return problems;
|
|
179
188
|
};
|
|
180
|
-
const nodeProblems = (node) => check(node,
|
|
181
|
-
...UNIVERSAL_PROPERTIES,
|
|
182
|
-
...ownValue(REQUIRED_NODE_PROPERTIES, node["type"])
|
|
183
|
-
});
|
|
189
|
+
const nodeProblems = (node) => check(node, constraintsFor(node["type"]));
|
|
184
190
|
/**
|
|
185
191
|
* The root is the one node Payload describes itself, down to refusing an
|
|
186
192
|
* unknown property, so it is checked against that description instead of the
|
|
@@ -194,11 +200,7 @@ const nodeProblems = (node) => check(node, {
|
|
|
194
200
|
* instead of a whole node. Absent where the table says nothing, matching the
|
|
195
201
|
* node walk: a project's own node may carry any property.
|
|
196
202
|
*/ const propertyProblem = (nodeType, property, value) => {
|
|
197
|
-
const
|
|
198
|
-
...UNIVERSAL_PROPERTIES,
|
|
199
|
-
...ownValue(REQUIRED_NODE_PROPERTIES, nodeType)
|
|
200
|
-
};
|
|
201
|
-
const constraint = ownValue(constraints, property);
|
|
203
|
+
const constraint = ownValue(nodeType === "root" ? ROOT_PROPERTIES : constraintsFor(nodeType), property);
|
|
202
204
|
return constraint === void 0 || accepts(constraint, value) ? void 0 : { needs: needs(constraint) };
|
|
203
205
|
};
|
|
204
206
|
const NODE_OPTION_SOURCES = [{
|
|
@@ -233,4 +235,4 @@ const stringList = (value) => Array.isArray(value) && value.every((entry) => typ
|
|
|
233
235
|
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
234
236
|
};
|
|
235
237
|
//#endregion
|
|
236
|
-
export { REQUIRED_NODE_PROPERTIES, ROOT_PROPERTIES, allowedNodeTypes, constrainsFields, lexicalSubSchema, nodeOptions, nodeProblems, nodePropertiesFor, propertyProblem, rootProblems, subSchemaNodeTypes };
|
|
238
|
+
export { REQUIRED_NODE_PROPERTIES, ROOT_PROPERTIES, allowedNodeTypes, constrainsFields, editorRoot, lexicalSubSchema, nodeOptions, nodeProblems, nodePropertiesFor, propertyProblem, rootProblems, subSchemaNodeTypes };
|