@mobilizehub/payload-plugin 0.5.3 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/resend-adapter.js +2 -2
- package/dist/adapters/resend-adapter.js.map +1 -1
- package/dist/collections/form-submissions/generateFormSubmissionsCollection.js +7 -0
- package/dist/collections/form-submissions/generateFormSubmissionsCollection.js.map +1 -1
- package/dist/collections/form-submissions/hooks/processFormSubmission.d.ts +11 -0
- package/dist/collections/form-submissions/hooks/processFormSubmission.js +153 -0
- package/dist/collections/form-submissions/hooks/processFormSubmission.js.map +1 -0
- package/dist/collections/form-submissions/hooks/sendAutoresponse.d.ts +16 -0
- package/dist/collections/form-submissions/hooks/sendAutoresponse.js +109 -0
- package/dist/collections/form-submissions/hooks/sendAutoresponse.js.map +1 -0
- package/dist/endpoints/formSubmissionHandler.d.ts +14 -0
- package/dist/endpoints/formSubmissionHandler.js +146 -0
- package/dist/endpoints/formSubmissionHandler.js.map +1 -0
- package/dist/endpoints/sendBroadcastHandler.js +1 -0
- package/dist/endpoints/sendBroadcastHandler.js.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +1 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/submit-form.d.ts +57 -0
- package/dist/react/submit-form.js +54 -0
- package/dist/react/submit-form.js.map +1 -0
- package/dist/tasks/sendBroadcastsTask.js +0 -1
- package/dist/tasks/sendBroadcastsTask.js.map +1 -1
- package/package.json +1 -1
|
@@ -183,7 +183,7 @@ async function handleWebhookEvent(payload, eventType, emailProviderId, logger) {
|
|
|
183
183
|
* ]
|
|
184
184
|
* ```
|
|
185
185
|
*/ export const resendAdapter = (opts)=>{
|
|
186
|
-
return ()=>({
|
|
186
|
+
return ({ payload })=>({
|
|
187
187
|
name: 'resend-mobilizehub-adapter',
|
|
188
188
|
defaultFromAddress: opts.defaultFromAddress,
|
|
189
189
|
defaultFromName: opts.defaultFromName,
|
|
@@ -195,7 +195,7 @@ async function handleWebhookEvent(payload, eventType, emailProviderId, logger) {
|
|
|
195
195
|
html: message.html,
|
|
196
196
|
subject: message.subject,
|
|
197
197
|
to: message.to
|
|
198
|
-
});
|
|
198
|
+
}, message.idempotencyKey);
|
|
199
199
|
},
|
|
200
200
|
webhookHandler: async (req)=>{
|
|
201
201
|
const { payload } = req;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adapters/resend-adapter.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\n\nimport crypto from 'crypto'\n\nimport type { EmailActivityType, EmailAdapter, EmailMessage } from '../types/index.js'\n\nimport { formatFromAddress } from '../utils/email.js'\n\ntype ResendAdapterOptions = {\n apiKey: string\n defaultFromAddress: string\n defaultFromName: string\n render: ReturnType<EmailAdapter>['render']\n webhookSecret: string\n}\n\ntype ResendWebhookPayload = {\n created_at: string\n data: {\n [key: string]: unknown\n email_id?: string\n }\n type: string\n}\n\nconst RESEND_API_URL = 'https://api.resend.com/emails'\nconst WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS = 5 * 60 // 5 minutes\n\nconst WEBHOOK_EVENT_TO_ACTIVITY: Record<string, EmailActivityType> = {\n 'email.bounced': 'bounced',\n 'email.clicked': 'clicked',\n 'email.complained': 'complained',\n 'email.delivered': 'delivered',\n 'email.delivery_delayed': 'delivery_delayed',\n 'email.failed': 'failed',\n 'email.opened': 'opened',\n 'email.received': 'received',\n 'email.sent': 'sent',\n}\n\nasync function sendResendEmail(\n apiKey: string,\n message: EmailMessage,\n idempotencyKey?: string,\n): Promise<{ providerId: string }> {\n const response = await fetch(RESEND_API_URL, {\n body: JSON.stringify({\n from: message.from,\n html: message.html,\n subject: message.subject,\n to: message.to,\n }),\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n 'Idempotency-Key': idempotencyKey || '',\n },\n method: 'POST',\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(`Resend API error: ${response.status} - ${errorText}`)\n }\n\n const data = (await response.json()) as { id: string }\n\n return { providerId: data.id }\n}\n\ntype SvixHeaders = {\n id: string\n signature: string\n timestamp: string\n}\n\nfunction extractSvixHeaders(req: PayloadRequest): SvixHeaders {\n const id = req.headers.get('svix-id')\n const timestamp = req.headers.get('svix-timestamp')\n const signature = req.headers.get('svix-signature')\n\n if (!id || !timestamp || !signature) {\n throw new Error('Missing required Svix headers')\n }\n\n return { id, signature, timestamp }\n}\n\nfunction verifyTimestamp(timestamp: string): void {\n const timestampSeconds = parseInt(timestamp, 10)\n const nowSeconds = Math.floor(Date.now() / 1000)\n\n if (Math.abs(nowSeconds - timestampSeconds) > WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS) {\n throw new Error('Webhook timestamp is too old')\n }\n}\n\nfunction computeExpectedSignature(\n svixId: string,\n svixTimestamp: string,\n body: string,\n webhookSecret: string,\n): string {\n const signedContent = `${svixId}.${svixTimestamp}.${body}`\n\n // Resend webhook secrets are prefixed with \"whsec_\" and base64 encoded\n const secretParts = webhookSecret.split('_')\n const secretBase64 = secretParts.length > 1 ? secretParts[1] : secretParts[0]\n const secretBytes = Buffer.from(secretBase64, 'base64')\n\n return crypto.createHmac('sha256', secretBytes).update(signedContent).digest('base64')\n}\n\nfunction verifySignature(svixSignature: string, expectedSignature: string): boolean {\n // Svix signatures can be multiple, space-delimited with version prefix (e.g., \"v1,<sig>\")\n const signatures = svixSignature.split(' ').map((sig) => {\n const [version, signature] = sig.split(',')\n return { signature, version }\n })\n\n return signatures.some(({ signature, version }) => {\n if (version !== 'v1') {\n return false\n }\n\n try {\n return crypto.timingSafeEqual(Buffer.from(expectedSignature), Buffer.from(signature))\n } catch {\n return false\n }\n })\n}\n\nasync function verifyWebhookSignature(\n req: PayloadRequest,\n webhookSecret: string,\n): Promise<ResendWebhookPayload> {\n const headers = extractSvixHeaders(req)\n\n verifyTimestamp(headers.timestamp)\n\n if (!req.text) {\n throw new Error('Request does not have a text body method')\n }\n\n const body = await req.text()\n\n if (!body) {\n throw new Error('No body in request')\n }\n\n const expectedSignature = computeExpectedSignature(\n headers.id,\n headers.timestamp,\n body,\n webhookSecret,\n )\n\n if (!verifySignature(headers.signature, expectedSignature)) {\n throw new Error('Invalid webhook signature')\n }\n\n return JSON.parse(body) as ResendWebhookPayload\n}\n\nasync function findEmailByProviderId(\n payload: Payload,\n providerId: string,\n): Promise<{ activity?: unknown[]; id: number | string } | null> {\n const result = await payload.find({\n collection: 'emails',\n limit: 1,\n where: {\n providerId: { equals: providerId },\n },\n })\n\n return (result.docs[0] as { activity?: unknown[]; id: number | string }) || null\n}\n\nasync function addEmailActivity(\n payload: Payload,\n emailId: number | string,\n currentActivity: undefined | unknown[],\n activityType: EmailActivityType,\n): Promise<void> {\n const updatedActivity = [\n ...(currentActivity || []),\n {\n type: activityType,\n timestamp: new Date().toISOString(),\n },\n ]\n\n await payload.update({\n id: emailId,\n collection: 'emails',\n data: {\n activity: updatedActivity,\n },\n })\n}\n\nasync function handleWebhookEvent(\n payload: Payload,\n eventType: string,\n emailProviderId: string,\n logger: Payload['logger'],\n): Promise<void> {\n const activityType = WEBHOOK_EVENT_TO_ACTIVITY[eventType]\n\n if (!activityType) {\n logger.warn(`Unhandled webhook event type: ${eventType}`)\n return\n }\n\n const email = await findEmailByProviderId(payload, emailProviderId)\n\n if (!email) {\n logger.error(`No email record found for provider ID: ${emailProviderId}`)\n return\n }\n\n await addEmailActivity(payload, email.id, email.activity, activityType)\n\n // Log based on severity\n const warnEvents = ['email.bounced', 'email.complained', 'email.delivery_delayed']\n const errorEvents = ['email.failed']\n\n if (errorEvents.includes(eventType)) {\n logger.error(`Email ${activityType}: ${emailProviderId}`)\n } else if (warnEvents.includes(eventType)) {\n logger.warn(`Email ${activityType}: ${emailProviderId}`)\n } else {\n logger.info(`Email ${activityType}: ${emailProviderId}`)\n }\n}\n\n/**\n * Resend email adapter for MobilizeHub\n *\n * @example\n * ```typescript\n * import { resendAdapter } from '@mobilizehub/payload-plugin/adapters'\n *\n * const emailAdapter = resendAdapter({\n * apiKey: process.env.RESEND_API_KEY!,\n * webhookSecret: process.env.RESEND_WEBHOOK_SECRET!,\n * defaultFromAddress: 'noreply@example.com',\n * defaultFromName: 'My App',\n * render: ({ html }) => html,\n * })\n *\n * // In your Payload config:\n * plugins: [\n * mobilizehub({\n * email: emailAdapter,\n * // ...\n * }),\n * ]\n * ```\n */\nexport const resendAdapter = (opts: ResendAdapterOptions): EmailAdapter => {\n return () => ({\n name: 'resend-mobilizehub-adapter',\n defaultFromAddress: opts.defaultFromAddress,\n defaultFromName: opts.defaultFromName,\n render: opts.render,\n\n sendEmail: async (message) => {\n const fromAddress =\n message.from || formatFromAddress(opts.defaultFromName, opts.defaultFromAddress)\n\n return sendResendEmail(opts.apiKey, {\n from: fromAddress,\n html: message.html,\n subject: message.subject,\n to: message.to,\n })\n },\n\n webhookHandler: async (req) => {\n const { payload } = req\n const logger = payload.logger\n\n try {\n const webhookPayload = await verifyWebhookSignature(req, opts.webhookSecret)\n\n const emailProviderId = webhookPayload.data.email_id\n\n if (!emailProviderId) {\n logger.warn(`Webhook event ${webhookPayload.type} has no email_id`)\n return\n }\n\n await handleWebhookEvent(payload, webhookPayload.type, emailProviderId, logger)\n } catch (error) {\n logger.error(\n `Resend webhook error: ${error instanceof Error ? error.message : 'Unknown error'}`,\n )\n throw error\n }\n },\n })\n}\n"],"names":["crypto","formatFromAddress","RESEND_API_URL","WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS","WEBHOOK_EVENT_TO_ACTIVITY","sendResendEmail","apiKey","message","idempotencyKey","response","fetch","body","JSON","stringify","from","html","subject","to","headers","Authorization","method","ok","errorText","text","Error","status","data","json","providerId","id","extractSvixHeaders","req","get","timestamp","signature","verifyTimestamp","timestampSeconds","parseInt","nowSeconds","Math","floor","Date","now","abs","computeExpectedSignature","svixId","svixTimestamp","webhookSecret","signedContent","secretParts","split","secretBase64","length","secretBytes","Buffer","createHmac","update","digest","verifySignature","svixSignature","expectedSignature","signatures","map","sig","version","some","timingSafeEqual","verifyWebhookSignature","parse","findEmailByProviderId","payload","result","find","collection","limit","where","equals","docs","addEmailActivity","emailId","currentActivity","activityType","updatedActivity","type","toISOString","activity","handleWebhookEvent","eventType","emailProviderId","logger","warn","email","error","warnEvents","errorEvents","includes","info","resendAdapter","opts","name","defaultFromAddress","defaultFromName","render","sendEmail","fromAddress","webhookHandler","webhookPayload","email_id"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/resend-adapter.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\n\nimport crypto from 'crypto'\n\nimport type { EmailActivityType, EmailAdapter, EmailMessage } from '../types/index.js'\n\nimport { formatFromAddress } from '../utils/email.js'\n\ntype ResendAdapterOptions = {\n apiKey: string\n defaultFromAddress: string\n defaultFromName: string\n render: ReturnType<EmailAdapter>['render']\n webhookSecret: string\n}\n\ntype ResendWebhookPayload = {\n created_at: string\n data: {\n [key: string]: unknown\n email_id?: string\n }\n type: string\n}\n\nconst RESEND_API_URL = 'https://api.resend.com/emails'\nconst WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS = 5 * 60 // 5 minutes\n\nconst WEBHOOK_EVENT_TO_ACTIVITY: Record<string, EmailActivityType> = {\n 'email.bounced': 'bounced',\n 'email.clicked': 'clicked',\n 'email.complained': 'complained',\n 'email.delivered': 'delivered',\n 'email.delivery_delayed': 'delivery_delayed',\n 'email.failed': 'failed',\n 'email.opened': 'opened',\n 'email.received': 'received',\n 'email.sent': 'sent',\n}\n\nasync function sendResendEmail(\n apiKey: string,\n message: EmailMessage,\n idempotencyKey?: string,\n): Promise<{ providerId: string }> {\n const response = await fetch(RESEND_API_URL, {\n body: JSON.stringify({\n from: message.from,\n html: message.html,\n subject: message.subject,\n to: message.to,\n }),\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n 'Idempotency-Key': idempotencyKey || '',\n },\n method: 'POST',\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(`Resend API error: ${response.status} - ${errorText}`)\n }\n\n const data = (await response.json()) as { id: string }\n\n return { providerId: data.id }\n}\n\ntype SvixHeaders = {\n id: string\n signature: string\n timestamp: string\n}\n\nfunction extractSvixHeaders(req: PayloadRequest): SvixHeaders {\n const id = req.headers.get('svix-id')\n const timestamp = req.headers.get('svix-timestamp')\n const signature = req.headers.get('svix-signature')\n\n if (!id || !timestamp || !signature) {\n throw new Error('Missing required Svix headers')\n }\n\n return { id, signature, timestamp }\n}\n\nfunction verifyTimestamp(timestamp: string): void {\n const timestampSeconds = parseInt(timestamp, 10)\n const nowSeconds = Math.floor(Date.now() / 1000)\n\n if (Math.abs(nowSeconds - timestampSeconds) > WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS) {\n throw new Error('Webhook timestamp is too old')\n }\n}\n\nfunction computeExpectedSignature(\n svixId: string,\n svixTimestamp: string,\n body: string,\n webhookSecret: string,\n): string {\n const signedContent = `${svixId}.${svixTimestamp}.${body}`\n\n // Resend webhook secrets are prefixed with \"whsec_\" and base64 encoded\n const secretParts = webhookSecret.split('_')\n const secretBase64 = secretParts.length > 1 ? secretParts[1] : secretParts[0]\n const secretBytes = Buffer.from(secretBase64, 'base64')\n\n return crypto.createHmac('sha256', secretBytes).update(signedContent).digest('base64')\n}\n\nfunction verifySignature(svixSignature: string, expectedSignature: string): boolean {\n // Svix signatures can be multiple, space-delimited with version prefix (e.g., \"v1,<sig>\")\n const signatures = svixSignature.split(' ').map((sig) => {\n const [version, signature] = sig.split(',')\n return { signature, version }\n })\n\n return signatures.some(({ signature, version }) => {\n if (version !== 'v1') {\n return false\n }\n\n try {\n return crypto.timingSafeEqual(Buffer.from(expectedSignature), Buffer.from(signature))\n } catch {\n return false\n }\n })\n}\n\nasync function verifyWebhookSignature(\n req: PayloadRequest,\n webhookSecret: string,\n): Promise<ResendWebhookPayload> {\n const headers = extractSvixHeaders(req)\n\n verifyTimestamp(headers.timestamp)\n\n if (!req.text) {\n throw new Error('Request does not have a text body method')\n }\n\n const body = await req.text()\n\n if (!body) {\n throw new Error('No body in request')\n }\n\n const expectedSignature = computeExpectedSignature(\n headers.id,\n headers.timestamp,\n body,\n webhookSecret,\n )\n\n if (!verifySignature(headers.signature, expectedSignature)) {\n throw new Error('Invalid webhook signature')\n }\n\n return JSON.parse(body) as ResendWebhookPayload\n}\n\nasync function findEmailByProviderId(\n payload: Payload,\n providerId: string,\n): Promise<{ activity?: unknown[]; id: number | string } | null> {\n const result = await payload.find({\n collection: 'emails',\n limit: 1,\n where: {\n providerId: { equals: providerId },\n },\n })\n\n return (result.docs[0] as { activity?: unknown[]; id: number | string }) || null\n}\n\nasync function addEmailActivity(\n payload: Payload,\n emailId: number | string,\n currentActivity: undefined | unknown[],\n activityType: EmailActivityType,\n): Promise<void> {\n const updatedActivity = [\n ...(currentActivity || []),\n {\n type: activityType,\n timestamp: new Date().toISOString(),\n },\n ]\n\n await payload.update({\n id: emailId,\n collection: 'emails',\n data: {\n activity: updatedActivity,\n },\n })\n}\n\nasync function handleWebhookEvent(\n payload: Payload,\n eventType: string,\n emailProviderId: string,\n logger: Payload['logger'],\n): Promise<void> {\n const activityType = WEBHOOK_EVENT_TO_ACTIVITY[eventType]\n\n if (!activityType) {\n logger.warn(`Unhandled webhook event type: ${eventType}`)\n return\n }\n\n const email = await findEmailByProviderId(payload, emailProviderId)\n\n if (!email) {\n logger.error(`No email record found for provider ID: ${emailProviderId}`)\n return\n }\n\n await addEmailActivity(payload, email.id, email.activity, activityType)\n\n // Log based on severity\n const warnEvents = ['email.bounced', 'email.complained', 'email.delivery_delayed']\n const errorEvents = ['email.failed']\n\n if (errorEvents.includes(eventType)) {\n logger.error(`Email ${activityType}: ${emailProviderId}`)\n } else if (warnEvents.includes(eventType)) {\n logger.warn(`Email ${activityType}: ${emailProviderId}`)\n } else {\n logger.info(`Email ${activityType}: ${emailProviderId}`)\n }\n}\n\n/**\n * Resend email adapter for MobilizeHub\n *\n * @example\n * ```typescript\n * import { resendAdapter } from '@mobilizehub/payload-plugin/adapters'\n *\n * const emailAdapter = resendAdapter({\n * apiKey: process.env.RESEND_API_KEY!,\n * webhookSecret: process.env.RESEND_WEBHOOK_SECRET!,\n * defaultFromAddress: 'noreply@example.com',\n * defaultFromName: 'My App',\n * render: ({ html }) => html,\n * })\n *\n * // In your Payload config:\n * plugins: [\n * mobilizehub({\n * email: emailAdapter,\n * // ...\n * }),\n * ]\n * ```\n */\nexport const resendAdapter = (opts: ResendAdapterOptions): EmailAdapter => {\n return ({ payload }) => ({\n name: 'resend-mobilizehub-adapter',\n defaultFromAddress: opts.defaultFromAddress,\n defaultFromName: opts.defaultFromName,\n render: opts.render,\n\n sendEmail: async (message) => {\n const fromAddress =\n message.from || formatFromAddress(opts.defaultFromName, opts.defaultFromAddress)\n\n return sendResendEmail(\n opts.apiKey,\n {\n from: fromAddress,\n html: message.html,\n subject: message.subject,\n to: message.to,\n },\n message.idempotencyKey,\n )\n },\n\n webhookHandler: async (req) => {\n const { payload } = req\n const logger = payload.logger\n\n try {\n const webhookPayload = await verifyWebhookSignature(req, opts.webhookSecret)\n\n const emailProviderId = webhookPayload.data.email_id\n\n if (!emailProviderId) {\n logger.warn(`Webhook event ${webhookPayload.type} has no email_id`)\n return\n }\n\n await handleWebhookEvent(payload, webhookPayload.type, emailProviderId, logger)\n } catch (error) {\n logger.error(\n `Resend webhook error: ${error instanceof Error ? error.message : 'Unknown error'}`,\n )\n throw error\n }\n },\n })\n}\n"],"names":["crypto","formatFromAddress","RESEND_API_URL","WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS","WEBHOOK_EVENT_TO_ACTIVITY","sendResendEmail","apiKey","message","idempotencyKey","response","fetch","body","JSON","stringify","from","html","subject","to","headers","Authorization","method","ok","errorText","text","Error","status","data","json","providerId","id","extractSvixHeaders","req","get","timestamp","signature","verifyTimestamp","timestampSeconds","parseInt","nowSeconds","Math","floor","Date","now","abs","computeExpectedSignature","svixId","svixTimestamp","webhookSecret","signedContent","secretParts","split","secretBase64","length","secretBytes","Buffer","createHmac","update","digest","verifySignature","svixSignature","expectedSignature","signatures","map","sig","version","some","timingSafeEqual","verifyWebhookSignature","parse","findEmailByProviderId","payload","result","find","collection","limit","where","equals","docs","addEmailActivity","emailId","currentActivity","activityType","updatedActivity","type","toISOString","activity","handleWebhookEvent","eventType","emailProviderId","logger","warn","email","error","warnEvents","errorEvents","includes","info","resendAdapter","opts","name","defaultFromAddress","defaultFromName","render","sendEmail","fromAddress","webhookHandler","webhookPayload","email_id"],"mappings":"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"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { authenticated } from '../../access/authenticated.js';
|
|
2
|
+
import { createProcessFormSubmissionHook } from './hooks/processFormSubmission.js';
|
|
3
|
+
import { createSendAutoresponseHook } from './hooks/sendAutoresponse.js';
|
|
2
4
|
export const generateFormSubmissionsCollection = (formSubmissionsConfig)=>{
|
|
3
5
|
const defaultFields = [
|
|
4
6
|
{
|
|
@@ -53,7 +55,12 @@ export const generateFormSubmissionsCollection = (formSubmissionsConfig)=>{
|
|
|
53
55
|
hooks: {
|
|
54
56
|
...formSubmissionsConfig.formSubmissionsOverrides?.hooks || {},
|
|
55
57
|
afterChange: [
|
|
58
|
+
createSendAutoresponseHook(formSubmissionsConfig),
|
|
56
59
|
...formSubmissionsConfig.formSubmissionsOverrides?.hooks?.afterChange || []
|
|
60
|
+
],
|
|
61
|
+
beforeChange: [
|
|
62
|
+
createProcessFormSubmissionHook(formSubmissionsConfig),
|
|
63
|
+
...formSubmissionsConfig.formSubmissionsOverrides?.hooks?.beforeChange || []
|
|
57
64
|
]
|
|
58
65
|
}
|
|
59
66
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/collections/form-submissions/generateFormSubmissionsCollection.ts"],"sourcesContent":["import type { CollectionConfig, Field } from 'payload'\n\nimport type { MobilizehubPluginConfig } from '../../types/index.js'\n\nimport { authenticated } from '../../access/authenticated.js'\n\nexport const generateFormSubmissionsCollection = (\n formSubmissionsConfig: MobilizehubPluginConfig,\n) => {\n const defaultFields: Field[] = [\n {\n name: 'form',\n type: 'relationship',\n admin: {\n position: 'sidebar',\n readOnly: true,\n },\n relationTo: 'forms',\n required: true,\n },\n {\n name: 'contact',\n type: 'relationship',\n admin: {\n position: 'sidebar',\n readOnly: true,\n },\n relationTo: 'contacts',\n },\n {\n name: 'createdAt',\n type: 'date',\n admin: {\n position: 'sidebar',\n readOnly: true,\n },\n },\n {\n name: 'data',\n type: 'json',\n label: 'Data',\n },\n ]\n\n const config: CollectionConfig = {\n ...(formSubmissionsConfig.formSubmissionsOverrides || {}),\n slug: formSubmissionsConfig.formSubmissionsOverrides?.slug || 'formSubmissions',\n access: {\n create: () => false,\n read: authenticated,\n update: () => false,\n ...(formSubmissionsConfig.formSubmissionsOverrides?.access || {}),\n },\n admin: {\n ...(formSubmissionsConfig.formSubmissionsOverrides?.admin || {}),\n hidden: formSubmissionsConfig.formSubmissionsOverrides?.admin?.hidden || true,\n },\n fields: formSubmissionsConfig.formSubmissionsOverrides?.fields\n ? formSubmissionsConfig.formSubmissionsOverrides.fields({ defaultFields })\n : defaultFields,\n hooks: {\n ...(formSubmissionsConfig.formSubmissionsOverrides?.hooks || {}),\n afterChange: [...(formSubmissionsConfig.formSubmissionsOverrides?.hooks?.afterChange || [])],\n },\n }\n\n return config\n}\n"],"names":["authenticated","generateFormSubmissionsCollection","formSubmissionsConfig","defaultFields","name","type","admin","position","readOnly","relationTo","required","label","config","formSubmissionsOverrides","slug","access","create","read","update","hidden","fields","hooks","afterChange"],"mappings":"AAIA,SAASA,aAAa,QAAQ,gCAA+B;
|
|
1
|
+
{"version":3,"sources":["../../../src/collections/form-submissions/generateFormSubmissionsCollection.ts"],"sourcesContent":["import type { CollectionConfig, Field } from 'payload'\n\nimport type { MobilizehubPluginConfig } from '../../types/index.js'\n\nimport { authenticated } from '../../access/authenticated.js'\nimport { createProcessFormSubmissionHook } from './hooks/processFormSubmission.js'\nimport { createSendAutoresponseHook } from './hooks/sendAutoresponse.js'\n\nexport const generateFormSubmissionsCollection = (\n formSubmissionsConfig: MobilizehubPluginConfig,\n) => {\n const defaultFields: Field[] = [\n {\n name: 'form',\n type: 'relationship',\n admin: {\n position: 'sidebar',\n readOnly: true,\n },\n relationTo: 'forms',\n required: true,\n },\n {\n name: 'contact',\n type: 'relationship',\n admin: {\n position: 'sidebar',\n readOnly: true,\n },\n relationTo: 'contacts',\n },\n {\n name: 'createdAt',\n type: 'date',\n admin: {\n position: 'sidebar',\n readOnly: true,\n },\n },\n {\n name: 'data',\n type: 'json',\n label: 'Data',\n },\n ]\n\n const config: CollectionConfig = {\n ...(formSubmissionsConfig.formSubmissionsOverrides || {}),\n slug: formSubmissionsConfig.formSubmissionsOverrides?.slug || 'formSubmissions',\n access: {\n create: () => false,\n read: authenticated,\n update: () => false,\n ...(formSubmissionsConfig.formSubmissionsOverrides?.access || {}),\n },\n admin: {\n ...(formSubmissionsConfig.formSubmissionsOverrides?.admin || {}),\n hidden: formSubmissionsConfig.formSubmissionsOverrides?.admin?.hidden || true,\n },\n fields: formSubmissionsConfig.formSubmissionsOverrides?.fields\n ? formSubmissionsConfig.formSubmissionsOverrides.fields({ defaultFields })\n : defaultFields,\n hooks: {\n ...(formSubmissionsConfig.formSubmissionsOverrides?.hooks || {}),\n afterChange: [\n createSendAutoresponseHook(formSubmissionsConfig),\n ...(formSubmissionsConfig.formSubmissionsOverrides?.hooks?.afterChange || []),\n ],\n beforeChange: [\n createProcessFormSubmissionHook(formSubmissionsConfig),\n ...(formSubmissionsConfig.formSubmissionsOverrides?.hooks?.beforeChange || []),\n ],\n },\n }\n\n return config\n}\n"],"names":["authenticated","createProcessFormSubmissionHook","createSendAutoresponseHook","generateFormSubmissionsCollection","formSubmissionsConfig","defaultFields","name","type","admin","position","readOnly","relationTo","required","label","config","formSubmissionsOverrides","slug","access","create","read","update","hidden","fields","hooks","afterChange","beforeChange"],"mappings":"AAIA,SAASA,aAAa,QAAQ,gCAA+B;AAC7D,SAASC,+BAA+B,QAAQ,mCAAkC;AAClF,SAASC,0BAA0B,QAAQ,8BAA6B;AAExE,OAAO,MAAMC,oCAAoC,CAC/CC;IAEA,MAAMC,gBAAyB;QAC7B;YACEC,MAAM;YACNC,MAAM;YACNC,OAAO;gBACLC,UAAU;gBACVC,UAAU;YACZ;YACAC,YAAY;YACZC,UAAU;QACZ;QACA;YACEN,MAAM;YACNC,MAAM;YACNC,OAAO;gBACLC,UAAU;gBACVC,UAAU;YACZ;YACAC,YAAY;QACd;QACA;YACEL,MAAM;YACNC,MAAM;YACNC,OAAO;gBACLC,UAAU;gBACVC,UAAU;YACZ;QACF;QACA;YACEJ,MAAM;YACNC,MAAM;YACNM,OAAO;QACT;KACD;IAED,MAAMC,SAA2B;QAC/B,GAAIV,sBAAsBW,wBAAwB,IAAI,CAAC,CAAC;QACxDC,MAAMZ,sBAAsBW,wBAAwB,EAAEC,QAAQ;QAC9DC,QAAQ;YACNC,QAAQ,IAAM;YACdC,MAAMnB;YACNoB,QAAQ,IAAM;YACd,GAAIhB,sBAAsBW,wBAAwB,EAAEE,UAAU,CAAC,CAAC;QAClE;QACAT,OAAO;YACL,GAAIJ,sBAAsBW,wBAAwB,EAAEP,SAAS,CAAC,CAAC;YAC/Da,QAAQjB,sBAAsBW,wBAAwB,EAAEP,OAAOa,UAAU;QAC3E;QACAC,QAAQlB,sBAAsBW,wBAAwB,EAAEO,SACpDlB,sBAAsBW,wBAAwB,CAACO,MAAM,CAAC;YAAEjB;QAAc,KACtEA;QACJkB,OAAO;YACL,GAAInB,sBAAsBW,wBAAwB,EAAEQ,SAAS,CAAC,CAAC;YAC/DC,aAAa;gBACXtB,2BAA2BE;mBACvBA,sBAAsBW,wBAAwB,EAAEQ,OAAOC,eAAe,EAAE;aAC7E;YACDC,cAAc;gBACZxB,gCAAgCG;mBAC5BA,sBAAsBW,wBAAwB,EAAEQ,OAAOE,gBAAgB,EAAE;aAC9E;QACH;IACF;IAEA,OAAOX;AACT,EAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CollectionBeforeChangeHook } from 'payload';
|
|
2
|
+
import type { MobilizehubPluginConfig } from '../../../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates the form submission processing hook.
|
|
5
|
+
*
|
|
6
|
+
* This hook:
|
|
7
|
+
* 1. Creates or updates a contact based on submission data
|
|
8
|
+
* 2. Applies form tags to the contact
|
|
9
|
+
* 3. Links the submission to the contact by returning modified data
|
|
10
|
+
*/
|
|
11
|
+
export declare const createProcessFormSubmissionHook: (pluginConfig: MobilizehubPluginConfig) => CollectionBeforeChangeHook;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contact field mapping from form submission data to contact fields.
|
|
3
|
+
*/ const CONTACT_FIELD_MAP = [
|
|
4
|
+
'email',
|
|
5
|
+
'emailOptIn',
|
|
6
|
+
'firstName',
|
|
7
|
+
'lastName',
|
|
8
|
+
'mobileNumber',
|
|
9
|
+
'mobileOptIn',
|
|
10
|
+
'address',
|
|
11
|
+
'city',
|
|
12
|
+
'state',
|
|
13
|
+
'zip',
|
|
14
|
+
'country'
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* Extracts contact-related fields from form submission data.
|
|
18
|
+
*/ function extractContactData(submissionData) {
|
|
19
|
+
const contactData = {};
|
|
20
|
+
for (const field of CONTACT_FIELD_MAP){
|
|
21
|
+
if (submissionData[field] !== undefined) {
|
|
22
|
+
contactData[field] = submissionData[field];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return contactData;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Creates the form submission processing hook.
|
|
29
|
+
*
|
|
30
|
+
* This hook:
|
|
31
|
+
* 1. Creates or updates a contact based on submission data
|
|
32
|
+
* 2. Applies form tags to the contact
|
|
33
|
+
* 3. Links the submission to the contact by returning modified data
|
|
34
|
+
*/ export const createProcessFormSubmissionHook = (pluginConfig)=>{
|
|
35
|
+
const contactsSlug = pluginConfig.contactsOverrides?.slug || 'contacts';
|
|
36
|
+
const formsSlug = pluginConfig.formsOverrides?.slug || 'forms';
|
|
37
|
+
return async ({ data, operation, req })=>{
|
|
38
|
+
// Only process on creation
|
|
39
|
+
if (operation !== 'create') {
|
|
40
|
+
return data;
|
|
41
|
+
}
|
|
42
|
+
const { payload } = req;
|
|
43
|
+
const logger = payload.logger;
|
|
44
|
+
// Parse submission data
|
|
45
|
+
const submissionData = data.data;
|
|
46
|
+
if (!submissionData) {
|
|
47
|
+
logger.warn('Form submission has no data');
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
// Email is required for contact creation
|
|
51
|
+
const email = submissionData.email;
|
|
52
|
+
if (!email || typeof email !== 'string') {
|
|
53
|
+
logger.info('Form submission has no email, skipping contact creation');
|
|
54
|
+
return data;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
// Extract contact fields from submission
|
|
58
|
+
const contactData = extractContactData(submissionData);
|
|
59
|
+
// Find or create contact
|
|
60
|
+
const existingContactResult = await payload.find({
|
|
61
|
+
collection: contactsSlug,
|
|
62
|
+
limit: 1,
|
|
63
|
+
where: {
|
|
64
|
+
email: {
|
|
65
|
+
equals: email
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
const existingContact = existingContactResult.docs[0];
|
|
70
|
+
let contactId;
|
|
71
|
+
if (existingContact) {
|
|
72
|
+
// Update existing contact (merge data, don't overwrite with empty values)
|
|
73
|
+
const updateData = {};
|
|
74
|
+
for (const [key, value] of Object.entries(contactData)){
|
|
75
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
76
|
+
updateData[key] = value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
await payload.update({
|
|
80
|
+
id: existingContact.id,
|
|
81
|
+
collection: contactsSlug,
|
|
82
|
+
data: updateData
|
|
83
|
+
});
|
|
84
|
+
contactId = existingContact.id;
|
|
85
|
+
logger.info(`Updated contact ${contactId} from form submission`);
|
|
86
|
+
} else {
|
|
87
|
+
// Create new contact
|
|
88
|
+
const newContact = await payload.create({
|
|
89
|
+
collection: contactsSlug,
|
|
90
|
+
data: contactData
|
|
91
|
+
});
|
|
92
|
+
contactId = newContact.id;
|
|
93
|
+
logger.info(`Created contact ${contactId} from form submission`);
|
|
94
|
+
}
|
|
95
|
+
const formId = data.form;
|
|
96
|
+
const formIdValue = typeof formId === 'object' ? formId.id : formId;
|
|
97
|
+
if (!formIdValue) {
|
|
98
|
+
logger.error('Form ID not found for form submission');
|
|
99
|
+
throw new Error('Form ID not found for form submission');
|
|
100
|
+
}
|
|
101
|
+
// Get form to check for tags
|
|
102
|
+
const form = await payload.findByID({
|
|
103
|
+
id: formIdValue,
|
|
104
|
+
collection: formsSlug,
|
|
105
|
+
depth: 0
|
|
106
|
+
});
|
|
107
|
+
// Apply form tags to contact
|
|
108
|
+
if (form && Array.isArray(form.tags) && form.tags.length > 0) {
|
|
109
|
+
// Get form tag IDs
|
|
110
|
+
const formTagIds = form.tags.map((t)=>typeof t === 'object' ? t.id : t);
|
|
111
|
+
// Fetch current contact to get up-to-date tags
|
|
112
|
+
const currentContact = await payload.findByID({
|
|
113
|
+
id: contactId,
|
|
114
|
+
collection: contactsSlug,
|
|
115
|
+
depth: 0
|
|
116
|
+
});
|
|
117
|
+
// Get existing tag IDs (normalize to IDs)
|
|
118
|
+
const existingTagIds = (currentContact.tags || []).map((t)=>typeof t === 'object' ? t.id : t);
|
|
119
|
+
// Find only new tags that don't already exist
|
|
120
|
+
const newTagIds = formTagIds.filter((tagId)=>!existingTagIds.includes(tagId));
|
|
121
|
+
if (newTagIds.length > 0) {
|
|
122
|
+
// Merge tags
|
|
123
|
+
const mergedTags = [
|
|
124
|
+
...existingTagIds,
|
|
125
|
+
...newTagIds
|
|
126
|
+
];
|
|
127
|
+
// Update contact with merged tags
|
|
128
|
+
await payload.update({
|
|
129
|
+
id: contactId,
|
|
130
|
+
collection: contactsSlug,
|
|
131
|
+
data: {
|
|
132
|
+
tags: mergedTags
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
logger.info(`Applied ${newTagIds.length} new tags to contact ${contactId}`);
|
|
136
|
+
} else {
|
|
137
|
+
logger.info(`Contact ${contactId} already has all form tags`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Return data with contact linked - no separate update needed
|
|
141
|
+
return {
|
|
142
|
+
...data,
|
|
143
|
+
contact: contactId
|
|
144
|
+
};
|
|
145
|
+
} catch (error) {
|
|
146
|
+
logger.error(error, 'Error processing form submission');
|
|
147
|
+
// Don't throw - we don't want to fail the submission
|
|
148
|
+
return data;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
//# sourceMappingURL=processFormSubmission.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/collections/form-submissions/hooks/processFormSubmission.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport type { MobilizehubPluginConfig } from '../../../types/index.js'\n\n/**\n * Contact field mapping from form submission data to contact fields.\n */\nconst CONTACT_FIELD_MAP = [\n 'email',\n 'emailOptIn',\n 'firstName',\n 'lastName',\n 'mobileNumber',\n 'mobileOptIn',\n 'address',\n 'city',\n 'state',\n 'zip',\n 'country',\n] as const\n\ntype ContactFieldName = (typeof CONTACT_FIELD_MAP)[number]\n\n/**\n * Extracts contact-related fields from form submission data.\n */\nfunction extractContactData(\n submissionData: Record<string, unknown>,\n): Partial<Record<ContactFieldName, unknown>> {\n const contactData: Partial<Record<ContactFieldName, unknown>> = {}\n\n for (const field of CONTACT_FIELD_MAP) {\n if (submissionData[field] !== undefined) {\n contactData[field] = submissionData[field]\n }\n }\n\n return contactData\n}\n\n/**\n * Creates the form submission processing hook.\n *\n * This hook:\n * 1. Creates or updates a contact based on submission data\n * 2. Applies form tags to the contact\n * 3. Links the submission to the contact by returning modified data\n */\nexport const createProcessFormSubmissionHook = (\n pluginConfig: MobilizehubPluginConfig,\n): CollectionBeforeChangeHook => {\n const contactsSlug = pluginConfig.contactsOverrides?.slug || 'contacts'\n const formsSlug = pluginConfig.formsOverrides?.slug || 'forms'\n\n return async ({ data, operation, req }) => {\n // Only process on creation\n if (operation !== 'create') {\n return data\n }\n\n const { payload } = req\n const logger = payload.logger\n\n // Parse submission data\n const submissionData = data.data as Record<string, unknown> | undefined\n\n if (!submissionData) {\n logger.warn('Form submission has no data')\n return data\n }\n\n // Email is required for contact creation\n const email = submissionData.email as string | undefined\n\n if (!email || typeof email !== 'string') {\n logger.info('Form submission has no email, skipping contact creation')\n return data\n }\n\n try {\n // Extract contact fields from submission\n const contactData = extractContactData(submissionData)\n\n // Find or create contact\n const existingContactResult = await payload.find({\n collection: contactsSlug,\n limit: 1,\n where: { email: { equals: email } },\n })\n\n const existingContact = existingContactResult.docs[0] as\n | { id: number | string; tags?: (number | string)[] }\n | undefined\n\n let contactId: number | string\n\n if (existingContact) {\n // Update existing contact (merge data, don't overwrite with empty values)\n const updateData: Record<string, unknown> = {}\n\n for (const [key, value] of Object.entries(contactData)) {\n if (value !== undefined && value !== null && value !== '') {\n updateData[key] = value\n }\n }\n\n await payload.update({\n id: existingContact.id,\n collection: contactsSlug,\n data: updateData,\n })\n\n contactId = existingContact.id\n logger.info(`Updated contact ${contactId} from form submission`)\n } else {\n // Create new contact\n const newContact = await payload.create({\n collection: contactsSlug,\n data: contactData,\n })\n\n contactId = newContact.id\n logger.info(`Created contact ${contactId} from form submission`)\n }\n\n const formId = data.form as { id: number | string } | number | string\n const formIdValue = typeof formId === 'object' ? formId.id : formId\n\n if (!formIdValue) {\n logger.error('Form ID not found for form submission')\n throw new Error('Form ID not found for form submission')\n }\n\n // Get form to check for tags\n const form = await payload.findByID({\n id: formIdValue,\n collection: formsSlug,\n depth: 0,\n })\n\n // Apply form tags to contact\n if (form && Array.isArray(form.tags) && form.tags.length > 0) {\n // Get form tag IDs\n const formTagIds = form.tags.map((t: { id: number | string } | number | string) =>\n typeof t === 'object' ? t.id : t,\n )\n\n // Fetch current contact to get up-to-date tags\n const currentContact = await payload.findByID({\n id: contactId,\n collection: contactsSlug,\n depth: 0,\n })\n\n // Get existing tag IDs (normalize to IDs)\n const existingTagIds = (\n (currentContact.tags as ({ id: number | string } | number | string)[]) || []\n ).map((t) => (typeof t === 'object' ? t.id : t))\n\n // Find only new tags that don't already exist\n const newTagIds = formTagIds.filter(\n (tagId: number | string) => !existingTagIds.includes(tagId),\n )\n\n if (newTagIds.length > 0) {\n // Merge tags\n const mergedTags = [...existingTagIds, ...newTagIds]\n\n // Update contact with merged tags\n await payload.update({\n id: contactId,\n collection: contactsSlug,\n data: { tags: mergedTags },\n })\n\n logger.info(`Applied ${newTagIds.length} new tags to contact ${contactId}`)\n } else {\n logger.info(`Contact ${contactId} already has all form tags`)\n }\n }\n\n // Return data with contact linked - no separate update needed\n return {\n ...data,\n contact: contactId,\n }\n } catch (error) {\n logger.error(error as Error, 'Error processing form submission')\n // Don't throw - we don't want to fail the submission\n return data\n }\n }\n}\n"],"names":["CONTACT_FIELD_MAP","extractContactData","submissionData","contactData","field","undefined","createProcessFormSubmissionHook","pluginConfig","contactsSlug","contactsOverrides","slug","formsSlug","formsOverrides","data","operation","req","payload","logger","warn","email","info","existingContactResult","find","collection","limit","where","equals","existingContact","docs","contactId","updateData","key","value","Object","entries","update","id","newContact","create","formId","form","formIdValue","error","Error","findByID","depth","Array","isArray","tags","length","formTagIds","map","t","currentContact","existingTagIds","newTagIds","filter","tagId","includes","mergedTags","contact"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CollectionAfterChangeHook } from 'payload';
|
|
2
|
+
import type { MobilizehubPluginConfig } from '../../../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates the autoresponse email hook for form submissions.
|
|
5
|
+
*
|
|
6
|
+
* This hook sends an automatic confirmation email to the form submitter
|
|
7
|
+
* when autoresponse is enabled on the form. It:
|
|
8
|
+
* 1. Checks if autoresponse is enabled on the form
|
|
9
|
+
* 2. Validates required autoresponse fields
|
|
10
|
+
* 3. Parses Lexical content to HTML
|
|
11
|
+
* 4. Renders through email template
|
|
12
|
+
* 5. Sends the email
|
|
13
|
+
*
|
|
14
|
+
* Errors are logged but never thrown to avoid failing the submission.
|
|
15
|
+
*/
|
|
16
|
+
export declare const createSendAutoresponseHook: (pluginConfig: MobilizehubPluginConfig) => CollectionAfterChangeHook;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { formatFromAddress } from '../../../utils/email.js';
|
|
2
|
+
import { parseLexicalContent } from '../../../utils/lexical.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates the autoresponse email hook for form submissions.
|
|
5
|
+
*
|
|
6
|
+
* This hook sends an automatic confirmation email to the form submitter
|
|
7
|
+
* when autoresponse is enabled on the form. It:
|
|
8
|
+
* 1. Checks if autoresponse is enabled on the form
|
|
9
|
+
* 2. Validates required autoresponse fields
|
|
10
|
+
* 3. Parses Lexical content to HTML
|
|
11
|
+
* 4. Renders through email template
|
|
12
|
+
* 5. Sends the email
|
|
13
|
+
*
|
|
14
|
+
* Errors are logged but never thrown to avoid failing the submission.
|
|
15
|
+
*/ export const createSendAutoresponseHook = (pluginConfig)=>{
|
|
16
|
+
const formsSlug = pluginConfig.formsOverrides?.slug || 'forms';
|
|
17
|
+
const contactsSlug = pluginConfig.contactsOverrides?.slug || 'contacts';
|
|
18
|
+
return async ({ doc, operation, req })=>{
|
|
19
|
+
// Only process on create
|
|
20
|
+
if (operation !== 'create') {
|
|
21
|
+
return doc;
|
|
22
|
+
}
|
|
23
|
+
const { payload } = req;
|
|
24
|
+
const logger = payload.logger;
|
|
25
|
+
try {
|
|
26
|
+
// Get form ID from submission
|
|
27
|
+
const formId = doc.form;
|
|
28
|
+
const formIdValue = typeof formId === 'object' ? formId.id : formId;
|
|
29
|
+
if (!formIdValue) {
|
|
30
|
+
return doc;
|
|
31
|
+
}
|
|
32
|
+
// Fetch form to get autoresponse configuration
|
|
33
|
+
const form = await payload.findByID({
|
|
34
|
+
id: formIdValue,
|
|
35
|
+
collection: formsSlug
|
|
36
|
+
});
|
|
37
|
+
if (!form) {
|
|
38
|
+
return doc;
|
|
39
|
+
}
|
|
40
|
+
// Check if autoresponse is enabled
|
|
41
|
+
const autoresponse = form.autoresponse;
|
|
42
|
+
if (!autoresponse?.enabled) {
|
|
43
|
+
return doc;
|
|
44
|
+
}
|
|
45
|
+
// Get contact email
|
|
46
|
+
const contactId = doc.contact;
|
|
47
|
+
if (!contactId) {
|
|
48
|
+
logger.warn('Form submission has no linked contact, skipping autoresponse');
|
|
49
|
+
return doc;
|
|
50
|
+
}
|
|
51
|
+
const contactIdValue = typeof contactId === 'object' ? contactId.id : contactId;
|
|
52
|
+
const contact = await payload.findByID({
|
|
53
|
+
id: contactIdValue,
|
|
54
|
+
collection: contactsSlug
|
|
55
|
+
});
|
|
56
|
+
const contactEmail = contact?.email;
|
|
57
|
+
if (!contactEmail) {
|
|
58
|
+
logger.warn(`Contact ${contactIdValue} has no email, skipping autoresponse`);
|
|
59
|
+
return doc;
|
|
60
|
+
}
|
|
61
|
+
// Validate required autoresponse fields
|
|
62
|
+
const { content, fromAddress, fromName, previewText, replyTo, subject } = autoresponse;
|
|
63
|
+
if (!subject) {
|
|
64
|
+
logger.warn(`Form ${formIdValue} autoresponse has no subject, skipping`);
|
|
65
|
+
return doc;
|
|
66
|
+
}
|
|
67
|
+
if (!content) {
|
|
68
|
+
logger.warn(`Form ${formIdValue} autoresponse has no content, skipping`);
|
|
69
|
+
return doc;
|
|
70
|
+
}
|
|
71
|
+
if (!fromName || !fromAddress) {
|
|
72
|
+
logger.warn(`Form ${formIdValue} autoresponse has no from address, skipping`);
|
|
73
|
+
return doc;
|
|
74
|
+
}
|
|
75
|
+
// Get email adapter
|
|
76
|
+
const { render, sendEmail } = pluginConfig.email(req);
|
|
77
|
+
// Parse Lexical content
|
|
78
|
+
const parsedContent = await parseLexicalContent(content, payload.config);
|
|
79
|
+
// Format from address
|
|
80
|
+
const formattedFromAddress = formatFromAddress(fromName, fromAddress);
|
|
81
|
+
// Render through email template
|
|
82
|
+
const html = render({
|
|
83
|
+
from: formattedFromAddress,
|
|
84
|
+
html: parsedContent.html,
|
|
85
|
+
markdown: parsedContent.markdown,
|
|
86
|
+
plainText: parsedContent.plainText,
|
|
87
|
+
previewText,
|
|
88
|
+
replyTo,
|
|
89
|
+
subject,
|
|
90
|
+
to: contactEmail,
|
|
91
|
+
token: ''
|
|
92
|
+
});
|
|
93
|
+
// Send the email
|
|
94
|
+
await sendEmail({
|
|
95
|
+
from: formattedFromAddress,
|
|
96
|
+
html,
|
|
97
|
+
subject,
|
|
98
|
+
to: contactEmail
|
|
99
|
+
});
|
|
100
|
+
logger.info(`Sent autoresponse to ${contactEmail} for form ${formIdValue}`);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
logger.error(error, 'Error sending autoresponse email');
|
|
103
|
+
// Don't throw - submission should still succeed
|
|
104
|
+
}
|
|
105
|
+
return doc;
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
//# sourceMappingURL=sendAutoresponse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/collections/form-submissions/hooks/sendAutoresponse.ts"],"sourcesContent":["import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'\nimport type { CollectionAfterChangeHook } from 'payload'\n\nimport type { MobilizehubPluginConfig } from '../../../types/index.js'\n\nimport { formatFromAddress } from '../../../utils/email.js'\nimport { parseLexicalContent } from '../../../utils/lexical.js'\n\n/**\n * Autoresponse configuration from form.\n */\ntype AutoresponseConfig = {\n content?: SerializedEditorState\n enabled?: boolean\n fromAddress?: string\n fromName?: string\n previewText?: string\n replyTo?: string\n subject?: string\n}\n\n/**\n * Creates the autoresponse email hook for form submissions.\n *\n * This hook sends an automatic confirmation email to the form submitter\n * when autoresponse is enabled on the form. It:\n * 1. Checks if autoresponse is enabled on the form\n * 2. Validates required autoresponse fields\n * 3. Parses Lexical content to HTML\n * 4. Renders through email template\n * 5. Sends the email\n *\n * Errors are logged but never thrown to avoid failing the submission.\n */\nexport const createSendAutoresponseHook = (\n pluginConfig: MobilizehubPluginConfig,\n): CollectionAfterChangeHook => {\n const formsSlug = pluginConfig.formsOverrides?.slug || 'forms'\n const contactsSlug = pluginConfig.contactsOverrides?.slug || 'contacts'\n\n return async ({ doc, operation, req }) => {\n // Only process on create\n if (operation !== 'create') {\n return doc\n }\n\n const { payload } = req\n const logger = payload.logger\n\n try {\n // Get form ID from submission\n const formId = doc.form as { id: number | string } | number | string\n const formIdValue = typeof formId === 'object' ? formId.id : formId\n\n if (!formIdValue) {\n return doc\n }\n\n // Fetch form to get autoresponse configuration\n const form = await payload.findByID({\n id: formIdValue,\n collection: formsSlug,\n })\n\n if (!form) {\n return doc\n }\n\n // Check if autoresponse is enabled\n const autoresponse = form.autoresponse as AutoresponseConfig | undefined\n\n if (!autoresponse?.enabled) {\n return doc\n }\n\n // Get contact email\n const contactId = doc.contact as { id: number | string } | number | string | undefined\n\n if (!contactId) {\n logger.warn('Form submission has no linked contact, skipping autoresponse')\n return doc\n }\n\n const contactIdValue = typeof contactId === 'object' ? contactId.id : contactId\n\n const contact = await payload.findByID({\n id: contactIdValue,\n collection: contactsSlug,\n })\n\n const contactEmail = contact?.email as string | undefined\n\n if (!contactEmail) {\n logger.warn(`Contact ${contactIdValue} has no email, skipping autoresponse`)\n return doc\n }\n\n // Validate required autoresponse fields\n const { content, fromAddress, fromName, previewText, replyTo, subject } = autoresponse\n\n if (!subject) {\n logger.warn(`Form ${formIdValue} autoresponse has no subject, skipping`)\n return doc\n }\n\n if (!content) {\n logger.warn(`Form ${formIdValue} autoresponse has no content, skipping`)\n return doc\n }\n\n if (!fromName || !fromAddress) {\n logger.warn(`Form ${formIdValue} autoresponse has no from address, skipping`)\n return doc\n }\n\n // Get email adapter\n const { render, sendEmail } = pluginConfig.email(req)\n\n // Parse Lexical content\n const parsedContent = await parseLexicalContent(content, payload.config)\n\n // Format from address\n const formattedFromAddress = formatFromAddress(fromName, fromAddress)\n\n // Render through email template\n const html = render({\n from: formattedFromAddress,\n html: parsedContent.html,\n markdown: parsedContent.markdown,\n plainText: parsedContent.plainText,\n previewText,\n replyTo,\n subject,\n to: contactEmail,\n token: '', // No unsubscribe token for autoresponse\n })\n\n // Send the email\n await sendEmail({\n from: formattedFromAddress,\n html,\n subject,\n to: contactEmail,\n })\n\n logger.info(`Sent autoresponse to ${contactEmail} for form ${formIdValue}`)\n } catch (error) {\n logger.error(error as Error, 'Error sending autoresponse email')\n // Don't throw - submission should still succeed\n }\n\n return doc\n }\n}\n"],"names":["formatFromAddress","parseLexicalContent","createSendAutoresponseHook","pluginConfig","formsSlug","formsOverrides","slug","contactsSlug","contactsOverrides","doc","operation","req","payload","logger","formId","form","formIdValue","id","findByID","collection","autoresponse","enabled","contactId","contact","warn","contactIdValue","contactEmail","email","content","fromAddress","fromName","previewText","replyTo","subject","render","sendEmail","parsedContent","config","formattedFromAddress","html","from","markdown","plainText","to","token","info","error"],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PayloadHandler } from 'payload';
|
|
2
|
+
import type { MobilizehubPluginConfig } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates the public form submission endpoint handler.
|
|
5
|
+
*
|
|
6
|
+
* Accepts form submissions from frontend applications, validates the data,
|
|
7
|
+
* creates a form submission record, and returns the appropriate confirmation.
|
|
8
|
+
*
|
|
9
|
+
* This endpoint is public (no authentication required) but validates:
|
|
10
|
+
* - Form exists and is published
|
|
11
|
+
* - Required fields are present
|
|
12
|
+
* - Email format is valid (if provided)
|
|
13
|
+
*/
|
|
14
|
+
export declare const formSubmissionHandler: (pluginConfig: MobilizehubPluginConfig) => PayloadHandler;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import { ErrorCodes, errorResponse, successResponse } from '../utils/api-response.js';
|
|
3
|
+
/**
|
|
4
|
+
* Schema for form submission request body.
|
|
5
|
+
*/ const FormSubmissionBodySchema = z.object({
|
|
6
|
+
data: z.record(z.string(), z.unknown()),
|
|
7
|
+
formId: z.union([
|
|
8
|
+
z.string(),
|
|
9
|
+
z.number()
|
|
10
|
+
])
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Validates submission data against form field configuration.
|
|
14
|
+
*/ function validateSubmissionData(data, contactFields) {
|
|
15
|
+
const errors = [];
|
|
16
|
+
if (!contactFields || contactFields.length === 0) {
|
|
17
|
+
return {
|
|
18
|
+
errors: [],
|
|
19
|
+
valid: true
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
for (const field of contactFields){
|
|
23
|
+
if (field.required) {
|
|
24
|
+
const value = data[field.blockType];
|
|
25
|
+
if (value === undefined || value === null || value === '') {
|
|
26
|
+
errors.push(`${field.blockType} is required`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Validate email format if present
|
|
31
|
+
if (data.email && typeof data.email === 'string') {
|
|
32
|
+
const emailRegex = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/;
|
|
33
|
+
if (!emailRegex.test(data.email)) {
|
|
34
|
+
errors.push('Invalid email format');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
errors,
|
|
39
|
+
valid: errors.length === 0
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Fetches and validates a form exists and is published.
|
|
44
|
+
*/ async function getPublishedForm(payload, formId, collectionSlug) {
|
|
45
|
+
try {
|
|
46
|
+
const form = await payload.findByID({
|
|
47
|
+
id: formId,
|
|
48
|
+
collection: collectionSlug
|
|
49
|
+
});
|
|
50
|
+
if (!form) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
// Only allow submissions to published forms
|
|
54
|
+
if (form.status !== 'published') {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return form;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Builds the confirmation response based on form settings.
|
|
64
|
+
*/ function buildConfirmationResponse(form) {
|
|
65
|
+
if (form.confirmationType === 'redirect' && form.reference) {
|
|
66
|
+
let redirectUrl;
|
|
67
|
+
if (form.url) {
|
|
68
|
+
redirectUrl = form.url;
|
|
69
|
+
} else if (typeof form.reference.value === 'object' && form.reference.value.slug) {
|
|
70
|
+
redirectUrl = `/${form.reference.value.slug}`;
|
|
71
|
+
}
|
|
72
|
+
if (redirectUrl) {
|
|
73
|
+
return {
|
|
74
|
+
type: 'redirect',
|
|
75
|
+
redirect: redirectUrl
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
type: 'message',
|
|
81
|
+
message: form.confirmationMessage || 'Thank you for your submission.'
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Creates the public form submission endpoint handler.
|
|
86
|
+
*
|
|
87
|
+
* Accepts form submissions from frontend applications, validates the data,
|
|
88
|
+
* creates a form submission record, and returns the appropriate confirmation.
|
|
89
|
+
*
|
|
90
|
+
* This endpoint is public (no authentication required) but validates:
|
|
91
|
+
* - Form exists and is published
|
|
92
|
+
* - Required fields are present
|
|
93
|
+
* - Email format is valid (if provided)
|
|
94
|
+
*/ export const formSubmissionHandler = (pluginConfig)=>{
|
|
95
|
+
const formsSlug = pluginConfig.formsOverrides?.slug || 'forms';
|
|
96
|
+
const formSubmissionsSlug = pluginConfig.formSubmissionsOverrides?.slug || 'formSubmissions';
|
|
97
|
+
return async (req)=>{
|
|
98
|
+
const { payload } = req;
|
|
99
|
+
const logger = payload.logger;
|
|
100
|
+
if (!req.json) {
|
|
101
|
+
return errorResponse(ErrorCodes.BAD_REQUEST, 'No JSON body provided', 400);
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const body = await req.json();
|
|
105
|
+
// Validate request body structure
|
|
106
|
+
const parseResult = FormSubmissionBodySchema.safeParse(body);
|
|
107
|
+
if (!parseResult.success) {
|
|
108
|
+
const firstError = parseResult.error.issues[0]?.message || 'Invalid request body';
|
|
109
|
+
return errorResponse(ErrorCodes.VALIDATION_ERROR, firstError, 400);
|
|
110
|
+
}
|
|
111
|
+
const { data, formId } = parseResult.data;
|
|
112
|
+
// Fetch and validate form
|
|
113
|
+
const form = await getPublishedForm(payload, formId, formsSlug);
|
|
114
|
+
if (!form) {
|
|
115
|
+
return errorResponse(ErrorCodes.NOT_FOUND, 'Form not found or not published', 404);
|
|
116
|
+
}
|
|
117
|
+
// Validate submission data against form fields
|
|
118
|
+
const validation = validateSubmissionData(data, form.contactFields);
|
|
119
|
+
if (!validation.valid) {
|
|
120
|
+
return errorResponse(ErrorCodes.VALIDATION_ERROR, validation.errors.join(', '), 400);
|
|
121
|
+
}
|
|
122
|
+
// Create the form submission
|
|
123
|
+
// Note: The beforeChange hook will handle contact creation/update
|
|
124
|
+
const submission = await payload.create({
|
|
125
|
+
collection: formSubmissionsSlug,
|
|
126
|
+
data: {
|
|
127
|
+
data,
|
|
128
|
+
form: form.id
|
|
129
|
+
},
|
|
130
|
+
// Use internal context to bypass access control
|
|
131
|
+
overrideAccess: true
|
|
132
|
+
});
|
|
133
|
+
// Build confirmation response
|
|
134
|
+
const confirmation = buildConfirmationResponse(form);
|
|
135
|
+
return successResponse({
|
|
136
|
+
confirmation,
|
|
137
|
+
submissionId: submission.id
|
|
138
|
+
}, 201);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
logger.error(error, 'Error processing form submission');
|
|
141
|
+
return errorResponse(ErrorCodes.INTERNAL_ERROR, error instanceof Error ? error.message : 'Failed to process submission', 500);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
//# sourceMappingURL=formSubmissionHandler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/formSubmissionHandler.ts"],"sourcesContent":["import type { CollectionSlug, Payload, PayloadHandler } from 'payload'\n\nimport z from 'zod'\n\nimport type { MobilizehubPluginConfig } from '../types/index.js'\n\nimport { ErrorCodes, errorResponse, successResponse } from '../utils/api-response.js'\n\n/**\n * Schema for form submission request body.\n */\nconst FormSubmissionBodySchema = z.object({\n data: z.record(z.string(), z.unknown()),\n formId: z.union([z.string(), z.number()]),\n})\n\n/**\n * Form type for validation.\n */\ntype FormDocument = {\n confirmationMessage?: unknown\n confirmationType?: 'message' | 'redirect'\n contactFields?: Array<{\n blockType: string\n required?: boolean\n }>\n id: number | string\n reference?: {\n relationTo: 'forms' | 'pages'\n value: { slug?: string } | number | string\n }\n status?: 'draft' | 'published'\n url?: string\n}\n\n/**\n * Validates submission data against form field configuration.\n */\nfunction validateSubmissionData(\n data: Record<string, unknown>,\n contactFields: FormDocument['contactFields'],\n): { errors: string[]; valid: boolean } {\n const errors: string[] = []\n\n if (!contactFields || contactFields.length === 0) {\n return { errors: [], valid: true }\n }\n\n for (const field of contactFields) {\n if (field.required) {\n const value = data[field.blockType]\n if (value === undefined || value === null || value === '') {\n errors.push(`${field.blockType} is required`)\n }\n }\n }\n\n // Validate email format if present\n if (data.email && typeof data.email === 'string') {\n const emailRegex = /^[^\\s@]+@[^\\s@][^\\s.@]*\\.[^\\s@]+$/\n if (!emailRegex.test(data.email)) {\n errors.push('Invalid email format')\n }\n }\n\n return {\n errors,\n valid: errors.length === 0,\n }\n}\n\n/**\n * Fetches and validates a form exists and is published.\n */\nasync function getPublishedForm(\n payload: Payload,\n formId: number | string,\n collectionSlug: CollectionSlug,\n): Promise<FormDocument | null> {\n try {\n const form = await payload.findByID({\n id: formId,\n collection: collectionSlug,\n })\n\n if (!form) {\n return null\n }\n\n // Only allow submissions to published forms\n if ((form as FormDocument).status !== 'published') {\n return null\n }\n\n return form as FormDocument\n } catch {\n return null\n }\n}\n\n/**\n * Builds the confirmation response based on form settings.\n */\nfunction buildConfirmationResponse(form: FormDocument): {\n message?: unknown\n redirect?: string\n type: 'message' | 'redirect'\n} {\n if (form.confirmationType === 'redirect' && form.reference) {\n let redirectUrl: string | undefined\n\n if (form.url) {\n redirectUrl = form.url\n } else if (typeof form.reference.value === 'object' && form.reference.value.slug) {\n redirectUrl = `/${form.reference.value.slug}`\n }\n\n if (redirectUrl) {\n return { type: 'redirect', redirect: redirectUrl }\n }\n }\n\n return {\n type: 'message',\n message: form.confirmationMessage || 'Thank you for your submission.',\n }\n}\n\n/**\n * Creates the public form submission endpoint handler.\n *\n * Accepts form submissions from frontend applications, validates the data,\n * creates a form submission record, and returns the appropriate confirmation.\n *\n * This endpoint is public (no authentication required) but validates:\n * - Form exists and is published\n * - Required fields are present\n * - Email format is valid (if provided)\n */\nexport const formSubmissionHandler = (\n pluginConfig: MobilizehubPluginConfig,\n): PayloadHandler => {\n const formsSlug = pluginConfig.formsOverrides?.slug || 'forms'\n const formSubmissionsSlug = pluginConfig.formSubmissionsOverrides?.slug || 'formSubmissions'\n\n return async (req) => {\n const { payload } = req\n const logger = payload.logger\n\n if (!req.json) {\n return errorResponse(ErrorCodes.BAD_REQUEST, 'No JSON body provided', 400)\n }\n\n try {\n const body = await req.json()\n\n // Validate request body structure\n const parseResult = FormSubmissionBodySchema.safeParse(body)\n\n if (!parseResult.success) {\n const firstError = parseResult.error.issues[0]?.message || 'Invalid request body'\n return errorResponse(ErrorCodes.VALIDATION_ERROR, firstError, 400)\n }\n\n const { data, formId } = parseResult.data\n\n // Fetch and validate form\n const form = await getPublishedForm(payload, formId, formsSlug)\n\n if (!form) {\n return errorResponse(ErrorCodes.NOT_FOUND, 'Form not found or not published', 404)\n }\n\n // Validate submission data against form fields\n const validation = validateSubmissionData(data, form.contactFields)\n\n if (!validation.valid) {\n return errorResponse(ErrorCodes.VALIDATION_ERROR, validation.errors.join(', '), 400)\n }\n\n // Create the form submission\n // Note: The beforeChange hook will handle contact creation/update\n const submission = await payload.create({\n collection: formSubmissionsSlug,\n data: {\n data,\n form: form.id as number,\n },\n // Use internal context to bypass access control\n overrideAccess: true,\n })\n\n // Build confirmation response\n const confirmation = buildConfirmationResponse(form)\n\n return successResponse(\n {\n confirmation,\n submissionId: submission.id,\n },\n 201,\n )\n } catch (error) {\n logger.error(error as Error, 'Error processing form submission')\n return errorResponse(\n ErrorCodes.INTERNAL_ERROR,\n error instanceof Error ? error.message : 'Failed to process submission',\n 500,\n )\n }\n }\n}\n"],"names":["z","ErrorCodes","errorResponse","successResponse","FormSubmissionBodySchema","object","data","record","string","unknown","formId","union","number","validateSubmissionData","contactFields","errors","length","valid","field","required","value","blockType","undefined","push","email","emailRegex","test","getPublishedForm","payload","collectionSlug","form","findByID","id","collection","status","buildConfirmationResponse","confirmationType","reference","redirectUrl","url","slug","type","redirect","message","confirmationMessage","formSubmissionHandler","pluginConfig","formsSlug","formsOverrides","formSubmissionsSlug","formSubmissionsOverrides","req","logger","json","BAD_REQUEST","body","parseResult","safeParse","success","firstError","error","issues","VALIDATION_ERROR","NOT_FOUND","validation","join","submission","create","overrideAccess","confirmation","submissionId","INTERNAL_ERROR","Error"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/sendBroadcastHandler.ts"],"sourcesContent":["import type { Payload, PayloadHandler, Where } from 'payload'\n\nimport { ErrorCodes, errorResponse, successResponse } from '../utils/api-response.js'\n\ntype SendBroadcastBody = {\n broadcastId?: unknown\n}\n\ntype Broadcast = {\n id: number | string\n status?: string\n tags?: Array<{ id: number | string } | number | string>\n to?: string\n}\n\n/**\n * Validates the request body contains a valid broadcastId.\n */\nfunction validateRequestBody(\n body: SendBroadcastBody,\n): { broadcastId: number | string; success: true } | { error: string; success: false } {\n if (!body.broadcastId) {\n return { error: 'broadcastId is required', success: false }\n }\n return { broadcastId: body.broadcastId as number | string, success: true }\n}\n\n/**\n * Fetches a broadcast by ID.\n * Returns null if the broadcast doesn't exist.\n */\nasync function findBroadcastById(\n payload: Payload,\n broadcastId: number | string,\n): Promise<Broadcast | null> {\n try {\n const broadcast = await payload.findByID({\n id: broadcastId,\n collection: 'broadcasts',\n })\n return broadcast as Broadcast | null\n } catch {\n return null\n }\n}\n\n/**\n * Builds the where clause for counting eligible contacts.\n * Adds tag filtering if the broadcast targets specific tags.\n */\nfunction buildContactsWhereClause(broadcast: Broadcast): Where {\n const whereClause: Where = {\n emailOptIn: { equals: true },\n }\n\n if (broadcast.to === 'tags' && Array.isArray(broadcast.tags) && broadcast.tags.length > 0) {\n const tagIds = broadcast.tags.map((t) => (typeof t === 'object' ? t.id : t))\n whereClause.tags = { in: tagIds }\n }\n\n return whereClause\n}\n\n/**\n * Creates the send-broadcast endpoint handler.\n *\n * Initiates sending a broadcast by validating the broadcast is in draft status,\n * counting eligible contacts, and updating the status to 'sending'. The actual\n * email delivery is handled by the send-broadcasts scheduled task.\n */\nexport const sendBroadcastHandler = (): PayloadHandler => {\n return async (req) => {\n const { payload } = req\n const logger = payload.logger\n\n if (!req.json) {\n return errorResponse(ErrorCodes.BAD_REQUEST, 'No JSON body provided', 400)\n }\n\n if (!req.user) {\n return errorResponse(ErrorCodes.UNAUTHORIZED, 'Unauthorized', 401)\n }\n\n try {\n const body = (await req.json()) as SendBroadcastBody\n\n const validation = validateRequestBody(body)\n if (!validation.success) {\n logger.error(validation.error)\n return errorResponse(ErrorCodes.BAD_REQUEST, validation.error, 400)\n }\n\n const { broadcastId } = validation\n\n const broadcast = await findBroadcastById(payload, broadcastId)\n\n if (!broadcast) {\n logger.error(`Broadcast ${broadcastId} not found`)\n return errorResponse(ErrorCodes.BROADCAST_NOT_FOUND, 'Broadcast not found', 404)\n }\n\n if (broadcast.status !== 'draft') {\n logger.error(`Broadcast ${broadcastId} is not in draft status (current: ${broadcast.status})
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/sendBroadcastHandler.ts"],"sourcesContent":["import type { Payload, PayloadHandler, Where } from 'payload'\n\nimport { ErrorCodes, errorResponse, successResponse } from '../utils/api-response.js'\n\ntype SendBroadcastBody = {\n broadcastId?: unknown\n}\n\ntype Broadcast = {\n id: number | string\n status?: string\n tags?: Array<{ id: number | string } | number | string>\n to?: string\n}\n\n/**\n * Validates the request body contains a valid broadcastId.\n */\nfunction validateRequestBody(\n body: SendBroadcastBody,\n): { broadcastId: number | string; success: true } | { error: string; success: false } {\n if (!body.broadcastId) {\n return { error: 'broadcastId is required', success: false }\n }\n return { broadcastId: body.broadcastId as number | string, success: true }\n}\n\n/**\n * Fetches a broadcast by ID.\n * Returns null if the broadcast doesn't exist.\n */\nasync function findBroadcastById(\n payload: Payload,\n broadcastId: number | string,\n): Promise<Broadcast | null> {\n try {\n const broadcast = await payload.findByID({\n id: broadcastId,\n collection: 'broadcasts',\n })\n return broadcast as Broadcast | null\n } catch {\n return null\n }\n}\n\n/**\n * Builds the where clause for counting eligible contacts.\n * Adds tag filtering if the broadcast targets specific tags.\n */\nfunction buildContactsWhereClause(broadcast: Broadcast): Where {\n const whereClause: Where = {\n emailOptIn: { equals: true },\n }\n\n if (broadcast.to === 'tags' && Array.isArray(broadcast.tags) && broadcast.tags.length > 0) {\n const tagIds = broadcast.tags.map((t) => (typeof t === 'object' ? t.id : t))\n whereClause.tags = { in: tagIds }\n }\n\n return whereClause\n}\n\n/**\n * Creates the send-broadcast endpoint handler.\n *\n * Initiates sending a broadcast by validating the broadcast is in draft status,\n * counting eligible contacts, and updating the status to 'sending'. The actual\n * email delivery is handled by the send-broadcasts scheduled task.\n */\nexport const sendBroadcastHandler = (): PayloadHandler => {\n return async (req) => {\n const { payload } = req\n const logger = payload.logger\n\n if (!req.json) {\n return errorResponse(ErrorCodes.BAD_REQUEST, 'No JSON body provided', 400)\n }\n\n if (!req.user) {\n return errorResponse(ErrorCodes.UNAUTHORIZED, 'Unauthorized', 401)\n }\n\n try {\n const body = (await req.json()) as SendBroadcastBody\n\n const validation = validateRequestBody(body)\n if (!validation.success) {\n logger.error(validation.error)\n return errorResponse(ErrorCodes.BAD_REQUEST, validation.error, 400)\n }\n\n const { broadcastId } = validation\n\n const broadcast = await findBroadcastById(payload, broadcastId)\n\n if (!broadcast) {\n logger.error(`Broadcast ${broadcastId} not found`)\n return errorResponse(ErrorCodes.BROADCAST_NOT_FOUND, 'Broadcast not found', 404)\n }\n\n if (broadcast.status !== 'draft') {\n logger.error(\n `Broadcast ${broadcastId} is not in draft status (current: ${broadcast.status})`,\n )\n return errorResponse(\n ErrorCodes.VALIDATION_ERROR,\n 'Broadcast must be in draft status to send',\n 400,\n )\n }\n\n const whereClause = buildContactsWhereClause(broadcast)\n\n const contactsCount = await payload.db.count({\n collection: 'contacts',\n where: whereClause,\n })\n\n await payload.update({\n id: broadcastId,\n collection: 'broadcasts',\n data: {\n meta: {\n contactsCount: contactsCount.totalDocs,\n lastProcessedContactId: 0,\n processedCount: 0,\n },\n status: 'sending',\n },\n })\n\n logger.info(`Broadcast ${broadcastId} queued for sending`)\n\n return successResponse({ message: 'Broadcast queued for sending' }, 200)\n } catch (err) {\n logger.error(err, 'Error occurred while queueing broadcast')\n return errorResponse(ErrorCodes.INTERNAL_ERROR, 'Error queueing broadcast', 500)\n }\n }\n}\n"],"names":["ErrorCodes","errorResponse","successResponse","validateRequestBody","body","broadcastId","error","success","findBroadcastById","payload","broadcast","findByID","id","collection","buildContactsWhereClause","whereClause","emailOptIn","equals","to","Array","isArray","tags","length","tagIds","map","t","in","sendBroadcastHandler","req","logger","json","BAD_REQUEST","user","UNAUTHORIZED","validation","BROADCAST_NOT_FOUND","status","VALIDATION_ERROR","contactsCount","db","count","where","update","data","meta","totalDocs","lastProcessedContactId","processedCount","info","message","err","INTERNAL_ERROR"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { generatePagesCollection } from './collections/pages/generatePagesCollec
|
|
|
7
7
|
import { generateTagsCollection } from './collections/tags/generateTagsCollection.js';
|
|
8
8
|
import { generateUnsubscribeTokensCollection } from './collections/unsubscribe-tokens/generateUnsubscribeTokens.js';
|
|
9
9
|
import { emailWebhookHandler } from './endpoints/emailWebhookHandler.js';
|
|
10
|
+
import { formSubmissionHandler } from './endpoints/formSubmissionHandler.js';
|
|
10
11
|
import { sendBroadcastHandler } from './endpoints/sendBroadcastHandler.js';
|
|
11
12
|
import { sendTestEmailHandler } from './endpoints/sendTestBroadcastHandler.js';
|
|
12
13
|
import { unsubscribeHandler } from './endpoints/unsubscribeHandler.js';
|
|
@@ -44,6 +45,11 @@ export const mobilizehubPlugin = (pluginOptions)=>(config)=>{
|
|
|
44
45
|
handler: emailWebhookHandler(pluginOptions),
|
|
45
46
|
method: 'post',
|
|
46
47
|
path: '/webhooks/email'
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
handler: formSubmissionHandler(pluginOptions),
|
|
51
|
+
method: 'post',
|
|
52
|
+
path: '/forms.createSubmission'
|
|
47
53
|
}
|
|
48
54
|
];
|
|
49
55
|
config.endpoints = [
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Config, Endpoint, TaskConfig } from 'payload'\n\nimport type { MobilizehubPluginConfig } from './types/index.js'\n\nimport { generateBroadcastsCollection } from './collections/broadcasts/generateBroadcastsCollection.js'\nimport { generateContactsCollection } from './collections/contacts/generateContactsCollection.js'\nimport { generateEmailsCollection } from './collections/emails/generateEmailsCollection.js'\nimport { generateFormSubmissionsCollection } from './collections/form-submissions/generateFormSubmissionsCollection.js'\nimport { generateFormsCollection } from './collections/forms/generateFormsCollection.js'\nimport { generatePagesCollection } from './collections/pages/generatePagesCollection.js'\nimport { generateTagsCollection } from './collections/tags/generateTagsCollection.js'\nimport { generateUnsubscribeTokensCollection } from './collections/unsubscribe-tokens/generateUnsubscribeTokens.js'\nimport { emailWebhookHandler } from './endpoints/emailWebhookHandler.js'\nimport { sendBroadcastHandler } from './endpoints/sendBroadcastHandler.js'\nimport { sendTestEmailHandler } from './endpoints/sendTestBroadcastHandler.js'\nimport { unsubscribeHandler } from './endpoints/unsubscribeHandler.js'\nimport { createSendBroadcastsTask } from './tasks/sendBroadcastsTask.js'\nimport { createSendEmailTask } from './tasks/sendEmailTask.js'\n\nexport * from './types/index.js'\n\nexport const mobilizehubPlugin =\n (pluginOptions: MobilizehubPluginConfig) =>\n (config: Config): Config => {\n if (!config.collections) {\n config.collections = []\n }\n\n config.collections.push(\n generateTagsCollection(pluginOptions),\n generateContactsCollection(pluginOptions),\n generateBroadcastsCollection(pluginOptions),\n generateEmailsCollection(pluginOptions),\n generatePagesCollection(pluginOptions),\n generateUnsubscribeTokensCollection(),\n generateFormSubmissionsCollection(pluginOptions),\n generateFormsCollection(pluginOptions),\n )\n\n if (pluginOptions.disabled) {\n return config\n }\n\n if (!config.endpoints) {\n config.endpoints = []\n }\n\n const endpoints: Endpoint[] = [\n {\n handler: sendBroadcastHandler(),\n method: 'post',\n path: '/send-broadcast',\n },\n {\n handler: sendTestEmailHandler(pluginOptions),\n method: 'post',\n path: '/send-test-email',\n },\n {\n handler: unsubscribeHandler(),\n method: 'post',\n path: '/unsubscribe',\n },\n {\n handler: emailWebhookHandler(pluginOptions),\n method: 'post',\n path: '/webhooks/email',\n },\n ]\n\n config.endpoints = [...config.endpoints, ...endpoints]\n\n if (!config.jobs) {\n config.jobs = {\n tasks: [],\n }\n }\n\n const tasks: TaskConfig[] = [\n createSendBroadcastsTask(pluginOptions),\n createSendEmailTask(pluginOptions),\n ]\n\n config.jobs.tasks = [...(config.jobs.tasks ?? []), ...tasks]\n\n const incomingOnInit = config.onInit\n\n config.onInit = async (payload) => {\n if (incomingOnInit) {\n await incomingOnInit(payload)\n }\n }\n\n return config\n }\n"],"names":["generateBroadcastsCollection","generateContactsCollection","generateEmailsCollection","generateFormSubmissionsCollection","generateFormsCollection","generatePagesCollection","generateTagsCollection","generateUnsubscribeTokensCollection","emailWebhookHandler","sendBroadcastHandler","sendTestEmailHandler","unsubscribeHandler","createSendBroadcastsTask","createSendEmailTask","mobilizehubPlugin","pluginOptions","config","collections","push","disabled","endpoints","handler","method","path","jobs","tasks","incomingOnInit","onInit","payload"],"mappings":"AAIA,SAASA,4BAA4B,QAAQ,2DAA0D;AACvG,SAASC,0BAA0B,QAAQ,uDAAsD;AACjG,SAASC,wBAAwB,QAAQ,mDAAkD;AAC3F,SAASC,iCAAiC,QAAQ,sEAAqE;AACvH,SAASC,uBAAuB,QAAQ,iDAAgD;AACxF,SAASC,uBAAuB,QAAQ,iDAAgD;AACxF,SAASC,sBAAsB,QAAQ,+CAA8C;AACrF,SAASC,mCAAmC,QAAQ,gEAA+D;AACnH,SAASC,mBAAmB,QAAQ,qCAAoC;AACxE,SAASC,oBAAoB,QAAQ,sCAAqC;AAC1E,SAASC,oBAAoB,QAAQ,0CAAyC;AAC9E,SAASC,kBAAkB,QAAQ,oCAAmC;AACtE,SAASC,wBAAwB,QAAQ,gCAA+B;AACxE,SAASC,mBAAmB,QAAQ,2BAA0B;AAE9D,cAAc,mBAAkB;AAEhC,OAAO,MAAMC,oBACX,CAACC,gBACD,CAACC;QACC,IAAI,CAACA,OAAOC,WAAW,EAAE;YACvBD,OAAOC,WAAW,GAAG,EAAE;QACzB;QAEAD,OAAOC,WAAW,CAACC,IAAI,
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Config, Endpoint, TaskConfig } from 'payload'\n\nimport type { MobilizehubPluginConfig } from './types/index.js'\n\nimport { generateBroadcastsCollection } from './collections/broadcasts/generateBroadcastsCollection.js'\nimport { generateContactsCollection } from './collections/contacts/generateContactsCollection.js'\nimport { generateEmailsCollection } from './collections/emails/generateEmailsCollection.js'\nimport { generateFormSubmissionsCollection } from './collections/form-submissions/generateFormSubmissionsCollection.js'\nimport { generateFormsCollection } from './collections/forms/generateFormsCollection.js'\nimport { generatePagesCollection } from './collections/pages/generatePagesCollection.js'\nimport { generateTagsCollection } from './collections/tags/generateTagsCollection.js'\nimport { generateUnsubscribeTokensCollection } from './collections/unsubscribe-tokens/generateUnsubscribeTokens.js'\nimport { emailWebhookHandler } from './endpoints/emailWebhookHandler.js'\nimport { formSubmissionHandler } from './endpoints/formSubmissionHandler.js'\nimport { sendBroadcastHandler } from './endpoints/sendBroadcastHandler.js'\nimport { sendTestEmailHandler } from './endpoints/sendTestBroadcastHandler.js'\nimport { unsubscribeHandler } from './endpoints/unsubscribeHandler.js'\nimport { createSendBroadcastsTask } from './tasks/sendBroadcastsTask.js'\nimport { createSendEmailTask } from './tasks/sendEmailTask.js'\n\nexport * from './types/index.js'\n\nexport const mobilizehubPlugin =\n (pluginOptions: MobilizehubPluginConfig) =>\n (config: Config): Config => {\n if (!config.collections) {\n config.collections = []\n }\n\n config.collections.push(\n generateTagsCollection(pluginOptions),\n generateContactsCollection(pluginOptions),\n generateBroadcastsCollection(pluginOptions),\n generateEmailsCollection(pluginOptions),\n generatePagesCollection(pluginOptions),\n generateUnsubscribeTokensCollection(),\n generateFormSubmissionsCollection(pluginOptions),\n generateFormsCollection(pluginOptions),\n )\n\n if (pluginOptions.disabled) {\n return config\n }\n\n if (!config.endpoints) {\n config.endpoints = []\n }\n\n const endpoints: Endpoint[] = [\n {\n handler: sendBroadcastHandler(),\n method: 'post',\n path: '/send-broadcast',\n },\n {\n handler: sendTestEmailHandler(pluginOptions),\n method: 'post',\n path: '/send-test-email',\n },\n {\n handler: unsubscribeHandler(),\n method: 'post',\n path: '/unsubscribe',\n },\n {\n handler: emailWebhookHandler(pluginOptions),\n method: 'post',\n path: '/webhooks/email',\n },\n {\n handler: formSubmissionHandler(pluginOptions),\n method: 'post',\n path: '/forms.createSubmission',\n },\n ]\n\n config.endpoints = [...config.endpoints, ...endpoints]\n\n if (!config.jobs) {\n config.jobs = {\n tasks: [],\n }\n }\n\n const tasks: TaskConfig[] = [\n createSendBroadcastsTask(pluginOptions),\n createSendEmailTask(pluginOptions),\n ]\n\n config.jobs.tasks = [...(config.jobs.tasks ?? []), ...tasks]\n\n const incomingOnInit = config.onInit\n\n config.onInit = async (payload) => {\n if (incomingOnInit) {\n await incomingOnInit(payload)\n }\n }\n\n return config\n }\n"],"names":["generateBroadcastsCollection","generateContactsCollection","generateEmailsCollection","generateFormSubmissionsCollection","generateFormsCollection","generatePagesCollection","generateTagsCollection","generateUnsubscribeTokensCollection","emailWebhookHandler","formSubmissionHandler","sendBroadcastHandler","sendTestEmailHandler","unsubscribeHandler","createSendBroadcastsTask","createSendEmailTask","mobilizehubPlugin","pluginOptions","config","collections","push","disabled","endpoints","handler","method","path","jobs","tasks","incomingOnInit","onInit","payload"],"mappings":"AAIA,SAASA,4BAA4B,QAAQ,2DAA0D;AACvG,SAASC,0BAA0B,QAAQ,uDAAsD;AACjG,SAASC,wBAAwB,QAAQ,mDAAkD;AAC3F,SAASC,iCAAiC,QAAQ,sEAAqE;AACvH,SAASC,uBAAuB,QAAQ,iDAAgD;AACxF,SAASC,uBAAuB,QAAQ,iDAAgD;AACxF,SAASC,sBAAsB,QAAQ,+CAA8C;AACrF,SAASC,mCAAmC,QAAQ,gEAA+D;AACnH,SAASC,mBAAmB,QAAQ,qCAAoC;AACxE,SAASC,qBAAqB,QAAQ,uCAAsC;AAC5E,SAASC,oBAAoB,QAAQ,sCAAqC;AAC1E,SAASC,oBAAoB,QAAQ,0CAAyC;AAC9E,SAASC,kBAAkB,QAAQ,oCAAmC;AACtE,SAASC,wBAAwB,QAAQ,gCAA+B;AACxE,SAASC,mBAAmB,QAAQ,2BAA0B;AAE9D,cAAc,mBAAkB;AAEhC,OAAO,MAAMC,oBACX,CAACC,gBACD,CAACC;QACC,IAAI,CAACA,OAAOC,WAAW,EAAE;YACvBD,OAAOC,WAAW,GAAG,EAAE;QACzB;QAEAD,OAAOC,WAAW,CAACC,IAAI,CACrBb,uBAAuBU,gBACvBf,2BAA2Be,gBAC3BhB,6BAA6BgB,gBAC7Bd,yBAAyBc,gBACzBX,wBAAwBW,gBACxBT,uCACAJ,kCAAkCa,gBAClCZ,wBAAwBY;QAG1B,IAAIA,cAAcI,QAAQ,EAAE;YAC1B,OAAOH;QACT;QAEA,IAAI,CAACA,OAAOI,SAAS,EAAE;YACrBJ,OAAOI,SAAS,GAAG,EAAE;QACvB;QAEA,MAAMA,YAAwB;YAC5B;gBACEC,SAASZ;gBACTa,QAAQ;gBACRC,MAAM;YACR;YACA;gBACEF,SAASX,qBAAqBK;gBAC9BO,QAAQ;gBACRC,MAAM;YACR;YACA;gBACEF,SAASV;gBACTW,QAAQ;gBACRC,MAAM;YACR;YACA;gBACEF,SAASd,oBAAoBQ;gBAC7BO,QAAQ;gBACRC,MAAM;YACR;YACA;gBACEF,SAASb,sBAAsBO;gBAC/BO,QAAQ;gBACRC,MAAM;YACR;SACD;QAEDP,OAAOI,SAAS,GAAG;eAAIJ,OAAOI,SAAS;eAAKA;SAAU;QAEtD,IAAI,CAACJ,OAAOQ,IAAI,EAAE;YAChBR,OAAOQ,IAAI,GAAG;gBACZC,OAAO,EAAE;YACX;QACF;QAEA,MAAMA,QAAsB;YAC1Bb,yBAAyBG;YACzBF,oBAAoBE;SACrB;QAEDC,OAAOQ,IAAI,CAACC,KAAK,GAAG;eAAKT,OAAOQ,IAAI,CAACC,KAAK,IAAI,EAAE;eAAMA;SAAM;QAE5D,MAAMC,iBAAiBV,OAAOW,MAAM;QAEpCX,OAAOW,MAAM,GAAG,OAAOC;YACrB,IAAIF,gBAAgB;gBAClB,MAAMA,eAAeE;YACvB;QACF;QAEA,OAAOZ;IACT,EAAC"}
|
package/dist/react/index.d.ts
CHANGED
package/dist/react/index.js
CHANGED
package/dist/react/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/index.ts"],"sourcesContent":["export { confirmUnsubscribe } from './unsubscribe.js'\n"],"names":["confirmUnsubscribe"],"mappings":"AAAA,SAASA,kBAAkB,QAAQ,mBAAkB"}
|
|
1
|
+
{"version":3,"sources":["../../src/react/index.ts"],"sourcesContent":["export { submitForm } from './submit-form.js'\nexport { confirmUnsubscribe } from './unsubscribe.js'\n"],"names":["submitForm","confirmUnsubscribe"],"mappings":"AAAA,SAASA,UAAU,QAAQ,mBAAkB;AAC7C,SAASC,kBAAkB,QAAQ,mBAAkB"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for handling form submission responses.
|
|
3
|
+
*/
|
|
4
|
+
type SubmitFormOptions = {
|
|
5
|
+
/**
|
|
6
|
+
* Called when the form returns a message confirmation.
|
|
7
|
+
* @param message - The confirmation message (may be a RichText object)
|
|
8
|
+
*/
|
|
9
|
+
onMessage?: (message: unknown) => void;
|
|
10
|
+
/**
|
|
11
|
+
* Called when the form should redirect after submission.
|
|
12
|
+
* @param redirect - The URL to redirect to
|
|
13
|
+
*/
|
|
14
|
+
onRedirect?: (redirect: string) => void;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Submits a form to the backend API.
|
|
18
|
+
*
|
|
19
|
+
* @param args - Form submission arguments
|
|
20
|
+
* @param args.formId - The ID of the form to submit to
|
|
21
|
+
* @param args.data - The form data to submit
|
|
22
|
+
* @param args.opts - Optional callbacks for handling the response
|
|
23
|
+
* @returns The submission data including confirmation and submissionId
|
|
24
|
+
* @throws Error if the submission fails
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* const handleSubmit = async (formData: Record<string, unknown>) => {
|
|
29
|
+
* try {
|
|
30
|
+
* const result = await submitForm({
|
|
31
|
+
* formId: '1',
|
|
32
|
+
* data: formData,
|
|
33
|
+
* opts: {
|
|
34
|
+
* onRedirect: (url) => router.push(url),
|
|
35
|
+
* onMessage: (message) => setConfirmation(message),
|
|
36
|
+
* },
|
|
37
|
+
* })
|
|
38
|
+
* console.log('Submission ID:', result.submissionId)
|
|
39
|
+
* } catch (error) {
|
|
40
|
+
* console.error('Form submission failed:', error)
|
|
41
|
+
* }
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function submitForm(args: {
|
|
46
|
+
data: Record<string, unknown>;
|
|
47
|
+
formId: number | string;
|
|
48
|
+
opts?: SubmitFormOptions;
|
|
49
|
+
}): Promise<{
|
|
50
|
+
confirmation: {
|
|
51
|
+
message?: unknown;
|
|
52
|
+
redirect?: string;
|
|
53
|
+
type: "message" | "redirect";
|
|
54
|
+
};
|
|
55
|
+
submissionId: number | string;
|
|
56
|
+
} | undefined>;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const apiUrl = '/api/forms.createSubmission';
|
|
2
|
+
/**
|
|
3
|
+
* Submits a form to the backend API.
|
|
4
|
+
*
|
|
5
|
+
* @param args - Form submission arguments
|
|
6
|
+
* @param args.formId - The ID of the form to submit to
|
|
7
|
+
* @param args.data - The form data to submit
|
|
8
|
+
* @param args.opts - Optional callbacks for handling the response
|
|
9
|
+
* @returns The submission data including confirmation and submissionId
|
|
10
|
+
* @throws Error if the submission fails
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* const handleSubmit = async (formData: Record<string, unknown>) => {
|
|
15
|
+
* try {
|
|
16
|
+
* const result = await submitForm({
|
|
17
|
+
* formId: '1',
|
|
18
|
+
* data: formData,
|
|
19
|
+
* opts: {
|
|
20
|
+
* onRedirect: (url) => router.push(url),
|
|
21
|
+
* onMessage: (message) => setConfirmation(message),
|
|
22
|
+
* },
|
|
23
|
+
* })
|
|
24
|
+
* console.log('Submission ID:', result.submissionId)
|
|
25
|
+
* } catch (error) {
|
|
26
|
+
* console.error('Form submission failed:', error)
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/ export async function submitForm(args) {
|
|
31
|
+
const response = await fetch(apiUrl, {
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
data: args.data,
|
|
34
|
+
formId: args.formId
|
|
35
|
+
}),
|
|
36
|
+
headers: {
|
|
37
|
+
'Content-Type': 'application/json'
|
|
38
|
+
},
|
|
39
|
+
method: 'POST'
|
|
40
|
+
});
|
|
41
|
+
const result = await response.json();
|
|
42
|
+
if (!result.success) {
|
|
43
|
+
throw new Error(result.error?.message || 'Submission failed');
|
|
44
|
+
}
|
|
45
|
+
if (result.data?.confirmation.type === 'redirect' && result.data.confirmation.redirect) {
|
|
46
|
+
args.opts?.onRedirect?.(result.data.confirmation.redirect);
|
|
47
|
+
}
|
|
48
|
+
if (result.data?.confirmation.type === 'message') {
|
|
49
|
+
args.opts?.onMessage?.(result.data.confirmation.message);
|
|
50
|
+
}
|
|
51
|
+
return result.data;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=submit-form.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/react/submit-form.ts"],"sourcesContent":["const apiUrl = '/api/forms.createSubmission'\n\n/**\n * Response type from the form submission API.\n */\ntype FormSubmissionResponse = {\n data?: {\n confirmation: {\n message?: unknown\n redirect?: string\n type: 'message' | 'redirect'\n }\n submissionId: number | string\n }\n error?: {\n code: string\n message: string\n }\n success: boolean\n}\n\n/**\n * Options for handling form submission responses.\n */\ntype SubmitFormOptions = {\n /**\n * Called when the form returns a message confirmation.\n * @param message - The confirmation message (may be a RichText object)\n */\n onMessage?: (message: unknown) => void\n /**\n * Called when the form should redirect after submission.\n * @param redirect - The URL to redirect to\n */\n onRedirect?: (redirect: string) => void\n}\n\n/**\n * Submits a form to the backend API.\n *\n * @param args - Form submission arguments\n * @param args.formId - The ID of the form to submit to\n * @param args.data - The form data to submit\n * @param args.opts - Optional callbacks for handling the response\n * @returns The submission data including confirmation and submissionId\n * @throws Error if the submission fails\n *\n * @example\n * ```tsx\n * const handleSubmit = async (formData: Record<string, unknown>) => {\n * try {\n * const result = await submitForm({\n * formId: '1',\n * data: formData,\n * opts: {\n * onRedirect: (url) => router.push(url),\n * onMessage: (message) => setConfirmation(message),\n * },\n * })\n * console.log('Submission ID:', result.submissionId)\n * } catch (error) {\n * console.error('Form submission failed:', error)\n * }\n * }\n * ```\n */\nexport async function submitForm(args: {\n data: Record<string, unknown>\n formId: number | string\n opts?: SubmitFormOptions\n}) {\n const response = await fetch(apiUrl, {\n body: JSON.stringify({ data: args.data, formId: args.formId }),\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n })\n\n const result: FormSubmissionResponse = await response.json()\n\n if (!result.success) {\n throw new Error(result.error?.message || 'Submission failed')\n }\n\n if (result.data?.confirmation.type === 'redirect' && result.data.confirmation.redirect) {\n args.opts?.onRedirect?.(result.data.confirmation.redirect)\n }\n\n if (result.data?.confirmation.type === 'message') {\n args.opts?.onMessage?.(result.data.confirmation.message)\n }\n\n return result.data\n}\n"],"names":["apiUrl","submitForm","args","response","fetch","body","JSON","stringify","data","formId","headers","method","result","json","success","Error","error","message","confirmation","type","redirect","opts","onRedirect","onMessage"],"mappings":"AAAA,MAAMA,SAAS;AAqCf;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,eAAeC,WAAWC,IAIhC;IACC,MAAMC,WAAW,MAAMC,MAAMJ,QAAQ;QACnCK,MAAMC,KAAKC,SAAS,CAAC;YAAEC,MAAMN,KAAKM,IAAI;YAAEC,QAAQP,KAAKO,MAAM;QAAC;QAC5DC,SAAS;YACP,gBAAgB;QAClB;QACAC,QAAQ;IACV;IAEA,MAAMC,SAAiC,MAAMT,SAASU,IAAI;IAE1D,IAAI,CAACD,OAAOE,OAAO,EAAE;QACnB,MAAM,IAAIC,MAAMH,OAAOI,KAAK,EAAEC,WAAW;IAC3C;IAEA,IAAIL,OAAOJ,IAAI,EAAEU,aAAaC,SAAS,cAAcP,OAAOJ,IAAI,CAACU,YAAY,CAACE,QAAQ,EAAE;QACtFlB,KAAKmB,IAAI,EAAEC,aAAaV,OAAOJ,IAAI,CAACU,YAAY,CAACE,QAAQ;IAC3D;IAEA,IAAIR,OAAOJ,IAAI,EAAEU,aAAaC,SAAS,WAAW;QAChDjB,KAAKmB,IAAI,EAAEE,YAAYX,OAAOJ,IAAI,CAACU,YAAY,CAACD,OAAO;IACzD;IAEA,OAAOL,OAAOJ,IAAI;AACpB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tasks/sendBroadcastsTask.ts"],"sourcesContent":["import type { TaskConfig, Where } from 'payload'\n\nimport z from 'zod'\n\nimport type { MobilizehubPluginConfig } from '../types/index.js'\n\n/**\n * Schema for validating broadcast documents before processing.\n */\nconst BroadcastSchema = z.object({\n id: z.number(),\n content: z.any().refine((val) => val !== undefined && val !== null, {\n message: 'Broadcast content is missing',\n }),\n fromAddress: z.email({ message: 'Invalid from address email format' }),\n fromName: z.string().min(1, { message: 'From name is required' }),\n meta: z.object({\n contactsCount: z.number().min(0),\n lastProcessedContactId: z.number().min(0).default(0),\n processedCount: z.number().min(0),\n }),\n subject: z.string().min(1, { message: 'Subject is required' }),\n tags: z.array(z.any()).optional(),\n to: z.literal('all').or(z.literal('tags')),\n})\n\ntype ParsedBroadcast = z.infer<typeof BroadcastSchema>\n\n/**\n * Validates a broadcast document against the schema.\n * Returns typed data on success, or flattened field errors on failure.\n */\nfunction safeParseBroadcast(broadcast: unknown) {\n const result = BroadcastSchema.safeParse(broadcast)\n\n if (!result.success) {\n return {\n data: null,\n errors: z.flattenError(result.error).fieldErrors,\n success: false as const,\n }\n }\n\n return {\n data: result.data,\n success: true as const,\n }\n}\n\n/**\n * Builds the where clause for fetching the next batch of contacts.\n * Uses cursor-based pagination (id > lastProcessedContactId) for consistent\n * ordering and efficient queries at scale.\n */\nfunction buildContactsWhereClause(\n broadcast: ParsedBroadcast,\n rawBroadcast: Record<string, unknown>,\n): Where {\n const conditions: Where[] = [\n { emailOptIn: { equals: true } },\n { id: { greater_than: broadcast.meta.lastProcessedContactId } },\n ]\n\n // Tags may be populated objects or raw IDs depending on query depth\n if (broadcast.to === 'tags' && Array.isArray(rawBroadcast.tags) && rawBroadcast.tags.length > 0) {\n const tagIds = rawBroadcast.tags.map((t: { id: number | string } | number | string) =>\n typeof t === 'object' ? t.id : t,\n )\n conditions.push({ tags: { in: tagIds } })\n }\n\n return { and: conditions }\n}\n\n/**\n * Extracts a numeric ID from a contact, handling string IDs from some database adapters.\n */\nfunction getLastContactId(contact: { id: number | string }): number {\n return typeof contact.id === 'number' ? contact.id : parseInt(contact.id, 10)\n}\n\n/**\n * Creates the send-broadcasts scheduled task.\n *\n * Processes broadcasts by polling for documents with status 'sending' and\n * queuing batches of send-email jobs. Each invocation processes one broadcast\n * and one batch of contacts, allowing the task to be distributed across\n * multiple schedule intervals.\n */\nexport const createSendBroadcastsTask = (pluginConfig: MobilizehubPluginConfig): TaskConfig => {\n const BATCH_SIZE = pluginConfig.broadcastConfig?.batchSize || 100\n const TASK_SCHEDULE = pluginConfig.broadcastConfig?.taskSchedule || '*/5 * * * *'\n const BROADCAST_QUEUE_NAME = pluginConfig.broadcastConfig?.broadcastQueueName || 'send-broadcasts'\n const EMAIL_QUEUE_NAME = pluginConfig.broadcastConfig?.emailQueueName || 'send-emails'\n\n return {\n slug: 'send-broadcasts',\n handler: async ({ req }) => {\n const { payload } = req\n const logger = payload.logger\n\n logger.info('Send Broadcast task handler called')\n\n const { docs } = await payload.find({\n collection: 'broadcasts',\n limit: 1,\n sort: 'id',\n where: { status: { equals: 'sending' } },\n })\n\n const rawBroadcast = docs[0]\n\n if (!rawBroadcast) {\n logger.info('No broadcasts with status \"sending\" found')\n return { output: { success: true } }\n }\n\n const parsed = safeParseBroadcast(rawBroadcast)\n\n if (!parsed.success) {\n logger.error({ errors: parsed.errors }, `Broadcast ${rawBroadcast.id} validation failed`)\n await payload.update({\n id: rawBroadcast.id,\n collection: 'broadcasts',\n data: { status: 'failed' },\n })\n return { output: { success: false } }\n }\n\n const broadcast = parsed.data\n const whereClause = buildContactsWhereClause(\n broadcast,\n rawBroadcast as Record<string, unknown>,\n )\n\n const { docs: contacts } = await payload.find({\n collection: 'contacts',\n limit: BATCH_SIZE,\n sort: 'id',\n where: whereClause,\n })\n\n if (contacts.length === 0) {\n logger.info(\n `Broadcast ${broadcast.id} complete. ` +\n `Processed: ${broadcast.meta.processedCount}, Expected: ${broadcast.meta.contactsCount}`,\n )\n await payload.update({\n id: broadcast.id,\n collection: 'broadcasts',\n data: { status: 'sent' },\n })\n return { output: { success: true } }\n }\n\n await Promise.all(\n contacts.map((contact) =>\n payload.jobs.queue({\n input: { broadcastId: broadcast.id, contactId: contact.id },\n queue: EMAIL_QUEUE_NAME,\n task: 'send-email',\n }),\n ),\n )\n\n await payload.update({\n id: broadcast.id,\n collection: 'broadcasts',\n data: {\n meta: {\n lastProcessedContactId: getLastContactId(contacts[contacts.length - 1]),\n processedCount: broadcast.meta.processedCount + contacts.length,\n },\n },\n })\n\n logger.info(\n `Broadcast ${broadcast.id}: queued ${contacts.length} emails, ` +\n `total processed: ${broadcast.meta.processedCount + contacts.length}`,\n )\n\n return { output: { success: true } }\n },\n outputSchema: [\n {\n name: 'success',\n type: 'checkbox',\n },\n ],\n retries: 3,\n schedule: [\n {\n cron: TASK_SCHEDULE,\n queue: BROADCAST_QUEUE_NAME,\n },\n ],\n }\n}\n"],"names":["z","BroadcastSchema","object","id","number","content","any","refine","val","undefined","message","fromAddress","email","fromName","string","min","meta","contactsCount","lastProcessedContactId","default","processedCount","subject","tags","array","optional","to","literal","or","safeParseBroadcast","broadcast","result","safeParse","success","data","errors","flattenError","error","fieldErrors","buildContactsWhereClause","rawBroadcast","conditions","emailOptIn","equals","greater_than","Array","isArray","length","tagIds","map","t","push","in","and","getLastContactId","contact","parseInt","createSendBroadcastsTask","pluginConfig","BATCH_SIZE","broadcastConfig","batchSize","TASK_SCHEDULE","taskSchedule","BROADCAST_QUEUE_NAME","broadcastQueueName","EMAIL_QUEUE_NAME","emailQueueName","slug","handler","req","payload","logger","info","docs","find","collection","limit","sort","where","status","output","parsed","update","whereClause","contacts","Promise","all","jobs","queue","input","broadcastId","contactId","task","outputSchema","name","type","retries","schedule","cron"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/tasks/sendBroadcastsTask.ts"],"sourcesContent":["import type { TaskConfig, Where } from 'payload'\n\nimport z from 'zod'\n\nimport type { MobilizehubPluginConfig } from '../types/index.js'\n\n/**\n * Schema for validating broadcast documents before processing.\n */\nconst BroadcastSchema = z.object({\n id: z.number(),\n content: z.any().refine((val) => val !== undefined && val !== null, {\n message: 'Broadcast content is missing',\n }),\n fromAddress: z.email({ message: 'Invalid from address email format' }),\n fromName: z.string().min(1, { message: 'From name is required' }),\n meta: z.object({\n contactsCount: z.number().min(0),\n lastProcessedContactId: z.number().min(0).default(0),\n processedCount: z.number().min(0),\n }),\n subject: z.string().min(1, { message: 'Subject is required' }),\n tags: z.array(z.any()).optional(),\n to: z.literal('all').or(z.literal('tags')),\n})\n\ntype ParsedBroadcast = z.infer<typeof BroadcastSchema>\n\n/**\n * Validates a broadcast document against the schema.\n * Returns typed data on success, or flattened field errors on failure.\n */\nfunction safeParseBroadcast(broadcast: unknown) {\n const result = BroadcastSchema.safeParse(broadcast)\n\n if (!result.success) {\n return {\n data: null,\n errors: z.flattenError(result.error).fieldErrors,\n success: false as const,\n }\n }\n\n return {\n data: result.data,\n success: true as const,\n }\n}\n\n/**\n * Builds the where clause for fetching the next batch of contacts.\n * Uses cursor-based pagination (id > lastProcessedContactId) for consistent\n * ordering and efficient queries at scale.\n */\nfunction buildContactsWhereClause(\n broadcast: ParsedBroadcast,\n rawBroadcast: Record<string, unknown>,\n): Where {\n const conditions: Where[] = [\n { emailOptIn: { equals: true } },\n { id: { greater_than: broadcast.meta.lastProcessedContactId } },\n ]\n\n // Tags may be populated objects or raw IDs depending on query depth\n if (broadcast.to === 'tags' && Array.isArray(rawBroadcast.tags) && rawBroadcast.tags.length > 0) {\n const tagIds = rawBroadcast.tags.map((t: { id: number | string } | number | string) =>\n typeof t === 'object' ? t.id : t,\n )\n conditions.push({ tags: { in: tagIds } })\n }\n\n return { and: conditions }\n}\n\n/**\n * Extracts a numeric ID from a contact, handling string IDs from some database adapters.\n */\nfunction getLastContactId(contact: { id: number | string }): number {\n return typeof contact.id === 'number' ? contact.id : parseInt(contact.id, 10)\n}\n\n/**\n * Creates the send-broadcasts scheduled task.\n *\n * Processes broadcasts by polling for documents with status 'sending' and\n * queuing batches of send-email jobs. Each invocation processes one broadcast\n * and one batch of contacts, allowing the task to be distributed across\n * multiple schedule intervals.\n */\nexport const createSendBroadcastsTask = (pluginConfig: MobilizehubPluginConfig): TaskConfig => {\n const BATCH_SIZE = pluginConfig.broadcastConfig?.batchSize || 100\n const TASK_SCHEDULE = pluginConfig.broadcastConfig?.taskSchedule || '*/5 * * * *'\n const BROADCAST_QUEUE_NAME = pluginConfig.broadcastConfig?.broadcastQueueName || 'send-broadcasts'\n const EMAIL_QUEUE_NAME = pluginConfig.broadcastConfig?.emailQueueName || 'send-emails'\n\n return {\n slug: 'send-broadcasts',\n handler: async ({ req }) => {\n const { payload } = req\n const logger = payload.logger\n\n const { docs } = await payload.find({\n collection: 'broadcasts',\n limit: 1,\n sort: 'id',\n where: { status: { equals: 'sending' } },\n })\n\n const rawBroadcast = docs[0]\n\n if (!rawBroadcast) {\n logger.info('No broadcasts with status \"sending\" found')\n return { output: { success: true } }\n }\n\n const parsed = safeParseBroadcast(rawBroadcast)\n\n if (!parsed.success) {\n logger.error({ errors: parsed.errors }, `Broadcast ${rawBroadcast.id} validation failed`)\n await payload.update({\n id: rawBroadcast.id,\n collection: 'broadcasts',\n data: { status: 'failed' },\n })\n return { output: { success: false } }\n }\n\n const broadcast = parsed.data\n const whereClause = buildContactsWhereClause(\n broadcast,\n rawBroadcast as Record<string, unknown>,\n )\n\n const { docs: contacts } = await payload.find({\n collection: 'contacts',\n limit: BATCH_SIZE,\n sort: 'id',\n where: whereClause,\n })\n\n if (contacts.length === 0) {\n logger.info(\n `Broadcast ${broadcast.id} complete. ` +\n `Processed: ${broadcast.meta.processedCount}, Expected: ${broadcast.meta.contactsCount}`,\n )\n await payload.update({\n id: broadcast.id,\n collection: 'broadcasts',\n data: { status: 'sent' },\n })\n return { output: { success: true } }\n }\n\n await Promise.all(\n contacts.map((contact) =>\n payload.jobs.queue({\n input: { broadcastId: broadcast.id, contactId: contact.id },\n queue: EMAIL_QUEUE_NAME,\n task: 'send-email',\n }),\n ),\n )\n\n await payload.update({\n id: broadcast.id,\n collection: 'broadcasts',\n data: {\n meta: {\n lastProcessedContactId: getLastContactId(contacts[contacts.length - 1]),\n processedCount: broadcast.meta.processedCount + contacts.length,\n },\n },\n })\n\n logger.info(\n `Broadcast ${broadcast.id}: queued ${contacts.length} emails, ` +\n `total processed: ${broadcast.meta.processedCount + contacts.length}`,\n )\n\n return { output: { success: true } }\n },\n outputSchema: [\n {\n name: 'success',\n type: 'checkbox',\n },\n ],\n retries: 3,\n schedule: [\n {\n cron: TASK_SCHEDULE,\n queue: BROADCAST_QUEUE_NAME,\n },\n ],\n }\n}\n"],"names":["z","BroadcastSchema","object","id","number","content","any","refine","val","undefined","message","fromAddress","email","fromName","string","min","meta","contactsCount","lastProcessedContactId","default","processedCount","subject","tags","array","optional","to","literal","or","safeParseBroadcast","broadcast","result","safeParse","success","data","errors","flattenError","error","fieldErrors","buildContactsWhereClause","rawBroadcast","conditions","emailOptIn","equals","greater_than","Array","isArray","length","tagIds","map","t","push","in","and","getLastContactId","contact","parseInt","createSendBroadcastsTask","pluginConfig","BATCH_SIZE","broadcastConfig","batchSize","TASK_SCHEDULE","taskSchedule","BROADCAST_QUEUE_NAME","broadcastQueueName","EMAIL_QUEUE_NAME","emailQueueName","slug","handler","req","payload","logger","docs","find","collection","limit","sort","where","status","info","output","parsed","update","whereClause","contacts","Promise","all","jobs","queue","input","broadcastId","contactId","task","outputSchema","name","type","retries","schedule","cron"],"mappings":"AAEA,OAAOA,OAAO,MAAK;AAInB;;CAEC,GACD,MAAMC,kBAAkBD,EAAEE,MAAM,CAAC;IAC/BC,IAAIH,EAAEI,MAAM;IACZC,SAASL,EAAEM,GAAG,GAAGC,MAAM,CAAC,CAACC,MAAQA,QAAQC,aAAaD,QAAQ,MAAM;QAClEE,SAAS;IACX;IACAC,aAAaX,EAAEY,KAAK,CAAC;QAAEF,SAAS;IAAoC;IACpEG,UAAUb,EAAEc,MAAM,GAAGC,GAAG,CAAC,GAAG;QAAEL,SAAS;IAAwB;IAC/DM,MAAMhB,EAAEE,MAAM,CAAC;QACbe,eAAejB,EAAEI,MAAM,GAAGW,GAAG,CAAC;QAC9BG,wBAAwBlB,EAAEI,MAAM,GAAGW,GAAG,CAAC,GAAGI,OAAO,CAAC;QAClDC,gBAAgBpB,EAAEI,MAAM,GAAGW,GAAG,CAAC;IACjC;IACAM,SAASrB,EAAEc,MAAM,GAAGC,GAAG,CAAC,GAAG;QAAEL,SAAS;IAAsB;IAC5DY,MAAMtB,EAAEuB,KAAK,CAACvB,EAAEM,GAAG,IAAIkB,QAAQ;IAC/BC,IAAIzB,EAAE0B,OAAO,CAAC,OAAOC,EAAE,CAAC3B,EAAE0B,OAAO,CAAC;AACpC;AAIA;;;CAGC,GACD,SAASE,mBAAmBC,SAAkB;IAC5C,MAAMC,SAAS7B,gBAAgB8B,SAAS,CAACF;IAEzC,IAAI,CAACC,OAAOE,OAAO,EAAE;QACnB,OAAO;YACLC,MAAM;YACNC,QAAQlC,EAAEmC,YAAY,CAACL,OAAOM,KAAK,EAAEC,WAAW;YAChDL,SAAS;QACX;IACF;IAEA,OAAO;QACLC,MAAMH,OAAOG,IAAI;QACjBD,SAAS;IACX;AACF;AAEA;;;;CAIC,GACD,SAASM,yBACPT,SAA0B,EAC1BU,YAAqC;IAErC,MAAMC,aAAsB;QAC1B;YAAEC,YAAY;gBAAEC,QAAQ;YAAK;QAAE;QAC/B;YAAEvC,IAAI;gBAAEwC,cAAcd,UAAUb,IAAI,CAACE,sBAAsB;YAAC;QAAE;KAC/D;IAED,oEAAoE;IACpE,IAAIW,UAAUJ,EAAE,KAAK,UAAUmB,MAAMC,OAAO,CAACN,aAAajB,IAAI,KAAKiB,aAAajB,IAAI,CAACwB,MAAM,GAAG,GAAG;QAC/F,MAAMC,SAASR,aAAajB,IAAI,CAAC0B,GAAG,CAAC,CAACC,IACpC,OAAOA,MAAM,WAAWA,EAAE9C,EAAE,GAAG8C;QAEjCT,WAAWU,IAAI,CAAC;YAAE5B,MAAM;gBAAE6B,IAAIJ;YAAO;QAAE;IACzC;IAEA,OAAO;QAAEK,KAAKZ;IAAW;AAC3B;AAEA;;CAEC,GACD,SAASa,iBAAiBC,OAAgC;IACxD,OAAO,OAAOA,QAAQnD,EAAE,KAAK,WAAWmD,QAAQnD,EAAE,GAAGoD,SAASD,QAAQnD,EAAE,EAAE;AAC5E;AAEA;;;;;;;CAOC,GACD,OAAO,MAAMqD,2BAA2B,CAACC;IACvC,MAAMC,aAAaD,aAAaE,eAAe,EAAEC,aAAa;IAC9D,MAAMC,gBAAgBJ,aAAaE,eAAe,EAAEG,gBAAgB;IACpE,MAAMC,uBAAuBN,aAAaE,eAAe,EAAEK,sBAAsB;IACjF,MAAMC,mBAAmBR,aAAaE,eAAe,EAAEO,kBAAkB;IAEzE,OAAO;QACLC,MAAM;QACNC,SAAS,OAAO,EAAEC,GAAG,EAAE;YACrB,MAAM,EAAEC,OAAO,EAAE,GAAGD;YACpB,MAAME,SAASD,QAAQC,MAAM;YAE7B,MAAM,EAAEC,IAAI,EAAE,GAAG,MAAMF,QAAQG,IAAI,CAAC;gBAClCC,YAAY;gBACZC,OAAO;gBACPC,MAAM;gBACNC,OAAO;oBAAEC,QAAQ;wBAAEpC,QAAQ;oBAAU;gBAAE;YACzC;YAEA,MAAMH,eAAeiC,IAAI,CAAC,EAAE;YAE5B,IAAI,CAACjC,cAAc;gBACjBgC,OAAOQ,IAAI,CAAC;gBACZ,OAAO;oBAAEC,QAAQ;wBAAEhD,SAAS;oBAAK;gBAAE;YACrC;YAEA,MAAMiD,SAASrD,mBAAmBW;YAElC,IAAI,CAAC0C,OAAOjD,OAAO,EAAE;gBACnBuC,OAAOnC,KAAK,CAAC;oBAAEF,QAAQ+C,OAAO/C,MAAM;gBAAC,GAAG,CAAC,UAAU,EAAEK,aAAapC,EAAE,CAAC,kBAAkB,CAAC;gBACxF,MAAMmE,QAAQY,MAAM,CAAC;oBACnB/E,IAAIoC,aAAapC,EAAE;oBACnBuE,YAAY;oBACZzC,MAAM;wBAAE6C,QAAQ;oBAAS;gBAC3B;gBACA,OAAO;oBAAEE,QAAQ;wBAAEhD,SAAS;oBAAM;gBAAE;YACtC;YAEA,MAAMH,YAAYoD,OAAOhD,IAAI;YAC7B,MAAMkD,cAAc7C,yBAClBT,WACAU;YAGF,MAAM,EAAEiC,MAAMY,QAAQ,EAAE,GAAG,MAAMd,QAAQG,IAAI,CAAC;gBAC5CC,YAAY;gBACZC,OAAOjB;gBACPkB,MAAM;gBACNC,OAAOM;YACT;YAEA,IAAIC,SAAStC,MAAM,KAAK,GAAG;gBACzByB,OAAOQ,IAAI,CACT,CAAC,UAAU,EAAElD,UAAU1B,EAAE,CAAC,WAAW,CAAC,GACpC,CAAC,WAAW,EAAE0B,UAAUb,IAAI,CAACI,cAAc,CAAC,YAAY,EAAES,UAAUb,IAAI,CAACC,aAAa,EAAE;gBAE5F,MAAMqD,QAAQY,MAAM,CAAC;oBACnB/E,IAAI0B,UAAU1B,EAAE;oBAChBuE,YAAY;oBACZzC,MAAM;wBAAE6C,QAAQ;oBAAO;gBACzB;gBACA,OAAO;oBAAEE,QAAQ;wBAAEhD,SAAS;oBAAK;gBAAE;YACrC;YAEA,MAAMqD,QAAQC,GAAG,CACfF,SAASpC,GAAG,CAAC,CAACM,UACZgB,QAAQiB,IAAI,CAACC,KAAK,CAAC;oBACjBC,OAAO;wBAAEC,aAAa7D,UAAU1B,EAAE;wBAAEwF,WAAWrC,QAAQnD,EAAE;oBAAC;oBAC1DqF,OAAOvB;oBACP2B,MAAM;gBACR;YAIJ,MAAMtB,QAAQY,MAAM,CAAC;gBACnB/E,IAAI0B,UAAU1B,EAAE;gBAChBuE,YAAY;gBACZzC,MAAM;oBACJjB,MAAM;wBACJE,wBAAwBmC,iBAAiB+B,QAAQ,CAACA,SAAStC,MAAM,GAAG,EAAE;wBACtE1B,gBAAgBS,UAAUb,IAAI,CAACI,cAAc,GAAGgE,SAAStC,MAAM;oBACjE;gBACF;YACF;YAEAyB,OAAOQ,IAAI,CACT,CAAC,UAAU,EAAElD,UAAU1B,EAAE,CAAC,SAAS,EAAEiF,SAAStC,MAAM,CAAC,SAAS,CAAC,GAC7D,CAAC,iBAAiB,EAAEjB,UAAUb,IAAI,CAACI,cAAc,GAAGgE,SAAStC,MAAM,EAAE;YAGzE,OAAO;gBAAEkC,QAAQ;oBAAEhD,SAAS;gBAAK;YAAE;QACrC;QACA6D,cAAc;YACZ;gBACEC,MAAM;gBACNC,MAAM;YACR;SACD;QACDC,SAAS;QACTC,UAAU;YACR;gBACEC,MAAMrC;gBACN2B,OAAOzB;YACT;SACD;IACH;AACF,EAAC"}
|