@mobilizehub/payload-plugin 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access/authenticated.d.ts +4 -0
- package/dist/access/authenticated.js +5 -0
- package/dist/access/authenticated.js.map +1 -0
- package/dist/access/authenticated.spec.d.ts +1 -0
- package/dist/access/authenticated.spec.js +33 -0
- package/dist/access/authenticated.spec.js.map +1 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/resend-adapter.d.ts +34 -0
- package/dist/adapters/resend-adapter.js +219 -0
- package/dist/adapters/resend-adapter.js.map +1 -0
- package/dist/collections/broadcasts/generateBroadcastsCollection.d.ts +3 -0
- package/dist/collections/broadcasts/generateBroadcastsCollection.js +241 -0
- package/dist/collections/broadcasts/generateBroadcastsCollection.js.map +1 -0
- package/dist/collections/contacts/generateContactsCollection.d.ts +22 -0
- package/dist/collections/contacts/generateContactsCollection.js +124 -0
- package/dist/collections/contacts/generateContactsCollection.js.map +1 -0
- package/dist/collections/emails/generateEmailsCollection.d.ts +3 -0
- package/dist/collections/emails/generateEmailsCollection.js +204 -0
- package/dist/collections/emails/generateEmailsCollection.js.map +1 -0
- package/dist/collections/emails/hooks/sync-status-from-activity.d.ts +5 -0
- package/dist/collections/emails/hooks/sync-status-from-activity.js +64 -0
- package/dist/collections/emails/hooks/sync-status-from-activity.js.map +1 -0
- package/dist/collections/tags/generateTagsCollection.d.ts +3 -0
- package/dist/collections/tags/generateTagsCollection.js +29 -0
- package/dist/collections/tags/generateTagsCollection.js.map +1 -0
- package/dist/collections/unsubscribe-tokens/generateUnsubscribeTokens.d.ts +2 -0
- package/dist/collections/unsubscribe-tokens/generateUnsubscribeTokens.js +48 -0
- package/dist/collections/unsubscribe-tokens/generateUnsubscribeTokens.js.map +1 -0
- package/dist/components/broadcast-metrics-card.d.ts +7 -0
- package/dist/components/broadcast-metrics-card.js +159 -0
- package/dist/components/broadcast-metrics-card.js.map +1 -0
- package/dist/components/broadcast-send-modal.d.ts +9 -0
- package/dist/components/broadcast-send-modal.js +51 -0
- package/dist/components/broadcast-send-modal.js.map +1 -0
- package/dist/components/broadcast-send-test-drawer.d.ts +7 -0
- package/dist/components/broadcast-send-test-drawer.js +154 -0
- package/dist/components/broadcast-send-test-drawer.js.map +1 -0
- package/dist/components/email-activity.d.ts +4 -0
- package/dist/components/email-activity.js +359 -0
- package/dist/components/email-activity.js.map +1 -0
- package/dist/components/email-preview.d.ts +2 -0
- package/dist/components/email-preview.js +95 -0
- package/dist/components/email-preview.js.map +1 -0
- package/dist/endpoints/sendBroadcastHandler.d.ts +9 -0
- package/dist/endpoints/sendBroadcastHandler.js +107 -0
- package/dist/endpoints/sendBroadcastHandler.js.map +1 -0
- package/dist/endpoints/sendTestBroadcastHandler.d.ts +10 -0
- package/dist/endpoints/sendTestBroadcastHandler.js +143 -0
- package/dist/endpoints/sendTestBroadcastHandler.js.map +1 -0
- package/dist/endpoints/unsubscribeHandler.d.ts +9 -0
- package/dist/endpoints/unsubscribeHandler.js +153 -0
- package/dist/endpoints/unsubscribeHandler.js.map +1 -0
- package/dist/exports/client.d.ts +3 -1
- package/dist/exports/client.js +3 -0
- package/dist/exports/client.js.map +1 -1
- package/dist/exports/rsc.d.ts +2 -1
- package/dist/exports/rsc.js +2 -0
- package/dist/exports/rsc.js.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.js +51 -2
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +3 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/unsubscribe.d.ts +6 -0
- package/dist/react/unsubscribe.js +16 -0
- package/dist/react/unsubscribe.js.map +1 -0
- package/dist/tasks/sendBroadcastsTask.d.ts +11 -0
- package/dist/tasks/sendBroadcastsTask.js +196 -0
- package/dist/tasks/sendBroadcastsTask.js.map +1 -0
- package/dist/tasks/sendEmailTask.d.ts +9 -0
- package/dist/tasks/sendEmailTask.js +167 -0
- package/dist/tasks/sendEmailTask.js.map +1 -0
- package/dist/types/index.d.ts +135 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/api-response.d.ts +72 -0
- package/dist/utils/api-response.js +66 -0
- package/dist/utils/api-response.js.map +1 -0
- package/dist/utils/email.d.ts +36 -0
- package/dist/utils/email.js +40 -0
- package/dist/utils/email.js.map +1 -0
- package/dist/utils/lexical.d.ts +13 -0
- package/dist/utils/lexical.js +27 -0
- package/dist/utils/lexical.js.map +1 -0
- package/dist/utils/unsubscribe-token.d.ts +67 -0
- package/dist/utils/unsubscribe-token.js +103 -0
- package/dist/utils/unsubscribe-token.js.map +1 -0
- package/package.json +25 -9
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Schema for validating broadcast documents before processing.
|
|
4
|
+
*/ const BroadcastSchema = z.object({
|
|
5
|
+
id: z.number(),
|
|
6
|
+
content: z.any().refine((val)=>val !== undefined && val !== null, {
|
|
7
|
+
message: 'Broadcast content is missing'
|
|
8
|
+
}),
|
|
9
|
+
fromAddress: z.email({
|
|
10
|
+
message: 'Invalid from address email format'
|
|
11
|
+
}),
|
|
12
|
+
fromName: z.string().min(1, {
|
|
13
|
+
message: 'From name is required'
|
|
14
|
+
}),
|
|
15
|
+
meta: z.object({
|
|
16
|
+
contactsCount: z.number().min(0),
|
|
17
|
+
lastProcessedContactId: z.number().min(0).default(0),
|
|
18
|
+
processedCount: z.number().min(0)
|
|
19
|
+
}),
|
|
20
|
+
subject: z.string().min(1, {
|
|
21
|
+
message: 'Subject is required'
|
|
22
|
+
}),
|
|
23
|
+
tags: z.array(z.any()).optional(),
|
|
24
|
+
to: z.literal('all').or(z.literal('tags'))
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* Validates a broadcast document against the schema.
|
|
28
|
+
* Returns typed data on success, or flattened field errors on failure.
|
|
29
|
+
*/ function safeParseBroadcast(broadcast) {
|
|
30
|
+
const result = BroadcastSchema.safeParse(broadcast);
|
|
31
|
+
if (!result.success) {
|
|
32
|
+
return {
|
|
33
|
+
data: null,
|
|
34
|
+
errors: z.flattenError(result.error).fieldErrors,
|
|
35
|
+
success: false
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
data: result.data,
|
|
40
|
+
success: true
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Builds the where clause for fetching the next batch of contacts.
|
|
45
|
+
* Uses cursor-based pagination (id > lastProcessedContactId) for consistent
|
|
46
|
+
* ordering and efficient queries at scale.
|
|
47
|
+
*/ function buildContactsWhereClause(broadcast, rawBroadcast) {
|
|
48
|
+
const conditions = [
|
|
49
|
+
{
|
|
50
|
+
emailOptIn: {
|
|
51
|
+
equals: true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: {
|
|
56
|
+
greater_than: broadcast.meta.lastProcessedContactId
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
];
|
|
60
|
+
// Tags may be populated objects or raw IDs depending on query depth
|
|
61
|
+
if (broadcast.to === 'tags' && Array.isArray(rawBroadcast.tags) && rawBroadcast.tags.length > 0) {
|
|
62
|
+
const tagIds = rawBroadcast.tags.map((t)=>typeof t === 'object' ? t.id : t);
|
|
63
|
+
conditions.push({
|
|
64
|
+
tags: {
|
|
65
|
+
in: tagIds
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
and: conditions
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Extracts a numeric ID from a contact, handling string IDs from some database adapters.
|
|
75
|
+
*/ function getLastContactId(contact) {
|
|
76
|
+
return typeof contact.id === 'number' ? contact.id : parseInt(contact.id, 10);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Creates the send-broadcasts scheduled task.
|
|
80
|
+
*
|
|
81
|
+
* Processes broadcasts by polling for documents with status 'sending' and
|
|
82
|
+
* queuing batches of send-email jobs. Each invocation processes one broadcast
|
|
83
|
+
* and one batch of contacts, allowing the task to be distributed across
|
|
84
|
+
* multiple schedule intervals.
|
|
85
|
+
*/ export const createSendBroadcastsTask = (pluginConfig)=>{
|
|
86
|
+
const BATCH_SIZE = pluginConfig.broadcastConfig?.batchSize || 100;
|
|
87
|
+
const TASK_SCHEDULE = pluginConfig.broadcastConfig?.taskSchedule || '*/5 * * * *';
|
|
88
|
+
const QUEUE_NAME = pluginConfig.broadcastConfig?.broadcastQueueName || 'send-broadcasts';
|
|
89
|
+
return {
|
|
90
|
+
slug: 'send-broadcasts',
|
|
91
|
+
handler: async ({ req })=>{
|
|
92
|
+
const { payload } = req;
|
|
93
|
+
const logger = payload.logger;
|
|
94
|
+
logger.info('Send Broadcast task handler called');
|
|
95
|
+
const { docs } = await payload.find({
|
|
96
|
+
collection: 'broadcasts',
|
|
97
|
+
limit: 1,
|
|
98
|
+
sort: 'id',
|
|
99
|
+
where: {
|
|
100
|
+
status: {
|
|
101
|
+
equals: 'sending'
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
const rawBroadcast = docs[0];
|
|
106
|
+
if (!rawBroadcast) {
|
|
107
|
+
logger.info('No broadcasts with status "sending" found');
|
|
108
|
+
return {
|
|
109
|
+
output: {
|
|
110
|
+
success: true
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const parsed = safeParseBroadcast(rawBroadcast);
|
|
115
|
+
if (!parsed.success) {
|
|
116
|
+
logger.error({
|
|
117
|
+
errors: parsed.errors
|
|
118
|
+
}, `Broadcast ${rawBroadcast.id} validation failed`);
|
|
119
|
+
await payload.update({
|
|
120
|
+
id: rawBroadcast.id,
|
|
121
|
+
collection: 'broadcasts',
|
|
122
|
+
data: {
|
|
123
|
+
status: 'failed'
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
output: {
|
|
128
|
+
success: false
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const broadcast = parsed.data;
|
|
133
|
+
const whereClause = buildContactsWhereClause(broadcast, rawBroadcast);
|
|
134
|
+
const { docs: contacts } = await payload.find({
|
|
135
|
+
collection: 'contacts',
|
|
136
|
+
limit: BATCH_SIZE,
|
|
137
|
+
sort: 'id',
|
|
138
|
+
where: whereClause
|
|
139
|
+
});
|
|
140
|
+
if (contacts.length === 0) {
|
|
141
|
+
logger.info(`Broadcast ${broadcast.id} complete. ` + `Processed: ${broadcast.meta.processedCount}, Expected: ${broadcast.meta.contactsCount}`);
|
|
142
|
+
await payload.update({
|
|
143
|
+
id: broadcast.id,
|
|
144
|
+
collection: 'broadcasts',
|
|
145
|
+
data: {
|
|
146
|
+
status: 'sent'
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
output: {
|
|
151
|
+
success: true
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
await Promise.all(contacts.map((contact)=>payload.jobs.queue({
|
|
156
|
+
input: {
|
|
157
|
+
broadcastId: broadcast.id,
|
|
158
|
+
contactId: contact.id
|
|
159
|
+
},
|
|
160
|
+
queue: 'send-emails',
|
|
161
|
+
task: 'send-email'
|
|
162
|
+
})));
|
|
163
|
+
await payload.update({
|
|
164
|
+
id: broadcast.id,
|
|
165
|
+
collection: 'broadcasts',
|
|
166
|
+
data: {
|
|
167
|
+
meta: {
|
|
168
|
+
lastProcessedContactId: getLastContactId(contacts[contacts.length - 1]),
|
|
169
|
+
processedCount: broadcast.meta.processedCount + contacts.length
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
logger.info(`Broadcast ${broadcast.id}: queued ${contacts.length} emails, ` + `total processed: ${broadcast.meta.processedCount + contacts.length}`);
|
|
174
|
+
return {
|
|
175
|
+
output: {
|
|
176
|
+
success: true
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
outputSchema: [
|
|
181
|
+
{
|
|
182
|
+
name: 'success',
|
|
183
|
+
type: 'checkbox'
|
|
184
|
+
}
|
|
185
|
+
],
|
|
186
|
+
retries: 3,
|
|
187
|
+
schedule: [
|
|
188
|
+
{
|
|
189
|
+
cron: TASK_SCHEDULE,
|
|
190
|
+
queue: QUEUE_NAME
|
|
191
|
+
}
|
|
192
|
+
]
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
//# sourceMappingURL=sendBroadcastsTask.js.map
|
|
@@ -0,0 +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 QUEUE_NAME = pluginConfig.broadcastConfig?.broadcastQueueName || 'send-broadcasts'\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: 'send-emails',\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: 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","QUEUE_NAME","broadcastQueueName","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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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TaskConfig } from 'payload';
|
|
2
|
+
import type { MobilizehubPluginConfig } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates the send-email task configuration.
|
|
5
|
+
*
|
|
6
|
+
* Handles delivery of a single email for one contact within a broadcast.
|
|
7
|
+
* Queued by the send-broadcasts task, one job per contact.
|
|
8
|
+
*/
|
|
9
|
+
export declare const createSendEmailTask: (pluginConfig: MobilizehubPluginConfig) => TaskConfig;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { formatFromAddress } from '../utils/email.js';
|
|
2
|
+
import { parseLexicalContent } from '../utils/lexical.js';
|
|
3
|
+
import { generateUnsubscribeToken } from '../utils/unsubscribe-token.js';
|
|
4
|
+
/**
|
|
5
|
+
* Resolves collection slugs from plugin config, falling back to defaults.
|
|
6
|
+
*/ function getCollectionSlugs(config) {
|
|
7
|
+
return {
|
|
8
|
+
broadcasts: config.broadcastsOverrides?.slug || 'broadcasts',
|
|
9
|
+
contacts: config.contactsOverrides?.slug || 'contacts',
|
|
10
|
+
emails: config.emailsOverrides?.slug || 'emails'
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Checks if an email record already exists for a broadcast-contact pair.
|
|
15
|
+
*/ async function checkEmailExists(payload, collection, broadcastId, contactId) {
|
|
16
|
+
const { docs } = await payload.find({
|
|
17
|
+
collection,
|
|
18
|
+
limit: 1,
|
|
19
|
+
where: {
|
|
20
|
+
and: [
|
|
21
|
+
{
|
|
22
|
+
broadcast: {
|
|
23
|
+
equals: broadcastId
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
contact: {
|
|
28
|
+
equals: contactId
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return docs[0] ?? null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Creates the send-email task configuration.
|
|
38
|
+
*
|
|
39
|
+
* Handles delivery of a single email for one contact within a broadcast.
|
|
40
|
+
* Queued by the send-broadcasts task, one job per contact.
|
|
41
|
+
*/ export const createSendEmailTask = (pluginConfig)=>{
|
|
42
|
+
const collections = getCollectionSlugs(pluginConfig);
|
|
43
|
+
return {
|
|
44
|
+
slug: 'send-email',
|
|
45
|
+
handler: async ({ input, req })=>{
|
|
46
|
+
const { payload } = req;
|
|
47
|
+
const logger = payload.logger;
|
|
48
|
+
const { broadcastId, contactId } = input;
|
|
49
|
+
// Skip if already processed (handles job retries)
|
|
50
|
+
const existingEmail = await checkEmailExists(payload, collections.emails, broadcastId, contactId);
|
|
51
|
+
if (existingEmail) {
|
|
52
|
+
logger.info(`Email already exists for broadcast ${broadcastId}, contact ${contactId} ` + `(email ID: ${existingEmail.id}, status: ${existingEmail.status})`);
|
|
53
|
+
return {
|
|
54
|
+
output: {
|
|
55
|
+
success: true
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const [contact, broadcast] = await Promise.all([
|
|
60
|
+
payload.findByID({
|
|
61
|
+
id: contactId,
|
|
62
|
+
collection: collections.contacts
|
|
63
|
+
}),
|
|
64
|
+
payload.findByID({
|
|
65
|
+
id: broadcastId,
|
|
66
|
+
collection: collections.broadcasts
|
|
67
|
+
})
|
|
68
|
+
]);
|
|
69
|
+
if (!contact) {
|
|
70
|
+
throw new Error(`Contact ${contactId} not found`);
|
|
71
|
+
}
|
|
72
|
+
if (!broadcast) {
|
|
73
|
+
throw new Error(`Broadcast ${broadcastId} not found`);
|
|
74
|
+
}
|
|
75
|
+
const sender = pluginConfig.email({
|
|
76
|
+
payload
|
|
77
|
+
});
|
|
78
|
+
const fromAddress = formatFromAddress(broadcast.fromName, broadcast.fromAddress);
|
|
79
|
+
const tokenId = crypto.randomUUID();
|
|
80
|
+
const unsubscribeToken = generateUnsubscribeToken({
|
|
81
|
+
tokenId
|
|
82
|
+
});
|
|
83
|
+
const parsedContent = await parseLexicalContent(broadcast.content, payload.config);
|
|
84
|
+
const html = sender.render({
|
|
85
|
+
from: fromAddress,
|
|
86
|
+
html: parsedContent.html,
|
|
87
|
+
markdown: parsedContent.markdown,
|
|
88
|
+
plainText: parsedContent.plainText,
|
|
89
|
+
subject: broadcast.subject,
|
|
90
|
+
to: contact.email,
|
|
91
|
+
token: unsubscribeToken
|
|
92
|
+
});
|
|
93
|
+
const email = await payload.create({
|
|
94
|
+
collection: collections.emails,
|
|
95
|
+
data: {
|
|
96
|
+
broadcast: broadcast.id,
|
|
97
|
+
contact: contact.id,
|
|
98
|
+
from: fromAddress,
|
|
99
|
+
html,
|
|
100
|
+
status: 'queued',
|
|
101
|
+
subject: broadcast.subject,
|
|
102
|
+
to: contact.email
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
await payload.create({
|
|
106
|
+
collection: 'emailUnsubscribeTokens',
|
|
107
|
+
data: {
|
|
108
|
+
id: tokenId,
|
|
109
|
+
emailId: email.id
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
const result = await sender.sendEmail({
|
|
113
|
+
from: fromAddress,
|
|
114
|
+
html,
|
|
115
|
+
idempotencyKey: `broadcast-${broadcastId}-contact-${contactId}`,
|
|
116
|
+
markdown: parsedContent.markdown,
|
|
117
|
+
plainText: parsedContent.plainText,
|
|
118
|
+
previewText: broadcast.previewText,
|
|
119
|
+
replyTo: broadcast.replyTo,
|
|
120
|
+
subject: broadcast.subject,
|
|
121
|
+
to: contact.email,
|
|
122
|
+
token: unsubscribeToken
|
|
123
|
+
});
|
|
124
|
+
await payload.update({
|
|
125
|
+
id: email.id,
|
|
126
|
+
collection: collections.emails,
|
|
127
|
+
data: {
|
|
128
|
+
activity: [
|
|
129
|
+
{
|
|
130
|
+
type: 'sent',
|
|
131
|
+
timestamp: new Date().toISOString()
|
|
132
|
+
}
|
|
133
|
+
],
|
|
134
|
+
providerId: result?.providerId,
|
|
135
|
+
status: 'sent'
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
logger.info(`Sent email ${email.id} for broadcast ${broadcastId} to ${contact.email}`);
|
|
139
|
+
return {
|
|
140
|
+
output: {
|
|
141
|
+
success: true
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
inputSchema: [
|
|
146
|
+
{
|
|
147
|
+
name: 'contactId',
|
|
148
|
+
type: 'number',
|
|
149
|
+
required: true
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'broadcastId',
|
|
153
|
+
type: 'number',
|
|
154
|
+
required: true
|
|
155
|
+
}
|
|
156
|
+
],
|
|
157
|
+
outputSchema: [
|
|
158
|
+
{
|
|
159
|
+
name: 'success',
|
|
160
|
+
type: 'checkbox'
|
|
161
|
+
}
|
|
162
|
+
],
|
|
163
|
+
retries: 3
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
//# sourceMappingURL=sendEmailTask.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/tasks/sendEmailTask.ts"],"sourcesContent":["import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'\nimport type { BasePayload, TaskConfig } from 'payload'\n\nimport type { MobilizehubPluginConfig } from '../types/index.js'\n\nimport { formatFromAddress } from '../utils/email.js'\nimport { parseLexicalContent } from '../utils/lexical.js'\nimport { generateUnsubscribeToken } from '../utils/unsubscribe-token.js'\n\n/**\n * Resolves collection slugs from plugin config, falling back to defaults.\n */\nfunction getCollectionSlugs(config: MobilizehubPluginConfig) {\n return {\n broadcasts: config.broadcastsOverrides?.slug || 'broadcasts',\n contacts: config.contactsOverrides?.slug || 'contacts',\n emails: config.emailsOverrides?.slug || 'emails',\n }\n}\n\n/**\n * Checks if an email record already exists for a broadcast-contact pair.\n */\nasync function checkEmailExists(\n payload: BasePayload,\n collection: string,\n broadcastId: number | string,\n contactId: number | string,\n) {\n const { docs } = await payload.find({\n collection,\n limit: 1,\n where: {\n and: [{ broadcast: { equals: broadcastId } }, { contact: { equals: contactId } }],\n },\n })\n\n return docs[0] ?? null\n}\n\n/**\n * Creates the send-email task configuration.\n *\n * Handles delivery of a single email for one contact within a broadcast.\n * Queued by the send-broadcasts task, one job per contact.\n */\nexport const createSendEmailTask = (pluginConfig: MobilizehubPluginConfig): TaskConfig => {\n const collections = getCollectionSlugs(pluginConfig)\n\n return {\n slug: 'send-email',\n handler: async ({ input, req }) => {\n const { payload } = req\n const logger = payload.logger\n\n const { broadcastId, contactId } = input as { broadcastId: number; contactId: number }\n\n // Skip if already processed (handles job retries)\n const existingEmail = await checkEmailExists(\n payload,\n collections.emails,\n broadcastId,\n contactId,\n )\n\n if (existingEmail) {\n logger.info(\n `Email already exists for broadcast ${broadcastId}, contact ${contactId} ` +\n `(email ID: ${existingEmail.id}, status: ${existingEmail.status})`,\n )\n return { output: { success: true } }\n }\n\n const [contact, broadcast] = await Promise.all([\n payload.findByID({ id: contactId, collection: collections.contacts }),\n payload.findByID({ id: broadcastId, collection: collections.broadcasts }),\n ])\n\n if (!contact) {\n throw new Error(`Contact ${contactId} not found`)\n }\n if (!broadcast) {\n throw new Error(`Broadcast ${broadcastId} not found`)\n }\n\n const sender = pluginConfig.email({ payload })\n const fromAddress = formatFromAddress(broadcast.fromName, broadcast.fromAddress)\n\n const tokenId = crypto.randomUUID()\n const unsubscribeToken = generateUnsubscribeToken({ tokenId })\n\n const parsedContent = await parseLexicalContent(\n broadcast.content as SerializedEditorState,\n payload.config,\n )\n\n const html = sender.render({\n from: fromAddress,\n html: parsedContent.html,\n markdown: parsedContent.markdown,\n plainText: parsedContent.plainText,\n subject: broadcast.subject,\n to: contact.email,\n token: unsubscribeToken,\n })\n\n const email = await payload.create({\n collection: collections.emails,\n data: {\n broadcast: broadcast.id,\n contact: contact.id,\n from: fromAddress,\n html,\n status: 'queued',\n subject: broadcast.subject,\n to: contact.email,\n },\n })\n\n await payload.create({\n collection: 'emailUnsubscribeTokens',\n data: { id: tokenId, emailId: email.id },\n })\n\n const result = await sender.sendEmail({\n from: fromAddress,\n html,\n idempotencyKey: `broadcast-${broadcastId}-contact-${contactId}`,\n markdown: parsedContent.markdown,\n plainText: parsedContent.plainText,\n previewText: broadcast.previewText,\n replyTo: broadcast.replyTo,\n subject: broadcast.subject,\n to: contact.email,\n token: unsubscribeToken,\n })\n\n await payload.update({\n id: email.id,\n collection: collections.emails,\n data: {\n activity: [{ type: 'sent', timestamp: new Date().toISOString() }],\n providerId: result?.providerId,\n status: 'sent',\n },\n })\n\n logger.info(`Sent email ${email.id} for broadcast ${broadcastId} to ${contact.email}`)\n\n return { output: { success: true } }\n },\n inputSchema: [\n { name: 'contactId', type: 'number', required: true },\n { name: 'broadcastId', type: 'number', required: true },\n ],\n outputSchema: [{ name: 'success', type: 'checkbox' }],\n retries: 3,\n }\n}\n"],"names":["formatFromAddress","parseLexicalContent","generateUnsubscribeToken","getCollectionSlugs","config","broadcasts","broadcastsOverrides","slug","contacts","contactsOverrides","emails","emailsOverrides","checkEmailExists","payload","collection","broadcastId","contactId","docs","find","limit","where","and","broadcast","equals","contact","createSendEmailTask","pluginConfig","collections","handler","input","req","logger","existingEmail","info","id","status","output","success","Promise","all","findByID","Error","sender","email","fromAddress","fromName","tokenId","crypto","randomUUID","unsubscribeToken","parsedContent","content","html","render","from","markdown","plainText","subject","to","token","create","data","emailId","result","sendEmail","idempotencyKey","previewText","replyTo","update","activity","type","timestamp","Date","toISOString","providerId","inputSchema","name","required","outputSchema","retries"],"mappings":"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"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { BasePayload, CollectionConfig, Field, PayloadRequest } from 'payload';
|
|
2
|
+
export type FieldsOverride = (args: {
|
|
3
|
+
defaultFields: Field[];
|
|
4
|
+
}) => Field[];
|
|
5
|
+
export type CollectionOverride = {
|
|
6
|
+
fields?: FieldsOverride;
|
|
7
|
+
} & Partial<Omit<CollectionConfig, 'fields'>>;
|
|
8
|
+
/**
|
|
9
|
+
* Contact type
|
|
10
|
+
*/
|
|
11
|
+
export type Contact = {
|
|
12
|
+
createdAt?: string;
|
|
13
|
+
email?: string;
|
|
14
|
+
emailOptIn: boolean;
|
|
15
|
+
firstName?: string;
|
|
16
|
+
id: number | string;
|
|
17
|
+
lastName?: string;
|
|
18
|
+
tags?: {
|
|
19
|
+
createdAt?: string;
|
|
20
|
+
id: number | string;
|
|
21
|
+
name?: string;
|
|
22
|
+
updatedAt?: string;
|
|
23
|
+
}[];
|
|
24
|
+
updatedAt?: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Unsubscribe token input structure
|
|
28
|
+
*/
|
|
29
|
+
export interface UnsubscribeTokenInput {
|
|
30
|
+
timestamp: number;
|
|
31
|
+
tokenId: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Unsubscribe token record structure
|
|
35
|
+
*/
|
|
36
|
+
export type UnsubscribeTokenRecord = {
|
|
37
|
+
emailId?: number | string;
|
|
38
|
+
expiresAt?: string;
|
|
39
|
+
id: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Email activity types
|
|
43
|
+
*/
|
|
44
|
+
export type EmailStatus = 'bounced' | 'complained' | 'delivered' | 'failed' | 'queued' | 'sent' | 'unsubscribed';
|
|
45
|
+
/**
|
|
46
|
+
* Email activity types from providers
|
|
47
|
+
*/
|
|
48
|
+
export type EmailActivityType = 'bounced' | 'clicked' | 'complained' | 'delivered' | 'delivery_delayed' | 'failed' | 'opened' | 'received' | 'sent';
|
|
49
|
+
/**
|
|
50
|
+
* Email message structure
|
|
51
|
+
*/
|
|
52
|
+
export type EmailMessage = {
|
|
53
|
+
from: string;
|
|
54
|
+
html: string;
|
|
55
|
+
idempotencyKey?: string;
|
|
56
|
+
markdown?: string;
|
|
57
|
+
plainText?: string;
|
|
58
|
+
previewText?: string;
|
|
59
|
+
replyTo?: string;
|
|
60
|
+
subject: string;
|
|
61
|
+
to: string;
|
|
62
|
+
token?: string;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Result of a webhook call
|
|
66
|
+
*/
|
|
67
|
+
export type WebhookResult = {
|
|
68
|
+
body?: unknown;
|
|
69
|
+
status: number;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Email adapter interface for sending emails
|
|
73
|
+
*/
|
|
74
|
+
export type EmailAdapter = ({ payload }: {
|
|
75
|
+
payload: BasePayload;
|
|
76
|
+
}) => {
|
|
77
|
+
defaultFromAddress: string;
|
|
78
|
+
defaultFromName: string;
|
|
79
|
+
name: string;
|
|
80
|
+
render: (args: EmailMessage) => string;
|
|
81
|
+
sendEmail: (args: EmailMessage) => Promise<{
|
|
82
|
+
providerId: string;
|
|
83
|
+
} | void>;
|
|
84
|
+
webhookHandler?: (req: PayloadRequest) => Promise<void | WebhookResult>;
|
|
85
|
+
};
|
|
86
|
+
export type MobilizehubPluginConfig = {
|
|
87
|
+
broadcastConfig?: {
|
|
88
|
+
/**
|
|
89
|
+
* Batch size for processing contacts in the broadcasts task.
|
|
90
|
+
* Higher values process faster but use more memory.
|
|
91
|
+
* @default 100
|
|
92
|
+
*/
|
|
93
|
+
batchSize?: number;
|
|
94
|
+
/**
|
|
95
|
+
* Optional custom queue name for the broadcasts task
|
|
96
|
+
* @default 'send-broadcasts'
|
|
97
|
+
*/
|
|
98
|
+
broadcastQueueName?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Optional custom queue name for the email sending task
|
|
101
|
+
* @default 'send-email'
|
|
102
|
+
*/
|
|
103
|
+
emailQueueName?: string;
|
|
104
|
+
/**
|
|
105
|
+
* Cron schedule for the broadcasts task
|
|
106
|
+
* On schedule the task will run to process and send pending broadcasts
|
|
107
|
+
* @default '5 * * * *' (every 5 minutes)
|
|
108
|
+
*/
|
|
109
|
+
taskSchedule?: string;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Overrides for the broadcasts collection
|
|
113
|
+
*/
|
|
114
|
+
broadcastsOverrides?: CollectionOverride;
|
|
115
|
+
/**
|
|
116
|
+
* Overrides for the contacts collection
|
|
117
|
+
*/
|
|
118
|
+
contactsOverrides?: CollectionOverride;
|
|
119
|
+
/**
|
|
120
|
+
* Disable the plugin
|
|
121
|
+
*/
|
|
122
|
+
disabled?: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Email adapter for sending emails
|
|
125
|
+
*/
|
|
126
|
+
email: EmailAdapter;
|
|
127
|
+
/**
|
|
128
|
+
* Overrides for the emails collection
|
|
129
|
+
*/
|
|
130
|
+
emailsOverrides?: CollectionOverride;
|
|
131
|
+
/**
|
|
132
|
+
* Overrides for the tags collection
|
|
133
|
+
*/
|
|
134
|
+
tagsOverrides?: CollectionOverride;
|
|
135
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/types/index.ts"],"sourcesContent":["import type { BasePayload, CollectionConfig, Field, PayloadRequest } from 'payload'\n\nexport type FieldsOverride = (args: { defaultFields: Field[] }) => Field[]\n\nexport type CollectionOverride = { fields?: FieldsOverride } & Partial<\n Omit<CollectionConfig, 'fields'>\n>\n\n/**\n * Contact type\n */\nexport type Contact = {\n createdAt?: string\n email?: string\n emailOptIn: boolean\n firstName?: string\n id: number | string\n lastName?: string\n tags?: {\n createdAt?: string\n id: number | string\n name?: string\n updatedAt?: string\n }[]\n updatedAt?: string\n}\n\n/**\n * Unsubscribe token input structure\n */\nexport interface UnsubscribeTokenInput {\n timestamp: number\n tokenId: string\n}\n\n/**\n * Unsubscribe token record structure\n */\nexport type UnsubscribeTokenRecord = {\n emailId?: number | string\n expiresAt?: string\n id: string\n}\n\n/**\n * Email activity types\n */\nexport type EmailStatus =\n | 'bounced'\n | 'complained'\n | 'delivered'\n | 'failed'\n | 'queued'\n | 'sent'\n | 'unsubscribed'\n\n/**\n * Email activity types from providers\n */\nexport type EmailActivityType =\n | 'bounced'\n | 'clicked'\n | 'complained'\n | 'delivered'\n | 'delivery_delayed'\n | 'failed'\n | 'opened'\n | 'received'\n | 'sent'\n\n/**\n * Email message structure\n */\nexport type EmailMessage = {\n from: string\n html: string\n idempotencyKey?: string\n markdown?: string\n plainText?: string\n previewText?: string\n replyTo?: string\n subject: string\n to: string\n token?: string\n}\n\n/**\n * Result of a webhook call\n */\nexport type WebhookResult = {\n body?: unknown\n status: number\n}\n\n/**\n * Email adapter interface for sending emails\n */\nexport type EmailAdapter = ({ payload }: { payload: BasePayload }) => {\n defaultFromAddress: string\n defaultFromName: string\n name: string\n render: (args: EmailMessage) => string\n sendEmail: (args: EmailMessage) => Promise<{ providerId: string } | void>\n webhookHandler?: (req: PayloadRequest) => Promise<void | WebhookResult>\n}\n\nexport type MobilizehubPluginConfig = {\n broadcastConfig?: {\n /**\n * Batch size for processing contacts in the broadcasts task.\n * Higher values process faster but use more memory.\n * @default 100\n */\n batchSize?: number\n /**\n * Optional custom queue name for the broadcasts task\n * @default 'send-broadcasts'\n */\n broadcastQueueName?: string\n /**\n * Optional custom queue name for the email sending task\n * @default 'send-email'\n */\n emailQueueName?: string\n /**\n * Cron schedule for the broadcasts task\n * On schedule the task will run to process and send pending broadcasts\n * @default '5 * * * *' (every 5 minutes)\n */\n taskSchedule?: string\n }\n /**\n * Overrides for the broadcasts collection\n */\n broadcastsOverrides?: CollectionOverride\n\n /**\n * Overrides for the contacts collection\n */\n contactsOverrides?: CollectionOverride\n /**\n * Disable the plugin\n */\n disabled?: boolean\n /**\n * Email adapter for sending emails\n */\n email: EmailAdapter\n /**\n * Overrides for the emails collection\n */\n emailsOverrides?: CollectionOverride\n /**\n * Overrides for the tags collection\n */\n tagsOverrides?: CollectionOverride\n}\n"],"names":[],"mappings":"AA0GA,WAkDC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard API success response format
|
|
3
|
+
* @template T - Type of the data payload
|
|
4
|
+
*/
|
|
5
|
+
export type ApiSuccessResponse<T = unknown> = {
|
|
6
|
+
data?: T;
|
|
7
|
+
success: true;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Standard API error response format
|
|
11
|
+
*/
|
|
12
|
+
export type ApiErrorResponse = {
|
|
13
|
+
error: {
|
|
14
|
+
/** Error code constant (e.g., 'BAD_REQUEST', 'UNAUTHORIZED') */
|
|
15
|
+
code: string;
|
|
16
|
+
/** Human-readable error message */
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
success: false;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Union type for all API responses
|
|
23
|
+
* @template T - Type of the data payload for success responses
|
|
24
|
+
*/
|
|
25
|
+
export type ApiResponse<T = unknown> = ApiErrorResponse | ApiSuccessResponse<T>;
|
|
26
|
+
/**
|
|
27
|
+
* Create a standardized success response
|
|
28
|
+
*
|
|
29
|
+
* @template T - Type of the response data
|
|
30
|
+
* @param data - Optional data payload to include in the response
|
|
31
|
+
* @param status - HTTP status code (default: 200)
|
|
32
|
+
* @returns Response object with JSON body
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* return successResponse({ message: 'Email sent successfully' }, 200)
|
|
37
|
+
* // Returns: { success: true, data: { message: 'Email sent successfully' } }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function successResponse<T>(data?: T, status?: number): Response;
|
|
41
|
+
/**
|
|
42
|
+
* Create a standardized error response
|
|
43
|
+
*
|
|
44
|
+
* @param code - Error code from ErrorCodes constant
|
|
45
|
+
* @param message - Human-readable error message
|
|
46
|
+
* @param status - HTTP status code (default: 400)
|
|
47
|
+
* @returns Response object with JSON error body
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* return errorResponse(ErrorCodes.UNAUTHORIZED, 'You must be logged in', 401)
|
|
52
|
+
* // Returns: { success: false, error: { code: 'UNAUTHORIZED', message: '...' } }
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function errorResponse(code: string, message: string, status?: number): Response;
|
|
56
|
+
/**
|
|
57
|
+
* Standard error codes used across the MobilizeHub plugin API
|
|
58
|
+
*/
|
|
59
|
+
export declare const ErrorCodes: {
|
|
60
|
+
readonly BAD_REQUEST: "BAD_REQUEST";
|
|
61
|
+
readonly BROADCAST_INVALID_STATUS: "BROADCAST_INVALID_STATUS";
|
|
62
|
+
readonly BROADCAST_NOT_FOUND: "BROADCAST_NOT_FOUND";
|
|
63
|
+
readonly CONTACT_NOT_FOUND: "CONTACT_NOT_FOUND";
|
|
64
|
+
readonly EMAIL_SEND_FAILED: "EMAIL_SEND_FAILED";
|
|
65
|
+
readonly INTERNAL_ERROR: "INTERNAL_ERROR";
|
|
66
|
+
readonly NOT_FOUND: "NOT_FOUND";
|
|
67
|
+
readonly RATE_LIMITED: "RATE_LIMITED";
|
|
68
|
+
readonly TOKEN_EXPIRED: "TOKEN_EXPIRED";
|
|
69
|
+
readonly TOKEN_INVALID: "TOKEN_INVALID";
|
|
70
|
+
readonly UNAUTHORIZED: "UNAUTHORIZED";
|
|
71
|
+
readonly VALIDATION_ERROR: "VALIDATION_ERROR";
|
|
72
|
+
};
|