From f43178690703c9aa71c6ef8cafbd1c92383cc557 Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Mon, 6 Oct 2025 23:48:23 +0200 Subject: [PATCH 1/2] Fix template relationship population in sendEmail and bump version to 0.4.18 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sendEmail function now properly populates the template relationship field when using template-based emails. This ensures: - Template relationship is set on the email document - templateSlug field is auto-populated via beforeChange hook - beforeSend hook has access to the full template relationship - Proper record of which template was used for each email 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- package.json | 2 +- src/sendEmail.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 52a6ea7..7409fb6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xtr-dev/payload-mailing", - "version": "0.4.17", + "version": "0.4.18", "description": "Template-based email system with scheduling and job processing for PayloadCMS", "type": "module", "main": "dist/index.js", diff --git a/src/sendEmail.ts b/src/sendEmail.ts index 3b6d624..9768aad 100644 --- a/src/sendEmail.ts +++ b/src/sendEmail.ts @@ -50,6 +50,22 @@ export const sendEmail = async = { ...options.data } as Partial if (options.template) { + // Find the template document to get its ID for the relationship field + const templatesCollection = mailingConfig.collections.templates || 'email-templates' + const { docs: templateDocs } = await payload.find({ + collection: templatesCollection as any, + where: { + slug: { + equals: options.template.slug, + }, + }, + limit: 1, + }) + + if (!templateDocs || templateDocs.length === 0) { + throw new Error(`Template not found: ${options.template.slug}`) + } + const { html, text, subject } = await renderTemplate( payload, options.template.slug, @@ -58,6 +74,7 @@ export const sendEmail = async Date: Mon, 6 Oct 2025 23:55:31 +0200 Subject: [PATCH 2/2] Refactor template lookup to eliminate duplication and improve type safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: - Added MailingService.renderTemplateDocument() method to render from template document - Created renderTemplateWithId() helper that combines lookup and rendering in one operation - Updated sendEmail() to use renderTemplateWithId() instead of separate lookup and render - Added runtime validation to ensure template collection exists before querying - Eliminated duplicate template lookup (previously looked up twice per email send) Benefits: - Improved performance by reducing database queries from 2 to 1 per template-based email - Better error messages when template collection is misconfigured - Runtime validation complements TypeScript type assertions for safer code - Cleaner separation of concerns in sendEmail() function 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/sendEmail.ts | 23 +++-------------- src/services/MailingService.ts | 11 ++++++++ src/utils/helpers.ts | 47 ++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 19 deletions(-) diff --git a/src/sendEmail.ts b/src/sendEmail.ts index 9768aad..2fddda6 100644 --- a/src/sendEmail.ts +++ b/src/sendEmail.ts @@ -1,5 +1,5 @@ import { Payload } from 'payload' -import { getMailing, renderTemplate, parseAndValidateEmails, sanitizeFromName } from './utils/helpers.js' +import { getMailing, renderTemplateWithId, parseAndValidateEmails, sanitizeFromName } from './utils/helpers.js' import { BaseEmailDocument } from './types/index.js' import { processJobById } from './utils/emailProcessor.js' import { createContextLogger } from './utils/logger.js' @@ -50,23 +50,8 @@ export const sendEmail = async = { ...options.data } as Partial if (options.template) { - // Find the template document to get its ID for the relationship field - const templatesCollection = mailingConfig.collections.templates || 'email-templates' - const { docs: templateDocs } = await payload.find({ - collection: templatesCollection as any, - where: { - slug: { - equals: options.template.slug, - }, - }, - limit: 1, - }) - - if (!templateDocs || templateDocs.length === 0) { - throw new Error(`Template not found: ${options.template.slug}`) - } - - const { html, text, subject } = await renderTemplate( + // Look up and render the template in a single operation to avoid duplicate lookups + const { html, text, subject, templateId } = await renderTemplateWithId( payload, options.template.slug, options.template.variables || {} @@ -74,7 +59,7 @@ export const sendEmail = async { + this.ensureInitialized() + const emailContent = await this.renderEmailTemplate(template, variables) const subject = await this.renderTemplateString(template.subject || '', variables) diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 6e7de20..e3662fa 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -130,6 +130,53 @@ export const renderTemplate = async (payload: Payload, templateSlug: string, var return mailing.service.renderTemplate(templateSlug, variables) } +/** + * Render a template and return both rendered content and template ID + * This is used by sendEmail to avoid duplicate template lookups + * @internal + */ +export const renderTemplateWithId = async ( + payload: Payload, + templateSlug: string, + variables: TemplateVariables +): Promise<{ html: string; text: string; subject: string; templateId: PayloadID }> => { + const mailing = getMailing(payload) + const templatesCollection = mailing.config.collections?.templates || 'email-templates' + + // Runtime validation: Ensure the collection exists in Payload + if (!payload.collections[templatesCollection]) { + throw new Error( + `Templates collection '${templatesCollection}' not found. ` + + `Available collections: ${Object.keys(payload.collections).join(', ')}` + ) + } + + // Look up the template document once + const { docs: templateDocs } = await payload.find({ + collection: templatesCollection as any, + where: { + slug: { + equals: templateSlug, + }, + }, + limit: 1, + }) + + if (!templateDocs || templateDocs.length === 0) { + throw new Error(`Template not found: ${templateSlug}`) + } + + const templateDoc = templateDocs[0] + + // Render using the document directly to avoid duplicate lookup + const rendered = await mailing.service.renderTemplateDocument(templateDoc, variables) + + return { + ...rendered, + templateId: templateDoc.id, + } +} + export const processEmails = async (payload: Payload): Promise => { const mailing = getMailing(payload) return mailing.service.processEmails()