@10x-media/form-builder 0.1.0-beta.25 → 0.1.0-beta.27

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 (108) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +21 -21
  3. package/dist/actions/body/serializeBody.d.ts +10 -14
  4. package/dist/actions/body/serializeBody.js +5 -10
  5. package/dist/actions/body/serializeBody.js.map +1 -1
  6. package/dist/actions/builtin/emailAction.d.ts +7 -0
  7. package/dist/actions/builtin/emailAction.js +14 -7
  8. package/dist/actions/builtin/emailAction.js.map +1 -1
  9. package/dist/actions/defineAction.d.ts +2 -4
  10. package/dist/actions/defineAction.js.map +1 -1
  11. package/dist/actions/dispatch.js +1 -1
  12. package/dist/actions/emailRender.d.ts +5 -5
  13. package/dist/actions/fromAddresses.d.ts +3 -1
  14. package/dist/actions/fromAddresses.js.map +1 -1
  15. package/dist/actions/recipientSources.d.ts +6 -17
  16. package/dist/actions/recipientSources.js.map +1 -1
  17. package/dist/actions/submissionContext.d.ts +34 -0
  18. package/dist/actions/task.js +12 -15
  19. package/dist/actions/task.js.map +1 -1
  20. package/dist/aggregation/aggregateResponses.js +3 -4
  21. package/dist/aggregation/aggregateResponses.js.map +1 -1
  22. package/dist/aggregation/resolveResultsRequest.js +38 -24
  23. package/dist/aggregation/resolveResultsRequest.js.map +1 -1
  24. package/dist/client/EndpointOptionsSelect.d.ts +1 -0
  25. package/dist/client/EndpointOptionsSelect.js +1 -0
  26. package/dist/client/EndpointOptionsSelect.js.map +1 -1
  27. package/dist/client/FieldNameSelect.d.ts +1 -0
  28. package/dist/client/FieldNameSelect.js +1 -0
  29. package/dist/client/FieldNameSelect.js.map +1 -1
  30. package/dist/client/RecipientsSelect.d.ts +1 -0
  31. package/dist/client/RecipientsSelect.js +1 -0
  32. package/dist/client/RecipientsSelect.js.map +1 -1
  33. package/dist/collections/formSubmissions.js +9 -10
  34. package/dist/collections/formSubmissions.js.map +1 -1
  35. package/dist/collections/forms.js +10 -10
  36. package/dist/collections/forms.js.map +1 -1
  37. package/dist/consent/resolveConsentSourcesRequest.js +2 -2
  38. package/dist/consent/resolveConsentSourcesRequest.js.map +1 -1
  39. package/dist/exports/react.d.ts +2 -2
  40. package/dist/exports/react.js +2 -2
  41. package/dist/exports/rsc.d.ts +3 -2
  42. package/dist/exports/rsc.js +5 -4
  43. package/dist/exports/types.d.ts +2 -1
  44. package/dist/form/findFormAtLocale.d.ts +30 -0
  45. package/dist/form/findFormAtLocale.js +88 -0
  46. package/dist/form/findFormAtLocale.js.map +1 -0
  47. package/dist/index.d.ts +5 -2
  48. package/dist/index.js +19 -11
  49. package/dist/index.js.map +1 -1
  50. package/dist/options.d.ts +39 -2
  51. package/dist/plugin/collectionSlugs.d.ts +28 -0
  52. package/dist/plugin/collectionSlugs.js +65 -0
  53. package/dist/plugin/collectionSlugs.js.map +1 -0
  54. package/dist/plugin/customState.js +1 -1
  55. package/dist/plugin/customState.js.map +1 -1
  56. package/dist/plugin/registerCollections.js +9 -4
  57. package/dist/plugin/registerCollections.js.map +1 -1
  58. package/dist/poll/closeJob.js +2 -2
  59. package/dist/poll/closeJob.js.map +1 -1
  60. package/dist/poll/outcomeBeforeChange.js +2 -2
  61. package/dist/poll/outcomeBeforeChange.js.map +1 -1
  62. package/dist/poll/resolvePollCloseRequest.js +3 -3
  63. package/dist/poll/resolvePollCloseRequest.js.map +1 -1
  64. package/dist/poll/resolvePollOptionsRequest.js +2 -2
  65. package/dist/poll/resolvePollOptionsRequest.js.map +1 -1
  66. package/dist/poll/resolvePollOutcome.js +4 -4
  67. package/dist/poll/resolvePollOutcome.js.map +1 -1
  68. package/dist/poll/votes/aggregateFromVotes.js +3 -2
  69. package/dist/poll/votes/aggregateFromVotes.js.map +1 -1
  70. package/dist/poll/votes/bumpPollVote.js +9 -6
  71. package/dist/poll/votes/bumpPollVote.js.map +1 -1
  72. package/dist/poll/votes/recountPollVotes.js +5 -5
  73. package/dist/poll/votes/recountPollVotes.js.map +1 -1
  74. package/dist/poll/votes/voteTallyHook.js +3 -3
  75. package/dist/poll/votes/voteTallyHook.js.map +1 -1
  76. package/dist/poll/votes/votesCollection.d.ts +5 -1
  77. package/dist/poll/votes/votesCollection.js +7 -2
  78. package/dist/poll/votes/votesCollection.js.map +1 -1
  79. package/dist/react/Form.d.ts +20 -7
  80. package/dist/react/Form.js +19 -13
  81. package/dist/react/Form.js.map +1 -1
  82. package/dist/react/Poll.d.ts +1 -1
  83. package/dist/react/Poll.js +7 -2
  84. package/dist/react/Poll.js.map +1 -1
  85. package/dist/react/fetchResults.d.ts +6 -3
  86. package/dist/react/fetchResults.js +10 -5
  87. package/dist/react/fetchResults.js.map +1 -1
  88. package/dist/react/submitForm.d.ts +12 -7
  89. package/dist/react/submitForm.js +5 -3
  90. package/dist/react/submitForm.js.map +1 -1
  91. package/dist/submissions/createSubmission.d.ts +7 -1
  92. package/dist/submissions/createSubmission.js +10 -5
  93. package/dist/submissions/createSubmission.js.map +1 -1
  94. package/dist/submissions/resolveVotedSubmission.js +5 -5
  95. package/dist/submissions/resolveVotedSubmission.js.map +1 -1
  96. package/dist/submissions/runSubmission.js +1 -1
  97. package/dist/submissions/submissionLocale.js +27 -6
  98. package/dist/submissions/submissionLocale.js.map +1 -1
  99. package/dist/submissions/validateSubmission.js +110 -103
  100. package/dist/submissions/validateSubmission.js.map +1 -1
  101. package/dist/submissions/voteChangeEndpoint.js +10 -9
  102. package/dist/submissions/voteChangeEndpoint.js.map +1 -1
  103. package/dist/submissions/votedCookie.d.ts +8 -4
  104. package/dist/submissions/votedCookie.js +18 -15
  105. package/dist/submissions/votedCookie.js.map +1 -1
  106. package/dist/validation/builtin/notAlreadySubmitted.js +2 -2
  107. package/dist/validation/builtin/notAlreadySubmitted.js.map +1 -1
  108. package/package.json +7 -5
@@ -0,0 +1,28 @@
1
+ import { CollectionSlug, Payload } from "payload";
2
+
3
+ //#region src/plugin/collectionSlugs.d.ts
4
+ /**
5
+ * The slugs of the collections the plugin registers, after host overrides. Typed as `CollectionSlug`
6
+ * because each one is a registered collection, so a host's generated slug union always contains it
7
+ * and the values pass straight to `payload.find({ collection })`.
8
+ */
9
+ type FormBuilderCollectionSlugs = {
10
+ forms: CollectionSlug;
11
+ formSubmissions: CollectionSlug;
12
+ pollVotes: CollectionSlug;
13
+ };
14
+ declare const DEFAULT_COLLECTION_SLUGS: Readonly<{
15
+ readonly forms: "forms";
16
+ readonly formSubmissions: "form-submissions";
17
+ readonly pollVotes: "form-poll-votes";
18
+ }>;
19
+ declare const DEFAULT_VOTED_COOKIE_PREFIX = "fb-voted-";
20
+ /**
21
+ * The collection slugs the plugin registered on this Payload instance, for code that queries the
22
+ * plugin's collections (`payload.find({ collection: collectionSlugsOf(payload).formSubmissions })`).
23
+ * The defaults when the plugin is disabled or absent.
24
+ */
25
+ declare const collectionSlugsOf: (payload: Payload) => FormBuilderCollectionSlugs;
26
+ //#endregion
27
+ export { DEFAULT_COLLECTION_SLUGS, DEFAULT_VOTED_COOKIE_PREFIX, FormBuilderCollectionSlugs, collectionSlugsOf };
28
+ //# sourceMappingURL=collectionSlugs.d.ts.map
@@ -0,0 +1,65 @@
1
+ import { customStateOf, stashCustomState } from "./customState.js";
2
+ //#region src/plugin/collectionSlugs.ts
3
+ const DEFAULT_COLLECTION_SLUGS = Object.freeze({
4
+ forms: "forms",
5
+ formSubmissions: "form-submissions",
6
+ pollVotes: "form-poll-votes"
7
+ });
8
+ const DEFAULT_VOTED_COOKIE_PREFIX = "fb-voted-";
9
+ const OPTION_PATHS = {
10
+ forms: "overrides.forms.slug",
11
+ formSubmissions: "overrides.formSubmissions.slug",
12
+ pollVotes: "poll.votes.overrides.slug"
13
+ };
14
+ const slugOf = (override, fallback) => {
15
+ const slug = override?.slug?.trim();
16
+ return slug ? slug : fallback;
17
+ };
18
+ /**
19
+ * Resolve every plugin collection slug from the `slug` each collection's override carries,
20
+ * falling back to the defaults. Throws when two plugin collections would share a slug, since
21
+ * Payload would merge them into one collection with the other's fields and hooks.
22
+ */
23
+ const resolveCollectionSlugs = (options) => {
24
+ const votes = options.poll?.votes;
25
+ const slugs = {
26
+ forms: slugOf(options.overrides?.forms, DEFAULT_COLLECTION_SLUGS.forms),
27
+ formSubmissions: slugOf(options.overrides?.formSubmissions, DEFAULT_COLLECTION_SLUGS.formSubmissions),
28
+ pollVotes: slugOf(votes === false ? void 0 : votes?.overrides, DEFAULT_COLLECTION_SLUGS.pollVotes)
29
+ };
30
+ const registered = Object.keys(slugs).filter((key) => key !== "pollVotes" || votes !== false);
31
+ for (const [index, key] of registered.entries()) {
32
+ const clash = registered.slice(index + 1).find((other) => slugs[other] === slugs[key]);
33
+ if (clash) throw new Error(`@10x-media/form-builder: ${OPTION_PATHS[key]} and ${OPTION_PATHS[clash]} both resolve to "${slugs[key]}"; each plugin collection needs its own slug.`);
34
+ }
35
+ return slugs;
36
+ };
37
+ const COOKIE_NAME_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
38
+ /** Validate `poll.cookiePrefix` at boot, so a bad name fails loudly instead of breaking `Set-Cookie`. */
39
+ const resolveVotedCookiePrefix = (prefix) => {
40
+ if (prefix === void 0) return DEFAULT_VOTED_COOKIE_PREFIX;
41
+ if (!COOKIE_NAME_TOKEN.test(prefix)) throw new Error(`@10x-media/form-builder: poll.cookiePrefix "${prefix}" is not a valid cookie name; use letters, digits, and !#$%&'*+-.^_\`|~ only.`);
42
+ return prefix;
43
+ };
44
+ const stashCollectionSlugs = (custom, collectionSlugs, votedCookiePrefix) => stashCustomState(custom, {
45
+ collectionSlugs,
46
+ ...votedCookiePrefix === void 0 ? {} : { votedCookiePrefix }
47
+ });
48
+ /**
49
+ * The collection slugs the plugin registered on this Payload instance, for code that queries the
50
+ * plugin's collections (`payload.find({ collection: collectionSlugsOf(payload).formSubmissions })`).
51
+ * The defaults when the plugin is disabled or absent.
52
+ */
53
+ const collectionSlugsOf = (payload) => customStateOf(payload).collectionSlugs ?? { ...DEFAULT_COLLECTION_SLUGS };
54
+ /**
55
+ * The resolved slugs typed as the default literals, for the plugin's own Local API calls. A plugin
56
+ * collection keeps the plugin's document shape under any slug, which is exactly what generated types
57
+ * for the default slugs describe, so reads narrow as they did when the slugs were constants. Host
58
+ * code uses `collectionSlugsOf`, whose `string` slugs match whatever the host registered.
59
+ */
60
+ const pluginSlugsOf = (payload) => collectionSlugsOf(payload);
61
+ const votedCookiePrefixOf = (payload) => customStateOf(payload).votedCookiePrefix ?? "fb-voted-";
62
+ //#endregion
63
+ export { DEFAULT_COLLECTION_SLUGS, DEFAULT_VOTED_COOKIE_PREFIX, collectionSlugsOf, pluginSlugsOf, resolveCollectionSlugs, resolveVotedCookiePrefix, stashCollectionSlugs, votedCookiePrefixOf };
64
+
65
+ //# sourceMappingURL=collectionSlugs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collectionSlugs.js","names":[],"sources":["../../src/plugin/collectionSlugs.ts"],"sourcesContent":["import type { CollectionSlug, Payload } from 'payload'\nimport { customStateOf, stashCustomState } from './customState'\n\n/**\n * The slugs of the collections the plugin registers, after host overrides. Typed as `CollectionSlug`\n * because each one is a registered collection, so a host's generated slug union always contains it\n * and the values pass straight to `payload.find({ collection })`.\n */\nexport type FormBuilderCollectionSlugs = {\n\tforms: CollectionSlug\n\tformSubmissions: CollectionSlug\n\tpollVotes: CollectionSlug\n}\n\nexport const DEFAULT_COLLECTION_SLUGS = Object.freeze({\n\tforms: 'forms',\n\tformSubmissions: 'form-submissions',\n\tpollVotes: 'form-poll-votes',\n} as const)\n\nexport const DEFAULT_VOTED_COOKIE_PREFIX = 'fb-voted-'\n\ntype SlugOverride = { slug?: string } | undefined\n\n/** The slice of the plugin options slugs are read from; structural so this module stays a leaf. */\nexport type CollectionSlugOptions = {\n\toverrides?: { forms?: SlugOverride; formSubmissions?: SlugOverride }\n\tpoll?: { votes?: false | { overrides?: SlugOverride } }\n}\n\nconst OPTION_PATHS: Record<keyof FormBuilderCollectionSlugs, string> = {\n\tforms: 'overrides.forms.slug',\n\tformSubmissions: 'overrides.formSubmissions.slug',\n\tpollVotes: 'poll.votes.overrides.slug',\n}\n\nconst slugOf = (override: SlugOverride, fallback: string): CollectionSlug => {\n\tconst slug = override?.slug?.trim()\n\t// Registered under this very slug at boot, so it is a member of the host's generated union.\n\treturn (slug ? slug : fallback) as CollectionSlug\n}\n\n/**\n * Resolve every plugin collection slug from the `slug` each collection's override carries,\n * falling back to the defaults. Throws when two plugin collections would share a slug, since\n * Payload would merge them into one collection with the other's fields and hooks.\n */\nexport const resolveCollectionSlugs = (\n\toptions: CollectionSlugOptions\n): FormBuilderCollectionSlugs => {\n\tconst votes = options.poll?.votes\n\tconst slugs: FormBuilderCollectionSlugs = {\n\t\tforms: slugOf(options.overrides?.forms, DEFAULT_COLLECTION_SLUGS.forms),\n\t\tformSubmissions: slugOf(\n\t\t\toptions.overrides?.formSubmissions,\n\t\t\tDEFAULT_COLLECTION_SLUGS.formSubmissions\n\t\t),\n\t\tpollVotes: slugOf(\n\t\t\tvotes === false ? undefined : votes?.overrides,\n\t\t\tDEFAULT_COLLECTION_SLUGS.pollVotes\n\t\t),\n\t}\n\tconst registered = (Object.keys(slugs) as (keyof FormBuilderCollectionSlugs)[]).filter(\n\t\t(key) => key !== 'pollVotes' || votes !== false\n\t)\n\tfor (const [index, key] of registered.entries()) {\n\t\tconst clash = registered.slice(index + 1).find((other) => slugs[other] === slugs[key])\n\t\tif (clash) {\n\t\t\tthrow new Error(\n\t\t\t\t`@10x-media/form-builder: ${OPTION_PATHS[key]} and ${OPTION_PATHS[clash]} both resolve to \"${slugs[key]}\"; each plugin collection needs its own slug.`\n\t\t\t)\n\t\t}\n\t}\n\treturn slugs\n}\n\nconst COOKIE_NAME_TOKEN = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/\n\n/** Validate `poll.cookiePrefix` at boot, so a bad name fails loudly instead of breaking `Set-Cookie`. */\nexport const resolveVotedCookiePrefix = (prefix: string | undefined): string => {\n\tif (prefix === undefined) {\n\t\treturn DEFAULT_VOTED_COOKIE_PREFIX\n\t}\n\tif (!COOKIE_NAME_TOKEN.test(prefix)) {\n\t\tthrow new Error(\n\t\t\t`@10x-media/form-builder: poll.cookiePrefix \"${prefix}\" is not a valid cookie name; use letters, digits, and !#$%&'*+-.^_\\`|~ only.`\n\t\t)\n\t}\n\treturn prefix\n}\n\ntype SlugState = { collectionSlugs: FormBuilderCollectionSlugs; votedCookiePrefix?: string }\n\nexport const stashCollectionSlugs = (\n\tcustom: Record<string, unknown> | undefined,\n\tcollectionSlugs: FormBuilderCollectionSlugs,\n\tvotedCookiePrefix?: string\n): Record<string, unknown> =>\n\tstashCustomState<SlugState>(custom, {\n\t\tcollectionSlugs,\n\t\t...(votedCookiePrefix === undefined ? {} : { votedCookiePrefix }),\n\t})\n\n/**\n * The collection slugs the plugin registered on this Payload instance, for code that queries the\n * plugin's collections (`payload.find({ collection: collectionSlugsOf(payload).formSubmissions })`).\n * The defaults when the plugin is disabled or absent.\n */\nexport const collectionSlugsOf = (payload: Payload): FormBuilderCollectionSlugs =>\n\tcustomStateOf<SlugState>(payload).collectionSlugs ?? { ...DEFAULT_COLLECTION_SLUGS }\n\n/**\n * The resolved slugs typed as the default literals, for the plugin's own Local API calls. A plugin\n * collection keeps the plugin's document shape under any slug, which is exactly what generated types\n * for the default slugs describe, so reads narrow as they did when the slugs were constants. Host\n * code uses `collectionSlugsOf`, whose `string` slugs match whatever the host registered.\n */\nexport const pluginSlugsOf = (payload: Payload): typeof DEFAULT_COLLECTION_SLUGS =>\n\tcollectionSlugsOf(payload) as unknown as typeof DEFAULT_COLLECTION_SLUGS\n\nexport const votedCookiePrefixOf = (payload: Payload): string =>\n\tcustomStateOf<SlugState>(payload).votedCookiePrefix ?? DEFAULT_VOTED_COOKIE_PREFIX\n"],"mappings":";;AAcA,MAAa,2BAA2B,OAAO,OAAO;CACrD,OAAO;CACP,iBAAiB;CACjB,WAAW;AACZ,CAAU;AAEV,MAAa,8BAA8B;AAU3C,MAAM,eAAiE;CACtE,OAAO;CACP,iBAAiB;CACjB,WAAW;AACZ;AAEA,MAAM,UAAU,UAAwB,aAAqC;CAC5E,MAAM,OAAO,UAAU,MAAM,KAAK;CAElC,OAAQ,OAAO,OAAO;AACvB;;;;;;AAOA,MAAa,0BACZ,YACgC;CAChC,MAAM,QAAQ,QAAQ,MAAM;CAC5B,MAAM,QAAoC;EACzC,OAAO,OAAO,QAAQ,WAAW,OAAO,yBAAyB,KAAK;EACtE,iBAAiB,OAChB,QAAQ,WAAW,iBACnB,yBAAyB,eAC1B;EACA,WAAW,OACV,UAAU,QAAQ,KAAA,IAAY,OAAO,WACrC,yBAAyB,SAC1B;CACD;CACA,MAAM,aAAc,OAAO,KAAK,KAAK,EAA2C,QAC9E,QAAQ,QAAQ,eAAe,UAAU,KAC3C;CACA,KAAK,MAAM,CAAC,OAAO,QAAQ,WAAW,QAAQ,GAAG;EAChD,MAAM,QAAQ,WAAW,MAAM,QAAQ,CAAC,EAAE,MAAM,UAAU,MAAM,WAAW,MAAM,IAAI;EACrF,IAAI,OACH,MAAM,IAAI,MACT,4BAA4B,aAAa,KAAK,OAAO,aAAa,OAAO,oBAAoB,MAAM,KAAK,8CACzG;CAEF;CACA,OAAO;AACR;AAEA,MAAM,oBAAoB;;AAG1B,MAAa,4BAA4B,WAAuC;CAC/E,IAAI,WAAW,KAAA,GACd,OAAO;CAER,IAAI,CAAC,kBAAkB,KAAK,MAAM,GACjC,MAAM,IAAI,MACT,+CAA+C,OAAO,8EACvD;CAED,OAAO;AACR;AAIA,MAAa,wBACZ,QACA,iBACA,sBAEA,iBAA4B,QAAQ;CACnC;CACA,GAAI,sBAAsB,KAAA,IAAY,CAAC,IAAI,EAAE,kBAAkB;AAChE,CAAC;;;;;;AAOF,MAAa,qBAAqB,YACjC,cAAyB,OAAO,EAAE,mBAAmB,EAAE,GAAG,yBAAyB;;;;;;;AAQpF,MAAa,iBAAiB,YAC7B,kBAAkB,OAAO;AAE1B,MAAa,uBAAuB,YACnC,cAAyB,OAAO,EAAE,qBAAA"}
@@ -19,7 +19,7 @@ const stashCustomState = (custom, patch) => {
19
19
  };
