@abinnovision/payloadcms-mcpx 2.0.0-beta.16 → 2.0.0-beta.19
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 +16 -15
- package/dist/admin/capability-matrix.d.mts +2 -1
- package/dist/admin/capability-matrix.mjs +275 -221
- package/dist/api-keys/capability-matrix.d.mts +1 -1
- package/dist/api-keys/capability-matrix.mjs +4 -4
- package/dist/api-keys/collection.mjs +12 -16
- package/dist/api-keys/fields.mjs +33 -27
- package/dist/builtin-tool-names.mjs +1 -0
- package/dist/endpoint/server.mjs +5 -1
- package/dist/tools/builtin.mjs +2 -0
- package/dist/tools/create-document.mjs +35 -24
- package/dist/tools/duplicate-document.mjs +49 -0
- package/dist/types.d.mts +3 -2
- package/dist/write/draft-guard.mjs +6 -1
- package/dist/write/publish-intent.mjs +9 -1
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -116,19 +116,20 @@ every check.
|
|
|
116
116
|
|
|
117
117
|
## Tools
|
|
118
118
|
|
|
119
|
-
| Tool
|
|
120
|
-
|
|
|
121
|
-
| `listCapabilities`
|
|
122
|
-
| `describeSchema`
|
|
123
|
-
| `findDocuments`
|
|
124
|
-
| `getDocument`
|
|
125
|
-
| `findVersions`
|
|
126
|
-
| `patchDocument`
|
|
127
|
-
| `createDocument`
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
119
|
+
| Tool | What it does | Needs |
|
|
120
|
+
| ------------------- | ------------------------------------------------------------------ | --------- |
|
|
121
|
+
| `listCapabilities` | Lists what this key may do. Clients call it first. | any key |
|
|
122
|
+
| `describeSchema` | Describes the fields at one schema path, with the paths below it. | `read` |
|
|
123
|
+
| `findDocuments` | Queries a collection with a Payload `where`, `sort` and `select`. | `read` |
|
|
124
|
+
| `getDocument` | Reads a document, a subtree of it, an old version or a diff. | `read` |
|
|
125
|
+
| `findVersions` | Lists the version history of a document or global, without bodies. | `read` |
|
|
126
|
+
| `patchDocument` | Applies JSON Patch operations to the current draft, or a new file. | `write` |
|
|
127
|
+
| `createDocument` | Creates a draft from a seed, with a file in an upload collection. | `write` |
|
|
128
|
+
| `duplicateDocument` | Copies a document into a new draft, in every locale. | `write` |
|
|
129
|
+
| `validateDocument` | Lists what blocks publishing, without saving. | `write` |
|
|
130
|
+
| `publishDocument` | Publishes the current draft. | `publish` |
|
|
131
|
+
| `deleteDocument` | Asks to delete a document, which the key's user approves. | `delete` |
|
|
132
|
+
| `runConfirmed` | Runs the approved calls. | `delete` |
|
|
132
133
|
|
|
133
134
|
Collections and globals use the same tools. A tool that addresses one document takes either
|
|
134
135
|
`collection` and `id`, or `global` alone. [`docs/concepts.md`](./docs/concepts.md#tools) lists
|
|
@@ -144,7 +145,7 @@ each one is readable, writable and publishable. A key cannot go past it.
|
|
|
144
145
|
Each key carries one checkbox per exposed entity and operation (`read`, `write`, `publish`,
|
|
145
146
|
`delete`, `deleteUnattended`), and
|
|
146
147
|
one per custom tool. The admin panel shows them as a matrix with a row per entity, an Access
|
|
147
|
-
control (None, Read, Write, Publish) and a Delete
|
|
148
|
+
control (None, Read, Write, Publish) and a Delete checkbox with an approval shield. Each access level
|
|
148
149
|
includes the ones before it, and a level the config does not expose has no segment. `publish`
|
|
149
150
|
exists only where the config exposes `write` and `publish` on an entity with drafts, and counts
|
|
150
151
|
only when `write` is ticked as well. `write` counts only when `read` is ticked, so a key never
|
|
@@ -153,7 +154,7 @@ immediately.
|
|
|
153
154
|
`delete` exists only where the config sets `delete: true` on a collection, and counts only when
|
|
154
155
|
`read` is ticked as well. Setting access to None clears it. A delete runs only after the key's user
|
|
155
156
|
approved it on the key's edit view, except where the config sets `delete: "unattended"` and the
|
|
156
|
-
key's
|
|
157
|
+
key's approval shield is off: there the document moves to trash at once. See
|
|
157
158
|
[deleting](./docs/concepts.md#deleting).
|
|
158
159
|
|
|
159
160
|
`tools/list` follows the key. A tool the key cannot use is not listed, and each `collection` and
|
|
@@ -26,7 +26,8 @@ interface McpxCapabilityMatrixProps {
|
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* What an API key may do, as one table per namespace: a row per entity with
|
|
29
|
-
*
|
|
29
|
+
* an Access control and a Delete checkbox with an approval shield, and an
|
|
30
|
+
* "All" row that sets every row.
|
|
30
31
|
* The nested checkbox fields back every control, so the stored document is
|
|
31
32
|
* unaffected.
|
|
32
33
|
*/
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { CAPABILITIES_FIELD } from "../capabilities.mjs";
|
|
3
|
-
import { CAPABILITY_OPERATIONS,
|
|
3
|
+
import { CAPABILITY_OPERATIONS, capabilityPaths, toolPath } from "../api-keys/capability-matrix.mjs";
|
|
4
|
+
import { translateStatic } from "../i18n.mjs";
|
|
4
5
|
import { accessLevelOf, accessLevelsOf, allAccessLevel, allDeleteMode, buildToggleActions, deleteModeOf, toolsState } from "./capability-toggles.mjs";
|
|
5
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
-
import { Button, CheckboxInput, FieldDescription, FieldLabel, Pill, useForm, useFormFields } from "@payloadcms/ui";
|
|
7
|
+
import { Button, CheckboxInput, FieldDescription, FieldLabel, Pill, useConfig, useForm, useFormFields, useTranslation } from "@payloadcms/ui";
|
|
7
8
|
import { useCallback, useMemo } from "react";
|
|
8
9
|
//#region src/admin/capability-matrix.tsx
|
|
9
10
|
const BASE_CLASS = "mcpx-capabilities";
|
|
10
11
|
const LIVE_WRITE = "Writes go live immediately.";
|
|
11
12
|
const SHEET = `
|
|
12
13
|
.${BASE_CLASS} tbody tr:hover { background: var(--theme-elevation-50); }
|
|
13
|
-
.${BASE_CLASS} section:last-of-type { margin-bottom: 0; }
|
|
14
14
|
.${BASE_CLASS}__segments {
|
|
15
15
|
display: inline-flex;
|
|
16
16
|
border: 1px solid var(--theme-elevation-150);
|
|
@@ -22,12 +22,31 @@ const SHEET = `
|
|
|
22
22
|
.${BASE_CLASS}__segments .btn:first-child { border-start-start-radius: inherit; border-end-start-radius: inherit; }
|
|
23
23
|
.${BASE_CLASS}__segments .btn:last-child { border-start-end-radius: inherit; border-end-end-radius: inherit; }
|
|
24
24
|
.${BASE_CLASS}__segments .btn--style-transparent { --hover-bg: var(--theme-elevation-100); }
|
|
25
|
-
.${BASE_CLASS}
|
|
25
|
+
.${BASE_CLASS}__shield {
|
|
26
|
+
display: inline-flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
justify-content: center;
|
|
29
|
+
padding: calc(var(--base) * 0.2);
|
|
30
|
+
border: 0;
|
|
31
|
+
border-radius: var(--style-radius-s);
|
|
32
|
+
background: transparent;
|
|
33
|
+
color: var(--theme-elevation-800);
|
|
34
|
+
line-height: 0;
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
}
|
|
37
|
+
.${BASE_CLASS}__shield:hover:not(:disabled) { background: var(--theme-elevation-100); }
|
|
38
|
+
.${BASE_CLASS}__shield:disabled { cursor: not-allowed; opacity: 0.6; }
|
|
39
|
+
.${BASE_CLASS}__shield.${BASE_CLASS}__danger { color: var(--theme-error-500); }
|
|
26
40
|
`;
|
|
27
41
|
const CELL_PADDING = "calc(var(--base) * 0.6)";
|
|
28
42
|
const EDGE_PADDING = "calc(var(--base) * 0.8)";
|
|
29
43
|
const styles = {
|
|
30
|
-
|
|
44
|
+
sections: {
|
|
45
|
+
display: "flex",
|
|
46
|
+
flexWrap: "wrap",
|
|
47
|
+
gap: "var(--base) calc(var(--base) * 2)",
|
|
48
|
+
alignItems: "flex-start"
|
|
49
|
+
},
|
|
31
50
|
table: {
|
|
32
51
|
tableLayout: "fixed",
|
|
33
52
|
width: "auto",
|
|
@@ -96,7 +115,12 @@ const styles = {
|
|
|
96
115
|
verticalAlign: "middle",
|
|
97
116
|
textAlign: "start"
|
|
98
117
|
},
|
|
99
|
-
lastCell: { paddingInlineEnd: EDGE_PADDING }
|
|
118
|
+
lastCell: { paddingInlineEnd: EDGE_PADDING },
|
|
119
|
+
deleteControls: {
|
|
120
|
+
display: "inline-flex",
|
|
121
|
+
alignItems: "center",
|
|
122
|
+
gap: "calc(var(--base) * 0.4)"
|
|
123
|
+
}
|
|
100
124
|
};
|
|
101
125
|
const Box = ({ label, onChange, readOnly, state }) => /*#__PURE__*/ jsx(CheckboxInput, {
|
|
102
126
|
checked: state === "on",
|
|
@@ -105,7 +129,6 @@ const Box = ({ label, onChange, readOnly, state }) => /*#__PURE__*/ jsx(Checkbox
|
|
|
105
129
|
partialChecked: state === "mixed",
|
|
106
130
|
readOnly
|
|
107
131
|
});
|
|
108
|
-
const descriptionOf = (operation) => STORED_OPERATIONS.find((candidate) => candidate.id === operation)?.description;
|
|
109
132
|
const ACCESS_OPTIONS = [{
|
|
110
133
|
id: "none",
|
|
111
134
|
label: "None",
|
|
@@ -115,23 +138,6 @@ const ACCESS_OPTIONS = [{
|
|
|
115
138
|
label: operation.label,
|
|
116
139
|
title: operation.description
|
|
117
140
|
}))];
|
|
118
|
-
const DELETE_OPTIONS = [
|
|
119
|
-
{
|
|
120
|
-
id: "off",
|
|
121
|
-
label: "Off",
|
|
122
|
-
title: "No delete"
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
id: "approve",
|
|
126
|
-
label: "Approval",
|
|
127
|
-
title: descriptionOf("delete")
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
id: "trash",
|
|
131
|
-
label: "Trash",
|
|
132
|
-
title: descriptionOf("deleteUnattended")
|
|
133
|
-
}
|
|
134
|
-
];
|
|
135
141
|
const ARROW_STEPS = {
|
|
136
142
|
ArrowDown: 1,
|
|
137
143
|
ArrowLeft: -1,
|
|
@@ -162,7 +168,6 @@ const Segments = ({ included = [], label, onSelect, options, readOnly, selected
|
|
|
162
168
|
const checked = option.id === selected;
|
|
163
169
|
return /*#__PURE__*/ jsx(Button, {
|
|
164
170
|
buttonStyle: checked ? "primary" : included.includes(option.id) ? "pill" : "transparent",
|
|
165
|
-
className: checked && option.id === "trash" ? `${BASE_CLASS}__danger` : "",
|
|
166
171
|
disabled: readOnly,
|
|
167
172
|
extraButtonProps: {
|
|
168
173
|
"aria-checked": checked,
|
|
@@ -185,10 +190,10 @@ const includedBelow = (options, level) => {
|
|
|
185
190
|
return selected < 1 ? [] : options.slice(1, selected).map((option) => option.id);
|
|
186
191
|
};
|
|
187
192
|
const accessOptions = (rows) => ACCESS_OPTIONS.filter((option) => rows.some((row) => accessLevelsOf(row).includes(option.id)));
|
|
188
|
-
const LABEL_WIDTH = "calc(var(--base) *
|
|
193
|
+
const LABEL_WIDTH = "calc(var(--base) * 11)";
|
|
189
194
|
const OPERATION_WIDTH = "calc(var(--base) * 3.5)";
|
|
190
195
|
const ACCESS_WIDTH = "calc(var(--base) * 13)";
|
|
191
|
-
const DELETE_WIDTH = "calc(var(--base) *
|
|
196
|
+
const DELETE_WIDTH = "calc(var(--base) * 4)";
|
|
192
197
|
const Columns = ({ widths }) => /*#__PURE__*/ jsxs("colgroup", { children: [/*#__PURE__*/ jsx("col", { style: { width: LABEL_WIDTH } }), widths.map((width, index) => /*#__PURE__*/ jsx("col", { style: { width } }, index))] });
|
|
193
198
|
const lastCellStyle = {
|
|
194
199
|
...styles.cell,
|
|
@@ -213,225 +218,275 @@ const LiveBadge = () => /*#__PURE__*/ jsx("span", {
|
|
|
213
218
|
})]
|
|
214
219
|
})
|
|
215
220
|
});
|
|
221
|
+
const APPROVED_ALWAYS = "Always approved: the plugin config does not allow unattended deletes";
|
|
222
|
+
const ApprovalToggle = ({ label, locked = false, onToggle, operation, pressed, readOnly }) => {
|
|
223
|
+
const title = locked ? APPROVED_ALWAYS : pressed ? `A person approves each ${operation}` : `Each ${operation} runs without approval`;
|
|
224
|
+
return /*#__PURE__*/ jsxs("button", {
|
|
225
|
+
"aria-pressed": pressed,
|
|
226
|
+
className: [`${BASE_CLASS}__shield`, !pressed && `${BASE_CLASS}__danger`].filter(Boolean).join(" "),
|
|
227
|
+
disabled: readOnly || locked,
|
|
228
|
+
onClick: onToggle,
|
|
229
|
+
title,
|
|
230
|
+
type: "button",
|
|
231
|
+
children: [/*#__PURE__*/ jsxs("svg", {
|
|
232
|
+
"aria-hidden": "true",
|
|
233
|
+
fill: "none",
|
|
234
|
+
height: "16",
|
|
235
|
+
stroke: "currentColor",
|
|
236
|
+
strokeLinecap: "round",
|
|
237
|
+
strokeLinejoin: "round",
|
|
238
|
+
strokeWidth: "2",
|
|
239
|
+
viewBox: "0 0 24 24",
|
|
240
|
+
width: "16",
|
|
241
|
+
children: [/*#__PURE__*/ jsx("path", { d: "M12 3l8 3v6c0 4.5-3.2 8-8 9-4.8-1-8-4.5-8-9V6z" }), pressed ? /*#__PURE__*/ jsx("path", { d: "M9 12l2 2 4-4" }) : null]
|
|
242
|
+
}), /*#__PURE__*/ jsx("span", {
|
|
243
|
+
style: styles.visuallyHidden,
|
|
244
|
+
children: `${label}: ${title}`
|
|
245
|
+
})]
|
|
246
|
+
});
|
|
247
|
+
};
|
|
216
248
|
const NamespaceTable = ({ matrix, namespace, path, readOnly, toggle, values }) => {
|
|
217
249
|
const rows = matrix[namespace.id];
|
|
218
250
|
const withDelete = rows.some((row) => row.delete);
|
|
219
251
|
const allOptions = accessOptions(rows);
|
|
220
252
|
const allLevel = allAccessLevel(matrix, path, namespace.id, values);
|
|
221
253
|
const entries = namespace.title.toLowerCase();
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
...styles.headTitle,
|
|
235
|
-
...styles.topStart
|
|
236
|
-
},
|
|
237
|
-
children: namespace.title
|
|
238
|
-
}),
|
|
239
|
-
/*#__PURE__*/ jsx("th", {
|
|
240
|
-
scope: "col",
|
|
241
|
-
style: {
|
|
242
|
-
...styles.headOperation,
|
|
243
|
-
textAlign: "start",
|
|
244
|
-
...withDelete ? {} : styles.topEnd
|
|
245
|
-
},
|
|
246
|
-
children: "Access"
|
|
247
|
-
}),
|
|
248
|
-
withDelete ? /*#__PURE__*/ jsx("th", {
|
|
249
|
-
scope: "col",
|
|
250
|
-
style: {
|
|
251
|
-
...styles.headOperation,
|
|
252
|
-
textAlign: "start",
|
|
253
|
-
...styles.topEnd
|
|
254
|
-
},
|
|
255
|
-
children: "Delete"
|
|
256
|
-
}) : null
|
|
257
|
-
] }), /*#__PURE__*/ jsxs("tr", { children: [
|
|
258
|
-
/*#__PURE__*/ jsx("th", {
|
|
259
|
-
scope: "row",
|
|
260
|
-
style: {
|
|
261
|
-
...styles.headLabel,
|
|
262
|
-
...styles.rule
|
|
263
|
-
},
|
|
264
|
-
children: "All"
|
|
265
|
-
}),
|
|
266
|
-
/*#__PURE__*/ jsx("td", {
|
|
267
|
-
style: {
|
|
268
|
-
...controlStyle(!withDelete),
|
|
269
|
-
...styles.rule
|
|
270
|
-
},
|
|
271
|
-
children: /*#__PURE__*/ jsx(Segments, {
|
|
272
|
-
included: includedBelow(allOptions, allLevel),
|
|
273
|
-
label: `Access for every ${entries} entry`,
|
|
274
|
-
onSelect: (level) => {
|
|
275
|
-
toggle({
|
|
276
|
-
kind: "access",
|
|
277
|
-
namespace: namespace.id,
|
|
278
|
-
level
|
|
279
|
-
});
|
|
280
|
-
},
|
|
281
|
-
options: allOptions,
|
|
282
|
-
readOnly,
|
|
283
|
-
selected: allLevel
|
|
284
|
-
})
|
|
285
|
-
}),
|
|
286
|
-
withDelete ? /*#__PURE__*/ jsx("td", {
|
|
287
|
-
style: {
|
|
288
|
-
...controlStyle(true),
|
|
289
|
-
...styles.rule
|
|
290
|
-
},
|
|
291
|
-
children: /*#__PURE__*/ jsx(Segments, {
|
|
292
|
-
label: `Delete for every ${entries} entry`,
|
|
293
|
-
onSelect: (mode) => {
|
|
294
|
-
toggle({
|
|
295
|
-
kind: "deleteMode",
|
|
296
|
-
namespace: namespace.id,
|
|
297
|
-
mode
|
|
298
|
-
});
|
|
299
|
-
},
|
|
300
|
-
options: DELETE_OPTIONS.filter((option) => option.id !== "trash"),
|
|
301
|
-
readOnly,
|
|
302
|
-
selected: allDeleteMode(matrix, path, namespace.id, values)
|
|
303
|
-
})
|
|
304
|
-
}) : null
|
|
305
|
-
] })]
|
|
306
|
-
}),
|
|
307
|
-
/*#__PURE__*/ jsx("tbody", { children: rows.map((row) => {
|
|
308
|
-
const options = accessOptions([row]);
|
|
309
|
-
const level = accessLevelOf(path, namespace.id, row, values);
|
|
310
|
-
return /*#__PURE__*/ jsxs("tr", { children: [
|
|
311
|
-
/*#__PURE__*/ jsxs("th", {
|
|
312
|
-
scope: "row",
|
|
313
|
-
style: styles.rowHeader,
|
|
314
|
-
children: [row.label, row.live ? /*#__PURE__*/ jsx(LiveBadge, {}) : null]
|
|
315
|
-
}),
|
|
316
|
-
/*#__PURE__*/ jsx("td", {
|
|
317
|
-
style: controlStyle(!withDelete),
|
|
318
|
-
children: /*#__PURE__*/ jsx(Segments, {
|
|
319
|
-
included: includedBelow(options, level),
|
|
320
|
-
label: `Access for ${row.label}`,
|
|
321
|
-
onSelect: (next) => {
|
|
322
|
-
toggle({
|
|
323
|
-
kind: "access",
|
|
324
|
-
namespace: namespace.id,
|
|
325
|
-
fieldName: row.fieldName,
|
|
326
|
-
level: next
|
|
327
|
-
});
|
|
328
|
-
},
|
|
329
|
-
options,
|
|
330
|
-
readOnly,
|
|
331
|
-
selected: level
|
|
332
|
-
})
|
|
333
|
-
}),
|
|
334
|
-
withDelete ? /*#__PURE__*/ jsx("td", {
|
|
335
|
-
style: controlStyle(true),
|
|
336
|
-
children: row.delete ? /*#__PURE__*/ jsx(Segments, {
|
|
337
|
-
label: `Delete for ${row.label}`,
|
|
338
|
-
onSelect: (mode) => {
|
|
339
|
-
toggle({
|
|
340
|
-
kind: "deleteMode",
|
|
341
|
-
namespace: namespace.id,
|
|
342
|
-
fieldName: row.fieldName,
|
|
343
|
-
mode
|
|
344
|
-
});
|
|
345
|
-
},
|
|
346
|
-
options: DELETE_OPTIONS.filter((option) => option.id !== "trash" || row.deleteUnattended),
|
|
347
|
-
readOnly,
|
|
348
|
-
selected: deleteModeOf(path, namespace.id, row, values)
|
|
349
|
-
}) : null
|
|
350
|
-
}) : null
|
|
351
|
-
] }, row.fieldName);
|
|
352
|
-
}) })
|
|
353
|
-
]
|
|
354
|
-
})
|
|
355
|
-
});
|
|
356
|
-
};
|
|
357
|
-
const ToolsTable = ({ matrix, path, readOnly, toggle, values }) => {
|
|
358
|
-
const toolState = toolsState(matrix, path, values);
|
|
359
|
-
return /*#__PURE__*/ jsx("section", {
|
|
360
|
-
style: styles.section,
|
|
361
|
-
children: /*#__PURE__*/ jsxs("table", {
|
|
362
|
-
style: styles.table,
|
|
363
|
-
children: [
|
|
364
|
-
/*#__PURE__*/ jsx(Columns, { widths: [OPERATION_WIDTH] }),
|
|
365
|
-
/*#__PURE__*/ jsxs("thead", {
|
|
366
|
-
style: styles.head,
|
|
367
|
-
children: [/*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsx("th", {
|
|
254
|
+
const { i18n } = useTranslation();
|
|
255
|
+
const { getEntityConfig } = useConfig();
|
|
256
|
+
const nameOf = (row) => translateStatic(namespace.id === "collections" ? getEntityConfig({ collectionSlug: row.slug }).labels.plural : getEntityConfig({ globalSlug: row.slug }).label, i18n) ?? row.slug;
|
|
257
|
+
const allDelete = allDeleteMode(matrix, path, namespace.id, values);
|
|
258
|
+
return /*#__PURE__*/ jsx("section", { children: /*#__PURE__*/ jsxs("table", {
|
|
259
|
+
style: styles.table,
|
|
260
|
+
children: [
|
|
261
|
+
/*#__PURE__*/ jsx(Columns, { widths: withDelete ? [ACCESS_WIDTH, DELETE_WIDTH] : [ACCESS_WIDTH] }),
|
|
262
|
+
/*#__PURE__*/ jsxs("thead", {
|
|
263
|
+
style: styles.head,
|
|
264
|
+
children: [/*#__PURE__*/ jsxs("tr", { children: [
|
|
265
|
+
/*#__PURE__*/ jsx("th", {
|
|
368
266
|
scope: "col",
|
|
369
267
|
style: {
|
|
370
268
|
...styles.headTitle,
|
|
371
269
|
...styles.topStart
|
|
372
270
|
},
|
|
373
|
-
children:
|
|
374
|
-
}),
|
|
271
|
+
children: namespace.title
|
|
272
|
+
}),
|
|
273
|
+
/*#__PURE__*/ jsx("th", {
|
|
274
|
+
scope: "col",
|
|
275
|
+
style: {
|
|
276
|
+
...styles.headOperation,
|
|
277
|
+
textAlign: "start",
|
|
278
|
+
...withDelete ? {} : styles.topEnd
|
|
279
|
+
},
|
|
280
|
+
children: "Access"
|
|
281
|
+
}),
|
|
282
|
+
withDelete ? /*#__PURE__*/ jsx("th", {
|
|
375
283
|
scope: "col",
|
|
376
284
|
style: {
|
|
377
285
|
...styles.headOperation,
|
|
286
|
+
textAlign: "start",
|
|
378
287
|
...styles.topEnd
|
|
379
288
|
},
|
|
380
|
-
children: "
|
|
381
|
-
})
|
|
289
|
+
children: "Delete"
|
|
290
|
+
}) : null
|
|
291
|
+
] }), /*#__PURE__*/ jsxs("tr", { children: [
|
|
292
|
+
/*#__PURE__*/ jsx("th", {
|
|
382
293
|
scope: "row",
|
|
383
294
|
style: {
|
|
384
295
|
...styles.headLabel,
|
|
385
296
|
...styles.rule
|
|
386
297
|
},
|
|
387
298
|
children: "All"
|
|
388
|
-
}),
|
|
299
|
+
}),
|
|
300
|
+
/*#__PURE__*/ jsx("td", {
|
|
301
|
+
style: {
|
|
302
|
+
...controlStyle(!withDelete),
|
|
303
|
+
...styles.rule
|
|
304
|
+
},
|
|
305
|
+
children: /*#__PURE__*/ jsx(Segments, {
|
|
306
|
+
included: includedBelow(allOptions, allLevel),
|
|
307
|
+
label: `Access for every ${entries} entry`,
|
|
308
|
+
onSelect: (level) => {
|
|
309
|
+
toggle({
|
|
310
|
+
kind: "access",
|
|
311
|
+
namespace: namespace.id,
|
|
312
|
+
level
|
|
313
|
+
});
|
|
314
|
+
},
|
|
315
|
+
options: allOptions,
|
|
316
|
+
readOnly,
|
|
317
|
+
selected: allLevel
|
|
318
|
+
})
|
|
319
|
+
}),
|
|
320
|
+
withDelete ? /*#__PURE__*/ jsx("td", {
|
|
389
321
|
style: {
|
|
390
|
-
...
|
|
322
|
+
...controlStyle(true),
|
|
391
323
|
...styles.rule
|
|
392
324
|
},
|
|
393
325
|
children: /*#__PURE__*/ jsx(Box, {
|
|
394
|
-
label:
|
|
326
|
+
label: `Delete for every ${entries} entry`,
|
|
395
327
|
onChange: () => {
|
|
396
328
|
toggle({
|
|
397
|
-
kind: "
|
|
398
|
-
|
|
329
|
+
kind: "deleteMode",
|
|
330
|
+
namespace: namespace.id,
|
|
331
|
+
mode: allDelete === "off" || allDelete === void 0 ? "approve" : "off"
|
|
332
|
+
});
|
|
333
|
+
},
|
|
334
|
+
readOnly,
|
|
335
|
+
state: allDelete === void 0 ? "mixed" : allDelete === "off" ? "off" : "on"
|
|
336
|
+
})
|
|
337
|
+
}) : null
|
|
338
|
+
] })]
|
|
339
|
+
}),
|
|
340
|
+
/*#__PURE__*/ jsx("tbody", { children: rows.map((row) => {
|
|
341
|
+
const options = accessOptions([row]);
|
|
342
|
+
const level = accessLevelOf(path, namespace.id, row, values);
|
|
343
|
+
const mode = deleteModeOf(path, namespace.id, row, values);
|
|
344
|
+
const name = nameOf(row);
|
|
345
|
+
return /*#__PURE__*/ jsxs("tr", { children: [
|
|
346
|
+
/*#__PURE__*/ jsxs("th", {
|
|
347
|
+
scope: "row",
|
|
348
|
+
style: styles.rowHeader,
|
|
349
|
+
children: [
|
|
350
|
+
name,
|
|
351
|
+
row.live ? /*#__PURE__*/ jsx(LiveBadge, {}) : null,
|
|
352
|
+
name === row.slug ? null : /*#__PURE__*/ jsx("span", {
|
|
353
|
+
style: styles.hint,
|
|
354
|
+
children: row.slug
|
|
355
|
+
})
|
|
356
|
+
]
|
|
357
|
+
}),
|
|
358
|
+
/*#__PURE__*/ jsx("td", {
|
|
359
|
+
style: controlStyle(!withDelete),
|
|
360
|
+
children: /*#__PURE__*/ jsx(Segments, {
|
|
361
|
+
included: includedBelow(options, level),
|
|
362
|
+
label: `Access for ${name}`,
|
|
363
|
+
onSelect: (next) => {
|
|
364
|
+
toggle({
|
|
365
|
+
kind: "access",
|
|
366
|
+
namespace: namespace.id,
|
|
367
|
+
fieldName: row.fieldName,
|
|
368
|
+
level: next
|
|
399
369
|
});
|
|
400
370
|
},
|
|
371
|
+
options,
|
|
401
372
|
readOnly,
|
|
402
|
-
|
|
373
|
+
selected: level
|
|
403
374
|
})
|
|
404
|
-
})
|
|
405
|
-
|
|
406
|
-
|
|
375
|
+
}),
|
|
376
|
+
withDelete ? /*#__PURE__*/ jsx("td", {
|
|
377
|
+
style: controlStyle(true),
|
|
378
|
+
children: row.delete ? /*#__PURE__*/ jsxs("span", {
|
|
379
|
+
style: styles.deleteControls,
|
|
380
|
+
children: [/*#__PURE__*/ jsx(Box, {
|
|
381
|
+
label: `Delete ${name}`,
|
|
382
|
+
onChange: () => {
|
|
383
|
+
toggle({
|
|
384
|
+
kind: "deleteMode",
|
|
385
|
+
namespace: namespace.id,
|
|
386
|
+
fieldName: row.fieldName,
|
|
387
|
+
mode: mode === "off" ? "approve" : "off"
|
|
388
|
+
});
|
|
389
|
+
},
|
|
390
|
+
readOnly,
|
|
391
|
+
state: mode === "off" ? "off" : "on"
|
|
392
|
+
}), mode === "off" ? null : /*#__PURE__*/ jsx(ApprovalToggle, {
|
|
393
|
+
label: `Approval for deleting ${name}`,
|
|
394
|
+
locked: !row.deleteUnattended,
|
|
395
|
+
onToggle: () => {
|
|
396
|
+
toggle({
|
|
397
|
+
kind: "deleteMode",
|
|
398
|
+
namespace: namespace.id,
|
|
399
|
+
fieldName: row.fieldName,
|
|
400
|
+
mode: mode === "trash" ? "approve" : "trash"
|
|
401
|
+
});
|
|
402
|
+
},
|
|
403
|
+
operation: "delete",
|
|
404
|
+
pressed: mode !== "trash",
|
|
405
|
+
readOnly
|
|
406
|
+
})]
|
|
407
|
+
}) : null
|
|
408
|
+
}) : null
|
|
409
|
+
] }, row.fieldName);
|
|
410
|
+
}) })
|
|
411
|
+
]
|
|
412
|
+
}) });
|
|
413
|
+
};
|
|
414
|
+
const ToolsTable = ({ matrix, path, readOnly, toggle, values }) => {
|
|
415
|
+
const toolState = toolsState(matrix, path, values);
|
|
416
|
+
return /*#__PURE__*/ jsx("section", { children: /*#__PURE__*/ jsxs("table", {
|
|
417
|
+
style: styles.table,
|
|
418
|
+
children: [
|
|
419
|
+
/*#__PURE__*/ jsx(Columns, { widths: [OPERATION_WIDTH] }),
|
|
420
|
+
/*#__PURE__*/ jsxs("thead", {
|
|
421
|
+
style: styles.head,
|
|
422
|
+
children: [/*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsx("th", {
|
|
423
|
+
scope: "col",
|
|
424
|
+
style: {
|
|
425
|
+
...styles.headTitle,
|
|
426
|
+
...styles.topStart
|
|
427
|
+
},
|
|
428
|
+
children: "Tools"
|
|
429
|
+
}), /*#__PURE__*/ jsx("th", {
|
|
430
|
+
scope: "col",
|
|
431
|
+
style: {
|
|
432
|
+
...styles.headOperation,
|
|
433
|
+
...styles.topEnd
|
|
434
|
+
},
|
|
435
|
+
children: "Enabled"
|
|
436
|
+
})] }), /*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsx("th", {
|
|
407
437
|
scope: "row",
|
|
408
|
-
style:
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
438
|
+
style: {
|
|
439
|
+
...styles.headLabel,
|
|
440
|
+
...styles.rule
|
|
441
|
+
},
|
|
442
|
+
children: "All"
|
|
413
443
|
}), /*#__PURE__*/ jsx("td", {
|
|
414
|
-
style:
|
|
444
|
+
style: {
|
|
445
|
+
...lastCellStyle,
|
|
446
|
+
...styles.rule
|
|
447
|
+
},
|
|
415
448
|
children: /*#__PURE__*/ jsx(Box, {
|
|
416
|
-
label:
|
|
449
|
+
label: "Enable every tool",
|
|
417
450
|
onChange: () => {
|
|
418
451
|
toggle({
|
|
419
|
-
kind: "
|
|
420
|
-
|
|
421
|
-
value: values[toolPath(path, tool.name)] !== true
|
|
452
|
+
kind: "tools",
|
|
453
|
+
value: toolState !== "on"
|
|
422
454
|
});
|
|
423
455
|
},
|
|
424
456
|
readOnly,
|
|
425
|
-
state:
|
|
457
|
+
state: toolState
|
|
426
458
|
})
|
|
427
|
-
})] }
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
459
|
+
})] })]
|
|
460
|
+
}),
|
|
461
|
+
/*#__PURE__*/ jsx("tbody", { children: matrix.tools.map((tool) => /*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsxs("th", {
|
|
462
|
+
scope: "row",
|
|
463
|
+
style: styles.rowHeader,
|
|
464
|
+
children: [tool.name, /*#__PURE__*/ jsx("span", {
|
|
465
|
+
style: styles.hint,
|
|
466
|
+
children: tool.description
|
|
467
|
+
})]
|
|
468
|
+
}), /*#__PURE__*/ jsx("td", {
|
|
469
|
+
style: lastCellStyle,
|
|
470
|
+
children: /*#__PURE__*/ jsx(Box, {
|
|
471
|
+
label: `Enable ${tool.name}`,
|
|
472
|
+
onChange: () => {
|
|
473
|
+
toggle({
|
|
474
|
+
kind: "tool",
|
|
475
|
+
name: tool.name,
|
|
476
|
+
value: values[toolPath(path, tool.name)] !== true
|
|
477
|
+
});
|
|
478
|
+
},
|
|
479
|
+
readOnly,
|
|
480
|
+
state: values[toolPath(path, tool.name)] === true ? "on" : "off"
|
|
481
|
+
})
|
|
482
|
+
})] }, tool.name)) })
|
|
483
|
+
]
|
|
484
|
+
}) });
|
|
431
485
|
};
|
|
432
486
|
/**
|
|
433
487
|
* What an API key may do, as one table per namespace: a row per entity with
|
|
434
|
-
*
|
|
488
|
+
* an Access control and a Delete checkbox with an approval shield, and an
|
|
489
|
+
* "All" row that sets every row.
|
|
435
490
|
* The nested checkbox fields back every control, so the stored document is
|
|
436
491
|
* unaffected.
|
|
437
492
|
*/ const McpxCapabilityMatrix = ({ field, matrix, path = CAPABILITIES_FIELD, readOnly = false, withinTab = false }) => {
|
|
@@ -468,36 +523,35 @@ const ToolsTable = ({ matrix, path, readOnly, toggle, values }) => {
|
|
|
468
523
|
].filter(Boolean).join(" "),
|
|
469
524
|
children: [/*#__PURE__*/ jsx("style", { children: SHEET }), /*#__PURE__*/ jsxs("div", {
|
|
470
525
|
className: "group-field__wrap",
|
|
471
|
-
children: [
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
namespaces.map((namespace) => /*#__PURE__*/ jsx(NamespaceTable, {
|
|
526
|
+
children: [/*#__PURE__*/ jsx("div", {
|
|
527
|
+
className: "group-field__header",
|
|
528
|
+
children: /*#__PURE__*/ jsxs("header", { children: [withinTab ? null : /*#__PURE__*/ jsx("h3", {
|
|
529
|
+
className: "group-field__title",
|
|
530
|
+
children: /*#__PURE__*/ jsx(FieldLabel, {
|
|
531
|
+
as: "span",
|
|
532
|
+
label: "Capabilities"
|
|
533
|
+
})
|
|
534
|
+
}), typeof field?.admin?.description === "string" ? /*#__PURE__*/ jsx(FieldDescription, {
|
|
535
|
+
description: field.admin.description,
|
|
536
|
+
path
|
|
537
|
+
}) : null] })
|
|
538
|
+
}), /*#__PURE__*/ jsxs("div", {
|
|
539
|
+
style: styles.sections,
|
|
540
|
+
children: [namespaces.map((namespace) => /*#__PURE__*/ jsx(NamespaceTable, {
|
|
486
541
|
matrix,
|
|
487
542
|
namespace,
|
|
488
543
|
path,
|
|
489
544
|
readOnly,
|
|
490
545
|
toggle,
|
|
491
546
|
values
|
|
492
|
-
}, namespace.id)),
|
|
493
|
-
matrix.tools.length > 0 ? /*#__PURE__*/ jsx(ToolsTable, {
|
|
547
|
+
}, namespace.id)), matrix.tools.length > 0 ? /*#__PURE__*/ jsx(ToolsTable, {
|
|
494
548
|
matrix,
|
|
495
549
|
path,
|
|
496
550
|
readOnly,
|
|
497
551
|
toggle,
|
|
498
552
|
values
|
|
499
|
-
}) : null
|
|
500
|
-
]
|
|
553
|
+
}) : null]
|
|
554
|
+
})]
|
|
501
555
|
})]
|
|
502
556
|
});
|
|
503
557
|
};
|
|
@@ -2,8 +2,8 @@ import { CAPABILITIES_FIELD, canPublish, canWrite, isLiveWrite } from "../capabi
|
|
|
2
2
|
//#region src/api-keys/capability-matrix.ts
|
|
3
3
|
/**
|
|
4
4
|
* The group's `admin.description`, rendered by the matrix in Payload's
|
|
5
|
-
* description slot. It explains how to read the
|
|
6
|
-
*/ const CAPABILITIES_DESCRIPTION = "What this key may do. Each level includes the ones before it, and a missing segment means the plugin config does not expose it.";
|
|
5
|
+
* description slot. It explains how to read the controls.
|
|
6
|
+
*/ const CAPABILITIES_DESCRIPTION = "What this key may do. Each level includes the ones before it, and a missing segment means the plugin config does not expose it. A shield means this key's user approves each call.";
|
|
7
7
|
/**
|
|
8
8
|
* The operations a collection or global can expose. The generated checkboxes
|
|
9
9
|
* and the matrix segments share this wording. `requires` names the
|
|
@@ -34,7 +34,7 @@ import { CAPABILITIES_FIELD, canPublish, canWrite, isLiveWrite } from "../capabi
|
|
|
34
34
|
}
|
|
35
35
|
];
|
|
36
36
|
/**
|
|
37
|
-
* Stored as a checkbox of its own but drawn as the
|
|
37
|
+
* Stored as a checkbox of its own but drawn as the approval shield of the Delete
|
|
38
38
|
* control.
|
|
39
39
|
*/ const DELETE_UNATTENDED = {
|
|
40
40
|
id: "deleteUnattended",
|
|
@@ -48,7 +48,7 @@ import { CAPABILITIES_FIELD, canPublish, canWrite, isLiveWrite } from "../capabi
|
|
|
48
48
|
*/ const STORED_OPERATIONS = [...CAPABILITY_OPERATIONS, DELETE_UNATTENDED];
|
|
49
49
|
const toRow = (entity) => ({
|
|
50
50
|
fieldName: entity.fieldName,
|
|
51
|
-
|
|
51
|
+
slug: entity.slug,
|
|
52
52
|
read: entity.read,
|
|
53
53
|
write: canWrite(entity),
|
|
54
54
|
publish: canPublish(entity),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createCapabilityFields, createConfirmationFields, createKeyFields,
|
|
1
|
+
import { createCapabilityFields, createConfirmationFields, createKeyFields, withKeyTabs } from "./fields.mjs";
|
|
2
2
|
import { generateApiKey, hashApiKey } from "./key.mjs";
|
|
3
3
|
//#region src/api-keys/collection.ts
|
|
4
4
|
/**
|
|
@@ -37,22 +37,18 @@ import { generateApiKey, hashApiKey } from "./key.mjs";
|
|
|
37
37
|
delete: ownKeysOnly
|
|
38
38
|
},
|
|
39
39
|
hooks: { beforeChange: [keyBeforeChange] },
|
|
40
|
-
fields: [...createConfirmationFields(options), ...
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
update: () => false
|
|
49
|
-
},
|
|
50
|
-
defaultValue: ({ req }) => isUser({ req }) ? req.user?.id : void 0,
|
|
51
|
-
admin: { description: "The user this key acts as." }
|
|
40
|
+
fields: [...createConfirmationFields(options), ...withKeyTabs([{
|
|
41
|
+
name: "user",
|
|
42
|
+
type: "relationship",
|
|
43
|
+
relationTo: userCollection,
|
|
44
|
+
required: true,
|
|
45
|
+
access: {
|
|
46
|
+
create: () => false,
|
|
47
|
+
update: () => false
|
|
52
48
|
},
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
], options)]
|
|
49
|
+
defaultValue: ({ req }) => isUser({ req }) ? req.user?.id : void 0,
|
|
50
|
+
admin: { description: "The user this key acts as." }
|
|
51
|
+
}, ...createKeyFields()], createCapabilityFields(options), options)]
|
|
56
52
|
};
|
|
57
53
|
};
|
|
58
54
|
//#endregion
|
package/dist/api-keys/fields.mjs
CHANGED
|
@@ -96,34 +96,40 @@ const SETUP_GUIDE_FIELD = "setupGuide";
|
|
|
96
96
|
}
|
|
97
97
|
}] : [];
|
|
98
98
|
/**
|
|
99
|
-
* Wraps the key fields and the setup guide in unnamed tabs,
|
|
100
|
-
* get the full form width. Named tabs would nest
|
|
101
|
-
* `capabilities` off the document root, which capability
|
|
99
|
+
* Wraps the key fields, the capabilities and the setup guide in unnamed tabs,
|
|
100
|
+
* so wide tables and snippets get the full form width. Named tabs would nest
|
|
101
|
+
* the data and move `capabilities` off the document root, which capability
|
|
102
|
+
* resolution reads.
|
|
102
103
|
*
|
|
103
104
|
* The guide tab only shows on update: on create there is no key to hand out.
|
|
104
|
-
*/ const
|
|
105
|
-
|
|
105
|
+
*/ const withKeyTabs = (keyFields, capabilityFields, options) => {
|
|
106
|
+
const tabs = [{
|
|
107
|
+
label: "Key",
|
|
108
|
+
fields: keyFields
|
|
109
|
+
}];
|
|
110
|
+
if (capabilityFields.length > 0) tabs.push({
|
|
111
|
+
label: "Capabilities",
|
|
112
|
+
fields: capabilityFields
|
|
113
|
+
});
|
|
114
|
+
if (options.setupGuide) tabs.push({
|
|
115
|
+
label: "Connect a client",
|
|
116
|
+
admin: { condition: (_data, _siblingData, { operation }) => operation === "update" },
|
|
117
|
+
fields: [{
|
|
118
|
+
name: SETUP_GUIDE_FIELD,
|
|
119
|
+
type: "ui",
|
|
120
|
+
admin: {
|
|
121
|
+
disableListColumn: true,
|
|
122
|
+
components: { Field: {
|
|
123
|
+
path: "@abinnovision/payloadcms-mcpx/admin",
|
|
124
|
+
exportName: "McpxSetupGuide",
|
|
125
|
+
clientProps: { endpointPath: options.endpointPath }
|
|
126
|
+
} }
|
|
127
|
+
}
|
|
128
|
+
}]
|
|
129
|
+
});
|
|
106
130
|
return [{
|
|
107
131
|
type: "tabs",
|
|
108
|
-
tabs
|
|
109
|
-
label: "Key",
|
|
110
|
-
fields: keyFields
|
|
111
|
-
}, {
|
|
112
|
-
label: "Connect a client",
|
|
113
|
-
admin: { condition: (_data, _siblingData, { operation }) => operation === "update" },
|
|
114
|
-
fields: [{
|
|
115
|
-
name: SETUP_GUIDE_FIELD,
|
|
116
|
-
type: "ui",
|
|
117
|
-
admin: {
|
|
118
|
-
disableListColumn: true,
|
|
119
|
-
components: { Field: {
|
|
120
|
-
path: "@abinnovision/payloadcms-mcpx/admin",
|
|
121
|
-
exportName: "McpxSetupGuide",
|
|
122
|
-
clientProps: { endpointPath: options.endpointPath }
|
|
123
|
-
} }
|
|
124
|
-
}
|
|
125
|
-
}]
|
|
126
|
-
}]
|
|
132
|
+
tabs
|
|
127
133
|
}];
|
|
128
134
|
};
|
|
129
135
|
/**
|
|
@@ -140,7 +146,7 @@ const SETUP_GUIDE_FIELD = "setupGuide";
|
|
|
140
146
|
const entityGroups = (rows) => rows.map((row) => ({
|
|
141
147
|
name: row.fieldName,
|
|
142
148
|
type: "group",
|
|
143
|
-
label: row.
|
|
149
|
+
label: row.slug,
|
|
144
150
|
fields: STORED_OPERATIONS.filter((operation) => row[operation.id]).map((operation) => checkbox(operation.id, operation.description))
|
|
145
151
|
}));
|
|
146
152
|
const collectionGroups = entityGroups(matrix.collections);
|
|
@@ -174,7 +180,7 @@ const SETUP_GUIDE_FIELD = "setupGuide";
|
|
|
174
180
|
exportName: "McpxCapabilityMatrix",
|
|
175
181
|
clientProps: {
|
|
176
182
|
matrix,
|
|
177
|
-
withinTab:
|
|
183
|
+
withinTab: true
|
|
178
184
|
}
|
|
179
185
|
} }
|
|
180
186
|
},
|
|
@@ -182,4 +188,4 @@ const SETUP_GUIDE_FIELD = "setupGuide";
|
|
|
182
188
|
}];
|
|
183
189
|
};
|
|
184
190
|
//#endregion
|
|
185
|
-
export { createCapabilityFields, createConfirmationFields, createKeyFields,
|
|
191
|
+
export { createCapabilityFields, createConfirmationFields, createKeyFields, withKeyTabs };
|
package/dist/endpoint/server.mjs
CHANGED
package/dist/tools/builtin.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createDocument } from "./create-document.mjs";
|
|
2
2
|
import { deleteDocument } from "./delete-document.mjs";
|
|
3
3
|
import { describeSchema } from "./describe-schema.mjs";
|
|
4
|
+
import { duplicateDocument } from "./duplicate-document.mjs";
|
|
4
5
|
import { findDocuments } from "./find-documents.mjs";
|
|
5
6
|
import { findVersions } from "./find-versions.mjs";
|
|
6
7
|
import { getDocument } from "./get-document.mjs";
|
|
@@ -23,6 +24,7 @@ import { validateDocument } from "./validate-document.mjs";
|
|
|
23
24
|
findVersions,
|
|
24
25
|
patchDocument,
|
|
25
26
|
createDocument,
|
|
27
|
+
duplicateDocument,
|
|
26
28
|
validateDocument,
|
|
27
29
|
publishDocument,
|
|
28
30
|
deleteDocument,
|
|
@@ -19,6 +19,36 @@ const DESCRIPTION = (scope) => `Creates a document in a collection. Use it only
|
|
|
19
19
|
|
|
20
20
|
${liveWriteSentence(scope, "create")}${uploadSentence(scope)}`;
|
|
21
21
|
/**
|
|
22
|
+
* What createDocument and duplicateDocument return: the new document, re-read
|
|
23
|
+
* privileged in `locale`, with its links and publish blockers.
|
|
24
|
+
*/ const createdResult = async (scope, args) => {
|
|
25
|
+
const { target, locale } = args;
|
|
26
|
+
const saved = await readDraft(scope, {
|
|
27
|
+
target: {
|
|
28
|
+
...target,
|
|
29
|
+
id: args.id
|
|
30
|
+
},
|
|
31
|
+
locale,
|
|
32
|
+
privileged: true
|
|
33
|
+
});
|
|
34
|
+
const validation = await collectPublishBlockers(scope.req, {
|
|
35
|
+
doc: saved,
|
|
36
|
+
entity: target
|
|
37
|
+
});
|
|
38
|
+
return jsonResult({
|
|
39
|
+
id: saved["id"],
|
|
40
|
+
status: saved["_status"],
|
|
41
|
+
updatedAt: saved["updatedAt"],
|
|
42
|
+
...await documentLinks(scope.req, {
|
|
43
|
+
target,
|
|
44
|
+
doc: saved,
|
|
45
|
+
locale
|
|
46
|
+
}),
|
|
47
|
+
...validation.blockers.length > 0 ? { publishBlockers: validation.blockers } : {},
|
|
48
|
+
...validation.unavailable ? { publishBlockersUnavailable: true } : {}
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
22
52
|
* Collection-only, because a global always exists. A create in an upload
|
|
23
53
|
* collection needs `file`: the call then returns an upload grant, and the PUT
|
|
24
54
|
* to it runs the call again with the bytes.
|
|
@@ -89,31 +119,12 @@ ${liveWriteSentence(scope, "create")}${uploadSentence(scope)}`;
|
|
|
89
119
|
...locale === void 0 ? {} : { locale },
|
|
90
120
|
...bytes === void 0 ? {} : { file: bytes }
|
|
91
121
|
});
|
|
92
|
-
|
|
93
|
-
target
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
},
|
|
97
|
-
locale,
|
|
98
|
-
privileged: true
|
|
99
|
-
});
|
|
100
|
-
const validation = await collectPublishBlockers(scope.req, {
|
|
101
|
-
doc: saved,
|
|
102
|
-
entity: target
|
|
103
|
-
});
|
|
104
|
-
return jsonResult({
|
|
105
|
-
id: saved["id"],
|
|
106
|
-
status: saved["_status"],
|
|
107
|
-
updatedAt: saved["updatedAt"],
|
|
108
|
-
...await documentLinks(scope.req, {
|
|
109
|
-
target,
|
|
110
|
-
doc: saved,
|
|
111
|
-
locale
|
|
112
|
-
}),
|
|
113
|
-
...validation.blockers.length > 0 ? { publishBlockers: validation.blockers } : {},
|
|
114
|
-
...validation.unavailable ? { publishBlockersUnavailable: true } : {}
|
|
122
|
+
return await createdResult(scope, {
|
|
123
|
+
target,
|
|
124
|
+
id: created["id"],
|
|
125
|
+
locale
|
|
115
126
|
});
|
|
116
127
|
}
|
|
117
128
|
});
|
|
118
129
|
//#endregion
|
|
119
|
-
export { createDocument };
|
|
130
|
+
export { createDocument, createdResult };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { defineMcpxTool } from "../define-tool.mjs";
|
|
2
|
+
import { idSchema, liveWriteSentence, localeOf, slugEnum, slugsFor } from "./shared.mjs";
|
|
3
|
+
import { resolveEntity } from "./document.mjs";
|
|
4
|
+
import { createdResult } from "./create-document.mjs";
|
|
5
|
+
import { withDuplicateIntent } from "../write/publish-intent.mjs";
|
|
6
|
+
import { Forbidden } from "payload";
|
|
7
|
+
//#region src/tools/duplicate-document.ts
|
|
8
|
+
const duplicableSlugs = (scope) => slugsFor(scope, "create").collections.filter((slug) => scope.req.payload.config.collections.find((collection) => collection.slug === slug)?.disableDuplicate !== true && !scope.exposure.collections.some((entity) => entity.slug === slug && entity.isUpload));
|
|
9
|
+
const DESCRIPTION = (scope) => `Copies a document into a new one in the same collection. Use it instead of getDocument and createDocument to copy a document. It copies the latest version, the draft if there is one, in every locale. Unique text fields get " - Copy" appended. Returns the same fields as createDocument. To rename the copy, call patchDocument with the returned "updatedAt".
|
|
10
|
+
|
|
11
|
+
${liveWriteSentence(scope, "create")}`;
|
|
12
|
+
/**
|
|
13
|
+
* Wraps Payload's duplicate, which checks read access on the source and create
|
|
14
|
+
* access on the copy, and runs the fields' `beforeDuplicate` hooks.
|
|
15
|
+
*/ const duplicateDocument = defineMcpxTool({
|
|
16
|
+
name: "duplicateDocument",
|
|
17
|
+
description: DESCRIPTION,
|
|
18
|
+
annotations: {
|
|
19
|
+
readOnlyHint: false,
|
|
20
|
+
destructiveHint: false,
|
|
21
|
+
idempotentHint: false,
|
|
22
|
+
openWorldHint: false
|
|
23
|
+
},
|
|
24
|
+
isEnabled: (scope) => duplicableSlugs(scope).length > 0,
|
|
25
|
+
inputSchema: (scope) => ({
|
|
26
|
+
collection: slugEnum(duplicableSlugs(scope)),
|
|
27
|
+
id: idSchema
|
|
28
|
+
}),
|
|
29
|
+
handler: async ({ args, scope }) => {
|
|
30
|
+
const target = resolveEntity(scope, { collection: args.collection }, "create");
|
|
31
|
+
if (target.kind === "global" || !duplicableSlugs(scope).includes(target.slug)) throw new Forbidden(scope.req.t);
|
|
32
|
+
const created = await scope.req.payload.duplicate({
|
|
33
|
+
collection: target.slug,
|
|
34
|
+
id: args.id,
|
|
35
|
+
data: withDuplicateIntent({}),
|
|
36
|
+
depth: 0,
|
|
37
|
+
draft: true,
|
|
38
|
+
overrideAccess: false,
|
|
39
|
+
req: scope.req
|
|
40
|
+
});
|
|
41
|
+
return await createdResult(scope, {
|
|
42
|
+
target,
|
|
43
|
+
id: created["id"],
|
|
44
|
+
locale: localeOf(scope, void 0)
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
//#endregion
|
|
49
|
+
export { duplicateDocument };
|
package/dist/types.d.mts
CHANGED
|
@@ -34,8 +34,9 @@ export interface McpxCollectionOptions {
|
|
|
34
34
|
*/
|
|
35
35
|
read?: boolean;
|
|
36
36
|
/**
|
|
37
|
-
* Expose `patchDocument`, `validateDocument
|
|
38
|
-
* collection is created in only where MCP
|
|
37
|
+
* Expose `patchDocument`, `validateDocument`, `createDocument` and
|
|
38
|
+
* `duplicateDocument`. An upload collection is created in only where MCP
|
|
39
|
+
* accepts its files, and is never duplicated. Without drafts
|
|
39
40
|
* a write changes live content. Needs `read`. Default `true` while `read` is.
|
|
40
41
|
*/
|
|
41
42
|
write?: boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hasPublishIntent, hasTrashIntent, takePublishIntent, takeTrashIntent, withTrashIntent } from "./publish-intent.mjs";
|
|
1
|
+
import { hasPublishIntent, hasTrashIntent, takeDuplicateIntent, takePublishIntent, takeTrashIntent, withTrashIntent } from "./publish-intent.mjs";
|
|
2
2
|
import { isMcpxRequest } from "../request.mjs";
|
|
3
3
|
import { APIError } from "payload";
|
|
4
4
|
import { hasDraftsEnabled } from "payload/shared";
|
|
@@ -30,6 +30,11 @@ const scrubWriteArgs = (args, intent) => {
|
|
|
30
30
|
const forceDraftWrite = (hookArgs) => {
|
|
31
31
|
const { args, operation, req } = hookArgs;
|
|
32
32
|
if (!isMcpxRequest(req) || operation !== "create" && operation !== "update") return args;
|
|
33
|
+
if (operation === "create" && takeDuplicateIntent(args.data)) {
|
|
34
|
+
const next = scrubWriteArgs(args, void 0);
|
|
35
|
+
next["duplicateFromID"] = args.duplicateFromID;
|
|
36
|
+
return next;
|
|
37
|
+
}
|
|
33
38
|
const intent = operation !== "update" ? void 0 : hasPublishIntent(args.data) ? "publish" : hasTrashIntent(args.data) ? "trash" : void 0;
|
|
34
39
|
return scrubWriteArgs(args, intent);
|
|
35
40
|
};
|
|
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
//#region src/write/publish-intent.ts
|
|
4
4
|
const PUBLISH_INTENT = "__mcpxPublishIntent";
|
|
5
5
|
const TRASH_INTENT = "__mcpxTrashIntent";
|
|
6
|
+
const DUPLICATE_INTENT = "__mcpxDuplicateIntent";
|
|
6
7
|
const TOKEN = randomUUID();
|
|
7
8
|
const take = (data, key) => {
|
|
8
9
|
if (!isPlainObject(data) || data[key] !== TOKEN) return false;
|
|
@@ -25,5 +26,12 @@ const hasTrashIntent = (data) => isPlainObject(data) && data[TRASH_INTENT] === T
|
|
|
25
26
|
/**
|
|
26
27
|
* The counterpart of {@link takePublishIntent}.
|
|
27
28
|
*/ const takeTrashIntent = (data) => take(data, TRASH_INTENT);
|
|
29
|
+
const withDuplicateIntent = (data) => ({
|
|
30
|
+
...data,
|
|
31
|
+
[DUPLICATE_INTENT]: TOKEN
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Removes the marker, so it never reaches the operation.
|
|
35
|
+
*/ const takeDuplicateIntent = (data) => take(data, DUPLICATE_INTENT);
|
|
28
36
|
//#endregion
|
|
29
|
-
export { hasPublishIntent, hasTrashIntent, takePublishIntent, takeTrashIntent, withPublishIntent, withTrashIntent };
|
|
37
|
+
export { hasPublishIntent, hasTrashIntent, takeDuplicateIntent, takePublishIntent, takeTrashIntent, withDuplicateIntent, withPublishIntent, withTrashIntent };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@abinnovision/payloadcms-mcpx",
|
|
4
|
-
"version": "2.0.0-beta.
|
|
4
|
+
"version": "2.0.0-beta.19",
|
|
5
5
|
"description": "Payload CMS plugin exposing a fixed, schema-aware MCP tool surface with draft-only writes and per-API-key capabilities.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"payload",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"prettier": "@abinnovision/prettier-config",
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
60
|
+
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
61
61
|
"rfc6902": "^5.3.0",
|
|
62
62
|
"zod": "^4.6.5"
|
|
63
63
|
},
|
|
@@ -69,20 +69,20 @@
|
|
|
69
69
|
"@payloadcms/db-sqlite": "3.90.2",
|
|
70
70
|
"@payloadcms/richtext-lexical": "3.90.2",
|
|
71
71
|
"@payloadcms/ui": "3.90.2",
|
|
72
|
-
"@swc/core": "^1.16.
|
|
73
|
-
"@types/node": "^26.
|
|
72
|
+
"@swc/core": "^1.16.12",
|
|
73
|
+
"@types/node": "^26.6.3",
|
|
74
74
|
"@types/react": "^19.3.0",
|
|
75
|
-
"@vitest/coverage-v8": "^5.0.
|
|
76
|
-
"eslint": "^10.
|
|
75
|
+
"@vitest/coverage-v8": "^5.0.2",
|
|
76
|
+
"eslint": "^10.11.0",
|
|
77
77
|
"payload": "3.90.2",
|
|
78
|
-
"prettier": "^3.9.
|
|
78
|
+
"prettier": "^3.9.9",
|
|
79
79
|
"publint": "^0.3.24",
|
|
80
80
|
"react": "^19.3.0",
|
|
81
81
|
"react-dom": "^19.3.0",
|
|
82
82
|
"tsdown": "^0.23.0",
|
|
83
83
|
"typescript": "^6.0.3",
|
|
84
|
-
"unplugin-swc": "^
|
|
85
|
-
"vitest": "^5.0.
|
|
84
|
+
"unplugin-swc": "^2.0.0",
|
|
85
|
+
"vitest": "^5.0.2"
|
|
86
86
|
},
|
|
87
87
|
"peerDependencies": {
|
|
88
88
|
"@payloadcms/ui": ">=3.88.0 <4",
|