event-handler.ts 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import { LanguageCode } from '@vendure/common/lib/generated-types';
  2. import { Type } from '@vendure/common/lib/shared-types';
  3. import { Injector } from '@vendure/core';
  4. import { EmailEventListener, EmailTemplateConfig, SetTemplateVarsFn } from './event-listener';
  5. import { EventWithAsyncData, EventWithContext, IntermediateEmailDetails, LoadDataFn } from './types';
  6. /**
  7. * @description
  8. * The EmailEventHandler defines how the EmailPlugin will respond to a given event.
  9. *
  10. * A handler is created by creating a new {@link EmailEventListener} and calling the `.on()` method
  11. * to specify which event to respond to.
  12. *
  13. * @example
  14. * ```ts
  15. * const confirmationHandler = new EmailEventListener('order-confirmation')
  16. * .on(OrderStateTransitionEvent)
  17. * .filter(event => event.toState === 'PaymentSettled')
  18. * .setRecipient(event => event.order.customer.emailAddress)
  19. * .setSubject(`Order confirmation for #{{ order.code }}`)
  20. * .setTemplateVars(event => ({ order: event.order }));
  21. * ```
  22. *
  23. * This example creates a handler which listens for the `OrderStateTransitionEvent` and if the Order has
  24. * transitioned to the `'PaymentSettled'` state, it will generate and send an email.
  25. *
  26. * ## Handling other languages
  27. *
  28. * By default, the handler will respond to all events on all channels and use the same subject ("Order confirmation for #12345" above)
  29. * and body template. Where the server is intended to support multiple languages, the `.addTemplate()` method may be used
  30. * to defined the subject and body template for specific language and channel combinations.
  31. *
  32. * @example
  33. * ```ts
  34. * const extendedConfirmationHandler = confirmationHandler
  35. * .addTemplate({
  36. * channelCode: 'default',
  37. * languageCode: LanguageCode.de,
  38. * templateFile: 'body.de.hbs',
  39. * subject: 'Bestellbestätigung für #{{ order.code }}',
  40. * })
  41. * ```
  42. *
  43. * @docsCategory EmailPlugin
  44. */
  45. export class EmailEventHandler<T extends string = string, Event extends EventWithContext = EventWithContext> {
  46. private setRecipientFn: (event: Event) => string;
  47. private setTemplateVarsFn: SetTemplateVarsFn<Event>;
  48. private filterFns: Array<(event: Event) => boolean> = [];
  49. private configurations: EmailTemplateConfig[] = [];
  50. private defaultSubject: string;
  51. private from: string;
  52. private _mockEvent: Omit<Event, 'ctx' | 'data'> | undefined;
  53. constructor(public listener: EmailEventListener<T>, public event: Type<Event>) {}
  54. /** @internal */
  55. get type(): T {
  56. return this.listener.type;
  57. }
  58. /** @internal */
  59. get mockEvent(): Omit<Event, 'ctx' | 'data'> | undefined {
  60. return this._mockEvent;
  61. }
  62. /**
  63. * @description
  64. * Defines a predicate function which is used to determine whether the event will trigger an email.
  65. * Multiple filter functions may be defined.
  66. */
  67. filter(filterFn: (event: Event) => boolean): EmailEventHandler<T, Event> {
  68. this.filterFns.push(filterFn);
  69. return this;
  70. }
  71. /**
  72. * @description
  73. * A function which defines how the recipient email address should be extracted from the incoming event.
  74. */
  75. setRecipient(setRecipientFn: (event: Event) => string): EmailEventHandler<T, Event> {
  76. this.setRecipientFn = setRecipientFn;
  77. return this;
  78. }
  79. /**
  80. * @description
  81. * A function which returns an object hash of variables which will be made available to the Handlebars template
  82. * and subject line for interpolation.
  83. */
  84. setTemplateVars(templateVarsFn: SetTemplateVarsFn<Event>): EmailEventHandler<T, Event> {
  85. this.setTemplateVarsFn = templateVarsFn;
  86. return this;
  87. }
  88. /**
  89. * @description
  90. * Sets the default subject of the email. The subject string may use Handlebars variables defined by the
  91. * setTemplateVars() method.
  92. */
  93. setSubject(defaultSubject: string): EmailEventHandler<T, Event> {
  94. this.defaultSubject = defaultSubject;
  95. return this;
  96. }
  97. /**
  98. * @description
  99. * Sets the default from field of the email. The from string may use Handlebars variables defined by the
  100. * setTemplateVars() method.
  101. */
  102. setFrom(from: string): EmailEventHandler<T, Event> {
  103. this.from = from;
  104. return this;
  105. }
  106. /**
  107. * @description
  108. * Add configuration for another template other than the default `"body.hbs"`. Use this method to define specific
  109. * templates for channels or languageCodes other than the default.
  110. */
  111. addTemplate(config: EmailTemplateConfig): EmailEventHandler<T, Event> {
  112. this.configurations.push(config);
  113. return this;
  114. }
  115. /**
  116. * @description
  117. * Allows data to be loaded asynchronously which can then be used as template variables.
  118. * The `loadDataFn` has access to the event, the TypeORM `Connection` object, and an
  119. * `inject()` function which can be used to inject any of the providers exported
  120. * by the {@link PluginCommonModule}. The return value of the `loadDataFn` will be
  121. * added to the `event` as the `data` property.
  122. *
  123. * @example
  124. * ```TypeScript
  125. * new EmailEventListener('order-confirmation')
  126. * .on(OrderStateTransitionEvent)
  127. * .filter(event => event.toState === 'PaymentSettled' && !!event.order.customer)
  128. * .loadData(({ event, inject}) => {
  129. * const orderService = inject(OrderService);
  130. * return orderService.getOrderPayments(event.order.id);
  131. * })
  132. * .setTemplateVars(event => ({
  133. * order: event.order,
  134. * payments: event.data,
  135. * }));
  136. * ```
  137. */
  138. loadData<R>(
  139. loadDataFn: LoadDataFn<Event, R>,
  140. ): EmailEventHandlerWithAsyncData<R, T, Event, EventWithAsyncData<Event, R>> {
  141. const asyncHandler = new EmailEventHandlerWithAsyncData(loadDataFn, this.listener, this.event);
  142. asyncHandler.setRecipientFn = this.setRecipientFn;
  143. asyncHandler.setTemplateVarsFn = this.setTemplateVarsFn;
  144. asyncHandler.filterFns = this.filterFns;
  145. asyncHandler.configurations = this.configurations;
  146. asyncHandler.defaultSubject = this.defaultSubject;
  147. asyncHandler.from = this.from;
  148. asyncHandler._mockEvent = this._mockEvent as any;
  149. return asyncHandler;
  150. }
  151. /**
  152. * @description
  153. * Used internally by the EmailPlugin to handle incoming events.
  154. *
  155. * @internal
  156. */
  157. async handle(
  158. event: Event,
  159. globals: { [key: string]: any } = {},
  160. injector: Injector,
  161. ): Promise<IntermediateEmailDetails | undefined> {
  162. for (const filterFn of this.filterFns) {
  163. if (!filterFn(event)) {
  164. return;
  165. }
  166. }
  167. if (this instanceof EmailEventHandlerWithAsyncData) {
  168. (event as EventWithAsyncData<Event, any>).data = await this._loadDataFn({
  169. event,
  170. injector,
  171. });
  172. }
  173. if (!this.setRecipientFn) {
  174. throw new Error(
  175. `No setRecipientFn has been defined. ` +
  176. `Remember to call ".setRecipient()" when setting up the EmailEventHandler for ${this.type}`,
  177. );
  178. }
  179. if (this.from === undefined) {
  180. throw new Error(
  181. `No from field has been defined. ` +
  182. `Remember to call ".setFrom()" when setting up the EmailEventHandler for ${this.type}`,
  183. );
  184. }
  185. const { ctx } = event;
  186. const configuration = this.getBestConfiguration(ctx.channel.code, ctx.languageCode);
  187. const subject = configuration ? configuration.subject : this.defaultSubject;
  188. if (subject == null) {
  189. throw new Error(
  190. `No subject field has been defined. ` +
  191. `Remember to call ".setSubject()" when setting up the EmailEventHandler for ${this.type}`,
  192. );
  193. }
  194. const recipient = this.setRecipientFn(event);
  195. const templateVars = this.setTemplateVarsFn ? this.setTemplateVarsFn(event, globals) : {};
  196. return {
  197. type: this.type,
  198. recipient,
  199. from: this.from,
  200. templateVars: { ...globals, ...templateVars },
  201. subject,
  202. templateFile: configuration ? configuration.templateFile : 'body.hbs',
  203. };
  204. }
  205. /**
  206. * @description
  207. * Optionally define a mock Event which is used by the dev mode mailbox app for generating mock emails
  208. * from this handler, which is useful when developing the email templates.
  209. */
  210. setMockEvent(event: Omit<Event, 'ctx' | 'data'>): EmailEventHandler<T, Event> {
  211. this._mockEvent = event;
  212. return this;
  213. }
  214. private getBestConfiguration(
  215. channelCode: string,
  216. languageCode: LanguageCode,
  217. ): EmailTemplateConfig | undefined {
  218. if (this.configurations.length === 0) {
  219. return;
  220. }
  221. const exactMatch = this.configurations.find(c => {
  222. return (
  223. (c.channelCode === channelCode || c.channelCode === 'default') &&
  224. c.languageCode === languageCode
  225. );
  226. });
  227. if (exactMatch) {
  228. return exactMatch;
  229. }
  230. const channelMatch = this.configurations.find(
  231. c => c.channelCode === channelCode && c.languageCode === 'default',
  232. );
  233. if (channelMatch) {
  234. return channelMatch;
  235. }
  236. return;
  237. }
  238. }
  239. /**
  240. * @description
  241. * Identical to the {@link EmailEventHandler} but with a `data` property added to the `event` based on the result
  242. * of the `.loadData()` function.
  243. *
  244. * @docsCategory EmailPlugin
  245. */
  246. export class EmailEventHandlerWithAsyncData<
  247. Data,
  248. T extends string = string,
  249. InputEvent extends EventWithContext = EventWithContext,
  250. Event extends EventWithAsyncData<InputEvent, Data> = EventWithAsyncData<InputEvent, Data>
  251. > extends EmailEventHandler<T, Event> {
  252. constructor(
  253. public _loadDataFn: LoadDataFn<InputEvent, Data>,
  254. listener: EmailEventListener<T>,
  255. event: Type<InputEvent>,
  256. ) {
  257. super(listener, event as any);
  258. }
  259. }