20
20
  };
21
21
  /** The plugin's slice of `payload.config.custom`, or an empty object when the plugin never booted. */
22
- const customStateOf = (payload) => payload.config.custom?.[CUSTOM_KEY] ?? {};
22
+ const customStateOf = (payload) => payload.config?.custom?.[CUSTOM_KEY] ?? {};
23
23
  //#endregion
24
24
  export { customStateOf, stashCustomState };
25
25
 
@@ -1 +1 @@
1
- {"version":3,"file":"customState.js","names":[],"sources":["../../src/plugin/customState.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\n/** Root key under `config.custom` where the plugin stashes runtime state for root-level helpers. */\nconst CUSTOM_KEY = '@10x-media/form-builder'\n\n/**\n * Merge `patch` into the plugin's slice of `config.custom`, leaving every other key untouched.\n * Root-level helpers (`resolvePollOptions`, `resolveConsentStatements`) are called by hosts with a\n * `payload` instance and no plugin state, so the plugin parks what they need on the config at boot\n * and they read it back through `payload.config`. Both readers share one key, so the plugin's slice\n * is patched rather than replaced.\n */\nexport const stashCustomState = <TState extends Record<string, unknown>>(\n\tcustom: Record<string, unknown> | undefined,\n\tpatch: TState\n): Record<string, unknown> => {\n\tconst existing =\n\t\tcustom?.[CUSTOM_KEY] != null && typeof custom[CUSTOM_KEY] === 'object'\n\t\t\t? (custom[CUSTOM_KEY] as Record<string, unknown>)\n\t\t\t: {}\n\treturn { ...(custom ?? {}), [CUSTOM_KEY]: { ...existing, ...patch } }\n}\n\n/** The plugin's slice of `payload.config.custom`, or an empty object when the plugin never booted. */\nexport const customStateOf = <TState extends Record<string, unknown>>(\n\tpayload: Payload\n): Partial<TState> => (payload.config.custom?.[CUSTOM_KEY] as Partial<TState> | undefined) ?? {}\n"],"mappings":";;AAGA,MAAM,aAAa;;;;;;;;AASnB,MAAa,oBACZ,QACA,UAC6B;CAC7B,MAAM,WACL,SAAS,eAAe,QAAQ,OAAO,OAAO,gBAAgB,WAC1D,OAAO,cACR,CAAC;CACL,OAAO;EAAE,GAAI,UAAU,CAAC;GAAK,aAAa;GAAE,GAAG;GAAU,GAAG;EAAM;CAAE;AACrE;;AAGA,MAAa,iBACZ,YACsB,QAAQ,OAAO,SAAS,eAA+C,CAAC"}
1
+ {"version":3,"file":"customState.js","names":[],"sources":["../../src/plugin/customState.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\n/** Root key under `config.custom` where the plugin stashes runtime state for root-level helpers. */\nconst CUSTOM_KEY = '@10x-media/form-builder'\n\n/**\n * Merge `patch` into the plugin's slice of `config.custom`, leaving every other key untouched.\n * Root-level helpers (`resolvePollOptions`, `resolveConsentStatements`) are called by hosts with a\n * `payload` instance and no plugin state, so the plugin parks what they need on the config at boot\n * and they read it back through `payload.config`. Both readers share one key, so the plugin's slice\n * is patched rather than replaced.\n */\nexport const stashCustomState = <TState extends Record<string, unknown>>(\n\tcustom: Record<string, unknown> | undefined,\n\tpatch: TState\n): Record<string, unknown> => {\n\tconst existing =\n\t\tcustom?.[CUSTOM_KEY] != null && typeof custom[CUSTOM_KEY] === 'object'\n\t\t\t? (custom[CUSTOM_KEY] as Record<string, unknown>)\n\t\t\t: {}\n\treturn { ...(custom ?? {}), [CUSTOM_KEY]: { ...existing, ...patch } }\n}\n\n/** The plugin's slice of `payload.config.custom`, or an empty object when the plugin never booted. */\nexport const customStateOf = <TState extends Record<string, unknown>>(\n\tpayload: Payload\n): Partial<TState> => (payload.config?.custom?.[CUSTOM_KEY] as Partial<TState> | undefined) ?? {}\n"],"mappings":";;AAGA,MAAM,aAAa;;;;;;;;AASnB,MAAa,oBACZ,QACA,UAC6B;CAC7B,MAAM,WACL,SAAS,eAAe,QAAQ,OAAO,OAAO,gBAAgB,WAC1D,OAAO,cACR,CAAC;CACL,OAAO;EAAE,GAAI,UAAU,CAAC;GAAK,aAAa;GAAE,GAAG;GAAU,GAAG;EAAM;CAAE;AACrE;;AAGA,MAAa,iBACZ,YACsB,QAAQ,QAAQ,SAAS,eAA+C,CAAC"}
@@ -1,11 +1,11 @@
1
+ import { registerActionsTask } from "../actions/task.js";
1
2
  import { buildPollVotesCollection } from "../poll/votes/votesCollection.js";
3
+ import { buildSubmissionsCollection } from "../collections/formSubmissions.js";
2
4
  import { registerPollCloseTask } from "../poll/closeJob.js";
3
5
  import { buildFormsCollection } from "../collections/forms.js";
4
- import { buildSubmissionsCollection } from "../collections/formSubmissions.js";
5
- import { registerActionsTask } from "../actions/task.js";
6
6
  import { attachUploadsCollection } from "./uploadsCollection.js";
7
7
  //#region src/plugin/registerCollections.ts
