| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269 |
- import { LanguageCode } from '@vendure/common/lib/generated-types';
- import { Type } from '@vendure/common/lib/shared-types';
- import { EmailEventListener, EmailTemplateConfig, SetTemplateVarsFn } from './event-listener';
- import { EventWithAsyncData, EventWithContext, LoadDataFn } from './types';
- /**
- * @description
- * The EmailEventHandler defines how the EmailPlugin will respond to a given event.
- *
- * A handler is created by creating a new {@link EmailEventListener} and calling the `.on()` method
- * to specify which event to respond to.
- *
- * @example
- * ```ts
- * const confirmationHandler = new EmailEventListener('order-confirmation')
- * .on(OrderStateTransitionEvent)
- * .filter(event => event.toState === 'PaymentSettled')
- * .setRecipient(event => event.order.customer.emailAddress)
- * .setSubject(`Order confirmation for #{{ order.code }}`)
- * .setTemplateVars(event => ({ order: event.order }));
- * ```
- *
- * This example creates a handler which listens for the `OrderStateTransitionEvent` and if the Order has
- * transitioned to the `'PaymentSettled'` state, it will generate and send an email.
- *
- * ## Handling other languages
- *
- * By default, the handler will respond to all events on all channels and use the same subject ("Order confirmation for #12345" above)
- * and body template. Where the server is intended to support multiple languages, the `.addTemplate()` method may be used
- * to defined the subject and body template for specific language and channel combinations.
- *
- * @example
- * ```ts
- * const extendedConfirmationHandler = confirmationHandler
- * .addTemplate({
- * channelCode: 'default',
- * languageCode: LanguageCode.de,
- * templateFile: 'body.de.hbs',
- * subject: 'Bestellbestätigung für #{{ order.code }}',
- * })
- * ```
- *
- * @docsCategory EmailPlugin
- */
- export class EmailEventHandler<T extends string = string, Event extends EventWithContext = EventWithContext> {
- private setRecipientFn: (event: Event) => string;
- private setTemplateVarsFn: SetTemplateVarsFn<Event>;
- private filterFns: Array<(event: Event) => boolean> = [];
- private configurations: EmailTemplateConfig[] = [];
- private defaultSubject: string;
- private from: string;
- private _mockEvent: Omit<Event, 'ctx'> | undefined;
- constructor(public listener: EmailEventListener<T>, public event: Type<Event>) {}
- /** @internal */
- get type(): T {
- return this.listener.type;
- }
- /** @internal */
- get mockEvent(): Omit<Event, 'ctx'> | undefined {
- return this._mockEvent;
- }
- /**
- * @description
- * Defines a predicate function which is used to determine whether the event will trigger an email.
- * Multiple filter functions may be defined.
- */
- filter(filterFn: (event: Event) => boolean): EmailEventHandler<T, Event> {
- this.filterFns.push(filterFn);
- return this;
- }
- /**
- * @description
- * A function which defines how the recipient email address should be extracted from the incoming event.
- */
- setRecipient(setRecipientFn: (event: Event) => string): EmailEventHandler<T, Event> {
- this.setRecipientFn = setRecipientFn;
- return this;
- }
- /**
- * @description
- * A function which returns an object hash of variables which will be made available to the Handlebars template
- * and subject line for interpolation.
- */
- setTemplateVars(templateVarsFn: SetTemplateVarsFn<Event>): EmailEventHandler<T, Event> {
- this.setTemplateVarsFn = templateVarsFn;
- return this;
- }
- /**
- * @description
- * Sets the default subject of the email. The subject string may use Handlebars variables defined by the
- * setTemplateVars() method.
- */
- setSubject(defaultSubject: string): EmailEventHandler<T, Event> {
- this.defaultSubject = defaultSubject;
- return this;
- }
- /**
- * @description
- * Sets the default from field of the email. The from string may use Handlebars variables defined by the
- * setTemplateVars() method.
- */
- setFrom(from: string): EmailEventHandler<T, Event> {
- this.from = from;
- return this;
- }
- /**
- * @description
- * Add configuration for another template other than the default `"body.hbs"`. Use this method to define specific
- * templates for channels or languageCodes other than the default.
- */
- addTemplate(config: EmailTemplateConfig): EmailEventHandler<T, Event> {
- this.configurations.push(config);
- return this;
- }
- /**
- * @description
- * Allows data to be loaded asynchronously which can then be used as template variables.
- * The `loadDataFn` has access to the event, the TypeORM `Connection` object, and an
- * `inject()` function which can be used to inject any of the providers exported
- * by the {@link PluginCommonModule}. The return value of the `loadDataFn` will be
- * added to the `event` as the `data` property.
- *
- * @example
- * ```TypeScript
- * new EmailEventListener('order-confirmation')
- * .on(OrderStateTransitionEvent)
- * .filter(event => event.toState === 'PaymentSettled' && !!event.order.customer)
- * .loadData(({ event, inject}) => {
- * const orderService = inject(OrderService);
- * return orderService.getOrderPayments(event.order.id);
- * })
- * .setTemplateVars(event => ({
- * order: event.order,
- * payments: event.data,
- * }));
- * ```
- */
- loadData<R>(
- loadDataFn: LoadDataFn<Event, R>,
- ): EmailEventHandlerWithAsyncData<R, T, Event, EventWithAsyncData<Event, R>> {
- const asyncHandler = new EmailEventHandlerWithAsyncData(loadDataFn, this.listener, this.event);
- asyncHandler.setRecipientFn = this.setRecipientFn;
- asyncHandler.setTemplateVarsFn = this.setTemplateVarsFn;
- asyncHandler.filterFns = this.filterFns;
- asyncHandler.configurations = this.configurations;
- asyncHandler.defaultSubject = this.defaultSubject;
- asyncHandler.from = this.from;
- asyncHandler._mockEvent = this._mockEvent as any;
- return asyncHandler;
- }
- /**
- * @description
- * Used internally by the EmailPlugin to handle incoming events.
- *
- * @internal
- */
- async handle(
- event: Event,
- globals: { [key: string]: any } = {},
- ): Promise<
- | { from: string; recipient: string; templateVars: any; subject: string; templateFile: string }
- | undefined
- > {
- for (const filterFn of this.filterFns) {
- if (!filterFn(event)) {
- return;
- }
- }
- if (!this.setRecipientFn) {
- throw new Error(
- `No setRecipientFn has been defined. ` +
- `Remember to call ".setRecipient()" when setting up the EmailEventHandler for ${this.type}`,
- );
- }
- if (this.from === undefined) {
- throw new Error(
- `No from field has been defined. ` +
- `Remember to call ".setFrom()" when setting up the EmailEventHandler for ${this.type}`,
- );
- }
- const { ctx } = event;
- const configuration = this.getBestConfiguration(ctx.channel.code, ctx.languageCode);
- const subject = configuration ? configuration.subject : this.defaultSubject;
- if (subject == null) {
- throw new Error(
- `No subject field has been defined. ` +
- `Remember to call ".setSubject()" when setting up the EmailEventHandler for ${this.type}`,
- );
- }
- const recipient = this.setRecipientFn(event);
- const templateVars = this.setTemplateVarsFn ? this.setTemplateVarsFn(event, globals) : {};
- return {
- recipient,
- from: this.from,
- templateVars: { ...globals, ...templateVars },
- subject,
- templateFile: configuration ? configuration.templateFile : 'body.hbs',
- };
- }
- /**
- * @description
- * Optionally define a mock Event which is used by the dev mode mailbox app for generating mock emails
- * from this handler, which is useful when developing the email templates.
- */
- setMockEvent(event: Omit<Event, 'ctx'>): EmailEventHandler<T, Event> {
- this._mockEvent = event;
- return this;
- }
- private getBestConfiguration(
- channelCode: string,
- languageCode: LanguageCode,
- ): EmailTemplateConfig | undefined {
- if (this.configurations.length === 0) {
- return;
- }
- const exactMatch = this.configurations.find(c => {
- return (
- (c.channelCode === channelCode || c.channelCode === 'default') &&
- c.languageCode === languageCode
- );
- });
- if (exactMatch) {
- return exactMatch;
- }
- const channelMatch = this.configurations.find(
- c => c.channelCode === channelCode && c.languageCode === 'default',
- );
- if (channelMatch) {
- return channelMatch;
- }
- return;
- }
- }
- /**
- * @description
- * Identical to the {@link EmailEventHandler} but with a `data` property added to the `event` based on the result
- * of the `.loadData()` function.
- *
- * @docsCategory EmailPlugin
- */
- export class EmailEventHandlerWithAsyncData<
- Data,
- T extends string = string,
- InputEvent extends EventWithContext = EventWithContext,
- Event extends EventWithAsyncData<InputEvent, Data> = EventWithAsyncData<InputEvent, Data>
- > extends EmailEventHandler<T, Event> {
- constructor(
- public _loadDataFn: LoadDataFn<InputEvent, Data>,
- listener: EmailEventListener<T>,
- event: Type<InputEvent>,
- ) {
- super(listener, event as any);
- }
- }
|