mirror of
https://github.com/xtr-dev/payload-mailing.git
synced 2025-12-10 08:13:23 +00:00
Support custom ID types (string/number) for improved compatibility
- Replace hardcoded payload-types imports with generic BaseEmailDocument interface - Update sendEmail and sendEmailTask to work with both string and number IDs - Refactor MailingService to use generic document types instead of specific ones - Add BaseEmailDocument and BaseEmailTemplateDocument interfaces supporting id: string | number - Export BaseEmailDocument for users to extend with their custom fields - Fix TypeScript compilation error in template subject handling - Add CUSTOM-TYPES.md documentation for users with different ID types Fixes compatibility issue where plugin required number IDs but user projects used string IDs. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
88
CUSTOM-TYPES.md
Normal file
88
CUSTOM-TYPES.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# 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!
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## ID Type Compatibility
|
||||||
|
|
||||||
|
The plugin works with both:
|
||||||
|
- **String IDs**: `id: string`
|
||||||
|
- **Number IDs**: `id: number`
|
||||||
|
|
||||||
|
Your Payload configuration determines which type is 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[]
|
||||||
|
bcc?: string[]
|
||||||
|
from?: string
|
||||||
|
replyTo?: string
|
||||||
|
subject: string
|
||||||
|
html: string
|
||||||
|
text?: string
|
||||||
|
variables?: Record<string, any>
|
||||||
|
scheduledAt?: string
|
||||||
|
sentAt?: string
|
||||||
|
status?: 'pending' | 'processing' | 'sent' | 'failed'
|
||||||
|
attempts?: number
|
||||||
|
lastAttemptAt?: string
|
||||||
|
error?: string
|
||||||
|
priority?: number
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseEmailTemplateDocument {
|
||||||
|
id: string | number
|
||||||
|
name: string
|
||||||
|
slug: string
|
||||||
|
subject?: string
|
||||||
|
content?: any
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
These provide a foundation that works with any ID type while maintaining type safety for the core email functionality.
|
||||||
@@ -16,7 +16,7 @@ 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
|
// Main email sending function
|
||||||
export { sendEmail, type SendEmailOptions } from './sendEmail.js'
|
export { sendEmail, type SendEmailOptions, type BaseEmailDocument } from './sendEmail.js'
|
||||||
export { default as sendEmailDefault } from './sendEmail.js'
|
export { default as sendEmailDefault } from './sendEmail.js'
|
||||||
|
|
||||||
// Utility functions for developers
|
// Utility functions for developers
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { sendEmail } from '../sendEmail.js'
|
import { sendEmail, BaseEmailDocument } from '../sendEmail.js'
|
||||||
import {Email, EmailTemplate} from '../payload-types.js'
|
|
||||||
|
|
||||||
export interface SendEmailTaskInput {
|
export interface SendEmailTaskInput {
|
||||||
// Template mode fields
|
// Template mode fields
|
||||||
@@ -170,7 +169,7 @@ export const sendEmailJob = {
|
|||||||
const sendEmailOptions = transformTaskInputToSendEmailOptions(taskInput)
|
const sendEmailOptions = transformTaskInputToSendEmailOptions(taskInput)
|
||||||
|
|
||||||
// Use the sendEmail helper to create the email
|
// Use the sendEmail helper to create the email
|
||||||
const email = await sendEmail<Email>(payload, sendEmailOptions)
|
const email = await sendEmail<BaseEmailDocument>(payload, sendEmailOptions)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
output: {
|
output: {
|
||||||
|
|||||||
@@ -1,9 +1,32 @@
|
|||||||
import { Payload } from 'payload'
|
import { Payload } from 'payload'
|
||||||
import { getMailing, renderTemplate, parseAndValidateEmails } from './utils/helpers.js'
|
import { getMailing, renderTemplate, parseAndValidateEmails } from './utils/helpers.js'
|
||||||
import {Email, EmailTemplate} from "./payload-types.js"
|
|
||||||
|
// Generic email interface that can work with any ID type
|
||||||
|
export interface BaseEmailDocument {
|
||||||
|
id: string | number
|
||||||
|
template?: any
|
||||||
|
to: string[]
|
||||||
|
cc?: string[]
|
||||||
|
bcc?: string[]
|
||||||
|
from?: string
|
||||||
|
replyTo?: string
|
||||||
|
subject: string
|
||||||
|
html: string
|
||||||
|
text?: string
|
||||||
|
variables?: Record<string, any>
|
||||||
|
scheduledAt?: string
|
||||||
|
sentAt?: string
|
||||||
|
status?: 'pending' | 'processing' | 'sent' | 'failed'
|
||||||
|
attempts?: number
|
||||||
|
lastAttemptAt?: string
|
||||||
|
error?: string
|
||||||
|
priority?: number
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
// Options for sending emails
|
// Options for sending emails
|
||||||
export interface SendEmailOptions<T extends Email = Email> {
|
export interface SendEmailOptions<T extends BaseEmailDocument = BaseEmailDocument> {
|
||||||
// Template-based email
|
// Template-based email
|
||||||
template?: {
|
template?: {
|
||||||
slug: string
|
slug: string
|
||||||
@@ -35,7 +58,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,
|
payload: Payload,
|
||||||
options: SendEmailOptions<TEmail>
|
options: SendEmailOptions<TEmail>
|
||||||
): Promise<TEmail> => {
|
): Promise<TEmail> => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
TemplateVariables,
|
TemplateVariables,
|
||||||
MailingService as IMailingService,
|
MailingService as IMailingService,
|
||||||
MailingTransportConfig,
|
MailingTransportConfig,
|
||||||
BaseEmail, BaseEmailTemplate
|
BaseEmail, BaseEmailTemplate, BaseEmailDocument, BaseEmailTemplateDocument
|
||||||
} from '../types/index.js'
|
} from '../types/index.js'
|
||||||
import { serializeRichTextToHTML, serializeRichTextToText } from '../utils/richTextSerializer.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 emailContent = await this.renderEmailTemplate(template, variables)
|
||||||
const subject = await this.renderTemplateString(template.subject, variables)
|
const subject = await this.renderTemplateString(template.subject || '', variables)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
html: emailContent.html,
|
html: emailContent.html,
|
||||||
@@ -236,7 +236,7 @@ export class MailingService implements IMailingService {
|
|||||||
const email = await this.payload.findByID({
|
const email = await this.payload.findByID({
|
||||||
collection: this.emailsCollection as any,
|
collection: this.emailsCollection as any,
|
||||||
id: emailId,
|
id: emailId,
|
||||||
}) as BaseEmail
|
}) as BaseEmailDocument
|
||||||
|
|
||||||
const mailOptions = {
|
const mailOptions = {
|
||||||
from: email.from,
|
from: email.from,
|
||||||
@@ -300,7 +300,7 @@ export class MailingService implements IMailingService {
|
|||||||
return newAttempts
|
return newAttempts
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getTemplateBySlug(templateSlug: string): Promise<BaseEmailTemplate | null> {
|
private async getTemplateBySlug(templateSlug: string): Promise<BaseEmailTemplateDocument | null> {
|
||||||
try {
|
try {
|
||||||
const { docs } = await this.payload.find({
|
const { docs } = await this.payload.find({
|
||||||
collection: this.templatesCollection as any,
|
collection: this.templatesCollection as any,
|
||||||
@@ -312,7 +312,7 @@ export class MailingService implements IMailingService {
|
|||||||
limit: 1,
|
limit: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
return docs.length > 0 ? docs[0] as BaseEmailTemplate : null
|
return docs.length > 0 ? docs[0] as BaseEmailTemplateDocument : null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Template with slug '${templateSlug}' not found:`, error)
|
console.error(`Template with slug '${templateSlug}' not found:`, error)
|
||||||
return null
|
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) {
|
if (!template.content) {
|
||||||
return { html: '', text: '' }
|
return { html: '', text: '' }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,44 @@
|
|||||||
import { Payload } from 'payload'
|
import { Payload } from 'payload'
|
||||||
import type { CollectionConfig, RichTextField } from 'payload'
|
import type { CollectionConfig, RichTextField } from 'payload'
|
||||||
import { Transporter } from 'nodemailer'
|
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
|
||||||
|
export interface BaseEmailDocument {
|
||||||
|
id: string | number
|
||||||
|
template?: any
|
||||||
|
to: string[]
|
||||||
|
cc?: string[]
|
||||||
|
bcc?: string[]
|
||||||
|
from?: string
|
||||||
|
replyTo?: string
|
||||||
|
subject: string
|
||||||
|
html: string
|
||||||
|
text?: string
|
||||||
|
variables?: Record<string, any>
|
||||||
|
scheduledAt?: string
|
||||||
|
sentAt?: string
|
||||||
|
status?: 'pending' | 'processing' | 'sent' | 'failed'
|
||||||
|
attempts?: number
|
||||||
|
lastAttemptAt?: string
|
||||||
|
error?: string
|
||||||
|
priority?: number
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type BaseEmailTemplate<TEmailTemplate extends EmailTemplate = EmailTemplate> = Omit<TEmailTemplate, 'id'>
|
export interface BaseEmailTemplateDocument {
|
||||||
|
id: string | number
|
||||||
|
name: string
|
||||||
|
slug: string
|
||||||
|
subject?: string
|
||||||
|
content?: any
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
export type TemplateRendererHook = (template: string, variables: Record<string, any>) => string | Promise<string>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user