@abinnovision/payloadcms-mcpx 2.0.0-beta.8 → 2.0.0
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 +17 -7
- 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 +6 -4
- package/dist/admin/setup-guide.mjs +51 -33
- package/dist/api-keys/capability-matrix.d.mts +12 -10
- package/dist/api-keys/capability-matrix.mjs +17 -37
- package/dist/api-keys/collection.mjs +18 -19
- package/dist/api-keys/confirmation-view.mjs +7 -0
- package/dist/api-keys/fields.mjs +67 -53
- package/dist/auth/resolve.mjs +79 -30
- package/dist/capabilities.d.mts +2 -0
- package/dist/capabilities.mjs +69 -15
- package/dist/define-tool.d.mts +10 -6
- 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 +52 -27
- package/dist/endpoint/upload.mjs +104 -0
- package/dist/grants/confirmation.mjs +75 -0
- package/dist/grants/grant.mjs +137 -0
- package/dist/guards.mjs +11 -2
- package/dist/i18n.mjs +3 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/options.mjs +73 -62
- 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 +21 -15
- package/dist/schema/outline.mjs +4 -5
- package/dist/schema/path.mjs +13 -4
- package/dist/schema/pointer.mjs +11 -17
- package/dist/schema/shape.mjs +39 -51
- package/dist/schema/walk.mjs +68 -21
- 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 +147 -21
- 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 +60 -54
- package/dist/tools/publish-document.mjs +23 -30
- package/dist/tools/query-paths.mjs +4 -2
- package/dist/tools/run-confirmed.mjs +114 -0
- package/dist/tools/shared.mjs +66 -50
- package/dist/tools/validate-document.mjs +10 -11
- package/dist/tools/versions.mjs +24 -10
- package/dist/types.d.mts +228 -60
- package/dist/upload/file.mjs +177 -0
- package/dist/version.mjs +4 -2
- package/dist/write/draft-guard.mjs +46 -37
- package/dist/write/patch.mjs +4 -4
- package/dist/write/publish-intent.mjs +25 -11
- package/dist/write/row-ids.mjs +6 -2
- package/dist/write/write-data.mjs +18 -31
- package/package.json +15 -14
- package/dist/builtin-tool-names.mjs +0 -16
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { defineMcpxTool, isToolEnabled, toolInputSchema } from "../define-tool.mjs";
|
|
2
|
+
import { grantContext } from "../grants/grant.mjs";
|
|
3
|
+
import { claimConfirmation, readConfirmation } from "../grants/confirmation.mjs";
|
|
4
|
+
import { jsonResult, parseResult } from "../result.mjs";
|
|
5
|
+
import { withRequestLocale } from "./document.mjs";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { APIError } from "payload";
|
|
8
|
+
//#region src/tools/run-confirmed.ts
|
|
9
|
+
const DESCRIPTION = `Runs calls that returned a "confirmation", once the user approved them in the admin panel. Returns one result per id, in the order given: "done" with the call's "result", "pending" while it awaits a decision, "skipped" with a "reason" when it no longer passes its checks, or "refused" when it was rejected, expired, already run or is unknown. A pending id stays usable; no other id can run again.`;
|
|
10
|
+
const NO_LONGER_ALLOWED = "This key no longer allows this call.";
|
|
11
|
+
const CONFIRMED = {
|
|
12
|
+
confirmed: true,
|
|
13
|
+
request: () => Promise.reject(/* @__PURE__ */ new Error("A confirmed run cannot request a confirmation."))
|
|
14
|
+
};
|
|
15
|
+
const execute = async (tool, confirmation, scope, extra) => {
|
|
16
|
+
const { req } = scope;
|
|
17
|
+
const args = toolInputSchema(tool, scope).safeParse(confirmation.args);
|
|
18
|
+
if (!args.success) return {
|
|
19
|
+
status: "skipped",
|
|
20
|
+
reason: NO_LONGER_ALLOWED
|
|
21
|
+
};
|
|
22
|
+
return await withRequestLocale(req, async () => {
|
|
23
|
+
req.locale = confirmation.locale;
|
|
24
|
+
req.fallbackLocale = confirmation.fallbackLocale;
|
|
25
|
+
try {
|
|
26
|
+
const result = await tool.handler({
|
|
27
|
+
args: args.data,
|
|
28
|
+
scope,
|
|
29
|
+
req,
|
|
30
|
+
extra,
|
|
31
|
+
confirmation: CONFIRMED
|
|
32
|
+
});
|
|
33
|
+
const data = parseResult(result);
|
|
34
|
+
return result.isError === true ? {
|
|
35
|
+
status: "skipped",
|
|
36
|
+
reason: String(data["error"])
|
|
37
|
+
} : {
|
|
38
|
+
status: "done",
|
|
39
|
+
result: data
|
|
40
|
+
};
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error instanceof APIError && error.isPublic) return {
|
|
43
|
+
status: "skipped",
|
|
44
|
+
reason: error.message
|
|
45
|
+
};
|
|
46
|
+
req.payload.logger.error({
|
|
47
|
+
err: error,
|
|
48
|
+
msg: "[payloadcms-mcpx] Confirmed call failed."
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
status: "skipped",
|
|
52
|
+
reason: "Internal error"
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
const claimApproved = async (req, slug, apiKeyId, id) => {
|
|
58
|
+
const { payload } = req;
|
|
59
|
+
try {
|
|
60
|
+
const confirmation = await readConfirmation(payload, slug, apiKeyId, id);
|
|
61
|
+
if (confirmation?.state === "pending") return { status: "pending" };
|
|
62
|
+
return confirmation?.state === "approved" && await claimConfirmation(payload, slug, apiKeyId, id, confirmation.exp) ? confirmation : { status: "refused" };
|
|
63
|
+
} catch (error) {
|
|
64
|
+
payload.logger.error({
|
|
65
|
+
err: error,
|
|
66
|
+
msg: "[payloadcms-mcpx] Could not claim a confirmed call."
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
status: "skipped",
|
|
70
|
+
reason: "Internal error"
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Runs approved calls of the confirmable tools in `tools`, which is read per
|
|
76
|
+
* call so the list may include this tool. Registered when the key may call one
|
|
77
|
+
* of them. Each call claims its entry first, so it runs at most once.
|
|
78
|
+
*/ const createRunConfirmed = (tools) => defineMcpxTool({
|
|
79
|
+
name: "runConfirmed",
|
|
80
|
+
description: DESCRIPTION,
|
|
81
|
+
annotations: {
|
|
82
|
+
destructiveHint: true,
|
|
83
|
+
openWorldHint: false
|
|
84
|
+
},
|
|
85
|
+
isEnabled: (scope) => tools().some((tool) => tool.confirm !== void 0 && isToolEnabled(tool, scope)),
|
|
86
|
+
inputSchema: { ids: z.array(z.string().max(64)).min(1).max(50).describe("\"id\" of each \"confirmation\".") },
|
|
87
|
+
handler: async ({ args, scope, extra }) => {
|
|
88
|
+
const { req } = scope;
|
|
89
|
+
const { slug, apiKeyId } = grantContext(req);
|
|
90
|
+
const outcomes = [];
|
|
91
|
+
for (const id of args.ids) {
|
|
92
|
+
const confirmation = await claimApproved(req, slug, apiKeyId, id);
|
|
93
|
+
if (!("tool" in confirmation)) {
|
|
94
|
+
outcomes.push({
|
|
95
|
+
id,
|
|
96
|
+
...confirmation
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const tool = tools().find((candidate) => candidate.name === confirmation.tool && candidate.confirm !== void 0);
|
|
101
|
+
const result = tool && isToolEnabled(tool, scope) ? await execute(tool, confirmation, scope, extra) : {
|
|
102
|
+
status: "skipped",
|
|
103
|
+
reason: NO_LONGER_ALLOWED
|
|
104
|
+
};
|
|
105
|
+
outcomes.push({
|
|
106
|
+
id,
|
|
107
|
+
...result
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return jsonResult({ results: outcomes });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
//#endregion
|
|
114
|
+
export { createRunConfirmed };
|
package/dist/tools/shared.mjs
CHANGED
|
@@ -1,68 +1,63 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { acceptsFiles } from "../upload/file.mjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
//#region src/tools/shared.ts
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* A value outside the list fails schema validation before a handler runs, so a
|
|
6
6
|
* client only ever sees what its key may touch.
|
|
7
|
-
*/ const
|
|
8
|
-
/**
|
|
7
|
+
*/ const stringEnum = (values) => z.enum(values);
|
|
8
|
+
/**
|
|
9
|
+
* Payload's id type follows the adapter, so both forms are handed on as read.
|
|
10
|
+
*/ const idSchema = z.union([z.string(), z.number()]);
|
|
9
11
|
const slugsOf = (scope, key) => ({
|
|
10
12
|
collections: scope.collections[key],
|
|
11
13
|
globals: scope.globals[key]
|
|
12
14
|
});
|
|
13
15
|
const slugsWhere = (scope, predicate, allowed) => {
|
|
14
16
|
const pick = (entities, slugs) => entities.filter((entity) => slugs.includes(entity.slug) && predicate(entity)).map((entity) => entity.slug);
|
|
15
|
-
return
|
|
17
|
+
return {
|
|
18
|
+
collections: pick(scope.exposure.collections, allowed.collections),
|
|
19
|
+
globals: pick(scope.exposure.globals, allowed.globals)
|
|
20
|
+
};
|
|
16
21
|
};
|
|
17
22
|
/**
|
|
18
|
-
*
|
|
19
|
-
* files
|
|
20
|
-
*/ const patchOnlySlugs = (scope) =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
23
|
+
* Collections this key may write but not create in: upload collections whose
|
|
24
|
+
* files MCP does not accept. Nothing creates a global either way.
|
|
25
|
+
*/ const patchOnlySlugs = (scope) => {
|
|
26
|
+
const creatable = slugsFor(scope, "create").collections;
|
|
27
|
+
return scope.collections.writable.filter((slug) => !creatable.includes(slug));
|
|
28
|
+
};
|
|
24
29
|
const widen = (branch) => branch;
|
|
25
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* The one list the shape helpers and {@link resolveEntity} both read.
|
|
32
|
+
*/ const slugsFor = (scope, operation) => {
|
|
26
33
|
switch (operation) {
|
|
27
|
-
case "create": return {
|
|
28
|
-
collections:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
case "create": return slugsWhere(scope, (entity) => entity.write && (!entity.isUpload || acceptsFiles(scope, entity.slug)), {
|
|
35
|
+
collections: scope.collections.writable,
|
|
36
|
+
globals: []
|
|
37
|
+
});
|
|
38
|
+
case "delete": return {
|
|
39
|
+
collections: scope.collections.deletable,
|
|
32
40
|
globals: []
|
|
33
41
|
};
|
|
34
42
|
case "publish": return slugsOf(scope, "publishable");
|
|
35
43
|
case "read": return slugsOf(scope, "readable");
|
|
36
|
-
case "versions": return
|
|
37
|
-
collections: slugsWhere(scope, (entity) => entity.hasVersions, {
|
|
38
|
-
collections: scope.collections.readable,
|
|
39
|
-
globals: []
|
|
40
|
-
}),
|
|
41
|
-
globals: slugsWhere(scope, (entity) => entity.hasVersions, {
|
|
42
|
-
collections: [],
|
|
43
|
-
globals: scope.globals.readable
|
|
44
|
-
})
|
|
45
|
-
};
|
|
44
|
+
case "versions": return slugsWhere(scope, (entity) => entity.hasVersions, slugsOf(scope, "readable"));
|
|
46
45
|
case "write": return slugsOf(scope, "writable");
|
|
47
46
|
}
|
|
48
47
|
};
|
|
49
48
|
/**
|
|
50
|
-
*
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
globals
|
|
62
|
-
})
|
|
63
|
-
};
|
|
49
|
+
* Whether `operation` reaches any collection or global.
|
|
50
|
+
*/ const reaches = (scope, operation) => {
|
|
51
|
+
const { collections, globals } = slugsFor(scope, operation);
|
|
52
|
+
return collections.length + globals.length > 0;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Slugs reachable by `operation` whose writes go live, because they have no
|
|
56
|
+
* draft to write.
|
|
57
|
+
*/ const liveWriteSlugs = (scope, operation) => {
|
|
58
|
+
const { collections, globals } = slugsWhere(scope, (entity) => entity.liveWrite, slugsFor(scope, operation));
|
|
59
|
+
return [...collections, ...globals];
|
|
64
60
|
};
|
|
65
|
-
/** Slugs reachable by `operation` whose writes go live, because they have no draft to write. */ const liveWriteSlugs = (scope, operation) => slugsWhere(scope, isLiveWrite, slugsFor(scope, operation));
|
|
66
61
|
/**
|
|
67
62
|
* Stated on the tools that write, since a client calling them must know before
|
|
68
63
|
* the call which writes are public.
|
|
@@ -72,16 +67,16 @@ const widen = (branch) => branch;
|
|
|
72
67
|
};
|
|
73
68
|
/**
|
|
74
69
|
* With no reachable global, `global` is left out and `collection` stays
|
|
75
|
-
* required, so a deployment without globals
|
|
70
|
+
* required, so a deployment without globals gets no extra argument. Only the
|
|
76
71
|
* mixed case makes either optional, and the handler enforces exclusivity there.
|
|
77
72
|
* `globalRule` states that rule on `global` for a tool without an `id`.
|
|
78
73
|
*/ const entityShape = (scope, operation, globalRule) => {
|
|
79
74
|
const { collections, globals } = slugsFor(scope, operation);
|
|
80
|
-
if (globals.length === 0) return widen({ collection:
|
|
81
|
-
if (collections.length === 0) return widen({ global:
|
|
82
|
-
const global =
|
|
75
|
+
if (globals.length === 0) return widen({ collection: stringEnum(collections) });
|
|
76
|
+
if (collections.length === 0) return widen({ global: stringEnum(globals) });
|
|
77
|
+
const global = stringEnum(globals).optional();
|
|
83
78
|
return widen({
|
|
84
|
-
collection:
|
|
79
|
+
collection: stringEnum(collections).optional(),
|
|
85
80
|
global: globalRule === void 0 ? global : global.describe(globalRule)
|
|
86
81
|
});
|
|
87
82
|
};
|
|
@@ -94,10 +89,12 @@ const widen = (branch) => branch;
|
|
|
94
89
|
if (globals.length === 0) return widen({ id: idSchema });
|
|
95
90
|
return widen({ id: idSchema.optional().describe("Required with \"collection\", omitted with \"global\".") });
|
|
96
91
|
};
|
|
97
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* For the reads that fall back to the default locale, unlike the writes.
|
|
94
|
+
*/ const READ_LOCALE_DESCRIPTION = "Default: the default locale. A value missing in this locale is shown from the default locale.";
|
|
98
95
|
const localeShape = (scope, options) => {
|
|
99
96
|
if (!scope.localization) return widen({});
|
|
100
|
-
const enumerated =
|
|
97
|
+
const enumerated = stringEnum(scope.localization.locales);
|
|
101
98
|
const locale = options.required ? enumerated : enumerated.optional();
|
|
102
99
|
return widen({ locale: options.description === void 0 ? locale : locale.describe(options.description) });
|
|
103
100
|
};
|
|
@@ -106,6 +103,25 @@ const localeShape = (scope, options) => {
|
|
|
106
103
|
* it can write back, and populating a relation costs a query.
|
|
107
104
|
*/ const depthShape = (scope) => ({ depth: z.number().int().min(0).max(scope.limits.maxDepth).optional().describe("Default 0. A relationship into a collection this key cannot read stays an id.") });
|
|
108
105
|
/**
|
|
106
|
+
* For the writes that refuse a document changed since the client read it.
|
|
107
|
+
*/ const expectedUpdatedAtShape = { expectedUpdatedAt: z.string().optional().describe("\"updatedAt\" from your last read. Refused if the document changed since.") };
|
|
108
|
+
/**
|
|
109
|
+
* `limit` is bounded by the configured ceiling, so the client sees it instead
|
|
110
|
+
* of being clamped silently.
|
|
111
|
+
*/ const pageShape = (scope) => ({
|
|
112
|
+
limit: z.number().int().min(1).max(scope.limits.maxLimit).optional().describe("Default 10."),
|
|
113
|
+
page: z.number().int().min(1).optional()
|
|
114
|
+
});
|
|
115
|
+
/**
|
|
116
|
+
* The paging fields of a result.
|
|
117
|
+
*/ const pageFields = (result) => ({
|
|
118
|
+
totalDocs: result.totalDocs,
|
|
119
|
+
page: result.page,
|
|
120
|
+
totalPages: result.totalPages,
|
|
121
|
+
limit: result.limit,
|
|
122
|
+
hasNextPage: result.hasNextPage
|
|
123
|
+
});
|
|
124
|
+
/**
|
|
109
125
|
* The locale to operate on: the explicit argument, else the request's, else
|
|
110
126
|
* the default. `undefined` when localization is off.
|
|
111
127
|
*/ const localeOf = (scope, locale) => {
|
|
@@ -115,4 +131,4 @@ const localeShape = (scope, options) => {
|
|
|
115
131
|
return requested && locales.includes(requested) ? requested : defaultLocale;
|
|
116
132
|
};
|
|
117
133
|
//#endregion
|
|
118
|
-
export { READ_LOCALE_DESCRIPTION, depthShape,
|
|
134
|
+
export { READ_LOCALE_DESCRIPTION, depthShape, entityShape, expectedUpdatedAtShape, idSchema, idShape, liveWriteSentence, liveWriteSlugs, localeOf, localeShape, pageFields, pageShape, patchOnlySlugs, reaches, slugsFor, slugsWhere, stringEnum, widen };
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { defineMcpxTool } from "../define-tool.mjs";
|
|
2
2
|
import { jsonResult } from "../result.mjs";
|
|
3
|
-
import { entityShape, idShape, localeOf, localeShape } from "./shared.mjs";
|
|
4
|
-
import { identityOf, readDraft, resolveDocument } from "./document.mjs";
|
|
5
3
|
import { collectPublishBlockers } from "../write/publish-blockers.mjs";
|
|
4
|
+
import { entityShape, idShape, localeOf, localeShape, reaches } from "./shared.mjs";
|
|
5
|
+
import { collectLocaleBlockers, documentSummary, readDraft, resolveDocument } from "./document.mjs";
|
|
6
6
|
/**
|
|
7
|
-
* Gated on write
|
|
8
|
-
* to a caller who can act on them.
|
|
7
|
+
* Gated on write, which implies read, because publish blockers only mean
|
|
8
|
+
* something to a caller who can act on them.
|
|
9
9
|
*
|
|
10
10
|
* It reads the document twice on purpose: once under the key's own access to
|
|
11
11
|
* refuse a caller who may not see it, then privileged, so the check covers every
|
|
@@ -13,16 +13,17 @@ import { collectPublishBlockers } from "../write/publish-blockers.mjs";
|
|
|
13
13
|
* the traversal fires field hooks.
|
|
14
14
|
*/ const validateDocument = defineMcpxTool({
|
|
15
15
|
name: "validateDocument",
|
|
16
|
-
description: `Lists what still blocks publishing one document or global, without saving. Returns "publishBlockers", each with a pointer and a message. An empty list means it can be published, unless "publishBlockersUnavailable" is true: then the check itself failed. The check runs the field hooks a save runs.`,
|
|
16
|
+
description: `Lists what still blocks publishing one document or global, without saving. Checks one locale when "locale" is given, otherwise every locale. Returns "publishBlockers", each with a pointer and a message, and a "locale" when every locale was checked. An empty list means it can be published, unless "publishBlockersUnavailable" is true: then the check itself failed. The check runs the field hooks a save runs.`,
|
|
17
17
|
annotations: { openWorldHint: false },
|
|
18
|
-
isEnabled: (scope) => scope
|
|
18
|
+
isEnabled: (scope) => reaches(scope, "write"),
|
|
19
19
|
inputSchema: (scope) => ({
|
|
20
20
|
...entityShape(scope, "write"),
|
|
21
21
|
...idShape(scope, "write"),
|
|
22
|
-
...localeShape(scope, { required:
|
|
22
|
+
...localeShape(scope, { required: false })
|
|
23
23
|
}),
|
|
24
24
|
handler: async ({ args, scope }) => {
|
|
25
25
|
const target = resolveDocument(scope, args, "write");
|
|
26
|
+
const { localization } = scope;
|
|
26
27
|
const locale = localeOf(scope, args.locale);
|
|
27
28
|
await readDraft(scope, {
|
|
28
29
|
target,
|
|
@@ -33,14 +34,12 @@ import { collectPublishBlockers } from "../write/publish-blockers.mjs";
|
|
|
33
34
|
locale,
|
|
34
35
|
privileged: true
|
|
35
36
|
});
|
|
36
|
-
const validation = await collectPublishBlockers(scope.req, {
|
|
37
|
+
const validation = localization && args.locale === void 0 ? await collectLocaleBlockers(scope, target, localization.locales) : await collectPublishBlockers(scope.req, {
|
|
37
38
|
doc,
|
|
38
39
|
entity: target
|
|
39
40
|
});
|
|
40
41
|
return jsonResult({
|
|
41
|
-
...
|
|
42
|
-
status: doc["_status"],
|
|
43
|
-
updatedAt: doc["updatedAt"],
|
|
42
|
+
...documentSummary(target, doc),
|
|
44
43
|
publishBlockers: validation.blockers,
|
|
45
44
|
...validation.unavailable ? { publishBlockersUnavailable: true } : {}
|
|
46
45
|
});
|
package/dist/tools/versions.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { definedProps, isPlainObject } from "../guards.mjs";
|
|
1
2
|
import { stripAdminHidden } from "../schema/admin-hidden.mjs";
|
|
2
3
|
import "../schema/index.mjs";
|
|
3
4
|
import { mcpxReadRequest } from "./read-request.mjs";
|
|
@@ -12,14 +13,19 @@ const NOISE = [
|
|
|
12
13
|
"_status"
|
|
13
14
|
];
|
|
14
15
|
const NEWEST_FIRST = ["-updatedAt", "-id"];
|
|
15
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* The document or global as the key's user sees it, without `admin.hidden`
|
|
18
|
+
* fields.
|
|
19
|
+
*/ const readLive = async (scope, read, options) => {
|
|
16
20
|
const shared = {
|
|
17
21
|
depth: read.depth,
|
|
18
22
|
draft: options.draft,
|
|
19
23
|
overrideAccess: false,
|
|
20
24
|
req: mcpxReadRequest(scope),
|
|
21
|
-
...
|
|
22
|
-
|
|
25
|
+
...definedProps({
|
|
26
|
+
select: options.select,
|
|
27
|
+
locale: read.locale
|
|
28
|
+
})
|
|
23
29
|
};
|
|
24
30
|
const doc = await (read.target.kind === "collection" ? scope.req.payload.findByID({
|
|
25
31
|
...shared,
|
|
@@ -44,9 +50,11 @@ const assertReadable = async (scope, read) => {
|
|
|
44
50
|
select: {}
|
|
45
51
|
});
|
|
46
52
|
};
|
|
47
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* `parent` comes back as a raw id or, populated, as the document itself.
|
|
55
|
+
*/ const isVersionOf = (version, id) => {
|
|
48
56
|
const parent = version["parent"];
|
|
49
|
-
const parentId =
|
|
57
|
+
const parentId = isPlainObject(parent) ? parent["id"] : parent;
|
|
50
58
|
return (typeof parentId === "string" || typeof parentId === "number") && String(parentId) === String(id);
|
|
51
59
|
};
|
|
52
60
|
/**
|
|
@@ -63,7 +71,7 @@ const assertReadable = async (scope, read) => {
|
|
|
63
71
|
disableErrors: true,
|
|
64
72
|
overrideAccess: false,
|
|
65
73
|
req: mcpxReadRequest(scope),
|
|
66
|
-
...
|
|
74
|
+
...definedProps({ locale: read.locale })
|
|
67
75
|
};
|
|
68
76
|
const version = await (read.target.kind === "collection" ? payload.findVersionByID({
|
|
69
77
|
...shared,
|
|
@@ -76,7 +84,9 @@ const assertReadable = async (scope, read) => {
|
|
|
76
84
|
if (read.target.kind === "collection" && !isVersionOf(version, read.target.id)) return null;
|
|
77
85
|
return withoutHidden(scope, read, version);
|
|
78
86
|
};
|
|
79
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* One page of a document's or global's history, newest first.
|
|
89
|
+
*/ const queryVersions = async (scope, read, options) => {
|
|
80
90
|
await assertReadable(scope, read);
|
|
81
91
|
const where = {
|
|
82
92
|
...read.target.kind === "collection" ? { parent: { equals: read.target.id } } : {},
|
|
@@ -89,8 +99,10 @@ const assertReadable = async (scope, read) => {
|
|
|
89
99
|
overrideAccess: false,
|
|
90
100
|
req: mcpxReadRequest(scope),
|
|
91
101
|
where,
|
|
92
|
-
...
|
|
93
|
-
|
|
102
|
+
...definedProps({
|
|
103
|
+
page: options.page,
|
|
104
|
+
locale: read.locale
|
|
105
|
+
})
|
|
94
106
|
};
|
|
95
107
|
return await (read.target.kind === "collection" ? scope.req.payload.findVersions({
|
|
96
108
|
...shared,
|
|
@@ -100,7 +112,9 @@ const assertReadable = async (scope, read) => {
|
|
|
100
112
|
slug: read.target.slug
|
|
101
113
|
}));
|
|
102
114
|
};
|
|
103
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* The newest version whose status is published, or `null` if none is.
|
|
117
|
+
*/ const loadPublished = async (scope, read) => {
|
|
104
118
|
if (!hasDraftsEnabled(read.target.config)) return null;
|
|
105
119
|
const [newest] = (await queryVersions(scope, read, {
|
|
106
120
|
status: "published",
|