mirror of
https://github.com/xtr-dev/payload-mailing.git
synced 2025-12-10 00:03:23 +00:00
🚀 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:
@@ -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
167
simplified-api-guide.md
Normal 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! 🚀
|
||||||
@@ -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'
|
||||||
@@ -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,69 +107,21 @@ 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> {
|
|
||||||
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 (!subject && !options.subject) {
|
const emailContent = await this.renderEmailTemplate(template, variables)
|
||||||
throw new Error('Email subject is required')
|
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<void> {
|
async processEmails(): Promise<void> {
|
||||||
@@ -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 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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> => {
|
||||||
|
|||||||
Reference in New Issue
Block a user