diff --git a/README.md b/README.md index 0ceab05..2381749 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ export default buildConfig({ ### 2. Send emails with type-safe helper ```typescript +// sendEmail is a primary export for easy access import { sendEmail } from '@xtr-dev/payload-mailing' import { Email } from './payload-types' // Your generated types diff --git a/src/index.ts b/src/index.ts index 70288ff..3606914 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,13 +15,15 @@ export { default as Emails } from './collections/Emails.js' export { mailingJobs, sendEmailJob } from './jobs/index.js' export type { SendEmailTaskInput } from './jobs/sendEmailTask.js' +// Main email sending function +export { sendEmail, type BaseEmailData, type SendEmailOptions } from './sendEmail.js' +export { default as sendEmailDefault } from './sendEmail.js' + // Utility functions for developers export { getMailing, renderTemplate, processEmails, retryFailedEmails, - sendEmail, - type BaseEmailData, - type SendEmailOptions, + parseAndValidateEmails, } from './utils/helpers.js' \ No newline at end of file diff --git a/src/jobs/sendEmailTask.ts b/src/jobs/sendEmailTask.ts index a0e8042..474867a 100644 --- a/src/jobs/sendEmailTask.ts +++ b/src/jobs/sendEmailTask.ts @@ -1,4 +1,4 @@ -import { sendEmail, type BaseEmailData } from '../utils/helpers.js' +import { sendEmail, type BaseEmailData } from '../sendEmail.js' export interface SendEmailTaskInput { // Template mode fields diff --git a/src/sendEmail.ts b/src/sendEmail.ts new file mode 100644 index 0000000..7f514de --- /dev/null +++ b/src/sendEmail.ts @@ -0,0 +1,110 @@ +import { Payload } from 'payload' +import { getMailing, renderTemplate, parseAndValidateEmails } from './utils/helpers.js' + +// Base type for email data that all emails must have +export interface BaseEmailData { + to: string | string[] + cc?: string | string[] + bcc?: string | string[] + subject?: string + html?: string + text?: string + scheduledAt?: string | Date + priority?: number + [key: string]: any +} + +// Options for sending emails +export interface SendEmailOptions { + // Template-based email + template?: { + slug: string + variables?: Record + } + // Direct email data + data?: Partial + // Common options + collectionSlug?: string // defaults to 'emails' +} + +/** + * Send an email with full type safety + * + * @example + * ```typescript + * // With your generated Email type + * import { Email } from './payload-types' + * + * const email = await sendEmail(payload, { + * template: { + * slug: 'welcome', + * variables: { name: 'John' } + * }, + * data: { + * to: 'user@example.com', + * customField: 'value' // Your custom fields are type-safe! + * } + * }) + * ``` + */ +export const sendEmail = async ( + payload: Payload, + options: SendEmailOptions +): Promise => { + const mailing = getMailing(payload) + const collectionSlug = options.collectionSlug || mailing.collections.emails || 'emails' + + let emailData: Partial = { ...options.data } as Partial + + // If using a template, render it first + if (options.template) { + const { html, text, subject } = await renderTemplate( + payload, + options.template.slug, + options.template.variables || {} + ) + + // Template values take precedence over data values + emailData = { + ...emailData, + subject, + html, + text, + } as Partial + } + + // Validate required fields + if (!emailData.to) { + throw new Error('Field "to" is required for sending emails') + } + + if (!emailData.subject || !emailData.html) { + throw new Error('Fields "subject" and "html" are required when not using a template') + } + + // Process email addresses using shared validation + if (emailData.to) { + emailData.to = parseAndValidateEmails(emailData.to as string | string[]) + } + if (emailData.cc) { + emailData.cc = parseAndValidateEmails(emailData.cc as string | string[]) + } + if (emailData.bcc) { + emailData.bcc = parseAndValidateEmails(emailData.bcc as string | string[]) + } + + // Convert scheduledAt to ISO string if it's a Date + if (emailData.scheduledAt instanceof Date) { + emailData.scheduledAt = emailData.scheduledAt.toISOString() + } + + // Create the email in the collection + const email = await payload.create({ + collection: collectionSlug as any, + data: emailData as any + }) + + return email as unknown as T +} + +export default sendEmail \ No newline at end of file diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index a2a70b9..deec373 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,32 +1,6 @@ import { Payload } from 'payload' import { TemplateVariables } from '../types/index.js' -// Base type for email data that all emails must have -export interface BaseEmailData { - to: string | string[] - cc?: string | string[] - bcc?: string | string[] - subject?: string - html?: string - text?: string - scheduledAt?: string | Date - priority?: number - [key: string]: any -} - -// Options for sending emails -export interface SendEmailOptions { - // Template-based email - template?: { - slug: string - variables?: Record - } - // Direct email data - data?: Partial - // Common options - collectionSlug?: string // defaults to 'emails' -} - /** * Parse and validate email addresses * @internal @@ -72,84 +46,4 @@ export const processEmails = async (payload: Payload): Promise => { export const retryFailedEmails = async (payload: Payload): Promise => { const mailing = getMailing(payload) return mailing.service.retryFailedEmails() -} - -/** - * Send an email with full type safety - * - * @example - * ```typescript - * // With your generated Email type - * import { Email } from './payload-types' - * - * const email = await sendEmail(payload, { - * template: { - * slug: 'welcome', - * variables: { name: 'John' } - * }, - * data: { - * to: 'user@example.com', - * customField: 'value' // Your custom fields are type-safe! - * } - * }) - * ``` - */ -export const sendEmail = async ( - payload: Payload, - options: SendEmailOptions -): Promise => { - const mailing = getMailing(payload) - const collectionSlug = options.collectionSlug || mailing.collections.emails || 'emails' - - let emailData: Partial = { ...options.data } as Partial - - // If using a template, render it first - if (options.template) { - const { html, text, subject } = await renderTemplate( - payload, - options.template.slug, - options.template.variables || {} - ) - - // Template values take precedence over data values - emailData = { - ...emailData, - subject, - html, - text, - } as Partial - } - - // Validate required fields - if (!emailData.to) { - throw new Error('Field "to" is required for sending emails') - } - - if (!emailData.subject || !emailData.html) { - throw new Error('Fields "subject" and "html" are required when not using a template') - } - - // Process email addresses using shared validation - if (emailData.to) { - emailData.to = parseAndValidateEmails(emailData.to as string | string[]) - } - if (emailData.cc) { - emailData.cc = parseAndValidateEmails(emailData.cc as string | string[]) - } - if (emailData.bcc) { - emailData.bcc = parseAndValidateEmails(emailData.bcc as string | string[]) - } - - // Convert scheduledAt to ISO string if it's a Date - if (emailData.scheduledAt instanceof Date) { - emailData.scheduledAt = emailData.scheduledAt.toISOString() - } - - // Create the email in the collection - const email = await payload.create({ - collection: collectionSlug as any, - data: emailData as any - }) - - return email as unknown as T } \ No newline at end of file