Compare commits

...

3 Commits

Author SHA1 Message Date
Bas
57984e8633 Merge pull request #65 from xtr-dev/dev
Fix template relationship population in sendEmail and bump version to…
2025-10-06 23:58:25 +02:00
d15fa454a0 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>
2025-10-06 23:55:31 +02:00
f431786907 Fix template relationship population in sendEmail and bump version to 0.4.18
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 <noreply@anthropic.com>
2025-10-06 23:48:23 +02:00
4 changed files with 63 additions and 3 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@xtr-dev/payload-mailing", "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", "description": "Template-based email system with scheduling and job processing for PayloadCMS",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

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,7 +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) {
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, payload,
options.template.slug, options.template.slug,
options.template.variables || {} options.template.variables || {}
@@ -58,6 +59,7 @@ export const sendEmail = async <TEmail extends BaseEmailDocument = BaseEmailDocu
emailData = { emailData = {
...emailData, ...emailData,
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()