From 74f565ab4efc2aeea6479f2c5aa8032e85e75714 Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Sat, 13 Sep 2025 18:23:05 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20BREAKING:=20Simplify=20API=20to?= =?UTF-8?q?=20use=20Payload=20collections=20directly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove complex sendEmail/scheduleEmail methods and SendEmailOptions types - Add simple renderTemplate() helper for template rendering - Users now create emails using payload.create() with full type safety - Leverage Payload's existing collection system instead of duplicating functionality - Provide comprehensive migration guide and usage examples BREAKING CHANGES: - sendEmail() and scheduleEmail() methods removed - SendEmailOptions type removed - Use payload.create() with email collection instead - Use renderTemplate() helper for template rendering Benefits: ✅ Full TypeScript support with generated Payload types ✅ Use any custom fields in your email collection ✅ Leverage Payload's validation, hooks, and access control ✅ Simpler, more consistent API ✅ Less code to maintain 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- package.json | 2 +- simplified-api-guide.md | 167 +++++++++++++++++++++++++++++++++ src/index.ts | 3 +- src/services/MailingService.ts | 78 +++------------ src/types/index.ts | 21 +---- src/utils/helpers.ts | 11 +-- 6 files changed, 192 insertions(+), 90 deletions(-) create mode 100644 simplified-api-guide.md diff --git a/package.json b/package.json index 94d8c49..71c4844 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xtr-dev/payload-mailing", - "version": "0.0.12", + "version": "0.1.0", "description": "Template-based email system with scheduling and job processing for PayloadCMS", "type": "module", "main": "dist/index.js", diff --git a/simplified-api-guide.md b/simplified-api-guide.md new file mode 100644 index 0000000..3a0bb12 --- /dev/null +++ b/simplified-api-guide.md @@ -0,0 +1,167 @@ +# Simplified API Guide + +The mailing plugin now uses a much simpler, type-safe API that leverages PayloadCMS's existing collection system instead of custom email methods. + +## New API Approach + +### ✅ **Recommended: Use Payload Collections Directly** + +```typescript +import { payload, renderTemplate } from '@xtr-dev/payload-mailing' + +// 1. Render a template (optional) +const rendered = await renderTemplate(payload, 'welcome-email', { + name: 'John Doe', + activationLink: 'https://example.com/activate/123' +}) + +// 2. Create an email using Payload's collection API +const email = await payload.create({ + collection: 'emails', // Your email collection name + data: { + to: ['user@example.com'], + subject: rendered.subject, // or your own subject + html: rendered.html, // or your own HTML + text: rendered.text, // or your own text + // Add any custom fields you've defined in your collection + priority: 1, + scheduledAt: new Date('2024-01-01T10:00:00Z'), // Optional scheduling + } +}) +``` + +### ❌ **Old API (Removed)** + +```typescript +// These methods have been removed to simplify the API +await sendEmail(payload, { to: '...', subject: '...' }) +await scheduleEmail(payload, { to: '...', scheduledAt: new Date() }) +``` + +## Benefits of the New Approach + +### 🎯 **Type Safety** +- Full TypeScript support using your generated Payload types +- IntelliSense for all your custom collection fields +- Compile-time validation of email data + +### 🚀 **Flexibility** +- Use any fields you've added to your email collection +- Leverage Payload's built-in validation, hooks, and access control +- Full control over email data structure + +### 🧹 **Simplicity** +- One consistent API (Payload collections) instead of custom methods +- No duplicate type definitions +- Less code to maintain + +## Usage Examples + +### Basic Email with Template +```typescript +import { renderTemplate } from '@xtr-dev/payload-mailing' + +const { html, text, subject } = await renderTemplate(payload, 'order-confirmation', { + orderNumber: '#12345', + customerName: 'Jane Smith', + items: [ + { name: 'Product 1', price: 29.99 }, + { name: 'Product 2', price: 49.99 } + ] +}) + +await payload.create({ + collection: 'emails', + data: { + to: ['customer@example.com'], + subject, + html, + text, + priority: 2, + // Add your custom fields + orderId: '12345', + customerSegment: 'premium' + } +}) +``` + +### Bulk Email Creation +```typescript +const customers = await payload.find({ + collection: 'customers', + where: { newsletter: { equals: true } } +}) + +for (const customer of customers.docs) { + const { html, text, subject } = await renderTemplate(payload, 'newsletter', { + name: customer.name, + unsubscribeLink: `https://example.com/unsubscribe/${customer.id}` + }) + + await payload.create({ + collection: 'emails', + data: { + to: [customer.email], + subject, + html, + text, + scheduledAt: new Date('2024-01-15T09:00:00Z'), // Send next week + } + }) +} +``` + +### Direct HTML Email (No Template) +```typescript +await payload.create({ + collection: 'emails', + data: { + to: ['admin@example.com'], + subject: 'System Alert', + html: '

