Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/email load globalTemplateVars async #2950

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion packages/email-plugin/src/plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
DefaultLogger,
EventBus,
Injector,
JobQueueService,
LanguageCode,
Logger,
LogLevel,
Expand Down Expand Up @@ -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" <[email protected]>')
.setRecipient(() => '[email protected]')
.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)
Expand Down Expand Up @@ -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();
}
}
Expand Down
38 changes: 37 additions & 1 deletion packages/email-plugin/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,38 @@ import {
* </mj-table>
* ```
*
* ### 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: '[email protected]'
* }
* })
* ```
* 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:
Expand Down Expand Up @@ -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) {
Expand Down
48 changes: 43 additions & 5 deletions packages/email-plugin/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -31,6 +31,41 @@ export type EventWithContext = VendureEvent & { ctx: RequestContext };
*/
export type EventWithAsyncData<Event extends EventWithContext, R> = 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting docsWeight 0 will force this type to the top of the EmailPluginOptions docs page, which is not what we want. Better to have the main EmailPluginOptions interface at the top. So remove this to be safe add it to the EmailPluginOptions doc block to ensure it always stays at the top.

*/
export type GlobalTemplateVarsFn = (
ctx: RequestContext,
injector: Injector,
) => Promise<{ [key: string]: any }>;

/**
* @description
* Configuration for the EmailPlugin.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading