Compare commits

..

4 Commits

Author SHA1 Message Date
Bas
0c4d894f51 Merge pull request #22 from xtr-dev/dev
Move sendEmail to dedicated file for better visibility
2025-09-13 20:58:13 +02:00
1af54c6573 Bump package version to 0.1.6 in package.json. 2025-09-13 20:55:00 +02:00
24f1f4c5a4 Fix broken development routes and imports
Critical fixes:
- Update dev/app/api/test-email/route.ts to use new sendEmail API instead of deprecated sendEmail/scheduleEmail
- Fix dev/test-plugin.mjs imports to remove scheduleEmail reference
- Update dev/README.md examples to use new sendEmail pattern
- Replace templateId with template.slug throughout dev examples
- Add support for direct HTML emails in test route

The development routes now work correctly with v0.1.5 API changes.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-13 20:53:54 +02:00
de41f4ecb2 Move sendEmail to dedicated file for better visibility
- Extract sendEmail function to src/sendEmail.ts as primary module
- Export BaseEmailData and SendEmailOptions interfaces alongside
- Update all imports to use new location
- Add sendEmailDefault export for CommonJS compatibility
- Export parseAndValidateEmails for external utility use
- Updated README to highlight sendEmail as primary export

Breaking change: BaseEmailData and SendEmailOptions now imported from main module, not utils/helpers

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-13 20:46:38 +02:00
9 changed files with 188 additions and 150 deletions

View File

