mirror of
https://github.com/xtr-dev/payload-mailing.git
synced 2025-12-10 16:23:23 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72f3d7f66d | ||
| ecc0b0a73e | |||
| a959673fc1 | |||
| 8809db6aff | |||
|
|
5905f732de | ||
| 4c495a72b0 | |||
| 8518c716e8 | |||
| 570190be01 |
90
CUSTOM-TYPES.md
Normal file
90
CUSTOM-TYPES.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Using Custom ID Types
|
||||
|
||||
The mailing plugin now supports both `string` and `number` ID types. By default, it works with the generic `BaseEmailDocument` interface, but you can provide your own types for full type safety.
|
||||
|
||||
## Usage with Your Generated Types
|
||||
|
||||
When you have your own generated Payload types (e.g., from `payload generate:types`), you can use them with the mailing plugin:
|
||||
|
||||
```typescript
|
||||
import { sendEmail, BaseEmailDocument } from '@xtr-dev/payload-mailing'
|
||||
import { Email } from './payload-types' // Your generated types
|
||||
|
||||
// Option 1: Use your specific Email type
|
||||
const email = await sendEmail<Email>(payload, {
|
||||
template: {
|
||||
slug: 'welcome',
|
||||
variables: { name: 'John' }
|
||||
},
|
||||
data: {
|
||||
to: 'user@example.com',
|
||||
// All your custom fields are now type-safe
|
||||
}
|
||||
})
|
||||
|
||||
// Option 2: Extend BaseEmailDocument for custom fields
|
||||
interface MyEmail extends BaseEmailDocument {
|
||||
customField: string
|
||||
anotherField?: number
|
||||
}
|
||||
|
||||
const customEmail = await sendEmail<MyEmail>(payload, {
|
||||
data: {
|
||||
to: 'user@example.com',
|
||||
subject: 'Hello',
|
||||
html: '<p>Hello World</p>',
|
||||
customField: 'my value', // Type-safe!
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Compatibility
|
||||
|
||||
The plugin works with:
|
||||
- **String IDs**: `id: string`
|
||||
- **Number IDs**: `id: number`
|
||||
- **Nullable fields**: Fields can be `null`, `undefined`, or have values
|
||||
- **Generated types**: Works with `payload generate:types` output
|
||||
|
||||
Your Payload configuration determines which types are used. The plugin automatically adapts to your setup.
|
||||
|
||||
## Type Definitions
|
||||
|
||||
The base interfaces provided by the plugin:
|
||||
|
||||
```typescript
|
||||
interface BaseEmailDocument {
|
||||
id: string | number
|
||||
template?: any
|
||||
to: string[]
|
||||
cc?: string[] | null
|
||||
bcc?: string[] | null
|
||||
from?: string | null
|
||||
replyTo?: string | null
|
||||
subject: string
|
||||
html: string
|
||||
text?: string | null
|
||||
variables?: Record<string, any> | null
|
||||
scheduledAt?: string | null
|
||||
sentAt?: string | null
|
||||
status?: 'pending' | 'processing' | 'sent' | 'failed' | null
|
||||
attempts?: number | null
|
||||
lastAttemptAt?: string | null
|
||||
error?: string | null
|
||||
priority?: number | null
|
||||
createdAt?: string | null
|
||||
updatedAt?: string | null
|
||||
}
|
||||
|
||||
interface BaseEmailTemplateDocument {
|
||||
id: string | number
|
||||
name: string
|
||||
slug: string
|
||||
subject?: string | null
|
||||
content?: any
|
||||
createdAt?: string | null
|
||||
updatedAt?: string | null
|
||||
}
|
||||
```
|
||||
|
||||
These provide a foundation that works with any ID type while maintaining type safety for the core email functionality.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xtr-dev/payload-mailing",
|
||||
"version": "0.1.12",
|
||||
"version": "0.1.14",
|
||||
"description": "Template-based email system with scheduling and job processing for PayloadCMS",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { sendEmail } from '../sendEmail.js'
|
||||
import {Email, EmailTemplate} from '../payload-types.js'
|
||||
import { BaseEmailDocument } from '../types/index.js'
|
||||
|
||||
export interface SendEmailTaskInput {
|
||||
// Template mode fields
|
||||
@@ -170,7 +170,7 @@ export const sendEmailJob = {
|
||||
const sendEmailOptions = transformTaskInputToSendEmailOptions(taskInput)
|
||||
|
||||
// Use the sendEmail helper to create the email
|
||||
const email = await sendEmail<Email>(payload, sendEmailOptions)
|
||||
const email = await sendEmail<BaseEmailDocument>(payload, sendEmailOptions)
|
||||
|
||||
return {
|
||||
output: {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Payload } from 'payload'
|
||||
import { getMailing, renderTemplate, parseAndValidateEmails } from './utils/helpers.js'
|
||||
import {Email, EmailTemplate} from "./payload-types.js"
|
||||
import { BaseEmailDocument } from './types/index.js'
|
||||
|
||||
// Options for sending emails
|
||||
export interface SendEmailOptions<T extends Email = Email> {
|
||||
export interface SendEmailOptions<T extends BaseEmailDocument = BaseEmailDocument> {
|
||||
// Template-based email
|
||||
template?: {
|
||||
slug: string
|
||||
@@ -35,7 +35,7 @@ export interface SendEmailOptions<T extends Email = Email> {
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const sendEmail = async <TEmail extends Email = Email>(
|
||||
export const sendEmail = async <TEmail extends BaseEmailDocument = BaseEmailDocument>(
|
||||
payload: Payload,
|
||||
options: SendEmailOptions<TEmail>
|
||||
): Promise<TEmail> => {
|
||||
@@ -83,12 +83,22 @@ export const sendEmail = async <TEmail extends Email = Email>(
|
||||
if (emailData.to) {
|
||||
emailData.to = parseAndValidateEmails(emailData.to as string | string[])
|
||||
}
|
||||
if (emailData.cc) {
|
||||
if (emailData.cc && emailData.cc !== null) {
|
||||
emailData.cc = parseAndValidateEmails(emailData.cc as string | string[])
|
||||
}
|
||||
if (emailData.bcc) {
|
||||
if (emailData.bcc && emailData.bcc !== null) {
|
||||
emailData.bcc = parseAndValidateEmails(emailData.bcc as string | string[])
|
||||
}
|
||||
if (emailData.replyTo && emailData.replyTo !== null) {
|
||||
const validated = parseAndValidateEmails(emailData.replyTo as string | string[])
|
||||
// replyTo should be a single email, so take the first one if array
|
||||
emailData.replyTo = validated && validated.length > 0 ? validated[0] : undefined
|
||||
}
|
||||
if (emailData.from && emailData.from !== null) {
|
||||
const validated = parseAndValidateEmails(emailData.from as string | string[])
|
||||
// from should be a single email, so take the first one if array
|
||||
emailData.from = validated && validated.length > 0 ? validated[0] : undefined
|
||||
}
|
||||
|
||||
// Create the email in the collection with proper typing
|
||||
const email = await payload.create({
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
TemplateVariables,
|
||||
MailingService as IMailingService,
|
||||
MailingTransportConfig,
|
||||
BaseEmail, BaseEmailTemplate
|
||||
BaseEmail, BaseEmailTemplate, BaseEmailDocument, BaseEmailTemplateDocument
|
||||
} from '../types/index.js'
|
||||
import { serializeRichTextToHTML, serializeRichTextToText } from '../utils/richTextSerializer.js'
|
||||
|
||||
@@ -131,7 +131,7 @@ export class MailingService implements IMailingService {
|
||||
}
|
||||
|
||||
const emailContent = await this.renderEmailTemplate(template, variables)
|
||||
const subject = await this.renderTemplateString(template.subject, variables)
|
||||
const subject = await this.renderTemplateString(template.subject || '', variables)
|
||||
|
||||
return {
|
||||
html: emailContent.html,
|
||||
@@ -236,7 +236,7 @@ export class MailingService implements IMailingService {
|
||||
const email = await this.payload.findByID({
|
||||
collection: this.emailsCollection as any,
|
||||
id: emailId,
|
||||
}) as BaseEmail
|
||||
}) as BaseEmailDocument
|
||||
|
||||
const mailOptions = {
|
||||
from: email.from,
|
||||
@@ -300,7 +300,7 @@ export class MailingService implements IMailingService {
|
||||
return newAttempts
|
||||
}
|
||||
|
||||
private async getTemplateBySlug(templateSlug: string): Promise<BaseEmailTemplate | null> {
|
||||
private async getTemplateBySlug(templateSlug: string): Promise<BaseEmailTemplateDocument | null> {
|
||||
try {
|
||||
const { docs } = await this.payload.find({
|
||||
collection: this.templatesCollection as any,
|
||||
@@ -312,7 +312,7 @@ export class MailingService implements IMailingService {
|
||||
limit: 1,
|
||||
})
|
||||
|
||||
return docs.length > 0 ? docs[0] as BaseEmailTemplate : null
|
||||
return docs.length > 0 ? docs[0] as BaseEmailTemplateDocument : null
|
||||
} catch (error) {
|
||||
console.error(`Template with slug '${templateSlug}' not found:`, error)
|
||||
return null
|
||||
@@ -377,7 +377,7 @@ export class MailingService implements IMailingService {
|
||||
})
|
||||
}
|
||||
|
||||
private async renderEmailTemplate(template: BaseEmailTemplate, variables: Record<string, any> = {}): Promise<{ html: string; text: string }> {
|
||||
private async renderEmailTemplate(template: BaseEmailTemplateDocument, variables: Record<string, any> = {}): Promise<{ html: string; text: string }> {
|
||||
if (!template.content) {
|
||||
return { html: '', text: '' }
|
||||
}
|
||||
|
||||
@@ -1,11 +1,44 @@
|
||||
import { Payload } from 'payload'
|
||||
import type { CollectionConfig, RichTextField } from 'payload'
|
||||
import { Transporter } from 'nodemailer'
|
||||
import {Email, EmailTemplate} from "../payload-types.js"
|
||||
|
||||
export type BaseEmail<TEmail extends Email = Email, TEmailTemplate extends EmailTemplate = EmailTemplate> = Omit<TEmail, 'id' | 'template'> & {template: Omit<TEmailTemplate, 'id'> | TEmailTemplate['id'] | undefined | null}
|
||||
// Generic base interfaces that work with any ID type and null values
|
||||
export interface BaseEmailDocument {
|
||||
id: string | number
|
||||
template?: any
|
||||
to: string[]
|
||||
cc?: string[] | null
|
||||
bcc?: string[] | null
|
||||
from?: string | null
|
||||
replyTo?: string | null
|
||||
subject: string
|
||||
html: string
|
||||
text?: string | null
|
||||
variables?: Record<string, any> | null
|
||||
scheduledAt?: string | null
|
||||
sentAt?: string | null
|
||||
status?: 'pending' | 'processing' | 'sent' | 'failed' | null
|
||||
attempts?: number | null
|
||||
lastAttemptAt?: string | null
|
||||
error?: string | null
|
||||
priority?: number | null
|
||||
createdAt?: string | null
|
||||
updatedAt?: string | null
|
||||
}
|
||||
|
||||
export type BaseEmailTemplate<TEmailTemplate extends EmailTemplate = EmailTemplate> = Omit<TEmailTemplate, 'id'>
|
||||
export interface BaseEmailTemplateDocument {
|
||||
id: string | number
|
||||
name: string
|
||||
slug: string
|
||||
subject?: string | null
|
||||
content?: any
|
||||
createdAt?: string | null
|
||||
updatedAt?: string | null
|
||||
}
|
||||
|
||||
export type BaseEmail<TEmail extends BaseEmailDocument = BaseEmailDocument, TEmailTemplate extends BaseEmailTemplateDocument = BaseEmailTemplateDocument> = Omit<TEmail, 'id' | 'template'> & {template: Omit<TEmailTemplate, 'id'> | TEmailTemplate['id'] | undefined | null}
|
||||
|
||||
export type BaseEmailTemplate<TEmailTemplate extends BaseEmailTemplateDocument = BaseEmailTemplateDocument> = Omit<TEmailTemplate, 'id'>
|
||||
|
||||
export type TemplateRendererHook = (template: string, variables: Record<string, any>) => string | Promise<string>
|
||||
|
||||
@@ -42,23 +75,23 @@ export interface MailingTransportConfig {
|
||||
|
||||
export interface QueuedEmail {
|
||||
id: string
|
||||
template?: string
|
||||
template?: string | null
|
||||
to: string[]
|
||||
cc?: string[]
|
||||
bcc?: string[]
|
||||
from?: string
|
||||
replyTo?: string
|
||||
cc?: string[] | null
|
||||
bcc?: string[] | null
|
||||
from?: string | null
|
||||
replyTo?: string | null
|
||||
subject: string
|
||||
html: string
|
||||
text?: string
|
||||
variables?: Record<string, any>
|
||||
scheduledAt?: string
|
||||
sentAt?: string
|
||||
text?: string | null
|
||||
variables?: Record<string, any> | null
|
||||
scheduledAt?: string | null
|
||||
sentAt?: string | null
|
||||
status: 'pending' | 'processing' | 'sent' | 'failed'
|
||||
attempts: number
|
||||
lastAttemptAt?: string
|
||||
error?: string
|
||||
priority?: number
|
||||
lastAttemptAt?: string | null
|
||||
error?: string | null
|
||||
priority?: number | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user