Refactor template lookup to eliminate duplication and improve type safety

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 <noreply@anthropic.com>
This commit is contained in:
2025-10-06 23:55:31 +02:00
parent f431786907
commit d15fa454a0
3 changed files with 62 additions and 19 deletions

View File

@@ -1,5 +1,5 @@
import { Payload } from 'payload' 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 { BaseEmailDocument } from './types/index.js'
import { processJobById } from './utils/emailProcessor.js' import { processJobById } from './utils/emailProcessor.js'
import { createContextLogger } from './utils/logger.js' import { createContextLogger } from './utils/logger.js'
@@ -50,23 +50,8 @@ export const sendEmail = async <TEmail extends BaseEmailDocument = BaseEmailDocu
let emailData: Partial<TEmail> = { ...options.data } as Partial<TEmail> let emailData: Partial<TEmail> = { ...options.data } as Partial<TEmail>
if (options.template) { if (options.template) {
// Find the template document to get its ID for the relationship field // Look up and render the template in a single operation to avoid duplicate lookups
const templatesCollection = mailingConfig.collections.templates || 'email-templates' const { html, text, subject, templateId } = await renderTemplateWithId(
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, payload,
options.template.slug, options.template.slug,
options.template.variables || {} options.template.variables || {}
@@ -74,7 +59,7 @@ export const sendEmail = async <TEmail extends BaseEmailDocument = BaseEmailDocu
emailData = { emailData = {
...emailData, ...emailData,
template: templateDocs[0].id, template: templateId,
subject, subject,
html, html,
text, text,

View File

@@ -127,6 +127,17 @@ export class MailingService implements IMailingService {
throw new Error(`Email template not found: ${templateSlug}`) throw new Error(`Email template not found: ${templateSlug}`)
} }
return this.renderTemplateDocument(template, variables)
}
/**
* Render a template document (for when you already have the template loaded)
* This avoids duplicate template lookups
* @internal
*/
async renderTemplateDocument(template: BaseEmailTemplateDocument, variables: TemplateVariables): Promise<{ html: string; text: string; subject: string }> {
this.ensureInitialized()
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)

View File

@@ -130,6 +130,53 @@ export const renderTemplate = async (payload: Payload, templateSlug: string, var
return mailing.service.renderTemplate(templateSlug, variables) 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<void> => { export const processEmails = async (payload: Payload): Promise<void> => {
const mailing = getMailing(payload) const mailing = getMailing(payload)
return mailing.service.processEmails() return mailing.service.processEmails()