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:
2025-09-13 23:18:37 +02:00
parent 79044b7bc3
commit 570190be01
6 changed files with 159 additions and 16 deletions

88
CUSTOM-TYPES.md Normal file
View 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.

View File

@@ -16,7 +16,7 @@ export { mailingJobs, sendEmailJob } from './jobs/index.js'
export type { SendEmailTaskInput } from './jobs/sendEmailTask.js'
// 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'
// Utility functions for developers

View File

@@ -1,5 +1,4 @@
import { sendEmail } from '../sendEmail.js'
import {Email, EmailTemplate} from '../payload-types.js'
import { sendEmail, BaseEmailDocument } from '../sendEmail.js'
export interface SendEmailTaskInput {
// Template mode fields
@@ -170,7 +169,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: {

View File

@@ -1,9 +1,32 @@
import { Payload } from 'payload'
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
export interface SendEmailOptions<T extends Email = Email> {
export interface SendEmailOptions<T extends BaseEmailDocument = BaseEmailDocument> {
// Template-based email
template?: {
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,
options: SendEmailOptions<TEmail>
): Promise<TEmail> => {

View File

@@ -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: '' }
}

View File

@@ -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
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>