Server Error

Please check the logs immediately.

', + text: 'Server Error: Please check the logs immediately.', + priority: 10, // High priority + } +}) +``` + +## Available Helper Functions + +```typescript +import { + renderTemplate, // Render email templates with variables + processEmails, // Process pending emails manually + retryFailedEmails, // Retry failed emails + getMailing // Get mailing service instance +} from '@xtr-dev/payload-mailing' +``` + +## Migration Guide + +If you were using the old `sendEmail`/`scheduleEmail` methods, update your code: + +**Before:** +```typescript +await sendEmail(payload, { + templateSlug: 'welcome', + to: 'user@example.com', + variables: { name: 'John' } +}) +``` + +**After:** +```typescript +const { html, text, subject } = await renderTemplate(payload, 'welcome', { name: 'John' }) + +await payload.create({ + collection: 'emails', + data: { + to: ['user@example.com'], + subject, + html, + text + } +}) +``` + +This new approach gives you the full power and type safety of PayloadCMS while keeping the mailing functionality simple and consistent! 🚀 \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 226aed5..74cc6bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,8 +16,7 @@ export { default as Emails } from './collections/Emails.js' // Utility functions for developers export { getMailing, - sendEmail, - scheduleEmail, + renderTemplate, processEmails, retryFailedEmails, } from './utils/helpers.js' \ No newline at end of file diff --git a/src/services/MailingService.ts b/src/services/MailingService.ts index adfc803..f835cba 100644 --- a/src/services/MailingService.ts +++ b/src/services/MailingService.ts @@ -3,7 +3,7 @@ import { Liquid } from 'liquidjs' import nodemailer, { Transporter } from 'nodemailer' import { MailingPluginConfig, - SendEmailOptions, + TemplateVariables, MailingService as IMailingService, EmailTemplate, QueuedEmail, @@ -107,69 +107,21 @@ export class MailingService implements IMailingService { } } - async sendEmail(options: SendEmailOptions): Promise { - const emailId = await this.scheduleEmail({ - ...options, - scheduledAt: new Date() - }) + async renderTemplate(templateSlug: string, variables: TemplateVariables): Promise<{ html: string; text: string; subject: string }> { + const template = await this.getTemplateBySlug(templateSlug) - await this.processEmailItem(emailId) - - return emailId - } - - async scheduleEmail(options: SendEmailOptions): Promise { - let html = options.html || '' - let text = options.text || '' - let subject = options.subject || '' - let templateId: string | undefined = undefined - - if (options.templateSlug) { - const template = await this.getTemplateBySlug(options.templateSlug) - - if (template) { - templateId = template.id - const variables = options.variables || {} - const renderedContent = await this.renderEmailTemplate(template, variables) - html = renderedContent.html - text = renderedContent.text - subject = await this.renderTemplate(template.subject, variables) - } else { - throw new Error(`Email template not found: ${options.templateSlug}`) - } + if (!template) { + throw new Error(`Email template not found: ${templateSlug}`) } - if (!subject && !options.subject) { - throw new Error('Email subject is required') + const emailContent = await this.renderEmailTemplate(template, variables) + const subject = await this.renderTemplateString(template.subject, variables) + + return { + html: emailContent.html, + text: emailContent.text, + subject } - - if (!html && !options.html) { - throw new Error('Email HTML content is required') - } - - const queueData = { - template: templateId, - to: Array.isArray(options.to) ? options.to : [options.to], - cc: options.cc ? (Array.isArray(options.cc) ? options.cc : [options.cc]) : undefined, - bcc: options.bcc ? (Array.isArray(options.bcc) ? options.bcc : [options.bcc]) : undefined, - from: options.from || this.getDefaultFrom(), - replyTo: options.replyTo, - subject: subject || options.subject, - html, - text, - variables: options.variables, - scheduledAt: options.scheduledAt?.toISOString(), - status: 'pending' as const, - attempts: 0, - priority: options.priority || 5, - } - - const result = await this.payload.create({ - collection: this.emailsCollection as any, - data: queueData, - }) - - return result.id as string } async processEmails(): Promise { @@ -366,7 +318,7 @@ export class MailingService implements IMailingService { } } - private async renderTemplate(template: string, variables: Record): Promise { + private async renderTemplateString(template: string, variables: Record): Promise { // Use custom template renderer if provided if (this.config.templateRenderer) { try { @@ -434,8 +386,8 @@ export class MailingService implements IMailingService { let text = serializeRichTextToText(template.content) // Apply template variables to the rendered content - html = await this.renderTemplate(html, variables) - text = await this.renderTemplate(text, variables) + html = await this.renderTemplateString(html, variables) + text = await this.renderTemplateString(text, variables) return { html, text } } diff --git a/src/types/index.ts b/src/types/index.ts index 14bb179..b29b150 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,5 @@ import { Payload } from 'payload' -import type { CollectionConfig, RichTextField } from 'payload' +import type { CollectionConfig, RichTextField, TypedCollection } from 'payload' import { Transporter } from 'nodemailer' export interface EmailObject { @@ -83,26 +83,15 @@ export interface QueuedEmail { updatedAt: string } -export interface SendEmailOptions { - templateSlug?: string - to: string | string[] - cc?: string | string[] - bcc?: string | string[] - from?: string - replyTo?: string - subject?: string - html?: string - text?: string - variables?: Record - scheduledAt?: Date - priority?: number +// Simple helper type for template variables +export interface TemplateVariables { + [key: string]: any } export interface MailingService { - sendEmail(options: SendEmailOptions): Promise - scheduleEmail(options: SendEmailOptions): Promise processEmails(): Promise retryFailedEmails(): Promise + renderTemplate(templateSlug: string, variables: TemplateVariables): Promise<{ html: string; text: string; subject: string }> } export interface MailingContext { diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 38212f6..1e37e48 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,5 +1,5 @@ import { Payload } from 'payload' -import { SendEmailOptions } from '../types/index.js' +import { TemplateVariables } from '../types/index.js' export const getMailing = (payload: Payload) => { const mailing = (payload as any).mailing @@ -9,14 +9,9 @@ export const getMailing = (payload: Payload) => { return mailing } -export const sendEmail = async (payload: Payload, options: SendEmailOptions): Promise => { +export const renderTemplate = async (payload: Payload, templateSlug: string, variables: TemplateVariables): Promise<{ html: string; text: string; subject: string }> => { const mailing = getMailing(payload) - return mailing.service.sendEmail(options) -} - -export const scheduleEmail = async (payload: Payload, options: SendEmailOptions): Promise => { - const mailing = getMailing(payload) - return mailing.service.scheduleEmail(options) + return mailing.service.renderTemplate(templateSlug, variables) } export const processEmails = async (payload: Payload): Promise => {