@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +56 -36
  3. package/dist/admin/capability-matrix.d.mts +17 -7
  4. package/dist/admin/capability-matrix.mjs +374 -272
  5. package/dist/admin/capability-toggles.mjs +79 -42
  6. package/dist/admin/confirmations-panel.mjs +232 -0
  7. package/dist/admin/confirmations.d.mts +15 -0
  8. package/dist/admin/confirmations.mjs +71 -0
  9. package/dist/admin/endpoint-url.mjs +7 -0
  10. package/dist/admin/index.d.mts +2 -1
  11. package/dist/admin/index.mjs +2 -1
  12. package/dist/admin/setup-guide.d.mts +6 -4
  13. package/dist/admin/setup-guide.mjs +51 -33
  14. package/dist/api-keys/capability-matrix.d.mts +12 -10
  15. package/dist/api-keys/capability-matrix.mjs +17 -37
  16. package/dist/api-keys/collection.mjs +18 -19
  17. package/dist/api-keys/confirmation-view.mjs +7 -0
  18. package/dist/api-keys/fields.mjs +67 -53
  19. package/dist/auth/resolve.mjs +79 -30
  20. package/dist/capabilities.d.mts +2 -0
  21. package/dist/capabilities.mjs +69 -15
  22. package/dist/define-tool.d.mts +10 -6
  23. package/dist/define-tool.mjs +8 -1
  24. package/dist/endpoint/confirmations.mjs +124 -0
  25. package/dist/endpoint/handler.mjs +25 -27
  26. package/dist/endpoint/http.mjs +27 -0
  27. package/dist/endpoint/server.mjs +52 -27
  28. package/dist/endpoint/upload.mjs +104 -0
  29. package/dist/grants/confirmation.mjs +75 -0
  30. package/dist/grants/grant.mjs +137 -0
  31. package/dist/guards.mjs +11 -2
  32. package/dist/i18n.mjs +3 -1
  33. package/dist/index.d.mts +2 -2
  34. package/dist/index.mjs +1 -1
  35. package/dist/options.mjs +73 -62
  36. package/dist/plugin.d.mts +3 -2
  37. package/dist/plugin.mjs +19 -4
  38. package/dist/request.mjs +5 -1
  39. package/dist/result.mjs +11 -1
  40. package/dist/schema/admin-hidden.mjs +4 -4
  41. package/dist/schema/describe.mjs +12 -18
  42. package/dist/schema/index.mjs +5 -6
  43. package/dist/schema/lexical-pointer.mjs +8 -8
  44. package/dist/schema/lexical.mjs +21 -15
  45. package/dist/schema/outline.mjs +4 -5
  46. package/dist/schema/path.mjs +13 -4
  47. package/dist/schema/pointer.mjs +11 -17
  48. package/dist/schema/shape.mjs +39 -51
  49. package/dist/schema/walk.mjs +68 -21
  50. package/dist/tools/builtin.mjs +7 -1
  51. package/dist/tools/create-document.mjs +38 -26
  52. package/dist/tools/delete-document.mjs +154 -0
  53. package/dist/tools/describe-schema.mjs +7 -5
  54. package/dist/tools/document.mjs +147 -21
  55. package/dist/tools/duplicate-document.mjs +50 -0
  56. package/dist/tools/find-documents.mjs +15 -20
  57. package/dist/tools/find-versions.mjs +11 -15
  58. package/dist/tools/get-document.mjs +31 -15
  59. package/dist/tools/links.mjs +53 -0
  60. package/dist/tools/list-capabilities.mjs +17 -7
  61. package/dist/tools/patch-document.mjs +60 -54
  62. package/dist/tools/publish-document.mjs +23 -30
  63. package/dist/tools/query-paths.mjs +4 -2
  64. package/dist/tools/run-confirmed.mjs +114 -0
  65. package/dist/tools/shared.mjs +66 -50
  66. package/dist/tools/validate-document.mjs +10 -11
  67. package/dist/tools/versions.mjs +24 -10
  68. package/dist/types.d.mts +228 -60
  69. package/dist/upload/file.mjs +177 -0
  70. package/dist/version.mjs +4 -2
  71. package/dist/write/draft-guard.mjs +46 -37
  72. package/dist/write/patch.mjs +4 -4
  73. package/dist/write/publish-intent.mjs +25 -11
  74. package/dist/write/row-ids.mjs +6 -2
  75. package/dist/write/write-data.mjs +18 -31
  76. package/package.json +15 -14
  77. 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 };
