diff --git a/README.md b/README.md index d565be1..0ceab05 100644 --- a/README.md +++ b/README.md @@ -56,41 +56,50 @@ export default buildConfig({ }) ``` -### 2. Send emails using Payload collections +### 2. Send emails with type-safe helper ```typescript -import { renderTemplate } from '@xtr-dev/payload-mailing' +import { sendEmail } from '@xtr-dev/payload-mailing' +import { Email } from './payload-types' // Your generated types -// Option 1: Using templates with variables -const { html, text, subject } = await renderTemplate(payload, 'welcome-email', { - firstName: 'John', - welcomeUrl: 'https://yoursite.com/welcome' -}) - -// Create email using Payload's collection API (full type safety!) -const email = await payload.create({ - collection: 'emails', +// Option 1: Using templates with full type safety +const email = await sendEmail(payload, { + template: { + slug: 'welcome-email', + variables: { + firstName: 'John', + welcomeUrl: 'https://yoursite.com/welcome' + } + }, data: { - to: ['user@example.com'], - subject, - html, - text, + to: 'user@example.com', // Schedule for later (optional) scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000), - // Add any custom fields you've defined priority: 1, - customField: 'your-value', // Your custom collection fields work! + // Your custom fields are type-safe! + customField: 'your-value', } }) -// Option 2: Direct HTML email (no template needed) -const directEmail = await payload.create({ - collection: 'emails', +// Option 2: Direct HTML email (no template) +const directEmail = await sendEmail(payload, { data: { - to: ['user@example.com'], + to: ['user@example.com', 'another@example.com'], subject: 'Welcome!', html: '

Welcome John!

Thanks for joining!

', text: 'Welcome John! Thanks for joining!', + // All your custom fields work with TypeScript autocomplete! + customField: 'value', + } +}) + +// Option 3: Use payload.create() directly for full control +const manualEmail = await payload.create({ + collection: 'emails', + data: { + to: ['user@example.com'], + subject: 'Hello', + html: '

Hello World

', } }) ``` @@ -840,6 +849,55 @@ import { } from '@xtr-dev/payload-mailing' ``` +## API Reference + +### `sendEmail(payload, options)` + +Type-safe email sending with automatic template rendering and validation. + +```typescript +import { sendEmail } from '@xtr-dev/payload-mailing' +import { Email } from './payload-types' + +const email = await sendEmail(payload, { + template: { + slug: 'template-slug', + variables: { /* template variables */ } + }, + data: { + to: 'user@example.com', + // Your custom fields are type-safe here! + } +}) +``` + +**Type Parameters:** +- `T extends BaseEmailData` - Your generated Email type for full type safety + +**Options:** +- `template.slug` - Template slug to render +- `template.variables` - Variables to pass to template +- `data` - Email data (merged with template output) +- `collectionSlug` - Custom collection name (defaults to 'emails') + +### `renderTemplate(payload, slug, variables)` + +Render an email template without sending. + +```typescript +const { html, text, subject } = await renderTemplate( + payload, + 'welcome-email', + { name: 'John' } +) +``` + +### Helper Functions + +- `getMailing(payload)` - Get mailing context +- `processEmails(payload)` - Manually trigger email processing +- `retryFailedEmails(payload)` - Manually retry failed emails + ## Migration Guide (v0.0.x → v0.1.0) **🚨 BREAKING CHANGES**: The API has been simplified to use Payload collections directly. diff --git a/src/index.ts b/src/index.ts index 3bed52c..70288ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,4 +21,7 @@ export { renderTemplate, processEmails, retryFailedEmails, + sendEmail, + type BaseEmailData, + type SendEmailOptions, } from './utils/helpers.js' \ No newline at end of file diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 1e37e48..3db18fb 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,6 +1,32 @@ 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' +} + export const getMailing = (payload: Payload) => { const mailing = (payload as any).mailing if (!mailing) { @@ -22,4 +48,105 @@ 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') + } + + // Parse and validate email addresses + const parseEmails = (emails: string | string[] | undefined): string[] | undefined => { + if (!emails) return undefined + + let emailList: string[] + if (Array.isArray(emails)) { + emailList = emails + } else { + emailList = emails.split(',').map(email => email.trim()).filter(Boolean) + } + + // Basic email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + const invalidEmails = emailList.filter(email => !emailRegex.test(email)) + if (invalidEmails.length > 0) { + throw new Error(`Invalid email addresses: ${invalidEmails.join(', ')}`) + } + + return emailList + } + + // Process email addresses + if (emailData.to) { + emailData.to = parseEmails(emailData.to as string | string[]) + } + if (emailData.cc) { + emailData.cc = parseEmails(emailData.cc as string | string[]) + } + if (emailData.bcc) { + emailData.bcc = parseEmails(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