diff --git a/packages/email-plugin/src/plugin.spec.ts b/packages/email-plugin/src/plugin.spec.ts index 7e8a7c15a5..9b4577b8df 100644 --- a/packages/email-plugin/src/plugin.spec.ts +++ b/packages/email-plugin/src/plugin.spec.ts @@ -6,6 +6,7 @@ import { DefaultLogger, EventBus, Injector, + JobQueueService, LanguageCode, Logger, LogLevel, @@ -239,6 +240,37 @@ describe('EmailPlugin', () => { expect(onSend.mock.calls[0][0].subject).toBe('Hello baz'); }); + it('loads globalTemplateVars async', async () => { + const handler = new EmailEventListener('test') + .on(MockEvent) + .setFrom('"test from" ') + .setRecipient(() => 'test@test.com') + .setSubject('Job {{ name }}, {{ primaryColor }}'); + + await initPluginWithHandlers([handler], { + globalTemplateVars: async (_ctxLocal: RequestContext, injector: Injector) => { + const jobQueueService = injector.get(JobQueueService); + const jobQueue = await jobQueueService.createQueue({ + name: 'hello-service', + // eslint-disable-next-line + process: async job => { + return 'hello'; + }, + }); + const name = jobQueue.name; + + return { + name, + primaryColor: 'blue', + }; + }, + }); + + eventBus.publish(new MockEvent(ctx, true)); + await pause(); + expect(onSend.mock.calls[0][0].subject).toBe(`Job hello-service, blue`); + }); + it('interpolates from', async () => { const handler = new EmailEventListener('test') .on(MockEvent) @@ -922,7 +954,10 @@ class FakeCustomSender implements EmailSender { const pause = () => new Promise(resolve => setTimeout(resolve, 100)); class MockEvent extends VendureEvent { - constructor(public ctx: RequestContext, public shouldSend: boolean) { + constructor( + public ctx: RequestContext, + public shouldSend: boolean, + ) { super(); } } diff --git a/packages/email-plugin/src/plugin.ts b/packages/email-plugin/src/plugin.ts index c78947332b..001ef50a1e 100644 --- a/packages/email-plugin/src/plugin.ts +++ b/packages/email-plugin/src/plugin.ts @@ -124,6 +124,38 @@ import { * * ``` * + * ### Setting global variables using `globalTemplateVars` + * + * `globalTemplateVars` is an object that can be passed to the configuration of the Email Plugin with static object variables. + * You can also pass an async function that will be called with the `RequestContext` and the `Injector` so you can access services + * and e.g. load channel specific theme configurations. + * + * @example + * ```ts + * EmailPlugin.init({ + * globalTemplateVars: { + * primaryColor: '#FF0000', + * fromAddress: 'no-reply@ourstore.com' + * } + * }) + * ``` + * or + * ```ts + * EmailPlugin.init({ + * globalTemplateVars: async (ctx, injector) => { + * const myAsyncService = injector.get(MyAsyncService); + * const asyncValue = await myAsyncService.get(ctx); + * const channel = ctx.channel; + * const { primaryColor } = channel.customFields.theme; + * const theme = { + * primaryColor, + * asyncValue, + * }; + * return theme; + * } + * }) + * ``` + * * ### Handlebars helpers * * The following helper functions are available for use in email templates: @@ -378,9 +410,13 @@ export class EmailPlugin implements OnApplicationBootstrap, OnApplicationShutdow const { type } = handler; try { const injector = new Injector(this.moduleRef); + let globalTemplateVars = this.options.globalTemplateVars; + if (typeof globalTemplateVars === 'function') { + globalTemplateVars = await globalTemplateVars(event.ctx, injector); + } const result = await handler.handle( event as any, - EmailPlugin.options.globalTemplateVars, + globalTemplateVars as { [key: string]: any }, injector, ); if (!result) { diff --git a/packages/email-plugin/src/types.ts b/packages/email-plugin/src/types.ts index b1bc82a82e..1b377468a8 100644 --- a/packages/email-plugin/src/types.ts +++ b/packages/email-plugin/src/types.ts @@ -4,9 +4,9 @@ import { Injector, RequestContext, SerializedRequestContext, VendureEvent } from import { Attachment } from 'nodemailer/lib/mailer'; import SESTransport from 'nodemailer/lib/ses-transport'; import SMTPTransport from 'nodemailer/lib/smtp-transport'; -import { EmailEventHandler } from './handler/event-handler'; import { EmailGenerator } from './generator/email-generator'; +import { EmailEventHandler } from './handler/event-handler'; import { EmailSender } from './sender/email-sender'; import { TemplateLoader } from './template-loader/template-loader'; @@ -31,6 +31,41 @@ export type EventWithContext = VendureEvent & { ctx: RequestContext }; */ export type EventWithAsyncData = Event & { data: R }; +/** + * @description + * Allows you to dynamically load the "globalTemplateVars" key async and access Vendure services + * to create the object. This is not a requirement. You can also specify a simple static object if your + * projects doesn't need to access async or dynamic values. + * + * @example + * ```ts + * + * EmailPlugin.init({ + * globalTemplateVars: async (ctx, injector) => { + * const myAsyncService = injector.get(MyAsyncService); + * const asyncValue = await myAsyncService.get(ctx); + * const channel = ctx.channel; + * const { primaryColor } = channel.customFields.theme; + * const theme = { + * primaryColor, + * asyncValue, + * }; + * return theme; + * } + * [...] + * }) + * + * ``` + * + * @docsCategory core plugins/EmailPlugin + * @docsPage EmailPluginOptions + * @docsWeight 0 + */ +export type GlobalTemplateVarsFn = ( + ctx: RequestContext, + injector: Injector, +) => Promise<{ [key: string]: any }>; + /** * @description * Configuration for the EmailPlugin. @@ -75,9 +110,10 @@ export interface EmailPluginOptions { * @description * An object containing variables which are made available to all templates. For example, * the storefront URL could be defined here and then used in the "email address verification" - * email. + * email. Use the GlobalTemplateVarsFn if you need to retrieve variables from Vendure or + * plugin services. */ - globalTemplateVars?: { [key: string]: any }; + globalTemplateVars?: { [key: string]: any } | GlobalTemplateVarsFn; /** * @description * An optional allowed EmailSender, used to allow custom implementations of the send functionality @@ -97,9 +133,11 @@ export interface EmailPluginOptions { } /** - * EmailPLuginOptions type after initialization, where templateLoader is no longer optional + * EmailPLuginOptions type after initialization, where templateLoader and themeInjector are no longer optional */ -export type InitializedEmailPluginOptions = EmailPluginOptions & { templateLoader: TemplateLoader }; +export type InitializedEmailPluginOptions = EmailPluginOptions & { + templateLoader: TemplateLoader; +}; /** * @description