@@ -1,68 +1,63 @@
1
- import { canCreate, isLiveWrite } from "../capabilities.mjs";
1
+ import { acceptsFiles } from "../upload/file.mjs";
2
2
  import { z } from "zod";
3
3
  //#region src/tools/shared.ts
4
4
  /**
5
- * An out-of-scope slug fails schema validation before a handler runs, so a
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 slugEnum = (slugs) => z.enum(slugs);
8
- /** Payload's id type follows the adapter, so both forms are handed on as read. */ const idSchema = z.union([z.string(), z.number()]);
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 [...pick(scope.exposure.collections, allowed.collections), ...pick(scope.exposure.globals, allowed.globals)];
17
+ return {
18
+ collections: pick(scope.exposure.collections, allowed.collections),
19
+ globals: pick(scope.exposure.globals, allowed.globals)
20
+ };
16
21
  };
17
22
  /**
18
- * Slugs this key may write but never create in, because their documents are
19
- * files. Collection-only, since nothing creates a global either way.
20
- */ const patchOnlySlugs = (scope) => slugsWhere(scope, (entity) => !canCreate(entity), {
21
- collections: scope.collections.writable,
22
- globals: []
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
- /** The one list the shape helpers and {@link resolveEntity} both read. */ const slugsFor = (scope, operation) => {
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: slugsWhere(scope, canCreate, {
29
- collections: scope.collections.writable,
30
- globals: []
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
- * The slugs of {@link slugsFor} `"versions"` that also have drafts. Only they
51
- * have `_status` on their versions, so only they can be queried by status.
52
- */ const draftVersionSlugs = (scope) => {
53
- const { collections, globals } = slugsFor(scope, "versions");
54
- return {
55
- collections: slugsWhere(scope, (entity) => entity.hasDrafts, {
56
- collections,
57
- globals: []
58
- }),
59
- globals: slugsWhere(scope, (entity) => entity.hasDrafts, {
60
- collections: [],
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 sees an unchanged schema. Only the
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: slugEnum(collections) });
81
- if (collections.length === 0) return widen({ global: slugEnum(globals) });
82
- const global = slugEnum(globals).optional();
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: slugEnum(collections).optional(),
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
- /** For the reads that fall back to the default locale, unlike the writes. */ const READ_LOCALE_DESCRIPTION = "Default: the default locale. A value missing in this locale is shown from the default locale.";
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 = z.enum(scope.localization.locales);
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, draftVersionSlugs, entityShape, idSchema, idShape, liveWriteSentence, liveWriteSlugs, localeOf, localeShape, patchOnlySlugs, slugEnum, slugsFor, widen };
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 rather than read, because publish blockers only mean something
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.collections.writable.length + scope.globals.writable.length > 0,
18
+ isEnabled: (scope) => reaches(scope, "write"),
19
19
  inputSchema: (scope) => ({
20
20
  ...entityShape(scope, "write"),
21
21
  ...idShape(scope, "write"),
22
- ...localeShape(scope, { required: true })
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
- ...identityOf(target, doc["id"]),
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
  });
@@ -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
- /** The document or global as the key's user sees it, without `admin.hidden` fields. */ const readLive = async (scope, read, options) => {
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
- ...options.select === void 0 ? {} : { select: options.select },
22
- ...read.locale === void 0 ? {} : { locale: read.locale }
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
- /** `parent` comes back as a raw id or, populated, as the document itself. */ const isVersionOf = (version, id) => {
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 = typeof parent === "object" && parent !== null ? parent["id"] : parent;
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
- ...read.locale === void 0 ? {} : { locale: read.locale }
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
- /** One page of a document's or global's history, newest first. */ const queryVersions = async (scope, read, options) => {
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
- ...options.page === void 0 ? {} : { page: options.page },
93
- ...read.locale === void 0 ? {} : { locale: read.locale }
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
- /** The newest version whose status is published, or `null` if none is. */ const loadPublished = async (scope, read) => {
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",