@@ -59,6 +59,7 @@ export default buildConfig({
### 2. Send emails with type-safe helper ### 2. Send emails with type-safe helper
```typescript ```typescript
// sendEmail is a primary export for easy access
import { sendEmail } from '@xtr-dev/payload-mailing' import { sendEmail } from '@xtr-dev/payload-mailing'
import { Email } from './payload-types' // Your generated types import { Email } from './payload-types' // Your generated types

View File

@@ -184,25 +184,43 @@ The plugin automatically processes the outbox every 5 minutes and retries failed
## Plugin API Usage ## Plugin API Usage
```javascript ```javascript
import { sendEmail, scheduleEmail } from '@xtr-dev/payload-mailing' import { sendEmail } from '@xtr-dev/payload-mailing'
// Send immediate email // Send immediate email with template
const emailId = await sendEmail(payload, { const email = await sendEmail(payload, {
templateId: 'welcome-template-id', template: {
to: 'user@example.com', slug: 'welcome-email',
variables: { variables: {
firstName: 'John', firstName: 'John',
siteName: 'My App' siteName: 'My App'
}
},
data: {
to: 'user@example.com',
} }
}) })
// Schedule email // Schedule email for later
const scheduledId = await scheduleEmail(payload, { const scheduledEmail = await sendEmail(payload, {
templateId: 'reminder-template-id', template: {
to: 'user@example.com', slug: 'reminder',
scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours variables: {
variables: { eventName: 'Product Launch'
eventName: 'Product Launch' }
},
data: {
to: 'user@example.com',
scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
}
})
// Send direct HTML email (no template)
const directEmail = await sendEmail(payload, {
data: {
to: 'user@example.com',
subject: 'Direct Email',
html: '<h1>Hello World</h1>',
text: 'Hello World'
} }
}) })
``` ```

View File

@@ -1,37 +1,50 @@
import { getPayload } from 'payload' import { getPayload } from 'payload'
import config from '@payload-config' import config from '@payload-config'
import { sendEmail, scheduleEmail } from '@xtr-dev/payload-mailing' import { sendEmail } from '@xtr-dev/payload-mailing'
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
const payload = await getPayload({ config }) const payload = await getPayload({ config })
const body = await request.json() const body = await request.json()
const { type = 'send', templateSlug, to, variables, scheduledAt } = body const { type = 'send', templateSlug, to, variables, scheduledAt, subject, html, text } = body
let result // Use the new sendEmail API
if (type === 'send') { const emailOptions: any = {
// Send immediately data: {
result = await sendEmail(payload, {
templateSlug,
to, to,
variables, }
})
} else if (type === 'schedule') {
// Schedule for later
result = await scheduleEmail(payload, {
templateSlug,
to,
variables,
scheduledAt: scheduledAt ? new Date(scheduledAt) : new Date(Date.now() + 60000), // Default to 1 minute
})
} else {
return Response.json({ error: 'Invalid type. Use "send" or "schedule"' }, { status: 400 })
} }
// Add template if provided
if (templateSlug) {
emailOptions.template = {
slug: templateSlug,
variables: variables || {}
}
} else if (subject && html) {
// Direct email without template
emailOptions.data.subject = subject
emailOptions.data.html = html
if (text) {
emailOptions.data.text = text
}
} else {
return Response.json({
error: 'Either templateSlug or subject+html must be provided'
}, { status: 400 })
}
// Add scheduling if needed
if (type === 'schedule' || scheduledAt) {
emailOptions.data.scheduledAt = scheduledAt ? new Date(scheduledAt) : new Date(Date.now() + 60000)
}
const result = await sendEmail(payload, emailOptions)
return Response.json({ return Response.json({
success: true, success: true,
emailId: result, emailId: result.id,
message: type === 'send' ? 'Email sent successfully' : 'Email scheduled successfully', message: scheduledAt ? 'Email scheduled successfully' : 'Email queued successfully',
}) })
} catch (error) { } catch (error) {
console.error('Test email error:', error) console.error('Test email error:', error)

View File

@@ -1,10 +1,10 @@
// Simple test to verify plugin can be imported and initialized // Simple test to verify plugin can be imported and initialized
import { mailingPlugin, sendEmail, scheduleEmail } from '@xtr-dev/payload-mailing' import { mailingPlugin, sendEmail, renderTemplate } from '@xtr-dev/payload-mailing'
console.log('✅ Plugin imports successfully') console.log('✅ Plugin imports successfully')
console.log('✅ mailingPlugin:', typeof mailingPlugin) console.log('✅ mailingPlugin:', typeof mailingPlugin)
console.log('✅ sendEmail:', typeof sendEmail) console.log('✅ sendEmail:', typeof sendEmail)
console.log('✅ scheduleEmail:', typeof scheduleEmail) console.log('✅ renderTemplate:', typeof renderTemplate)
// Test plugin configuration // Test plugin configuration
try { try {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@xtr-dev/payload-mailing", "name": "@xtr-dev/payload-mailing",
"version": "0.1.5", "version": "0.1.6",
"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",

View File

@@ -15,13 +15,15 @@ export { default as Emails } from './collections/Emails.js'
export { mailingJobs, sendEmailJob } from './jobs/index.js' export { mailingJobs, sendEmailJob } from './jobs/index.js'
export type { SendEmailTaskInput } from './jobs/sendEmailTask.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 // Utility functions for developers
export { export {
getMailing, getMailing,
renderTemplate, renderTemplate,
processEmails, processEmails,
retryFailedEmails, retryFailedEmails,
sendEmail, parseAndValidateEmails,
type BaseEmailData,
type SendEmailOptions,
} from './utils/helpers.js' } from './utils/helpers.js'

View File

@@ -1,4 +1,4 @@
import { sendEmail, type BaseEmailData } from '../utils/helpers.js' import { sendEmail, type BaseEmailData } from '../sendEmail.js'
export interface SendEmailTaskInput { export interface SendEmailTaskInput {
// Template mode fields // Template mode fields

110
src/sendEmail.ts Normal file
View File

@@ -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<T extends BaseEmailData = BaseEmailData> {
// Template-based email
template?: {
slug: string
variables?: Record<string, any>
}
// Direct email data
data?: Partial<T>
// 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<Email>(payload, {
* template: {
* slug: 'welcome',
* variables: { name: 'John' }
* },
* data: {
* to: 'user@example.com',
* customField: 'value' // Your custom fields are type-safe!
* }
* })
* ```
*/
export const sendEmail = async <T extends BaseEmailData = BaseEmailData>(
payload: Payload,
options: SendEmailOptions<T>
): Promise<T> => {
const mailing = getMailing(payload)
const collectionSlug = options.collectionSlug || mailing.collections.emails || 'emails'
let emailData: Partial<T> = { ...options.data } as Partial<T>
// 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<T>
}
// 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

View File

@@ -1,32 +1,6 @@
import { Payload } from 'payload' import { Payload } from 'payload'
import { TemplateVariables } from '../types/index.js' 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<T extends BaseEmailData = BaseEmailData> {
// Template-based email
template?: {
slug: string
variables?: Record<string, any>
}
// Direct email data
data?: Partial<T>
// Common options
collectionSlug?: string // defaults to 'emails'
}
/** /**
* Parse and validate email addresses * Parse and validate email addresses
* @internal * @internal
@@ -72,84 +46,4 @@ export const processEmails = async (payload: Payload): Promise<void> => {
export const retryFailedEmails = async (payload: Payload): Promise<void> => { export const retryFailedEmails = async (payload: Payload): Promise<void> => {
const mailing = getMailing(payload) const mailing = getMailing(payload)
return mailing.service.retryFailedEmails() 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<Email>(payload, {
* template: {
* slug: 'welcome',
* variables: { name: 'John' }
* },
* data: {
* to: 'user@example.com',
* customField: 'value' // Your custom fields are type-safe!
* }
* })
* ```
*/
export const sendEmail = async <T extends BaseEmailData = BaseEmailData>(
payload: Payload,
options: SendEmailOptions<T>
): Promise<T> => {
const mailing = getMailing(payload)
const collectionSlug = options.collectionSlug || mailing.collections.emails || 'emails'
let emailData: Partial<T> = { ...options.data } as Partial<T>
// 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<T>
}
// 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
} }