🚀 BREAKING: Simplify API to use Payload collections directly

- 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 <noreply@anthropic.com>
This commit is contained in:
2025-09-13 18:23:05 +02:00
parent cfc3ce5a7e
commit 74f565ab4e
6 changed files with 192 additions and 90 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@xtr-dev/payload-mailing", "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", "description": "Template-based email system with scheduling and job processing for PayloadCMS",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

167
simplified-api-guide.md Normal file
View File

@@ -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: '<h1>Server Error</h1><p>Please check the logs immediately.</p>',
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! 🚀

View File

@@ -16,8 +16,7 @@ export { default as Emails } from './collections/Emails.js'
// Utility functions for developers // Utility functions for developers
export { export {
getMailing, getMailing,
sendEmail, renderTemplate,
scheduleEmail,
processEmails, processEmails,
retryFailedEmails, retryFailedEmails,
} from './utils/helpers.js' } from './utils/helpers.js'

View File

@@ -3,7 +3,7 @@ import { Liquid } from 'liquidjs'
import nodemailer, { Transporter } from 'nodemailer' import nodemailer, { Transporter } from 'nodemailer'
import { import {
MailingPluginConfig, MailingPluginConfig,
SendEmailOptions, TemplateVariables,
MailingService as IMailingService, MailingService as IMailingService,
EmailTemplate, EmailTemplate,
QueuedEmail, QueuedEmail,
@@ -107,71 +107,23 @@ export class MailingService implements IMailingService {
} }
} }
async sendEmail(options: SendEmailOptions): Promise<string> { async renderTemplate(templateSlug: string, variables: TemplateVariables): Promise<{ html: string; text: string; subject: string }> {
const emailId = await this.scheduleEmail({ const template = await this.getTemplateBySlug(templateSlug)
...options,
scheduledAt: new Date()
})
await this.processEmailItem(emailId) if (!template) {
throw new Error(`Email template not found: ${templateSlug}`)
return emailId
} }
async scheduleEmail(options: SendEmailOptions): Promise<string> { const emailContent = await this.renderEmailTemplate(template, variables)
let html = options.html || '' const subject = await this.renderTemplateString(template.subject, variables)
let text = options.text || ''
let subject = options.subject || ''
let templateId: string | undefined = undefined
if (options.templateSlug) { return {
const template = await this.getTemplateBySlug(options.templateSlug) html: emailContent.html,
text: emailContent.text,
if (template) { subject
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 (!subject && !options.subject) {
throw new Error('Email subject is required')
}
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<void> { async processEmails(): Promise<void> {
const currentTime = new Date().toISOString() const currentTime = new Date().toISOString()
@@ -366,7 +318,7 @@ export class MailingService implements IMailingService {
} }
} }
private async renderTemplate(template: string, variables: Record<string, any>): Promise<string> { private async renderTemplateString(template: string, variables: Record<string, any>): Promise<string> {
// Use custom template renderer if provided // Use custom template renderer if provided
if (this.config.templateRenderer) { if (this.config.templateRenderer) {
try { try {
@@ -434,8 +386,8 @@ export class MailingService implements IMailingService {
let text = serializeRichTextToText(template.content) let text = serializeRichTextToText(template.content)
// Apply template variables to the rendered content // Apply template variables to the rendered content
html = await this.renderTemplate(html, variables) html = await this.renderTemplateString(html, variables)
text = await this.renderTemplate(text, variables) text = await this.renderTemplateString(text, variables)
return { html, text } return { html, text }
} }

View File

@@ -1,5 +1,5 @@
import { Payload } from 'payload' import { Payload } from 'payload'
import type { CollectionConfig, RichTextField } from 'payload' import type { CollectionConfig, RichTextField, TypedCollection } from 'payload'
import { Transporter } from 'nodemailer' import { Transporter } from 'nodemailer'
export interface EmailObject { export interface EmailObject {
@@ -83,26 +83,15 @@ export interface QueuedEmail {
updatedAt: string updatedAt: string
} }
export interface SendEmailOptions { // Simple helper type for template variables
templateSlug?: string export interface TemplateVariables {
to: string | string[] [key: string]: any
cc?: string | string[]
bcc?: string | string[]
from?: string
replyTo?: string
subject?: string
html?: string
text?: string
variables?: Record<string, any>
scheduledAt?: Date
priority?: number
} }
export interface MailingService { export interface MailingService {
sendEmail(options: SendEmailOptions): Promise<string>
scheduleEmail(options: SendEmailOptions): Promise<string>
processEmails(): Promise<void> processEmails(): Promise<void>
retryFailedEmails(): Promise<void> retryFailedEmails(): Promise<void>
renderTemplate(templateSlug: string, variables: TemplateVariables): Promise<{ html: string; text: string; subject: string }>
} }
export interface MailingContext { export interface MailingContext {

View File

@@ -1,5 +1,5 @@
import { Payload } from 'payload' import { Payload } from 'payload'
import { SendEmailOptions } from '../types/index.js' import { TemplateVariables } from '../types/index.js'
export const getMailing = (payload: Payload) => { export const getMailing = (payload: Payload) => {
const mailing = (payload as any).mailing const mailing = (payload as any).mailing
@@ -9,14 +9,9 @@ export const getMailing = (payload: Payload) => {
return mailing return mailing
} }
export const sendEmail = async (payload: Payload, options: SendEmailOptions): Promise<string> => { export const renderTemplate = async (payload: Payload, templateSlug: string, variables: TemplateVariables): Promise<{ html: string; text: string; subject: string }> => {
const mailing = getMailing(payload) const mailing = getMailing(payload)
return mailing.service.sendEmail(options) return mailing.service.renderTemplate(templateSlug, variables)
}
export const scheduleEmail = async (payload: Payload, options: SendEmailOptions): Promise<string> => {
const mailing = getMailing(payload)
return mailing.service.scheduleEmail(options)
} }
export const processEmails = async (payload: Payload): Promise<void> => { export const processEmails = async (payload: Payload): Promise<void> => {