From 9520ec5ed131a3a652b325a3a91a9085336e74f3 Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Sat, 13 Sep 2025 23:00:41 +0200 Subject: [PATCH 1/4] Refactor email types for enhanced consistency and type safety - Replace `EmailTemplate` with `BaseEmailTemplate` for stricter type validation. - Update `sendEmail` and `sendEmailTask` to utilize refined `BaseEmail` structure. - Simplify type definitions in `MailingService` and related modules. --- src/jobs/sendEmailTask.ts | 4 ++-- src/sendEmail.ts | 14 +++++++------- src/services/MailingService.ts | 12 +++++------- src/types/index.ts | 16 ++++------------ 4 files changed, 18 insertions(+), 28 deletions(-) diff --git a/src/jobs/sendEmailTask.ts b/src/jobs/sendEmailTask.ts index 0c808af..eac8344 100644 --- a/src/jobs/sendEmailTask.ts +++ b/src/jobs/sendEmailTask.ts @@ -1,5 +1,5 @@ import { sendEmail } from '../sendEmail.js' -import { Email } from '../payload-types.js' +import {Email, EmailTemplate} from '../payload-types.js' import {BaseEmail} from "../types/index.js" export interface SendEmailTaskInput { @@ -161,7 +161,7 @@ export const sendEmailJob = { }) // Use the sendEmail helper to create the email - const email = await sendEmail(payload, sendEmailOptions) + const email = await sendEmail(payload, sendEmailOptions) return { output: { diff --git a/src/sendEmail.ts b/src/sendEmail.ts index e83052f..a14b1cc 100644 --- a/src/sendEmail.ts +++ b/src/sendEmail.ts @@ -1,6 +1,6 @@ import { Payload } from 'payload' import { getMailing, renderTemplate, parseAndValidateEmails } from './utils/helpers.js' -import {Email} from "./payload-types.js" +import {Email, EmailTemplate} from "./payload-types.js" import {BaseEmail} from "./types/index.js" // Options for sending emails @@ -36,14 +36,14 @@ export interface SendEmailOptions { * }) * ``` */ -export const sendEmail = async ( +export const sendEmail = async ( payload: Payload, - options: SendEmailOptions -): Promise => { + options: SendEmailOptions> +): Promise => { const mailing = getMailing(payload) const collectionSlug = options.collectionSlug || mailing.collections.emails || 'emails' - let emailData: Partial = { ...options.data } as Partial + let emailData: Partial = { ...options.data } as Partial // If using a template, render it first if (options.template) { @@ -59,7 +59,7 @@ export const sendEmail = async + } as Partial } // Validate required fields @@ -97,7 +97,7 @@ export const sendEmail = async { + private async getTemplateBySlug(templateSlug: string): Promise { try { const { docs } = await this.payload.find({ collection: this.templatesCollection as any, @@ -314,7 +312,7 @@ export class MailingService implements IMailingService { limit: 1, }) - return docs.length > 0 ? docs[0] as EmailTemplate : null + return docs.length > 0 ? docs[0] as BaseEmailTemplate : null } catch (error) { console.error(`Template with slug '${templateSlug}' not found:`, error) return null @@ -379,7 +377,7 @@ export class MailingService implements IMailingService { }) } - private async renderEmailTemplate(template: EmailTemplate, variables: Record = {}): Promise<{ html: string; text: string }> { + private async renderEmailTemplate(template: BaseEmailTemplate, variables: Record = {}): Promise<{ html: string; text: string }> { if (!template.content) { return { html: '', text: '' } } diff --git a/src/types/index.ts b/src/types/index.ts index c8a997c..d14d8d7 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,9 +1,11 @@ import { Payload } from 'payload' import type { CollectionConfig, RichTextField } from 'payload' import { Transporter } from 'nodemailer' -import {Email} from "../payload-types.js" +import {Email, EmailTemplate} from "../payload-types.js" -export type BaseEmail = Omit & {template: Omit} +export type BaseEmail = Omit & {template: Omit | TEmailTemplate['id'] | undefined | null} + +export type BaseEmailTemplate = Omit export type TemplateRendererHook = (template: string, variables: Record) => string | Promise @@ -37,16 +39,6 @@ export interface MailingTransportConfig { } } -export interface EmailTemplate { - id: string - name: string - slug: string - subject: string - content: any // Lexical editor state - createdAt: string - updatedAt: string -} - export interface QueuedEmail { id: string From 790eedfee7ec1cc7e3f9faf5044baa7b50fa9fe4 Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Sat, 13 Sep 2025 23:01:06 +0200 Subject: [PATCH 2/4] Bump package version to 0.1.12 in `package.json`. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7dce23a..5bf82df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xtr-dev/payload-mailing", - "version": "0.1.11", + "version": "0.1.12", "description": "Template-based email system with scheduling and job processing for PayloadCMS", "type": "module", "main": "dist/index.js", From e7304fe1a2fbf4a42c0c5b8892abf5e158cac319 Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Sat, 13 Sep 2025 23:06:02 +0200 Subject: [PATCH 3/4] Improve type safety, error handling, and code maintainability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Simplify sendEmail generic constraints for better type safety - Add validation before type assertions in sendEmail - Preserve error stack traces in sendEmailTask error handling - Extract field copying logic into reusable helper function - Improve code documentation and separation of concerns 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/jobs/sendEmailTask.ts | 85 +++++++++++++++++++++++---------------- src/sendEmail.ts | 11 +++-- 2 files changed, 59 insertions(+), 37 deletions(-) diff --git a/src/jobs/sendEmailTask.ts b/src/jobs/sendEmailTask.ts index eac8344..e5a147f 100644 --- a/src/jobs/sendEmailTask.ts +++ b/src/jobs/sendEmailTask.ts @@ -23,6 +23,45 @@ export interface SendEmailTaskInput { [key: string]: any } +/** + * Transforms task input into sendEmail options by separating template and data fields + */ +function transformTaskInputToSendEmailOptions(taskInput: SendEmailTaskInput) { + const sendEmailOptions: any = { + data: {} + } + + // If using template mode, set template options + if (taskInput.templateSlug) { + sendEmailOptions.template = { + slug: taskInput.templateSlug, + variables: taskInput.variables || {} + } + } + + // Standard email fields that should be copied to data + const standardFields = ['to', 'cc', 'bcc', 'subject', 'html', 'text', 'scheduledAt', 'priority'] + + // Template-specific fields that should not be copied to data + const templateFields = ['templateSlug', 'variables'] + + // Copy standard fields to data + standardFields.forEach(field => { + if (taskInput[field] !== undefined) { + sendEmailOptions.data[field] = taskInput[field] + } + }) + + // Copy any additional custom fields that aren't template or standard fields + Object.keys(taskInput).forEach(key => { + if (!templateFields.includes(key) && !standardFields.includes(key)) { + sendEmailOptions.data[key] = taskInput[key] + } + }) + + return sendEmailOptions +} + export const sendEmailJob = { slug: 'send-email', label: 'Send Email', @@ -128,40 +167,11 @@ export const sendEmailJob = { const taskInput = input as SendEmailTaskInput try { - // Prepare options for sendEmail based on task input - const sendEmailOptions: any = { - data: {} - } - - // If using template mode - if (taskInput.templateSlug) { - sendEmailOptions.template = { - slug: taskInput.templateSlug, - variables: taskInput.variables || {} - } - } - - // Build data object from task input - const dataFields = ['to', 'cc', 'bcc', 'subject', 'html', 'text', 'scheduledAt', 'priority'] - const additionalFields: string[] = [] - - // Copy standard fields - dataFields.forEach(field => { - if (taskInput[field] !== undefined) { - sendEmailOptions.data[field] = taskInput[field] - } - }) - - // Copy any additional custom fields - Object.keys(taskInput).forEach(key => { - if (!['templateSlug', 'variables', ...dataFields].includes(key)) { - sendEmailOptions.data[key] = taskInput[key] - additionalFields.push(key) - } - }) + // Transform task input into sendEmail options using helper function + const sendEmailOptions = transformTaskInputToSendEmailOptions(taskInput) // Use the sendEmail helper to create the email - const email = await sendEmail(payload, sendEmailOptions) + const email = await sendEmail(payload, sendEmailOptions) return { output: { @@ -171,8 +181,15 @@ export const sendEmailJob = { } } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error' - throw new Error(`Failed to queue email: ${errorMessage}`) + if (error instanceof Error) { + // Preserve original error and stack trace + const wrappedError = new Error(`Failed to queue email: ${error.message}`) + wrappedError.stack = error.stack + wrappedError.cause = error + throw wrappedError + } else { + throw new Error(`Failed to queue email: ${String(error)}`) + } } } } diff --git a/src/sendEmail.ts b/src/sendEmail.ts index a14b1cc..5729b6c 100644 --- a/src/sendEmail.ts +++ b/src/sendEmail.ts @@ -4,7 +4,7 @@ import {Email, EmailTemplate} from "./payload-types.js" import {BaseEmail} from "./types/index.js" // Options for sending emails -export interface SendEmailOptions { +export interface SendEmailOptions { // Template-based email template?: { slug: string @@ -36,9 +36,9 @@ export interface SendEmailOptions { * }) * ``` */ -export const sendEmail = async ( +export const sendEmail = async ( payload: Payload, - options: SendEmailOptions> + options: SendEmailOptions ): Promise => { const mailing = getMailing(payload) const collectionSlug = options.collectionSlug || mailing.collections.emails || 'emails' @@ -97,6 +97,11 @@ export const sendEmail = async Date: Sat, 13 Sep 2025 23:10:20 +0200 Subject: [PATCH 4/4] Remove unused BaseEmail imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove BaseEmail import from sendEmail.ts (no longer used after type refactoring) - Remove BaseEmail import from sendEmailTask.ts (no longer used after type refactoring) - BaseEmail types are still used in MailingService.ts for proper type casting 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/jobs/sendEmailTask.ts | 1 - src/sendEmail.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/jobs/sendEmailTask.ts b/src/jobs/sendEmailTask.ts index e5a147f..ac8e067 100644 --- a/src/jobs/sendEmailTask.ts +++ b/src/jobs/sendEmailTask.ts @@ -1,6 +1,5 @@ import { sendEmail } from '../sendEmail.js' import {Email, EmailTemplate} from '../payload-types.js' -import {BaseEmail} from "../types/index.js" export interface SendEmailTaskInput { // Template mode fields diff --git a/src/sendEmail.ts b/src/sendEmail.ts index 5729b6c..25f4616 100644 --- a/src/sendEmail.ts +++ b/src/sendEmail.ts @@ -1,7 +1,6 @@ import { Payload } from 'payload' import { getMailing, renderTemplate, parseAndValidateEmails } from './utils/helpers.js' import {Email, EmailTemplate} from "./payload-types.js" -import {BaseEmail} from "./types/index.js" // Options for sending emails export interface SendEmailOptions {