8
- const registerCollections = ({ config, registry, ruleRegistry, calcAllowed, calcSources, calcFunctions, consentSources, consentSnapshot, consentResolveOnRead, actionRegistry, richText, hasJobsPlugin, dispatchDeadlineMs, events, uploads, spam, showSubmissionRawFields, localizeContent, resultsAccess, votedCookie, pollSourceRegistry, pollTypeRegistry, outcomeFields, pollVotes, buttons, settings, response, fromAddresses, fromSources, departments, redirectRelationships, overrides }) => {
8
+ const registerCollections = ({ config, registry, ruleRegistry, calcAllowed, calcSources, calcFunctions, consentSources, consentSnapshot, consentResolveOnRead, actionRegistry, richText, hasJobsPlugin, dispatchDeadlineMs, events, uploads, spam, showSubmissionRawFields, localizeContent, resultsAccess, votedCookie, pollSourceRegistry, pollTypeRegistry, outcomeFields, pollVotes, buttons, settings, response, fromAddresses, fromSources, departments, redirectRelationships, slugs, overrides }) => {
9
9
  const pollVotesEnabled = pollVotes !== false;
10
10
  registerActionsTask(config, actionRegistry, richText);
11
11
  registerPollCloseTask(config, pollVotesEnabled);
@@ -41,6 +41,7 @@ const registerCollections = ({ config, registry, ruleRegistry, calcAllowed, calc
41
41
  fromSources,
42
42
  departments,
43
43
  redirectRelationships,
44
+ slugs,
44
45
  overrides: overrides?.forms
45
46
  }),
46
47
  buildSubmissionsCollection({
@@ -61,9 +62,13 @@ const registerCollections = ({ config, registry, ruleRegistry, calcAllowed, calc
61
62
  pollSourceRegistry,
62
63
  pollVotes,
63
64
  showRawFields: showSubmissionRawFields,
65
+ slugs,
64
66
  overrides: overrides?.formSubmissions
65
67
  }),
66
- ...pollVotes === false ? [] : [buildPollVotesCollection({ overrides: pollVotes.overrides })]
68
+ ...pollVotes === false ? [] : [buildPollVotesCollection({
69
+ overrides: pollVotes.overrides,
70
+ slug: slugs.pollVotes
71
+ })]
67
72
  ];
68
73
  };
69
74
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"registerCollections.js","names":[],"sources":["../../src/plugin/registerCollections.ts"],"sourcesContent":["import type { CollectionSlug, Config } from 'payload'\nimport type { RichTextBodyOption } from '../actions/body/serializeBody'\nimport type { FromAddressesResolver, FromAddressSourceRegistry } from '../actions/fromAddresses'\nimport type { ActionRegistry } from '../actions/registry'\nimport { registerActionsTask } from '../actions/task'\nimport type { FormResultsAccess } from '../aggregation/resolveResultsRequest'\nimport type { CalcAllowed } from '../calc/normalizeCalc'\nimport type { CalcFunction, CalcSource } from '../calc/registry'\nimport type { ButtonsOption } from '../collections/buttonFields'\nimport { buildSubmissionsCollection } from '../collections/formSubmissions'\nimport { buildFormsCollection } from '../collections/forms'\nimport type { ResponseOption } from '../collections/redirectFields'\nimport type { SettingsOption } from '../collections/settingsFields'\nimport type { ConsentSnapshotMode } from '../consent/captureConsent'\nimport type { ConsentSourcesResolver } from '../consent/types'\nimport type { DepartmentEmailsResolver } from '../email/departments'\nimport type { FormEventSink } from '../events/types'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport { registerPollCloseTask } from '../poll/closeJob'\nimport type { OutcomeFieldsOverride } from '../poll/outcomeFields'\nimport type { PollTypeRegistry } from '../poll/pollTypeRegistry'\nimport type { PollOptionSourceRegistry } from '../poll/registry'\nimport { buildPollVotesCollection } from '../poll/votes/votesCollection'\nimport type { ResolvedSpamConfig } from '../spam/types'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport type { CollectionOverrides } from './collectionOverrides'\nimport { attachUploadsCollection, type UploadsOption } from './uploadsCollection'\n\ntype RegisterCollectionsArgs = {\n\tconfig: Config\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\t/** Registered calc extension names; gates which expressions the forms beforeValidate accepts. */\n\tcalcAllowed?: CalcAllowed\n\t/** Registered calc sources (plugin option `calc.sources`); resolved on form reads and at submit. */\n\tcalcSources?: Record<string, CalcSource>\n\t/** Registered calc functions (plugin option `calc.functions`); threaded into submit-time evaluation. */\n\tcalcFunctions?: Record<string, CalcFunction>\n\tconsentSources?: ConsentSourcesResolver\n\tconsentSnapshot?: ConsentSnapshotMode\n\t/** Plugin `consent.resolveOnRead` (default true); false skips the per-read consent afterRead hook. */\n\tconsentResolveOnRead?: boolean\n\tactionRegistry: ActionRegistry\n\trichText?: RichTextBodyOption\n\thasJobsPlugin: boolean\n\t/** Host override for the inline dispatch deadline (plugin option `dispatch.deadlineMs`). */\n\tdispatchDeadlineMs?: number\n\tevents?: FormEventSink\n\tuploads: UploadsOption\n\tspam: ResolvedSpamConfig | false\n\tshowSubmissionRawFields: boolean\n\tlocalizeContent: boolean\n\tresultsAccess?: FormResultsAccess\n\tvotedCookie: boolean\n\tpollSourceRegistry: PollOptionSourceRegistry\n\tpollTypeRegistry: PollTypeRegistry\n\toutcomeFields?: OutcomeFieldsOverride\n\t/** Resolved `poll.votes` option; `false` skips registering the hidden tally collection. */\n\tpollVotes: false | { overrides?: CollectionOverrides }\n\tbuttons?: ButtonsOption\n\tsettings?: SettingsOption\n\tresponse?: ResponseOption\n\tfromAddresses?: FromAddressesResolver\n\tfromSources?: FromAddressSourceRegistry\n\tdepartments?: DepartmentEmailsResolver\n\tredirectRelationships?: CollectionSlug[]\n\toverrides?: {\n\t\tforms?: CollectionOverrides\n\t\tformSubmissions?: CollectionOverrides\n\t}\n}\n\nexport const registerCollections = ({\n\tconfig,\n\tregistry,\n\truleRegistry,\n\tcalcAllowed,\n\tcalcSources,\n\tcalcFunctions,\n\tconsentSources,\n\tconsentSnapshot,\n\tconsentResolveOnRead,\n\tactionRegistry,\n\trichText,\n\thasJobsPlugin,\n\tdispatchDeadlineMs,\n\tevents,\n\tuploads,\n\tspam,\n\tshowSubmissionRawFields,\n\tlocalizeContent,\n\tresultsAccess,\n\tvotedCookie,\n\tpollSourceRegistry,\n\tpollTypeRegistry,\n\toutcomeFields,\n\tpollVotes,\n\tbuttons,\n\tsettings,\n\tresponse,\n\tfromAddresses,\n\tfromSources,\n\tdepartments,\n\tredirectRelationships,\n\toverrides,\n}: RegisterCollectionsArgs): void => {\n\tconst pollVotesEnabled = pollVotes !== false\n\tregisterActionsTask(config, actionRegistry, richText)\n\tregisterPollCloseTask(config, pollVotesEnabled)\n\tconst hasRunner = Boolean(config.jobs?.autoRun) || hasJobsPlugin\n\n\tconst uploadSlug = uploads === false ? undefined : uploads.collection\n\tif (uploads !== false) {\n\t\tattachUploadsCollection({ config, slug: uploads.collection, spam })\n\t}\n\n\t// Payload lists nav entries in registration order (no nav-sort option). The primary\n\t// collection leads: forms, then form-submissions. Ordering the plugin's nav group against\n\t// host collections (including a BYO uploads collection) stays a host concern.\n\tconfig.collections = [\n\t\t...(config.collections ?? []),\n\t\tbuildFormsCollection({\n\t\t\tregistry,\n\t\t\truleRegistry,\n\t\t\tcalcAllowed,\n\t\t\tcalcSources,\n\t\t\tconsentSources,\n\t\t\tconsentResolveOnRead,\n\t\t\tactionRegistry,\n\t\t\tlocalizeContent,\n\t\t\trichText,\n\t\t\tuploadsCollectionSlug: uploadSlug,\n\t\t\tresultsAccess,\n\t\t\tpollVotesEnabled,\n\t\t\tpollSourceRegistry,\n\t\t\tpollTypeRegistry,\n\t\t\toutcomeFields,\n\t\t\tbuttons,\n\t\t\tsettings,\n\t\t\tresponse,\n\t\t\tfromAddresses,\n\t\t\tfromSources,\n\t\t\tdepartments,\n\t\t\tredirectRelationships,\n\t\t\toverrides: overrides?.forms,\n\t\t}),\n\t\tbuildSubmissionsCollection({\n\t\t\tregistry,\n\t\t\truleRegistry,\n\t\t\tcalcSources,\n\t\t\tcalcFunctions,\n\t\t\tconsentSources,\n\t\t\tconsentSnapshot,\n\t\t\tactionRegistry,\n\t\t\trichText,\n\t\t\tevents,\n\t\t\thasRunner,\n\t\t\tdispatchDeadlineMs,\n\t\t\tuploadSlug,\n\t\t\tspam,\n\t\t\tvotedCookie,\n\t\t\tpollSourceRegistry,\n\t\t\tpollVotes,\n\t\t\tshowRawFields: showSubmissionRawFields,\n\t\t\toverrides: overrides?.formSubmissions,\n\t\t}),\n\t\t...(pollVotes === false ? [] : [buildPollVotesCollection({ overrides: pollVotes.overrides })]),\n\t]\n}\n"],"mappings":";;;;;;;AAwEA,MAAa,uBAAuB,EACnC,QACA,UACA,cACA,aACA,aACA,eACA,gBACA,iBACA,sBACA,gBACA,UACA,eACA,oBACA,QACA,SACA,MACA,yBACA,iBACA,eACA,aACA,oBACA,kBACA,eACA,WACA,SACA,UACA,UACA,eACA,aACA,aACA,uBACA,gBACoC;CACpC,MAAM,mBAAmB,cAAc;CACvC,oBAAoB,QAAQ,gBAAgB,QAAQ;CACpD,sBAAsB,QAAQ,gBAAgB;CAC9C,MAAM,YAAY,QAAQ,OAAO,MAAM,OAAO,KAAK;CAEnD,MAAM,aAAa,YAAY,QAAQ,KAAA,IAAY,QAAQ;CAC3D,IAAI,YAAY,OACf,wBAAwB;EAAE;EAAQ,MAAM,QAAQ;EAAY;CAAK,CAAC;CAMnE,OAAO,cAAc;EACpB,GAAI,OAAO,eAAe,CAAC;EAC3B,qBAAqB;GACpB;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA,uBAAuB;GACvB;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA,WAAW,WAAW;EACvB,CAAC;EACD,2BAA2B;GAC1B;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA,eAAe;GACf,WAAW,WAAW;EACvB,CAAC;EACD,GAAI,cAAc,QAAQ,CAAC,IAAI,CAAC,yBAAyB,EAAE,WAAW,UAAU,UAAU,CAAC,CAAC;CAC7F;AACD"}
1
+ {"version":3,"file":"registerCollections.js","names":[],"sources":["../../src/plugin/registerCollections.ts"],"sourcesContent":["import type { CollectionSlug, Config } from 'payload'\nimport type { RichTextBodyOption } from '../actions/body/serializeBody'\nimport type { FromAddressesResolver, FromAddressSourceRegistry } from '../actions/fromAddresses'\nimport type { ActionRegistry } from '../actions/registry'\nimport { registerActionsTask } from '../actions/task'\nimport type { FormResultsAccess } from '../aggregation/resolveResultsRequest'\nimport type { CalcAllowed } from '../calc/normalizeCalc'\nimport type { CalcFunction, CalcSource } from '../calc/registry'\nimport type { ButtonsOption } from '../collections/buttonFields'\nimport { buildSubmissionsCollection } from '../collections/formSubmissions'\nimport { buildFormsCollection } from '../collections/forms'\nimport type { ResponseOption } from '../collections/redirectFields'\nimport type { SettingsOption } from '../collections/settingsFields'\nimport type { ConsentSnapshotMode } from '../consent/captureConsent'\nimport type { ConsentSourcesResolver } from '../consent/types'\nimport type { DepartmentEmailsResolver } from '../email/departments'\nimport type { FormEventSink } from '../events/types'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport { registerPollCloseTask } from '../poll/closeJob'\nimport type { OutcomeFieldsOverride } from '../poll/outcomeFields'\nimport type { PollTypeRegistry } from '../poll/pollTypeRegistry'\nimport type { PollOptionSourceRegistry } from '../poll/registry'\nimport { buildPollVotesCollection } from '../poll/votes/votesCollection'\nimport type { ResolvedSpamConfig } from '../spam/types'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport type { CollectionOverrides } from './collectionOverrides'\nimport type { FormBuilderCollectionSlugs } from './collectionSlugs'\nimport { attachUploadsCollection, type UploadsOption } from './uploadsCollection'\n\ntype RegisterCollectionsArgs = {\n\tconfig: Config\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\t/** Registered calc extension names; gates which expressions the forms beforeValidate accepts. */\n\tcalcAllowed?: CalcAllowed\n\t/** Registered calc sources (plugin option `calc.sources`); resolved on form reads and at submit. */\n\tcalcSources?: Record<string, CalcSource>\n\t/** Registered calc functions (plugin option `calc.functions`); threaded into submit-time evaluation. */\n\tcalcFunctions?: Record<string, CalcFunction>\n\tconsentSources?: ConsentSourcesResolver\n\tconsentSnapshot?: ConsentSnapshotMode\n\t/** Plugin `consent.resolveOnRead` (default true); false skips the per-read consent afterRead hook. */\n\tconsentResolveOnRead?: boolean\n\tactionRegistry: ActionRegistry\n\trichText?: RichTextBodyOption\n\thasJobsPlugin: boolean\n\t/** Host override for the inline dispatch deadline (plugin option `dispatch.deadlineMs`). */\n\tdispatchDeadlineMs?: number\n\tevents?: FormEventSink\n\tuploads: UploadsOption\n\tspam: ResolvedSpamConfig | false\n\tshowSubmissionRawFields: boolean\n\tlocalizeContent: boolean\n\tresultsAccess?: FormResultsAccess\n\tvotedCookie: boolean\n\tpollSourceRegistry: PollOptionSourceRegistry\n\tpollTypeRegistry: PollTypeRegistry\n\toutcomeFields?: OutcomeFieldsOverride\n\t/** Resolved `poll.votes` option; `false` skips registering the hidden tally collection. */\n\tpollVotes: false | { overrides?: CollectionOverrides }\n\tbuttons?: ButtonsOption\n\tsettings?: SettingsOption\n\tresponse?: ResponseOption\n\tfromAddresses?: FromAddressesResolver\n\tfromSources?: FromAddressSourceRegistry\n\tdepartments?: DepartmentEmailsResolver\n\tredirectRelationships?: CollectionSlug[]\n\t/** Resolved plugin collection slugs; see `resolveCollectionSlugs`. */\n\tslugs: FormBuilderCollectionSlugs\n\toverrides?: {\n\t\tforms?: CollectionOverrides\n\t\tformSubmissions?: CollectionOverrides\n\t}\n}\n\nexport const registerCollections = ({\n\tconfig,\n\tregistry,\n\truleRegistry,\n\tcalcAllowed,\n\tcalcSources,\n\tcalcFunctions,\n\tconsentSources,\n\tconsentSnapshot,\n\tconsentResolveOnRead,\n\tactionRegistry,\n\trichText,\n\thasJobsPlugin,\n\tdispatchDeadlineMs,\n\tevents,\n\tuploads,\n\tspam,\n\tshowSubmissionRawFields,\n\tlocalizeContent,\n\tresultsAccess,\n\tvotedCookie,\n\tpollSourceRegistry,\n\tpollTypeRegistry,\n\toutcomeFields,\n\tpollVotes,\n\tbuttons,\n\tsettings,\n\tresponse,\n\tfromAddresses,\n\tfromSources,\n\tdepartments,\n\tredirectRelationships,\n\tslugs,\n\toverrides,\n}: RegisterCollectionsArgs): void => {\n\tconst pollVotesEnabled = pollVotes !== false\n\tregisterActionsTask(config, actionRegistry, richText)\n\tregisterPollCloseTask(config, pollVotesEnabled)\n\tconst hasRunner = Boolean(config.jobs?.autoRun) || hasJobsPlugin\n\n\tconst uploadSlug = uploads === false ? undefined : uploads.collection\n\tif (uploads !== false) {\n\t\tattachUploadsCollection({ config, slug: uploads.collection, spam })\n\t}\n\n\t// Payload lists nav entries in registration order (no nav-sort option). The primary\n\t// collection leads: forms, then form-submissions. Ordering the plugin's nav group against\n\t// host collections (including a BYO uploads collection) stays a host concern.\n\tconfig.collections = [\n\t\t...(config.collections ?? []),\n\t\tbuildFormsCollection({\n\t\t\tregistry,\n\t\t\truleRegistry,\n\t\t\tcalcAllowed,\n\t\t\tcalcSources,\n\t\t\tconsentSources,\n\t\t\tconsentResolveOnRead,\n\t\t\tactionRegistry,\n\t\t\tlocalizeContent,\n\t\t\trichText,\n\t\t\tuploadsCollectionSlug: uploadSlug,\n\t\t\tresultsAccess,\n\t\t\tpollVotesEnabled,\n\t\t\tpollSourceRegistry,\n\t\t\tpollTypeRegistry,\n\t\t\toutcomeFields,\n\t\t\tbuttons,\n\t\t\tsettings,\n\t\t\tresponse,\n\t\t\tfromAddresses,\n\t\t\tfromSources,\n\t\t\tdepartments,\n\t\t\tredirectRelationships,\n\t\t\tslugs,\n\t\t\toverrides: overrides?.forms,\n\t\t}),\n\t\tbuildSubmissionsCollection({\n\t\t\tregistry,\n\t\t\truleRegistry,\n\t\t\tcalcSources,\n\t\t\tcalcFunctions,\n\t\t\tconsentSources,\n\t\t\tconsentSnapshot,\n\t\t\tactionRegistry,\n\t\t\trichText,\n\t\t\tevents,\n\t\t\thasRunner,\n\t\t\tdispatchDeadlineMs,\n\t\t\tuploadSlug,\n\t\t\tspam,\n\t\t\tvotedCookie,\n\t\t\tpollSourceRegistry,\n\t\t\tpollVotes,\n\t\t\tshowRawFields: showSubmissionRawFields,\n\t\t\tslugs,\n\t\t\toverrides: overrides?.formSubmissions,\n\t\t}),\n\t\t...(pollVotes === false\n\t\t\t? []\n\t\t\t: [buildPollVotesCollection({ overrides: pollVotes.overrides, slug: slugs.pollVotes })]),\n\t]\n}\n"],"mappings":";;;;;;;AA2EA,MAAa,uBAAuB,EACnC,QACA,UACA,cACA,aACA,aACA,eACA,gBACA,iBACA,sBACA,gBACA,UACA,eACA,oBACA,QACA,SACA,MACA,yBACA,iBACA,eACA,aACA,oBACA,kBACA,eACA,WACA,SACA,UACA,UACA,eACA,aACA,aACA,uBACA,OACA,gBACoC;CACpC,MAAM,mBAAmB,cAAc;CACvC,oBAAoB,QAAQ,gBAAgB,QAAQ;CACpD,sBAAsB,QAAQ,gBAAgB;CAC9C,MAAM,YAAY,QAAQ,OAAO,MAAM,OAAO,KAAK;CAEnD,MAAM,aAAa,YAAY,QAAQ,KAAA,IAAY,QAAQ;CAC3D,IAAI,YAAY,OACf,wBAAwB;EAAE;EAAQ,MAAM,QAAQ;EAAY;CAAK,CAAC;CAMnE,OAAO,cAAc;EACpB,GAAI,OAAO,eAAe,CAAC;EAC3B,qBAAqB;GACpB;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA,uBAAuB;GACvB;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA,WAAW,WAAW;EACvB,CAAC;EACD,2BAA2B;GAC1B;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA,eAAe;GACf;GACA,WAAW,WAAW;EACvB,CAAC;EACD,GAAI,cAAc,QACf,CAAC,IACD,CAAC,yBAAyB;GAAE,WAAW,UAAU;GAAW,MAAM,MAAM;EAAU,CAAC,CAAC;CACxF;AACD"}
@@ -1,6 +1,6 @@
1
+ import { pluginSlugsOf } from "../plugin/collectionSlugs.js";
1
2
  import { isPollClosed, pollConfigOf } from "../form/pollState.js";
2
3
  import { resolvePollOutcome } from "./resolvePollOutcome.js";
3
- import { FORMS_SLUG } from "../collections/forms.js";
4
4
  //#region src/poll/closeJob.ts
5
5
  const POLL_CLOSE_TASK_SLUG = "form-builder-poll-close";
6
6
  /** Payload's jobs collection; queried directly to supersede a superseded pending close job. */
@@ -27,7 +27,7 @@ const shouldAutoResolvePoll = (form) => {
27
27
  const runPollClose = async (args) => {
28
28
  const { payload, input, pollVotesEnabled, req } = args;
29
29
  const form = await payload.findByID({
30
- collection: FORMS_SLUG,
30
+ collection: pluginSlugsOf(payload).forms,
31
31
  id: input.formId,
32
32
  depth: 0,
33
33
  overrideAccess: true,
@@ -1 +1 @@
1
- {"version":3,"file":"closeJob.js","names":[],"sources":["../../src/poll/closeJob.ts"],"sourcesContent":["import type { Config, Payload, PayloadRequest, TaskConfig } from 'payload'\nimport { FORMS_SLUG } from '../collections/forms'\nimport { isPollClosed, type PollConfigLike, pollConfigOf } from '../form/pollState'\nimport { resolvePollOutcome } from './resolvePollOutcome'\n\nexport const POLL_CLOSE_TASK_SLUG = 'form-builder-poll-close'\n\n/** Payload's jobs collection; queried directly to supersede a superseded pending close job. */\nconst JOBS_SLUG = 'payload-jobs'\n\n/** Input the enqueue path stores; the handler re-loads everything else from the DB. */\nexport type PollCloseTaskInput = { formId: number | string }\n\n/** The slice of a loaded form the close gate reads. */\nexport type PollFormLike = { pollEnabled?: boolean | null; poll?: unknown }\n\nconst hasEmptyWinners = (poll: PollConfigLike): boolean => {\n\tconst winners = poll.outcome?.winningValues\n\treturn (\n\t\t!Array.isArray(winners) ||\n\t\twinners.filter((value) => typeof value === 'string' && value.length > 0).length === 0\n\t)\n}\n\nconst strategyOf = (poll: PollConfigLike): string =>\n\ttypeof poll.type === 'string' && poll.type.length > 0 ? poll.type : 'manual'\n\n/**\n * True when a poll's outcome should auto-resolve now: enabled, closed, still unresolved, and its\n * strategy is not `manual`. Shared by the close-job handler and the results-read fallback so the two\n * triggers apply the exact same gate.\n */\nexport const shouldAutoResolvePoll = (form: PollFormLike): boolean => {\n\tconst poll = pollConfigOf(form.poll)\n\treturn (\n\t\tform.pollEnabled === true &&\n\t\tpoll != null &&\n\t\tisPollClosed(poll) &&\n\t\thasEmptyWinners(poll) &&\n\t\tstrategyOf(poll) !== 'manual'\n\t)\n}\n\n/**\n * Run the scheduled close for one form. Idempotent: re-loads the form and only resolves when the\n * poll is enabled, closed, still unresolved, and not `manual`, so a re-run after the outcome was\n * recorded (or a job that fires slightly early) is a safe no-op. Tolerates a deleted form.\n */\nexport const runPollClose = async (args: {\n\tpayload: Payload\n\tinput: PollCloseTaskInput\n\t/** Whether the hidden tally store backs the outcome aggregation (see `resolvePollOutcome`). */\n\tpollVotesEnabled?: boolean\n\treq?: PayloadRequest\n}): Promise<void> => {\n\tconst { payload, input, pollVotesEnabled, req } = args\n\tconst form = await payload\n\t\t.findByID({ collection: FORMS_SLUG, id: input.formId, depth: 0, overrideAccess: true, req })\n\t\t.catch(() => null)\n\tif (!form || !shouldAutoResolvePoll(form as PollFormLike)) {\n\t\treturn\n\t}\n\tawait resolvePollOutcome({ payload, formId: input.formId, pollVotesEnabled, req })\n}\n\n/** Native Payload jobs task that resolves a poll's outcome once it closes. */\nexport const buildPollCloseTask = (pollVotesEnabled = false): TaskConfig =>\n\t({\n\t\tslug: POLL_CLOSE_TASK_SLUG,\n\t\tinputSchema: [{ name: 'formId', type: 'text', required: true }],\n\t\thandler: async ({ input, req }) => {\n\t\t\tawait runPollClose({\n\t\t\t\tpayload: req.payload,\n\t\t\t\tinput: input as PollCloseTaskInput,\n\t\t\t\tpollVotesEnabled,\n\t\t\t\treq,\n\t\t\t})\n\t\t\treturn { output: {} }\n\t\t},\n\t}) as TaskConfig\n\n/** Register the poll-close task on `config.jobs.tasks`, creating the jobs config if absent. */\nexport const registerPollCloseTask = (config: Config, pollVotesEnabled = false): void => {\n\tconfig.jobs ??= {}\n\tconfig.jobs.tasks ??= []\n\tconfig.jobs.tasks.push(buildPollCloseTask(pollVotesEnabled))\n}\n\nconst canQueue = (payload: Payload): boolean => typeof payload.jobs?.queue === 'function'\n\n/**\n * Delete this form's pending (never-run) close jobs so a re-enqueue supersedes the prior one. Payload\n * 3.85's `jobs.queue` has no concurrency key, so this mirrors core's `deleteScheduledPublishJobs`:\n * a direct `db.deleteMany` on the jobs collection, scoped by task slug and the JSON `input.formId`.\n * Best-effort; a failure here only risks a duplicate future job, which the idempotent handler absorbs.\n */\nconst deletePendingCloseJobs = async (\n\tpayload: Payload,\n\tformId: number | string,\n\treq?: PayloadRequest\n): Promise<void> => {\n\ttry {\n\t\tawait payload.db.deleteMany({\n\t\t\tcollection: JOBS_SLUG,\n\t\t\treq,\n\t\t\twhere: {\n\t\t\t\tand: [\n\t\t\t\t\t{ completedAt: { exists: false } },\n\t\t\t\t\t{ processing: { equals: false } },\n\t\t\t\t\t{ taskSlug: { equals: POLL_CLOSE_TASK_SLUG } },\n\t\t\t\t\t{ 'input.formId': { equals: String(formId) } },\n\t\t\t\t],\n\t\t\t},\n\t\t})\n\t} catch (error) {\n\t\tpayload.logger?.error(\n\t\t\t`@10x-media/form-builder: failed to clear pending poll-close jobs for form ${String(formId)}: ${\n\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t}`\n\t\t)\n\t}\n}\n\n/**\n * Schedule (or reschedule) the auto-close for a poll after a save. Enqueues a `waitUntil: closesAt`\n * job when the poll is enabled, has a `closesAt`, is still unresolved, and its strategy is not\n * `manual`, and only when a job runner is present (`payload.jobs.queue`); otherwise the resolve-on-read\n * fallback is the safety net. Any prior pending close job for the form is deleted first so the newest\n * `closesAt` wins. Never throws; a queue failure is logged and swallowed.\n */\nexport const enqueuePollClose = async (args: {\n\tpayload: Payload\n\tform: { id: number | string; pollEnabled?: boolean | null; poll?: unknown }\n\treq?: PayloadRequest\n}): Promise<void> => {\n\tconst { payload, form, req } = args\n\tconst poll = pollConfigOf(form.poll)\n\tif (form.pollEnabled !== true || !poll) {\n\t\treturn\n\t}\n\tconst closesAt =\n\t\ttypeof poll.closesAt === 'string' && poll.closesAt.length > 0 ? poll.closesAt : undefined\n\tif (!closesAt || !hasEmptyWinners(poll) || strategyOf(poll) === 'manual' || !canQueue(payload)) {\n\t\treturn\n\t}\n\tconst waitUntil = new Date(closesAt)\n\tif (Number.isNaN(waitUntil.getTime())) {\n\t\treturn\n\t}\n\tawait deletePendingCloseJobs(payload, form.id, req)\n\ttry {\n\t\tawait payload.jobs.queue({\n\t\t\ttask: POLL_CLOSE_TASK_SLUG,\n\t\t\tinput: { formId: String(form.id) },\n\t\t\twaitUntil,\n\t\t\treq,\n\t\t})\n\t} catch (error) {\n\t\tpayload.logger?.error(\n\t\t\t`@10x-media/form-builder: failed to enqueue poll-close for form ${String(form.id)}: ${\n\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t}`\n\t\t)\n\t}\n}\n"],"mappings":";;;;AAKA,MAAa,uBAAuB;;AAGpC,MAAM,YAAY;AAQlB,MAAM,mBAAmB,SAAkC;CAC1D,MAAM,UAAU,KAAK,SAAS;CAC9B,OACC,CAAC,MAAM,QAAQ,OAAO,KACtB,QAAQ,QAAQ,UAAU,OAAO,UAAU,YAAY,MAAM,SAAS,CAAC,EAAE,WAAW;AAEtF;AAEA,MAAM,cAAc,SACnB,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,SAAS,IAAI,KAAK,OAAO;;;;;;AAOrE,MAAa,yBAAyB,SAAgC;CACrE,MAAM,OAAO,aAAa,KAAK,IAAI;CACnC,OACC,KAAK,gBAAgB,QACrB,QAAQ,QACR,aAAa,IAAI,KACjB,gBAAgB,IAAI,KACpB,WAAW,IAAI,MAAM;AAEvB;;;;;;AAOA,MAAa,eAAe,OAAO,SAMd;CACpB,MAAM,EAAE,SAAS,OAAO,kBAAkB,QAAQ;CAClD,MAAM,OAAO,MAAM,QACjB,SAAS;EAAE,YAAY;EAAY,IAAI,MAAM;EAAQ,OAAO;EAAG,gBAAgB;EAAM;CAAI,CAAC,EAC1F,YAAY,IAAI;CAClB,IAAI,CAAC,QAAQ,CAAC,sBAAsB,IAAoB,GACvD;CAED,MAAM,mBAAmB;EAAE;EAAS,QAAQ,MAAM;EAAQ;EAAkB;CAAI,CAAC;AAClF;;AAGA,MAAa,sBAAsB,mBAAmB,WACpD;CACA,MAAM;CACN,aAAa,CAAC;EAAE,MAAM;EAAU,MAAM;EAAQ,UAAU;CAAK,CAAC;CAC9D,SAAS,OAAO,EAAE,OAAO,UAAU;EAClC,MAAM,aAAa;GAClB,SAAS,IAAI;GACN;GACP;GACA;EACD,CAAC;EACD,OAAO,EAAE,QAAQ,CAAC,EAAE;CACrB;AACD;;AAGD,MAAa,yBAAyB,QAAgB,mBAAmB,UAAgB;CACxF,OAAO,SAAS,CAAC;CACjB,OAAO,KAAK,UAAU,CAAC;CACvB,OAAO,KAAK,MAAM,KAAK,mBAAmB,gBAAgB,CAAC;AAC5D;AAEA,MAAM,YAAY,YAA8B,OAAO,QAAQ,MAAM,UAAU;;;;;;;AAQ/E,MAAM,yBAAyB,OAC9B,SACA,QACA,QACmB;CACnB,IAAI;EACH,MAAM,QAAQ,GAAG,WAAW;GAC3B,YAAY;GACZ;GACA,OAAO,EACN,KAAK;IACJ,EAAE,aAAa,EAAE,QAAQ,MAAM,EAAE;IACjC,EAAE,YAAY,EAAE,QAAQ,MAAM,EAAE;IAChC,EAAE,UAAU,EAAE,QAAQ,qBAAqB,EAAE;IAC7C,EAAE,gBAAgB,EAAE,QAAQ,OAAO,MAAM,EAAE,EAAE;GAC9C,EACD;EACD,CAAC;CACF,SAAS,OAAO;EACf,QAAQ,QAAQ,MACf,6EAA6E,OAAO,MAAM,EAAE,IAC3F,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAEvD;CACD;AACD;;;;;;;;AASA,MAAa,mBAAmB,OAAO,SAIlB;CACpB,MAAM,EAAE,SAAS,MAAM,QAAQ;CAC/B,MAAM,OAAO,aAAa,KAAK,IAAI;CACnC,IAAI,KAAK,gBAAgB,QAAQ,CAAC,MACjC;CAED,MAAM,WACL,OAAO,KAAK,aAAa,YAAY,KAAK,SAAS,SAAS,IAAI,KAAK,WAAW,KAAA;CACjF,IAAI,CAAC,YAAY,CAAC,gBAAgB,IAAI,KAAK,WAAW,IAAI,MAAM,YAAY,CAAC,SAAS,OAAO,GAC5F;CAED,MAAM,YAAY,IAAI,KAAK,QAAQ;CACnC,IAAI,OAAO,MAAM,UAAU,QAAQ,CAAC,GACnC;CAED,MAAM,uBAAuB,SAAS,KAAK,IAAI,GAAG;CAClD,IAAI;EACH,MAAM,QAAQ,KAAK,MAAM;GACxB,MAAM;GACN,OAAO,EAAE,QAAQ,OAAO,KAAK,EAAE,EAAE;GACjC;GACA;EACD,CAAC;CACF,SAAS,OAAO;EACf,QAAQ,QAAQ,MACf,kEAAkE,OAAO,KAAK,EAAE,EAAE,IACjF,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAEvD;CACD;AACD"}
1
+ {"version":3,"file":"closeJob.js","names":[],"sources":["../../src/poll/closeJob.ts"],"sourcesContent":["import type { Config, Payload, PayloadRequest, TaskConfig } from 'payload'\nimport { isPollClosed, type PollConfigLike, pollConfigOf } from '../form/pollState'\nimport { pluginSlugsOf } from '../plugin/collectionSlugs'\nimport { resolvePollOutcome } from './resolvePollOutcome'\n\nexport const POLL_CLOSE_TASK_SLUG = 'form-builder-poll-close'\n\n/** Payload's jobs collection; queried directly to supersede a superseded pending close job. */\nconst JOBS_SLUG = 'payload-jobs'\n\n/** Input the enqueue path stores; the handler re-loads everything else from the DB. */\nexport type PollCloseTaskInput = { formId: number | string }\n\n/** The slice of a loaded form the close gate reads. */\nexport type PollFormLike = { pollEnabled?: boolean | null; poll?: unknown }\n\nconst hasEmptyWinners = (poll: PollConfigLike): boolean => {\n\tconst winners = poll.outcome?.winningValues\n\treturn (\n\t\t!Array.isArray(winners) ||\n\t\twinners.filter((value) => typeof value === 'string' && value.length > 0).length === 0\n\t)\n}\n\nconst strategyOf = (poll: PollConfigLike): string =>\n\ttypeof poll.type === 'string' && poll.type.length > 0 ? poll.type : 'manual'\n\n/**\n * True when a poll's outcome should auto-resolve now: enabled, closed, still unresolved, and its\n * strategy is not `manual`. Shared by the close-job handler and the results-read fallback so the two\n * triggers apply the exact same gate.\n */\nexport const shouldAutoResolvePoll = (form: PollFormLike): boolean => {\n\tconst poll = pollConfigOf(form.poll)\n\treturn (\n\t\tform.pollEnabled === true &&\n\t\tpoll != null &&\n\t\tisPollClosed(poll) &&\n\t\thasEmptyWinners(poll) &&\n\t\tstrategyOf(poll) !== 'manual'\n\t)\n}\n\n/**\n * Run the scheduled close for one form. Idempotent: re-loads the form and only resolves when the\n * poll is enabled, closed, still unresolved, and not `manual`, so a re-run after the outcome was\n * recorded (or a job that fires slightly early) is a safe no-op. Tolerates a deleted form.\n */\nexport const runPollClose = async (args: {\n\tpayload: Payload\n\tinput: PollCloseTaskInput\n\t/** Whether the hidden tally store backs the outcome aggregation (see `resolvePollOutcome`). */\n\tpollVotesEnabled?: boolean\n\treq?: PayloadRequest\n}): Promise<void> => {\n\tconst { payload, input, pollVotesEnabled, req } = args\n\tconst form = await payload\n\t\t.findByID({\n\t\t\tcollection: pluginSlugsOf(payload).forms,\n\t\t\tid: input.formId,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t})\n\t\t.catch(() => null)\n\tif (!form || !shouldAutoResolvePoll(form as PollFormLike)) {\n\t\treturn\n\t}\n\tawait resolvePollOutcome({ payload, formId: input.formId, pollVotesEnabled, req })\n}\n\n/** Native Payload jobs task that resolves a poll's outcome once it closes. */\nexport const buildPollCloseTask = (pollVotesEnabled = false): TaskConfig =>\n\t({\n\t\tslug: POLL_CLOSE_TASK_SLUG,\n\t\tinputSchema: [{ name: 'formId', type: 'text', required: true }],\n\t\thandler: async ({ input, req }) => {\n\t\t\tawait runPollClose({\n\t\t\t\tpayload: req.payload,\n\t\t\t\tinput: input as PollCloseTaskInput,\n\t\t\t\tpollVotesEnabled,\n\t\t\t\treq,\n\t\t\t})\n\t\t\treturn { output: {} }\n\t\t},\n\t}) as TaskConfig\n\n/** Register the poll-close task on `config.jobs.tasks`, creating the jobs config if absent. */\nexport const registerPollCloseTask = (config: Config, pollVotesEnabled = false): void => {\n\tconfig.jobs ??= {}\n\tconfig.jobs.tasks ??= []\n\tconfig.jobs.tasks.push(buildPollCloseTask(pollVotesEnabled))\n}\n\nconst canQueue = (payload: Payload): boolean => typeof payload.jobs?.queue === 'function'\n\n/**\n * Delete this form's pending (never-run) close jobs so a re-enqueue supersedes the prior one. Payload\n * 3.85's `jobs.queue` has no concurrency key, so this mirrors core's `deleteScheduledPublishJobs`:\n * a direct `db.deleteMany` on the jobs collection, scoped by task slug and the JSON `input.formId`.\n * Best-effort; a failure here only risks a duplicate future job, which the idempotent handler absorbs.\n */\nconst deletePendingCloseJobs = async (\n\tpayload: Payload,\n\tformId: number | string,\n\treq?: PayloadRequest\n): Promise<void> => {\n\ttry {\n\t\tawait payload.db.deleteMany({\n\t\t\tcollection: JOBS_SLUG,\n\t\t\treq,\n\t\t\twhere: {\n\t\t\t\tand: [\n\t\t\t\t\t{ completedAt: { exists: false } },\n\t\t\t\t\t{ processing: { equals: false } },\n\t\t\t\t\t{ taskSlug: { equals: POLL_CLOSE_TASK_SLUG } },\n\t\t\t\t\t{ 'input.formId': { equals: String(formId) } },\n\t\t\t\t],\n\t\t\t},\n\t\t})\n\t} catch (error) {\n\t\tpayload.logger?.error(\n\t\t\t`@10x-media/form-builder: failed to clear pending poll-close jobs for form ${String(formId)}: ${\n\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t}`\n\t\t)\n\t}\n}\n\n/**\n * Schedule (or reschedule) the auto-close for a poll after a save. Enqueues a `waitUntil: closesAt`\n * job when the poll is enabled, has a `closesAt`, is still unresolved, and its strategy is not\n * `manual`, and only when a job runner is present (`payload.jobs.queue`); otherwise the resolve-on-read\n * fallback is the safety net. Any prior pending close job for the form is deleted first so the newest\n * `closesAt` wins. Never throws; a queue failure is logged and swallowed.\n */\nexport const enqueuePollClose = async (args: {\n\tpayload: Payload\n\tform: { id: number | string; pollEnabled?: boolean | null; poll?: unknown }\n\treq?: PayloadRequest\n}): Promise<void> => {\n\tconst { payload, form, req } = args\n\tconst poll = pollConfigOf(form.poll)\n\tif (form.pollEnabled !== true || !poll) {\n\t\treturn\n\t}\n\tconst closesAt =\n\t\ttypeof poll.closesAt === 'string' && poll.closesAt.length > 0 ? poll.closesAt : undefined\n\tif (!closesAt || !hasEmptyWinners(poll) || strategyOf(poll) === 'manual' || !canQueue(payload)) {\n\t\treturn\n\t}\n\tconst waitUntil = new Date(closesAt)\n\tif (Number.isNaN(waitUntil.getTime())) {\n\t\treturn\n\t}\n\tawait deletePendingCloseJobs(payload, form.id, req)\n\ttry {\n\t\tawait payload.jobs.queue({\n\t\t\ttask: POLL_CLOSE_TASK_SLUG,\n\t\t\tinput: { formId: String(form.id) },\n\t\t\twaitUntil,\n\t\t\treq,\n\t\t})\n\t} catch (error) {\n\t\tpayload.logger?.error(\n\t\t\t`@10x-media/form-builder: failed to enqueue poll-close for form ${String(form.id)}: ${\n\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t}`\n\t\t)\n\t}\n}\n"],"mappings":";;;;AAKA,MAAa,uBAAuB;;AAGpC,MAAM,YAAY;AAQlB,MAAM,mBAAmB,SAAkC;CAC1D,MAAM,UAAU,KAAK,SAAS;CAC9B,OACC,CAAC,MAAM,QAAQ,OAAO,KACtB,QAAQ,QAAQ,UAAU,OAAO,UAAU,YAAY,MAAM,SAAS,CAAC,EAAE,WAAW;AAEtF;AAEA,MAAM,cAAc,SACnB,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,SAAS,IAAI,KAAK,OAAO;;;;;;AAOrE,MAAa,yBAAyB,SAAgC;CACrE,MAAM,OAAO,aAAa,KAAK,IAAI;CACnC,OACC,KAAK,gBAAgB,QACrB,QAAQ,QACR,aAAa,IAAI,KACjB,gBAAgB,IAAI,KACpB,WAAW,IAAI,MAAM;AAEvB;;;;;;AAOA,MAAa,eAAe,OAAO,SAMd;CACpB,MAAM,EAAE,SAAS,OAAO,kBAAkB,QAAQ;CAClD,MAAM,OAAO,MAAM,QACjB,SAAS;EACT,YAAY,cAAc,OAAO,EAAE;EACnC,IAAI,MAAM;EACV,OAAO;EACP,gBAAgB;EAChB;CACD,CAAC,EACA,YAAY,IAAI;CAClB,IAAI,CAAC,QAAQ,CAAC,sBAAsB,IAAoB,GACvD;CAED,MAAM,mBAAmB;EAAE;EAAS,QAAQ,MAAM;EAAQ;EAAkB;CAAI,CAAC;AAClF;;AAGA,MAAa,sBAAsB,mBAAmB,WACpD;CACA,MAAM;CACN,aAAa,CAAC;EAAE,MAAM;EAAU,MAAM;EAAQ,UAAU;CAAK,CAAC;CAC9D,SAAS,OAAO,EAAE,OAAO,UAAU;EAClC,MAAM,aAAa;GAClB,SAAS,IAAI;GACN;GACP;GACA;EACD,CAAC;EACD,OAAO,EAAE,QAAQ,CAAC,EAAE;CACrB;AACD;;AAGD,MAAa,yBAAyB,QAAgB,mBAAmB,UAAgB;CACxF,OAAO,SAAS,CAAC;CACjB,OAAO,KAAK,UAAU,CAAC;CACvB,OAAO,KAAK,MAAM,KAAK,mBAAmB,gBAAgB,CAAC;AAC5D;AAEA,MAAM,YAAY,YAA8B,OAAO,QAAQ,MAAM,UAAU;;;;;;;AAQ/E,MAAM,yBAAyB,OAC9B,SACA,QACA,QACmB;CACnB,IAAI;EACH,MAAM,QAAQ,GAAG,WAAW;GAC3B,YAAY;GACZ;GACA,OAAO,EACN,KAAK;IACJ,EAAE,aAAa,EAAE,QAAQ,MAAM,EAAE;IACjC,EAAE,YAAY,EAAE,QAAQ,MAAM,EAAE;IAChC,EAAE,UAAU,EAAE,QAAQ,qBAAqB,EAAE;IAC7C,EAAE,gBAAgB,EAAE,QAAQ,OAAO,MAAM,EAAE,EAAE;GAC9C,EACD;EACD,CAAC;CACF,SAAS,OAAO;EACf,QAAQ,QAAQ,MACf,6EAA6E,OAAO,MAAM,EAAE,IAC3F,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAEvD;CACD;AACD;;;;;;;;AASA,MAAa,mBAAmB,OAAO,SAIlB;CACpB,MAAM,EAAE,SAAS,MAAM,QAAQ;CAC/B,MAAM,OAAO,aAAa,KAAK,IAAI;CACnC,IAAI,KAAK,gBAAgB,QAAQ,CAAC,MACjC;CAED,MAAM,WACL,OAAO,KAAK,aAAa,YAAY,KAAK,SAAS,SAAS,IAAI,KAAK,WAAW,KAAA;CACjF,IAAI,CAAC,YAAY,CAAC,gBAAgB,IAAI,KAAK,WAAW,IAAI,MAAM,YAAY,CAAC,SAAS,OAAO,GAC5F;CAED,MAAM,YAAY,IAAI,KAAK,QAAQ;CACnC,IAAI,OAAO,MAAM,UAAU,QAAQ,CAAC,GACnC;CAED,MAAM,uBAAuB,SAAS,KAAK,IAAI,GAAG;CAClD,IAAI;EACH,MAAM,QAAQ,KAAK,MAAM;GACxB,MAAM;GACN,OAAO,EAAE,QAAQ,OAAO,KAAK,EAAE,EAAE;GACjC;GACA;EACD,CAAC;CACF,SAAS,OAAO;EACf,QAAQ,QAAQ,MACf,kEAAkE,OAAO,KAAK,EAAE,EAAE,IACjF,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAEvD;CACD;AACD"}
@@ -1,8 +1,8 @@
1
1
  import { keys } from "../translations/keys.js";
2
2
  import { asTranslate } from "../translations/server.js";
3
+ import { pluginSlugsOf } from "../plugin/collectionSlugs.js";
3
4
  import { pollConfigOf } from "../form/pollState.js";
4
5
  import { resolveEffectivePollOptions } from "./effectivePollOptions.js";
5
- import { FORMS_SLUG } from "../collections/forms.js";
6
6
  import { ValidationError } from "payload";
7
7
  //#region src/poll/outcomeBeforeChange.ts
8
8
  const WINNING_VALUES_PATH = "poll.outcome.winningValues";
@@ -46,7 +46,7 @@ const pollOutcomeBeforeChange = async ({ data, originalDoc, req }) => {
46
46
  const t = asTranslate(req.t);
47
47
  const fail = (message) => {
48
48
  throw new ValidationError({
49
- collection: FORMS_SLUG,
49
+ collection: pluginSlugsOf(req.payload).forms,
50
50
  errors: [{
51
51
  path: WINNING_VALUES_PATH,
52
52
  message
@@ -1 +1 @@
1
- {"version":3,"file":"outcomeBeforeChange.js","names":[],"sources":["../../src/poll/outcomeBeforeChange.ts"],"sourcesContent":["import { type CollectionBeforeChangeHook, ValidationError } from 'payload'\nimport { FORMS_SLUG } from '../collections/forms'\nimport { pollConfigOf } from '../form/pollState'\nimport { keys } from '../translations/keys'\nimport { asTranslate } from '../translations/server'\nimport type { PollOption } from './definePollOptionSource'\nimport { resolveEffectivePollOptions } from './effectivePollOptions'\n\nconst WINNING_VALUES_PATH = 'poll.outcome.winningValues'\n\n/** The winning set as a deduped array of non-empty strings, ready to store and to compare. */\nconst normalizeWinning = (value: unknown): string[] => {\n\tif (!Array.isArray(value)) {\n\t\treturn []\n\t}\n\tconst seen = new Set<string>()\n\tfor (const entry of value) {\n\t\tif (typeof entry === 'string' && entry.length > 0) {\n\t\t\tseen.add(entry)\n\t\t}\n\t}\n\treturn [...seen]\n}\n\n/** Set equality on two already-deduped winner arrays: order-insensitive membership. */\nconst sameWinners = (a: string[], b: string[]): boolean => {\n\tif (a.length !== b.length) {\n\t\treturn false\n\t}\n\tconst set = new Set(a)\n\treturn b.every((value) => set.has(value))\n}\n\n/**\n * Owns the poll outcome lifecycle for every create and update, admin and API alike. When the save\n * writes `poll.outcome.winningValues` it validates a set or changed winner set against the poll's\n * effective options (source-resolved or authored, over a merged view of the doc so partial updates\n * validate against stored config) and requires the poll to be enabled; clearing is always allowed.\n * A tie is a set with more than one winner; every value must be a member of the effective options.\n *\n * It is also the single `resolvedAt` stamping point: stamped when the winner set is set or changed,\n * cleared with the set, untouched when the set is unchanged (order-insensitive). Callers can never\n * write `resolvedAt` themselves (field-level access strips it during the beforeValidate traversal,\n * before this hook runs), while values this hook sets survive to the database, so the stamp always\n * reflects the actual transition regardless of the write path.\n */\nexport const pollOutcomeBeforeChange: CollectionBeforeChangeHook = async ({\n\tdata,\n\toriginalDoc,\n\treq,\n}) => {\n\tconst incomingPoll =\n\t\tdata?.poll != null && typeof data.poll === 'object'\n\t\t\t? (data.poll as Record<string, unknown>)\n\t\t\t: undefined\n\tconst outcome =\n\t\tincomingPoll?.outcome != null && typeof incomingPoll.outcome === 'object'\n\t\t\t? (incomingPoll.outcome as Record<string, unknown>)\n\t\t\t: undefined\n\tif (!outcome || !('winningValues' in outcome)) {\n\t\treturn data\n\t}\n\tconst next = normalizeWinning(outcome.winningValues)\n\toutcome.winningValues = next\n\tconst previous = normalizeWinning(pollConfigOf(originalDoc?.poll)?.outcome?.winningValues)\n\tif (sameWinners(next, previous)) {\n\t\treturn data\n\t}\n\tif (next.length === 0) {\n\t\toutcome.resolvedAt = null\n\t\treturn data\n\t}\n\n\tconst t = asTranslate(req.t)\n\tconst fail = (message: string): never => {\n\t\tthrow new ValidationError(\n\t\t\t{ collection: FORMS_SLUG, errors: [{ path: WINNING_VALUES_PATH, message }] },\n\t\t\treq.t\n\t\t)\n\t}\n\tconst originalPoll =\n\t\toriginalDoc?.poll != null && typeof originalDoc.poll === 'object'\n\t\t\t? (originalDoc.poll as Record<string, unknown>)\n\t\t\t: undefined\n\tconst mergedPoll = { ...(originalPoll ?? {}), ...incomingPoll }\n\t// The poll-enable flag lives at the document root now; merge the incoming partial over the stored\n\t// doc so a partial update that omits it still validates against the persisted value.\n\tconst mergedPollEnabled = { ...(originalDoc ?? {}), ...data }.pollEnabled === true\n\tif (!mergedPollEnabled) {\n\t\tfail(t(keys.validationWinningValueDisabled))\n\t}\n\tconst id = (originalDoc?.id ?? data?.id) as number | string | undefined\n\tconst form = {\n\t\tid: id ?? '',\n\t\ttitle:\n\t\t\ttypeof data?.title === 'string' ? data.title : (originalDoc?.title as string | undefined),\n\t\tpollEnabled: mergedPollEnabled,\n\t\tpoll: mergedPoll,\n\t\tfields: data?.fields ?? originalDoc?.fields,\n\t}\n\tlet options: PollOption[]\n\ttry {\n\t\t// Without a doc id (create) the per-request options cache would key on '', so skip it.\n\t\toptions = await resolveEffectivePollOptions({\n\t\t\tpayload: req.payload,\n\t\t\treq: id != null ? req : undefined,\n\t\t\tform,\n\t\t})\n\t} catch {\n\t\treturn fail(t(keys.pollOptionsUnavailable))\n\t}\n\tconst allowed = new Set(options.map((option) => option.value))\n\tif (!next.every((value) => allowed.has(value))) {\n\t\tfail(t(keys.validationWinningValueUnknown))\n\t}\n\toutcome.resolvedAt = new Date().toISOString()\n\treturn data\n}\n"],"mappings":";;;;;;;AAQA,MAAM,sBAAsB;;AAG5B,MAAM,oBAAoB,UAA6B;CACtD,IAAI,CAAC,MAAM,QAAQ,KAAK,GACvB,OAAO,CAAC;CAET,MAAM,uBAAO,IAAI,IAAY;CAC7B,KAAK,MAAM,SAAS,OACnB,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAC/C,KAAK,IAAI,KAAK;CAGhB,OAAO,CAAC,GAAG,IAAI;AAChB;;AAGA,MAAM,eAAe,GAAa,MAAyB;CAC1D,IAAI,EAAE,WAAW,EAAE,QAClB,OAAO;CAER,MAAM,MAAM,IAAI,IAAI,CAAC;CACrB,OAAO,EAAE,OAAO,UAAU,IAAI,IAAI,KAAK,CAAC;AACzC;;;;;;;;;;;;;;AAeA,MAAa,0BAAsD,OAAO,EACzE,MACA,aACA,UACK;CACL,MAAM,eACL,MAAM,QAAQ,QAAQ,OAAO,KAAK,SAAS,WACvC,KAAK,OACN,KAAA;CACJ,MAAM,UACL,cAAc,WAAW,QAAQ,OAAO,aAAa,YAAY,WAC7D,aAAa,UACd,KAAA;CACJ,IAAI,CAAC,WAAW,EAAE,mBAAmB,UACpC,OAAO;CAER,MAAM,OAAO,iBAAiB,QAAQ,aAAa;CACnD,QAAQ,gBAAgB;CAExB,IAAI,YAAY,MADC,iBAAiB,aAAa,aAAa,IAAI,GAAG,SAAS,aAC/C,CAAC,GAC7B,OAAO;CAER,IAAI,KAAK,WAAW,GAAG;EACtB,QAAQ,aAAa;EACrB,OAAO;CACR;CAEA,MAAM,IAAI,YAAY,IAAI,CAAC;CAC3B,MAAM,QAAQ,YAA2B;EACxC,MAAM,IAAI,gBACT;GAAE,YAAY;GAAY,QAAQ,CAAC;IAAE,MAAM;IAAqB;GAAQ,CAAC;EAAE,GAC3E,IAAI,CACL;CACD;CAKA,MAAM,aAAa;EAAE,IAHpB,aAAa,QAAQ,QAAQ,OAAO,YAAY,SAAS,WACrD,YAAY,OACb,KAAA,MACqC,CAAC;EAAI,GAAG;CAAa;CAG9D,MAAM,oBAAoB;EAAE,GAAI,eAAe,CAAC;EAAI,GAAG;CAAK,EAAE,gBAAgB;CAC9E,IAAI,CAAC,mBACJ,KAAK,EAAE,KAAK,8BAA8B,CAAC;CAE5C,MAAM,KAAM,aAAa,MAAM,MAAM;CACrC,MAAM,OAAO;EACZ,IAAI,MAAM;EACV,OACC,OAAO,MAAM,UAAU,WAAW,KAAK,QAAS,aAAa;EAC9D,aAAa;EACb,MAAM;EACN,QAAQ,MAAM,UAAU,aAAa;CACtC;CACA,IAAI;CACJ,IAAI;EAEH,UAAU,MAAM,4BAA4B;GAC3C,SAAS,IAAI;GACb,KAAK,MAAM,OAAO,MAAM,KAAA;GACxB;EACD,CAAC;CACF,QAAQ;EACP,OAAO,KAAK,EAAE,KAAK,sBAAsB,CAAC;CAC3C;CACA,MAAM,UAAU,IAAI,IAAI,QAAQ,KAAK,WAAW,OAAO,KAAK,CAAC;CAC7D,IAAI,CAAC,KAAK,OAAO,UAAU,QAAQ,IAAI,KAAK,CAAC,GAC5C,KAAK,EAAE,KAAK,6BAA6B,CAAC;CAE3C,QAAQ,8BAAa,IAAI,KAAK,GAAE,YAAY;CAC5C,OAAO;AACR"}
1
+ {"version":3,"file":"outcomeBeforeChange.js","names":[],"sources":["../../src/poll/outcomeBeforeChange.ts"],"sourcesContent":["import { type CollectionBeforeChangeHook, ValidationError } from 'payload'\nimport { pollConfigOf } from '../form/pollState'\nimport { pluginSlugsOf } from '../plugin/collectionSlugs'\nimport { keys } from '../translations/keys'\nimport { asTranslate } from '../translations/server'\nimport type { PollOption } from './definePollOptionSource'\nimport { resolveEffectivePollOptions } from './effectivePollOptions'\n\nconst WINNING_VALUES_PATH = 'poll.outcome.winningValues'\n\n/** The winning set as a deduped array of non-empty strings, ready to store and to compare. */\nconst normalizeWinning = (value: unknown): string[] => {\n\tif (!Array.isArray(value)) {\n\t\treturn []\n\t}\n\tconst seen = new Set<string>()\n\tfor (const entry of value) {\n\t\tif (typeof entry === 'string' && entry.length > 0) {\n\t\t\tseen.add(entry)\n\t\t}\n\t}\n\treturn [...seen]\n}\n\n/** Set equality on two already-deduped winner arrays: order-insensitive membership. */\nconst sameWinners = (a: string[], b: string[]): boolean => {\n\tif (a.length !== b.length) {\n\t\treturn false\n\t}\n\tconst set = new Set(a)\n\treturn b.every((value) => set.has(value))\n}\n\n/**\n * Owns the poll outcome lifecycle for every create and update, admin and API alike. When the save\n * writes `poll.outcome.winningValues` it validates a set or changed winner set against the poll's\n * effective options (source-resolved or authored, over a merged view of the doc so partial updates\n * validate against stored config) and requires the poll to be enabled; clearing is always allowed.\n * A tie is a set with more than one winner; every value must be a member of the effective options.\n *\n * It is also the single `resolvedAt` stamping point: stamped when the winner set is set or changed,\n * cleared with the set, untouched when the set is unchanged (order-insensitive). Callers can never\n * write `resolvedAt` themselves (field-level access strips it during the beforeValidate traversal,\n * before this hook runs), while values this hook sets survive to the database, so the stamp always\n * reflects the actual transition regardless of the write path.\n */\nexport const pollOutcomeBeforeChange: CollectionBeforeChangeHook = async ({\n\tdata,\n\toriginalDoc,\n\treq,\n}) => {\n\tconst incomingPoll =\n\t\tdata?.poll != null && typeof data.poll === 'object'\n\t\t\t? (data.poll as Record<string, unknown>)\n\t\t\t: undefined\n\tconst outcome =\n\t\tincomingPoll?.outcome != null && typeof incomingPoll.outcome === 'object'\n\t\t\t? (incomingPoll.outcome as Record<string, unknown>)\n\t\t\t: undefined\n\tif (!outcome || !('winningValues' in outcome)) {\n\t\treturn data\n\t}\n\tconst next = normalizeWinning(outcome.winningValues)\n\toutcome.winningValues = next\n\tconst previous = normalizeWinning(pollConfigOf(originalDoc?.poll)?.outcome?.winningValues)\n\tif (sameWinners(next, previous)) {\n\t\treturn data\n\t}\n\tif (next.length === 0) {\n\t\toutcome.resolvedAt = null\n\t\treturn data\n\t}\n\n\tconst t = asTranslate(req.t)\n\tconst fail = (message: string): never => {\n\t\tthrow new ValidationError(\n\t\t\t{\n\t\t\t\tcollection: pluginSlugsOf(req.payload).forms,\n\t\t\t\terrors: [{ path: WINNING_VALUES_PATH, message }],\n\t\t\t},\n\t\t\treq.t\n\t\t)\n\t}\n\tconst originalPoll =\n\t\toriginalDoc?.poll != null && typeof originalDoc.poll === 'object'\n\t\t\t? (originalDoc.poll as Record<string, unknown>)\n\t\t\t: undefined\n\tconst mergedPoll = { ...(originalPoll ?? {}), ...incomingPoll }\n\t// The poll-enable flag lives at the document root now; merge the incoming partial over the stored\n\t// doc so a partial update that omits it still validates against the persisted value.\n\tconst mergedPollEnabled = { ...(originalDoc ?? {}), ...data }.pollEnabled === true\n\tif (!mergedPollEnabled) {\n\t\tfail(t(keys.validationWinningValueDisabled))\n\t}\n\tconst id = (originalDoc?.id ?? data?.id) as number | string | undefined\n\tconst form = {\n\t\tid: id ?? '',\n\t\ttitle:\n\t\t\ttypeof data?.title === 'string' ? data.title : (originalDoc?.title as string | undefined),\n\t\tpollEnabled: mergedPollEnabled,\n\t\tpoll: mergedPoll,\n\t\tfields: data?.fields ?? originalDoc?.fields,\n\t}\n\tlet options: PollOption[]\n\ttry {\n\t\t// Without a doc id (create) the per-request options cache would key on '', so skip it.\n\t\toptions = await resolveEffectivePollOptions({\n\t\t\tpayload: req.payload,\n\t\t\treq: id != null ? req : undefined,\n\t\t\tform,\n\t\t})\n\t} catch {\n\t\treturn fail(t(keys.pollOptionsUnavailable))\n\t}\n\tconst allowed = new Set(options.map((option) => option.value))\n\tif (!next.every((value) => allowed.has(value))) {\n\t\tfail(t(keys.validationWinningValueUnknown))\n\t}\n\toutcome.resolvedAt = new Date().toISOString()\n\treturn data\n}\n"],"mappings":";;;;;;;AAQA,MAAM,sBAAsB;;AAG5B,MAAM,oBAAoB,UAA6B;CACtD,IAAI,CAAC,MAAM,QAAQ,KAAK,GACvB,OAAO,CAAC;CAET,MAAM,uBAAO,IAAI,IAAY;CAC7B,KAAK,MAAM,SAAS,OACnB,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAC/C,KAAK,IAAI,KAAK;CAGhB,OAAO,CAAC,GAAG,IAAI;AAChB;;AAGA,MAAM,eAAe,GAAa,MAAyB;CAC1D,IAAI,EAAE,WAAW,EAAE,QAClB,OAAO;CAER,MAAM,MAAM,IAAI,IAAI,CAAC;CACrB,OAAO,EAAE,OAAO,UAAU,IAAI,IAAI,KAAK,CAAC;AACzC;;;;;;;;;;;;;;AAeA,MAAa,0BAAsD,OAAO,EACzE,MACA,aACA,UACK;CACL,MAAM,eACL,MAAM,QAAQ,QAAQ,OAAO,KAAK,SAAS,WACvC,KAAK,OACN,KAAA;CACJ,MAAM,UACL,cAAc,WAAW,QAAQ,OAAO,aAAa,YAAY,WAC7D,aAAa,UACd,KAAA;CACJ,IAAI,CAAC,WAAW,EAAE,mBAAmB,UACpC,OAAO;CAER,MAAM,OAAO,iBAAiB,QAAQ,aAAa;CACnD,QAAQ,gBAAgB;CAExB,IAAI,YAAY,MADC,iBAAiB,aAAa,aAAa,IAAI,GAAG,SAAS,aAC/C,CAAC,GAC7B,OAAO;CAER,IAAI,KAAK,WAAW,GAAG;EACtB,QAAQ,aAAa;EACrB,OAAO;CACR;CAEA,MAAM,IAAI,YAAY,IAAI,CAAC;CAC3B,MAAM,QAAQ,YAA2B;EACxC,MAAM,IAAI,gBACT;GACC,YAAY,cAAc,IAAI,OAAO,EAAE;GACvC,QAAQ,CAAC;IAAE,MAAM;IAAqB;GAAQ,CAAC;EAChD,GACA,IAAI,CACL;CACD;CAKA,MAAM,aAAa;EAAE,IAHpB,aAAa,QAAQ,QAAQ,OAAO,YAAY,SAAS,WACrD,YAAY,OACb,KAAA,MACqC,CAAC;EAAI,GAAG;CAAa;CAG9D,MAAM,oBAAoB;EAAE,GAAI,eAAe,CAAC;EAAI,GAAG;CAAK,EAAE,gBAAgB;CAC9E,IAAI,CAAC,mBACJ,KAAK,EAAE,KAAK,8BAA8B,CAAC;CAE5C,MAAM,KAAM,aAAa,MAAM,MAAM;CACrC,MAAM,OAAO;EACZ,IAAI,MAAM;EACV,OACC,OAAO,MAAM,UAAU,WAAW,KAAK,QAAS,aAAa;EAC9D,aAAa;EACb,MAAM;EACN,QAAQ,MAAM,UAAU,aAAa;CACtC;CACA,IAAI;CACJ,IAAI;EAEH,UAAU,MAAM,4BAA4B;GAC3C,SAAS,IAAI;GACb,KAAK,MAAM,OAAO,MAAM,KAAA;GACxB;EACD,CAAC;CACF,QAAQ;EACP,OAAO,KAAK,EAAE,KAAK,sBAAsB,CAAC;CAC3C;CACA,MAAM,UAAU,IAAI,IAAI,QAAQ,KAAK,WAAW,OAAO,KAAK,CAAC;CAC7D,IAAI,CAAC,KAAK,OAAO,UAAU,QAAQ,IAAI,KAAK,CAAC,GAC5C,KAAK,EAAE,KAAK,6BAA6B,CAAC;CAE3C,QAAQ,8BAAa,IAAI,KAAK,GAAE,YAAY;CAC5C,OAAO;AACR"}
@@ -1,8 +1,8 @@
1
1
  import { keys } from "../translations/keys.js";
2
2
  import { asTranslate } from "../translations/server.js";
3
+ import { pluginSlugsOf } from "../plugin/collectionSlugs.js";
3
4
  import { isPollClosed, pollConfigOf } from "../form/pollState.js";
4
5
  import { resolvePollOutcome } from "./resolvePollOutcome.js";
5
- import { FORMS_SLUG } from "../collections/forms.js";
6
6
  //#region src/poll/resolvePollCloseRequest.ts
7
7
  /** The recorded winning set as non-empty strings; empty means no winner has been set yet. */
8
8
  const winnersOf = (poll) => {
@@ -57,7 +57,7 @@ const resolvePollCloseRequest = async (args) => {
57
57
  body: { errors: [{ message: "Missing form id" }] }
58
58
  };
59
59
  const form = await payload.findByID({
60
- collection: FORMS_SLUG,
60
+ collection: pluginSlugsOf(payload).forms,
61
61
  id: formId,
62
62
  depth: 0,
63
63
  overrideAccess: true,
@@ -80,7 +80,7 @@ const resolvePollCloseRequest = async (args) => {
80
80
  }
81
81
  const closesAt = (/* @__PURE__ */ new Date()).toISOString();
82
82
  await payload.update({
83
- collection: FORMS_SLUG,
83
+ collection: pluginSlugsOf(payload).forms,
84
84
  id: formId,
85
85
  data: { poll: { closesAt } },
86
86
  depth: 0,
@@ -1 +1 @@
1
- {"version":3,"file":"resolvePollCloseRequest.js","names":[],"sources":["../../src/poll/resolvePollCloseRequest.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { FORMS_SLUG } from '../collections/forms'\nimport { isPollClosed, type PollConfigLike, pollConfigOf } from '../form/pollState'\nimport { keys } from '../translations/keys'\nimport { asTranslate } from '../translations/server'\nimport { resolvePollOutcome } from './resolvePollOutcome'\n\n/** The recorded winning set as non-empty strings; empty means no winner has been set yet. */\nconst winnersOf = (poll: PollConfigLike): string[] => {\n\tconst winners = poll.outcome?.winningValues\n\tif (!Array.isArray(winners)) {\n\t\treturn []\n\t}\n\treturn winners.filter((value): value is string => typeof value === 'string' && value.length > 0)\n}\n\n/** The selected outcome strategy slug; an absent or empty `type` is `manual`. */\nconst strategyOf = (poll: PollConfigLike): string =>\n\ttypeof poll.type === 'string' && poll.type.length > 0 ? poll.type : 'manual'\n\n/** The slice of a loaded form the close planner reads. */\nexport type PollCloseFormLike = { pollEnabled?: boolean | null; poll?: unknown }\n\nexport type PollClosePlan =\n\t| { ok: false; reason: 'already-closed' | 'manual-no-winner' | 'not-poll' }\n\t| { ok: true; resolveOutcome: boolean }\n\n/**\n * Decide what closing a poll now should do, purely from a loaded form so the branch logic is testable\n * without a boot. `manual` requires a winner to already be recorded (closing only stamps the time);\n * `mostVoted`, `source`, and any host strategy auto-resolve on close. Refuses a non-poll form and an\n * already-closed poll (idempotency), so the request helper performs no write unless this returns `ok`.\n */\nexport const planPollClose = (form: PollCloseFormLike): PollClosePlan => {\n\tconst poll = pollConfigOf(form.poll)\n\tif (form.pollEnabled !== true || !poll) {\n\t\treturn { ok: false, reason: 'not-poll' }\n\t}\n\tif (isPollClosed(poll)) {\n\t\treturn { ok: false, reason: 'already-closed' }\n\t}\n\tif (strategyOf(poll) === 'manual' && winnersOf(poll).length === 0) {\n\t\treturn { ok: false, reason: 'manual-no-winner' }\n\t}\n\treturn { ok: true, resolveOutcome: strategyOf(poll) !== 'manual' }\n}\n\nexport type ResolvePollCloseRequestArgs = {\n\tpayload: Payload\n\tformId: number | string | undefined\n\t/** Whether the caller is authenticated (an admin/user). */\n\tisAuthed: boolean\n\t/** Whether the hidden tally store backs the outcome aggregation (see `resolvePollOutcome`). */\n\tpollVotesEnabled?: boolean\n\treq: PayloadRequest\n}\n\nexport type ResolvePollCloseRequestResult = {\n\tstatus: number\n\tbody: { closesAt: string; winningValues: string[] } | { errors: { message: string }[] }\n}\n\n/**\n * Authorize and perform a manual poll close backing `POST /:id/close`. Only an authenticated caller\n * may close (anonymous is always 403, mirroring the other resolve* helpers); the route is a trusted\n * admin action, so the load and write run with `overrideAccess`. Sets `closesAt` to now through\n * `payload.update` so the collection hooks run, then for a non-manual strategy resolves the winners via\n * `resolvePollOutcome` (auto mode; a non-decidable poll simply records none and the read fallback heals\n * it later). A `manual` poll with no winner recorded is refused (400) so closing never strands a poll\n * with no result. Idempotent: an already-closed poll returns 400 rather than re-closing.\n */\nexport const resolvePollCloseRequest = async (\n\targs: ResolvePollCloseRequestArgs\n): Promise<ResolvePollCloseRequestResult> => {\n\tconst { payload, formId, isAuthed, pollVotesEnabled, req } = args\n\tif (!isAuthed) {\n\t\treturn { status: 403, body: { errors: [{ message: 'Forbidden' }] } }\n\t}\n\tif (formId == null) {\n\t\treturn { status: 400, body: { errors: [{ message: 'Missing form id' }] } }\n\t}\n\tconst form = await payload\n\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, overrideAccess: true, req })\n\t\t.catch(() => null)\n\tif (!form) {\n\t\treturn { status: 404, body: { errors: [{ message: 'Not found' }] } }\n\t}\n\n\tconst plan = planPollClose(form as PollCloseFormLike)\n\tif (!plan.ok) {\n\t\tif (plan.reason === 'manual-no-winner') {\n\t\t\treturn {\n\t\t\t\tstatus: 400,\n\t\t\t\tbody: { errors: [{ message: asTranslate(req.t)(keys.pollCloseManualNoWinner) }] },\n\t\t\t}\n\t\t}\n\t\tconst message = plan.reason === 'already-closed' ? 'Poll already closed' : 'Not a poll'\n\t\treturn { status: 400, body: { errors: [{ message }] } }\n\t}\n\n\tconst closesAt = new Date().toISOString()\n\tawait payload.update({\n\t\tcollection: FORMS_SLUG,\n\t\tid: formId,\n\t\tdata: { poll: { closesAt } },\n\t\tdepth: 0,\n\t\toverrideAccess: true,\n\t\treq,\n\t})\n\n\tconst winningValues = plan.resolveOutcome\n\t\t? await resolvePollOutcome({ payload, formId, pollVotesEnabled, req })\n\t\t: winnersOf(pollConfigOf(form.poll) ?? {})\n\treturn { status: 200, body: { closesAt, winningValues } }\n}\n"],"mappings":";;;;;;;AAQA,MAAM,aAAa,SAAmC;CACrD,MAAM,UAAU,KAAK,SAAS;CAC9B,IAAI,CAAC,MAAM,QAAQ,OAAO,GACzB,OAAO,CAAC;CAET,OAAO,QAAQ,QAAQ,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS,CAAC;AAChG;;AAGA,MAAM,cAAc,SACnB,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,SAAS,IAAI,KAAK,OAAO;;;;;;;AAerE,MAAa,iBAAiB,SAA2C;CACxE,MAAM,OAAO,aAAa,KAAK,IAAI;CACnC,IAAI,KAAK,gBAAgB,QAAQ,CAAC,MACjC,OAAO;EAAE,IAAI;EAAO,QAAQ;CAAW;CAExC,IAAI,aAAa,IAAI,GACpB,OAAO;EAAE,IAAI;EAAO,QAAQ;CAAiB;CAE9C,IAAI,WAAW,IAAI,MAAM,YAAY,UAAU,IAAI,EAAE,WAAW,GAC/D,OAAO;EAAE,IAAI;EAAO,QAAQ;CAAmB;CAEhD,OAAO;EAAE,IAAI;EAAM,gBAAgB,WAAW,IAAI,MAAM;CAAS;AAClE;;;;;;;;;;AA0BA,MAAa,0BAA0B,OACtC,SAC4C;CAC5C,MAAM,EAAE,SAAS,QAAQ,UAAU,kBAAkB,QAAQ;CAC7D,IAAI,CAAC,UACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAEpE,IAAI,UAAU,MACb,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,kBAAkB,CAAC,EAAE;CAAE;CAE1E,MAAM,OAAO,MAAM,QACjB,SAAS;EAAE,YAAY;EAAY,IAAI;EAAQ,OAAO;EAAG,gBAAgB;EAAM;CAAI,CAAC,EACpF,YAAY,IAAI;CAClB,IAAI,CAAC,MACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAGpE,MAAM,OAAO,cAAc,IAAyB;CACpD,IAAI,CAAC,KAAK,IAAI;EACb,IAAI,KAAK,WAAW,oBACnB,OAAO;GACN,QAAQ;GACR,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,IAAI,CAAC,EAAE,KAAK,uBAAuB,EAAE,CAAC,EAAE;EACjF;EAGD,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SADzB,KAAK,WAAW,mBAAmB,wBAAwB,aAC1B,CAAC,EAAE;EAAE;CACvD;CAEA,MAAM,4BAAW,IAAI,KAAK,GAAE,YAAY;CACxC,MAAM,QAAQ,OAAO;EACpB,YAAY;EACZ,IAAI;EACJ,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE;EAC3B,OAAO;EACP,gBAAgB;EAChB;CACD,CAAC;CAKD,OAAO;EAAE,QAAQ;EAAK,MAAM;GAAE;GAAU,eAHlB,KAAK,iBACxB,MAAM,mBAAmB;IAAE;IAAS;IAAQ;IAAkB;GAAI,CAAC,IACnE,UAAU,aAAa,KAAK,IAAI,KAAK,CAAC,CAAC;EACY;CAAE;AACzD"}
1
+ {"version":3,"file":"resolvePollCloseRequest.js","names":[],"sources":["../../src/poll/resolvePollCloseRequest.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { isPollClosed, type PollConfigLike, pollConfigOf } from '../form/pollState'\nimport { pluginSlugsOf } from '../plugin/collectionSlugs'\nimport { keys } from '../translations/keys'\nimport { asTranslate } from '../translations/server'\nimport { resolvePollOutcome } from './resolvePollOutcome'\n\n/** The recorded winning set as non-empty strings; empty means no winner has been set yet. */\nconst winnersOf = (poll: PollConfigLike): string[] => {\n\tconst winners = poll.outcome?.winningValues\n\tif (!Array.isArray(winners)) {\n\t\treturn []\n\t}\n\treturn winners.filter((value): value is string => typeof value === 'string' && value.length > 0)\n}\n\n/** The selected outcome strategy slug; an absent or empty `type` is `manual`. */\nconst strategyOf = (poll: PollConfigLike): string =>\n\ttypeof poll.type === 'string' && poll.type.length > 0 ? poll.type : 'manual'\n\n/** The slice of a loaded form the close planner reads. */\nexport type PollCloseFormLike = { pollEnabled?: boolean | null; poll?: unknown }\n\nexport type PollClosePlan =\n\t| { ok: false; reason: 'already-closed' | 'manual-no-winner' | 'not-poll' }\n\t| { ok: true; resolveOutcome: boolean }\n\n/**\n * Decide what closing a poll now should do, purely from a loaded form so the branch logic is testable\n * without a boot. `manual` requires a winner to already be recorded (closing only stamps the time);\n * `mostVoted`, `source`, and any host strategy auto-resolve on close. Refuses a non-poll form and an\n * already-closed poll (idempotency), so the request helper performs no write unless this returns `ok`.\n */\nexport const planPollClose = (form: PollCloseFormLike): PollClosePlan => {\n\tconst poll = pollConfigOf(form.poll)\n\tif (form.pollEnabled !== true || !poll) {\n\t\treturn { ok: false, reason: 'not-poll' }\n\t}\n\tif (isPollClosed(poll)) {\n\t\treturn { ok: false, reason: 'already-closed' }\n\t}\n\tif (strategyOf(poll) === 'manual' && winnersOf(poll).length === 0) {\n\t\treturn { ok: false, reason: 'manual-no-winner' }\n\t}\n\treturn { ok: true, resolveOutcome: strategyOf(poll) !== 'manual' }\n}\n\nexport type ResolvePollCloseRequestArgs = {\n\tpayload: Payload\n\tformId: number | string | undefined\n\t/** Whether the caller is authenticated (an admin/user). */\n\tisAuthed: boolean\n\t/** Whether the hidden tally store backs the outcome aggregation (see `resolvePollOutcome`). */\n\tpollVotesEnabled?: boolean\n\treq: PayloadRequest\n}\n\nexport type ResolvePollCloseRequestResult = {\n\tstatus: number\n\tbody: { closesAt: string; winningValues: string[] } | { errors: { message: string }[] }\n}\n\n/**\n * Authorize and perform a manual poll close backing `POST /:id/close`. Only an authenticated caller\n * may close (anonymous is always 403, mirroring the other resolve* helpers); the route is a trusted\n * admin action, so the load and write run with `overrideAccess`. Sets `closesAt` to now through\n * `payload.update` so the collection hooks run, then for a non-manual strategy resolves the winners via\n * `resolvePollOutcome` (auto mode; a non-decidable poll simply records none and the read fallback heals\n * it later). A `manual` poll with no winner recorded is refused (400) so closing never strands a poll\n * with no result. Idempotent: an already-closed poll returns 400 rather than re-closing.\n */\nexport const resolvePollCloseRequest = async (\n\targs: ResolvePollCloseRequestArgs\n): Promise<ResolvePollCloseRequestResult> => {\n\tconst { payload, formId, isAuthed, pollVotesEnabled, req } = args\n\tif (!isAuthed) {\n\t\treturn { status: 403, body: { errors: [{ message: 'Forbidden' }] } }\n\t}\n\tif (formId == null) {\n\t\treturn { status: 400, body: { errors: [{ message: 'Missing form id' }] } }\n\t}\n\tconst form = await payload\n\t\t.findByID({\n\t\t\tcollection: pluginSlugsOf(payload).forms,\n\t\t\tid: formId,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t})\n\t\t.catch(() => null)\n\tif (!form) {\n\t\treturn { status: 404, body: { errors: [{ message: 'Not found' }] } }\n\t}\n\n\tconst plan = planPollClose(form as PollCloseFormLike)\n\tif (!plan.ok) {\n\t\tif (plan.reason === 'manual-no-winner') {\n\t\t\treturn {\n\t\t\t\tstatus: 400,\n\t\t\t\tbody: { errors: [{ message: asTranslate(req.t)(keys.pollCloseManualNoWinner) }] },\n\t\t\t}\n\t\t}\n\t\tconst message = plan.reason === 'already-closed' ? 'Poll already closed' : 'Not a poll'\n\t\treturn { status: 400, body: { errors: [{ message }] } }\n\t}\n\n\tconst closesAt = new Date().toISOString()\n\tawait payload.update({\n\t\tcollection: pluginSlugsOf(payload).forms,\n\t\tid: formId,\n\t\tdata: { poll: { closesAt } },\n\t\tdepth: 0,\n\t\toverrideAccess: true,\n\t\treq,\n\t})\n\n\tconst winningValues = plan.resolveOutcome\n\t\t? await resolvePollOutcome({ payload, formId, pollVotesEnabled, req })\n\t\t: winnersOf(pollConfigOf(form.poll) ?? {})\n\treturn { status: 200, body: { closesAt, winningValues } }\n}\n"],"mappings":";;;;;;;AAQA,MAAM,aAAa,SAAmC;CACrD,MAAM,UAAU,KAAK,SAAS;CAC9B,IAAI,CAAC,MAAM,QAAQ,OAAO,GACzB,OAAO,CAAC;CAET,OAAO,QAAQ,QAAQ,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS,CAAC;AAChG;;AAGA,MAAM,cAAc,SACnB,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,SAAS,IAAI,KAAK,OAAO;;;;;;;AAerE,MAAa,iBAAiB,SAA2C;CACxE,MAAM,OAAO,aAAa,KAAK,IAAI;CACnC,IAAI,KAAK,gBAAgB,QAAQ,CAAC,MACjC,OAAO;EAAE,IAAI;EAAO,QAAQ;CAAW;CAExC,IAAI,aAAa,IAAI,GACpB,OAAO;EAAE,IAAI;EAAO,QAAQ;CAAiB;CAE9C,IAAI,WAAW,IAAI,MAAM,YAAY,UAAU,IAAI,EAAE,WAAW,GAC/D,OAAO;EAAE,IAAI;EAAO,QAAQ;CAAmB;CAEhD,OAAO;EAAE,IAAI;EAAM,gBAAgB,WAAW,IAAI,MAAM;CAAS;AAClE;;;;;;;;;;AA0BA,MAAa,0BAA0B,OACtC,SAC4C;CAC5C,MAAM,EAAE,SAAS,QAAQ,UAAU,kBAAkB,QAAQ;CAC7D,IAAI,CAAC,UACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAEpE,IAAI,UAAU,MACb,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,kBAAkB,CAAC,EAAE;CAAE;CAE1E,MAAM,OAAO,MAAM,QACjB,SAAS;EACT,YAAY,cAAc,OAAO,EAAE;EACnC,IAAI;EACJ,OAAO;EACP,gBAAgB;EAChB;CACD,CAAC,EACA,YAAY,IAAI;CAClB,IAAI,CAAC,MACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAGpE,MAAM,OAAO,cAAc,IAAyB;CACpD,IAAI,CAAC,KAAK,IAAI;EACb,IAAI,KAAK,WAAW,oBACnB,OAAO;GACN,QAAQ;GACR,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,IAAI,CAAC,EAAE,KAAK,uBAAuB,EAAE,CAAC,EAAE;EACjF;EAGD,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SADzB,KAAK,WAAW,mBAAmB,wBAAwB,aAC1B,CAAC,EAAE;EAAE;CACvD;CAEA,MAAM,4BAAW,IAAI,KAAK,GAAE,YAAY;CACxC,MAAM,QAAQ,OAAO;EACpB,YAAY,cAAc,OAAO,EAAE;EACnC,IAAI;EACJ,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE;EAC3B,OAAO;EACP,gBAAgB;EAChB;CACD,CAAC;CAKD,OAAO;EAAE,QAAQ;EAAK,MAAM;GAAE;GAAU,eAHlB,KAAK,iBACxB,MAAM,mBAAmB;IAAE;IAAS;IAAQ;IAAkB;GAAI,CAAC,IACnE,UAAU,aAAa,KAAK,IAAI,KAAK,CAAC,CAAC;EACY;CAAE;AACzD"}
@@ -1,5 +1,5 @@
1
+ import { pluginSlugsOf } from "../plugin/collectionSlugs.js";
1
2
  import { resolveEffectivePollOptions } from "./effectivePollOptions.js";
2
- import { FORMS_SLUG } from "../collections/forms.js";
3
3
  //#region src/poll/resolvePollOptionsRequest.ts
4
4
  /**
5
5
  * Authorize and resolve the `GET /:id/poll-options` request backing the admin winner select:
@@ -19,7 +19,7 @@ const resolvePollOptionsRequest = async (args) => {
19
19
  body: { errors: [{ message: "Missing form id" }] }
20
20
  };
21
21
  const form = await payload.findByID({
22
- collection: FORMS_SLUG,
22
+ collection: pluginSlugsOf(payload).forms,
23
23
  id: formId,
24
24
  depth: 0,
25
25
  req
@@ -1 +1 @@
1
- {"version":3,"file":"resolvePollOptionsRequest.js","names":[],"sources":["../../src/poll/resolvePollOptionsRequest.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { FORMS_SLUG } from '../collections/forms'\nimport type { PollOption } from './definePollOptionSource'\nimport { resolveEffectivePollOptions } from './effectivePollOptions'\n\nexport type ResolvePollOptionsRequestArgs = {\n\tpayload: Payload\n\tformId: number | string | undefined\n\t/** Whether the caller is authenticated (an admin/user). */\n\tisAuthed: boolean\n\treq?: PayloadRequest\n}\n\nexport type ResolvePollOptionsRequestResult = {\n\tstatus: number\n\tbody: { options: PollOption[] } | { errors: { message: string }[] }\n}\n\n/**\n * Authorize and resolve the `GET /:id/poll-options` request backing the admin winner select:\n * authenticated callers get the poll's effective options (source-resolved or authored, exactly the\n * set outcome validation accepts), anonymous callers are always refused. Statuses mirror the\n * results endpoint: 400 missing id, 403 unauthenticated, 404 unknown form, 503 when the option\n * source fails (fail closed).\n */\nexport const resolvePollOptionsRequest = async (\n\targs: ResolvePollOptionsRequestArgs\n): Promise<ResolvePollOptionsRequestResult> => {\n\tconst { payload, formId, isAuthed, req } = args\n\tif (!isAuthed) {\n\t\treturn { status: 403, body: { errors: [{ message: 'Forbidden' }] } }\n\t}\n\tif (formId == null) {\n\t\treturn { status: 400, body: { errors: [{ message: 'Missing form id' }] } }\n\t}\n\t// Load under the caller's own read access (no overrideAccess): a tenant-scoped `forms.access.read`\n\t// then refuses a cross-tenant form id (null -> 404), so an authed user cannot read another tenant's\n\t// poll options.\n\tconst form = await payload\n\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, req })\n\t\t.catch(() => null)\n\tif (!form) {\n\t\treturn { status: 404, body: { errors: [{ message: 'Not found' }] } }\n\t}\n\ttry {\n\t\tconst options = await resolveEffectivePollOptions({ payload, req, form })\n\t\treturn { status: 200, body: { options } }\n\t} catch {\n\t\treturn { status: 503, body: { errors: [{ message: 'Poll options unavailable' }] } }\n\t}\n}\n"],"mappings":";;;;;;;;;;AAyBA,MAAa,4BAA4B,OACxC,SAC8C;CAC9C,MAAM,EAAE,SAAS,QAAQ,UAAU,QAAQ;CAC3C,IAAI,CAAC,UACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAEpE,IAAI,UAAU,MACb,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,kBAAkB,CAAC,EAAE;CAAE;CAK1E,MAAM,OAAO,MAAM,QACjB,SAAS;EAAE,YAAY;EAAY,IAAI;EAAQ,OAAO;EAAG;CAAI,CAAC,EAC9D,YAAY,IAAI;CAClB,IAAI,CAAC,MACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAEpE,IAAI;EAEH,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,SAAA,MADR,4BAA4B;IAAE;IAAS;IAAK;GAAK,CAAC,EAClC;EAAE;CACzC,QAAQ;EACP,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,2BAA2B,CAAC,EAAE;EAAE;CACnF;AACD"}
1
+ {"version":3,"file":"resolvePollOptionsRequest.js","names":[],"sources":["../../src/poll/resolvePollOptionsRequest.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { pluginSlugsOf } from '../plugin/collectionSlugs'\nimport type { PollOption } from './definePollOptionSource'\nimport { resolveEffectivePollOptions } from './effectivePollOptions'\n\nexport type ResolvePollOptionsRequestArgs = {\n\tpayload: Payload\n\tformId: number | string | undefined\n\t/** Whether the caller is authenticated (an admin/user). */\n\tisAuthed: boolean\n\treq?: PayloadRequest\n}\n\nexport type ResolvePollOptionsRequestResult = {\n\tstatus: number\n\tbody: { options: PollOption[] } | { errors: { message: string }[] }\n}\n\n/**\n * Authorize and resolve the `GET /:id/poll-options` request backing the admin winner select:\n * authenticated callers get the poll's effective options (source-resolved or authored, exactly the\n * set outcome validation accepts), anonymous callers are always refused. Statuses mirror the\n * results endpoint: 400 missing id, 403 unauthenticated, 404 unknown form, 503 when the option\n * source fails (fail closed).\n */\nexport const resolvePollOptionsRequest = async (\n\targs: ResolvePollOptionsRequestArgs\n): Promise<ResolvePollOptionsRequestResult> => {\n\tconst { payload, formId, isAuthed, req } = args\n\tif (!isAuthed) {\n\t\treturn { status: 403, body: { errors: [{ message: 'Forbidden' }] } }\n\t}\n\tif (formId == null) {\n\t\treturn { status: 400, body: { errors: [{ message: 'Missing form id' }] } }\n\t}\n\t// Load under the caller's own read access (no overrideAccess): a tenant-scoped `forms.access.read`\n\t// then refuses a cross-tenant form id (null -> 404), so an authed user cannot read another tenant's\n\t// poll options.\n\tconst form = await payload\n\t\t.findByID({ collection: pluginSlugsOf(payload).forms, id: formId, depth: 0, req })\n\t\t.catch(() => null)\n\tif (!form) {\n\t\treturn { status: 404, body: { errors: [{ message: 'Not found' }] } }\n\t}\n\ttry {\n\t\tconst options = await resolveEffectivePollOptions({ payload, req, form })\n\t\treturn { status: 200, body: { options } }\n\t} catch {\n\t\treturn { status: 503, body: { errors: [{ message: 'Poll options unavailable' }] } }\n\t}\n}\n"],"mappings":";;;;;;;;;;AAyBA,MAAa,4BAA4B,OACxC,SAC8C;CAC9C,MAAM,EAAE,SAAS,QAAQ,UAAU,QAAQ;CAC3C,IAAI,CAAC,UACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAEpE,IAAI,UAAU,MACb,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,kBAAkB,CAAC,EAAE;CAAE;CAK1E,MAAM,OAAO,MAAM,QACjB,SAAS;EAAE,YAAY,cAAc,OAAO,EAAE;EAAO,IAAI;EAAQ,OAAO;EAAG;CAAI,CAAC,EAChF,YAAY,IAAI;CAClB,IAAI,CAAC,MACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAEpE,IAAI;EAEH,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,SAAA,MADR,4BAA4B;IAAE;IAAS;IAAK;GAAK,CAAC,EAClC;EAAE;CACzC,QAAQ;EACP,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,2BAA2B,CAAC,EAAE;EAAE;CACnF;AACD"}
@@ -1,9 +1,9 @@
1
+ import { pluginSlugsOf } from "../plugin/collectionSlugs.js";
1
2
  import { pollConfigOf } from "../form/pollState.js";
2
- import { aggregateFieldResponses } from "../aggregation/aggregateResponses.js";
3
3
  import { resolveEffectivePollOptions } from "./effectivePollOptions.js";
4
+ import { aggregateFieldResponses } from "../aggregation/aggregateResponses.js";
4
5
  import { pollTypesOf } from "./pollTypeRegistry.js";
5
6
  import { aggregateFromVotes } from "./votes/aggregateFromVotes.js";
6
- import { FORMS_SLUG } from "../collections/forms.js";
7
7
  //#region src/poll/resolvePollOutcome.ts
8
8
  /** A strategy result narrowed to a deduped set of non-empty strings. */
9
9
  const cleanWinners = (value) => value === void 0 ? [] : [...new Set(value.filter((entry) => typeof entry === "string" && entry.length > 0))];
@@ -26,7 +26,7 @@ const cleanWinners = (value) => value === void 0 ? [] : [...new Set(value.filter
26
26
  const resolvePollOutcome = async (args) => {
27
27
  const { payload, formId, pollVotesEnabled, req } = args;
28
28
  const form = await payload.findByID({
29
- collection: FORMS_SLUG,
29
+ collection: pluginSlugsOf(payload).forms,
30
30
  id: formId,
31
31
  depth: 0,
32
32
  overrideAccess: true,
@@ -75,7 +75,7 @@ const resolvePollOutcome = async (args) => {
75
75
  if (winningValues.length === 0) return [];
76
76
  }
77
77
  await payload.update({
78
- collection: FORMS_SLUG,
78
+ collection: pluginSlugsOf(payload).forms,
79
79
  id: formId,
80
80
  data: { poll: { outcome: { winningValues } } },
81
81
  depth: 0,
@@ -1 +1 @@
1
- {"version":3,"file":"resolvePollOutcome.js","names":[],"sources":["../../src/poll/resolvePollOutcome.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { aggregateFieldResponses } from '../aggregation/aggregateResponses'\nimport type { FieldAggregation } from '../aggregation/types'\nimport { FORMS_SLUG } from '../collections/forms'\nimport { pollConfigOf } from '../form/pollState'\nimport type { PollOutcomeStrategyArgs } from './definePollType'\nimport { resolveEffectivePollOptions } from './effectivePollOptions'\nimport { pollTypesOf } from './pollTypeRegistry'\nimport { aggregateFromVotes } from './votes/aggregateFromVotes'\n\nexport type ResolvePollOutcomeArgs = {\n\tpayload: Payload\n\tformId: number | string\n\t/**\n\t * The winning set: stable option values, each matching one of the poll's choice values (the\n\t * host's contract with `PollOption.value`). Pass more than one for a tie, an empty array to clear\n\t * the outcome. Omit to auto-resolve via the poll's selected outcome strategy (`poll.type`).\n\t */\n\twinningValues?: string[]\n\t/**\n\t * Whether the hidden tally store backs the auto-resolve aggregation (`mostVoted` and any host\n\t * strategy that calls `aggregate()`). False keeps the submission scan, so persist-off forms\n\t * only resolve correctly with the store enabled.\n\t */\n\tpollVotesEnabled?: boolean\n\treq?: PayloadRequest\n}\n\n/** A strategy result narrowed to a deduped set of non-empty strings. */\nconst cleanWinners = (value: string[] | undefined): string[] =>\n\tvalue === undefined\n\t\t? []\n\t\t: [\n\t\t\t\t...new Set(\n\t\t\t\t\tvalue.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)\n\t\t\t\t),\n\t\t\t]\n\n/**\n * Record a poll's final outcome server-side, in one of two modes:\n * - explicit: pass `winningValues` when host code already knows the winner(s); more than one is a\n * tie, and an empty array clears the outcome;\n * - auto: omit it and the poll's selected outcome strategy (`poll.type`, from the `poll.types`\n * registry, defaulting to `manual`) resolves the winners. `manual` is a no-op; `mostVoted` reads\n * the aggregated results field; `source` delegates to the option source. A strategy that is not\n * yet decidable (or `manual`) writes nothing and this returns an empty set, so auto mode is safe\n * to call repeatedly from the close job and the read fallback.\n *\n * Every write runs through `payload.update` with `overrideAccess`, so the forms collection hook\n * validates each value against the poll's effective options and owns the `poll.outcome.resolvedAt`\n * stamp in both modes (auto-resolved winners are validated exactly as hand-picked ones). Throws only\n * when the form does not exist or is not poll-enabled. Returns the recorded winning set (empty when\n * auto mode did not decide).\n */\nexport const resolvePollOutcome = async (args: ResolvePollOutcomeArgs): Promise<string[]> => {\n\tconst { payload, formId, pollVotesEnabled, req } = args\n\tconst form = await payload.findByID({\n\t\tcollection: FORMS_SLUG,\n\t\tid: formId,\n\t\tdepth: 0,\n\t\toverrideAccess: true,\n\t\treq,\n\t})\n\tconst poll = pollConfigOf(form.poll)\n\tif (form.pollEnabled !== true || !poll) {\n\t\tthrow new Error(`Form ${String(formId)} is not poll-enabled; cannot record an outcome.`)\n\t}\n\n\tlet winningValues: string[]\n\tif (args.winningValues !== undefined) {\n\t\twinningValues = args.winningValues\n\t} else {\n\t\tconst strategyType =\n\t\t\ttypeof poll.type === 'string' && poll.type.length > 0 ? poll.type : 'manual'\n\t\tconst strategy = pollTypesOf(payload).get(strategyType)\n\t\tif (!strategy) {\n\t\t\treturn []\n\t\t}\n\t\tlet aggregated: FieldAggregation | null | undefined\n\t\tconst aggregate = async (): Promise<FieldAggregation | null> => {\n\t\t\tif (aggregated === undefined) {\n\t\t\t\tconst resultsField =\n\t\t\t\t\ttypeof poll.resultsField === 'string' && poll.resultsField.length > 0\n\t\t\t\t\t\t? poll.resultsField\n\t\t\t\t\t\t: undefined\n\t\t\t\tif (!resultsField) {\n\t\t\t\t\taggregated = null\n\t\t\t\t} else if (pollVotesEnabled === true) {\n\t\t\t\t\t// Best-effort options: a source/resolver failure degrades to raw tally values\n\t\t\t\t\t// (the outcome hook still validates winners against the effective options).\n\t\t\t\t\tconst options = await resolveEffectivePollOptions({ payload, req, form }).catch(() => [])\n\t\t\t\t\taggregated = await aggregateFromVotes({\n\t\t\t\t\t\tpayload,\n\t\t\t\t\t\tformId,\n\t\t\t\t\t\tfield: resultsField,\n\t\t\t\t\t\toptions,\n\t\t\t\t\t\treq,\n\t\t\t\t\t})\n\t\t\t\t} else {\n\t\t\t\t\taggregated = await aggregateFieldResponses({ payload, formId, field: resultsField, req })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn aggregated\n\t\t}\n\t\twinningValues = cleanWinners(\n\t\t\tawait strategy.resolveOutcome({\n\t\t\t\tpayload,\n\t\t\t\treq,\n\t\t\t\tform: form as unknown as PollOutcomeStrategyArgs['form'],\n\t\t\t\tpoll,\n\t\t\t\taggregate,\n\t\t\t})\n\t\t)\n\t\tif (winningValues.length === 0) {\n\t\t\treturn []\n\t\t}\n\t}\n\n\tawait payload.update({\n\t\tcollection: FORMS_SLUG,\n\t\tid: formId,\n\t\tdata: { poll: { outcome: { winningValues } } },\n\t\tdepth: 0,\n\t\toverrideAccess: true,\n\t\treq,\n\t})\n\treturn winningValues\n}\n"],"mappings":";;;;;;;;AA6BA,MAAM,gBAAgB,UACrB,UAAU,KAAA,IACP,CAAC,IACD,CACA,GAAG,IAAI,IACN,MAAM,QAAQ,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS,CAAC,CACvF,CACD;;;;;;;;;;;;;;;;;AAkBH,MAAa,qBAAqB,OAAO,SAAoD;CAC5F,MAAM,EAAE,SAAS,QAAQ,kBAAkB,QAAQ;CACnD,MAAM,OAAO,MAAM,QAAQ,SAAS;EACnC,YAAY;EACZ,IAAI;EACJ,OAAO;EACP,gBAAgB;EAChB;CACD,CAAC;CACD,MAAM,OAAO,aAAa,KAAK,IAAI;CACnC,IAAI,KAAK,gBAAgB,QAAQ,CAAC,MACjC,MAAM,IAAI,MAAM,QAAQ,OAAO,MAAM,EAAE,gDAAgD;CAGxF,IAAI;CACJ,IAAI,KAAK,kBAAkB,KAAA,GAC1B,gBAAgB,KAAK;MACf;EACN,MAAM,eACL,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,SAAS,IAAI,KAAK,OAAO;EACrE,MAAM,WAAW,YAAY,OAAO,EAAE,IAAI,YAAY;EACtD,IAAI,CAAC,UACJ,OAAO,CAAC;EAET,IAAI;EACJ,MAAM,YAAY,YAA8C;GAC/D,IAAI,eAAe,KAAA,GAAW;IAC7B,MAAM,eACL,OAAO,KAAK,iBAAiB,YAAY,KAAK,aAAa,SAAS,IACjE,KAAK,eACL,KAAA;IACJ,IAAI,CAAC,cACJ,aAAa;SACP,IAAI,qBAAqB,MAI/B,aAAa,MAAM,mBAAmB;KACrC;KACA;KACA,OAAO;KACP,SAAA,MALqB,4BAA4B;MAAE;MAAS;MAAK;KAAK,CAAC,EAAE,YAAY,CAAC,CAAC;KAMvF;IACD,CAAC;SAED,aAAa,MAAM,wBAAwB;KAAE;KAAS;KAAQ,OAAO;KAAc;IAAI,CAAC;GAE1F;GACA,OAAO;EACR;EACA,gBAAgB,aACf,MAAM,SAAS,eAAe;GAC7B;GACA;GACM;GACN;GACA;EACD,CAAC,CACF;EACA,IAAI,cAAc,WAAW,GAC5B,OAAO,CAAC;CAEV;CAEA,MAAM,QAAQ,OAAO;EACpB,YAAY;EACZ,IAAI;EACJ,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,EAAE;EAC7C,OAAO;EACP,gBAAgB;EAChB;CACD,CAAC;CACD,OAAO;AACR"}
1
+ {"version":3,"file":"resolvePollOutcome.js","names":[],"sources":["../../src/poll/resolvePollOutcome.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { aggregateFieldResponses } from '../aggregation/aggregateResponses'\nimport type { FieldAggregation } from '../aggregation/types'\nimport { pollConfigOf } from '../form/pollState'\nimport { pluginSlugsOf } from '../plugin/collectionSlugs'\nimport type { PollOutcomeStrategyArgs } from './definePollType'\nimport { resolveEffectivePollOptions } from './effectivePollOptions'\nimport { pollTypesOf } from './pollTypeRegistry'\nimport { aggregateFromVotes } from './votes/aggregateFromVotes'\n\nexport type ResolvePollOutcomeArgs = {\n\tpayload: Payload\n\tformId: number | string\n\t/**\n\t * The winning set: stable option values, each matching one of the poll's choice values (the\n\t * host's contract with `PollOption.value`). Pass more than one for a tie, an empty array to clear\n\t * the outcome. Omit to auto-resolve via the poll's selected outcome strategy (`poll.type`).\n\t */\n\twinningValues?: string[]\n\t/**\n\t * Whether the hidden tally store backs the auto-resolve aggregation (`mostVoted` and any host\n\t * strategy that calls `aggregate()`). False keeps the submission scan, so persist-off forms\n\t * only resolve correctly with the store enabled.\n\t */\n\tpollVotesEnabled?: boolean\n\treq?: PayloadRequest\n}\n\n/** A strategy result narrowed to a deduped set of non-empty strings. */\nconst cleanWinners = (value: string[] | undefined): string[] =>\n\tvalue === undefined\n\t\t? []\n\t\t: [\n\t\t\t\t...new Set(\n\t\t\t\t\tvalue.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)\n\t\t\t\t),\n\t\t\t]\n\n/**\n * Record a poll's final outcome server-side, in one of two modes:\n * - explicit: pass `winningValues` when host code already knows the winner(s); more than one is a\n * tie, and an empty array clears the outcome;\n * - auto: omit it and the poll's selected outcome strategy (`poll.type`, from the `poll.types`\n * registry, defaulting to `manual`) resolves the winners. `manual` is a no-op; `mostVoted` reads\n * the aggregated results field; `source` delegates to the option source. A strategy that is not\n * yet decidable (or `manual`) writes nothing and this returns an empty set, so auto mode is safe\n * to call repeatedly from the close job and the read fallback.\n *\n * Every write runs through `payload.update` with `overrideAccess`, so the forms collection hook\n * validates each value against the poll's effective options and owns the `poll.outcome.resolvedAt`\n * stamp in both modes (auto-resolved winners are validated exactly as hand-picked ones). Throws only\n * when the form does not exist or is not poll-enabled. Returns the recorded winning set (empty when\n * auto mode did not decide).\n */\nexport const resolvePollOutcome = async (args: ResolvePollOutcomeArgs): Promise<string[]> => {\n\tconst { payload, formId, pollVotesEnabled, req } = args\n\tconst form = await payload.findByID({\n\t\tcollection: pluginSlugsOf(payload).forms,\n\t\tid: formId,\n\t\tdepth: 0,\n\t\toverrideAccess: true,\n\t\treq,\n\t})\n\tconst poll = pollConfigOf(form.poll)\n\tif (form.pollEnabled !== true || !poll) {\n\t\tthrow new Error(`Form ${String(formId)} is not poll-enabled; cannot record an outcome.`)\n\t}\n\n\tlet winningValues: string[]\n\tif (args.winningValues !== undefined) {\n\t\twinningValues = args.winningValues\n\t} else {\n\t\tconst strategyType =\n\t\t\ttypeof poll.type === 'string' && poll.type.length > 0 ? poll.type : 'manual'\n\t\tconst strategy = pollTypesOf(payload).get(strategyType)\n\t\tif (!strategy) {\n\t\t\treturn []\n\t\t}\n\t\tlet aggregated: FieldAggregation | null | undefined\n\t\tconst aggregate = async (): Promise<FieldAggregation | null> => {\n\t\t\tif (aggregated === undefined) {\n\t\t\t\tconst resultsField =\n\t\t\t\t\ttypeof poll.resultsField === 'string' && poll.resultsField.length > 0\n\t\t\t\t\t\t? poll.resultsField\n\t\t\t\t\t\t: undefined\n\t\t\t\tif (!resultsField) {\n\t\t\t\t\taggregated = null\n\t\t\t\t} else if (pollVotesEnabled === true) {\n\t\t\t\t\t// Best-effort options: a source/resolver failure degrades to raw tally values\n\t\t\t\t\t// (the outcome hook still validates winners against the effective options).\n\t\t\t\t\tconst options = await resolveEffectivePollOptions({ payload, req, form }).catch(() => [])\n\t\t\t\t\taggregated = await aggregateFromVotes({\n\t\t\t\t\t\tpayload,\n\t\t\t\t\t\tformId,\n\t\t\t\t\t\tfield: resultsField,\n\t\t\t\t\t\toptions,\n\t\t\t\t\t\treq,\n\t\t\t\t\t})\n\t\t\t\t} else {\n\t\t\t\t\taggregated = await aggregateFieldResponses({ payload, formId, field: resultsField, req })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn aggregated\n\t\t}\n\t\twinningValues = cleanWinners(\n\t\t\tawait strategy.resolveOutcome({\n\t\t\t\tpayload,\n\t\t\t\treq,\n\t\t\t\tform: form as unknown as PollOutcomeStrategyArgs['form'],\n\t\t\t\tpoll,\n\t\t\t\taggregate,\n\t\t\t})\n\t\t)\n\t\tif (winningValues.length === 0) {\n\t\t\treturn []\n\t\t}\n\t}\n\n\tawait payload.update({\n\t\tcollection: pluginSlugsOf(payload).forms,\n\t\tid: formId,\n\t\tdata: { poll: { outcome: { winningValues } } },\n\t\tdepth: 0,\n\t\toverrideAccess: true,\n\t\treq,\n\t})\n\treturn winningValues\n}\n"],"mappings":";;;;;;;;AA6BA,MAAM,gBAAgB,UACrB,UAAU,KAAA,IACP,CAAC,IACD,CACA,GAAG,IAAI,IACN,MAAM,QAAQ,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS,CAAC,CACvF,CACD;;;;;;;;;;;;;;;;;AAkBH,MAAa,qBAAqB,OAAO,SAAoD;CAC5F,MAAM,EAAE,SAAS,QAAQ,kBAAkB,QAAQ;CACnD,MAAM,OAAO,MAAM,QAAQ,SAAS;EACnC,YAAY,cAAc,OAAO,EAAE;EACnC,IAAI;EACJ,OAAO;EACP,gBAAgB;EAChB;CACD,CAAC;CACD,MAAM,OAAO,aAAa,KAAK,IAAI;CACnC,IAAI,KAAK,gBAAgB,QAAQ,CAAC,MACjC,MAAM,IAAI,MAAM,QAAQ,OAAO,MAAM,EAAE,gDAAgD;CAGxF,IAAI;CACJ,IAAI,KAAK,kBAAkB,KAAA,GAC1B,gBAAgB,KAAK;MACf;EACN,MAAM,eACL,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,SAAS,IAAI,KAAK,OAAO;EACrE,MAAM,WAAW,YAAY,OAAO,EAAE,IAAI,YAAY;EACtD,IAAI,CAAC,UACJ,OAAO,CAAC;EAET,IAAI;EACJ,MAAM,YAAY,YAA8C;GAC/D,IAAI,eAAe,KAAA,GAAW;IAC7B,MAAM,eACL,OAAO,KAAK,iBAAiB,YAAY,KAAK,aAAa,SAAS,IACjE,KAAK,eACL,KAAA;IACJ,IAAI,CAAC,cACJ,aAAa;SACP,IAAI,qBAAqB,MAI/B,aAAa,MAAM,mBAAmB;KACrC;KACA;KACA,OAAO;KACP,SAAA,MALqB,4BAA4B;MAAE;MAAS;MAAK;KAAK,CAAC,EAAE,YAAY,CAAC,CAAC;KAMvF;IACD,CAAC;SAED,aAAa,MAAM,wBAAwB;KAAE;KAAS;KAAQ,OAAO;KAAc;IAAI,CAAC;GAE1F;GACA,OAAO;EACR;EACA,gBAAgB,aACf,MAAM,SAAS,eAAe;GAC7B;GACA;GACM;GACN;GACA;EACD,CAAC,CACF;EACA,IAAI,cAAc,WAAW,GAC5B,OAAO,CAAC;CAEV;CAEA,MAAM,QAAQ,OAAO;EACpB,YAAY,cAAc,OAAO,EAAE;EACnC,IAAI;EACJ,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,EAAE;EAC7C,OAAO;EACP,gBAAgB;EAChB;CACD,CAAC;CACD,OAAO;AACR"}
@@ -1,4 +1,5 @@
1
- import { POLL_VOTES_SLUG } from "./votesCollection.js";
1
+ import { pluginSlugsOf } from "../../plugin/collectionSlugs.js";
2
+ import "./votesCollection.js";
2
3
  //#region src/poll/votes/aggregateFromVotes.ts
3
4
  const round1 = (n) => Math.round(n * 10) / 10;
4
5
  /**
@@ -10,7 +11,7 @@ const round1 = (n) => Math.round(n * 10) / 10;
10
11
  const aggregateFromVotes = async (args) => {
11
12
  const { payload, formId, field, meta, options, req } = args;
12
13
  const { docs } = await payload.find({
13
- collection: POLL_VOTES_SLUG,
14
+ collection: pluginSlugsOf(payload).pollVotes,
14
15
  where: { and: [{ form: { equals: String(formId) } }, { field: { equals: field } }] },
15
16
  limit: 1e5,
16
17
  pagination: false,
@@ -1 +1 @@
1
- {"version":3,"file":"aggregateFromVotes.js","names":[],"sources":["../../../src/poll/votes/aggregateFromVotes.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport type { FieldAggregation } from '../../aggregation/types'\nimport { POLL_VOTES_SLUG, RESPONDENTS_VALUE } from './votesCollection'\n\ntype VoteRow = { field?: unknown; value?: unknown; count?: unknown }\n\nconst round1 = (n: number): number => Math.round(n * 10) / 10\n\n/**\n * Poll results from the tally store: one find over the form's rows for the field (O(options),\n * no scan, no truncation), buckets zero-seeded from the effective options so order and labels\n * match the scan-based shape. Counts sum across shard rows (see VOTE_SHARDS). `total` comes\n * from the respondents rows, keeping the percentage denominator exact for multi-value answers.\n */\nexport const aggregateFromVotes = async (args: {\n\tpayload: Payload\n\tformId: number | string\n\tfield: string\n\tmeta?: { label?: string; fieldType?: string }\n\toptions: { value: string; label: string }[]\n\treq?: PayloadRequest\n}): Promise<FieldAggregation> => {\n\tconst { payload, formId, field, meta, options, req } = args\n\tconst { docs } = await payload.find({\n\t\tcollection: POLL_VOTES_SLUG,\n\t\twhere: { and: [{ form: { equals: String(formId) } }, { field: { equals: field } }] },\n\t\tlimit: 100_000,\n\t\tpagination: false,\n\t\tdepth: 0,\n\t\toverrideAccess: true,\n\t\treq,\n\t})\n\n\tconst counts = new Map<string, number>()\n\tlet total = 0\n\tfor (const doc of docs as VoteRow[]) {\n\t\tconst value = doc.value == null ? RESPONDENTS_VALUE : String(doc.value)\n\t\tconst count = typeof doc.count === 'number' ? doc.count : 0\n\t\tif (value === RESPONDENTS_VALUE) {\n\t\t\ttotal += count\n\t\t} else {\n\t\t\tcounts.set(value, (counts.get(value) ?? 0) + count)\n\t\t}\n\t}\n\n\t// Clamp at read: a concurrent double-change can transiently double-decrement one value (see\n\t// the tally hook's drift note), and a negative bucket count must never surface to a reader.\n\ttotal = Math.max(0, total)\n\n\tconst optionValues = new Set(options.map((option) => option.value))\n\tconst leftovers = [...counts.keys()]\n\t\t.filter((value) => !optionValues.has(value))\n\t\t.sort((a, b) => (counts.get(b) ?? 0) - (counts.get(a) ?? 0))\n\tconst labelByValue = new Map(options.map((option) => [option.value, option.label]))\n\n\tconst buckets = [...options.map((option) => option.value), ...leftovers].map((value) => {\n\t\tconst count = Math.max(0, counts.get(value) ?? 0)\n\t\treturn {\n\t\t\tvalue,\n\t\t\tlabel: labelByValue.get(value) ?? value,\n\t\t\tcount,\n\t\t\tpercentage: total > 0 ? round1((count / total) * 100) : 0,\n\t\t}\n\t})\n\n\treturn {\n\t\tfield,\n\t\tlabel: meta?.label,\n\t\tfieldType: meta?.fieldType,\n\t\ttotal,\n\t\tbuckets,\n\t\ttruncated: false,\n\t}\n}\n"],"mappings":";;AAMA,MAAM,UAAU,MAAsB,KAAK,MAAM,IAAI,EAAE,IAAI;;;;;;;AAQ3D,MAAa,qBAAqB,OAAO,SAOR;CAChC,MAAM,EAAE,SAAS,QAAQ,OAAO,MAAM,SAAS,QAAQ;CACvD,MAAM,EAAE,SAAS,MAAM,QAAQ,KAAK;EACnC,YAAY;EACZ,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,OAAO,MAAM,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,MAAM,EAAE,CAAC,EAAE;EACnF,OAAO;EACP,YAAY;EACZ,OAAO;EACP,gBAAgB;EAChB;CACD,CAAC;CAED,MAAM,yBAAS,IAAI,IAAoB;CACvC,IAAI,QAAQ;CACZ,KAAK,MAAM,OAAO,MAAmB;EACpC,MAAM,QAAQ,IAAI,SAAS,OAAA,KAA2B,OAAO,IAAI,KAAK;EACtE,MAAM,QAAQ,OAAO,IAAI,UAAU,WAAW,IAAI,QAAQ;EAC1D,IAAI,UAAA,IACH,SAAS;OAET,OAAO,IAAI,QAAQ,OAAO,IAAI,KAAK,KAAK,KAAK,KAAK;CAEpD;CAIA,QAAQ,KAAK,IAAI,GAAG,KAAK;CAEzB,MAAM,eAAe,IAAI,IAAI,QAAQ,KAAK,WAAW,OAAO,KAAK,CAAC;CAClE,MAAM,YAAY,CAAC,GAAG,OAAO,KAAK,CAAC,EACjC,QAAQ,UAAU,CAAC,aAAa,IAAI,KAAK,CAAC,EAC1C,MAAM,GAAG,OAAO,OAAO,IAAI,CAAC,KAAK,MAAM,OAAO,IAAI,CAAC,KAAK,EAAE;CAC5D,MAAM,eAAe,IAAI,IAAI,QAAQ,KAAK,WAAW,CAAC,OAAO,OAAO,OAAO,KAAK,CAAC,CAAC;CAElF,MAAM,UAAU,CAAC,GAAG,QAAQ,KAAK,WAAW,OAAO,KAAK,GAAG,GAAG,SAAS,EAAE,KAAK,UAAU;EACvF,MAAM,QAAQ,KAAK,IAAI,GAAG,OAAO,IAAI,KAAK,KAAK,CAAC;EAChD,OAAO;GACN;GACA,OAAO,aAAa,IAAI,KAAK,KAAK;GAClC;GACA,YAAY,QAAQ,IAAI,OAAQ,QAAQ,QAAS,GAAG,IAAI;EACzD;CACD,CAAC;CAED,OAAO;EACN;EACA,OAAO,MAAM;EACb,WAAW,MAAM;EACjB;EACA;EACA,WAAW;CACZ;AACD"}
1
+ {"version":3,"file":"aggregateFromVotes.js","names":[],"sources":["../../../src/poll/votes/aggregateFromVotes.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport type { FieldAggregation } from '../../aggregation/types'\nimport { pluginSlugsOf } from '../../plugin/collectionSlugs'\nimport { RESPONDENTS_VALUE } from './votesCollection'\n\ntype VoteRow = { field?: unknown; value?: unknown; count?: unknown }\n\nconst round1 = (n: number): number => Math.round(n * 10) / 10\n\n/**\n * Poll results from the tally store: one find over the form's rows for the field (O(options),\n * no scan, no truncation), buckets zero-seeded from the effective options so order and labels\n * match the scan-based shape. Counts sum across shard rows (see VOTE_SHARDS). `total` comes\n * from the respondents rows, keeping the percentage denominator exact for multi-value answers.\n */\nexport const aggregateFromVotes = async (args: {\n\tpayload: Payload\n\tformId: number | string\n\tfield: string\n\tmeta?: { label?: string; fieldType?: string }\n\toptions: { value: string; label: string }[]\n\treq?: PayloadRequest\n}): Promise<FieldAggregation> => {\n\tconst { payload, formId, field, meta, options, req } = args\n\tconst { docs } = await payload.find({\n\t\tcollection: pluginSlugsOf(payload).pollVotes,\n\t\twhere: { and: [{ form: { equals: String(formId) } }, { field: { equals: field } }] },\n\t\tlimit: 100_000,\n\t\tpagination: false,\n\t\tdepth: 0,\n\t\toverrideAccess: true,\n\t\treq,\n\t})\n\n\tconst counts = new Map<string, number>()\n\tlet total = 0\n\tfor (const doc of docs as VoteRow[]) {\n\t\tconst value = doc.value == null ? RESPONDENTS_VALUE : String(doc.value)\n\t\tconst count = typeof doc.count === 'number' ? doc.count : 0\n\t\tif (value === RESPONDENTS_VALUE) {\n\t\t\ttotal += count\n\t\t} else {\n\t\t\tcounts.set(value, (counts.get(value) ?? 0) + count)\n\t\t}\n\t}\n\n\t// Clamp at read: a concurrent double-change can transiently double-decrement one value (see\n\t// the tally hook's drift note), and a negative bucket count must never surface to a reader.\n\ttotal = Math.max(0, total)\n\n\tconst optionValues = new Set(options.map((option) => option.value))\n\tconst leftovers = [...counts.keys()]\n\t\t.filter((value) => !optionValues.has(value))\n\t\t.sort((a, b) => (counts.get(b) ?? 0) - (counts.get(a) ?? 0))\n\tconst labelByValue = new Map(options.map((option) => [option.value, option.label]))\n\n\tconst buckets = [...options.map((option) => option.value), ...leftovers].map((value) => {\n\t\tconst count = Math.max(0, counts.get(value) ?? 0)\n\t\treturn {\n\t\t\tvalue,\n\t\t\tlabel: labelByValue.get(value) ?? value,\n\t\t\tcount,\n\t\t\tpercentage: total > 0 ? round1((count / total) * 100) : 0,\n\t\t}\n\t})\n\n\treturn {\n\t\tfield,\n\t\tlabel: meta?.label,\n\t\tfieldType: meta?.fieldType,\n\t\ttotal,\n\t\tbuckets,\n\t\ttruncated: false,\n\t}\n}\n"],"mappings":";;;AAOA,MAAM,UAAU,MAAsB,KAAK,MAAM,IAAI,EAAE,IAAI;;;;;;;AAQ3D,MAAa,qBAAqB,OAAO,SAOR;CAChC,MAAM,EAAE,SAAS,QAAQ,OAAO,MAAM,SAAS,QAAQ;CACvD,MAAM,EAAE,SAAS,MAAM,QAAQ,KAAK;EACnC,YAAY,cAAc,OAAO,EAAE;EACnC,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,OAAO,MAAM,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,MAAM,EAAE,CAAC,EAAE;EACnF,OAAO;EACP,YAAY;EACZ,OAAO;EACP,gBAAgB;EAChB;CACD,CAAC;CAED,MAAM,yBAAS,IAAI,IAAoB;CACvC,IAAI,QAAQ;CACZ,KAAK,MAAM,OAAO,MAAmB;EACpC,MAAM,QAAQ,IAAI,SAAS,OAAA,KAA2B,OAAO,IAAI,KAAK;EACtE,MAAM,QAAQ,OAAO,IAAI,UAAU,WAAW,IAAI,QAAQ;EAC1D,IAAI,UAAA,IACH,SAAS;OAET,OAAO,IAAI,QAAQ,OAAO,IAAI,KAAK,KAAK,KAAK,KAAK;CAEpD;CAIA,QAAQ,KAAK,IAAI,GAAG,KAAK;CAEzB,MAAM,eAAe,IAAI,IAAI,QAAQ,KAAK,WAAW,OAAO,KAAK,CAAC;CAClE,MAAM,YAAY,CAAC,GAAG,OAAO,KAAK,CAAC,EACjC,QAAQ,UAAU,CAAC,aAAa,IAAI,KAAK,CAAC,EAC1C,MAAM,GAAG,OAAO,OAAO,IAAI,CAAC,KAAK,MAAM,OAAO,IAAI,CAAC,KAAK,EAAE;CAC5D,MAAM,eAAe,IAAI,IAAI,QAAQ,KAAK,WAAW,CAAC,OAAO,OAAO,OAAO,KAAK,CAAC,CAAC;CAElF,MAAM,UAAU,CAAC,GAAG,QAAQ,KAAK,WAAW,OAAO,KAAK,GAAG,GAAG,SAAS,EAAE,KAAK,UAAU;EACvF,MAAM,QAAQ,KAAK,IAAI,GAAG,OAAO,IAAI,KAAK,KAAK,CAAC;EAChD,OAAO;GACN;GACA,OAAO,aAAa,IAAI,KAAK,KAAK;GAClC;GACA,YAAY,QAAQ,IAAI,OAAQ,QAAQ,QAAS,GAAG,IAAI;EACzD;CACD,CAAC;CAED,OAAO;EACN;EACA,OAAO,MAAM;EACb,WAAW,MAAM;EACjB;EACA;EACA,WAAW;CACZ;AACD"}
@@ -1,6 +1,7 @@
1
- import { POLL_VOTES_SLUG } from "./votesCollection.js";
1
+ import { pluginSlugsOf } from "../../plugin/collectionSlugs.js";
2
+ import "./votesCollection.js";
3
+ import toSnakeCase from "to-snake-case";
2
4
  //#region src/poll/votes/bumpPollVote.ts
3
- const PG_TABLE_KEY = "form_poll_votes";
4
5
  /**
5
6
  * Loads the optional Postgres peer without a literal specifier: bundlers (Turbopack, webpack,
6
7
  * Vite) resolve literal dynamic imports at build time, which fails a Mongo host's build even
@@ -35,10 +36,11 @@ const importPostgresSql = () => {
35
36
  * recount from stored submissions is the healer for any drift that window allows.
36
37
  */
37
38
  async function bumpPollVote(payload, key, by, transactionID) {
39
+ const slug = pluginSlugsOf(payload).pollVotes;
38
40
  if (payload.db.name === "mongoose") {
39
41
  const db = payload.db;
40
- const model = db.collections[POLL_VOTES_SLUG];
41
- if (!model) throw new Error(`form-builder: mongoose collection "${POLL_VOTES_SLUG}" not found`);
42
+ const model = db.collections[slug];
43
+ if (!model) throw new Error(`form-builder: mongoose collection "${slug}" not found`);
42
44
  const session = transactionID !== void 0 ? db.sessions?.[transactionID] : void 0;
43
45
  const shard = session ? Math.floor(Math.random() * 8) : 0;
44
46
  const shardedKey = {
@@ -64,8 +66,9 @@ async function bumpPollVote(payload, key, by, transactionID) {
64
66
  }
65
67
  const { sql } = await importPostgresSql();
66
68
  const db = payload.db;
67
- const tableName = db.tableNameMap.get(PG_TABLE_KEY);
68
- if (!tableName) throw new Error(`form-builder: drizzle table "${PG_TABLE_KEY}" not found`);
69
+ const tableKey = toSnakeCase(slug);
70
+ const tableName = db.tableNameMap.get(tableKey);
71
+ if (!tableName) throw new Error(`form-builder: drizzle table "${tableKey}" not found`);
69
72
  const table = db.tables[tableName];
70
73
  if (!table) throw new Error(`form-builder: drizzle table object for "${tableName}" not found`);
71
74
  await ((transactionID !== void 0 ? db.sessions?.[transactionID]?.db : void 0) ?? db.drizzle).insert(table).values({