Improve type safety, error handling, and code maintainability

- 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 <noreply@anthropic.com>
This commit is contained in:
2025-09-13 23:06:02 +02:00
parent 790eedfee7
commit e7304fe1a2
2 changed files with 59 additions and 37 deletions

View File

@@ -23,6 +23,45 @@ export interface SendEmailTaskInput {
[key: string]: any [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 = { export const sendEmailJob = {
slug: 'send-email', slug: 'send-email',
label: 'Send Email', label: 'Send Email',
@@ -128,40 +167,11 @@ export const sendEmailJob = {
const taskInput = input as SendEmailTaskInput const taskInput = input as SendEmailTaskInput
try { try {
// Prepare options for sendEmail based on task input // Transform task input into sendEmail options using helper function
const sendEmailOptions: any = { const sendEmailOptions = transformTaskInputToSendEmailOptions(taskInput)
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)
}
})
// Use the sendEmail helper to create the email // Use the sendEmail helper to create the email
const email = await sendEmail<Email, EmailTemplate>(payload, sendEmailOptions) const email = await sendEmail<Email>(payload, sendEmailOptions)
return { return {
output: { output: {
@@ -171,8 +181,15 @@ export const sendEmailJob = {
} }
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error' if (error instanceof Error) {
throw new Error(`Failed to queue email: ${errorMessage}`) // 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)}`)
}
} }
} }
} }

View File

@@ -4,7 +4,7 @@ import {Email, EmailTemplate} from "./payload-types.js"
import {BaseEmail} from "./types/index.js" import {BaseEmail} from "./types/index.js"
// Options for sending emails // Options for sending emails
export interface SendEmailOptions<T extends BaseEmail = BaseEmail> { export interface SendEmailOptions<T extends Email = Email> {
// Template-based email // Template-based email
template?: { template?: {
slug: string slug: string
@@ -36,9 +36,9 @@ export interface SendEmailOptions<T extends BaseEmail = BaseEmail> {
* }) * })
* ``` * ```
*/ */
export const sendEmail = async <TEmail extends Email = Email, TEmailTemplate extends EmailTemplate = EmailTemplate>( export const sendEmail = async <TEmail extends Email = Email>(
payload: Payload, payload: Payload,
options: SendEmailOptions<BaseEmail<TEmail, TEmailTemplate>> options: SendEmailOptions<TEmail>
): Promise<TEmail> => { ): Promise<TEmail> => {
const mailing = getMailing(payload) const mailing = getMailing(payload)
const collectionSlug = options.collectionSlug || mailing.collections.emails || 'emails' const collectionSlug = options.collectionSlug || mailing.collections.emails || 'emails'
@@ -97,6 +97,11 @@ export const sendEmail = async <TEmail extends Email = Email, TEmailTemplate ext
data: emailData data: emailData
}) })
// Validate that the created email has the expected structure
if (!email || typeof email !== 'object' || !email.id) {
throw new Error('Failed to create email: invalid response from database')
}
return email as TEmail return email as TEmail
} }