From 949245a22a084cd8490ced48fa58bd37569cad28 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Mon, 13 May 2024 13:28:03 +0200 Subject: [PATCH 01/14] detele shop info fetcher --- .../modules/shop-info/shop-info-fetcher.ts | 43 ------------------- 1 file changed, 43 deletions(-) delete mode 100644 apps/smtp/src/modules/shop-info/shop-info-fetcher.ts diff --git a/apps/smtp/src/modules/shop-info/shop-info-fetcher.ts b/apps/smtp/src/modules/shop-info/shop-info-fetcher.ts deleted file mode 100644 index ba3f0c83d..000000000 --- a/apps/smtp/src/modules/shop-info/shop-info-fetcher.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Client, gql } from "urql"; -import { ShopInfoDocument, ShopInfoFragment } from "../../../generated/graphql"; - -gql` - fragment ShopInfo on Shop { - companyAddress { - country { - country - code - } - city - firstName - lastName - streetAddress1 - streetAddress2 - companyName - phone - postalCode - countryArea - cityArea - } - } - query ShopInfo { - shop { - ...ShopInfo - } - } -`; - -export interface IShopInfoFetcher { - fetchShopInfo(): Promise; -} - -export class ShopInfoFetcher implements IShopInfoFetcher { - constructor(private client: Client) {} - - fetchShopInfo(): Promise { - return this.client - .query(ShopInfoDocument, {}) - .toPromise() - .then((resp) => resp.data?.shop ?? null); - } -} From 5a3ccddc72c8afc35499aceb5d5237ac6e4cd72e Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Mon, 13 May 2024 13:57:49 +0200 Subject: [PATCH 02/14] implement errors handling in email compiler --- .../smtp/compile-handlebars-template.ts | 22 ----- .../src/modules/smtp/email-compiler.test.ts | 0 apps/smtp/src/modules/smtp/email-compiler.ts | 80 ++++++++++++------- .../src/modules/smtp/template-compiler.ts | 49 ++++++++++++ 4 files changed, 99 insertions(+), 52 deletions(-) delete mode 100644 apps/smtp/src/modules/smtp/compile-handlebars-template.ts create mode 100644 apps/smtp/src/modules/smtp/email-compiler.test.ts create mode 100644 apps/smtp/src/modules/smtp/template-compiler.ts diff --git a/apps/smtp/src/modules/smtp/compile-handlebars-template.ts b/apps/smtp/src/modules/smtp/compile-handlebars-template.ts deleted file mode 100644 index 8e567c3bb..000000000 --- a/apps/smtp/src/modules/smtp/compile-handlebars-template.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Handlebars from "handlebars"; -import { createLogger } from "../../logger"; - -const logger = createLogger("compileHandlebarsTemplate"); - -export const compileHandlebarsTemplate = (template: string, variables: any) => { - logger.debug("Compiling handlebars template"); - try { - const templateDelegate = Handlebars.compile(template); - const htmlTemplate = templateDelegate(variables); - - logger.debug("Template successfully compiled"); - return { - template: htmlTemplate, - }; - } catch (error) { - logger.error(error); - return { - errors: [{ message: "Error during the using the handlebars template" }], - }; - } -}; diff --git a/apps/smtp/src/modules/smtp/email-compiler.test.ts b/apps/smtp/src/modules/smtp/email-compiler.test.ts new file mode 100644 index 000000000..e69de29bb diff --git a/apps/smtp/src/modules/smtp/email-compiler.ts b/apps/smtp/src/modules/smtp/email-compiler.ts index 0740f2878..acfb43367 100644 --- a/apps/smtp/src/modules/smtp/email-compiler.ts +++ b/apps/smtp/src/modules/smtp/email-compiler.ts @@ -1,9 +1,11 @@ import { compileMjml } from "./compile-mjml"; -import { compileHandlebarsTemplate } from "./compile-handlebars-template"; +import { ITemplateCompiler } from "./template-compiler"; import { MessageEventTypes } from "../event-handlers/message-event-types"; import { htmlToPlaintext } from "./html-to-plaintext"; import { SmtpConfiguration } from "./configuration/smtp-config-schema"; import { createLogger } from "../../logger"; +import { BaseError } from "../../errors"; +import { err, ok, Result } from "neverthrow"; interface CompileArgs { smtpConfiguration: SmtpConfiguration; @@ -21,21 +23,30 @@ export interface CompiledEmail { } export interface IEmailCompiler { - compile(args: CompileArgs): CompiledEmail | undefined; + compile(args: CompileArgs): Result>; } /* * todo introduce modern-errors */ export class EmailCompiler implements IEmailCompiler { - constructor() {} + static EmailCompilerError = BaseError.subclass("EmailCompilerError"); + static MissingEventSettingsError = this.EmailCompilerError.subclass("MissingEventSettingsError"); + static EventNotEnabledInSettingsError = this.EmailCompilerError.subclass( + "EventNotEnabledInSettingsError", + ); + static CompilationFailedError = this.EmailCompilerError.subclass("CompilationFailedError"); + static EmptyEmailSubjectError = this.EmailCompilerError.subclass("EmptyEmailSubjectError"); + static EmptyEmailBodyError = this.EmailCompilerError.subclass("EmptyEmailBodyError"); + + constructor(private templateCompiler: ITemplateCompiler) {} compile({ payload, recipientEmail, event, smtpConfiguration, - }: CompileArgs): CompiledEmail | undefined { + }: CompileArgs): Result> { const logger = createLogger("sendSmtp", { name: "sendSmtp", event, @@ -45,56 +56,65 @@ export class EmailCompiler implements IEmailCompiler { if (!eventSettings) { logger.debug("No active settings for this event, skipping"); - return; + + return err(new EmailCompiler.MissingEventSettingsError("No active settings for this event")); } if (!eventSettings.active) { logger.debug("Event settings are not active, skipping"); - return; + + return err(new EmailCompiler.EventNotEnabledInSettingsError("Event settings are not active")); } - logger.debug("Sending an email using MJML"); + logger.debug("Compiling an email using MJML"); const { template: rawTemplate, subject } = eventSettings; - const { template: emailSubject, errors: handlebarsSubjectErrors } = compileHandlebarsTemplate( - subject, - payload, - ); - - if (handlebarsSubjectErrors?.length) { - logger.error("Error during the handlebars subject template compilation"); + const subjectCompilationResult = this.templateCompiler.compile(subject, payload); - throw new Error("Error during the handlebars subject template compilation"); + if (subjectCompilationResult.isErr()) { + return err( + new EmailCompiler.CompilationFailedError("Failed to compile email subject template", { + errors: [subjectCompilationResult.error], + }), + ); } + const { template: emailSubject } = subjectCompilationResult.value; + if (!emailSubject || !emailSubject?.length) { logger.error("Mjml subject message is empty, skipping"); - throw new Error("Mjml subject message is empty, skipping"); + return err( + new EmailCompiler.EmptyEmailSubjectError("Mjml subject message is empty, skipping", { + props: { + subject: emailSubject, + }, + }), + ); } logger.debug({ emailSubject }, "Subject compiled"); - const { template: mjmlTemplate, errors: handlebarsErrors } = compileHandlebarsTemplate( - rawTemplate, - payload, - ); - - if (handlebarsErrors?.length) { - logger.error("Error during the handlebars template compilation"); + const bodyCompilationResult = this.templateCompiler.compile(rawTemplate, payload); - throw new Error("Error during the handlebars template compilation"); + if (bodyCompilationResult.isErr()) { + return err( + new EmailCompiler.CompilationFailedError("Failed to compile email body template", { + errors: [bodyCompilationResult.error], + }), + ); } - if (!mjmlTemplate || !mjmlTemplate?.length) { - logger.error("Mjml template message is empty, skipping"); - throw new Error("Mjml template message is empty, skipping"); + const { template: emailTemplate } = bodyCompilationResult.value; + + if (!emailTemplate || !emailTemplate?.length) { + return err(new EmailCompiler.EmptyEmailBodyError("MJML template body is empty")); } logger.debug("Handlebars template compiled"); - const { html: emailBodyHtml, errors: mjmlCompilationErrors } = compileMjml(mjmlTemplate); + const { html: emailBodyHtml, errors: mjmlCompilationErrors } = compileMjml(emailTemplate); if (mjmlCompilationErrors.length) { logger.error("Error during the MJML compilation"); @@ -121,12 +141,12 @@ export class EmailCompiler implements IEmailCompiler { logger.debug("Email body converted to plaintext"); - return { + return ok({ text: emailBodyPlaintext, html: emailBodyHtml, from: `${smtpConfiguration.senderName} <${smtpConfiguration.senderEmail}>`, to: recipientEmail, subject: emailSubject, - }; + }); } } diff --git a/apps/smtp/src/modules/smtp/template-compiler.ts b/apps/smtp/src/modules/smtp/template-compiler.ts new file mode 100644 index 000000000..bde9ea61b --- /dev/null +++ b/apps/smtp/src/modules/smtp/template-compiler.ts @@ -0,0 +1,49 @@ +import Handlebars from "handlebars"; +import { createLogger } from "../../logger"; +import { err, ok, Result } from "neverthrow"; +import { BaseError } from "../../errors"; + +const logger = createLogger("compileHandlebarsTemplate"); + +export interface ITemplateCompiler { + compile( + template: string, + variables: any, + ): Result< + { template: string }, + InstanceType + >; +} + +export class HandlebarsTemplateCompiler implements ITemplateCompiler { + static HandlebarsTemplateCompilerError = BaseError.subclass("HandlebarsTemplateCompilerError"); + static FailedCompileError = this.HandlebarsTemplateCompilerError.subclass("FailedCompileError"); + + compile( + template: string, + variables: any, + ): Result< + { template: string }, + InstanceType + > { + logger.debug("Compiling handlebars template"); + try { + const templateDelegate = Handlebars.compile(template); + const htmlTemplate = templateDelegate(variables); + + logger.debug("Template successfully compiled"); + + return ok({ + template: htmlTemplate, + }); + } catch (error) { + logger.error(error); + + return err( + new HandlebarsTemplateCompiler.FailedCompileError("Failed to compile template", { + errors: [error], + }), + ); + } + } +} From 587c3d59943021568af2a73e8407f52f4c6f1e5a Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Mon, 13 May 2024 19:07:26 +0200 Subject: [PATCH 03/14] test for compiler --- apps/smtp/src/modules/smtp/compile-mjml.ts | 1 + .../src/modules/smtp/email-compiler.test.ts | 132 ++++++++++++++++++ apps/smtp/src/modules/smtp/email-compiler.ts | 10 +- .../src/modules/smtp/template-compiler.ts | 2 +- 4 files changed, 142 insertions(+), 3 deletions(-) diff --git a/apps/smtp/src/modules/smtp/compile-mjml.ts b/apps/smtp/src/modules/smtp/compile-mjml.ts index f70bfe7a6..9505d716b 100644 --- a/apps/smtp/src/modules/smtp/compile-mjml.ts +++ b/apps/smtp/src/modules/smtp/compile-mjml.ts @@ -3,6 +3,7 @@ import { createLogger } from "../../logger"; const logger = createLogger("compileMjml"); +// todo clas export const compileMjml = (mjml: string) => { logger.debug("Converting MJML template to HTML"); try { diff --git a/apps/smtp/src/modules/smtp/email-compiler.test.ts b/apps/smtp/src/modules/smtp/email-compiler.test.ts index e69de29bb..c60d1f49c 100644 --- a/apps/smtp/src/modules/smtp/email-compiler.test.ts +++ b/apps/smtp/src/modules/smtp/email-compiler.test.ts @@ -0,0 +1,132 @@ +import { describe, it, expect, beforeEach, vi, Mock } from "vitest"; +import { EmailCompiler } from "./email-compiler"; +import { HandlebarsTemplateCompiler, ITemplateCompiler } from "./template-compiler"; +import { err, ok, Result } from "neverthrow"; + +describe("EmailCompiler", () => { + let templateCompiler = new HandlebarsTemplateCompiler(); + let compiler = new EmailCompiler(new HandlebarsTemplateCompiler()); + + beforeEach(() => { + vi.resetAllMocks(); + + templateCompiler = new HandlebarsTemplateCompiler(); + compiler = new EmailCompiler(templateCompiler); + }); + + it("Returns EmptyEmailSubjectError error if subject is empty", () => { + const result = compiler.compile({ + event: "ACCOUNT_CHANGE_EMAIL_CONFIRM", + recipientEmail: "foo@bar.com", + payload: {}, + smtpConfiguration: { + senderEmail: "sender@saleor.io", + senderName: "John Doe from Saleor", + events: [ + { + template: "template string", + subject: "", + eventType: "ACCOUNT_CHANGE_EMAIL_CONFIRM", + active: true, + }, + ], + }, + }); + + expect(result._unsafeUnwrapErr()).toBeInstanceOf(EmailCompiler.EmptyEmailSubjectError); + }); + + it("Returns CompilationFailedError if underlying template compiler fails to compile", () => { + const compiler = new EmailCompiler({ + compile(): Result< + { + template: string; + }, + InstanceType + > { + return err( + new HandlebarsTemplateCompiler.HandlebarsTemplateCompilerError("Error to compile"), + ); + }, + }); + + const result = compiler.compile({ + payload: { foo: 1 }, + recipientEmail: "recipien@test.com", + event: "ACCOUNT_DELETE", + smtpConfiguration: { + senderEmail: "sender@test.com", + senderName: "sender test", + events: [ + { + template: "test {{foo}}", + active: true, + eventType: "ACCOUNT_DELETE", + subject: "test subject", + }, + ], + }, + }); + + expect(result._unsafeUnwrapErr()).toBeInstanceOf(EmailCompiler.CompilationFailedError); + }); + + it("Returns EmptyEmailBodyError if body of email is empty", () => { + const result = compiler.compile({ + payload: { foo: 1 }, + recipientEmail: "recipien@test.com", + event: "ACCOUNT_DELETE", + smtpConfiguration: { + senderEmail: "sender@test.com", + senderName: "sender test", + events: [ + { + template: "", + active: true, + eventType: "ACCOUNT_DELETE", + subject: "test subject", + }, + ], + }, + }); + + expect(result._unsafeUnwrapErr()).toBeInstanceOf(EmailCompiler.EmptyEmailBodyError); + }); + + it("Returns valid compilation result", () => { + const result = compiler.compile({ + payload: { foo: 2137 }, + recipientEmail: "recipien@test.com", + event: "ACCOUNT_DELETE", + smtpConfiguration: { + senderEmail: "sender@test.com", + senderName: "sender test", + events: [ + { + template: ` + + + + {{foo}} + + + +`, + active: true, + eventType: "ACCOUNT_DELETE", + subject: "test subject", + }, + ], + }, + }); + + const resultValue = result._unsafeUnwrap(); + + expect(resultValue.from).toEqual("sender test "); + expect(resultValue.to).toEqual("recipien@test.com"); + expect(resultValue.subject).toEqual("test subject"); + expect(resultValue.text).toEqual("2137"); + // Simple assertion to check if result HTML includes injected value + expect(resultValue.html.includes("2137")).toBe(true); + }); +}); diff --git a/apps/smtp/src/modules/smtp/email-compiler.ts b/apps/smtp/src/modules/smtp/email-compiler.ts index acfb43367..273605b19 100644 --- a/apps/smtp/src/modules/smtp/email-compiler.ts +++ b/apps/smtp/src/modules/smtp/email-compiler.ts @@ -8,10 +8,10 @@ import { BaseError } from "../../errors"; import { err, ok, Result } from "neverthrow"; interface CompileArgs { - smtpConfiguration: SmtpConfiguration; + smtpConfiguration: Pick; recipientEmail: string; event: MessageEventTypes; - payload: any; + payload: unknown; } export interface CompiledEmail { @@ -52,6 +52,9 @@ export class EmailCompiler implements IEmailCompiler { event, }); + /** + * TODO: Move these checks higher, compile should only have necessary data + */ const eventSettings = smtpConfiguration.events.find((e) => e.eventType === event); if (!eventSettings) { @@ -60,6 +63,9 @@ export class EmailCompiler implements IEmailCompiler { return err(new EmailCompiler.MissingEventSettingsError("No active settings for this event")); } + /** + * TODO: Move these checks higher, compile should only have necessary data + */ if (!eventSettings.active) { logger.debug("Event settings are not active, skipping"); diff --git a/apps/smtp/src/modules/smtp/template-compiler.ts b/apps/smtp/src/modules/smtp/template-compiler.ts index bde9ea61b..c5f61fcf0 100644 --- a/apps/smtp/src/modules/smtp/template-compiler.ts +++ b/apps/smtp/src/modules/smtp/template-compiler.ts @@ -8,7 +8,7 @@ const logger = createLogger("compileHandlebarsTemplate"); export interface ITemplateCompiler { compile( template: string, - variables: any, + variables: unknown, ): Result< { template: string }, InstanceType From 0fdc1c76cc984da8e04aa1b33e6389c0bbed2308 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Mon, 13 May 2024 19:29:52 +0200 Subject: [PATCH 04/14] tests to compiler --- .../send-event-messages.use-case.ts | 39 ++++++++-- .../src/modules/smtp/email-compiler.test.ts | 74 +++++++------------ apps/smtp/src/modules/smtp/email-compiler.ts | 45 +++-------- .../src/pages/api/webhooks/gift-card-sent.ts | 3 +- .../src/pages/api/webhooks/invoice-sent.ts | 3 +- apps/smtp/src/pages/api/webhooks/notify.ts | 3 +- .../src/pages/api/webhooks/order-cancelled.ts | 3 +- .../src/pages/api/webhooks/order-confirmed.ts | 3 +- .../src/pages/api/webhooks/order-created.ts | 3 +- .../src/pages/api/webhooks/order-fulfilled.ts | 3 +- .../pages/api/webhooks/order-fully-paid.ts | 3 +- .../src/pages/api/webhooks/order-refunded.ts | 3 +- 12 files changed, 89 insertions(+), 96 deletions(-) diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts index 73a7e3c2e..a7ed1ab0d 100644 --- a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts +++ b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts @@ -4,6 +4,7 @@ import { MessageEventTypes } from "./message-event-types"; import { createLogger } from "../../logger"; import { ISMTPEmailSender, SendMailArgs } from "../smtp/smtp-email-sender"; +// todo test export class SendEventMessagesUseCase { private logger = createLogger("SendEventMessagesUseCase"); @@ -42,15 +43,43 @@ export class SendEventMessagesUseCase { */ for (const smtpConfiguration of availableSmtpConfigurations.value) { try { - const preparedEmail = this.deps.emailCompiler.compile({ + const eventSettings = smtpConfiguration.events.find((e) => e.eventType === event); + + if (!eventSettings) { + /* + * Config missing, ignore + * todo log + */ + return; + } + + if (!eventSettings.active) { + /** + * Config found, but set as disabled, ignore. + * todo: log + */ + return; + } + + if (!smtpConfiguration.senderName || !smtpConfiguration.senderEmail) { + /** + * TODO: check if this should be allowed + */ + return; + } + + const preparedEmailResult = this.deps.emailCompiler.compile({ event: event, payload: payload, recipientEmail: recipientEmail, - smtpConfiguration, + bodyTemplate: eventSettings.template, + subjectTemplate: eventSettings.subject, + senderEmail: smtpConfiguration.senderEmail, + senderName: smtpConfiguration.senderName, }); - if (!preparedEmail) { - return; // todo log + if (preparedEmailResult.isErr()) { + return; // todo log + what should we do? } const smtpSettings: SendMailArgs["smtpSettings"] = { @@ -68,7 +97,7 @@ export class SendEventMessagesUseCase { try { await this.deps.emailSender.sendEmailWithSmtp({ - mailData: preparedEmail, + mailData: preparedEmailResult.value, smtpSettings, }); } catch (e) { diff --git a/apps/smtp/src/modules/smtp/email-compiler.test.ts b/apps/smtp/src/modules/smtp/email-compiler.test.ts index c60d1f49c..77277d3af 100644 --- a/apps/smtp/src/modules/smtp/email-compiler.test.ts +++ b/apps/smtp/src/modules/smtp/email-compiler.test.ts @@ -3,6 +3,16 @@ import { EmailCompiler } from "./email-compiler"; import { HandlebarsTemplateCompiler, ITemplateCompiler } from "./template-compiler"; import { err, ok, Result } from "neverthrow"; +const getMjmlTemplate = (injectedValue: string | number) => ` + + + + {{injectedValue}} + + + +`; + describe("EmailCompiler", () => { let templateCompiler = new HandlebarsTemplateCompiler(); let compiler = new EmailCompiler(new HandlebarsTemplateCompiler()); @@ -19,18 +29,10 @@ describe("EmailCompiler", () => { event: "ACCOUNT_CHANGE_EMAIL_CONFIRM", recipientEmail: "foo@bar.com", payload: {}, - smtpConfiguration: { - senderEmail: "sender@saleor.io", - senderName: "John Doe from Saleor", - events: [ - { - template: "template string", - subject: "", - eventType: "ACCOUNT_CHANGE_EMAIL_CONFIRM", - active: true, - }, - ], - }, + bodyTemplate: getMjmlTemplate("2137"), + senderEmail: "sender@saleor.io", + senderName: "John Doe from Saleor", + subjectTemplate: "", }); expect(result._unsafeUnwrapErr()).toBeInstanceOf(EmailCompiler.EmptyEmailSubjectError); @@ -54,18 +56,10 @@ describe("EmailCompiler", () => { payload: { foo: 1 }, recipientEmail: "recipien@test.com", event: "ACCOUNT_DELETE", - smtpConfiguration: { - senderEmail: "sender@test.com", - senderName: "sender test", - events: [ - { - template: "test {{foo}}", - active: true, - eventType: "ACCOUNT_DELETE", - subject: "test subject", - }, - ], - }, + bodyTemplate: getMjmlTemplate("2137"), + senderEmail: "sender@saleor.io", + senderName: "John Doe from Saleor", + subjectTemplate: "", }); expect(result._unsafeUnwrapErr()).toBeInstanceOf(EmailCompiler.CompilationFailedError); @@ -76,18 +70,10 @@ describe("EmailCompiler", () => { payload: { foo: 1 }, recipientEmail: "recipien@test.com", event: "ACCOUNT_DELETE", - smtpConfiguration: { - senderEmail: "sender@test.com", - senderName: "sender test", - events: [ - { - template: "", - active: true, - eventType: "ACCOUNT_DELETE", - subject: "test subject", - }, - ], - }, + bodyTemplate: "", + senderEmail: "sender@saleor.io", + senderName: "John Doe from Saleor", + subjectTemplate: "subject", }); expect(result._unsafeUnwrapErr()).toBeInstanceOf(EmailCompiler.EmptyEmailBodyError); @@ -98,12 +84,10 @@ describe("EmailCompiler", () => { payload: { foo: 2137 }, recipientEmail: "recipien@test.com", event: "ACCOUNT_DELETE", - smtpConfiguration: { - senderEmail: "sender@test.com", - senderName: "sender test", - events: [ - { - template: ` + bodyTemplate: getMjmlTemplate("2137"), + senderEmail: "sender@saleor.io", + senderName: "John Doe from Saleor", + subjectTemplate: ` @@ -112,12 +96,6 @@ describe("EmailCompiler", () => { `, - active: true, - eventType: "ACCOUNT_DELETE", - subject: "test subject", - }, - ], - }, }); const resultValue = result._unsafeUnwrap(); diff --git a/apps/smtp/src/modules/smtp/email-compiler.ts b/apps/smtp/src/modules/smtp/email-compiler.ts index 273605b19..f6f85baf6 100644 --- a/apps/smtp/src/modules/smtp/email-compiler.ts +++ b/apps/smtp/src/modules/smtp/email-compiler.ts @@ -8,10 +8,13 @@ import { BaseError } from "../../errors"; import { err, ok, Result } from "neverthrow"; interface CompileArgs { - smtpConfiguration: Pick; recipientEmail: string; event: MessageEventTypes; payload: unknown; + bodyTemplate: string; + subjectTemplate: string; + senderName: string; + senderEmail: string; } export interface CompiledEmail { @@ -26,15 +29,8 @@ export interface IEmailCompiler { compile(args: CompileArgs): Result>; } -/* - * todo introduce modern-errors - */ export class EmailCompiler implements IEmailCompiler { static EmailCompilerError = BaseError.subclass("EmailCompilerError"); - static MissingEventSettingsError = this.EmailCompilerError.subclass("MissingEventSettingsError"); - static EventNotEnabledInSettingsError = this.EmailCompilerError.subclass( - "EventNotEnabledInSettingsError", - ); static CompilationFailedError = this.EmailCompilerError.subclass("CompilationFailedError"); static EmptyEmailSubjectError = this.EmailCompilerError.subclass("EmptyEmailSubjectError"); static EmptyEmailBodyError = this.EmailCompilerError.subclass("EmptyEmailBodyError"); @@ -45,38 +41,19 @@ export class EmailCompiler implements IEmailCompiler { payload, recipientEmail, event, - smtpConfiguration, + subjectTemplate, + bodyTemplate, + senderEmail, + senderName, }: CompileArgs): Result> { const logger = createLogger("sendSmtp", { name: "sendSmtp", event, }); - /** - * TODO: Move these checks higher, compile should only have necessary data - */ - const eventSettings = smtpConfiguration.events.find((e) => e.eventType === event); - - if (!eventSettings) { - logger.debug("No active settings for this event, skipping"); - - return err(new EmailCompiler.MissingEventSettingsError("No active settings for this event")); - } - - /** - * TODO: Move these checks higher, compile should only have necessary data - */ - if (!eventSettings.active) { - logger.debug("Event settings are not active, skipping"); - - return err(new EmailCompiler.EventNotEnabledInSettingsError("Event settings are not active")); - } - logger.debug("Compiling an email using MJML"); - const { template: rawTemplate, subject } = eventSettings; - - const subjectCompilationResult = this.templateCompiler.compile(subject, payload); + const subjectCompilationResult = this.templateCompiler.compile(subjectTemplate, payload); if (subjectCompilationResult.isErr()) { return err( @@ -102,7 +79,7 @@ export class EmailCompiler implements IEmailCompiler { logger.debug({ emailSubject }, "Subject compiled"); - const bodyCompilationResult = this.templateCompiler.compile(rawTemplate, payload); + const bodyCompilationResult = this.templateCompiler.compile(bodyTemplate, payload); if (bodyCompilationResult.isErr()) { return err( @@ -150,7 +127,7 @@ export class EmailCompiler implements IEmailCompiler { return ok({ text: emailBodyPlaintext, html: emailBodyHtml, - from: `${smtpConfiguration.senderName} <${smtpConfiguration.senderEmail}>`, + from: `${senderName} <${senderEmail}>`, to: recipientEmail, subject: emailSubject, }); diff --git a/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts b/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts index 57a4b7d65..df01eefdc 100644 --- a/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts +++ b/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts @@ -12,6 +12,7 @@ import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-me import { createSettingsManager } from "../../../lib/metadata-manager"; import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; import { EmailCompiler } from "../../../modules/smtp/email-compiler"; +import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; const GiftCardSentWebhookPayload = gql` fragment GiftCardSentWebhookPayload on GiftCardSent { @@ -113,7 +114,7 @@ const handler: NextWebhookApiHandler = async const useCase = new SendEventMessagesUseCase({ emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler(), + emailCompiler: new EmailCompiler(new HandlebarsTemplateCompiler()), smtpConfigurationService: new SmtpConfigurationService({ featureFlagService: new FeatureFlagService({ client }), metadataManager: new SmtpMetadataManager( diff --git a/apps/smtp/src/pages/api/webhooks/invoice-sent.ts b/apps/smtp/src/pages/api/webhooks/invoice-sent.ts index 3c8197b1d..c2619e95c 100644 --- a/apps/smtp/src/pages/api/webhooks/invoice-sent.ts +++ b/apps/smtp/src/pages/api/webhooks/invoice-sent.ts @@ -15,6 +15,7 @@ import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-me import { createSettingsManager } from "../../../lib/metadata-manager"; import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; import { EmailCompiler } from "../../../modules/smtp/email-compiler"; +import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; const InvoiceSentWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -93,7 +94,7 @@ const handler: NextWebhookApiHandler = async const useCase = new SendEventMessagesUseCase({ emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler(), + emailCompiler: new EmailCompiler(new HandlebarsTemplateCompiler()), smtpConfigurationService: new SmtpConfigurationService({ featureFlagService: new FeatureFlagService({ client }), metadataManager: new SmtpMetadataManager( diff --git a/apps/smtp/src/pages/api/webhooks/notify.ts b/apps/smtp/src/pages/api/webhooks/notify.ts index c79beb0d2..7e70cb754 100644 --- a/apps/smtp/src/pages/api/webhooks/notify.ts +++ b/apps/smtp/src/pages/api/webhooks/notify.ts @@ -11,6 +11,7 @@ import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-me import { createSettingsManager } from "../../../lib/metadata-manager"; import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; import { EmailCompiler } from "../../../modules/smtp/email-compiler"; +import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; /* * The Notify webhook is triggered on multiple Saleor events. @@ -58,7 +59,7 @@ const handler: NextWebhookApiHandler = async (req, re const useCase = new SendEventMessagesUseCase({ emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler(), + emailCompiler: new EmailCompiler(new HandlebarsTemplateCompiler()), smtpConfigurationService: new SmtpConfigurationService({ featureFlagService: new FeatureFlagService({ client }), metadataManager: new SmtpMetadataManager( diff --git a/apps/smtp/src/pages/api/webhooks/order-cancelled.ts b/apps/smtp/src/pages/api/webhooks/order-cancelled.ts index 3892ef2c6..152c130f6 100644 --- a/apps/smtp/src/pages/api/webhooks/order-cancelled.ts +++ b/apps/smtp/src/pages/api/webhooks/order-cancelled.ts @@ -15,6 +15,7 @@ import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-me import { createSettingsManager } from "../../../lib/metadata-manager"; import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; import { EmailCompiler } from "../../../modules/smtp/email-compiler"; +import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; const OrderCancelledWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -76,7 +77,7 @@ const handler: NextWebhookApiHandler = asy const useCase = new SendEventMessagesUseCase({ emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler(), + emailCompiler: new EmailCompiler(new HandlebarsTemplateCompiler()), smtpConfigurationService: new SmtpConfigurationService({ featureFlagService: new FeatureFlagService({ client }), metadataManager: new SmtpMetadataManager( diff --git a/apps/smtp/src/pages/api/webhooks/order-confirmed.ts b/apps/smtp/src/pages/api/webhooks/order-confirmed.ts index 8bf687ba9..4ec1cf29e 100644 --- a/apps/smtp/src/pages/api/webhooks/order-confirmed.ts +++ b/apps/smtp/src/pages/api/webhooks/order-confirmed.ts @@ -15,6 +15,7 @@ import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-me import { createSettingsManager } from "../../../lib/metadata-manager"; import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; import { EmailCompiler } from "../../../modules/smtp/email-compiler"; +import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; const OrderConfirmedWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -77,7 +78,7 @@ const handler: NextWebhookApiHandler = asy const useCase = new SendEventMessagesUseCase({ emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler(), + emailCompiler: new EmailCompiler(new HandlebarsTemplateCompiler()), smtpConfigurationService: new SmtpConfigurationService({ featureFlagService: new FeatureFlagService({ client }), metadataManager: new SmtpMetadataManager( diff --git a/apps/smtp/src/pages/api/webhooks/order-created.ts b/apps/smtp/src/pages/api/webhooks/order-created.ts index dbf6c6be0..6d56984f1 100644 --- a/apps/smtp/src/pages/api/webhooks/order-created.ts +++ b/apps/smtp/src/pages/api/webhooks/order-created.ts @@ -13,6 +13,7 @@ import { createSettingsManager } from "../../../lib/metadata-manager"; import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-metadata-manager"; import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; import { EmailCompiler } from "../../../modules/smtp/email-compiler"; +import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; const OrderCreatedWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -74,7 +75,7 @@ const handler: NextWebhookApiHandler = async const useCase = new SendEventMessagesUseCase({ emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler(), + emailCompiler: new EmailCompiler(new HandlebarsTemplateCompiler()), smtpConfigurationService: new SmtpConfigurationService({ featureFlagService: new FeatureFlagService({ client }), metadataManager: new SmtpMetadataManager( diff --git a/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts b/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts index f633bb3ad..8c0494e7d 100644 --- a/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts +++ b/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts @@ -15,6 +15,7 @@ import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-me import { createSettingsManager } from "../../../lib/metadata-manager"; import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; import { EmailCompiler } from "../../../modules/smtp/email-compiler"; +import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; const OrderFulfilledWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -77,7 +78,7 @@ const handler: NextWebhookApiHandler = asy const useCase = new SendEventMessagesUseCase({ emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler(), + emailCompiler: new EmailCompiler(new HandlebarsTemplateCompiler()), smtpConfigurationService: new SmtpConfigurationService({ featureFlagService: new FeatureFlagService({ client }), metadataManager: new SmtpMetadataManager( diff --git a/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts b/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts index 638bcce0b..850848756 100644 --- a/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts +++ b/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts @@ -15,6 +15,7 @@ import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-me import { createSettingsManager } from "../../../lib/metadata-manager"; import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; import { EmailCompiler } from "../../../modules/smtp/email-compiler"; +import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; const OrderFullyPaidWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -77,7 +78,7 @@ const handler: NextWebhookApiHandler = asy const useCase = new SendEventMessagesUseCase({ emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler(), + emailCompiler: new EmailCompiler(new HandlebarsTemplateCompiler()), smtpConfigurationService: new SmtpConfigurationService({ featureFlagService: new FeatureFlagService({ client }), metadataManager: new SmtpMetadataManager( diff --git a/apps/smtp/src/pages/api/webhooks/order-refunded.ts b/apps/smtp/src/pages/api/webhooks/order-refunded.ts index e9f95689d..40e9f72b5 100644 --- a/apps/smtp/src/pages/api/webhooks/order-refunded.ts +++ b/apps/smtp/src/pages/api/webhooks/order-refunded.ts @@ -15,6 +15,7 @@ import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-me import { createSettingsManager } from "../../../lib/metadata-manager"; import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; import { EmailCompiler } from "../../../modules/smtp/email-compiler"; +import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; const OrderRefundedWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -76,7 +77,7 @@ const handler: NextWebhookApiHandler = asyn const useCase = new SendEventMessagesUseCase({ emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler(), + emailCompiler: new EmailCompiler(new HandlebarsTemplateCompiler()), smtpConfigurationService: new SmtpConfigurationService({ featureFlagService: new FeatureFlagService({ client }), metadataManager: new SmtpMetadataManager( From 5382134c80a7633fbbdcb5fa20c1347faa98651d Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Mon, 13 May 2024 19:36:36 +0200 Subject: [PATCH 05/14] refactored html to plain text --- apps/smtp/src/modules/smtp/email-compiler.ts | 22 +++++++----- .../src/modules/smtp/html-to-plaintext.ts | 36 +++++++++++-------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/apps/smtp/src/modules/smtp/email-compiler.ts b/apps/smtp/src/modules/smtp/email-compiler.ts index f6f85baf6..6900a4d58 100644 --- a/apps/smtp/src/modules/smtp/email-compiler.ts +++ b/apps/smtp/src/modules/smtp/email-compiler.ts @@ -1,8 +1,7 @@ import { compileMjml } from "./compile-mjml"; import { ITemplateCompiler } from "./template-compiler"; import { MessageEventTypes } from "../event-handlers/message-event-types"; -import { htmlToPlaintext } from "./html-to-plaintext"; -import { SmtpConfiguration } from "./configuration/smtp-config-schema"; +import { IHtmlToTextCompiler, HtmlToTextCompiler } from "./html-to-plaintext"; import { createLogger } from "../../logger"; import { BaseError } from "../../errors"; import { err, ok, Result } from "neverthrow"; @@ -35,7 +34,10 @@ export class EmailCompiler implements IEmailCompiler { static EmptyEmailSubjectError = this.EmailCompilerError.subclass("EmptyEmailSubjectError"); static EmptyEmailBodyError = this.EmailCompilerError.subclass("EmptyEmailBodyError"); - constructor(private templateCompiler: ITemplateCompiler) {} + constructor( + private templateCompiler: ITemplateCompiler, + private htmlToTextCompiler: IHtmlToTextCompiler, + ) {} compile({ payload, @@ -114,18 +116,20 @@ export class EmailCompiler implements IEmailCompiler { logger.debug("MJML template compiled"); - const { plaintext: emailBodyPlaintext } = htmlToPlaintext(emailBodyHtml); + const plainTextCompilationResult = this.htmlToTextCompiler.compile(emailBodyHtml); - if (!emailBodyPlaintext || !emailBodyPlaintext?.length) { - logger.error("Email body could not be converted to plaintext"); - - throw new Error("Email body could not be converted to plaintext"); + if (plainTextCompilationResult.isErr()) { + return err( + new EmailCompiler.CompilationFailedError("Failed to compile body to plain text", { + errors: [plainTextCompilationResult.error], + }), + ); } logger.debug("Email body converted to plaintext"); return ok({ - text: emailBodyPlaintext, + text: plainTextCompilationResult.value, html: emailBodyHtml, from: `${senderName} <${senderEmail}>`, to: recipientEmail, diff --git a/apps/smtp/src/modules/smtp/html-to-plaintext.ts b/apps/smtp/src/modules/smtp/html-to-plaintext.ts index 7d884ffb3..49108559d 100644 --- a/apps/smtp/src/modules/smtp/html-to-plaintext.ts +++ b/apps/smtp/src/modules/smtp/html-to-plaintext.ts @@ -1,22 +1,28 @@ import { convert } from "html-to-text"; import { createLogger } from "../../logger"; +import { fromThrowable, Result } from "neverthrow"; +import { BaseError } from "../../errors"; -const logger = createLogger("htmlToPlaintext"); +export interface IHtmlToTextCompiler { + compile(html: string): Result>; +} -export const htmlToPlaintext = (html: string) => { - logger.debug("Converting HTML template to plaintext"); - try { - const plaintext = convert(html); +export class HtmlToTextCompiler implements IHtmlToTextCompiler { + static HtmlToTextCompilerError = BaseError.subclass("HtmlToTextCompilerError"); + static FailedToConvertError = this.HtmlToTextCompilerError.subclass("FailedToConvertError"); - logger.debug("Converted successfully"); - return { - plaintext, - }; - } catch (error) { - logger.error(error); - return { - errors: [{ message: "Could not convert html to plaintext" }], - }; + private logger = createLogger("HtmlToTextCompiler"); + + compile(html: string): Result> { + this.logger.debug("Trying to convert HTML to plaintext"); + + return fromThrowable( + convert, + (err) => + new HtmlToTextCompiler.FailedToConvertError("Failed to compile HTML to plain text", { + errors: [err], + }), + )(html); } -}; +} From d41511d6fffcc2cb64ce6fe9fbb328902a7e4ae9 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Mon, 13 May 2024 20:11:49 +0200 Subject: [PATCH 06/14] extracted email compilation services --- apps/smtp/src/modules/smtp/compile-mjml.ts | 50 ++++++++++++++----- .../smtp-configuration.router.ts | 8 +-- .../src/modules/smtp/email-compiler.test.ts | 45 +++++++++++------ apps/smtp/src/modules/smtp/email-compiler.ts | 28 +++++------ .../src/pages/api/webhooks/gift-card-sent.ts | 8 ++- .../src/pages/api/webhooks/invoice-sent.ts | 8 ++- apps/smtp/src/pages/api/webhooks/notify.ts | 8 ++- .../src/pages/api/webhooks/order-cancelled.ts | 8 ++- .../src/pages/api/webhooks/order-confirmed.ts | 8 ++- .../src/pages/api/webhooks/order-created.ts | 8 ++- .../src/pages/api/webhooks/order-fulfilled.ts | 8 ++- .../pages/api/webhooks/order-fully-paid.ts | 8 ++- .../src/pages/api/webhooks/order-refunded.ts | 8 ++- 13 files changed, 145 insertions(+), 58 deletions(-) diff --git a/apps/smtp/src/modules/smtp/compile-mjml.ts b/apps/smtp/src/modules/smtp/compile-mjml.ts index 9505d716b..ff7cdaf49 100644 --- a/apps/smtp/src/modules/smtp/compile-mjml.ts +++ b/apps/smtp/src/modules/smtp/compile-mjml.ts @@ -1,18 +1,42 @@ import mjml2html from "mjml"; import { createLogger } from "../../logger"; +import { err, fromThrowable, ok, Result } from "neverthrow"; +import { BaseError } from "../../errors"; -const logger = createLogger("compileMjml"); +export interface IMjmlCompiler { + compile(mjml: string): Result>; +} -// todo clas -export const compileMjml = (mjml: string) => { - logger.debug("Converting MJML template to HTML"); - try { - return mjml2html(mjml); - } catch (error) { - logger.error(error); - return { - html: undefined, - errors: [{ message: "Critical error during the mjml to html compilation" }], - }; +/** + * todo test + */ +export class MjmlCompiler implements IMjmlCompiler { + static MjmlCompilerError = BaseError.subclass("MjmlCompilerError"); + static FailedToCompileError = this.MjmlCompilerError.subclass("FailedToCompileError"); + + private logger = createLogger("MjmlCompiler"); + + compile(mjml: string): Result> { + this.logger.debug("Trying to compile MJML template"); + + const safeCompile = fromThrowable( + mjml2html, + (error) => + new MjmlCompiler.FailedToCompileError("Failed to compile MJML", { + errors: [error], + }), + ); + + return safeCompile(mjml).andThen((value) => { + if (value.errors && value.errors.length > 0) { + return err( + new MjmlCompiler.FailedToCompileError("Failed to compile MJML", { + errors: value.errors, + }), + ); + } else { + return ok(value.html); + } + }); } -}; +} diff --git a/apps/smtp/src/modules/smtp/configuration/smtp-configuration.router.ts b/apps/smtp/src/modules/smtp/configuration/smtp-configuration.router.ts index 9f108372d..3039e32e4 100644 --- a/apps/smtp/src/modules/smtp/configuration/smtp-configuration.router.ts +++ b/apps/smtp/src/modules/smtp/configuration/smtp-configuration.router.ts @@ -1,7 +1,7 @@ import { SmtpConfigurationService } from "./smtp-configuration.service"; import { router } from "../../trpc/trpc-server"; import { z } from "zod"; -import { compileMjml } from "../compile-mjml"; +import { MjmlCompiler } from "../compile-mjml"; import Handlebars from "handlebars"; import { TRPCError } from "@trpc/server"; import { @@ -162,10 +162,10 @@ export const smtpConfigurationRouter = router({ const compiledSubjectTemplate = Handlebars.compile(input.template); const templatedEmail = compiledSubjectTemplate(payload); - const { html: rawHtml } = compileMjml(templatedEmail); + const compilationResult = new MjmlCompiler().compile(templatedEmail); - if (rawHtml) { - renderedEmail = rawHtml; + if (compilationResult.isOk()) { + renderedEmail = compilationResult.value; } } diff --git a/apps/smtp/src/modules/smtp/email-compiler.test.ts b/apps/smtp/src/modules/smtp/email-compiler.test.ts index 77277d3af..1edd45392 100644 --- a/apps/smtp/src/modules/smtp/email-compiler.test.ts +++ b/apps/smtp/src/modules/smtp/email-compiler.test.ts @@ -2,6 +2,8 @@ import { describe, it, expect, beforeEach, vi, Mock } from "vitest"; import { EmailCompiler } from "./email-compiler"; import { HandlebarsTemplateCompiler, ITemplateCompiler } from "./template-compiler"; import { err, ok, Result } from "neverthrow"; +import { HtmlToTextCompiler } from "./html-to-plaintext"; +import { MjmlCompiler } from "./compile-mjml"; const getMjmlTemplate = (injectedValue: string | number) => ` @@ -15,13 +17,20 @@ const getMjmlTemplate = (injectedValue: string | number) => ` describe("EmailCompiler", () => { let templateCompiler = new HandlebarsTemplateCompiler(); - let compiler = new EmailCompiler(new HandlebarsTemplateCompiler()); + let compiler = new EmailCompiler( + new HandlebarsTemplateCompiler(), + new HtmlToTextCompiler(), + new MjmlCompiler(), + ); beforeEach(() => { vi.resetAllMocks(); - templateCompiler = new HandlebarsTemplateCompiler(); - compiler = new EmailCompiler(templateCompiler); + compiler = new EmailCompiler( + new HandlebarsTemplateCompiler(), + new HtmlToTextCompiler(), + new MjmlCompiler(), + ); }); it("Returns EmptyEmailSubjectError error if subject is empty", () => { @@ -39,18 +48,22 @@ describe("EmailCompiler", () => { }); it("Returns CompilationFailedError if underlying template compiler fails to compile", () => { - const compiler = new EmailCompiler({ - compile(): Result< - { - template: string; + const compiler = new EmailCompiler( + { + compile(): Result< + { + template: string; + }, + InstanceType + > { + return err( + new HandlebarsTemplateCompiler.HandlebarsTemplateCompilerError("Error to compile"), + ); }, - InstanceType - > { - return err( - new HandlebarsTemplateCompiler.HandlebarsTemplateCompilerError("Error to compile"), - ); }, - }); + new HtmlToTextCompiler(), + new MjmlCompiler(), + ); const result = compiler.compile({ payload: { foo: 1 }, @@ -84,10 +97,10 @@ describe("EmailCompiler", () => { payload: { foo: 2137 }, recipientEmail: "recipien@test.com", event: "ACCOUNT_DELETE", - bodyTemplate: getMjmlTemplate("2137"), + subjectTemplate: "test subject", senderEmail: "sender@saleor.io", senderName: "John Doe from Saleor", - subjectTemplate: ` + bodyTemplate: ` @@ -100,7 +113,7 @@ describe("EmailCompiler", () => { const resultValue = result._unsafeUnwrap(); - expect(resultValue.from).toEqual("sender test "); + expect(resultValue.from).toEqual("John Doe from Saleor "); expect(resultValue.to).toEqual("recipien@test.com"); expect(resultValue.subject).toEqual("test subject"); expect(resultValue.text).toEqual("2137"); diff --git a/apps/smtp/src/modules/smtp/email-compiler.ts b/apps/smtp/src/modules/smtp/email-compiler.ts index 6900a4d58..fa63ca4cf 100644 --- a/apps/smtp/src/modules/smtp/email-compiler.ts +++ b/apps/smtp/src/modules/smtp/email-compiler.ts @@ -1,7 +1,7 @@ -import { compileMjml } from "./compile-mjml"; +import { IMjmlCompiler } from "./compile-mjml"; import { ITemplateCompiler } from "./template-compiler"; import { MessageEventTypes } from "../event-handlers/message-event-types"; -import { IHtmlToTextCompiler, HtmlToTextCompiler } from "./html-to-plaintext"; +import { IHtmlToTextCompiler } from "./html-to-plaintext"; import { createLogger } from "../../logger"; import { BaseError } from "../../errors"; import { err, ok, Result } from "neverthrow"; @@ -37,6 +37,7 @@ export class EmailCompiler implements IEmailCompiler { constructor( private templateCompiler: ITemplateCompiler, private htmlToTextCompiler: IHtmlToTextCompiler, + private mjmlCompiler: IMjmlCompiler, ) {} compile({ @@ -99,24 +100,19 @@ export class EmailCompiler implements IEmailCompiler { logger.debug("Handlebars template compiled"); - const { html: emailBodyHtml, errors: mjmlCompilationErrors } = compileMjml(emailTemplate); + const mjmlCompilationResult = this.mjmlCompiler.compile(emailTemplate); - if (mjmlCompilationErrors.length) { - logger.error("Error during the MJML compilation"); - logger.error(mjmlCompilationErrors); - - throw new Error("Error during the MJML compilation. Please Validate your MJML template"); - } - - if (!emailBodyHtml || !emailBodyHtml?.length) { - logger.error("No MJML template returned after the compilation"); - - throw new Error("No MJML template returned after the compilation"); + if (mjmlCompilationResult.isErr()) { + return err( + new EmailCompiler.CompilationFailedError("Failed to compile MJML", { + errors: [mjmlCompilationResult.error], + }), + ); } logger.debug("MJML template compiled"); - const plainTextCompilationResult = this.htmlToTextCompiler.compile(emailBodyHtml); + const plainTextCompilationResult = this.htmlToTextCompiler.compile(mjmlCompilationResult.value); if (plainTextCompilationResult.isErr()) { return err( @@ -130,7 +126,7 @@ export class EmailCompiler implements IEmailCompiler { return ok({ text: plainTextCompilationResult.value, - html: emailBodyHtml, + html: mjmlCompilationResult.value, from: `${senderName} <${senderEmail}>`, to: recipientEmail, subject: emailSubject, diff --git a/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts b/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts index df01eefdc..1dfb1ae51 100644 --- a/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts +++ b/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts @@ -13,6 +13,8 @@ import { createSettingsManager } from "../../../lib/metadata-manager"; import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; import { EmailCompiler } from "../../../modules/smtp/email-compiler"; import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; +import { HtmlToTextCompiler } from "../../../modules/smtp/html-to-plaintext"; +import { MjmlCompiler } from "../../../modules/smtp/compile-mjml"; const GiftCardSentWebhookPayload = gql` fragment GiftCardSentWebhookPayload on GiftCardSent { @@ -114,7 +116,11 @@ const handler: NextWebhookApiHandler = async const useCase = new SendEventMessagesUseCase({ emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler(new HandlebarsTemplateCompiler()), + emailCompiler: new EmailCompiler( + new HandlebarsTemplateCompiler(), + new HtmlToTextCompiler(), + new MjmlCompiler(), + ), smtpConfigurationService: new SmtpConfigurationService({ featureFlagService: new FeatureFlagService({ client }), metadataManager: new SmtpMetadataManager( diff --git a/apps/smtp/src/pages/api/webhooks/invoice-sent.ts b/apps/smtp/src/pages/api/webhooks/invoice-sent.ts index c2619e95c..3906a3770 100644 --- a/apps/smtp/src/pages/api/webhooks/invoice-sent.ts +++ b/apps/smtp/src/pages/api/webhooks/invoice-sent.ts @@ -16,6 +16,8 @@ import { createSettingsManager } from "../../../lib/metadata-manager"; import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; import { EmailCompiler } from "../../../modules/smtp/email-compiler"; import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; +import { HtmlToTextCompiler } from "../../../modules/smtp/html-to-plaintext"; +import { MjmlCompiler } from "../../../modules/smtp/compile-mjml"; const InvoiceSentWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -94,7 +96,11 @@ const handler: NextWebhookApiHandler = async const useCase = new SendEventMessagesUseCase({ emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler(new HandlebarsTemplateCompiler()), + emailCompiler: new EmailCompiler( + new HandlebarsTemplateCompiler(), + new HtmlToTextCompiler(), + new MjmlCompiler(), + ), smtpConfigurationService: new SmtpConfigurationService({ featureFlagService: new FeatureFlagService({ client }), metadataManager: new SmtpMetadataManager( diff --git a/apps/smtp/src/pages/api/webhooks/notify.ts b/apps/smtp/src/pages/api/webhooks/notify.ts index 7e70cb754..908d94522 100644 --- a/apps/smtp/src/pages/api/webhooks/notify.ts +++ b/apps/smtp/src/pages/api/webhooks/notify.ts @@ -12,6 +12,8 @@ import { createSettingsManager } from "../../../lib/metadata-manager"; import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; import { EmailCompiler } from "../../../modules/smtp/email-compiler"; import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; +import { HtmlToTextCompiler } from "../../../modules/smtp/html-to-plaintext"; +import { MjmlCompiler } from "../../../modules/smtp/compile-mjml"; /* * The Notify webhook is triggered on multiple Saleor events. @@ -59,7 +61,11 @@ const handler: NextWebhookApiHandler = async (req, re const useCase = new SendEventMessagesUseCase({ emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler(new HandlebarsTemplateCompiler()), + emailCompiler: new EmailCompiler( + new HandlebarsTemplateCompiler(), + new HtmlToTextCompiler(), + new MjmlCompiler(), + ), smtpConfigurationService: new SmtpConfigurationService({ featureFlagService: new FeatureFlagService({ client }), metadataManager: new SmtpMetadataManager( diff --git a/apps/smtp/src/pages/api/webhooks/order-cancelled.ts b/apps/smtp/src/pages/api/webhooks/order-cancelled.ts index 152c130f6..ec3b18ff4 100644 --- a/apps/smtp/src/pages/api/webhooks/order-cancelled.ts +++ b/apps/smtp/src/pages/api/webhooks/order-cancelled.ts @@ -16,6 +16,8 @@ import { createSettingsManager } from "../../../lib/metadata-manager"; import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; import { EmailCompiler } from "../../../modules/smtp/email-compiler"; import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; +import { HtmlToTextCompiler } from "../../../modules/smtp/html-to-plaintext"; +import { MjmlCompiler } from "../../../modules/smtp/compile-mjml"; const OrderCancelledWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -77,7 +79,11 @@ const handler: NextWebhookApiHandler = asy const useCase = new SendEventMessagesUseCase({ emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler(new HandlebarsTemplateCompiler()), + emailCompiler: new EmailCompiler( + new HandlebarsTemplateCompiler(), + new HtmlToTextCompiler(), + new MjmlCompiler(), + ), smtpConfigurationService: new SmtpConfigurationService({ featureFlagService: new FeatureFlagService({ client }), metadataManager: new SmtpMetadataManager( diff --git a/apps/smtp/src/pages/api/webhooks/order-confirmed.ts b/apps/smtp/src/pages/api/webhooks/order-confirmed.ts index 4ec1cf29e..276ec79f0 100644 --- a/apps/smtp/src/pages/api/webhooks/order-confirmed.ts +++ b/apps/smtp/src/pages/api/webhooks/order-confirmed.ts @@ -16,6 +16,8 @@ import { createSettingsManager } from "../../../lib/metadata-manager"; import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; import { EmailCompiler } from "../../../modules/smtp/email-compiler"; import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; +import { HtmlToTextCompiler } from "../../../modules/smtp/html-to-plaintext"; +import { MjmlCompiler } from "../../../modules/smtp/compile-mjml"; const OrderConfirmedWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -78,7 +80,11 @@ const handler: NextWebhookApiHandler = asy const useCase = new SendEventMessagesUseCase({ emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler(new HandlebarsTemplateCompiler()), + emailCompiler: new EmailCompiler( + new HandlebarsTemplateCompiler(), + new HtmlToTextCompiler(), + new MjmlCompiler(), + ), smtpConfigurationService: new SmtpConfigurationService({ featureFlagService: new FeatureFlagService({ client }), metadataManager: new SmtpMetadataManager( diff --git a/apps/smtp/src/pages/api/webhooks/order-created.ts b/apps/smtp/src/pages/api/webhooks/order-created.ts index 6d56984f1..08b5fa33c 100644 --- a/apps/smtp/src/pages/api/webhooks/order-created.ts +++ b/apps/smtp/src/pages/api/webhooks/order-created.ts @@ -14,6 +14,8 @@ import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-me import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; import { EmailCompiler } from "../../../modules/smtp/email-compiler"; import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; +import { HtmlToTextCompiler } from "../../../modules/smtp/html-to-plaintext"; +import { MjmlCompiler } from "../../../modules/smtp/compile-mjml"; const OrderCreatedWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -75,7 +77,11 @@ const handler: NextWebhookApiHandler = async const useCase = new SendEventMessagesUseCase({ emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler(new HandlebarsTemplateCompiler()), + emailCompiler: new EmailCompiler( + new HandlebarsTemplateCompiler(), + new HtmlToTextCompiler(), + new MjmlCompiler(), + ), smtpConfigurationService: new SmtpConfigurationService({ featureFlagService: new FeatureFlagService({ client }), metadataManager: new SmtpMetadataManager( diff --git a/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts b/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts index 8c0494e7d..c720ecd0d 100644 --- a/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts +++ b/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts @@ -16,6 +16,8 @@ import { createSettingsManager } from "../../../lib/metadata-manager"; import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; import { EmailCompiler } from "../../../modules/smtp/email-compiler"; import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; +import { HtmlToTextCompiler } from "../../../modules/smtp/html-to-plaintext"; +import { MjmlCompiler } from "../../../modules/smtp/compile-mjml"; const OrderFulfilledWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -78,7 +80,11 @@ const handler: NextWebhookApiHandler = asy const useCase = new SendEventMessagesUseCase({ emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler(new HandlebarsTemplateCompiler()), + emailCompiler: new EmailCompiler( + new HandlebarsTemplateCompiler(), + new HtmlToTextCompiler(), + new MjmlCompiler(), + ), smtpConfigurationService: new SmtpConfigurationService({ featureFlagService: new FeatureFlagService({ client }), metadataManager: new SmtpMetadataManager( diff --git a/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts b/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts index 850848756..def2a95f2 100644 --- a/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts +++ b/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts @@ -16,6 +16,8 @@ import { createSettingsManager } from "../../../lib/metadata-manager"; import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; import { EmailCompiler } from "../../../modules/smtp/email-compiler"; import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; +import { HtmlToTextCompiler } from "../../../modules/smtp/html-to-plaintext"; +import { MjmlCompiler } from "../../../modules/smtp/compile-mjml"; const OrderFullyPaidWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -78,7 +80,11 @@ const handler: NextWebhookApiHandler = asy const useCase = new SendEventMessagesUseCase({ emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler(new HandlebarsTemplateCompiler()), + emailCompiler: new EmailCompiler( + new HandlebarsTemplateCompiler(), + new HtmlToTextCompiler(), + new MjmlCompiler(), + ), smtpConfigurationService: new SmtpConfigurationService({ featureFlagService: new FeatureFlagService({ client }), metadataManager: new SmtpMetadataManager( diff --git a/apps/smtp/src/pages/api/webhooks/order-refunded.ts b/apps/smtp/src/pages/api/webhooks/order-refunded.ts index 40e9f72b5..c6db8603b 100644 --- a/apps/smtp/src/pages/api/webhooks/order-refunded.ts +++ b/apps/smtp/src/pages/api/webhooks/order-refunded.ts @@ -16,6 +16,8 @@ import { createSettingsManager } from "../../../lib/metadata-manager"; import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; import { EmailCompiler } from "../../../modules/smtp/email-compiler"; import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; +import { HtmlToTextCompiler } from "../../../modules/smtp/html-to-plaintext"; +import { MjmlCompiler } from "../../../modules/smtp/compile-mjml"; const OrderRefundedWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -77,7 +79,11 @@ const handler: NextWebhookApiHandler = asyn const useCase = new SendEventMessagesUseCase({ emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler(new HandlebarsTemplateCompiler()), + emailCompiler: new EmailCompiler( + new HandlebarsTemplateCompiler(), + new HtmlToTextCompiler(), + new MjmlCompiler(), + ), smtpConfigurationService: new SmtpConfigurationService({ featureFlagService: new FeatureFlagService({ client }), metadataManager: new SmtpMetadataManager( From 8302b067c12fe0687142372aa35cd005cdd2b657 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Mon, 13 May 2024 20:13:43 +0200 Subject: [PATCH 07/14] move files aroudn --- .../event-handlers/send-event-messages.use-case.ts | 4 ++-- .../smtp/configuration/smtp-configuration.router.ts | 2 +- .../smtp/{ => services}/email-compiler.test.ts | 6 +++--- .../modules/smtp/{ => services}/email-compiler.ts | 12 ++++++------ .../handlebars-template-compiler.ts} | 4 ++-- .../html-to-text-compiler.ts} | 4 ++-- .../{compile-mjml.ts => services/mjml-compiler.ts} | 4 ++-- .../modules/smtp/{ => services}/smtp-email-sender.ts | 4 ++-- apps/smtp/src/pages/api/webhooks/gift-card-sent.ts | 10 +++++----- apps/smtp/src/pages/api/webhooks/invoice-sent.ts | 10 +++++----- apps/smtp/src/pages/api/webhooks/notify.ts | 10 +++++----- apps/smtp/src/pages/api/webhooks/order-cancelled.ts | 10 +++++----- apps/smtp/src/pages/api/webhooks/order-confirmed.ts | 10 +++++----- apps/smtp/src/pages/api/webhooks/order-created.ts | 10 +++++----- apps/smtp/src/pages/api/webhooks/order-fulfilled.ts | 10 +++++----- apps/smtp/src/pages/api/webhooks/order-fully-paid.ts | 10 +++++----- apps/smtp/src/pages/api/webhooks/order-refunded.ts | 10 +++++----- 17 files changed, 65 insertions(+), 65 deletions(-) rename apps/smtp/src/modules/smtp/{ => services}/email-compiler.test.ts (96%) rename apps/smtp/src/modules/smtp/{ => services}/email-compiler.ts (91%) rename apps/smtp/src/modules/smtp/{template-compiler.ts => services/handlebars-template-compiler.ts} (93%) rename apps/smtp/src/modules/smtp/{html-to-plaintext.ts => services/html-to-text-compiler.ts} (90%) rename apps/smtp/src/modules/smtp/{compile-mjml.ts => services/mjml-compiler.ts} (92%) rename apps/smtp/src/modules/smtp/{ => services}/smtp-email-sender.ts (95%) diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts index a7ed1ab0d..66dc7f098 100644 --- a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts +++ b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts @@ -1,8 +1,8 @@ import { SmtpConfigurationService } from "../smtp/configuration/smtp-configuration.service"; -import { IEmailCompiler } from "../smtp/email-compiler"; +import { IEmailCompiler } from "../smtp/services/email-compiler"; import { MessageEventTypes } from "./message-event-types"; import { createLogger } from "../../logger"; -import { ISMTPEmailSender, SendMailArgs } from "../smtp/smtp-email-sender"; +import { ISMTPEmailSender, SendMailArgs } from "../smtp/services/smtp-email-sender"; // todo test export class SendEventMessagesUseCase { diff --git a/apps/smtp/src/modules/smtp/configuration/smtp-configuration.router.ts b/apps/smtp/src/modules/smtp/configuration/smtp-configuration.router.ts index 3039e32e4..c209e850e 100644 --- a/apps/smtp/src/modules/smtp/configuration/smtp-configuration.router.ts +++ b/apps/smtp/src/modules/smtp/configuration/smtp-configuration.router.ts @@ -1,7 +1,7 @@ import { SmtpConfigurationService } from "./smtp-configuration.service"; import { router } from "../../trpc/trpc-server"; import { z } from "zod"; -import { MjmlCompiler } from "../compile-mjml"; +import { MjmlCompiler } from "../services/mjml-compiler"; import Handlebars from "handlebars"; import { TRPCError } from "@trpc/server"; import { diff --git a/apps/smtp/src/modules/smtp/email-compiler.test.ts b/apps/smtp/src/modules/smtp/services/email-compiler.test.ts similarity index 96% rename from apps/smtp/src/modules/smtp/email-compiler.test.ts rename to apps/smtp/src/modules/smtp/services/email-compiler.test.ts index 1edd45392..00063612f 100644 --- a/apps/smtp/src/modules/smtp/email-compiler.test.ts +++ b/apps/smtp/src/modules/smtp/services/email-compiler.test.ts @@ -1,9 +1,9 @@ import { describe, it, expect, beforeEach, vi, Mock } from "vitest"; import { EmailCompiler } from "./email-compiler"; -import { HandlebarsTemplateCompiler, ITemplateCompiler } from "./template-compiler"; +import { HandlebarsTemplateCompiler, ITemplateCompiler } from "./handlebars-template-compiler"; import { err, ok, Result } from "neverthrow"; -import { HtmlToTextCompiler } from "./html-to-plaintext"; -import { MjmlCompiler } from "./compile-mjml"; +import { HtmlToTextCompiler } from "./html-to-text-compiler"; +import { MjmlCompiler } from "./mjml-compiler"; const getMjmlTemplate = (injectedValue: string | number) => ` diff --git a/apps/smtp/src/modules/smtp/email-compiler.ts b/apps/smtp/src/modules/smtp/services/email-compiler.ts similarity index 91% rename from apps/smtp/src/modules/smtp/email-compiler.ts rename to apps/smtp/src/modules/smtp/services/email-compiler.ts index fa63ca4cf..ecba8e930 100644 --- a/apps/smtp/src/modules/smtp/email-compiler.ts +++ b/apps/smtp/src/modules/smtp/services/email-compiler.ts @@ -1,9 +1,9 @@ -import { IMjmlCompiler } from "./compile-mjml"; -import { ITemplateCompiler } from "./template-compiler"; -import { MessageEventTypes } from "../event-handlers/message-event-types"; -import { IHtmlToTextCompiler } from "./html-to-plaintext"; -import { createLogger } from "../../logger"; -import { BaseError } from "../../errors"; +import { IMjmlCompiler } from "./mjml-compiler"; +import { ITemplateCompiler } from "./handlebars-template-compiler"; +import { MessageEventTypes } from "../../event-handlers/message-event-types"; +import { IHtmlToTextCompiler } from "./html-to-text-compiler"; +import { createLogger } from "../../../logger"; +import { BaseError } from "../../../errors"; import { err, ok, Result } from "neverthrow"; interface CompileArgs { diff --git a/apps/smtp/src/modules/smtp/template-compiler.ts b/apps/smtp/src/modules/smtp/services/handlebars-template-compiler.ts similarity index 93% rename from apps/smtp/src/modules/smtp/template-compiler.ts rename to apps/smtp/src/modules/smtp/services/handlebars-template-compiler.ts index c5f61fcf0..00e6a5c94 100644 --- a/apps/smtp/src/modules/smtp/template-compiler.ts +++ b/apps/smtp/src/modules/smtp/services/handlebars-template-compiler.ts @@ -1,7 +1,7 @@ import Handlebars from "handlebars"; -import { createLogger } from "../../logger"; +import { createLogger } from "../../../logger"; import { err, ok, Result } from "neverthrow"; -import { BaseError } from "../../errors"; +import { BaseError } from "../../../errors"; const logger = createLogger("compileHandlebarsTemplate"); diff --git a/apps/smtp/src/modules/smtp/html-to-plaintext.ts b/apps/smtp/src/modules/smtp/services/html-to-text-compiler.ts similarity index 90% rename from apps/smtp/src/modules/smtp/html-to-plaintext.ts rename to apps/smtp/src/modules/smtp/services/html-to-text-compiler.ts index 49108559d..baa67adaf 100644 --- a/apps/smtp/src/modules/smtp/html-to-plaintext.ts +++ b/apps/smtp/src/modules/smtp/services/html-to-text-compiler.ts @@ -1,8 +1,8 @@ import { convert } from "html-to-text"; -import { createLogger } from "../../logger"; +import { createLogger } from "../../../logger"; import { fromThrowable, Result } from "neverthrow"; -import { BaseError } from "../../errors"; +import { BaseError } from "../../../errors"; export interface IHtmlToTextCompiler { compile(html: string): Result>; diff --git a/apps/smtp/src/modules/smtp/compile-mjml.ts b/apps/smtp/src/modules/smtp/services/mjml-compiler.ts similarity index 92% rename from apps/smtp/src/modules/smtp/compile-mjml.ts rename to apps/smtp/src/modules/smtp/services/mjml-compiler.ts index ff7cdaf49..fdb7bcd7e 100644 --- a/apps/smtp/src/modules/smtp/compile-mjml.ts +++ b/apps/smtp/src/modules/smtp/services/mjml-compiler.ts @@ -1,7 +1,7 @@ import mjml2html from "mjml"; -import { createLogger } from "../../logger"; +import { createLogger } from "../../../logger"; import { err, fromThrowable, ok, Result } from "neverthrow"; -import { BaseError } from "../../errors"; +import { BaseError } from "../../../errors"; export interface IMjmlCompiler { compile(mjml: string): Result>; diff --git a/apps/smtp/src/modules/smtp/smtp-email-sender.ts b/apps/smtp/src/modules/smtp/services/smtp-email-sender.ts similarity index 95% rename from apps/smtp/src/modules/smtp/smtp-email-sender.ts rename to apps/smtp/src/modules/smtp/services/smtp-email-sender.ts index be88a4cd0..15f309a57 100644 --- a/apps/smtp/src/modules/smtp/smtp-email-sender.ts +++ b/apps/smtp/src/modules/smtp/services/smtp-email-sender.ts @@ -1,6 +1,6 @@ import nodemailer from "nodemailer"; -import { createLogger } from "../../logger"; -import { SmtpEncryptionType } from "./configuration/smtp-config-schema"; +import { createLogger } from "../../../logger"; +import { SmtpEncryptionType } from "../configuration/smtp-config-schema"; export interface SendMailArgs { smtpSettings: { diff --git a/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts b/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts index 1dfb1ae51..698747f72 100644 --- a/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts +++ b/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts @@ -10,11 +10,11 @@ import { SmtpConfigurationService } from "../../../modules/smtp/configuration/sm import { FeatureFlagService } from "../../../modules/feature-flag-service/feature-flag-service"; import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-metadata-manager"; import { createSettingsManager } from "../../../lib/metadata-manager"; -import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; -import { EmailCompiler } from "../../../modules/smtp/email-compiler"; -import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; -import { HtmlToTextCompiler } from "../../../modules/smtp/html-to-plaintext"; -import { MjmlCompiler } from "../../../modules/smtp/compile-mjml"; +import { SmtpEmailSender } from "../../../modules/smtp/services/smtp-email-sender"; +import { EmailCompiler } from "../../../modules/smtp/services/email-compiler"; +import { HandlebarsTemplateCompiler } from "../../../modules/smtp/services/handlebars-template-compiler"; +import { HtmlToTextCompiler } from "../../../modules/smtp/services/html-to-text-compiler"; +import { MjmlCompiler } from "../../../modules/smtp/services/mjml-compiler"; const GiftCardSentWebhookPayload = gql` fragment GiftCardSentWebhookPayload on GiftCardSent { diff --git a/apps/smtp/src/pages/api/webhooks/invoice-sent.ts b/apps/smtp/src/pages/api/webhooks/invoice-sent.ts index 3906a3770..e43fc2e12 100644 --- a/apps/smtp/src/pages/api/webhooks/invoice-sent.ts +++ b/apps/smtp/src/pages/api/webhooks/invoice-sent.ts @@ -13,11 +13,11 @@ import { SmtpConfigurationService } from "../../../modules/smtp/configuration/sm import { FeatureFlagService } from "../../../modules/feature-flag-service/feature-flag-service"; import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-metadata-manager"; import { createSettingsManager } from "../../../lib/metadata-manager"; -import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; -import { EmailCompiler } from "../../../modules/smtp/email-compiler"; -import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; -import { HtmlToTextCompiler } from "../../../modules/smtp/html-to-plaintext"; -import { MjmlCompiler } from "../../../modules/smtp/compile-mjml"; +import { SmtpEmailSender } from "../../../modules/smtp/services/smtp-email-sender"; +import { EmailCompiler } from "../../../modules/smtp/services/email-compiler"; +import { HandlebarsTemplateCompiler } from "../../../modules/smtp/services/handlebars-template-compiler"; +import { HtmlToTextCompiler } from "../../../modules/smtp/services/html-to-text-compiler"; +import { MjmlCompiler } from "../../../modules/smtp/services/mjml-compiler"; const InvoiceSentWebhookPayload = gql` ${OrderDetailsFragmentDoc} diff --git a/apps/smtp/src/pages/api/webhooks/notify.ts b/apps/smtp/src/pages/api/webhooks/notify.ts index 908d94522..6d69909e1 100644 --- a/apps/smtp/src/pages/api/webhooks/notify.ts +++ b/apps/smtp/src/pages/api/webhooks/notify.ts @@ -9,11 +9,11 @@ import { SmtpConfigurationService } from "../../../modules/smtp/configuration/sm import { FeatureFlagService } from "../../../modules/feature-flag-service/feature-flag-service"; import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-metadata-manager"; import { createSettingsManager } from "../../../lib/metadata-manager"; -import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; -import { EmailCompiler } from "../../../modules/smtp/email-compiler"; -import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; -import { HtmlToTextCompiler } from "../../../modules/smtp/html-to-plaintext"; -import { MjmlCompiler } from "../../../modules/smtp/compile-mjml"; +import { SmtpEmailSender } from "../../../modules/smtp/services/smtp-email-sender"; +import { EmailCompiler } from "../../../modules/smtp/services/email-compiler"; +import { HandlebarsTemplateCompiler } from "../../../modules/smtp/services/handlebars-template-compiler"; +import { HtmlToTextCompiler } from "../../../modules/smtp/services/html-to-text-compiler"; +import { MjmlCompiler } from "../../../modules/smtp/services/mjml-compiler"; /* * The Notify webhook is triggered on multiple Saleor events. diff --git a/apps/smtp/src/pages/api/webhooks/order-cancelled.ts b/apps/smtp/src/pages/api/webhooks/order-cancelled.ts index ec3b18ff4..a5b1d6164 100644 --- a/apps/smtp/src/pages/api/webhooks/order-cancelled.ts +++ b/apps/smtp/src/pages/api/webhooks/order-cancelled.ts @@ -13,11 +13,11 @@ import { SmtpConfigurationService } from "../../../modules/smtp/configuration/sm import { FeatureFlagService } from "../../../modules/feature-flag-service/feature-flag-service"; import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-metadata-manager"; import { createSettingsManager } from "../../../lib/metadata-manager"; -import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; -import { EmailCompiler } from "../../../modules/smtp/email-compiler"; -import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; -import { HtmlToTextCompiler } from "../../../modules/smtp/html-to-plaintext"; -import { MjmlCompiler } from "../../../modules/smtp/compile-mjml"; +import { SmtpEmailSender } from "../../../modules/smtp/services/smtp-email-sender"; +import { EmailCompiler } from "../../../modules/smtp/services/email-compiler"; +import { HandlebarsTemplateCompiler } from "../../../modules/smtp/services/handlebars-template-compiler"; +import { HtmlToTextCompiler } from "../../../modules/smtp/services/html-to-text-compiler"; +import { MjmlCompiler } from "../../../modules/smtp/services/mjml-compiler"; const OrderCancelledWebhookPayload = gql` ${OrderDetailsFragmentDoc} diff --git a/apps/smtp/src/pages/api/webhooks/order-confirmed.ts b/apps/smtp/src/pages/api/webhooks/order-confirmed.ts index 276ec79f0..e8cde0dfe 100644 --- a/apps/smtp/src/pages/api/webhooks/order-confirmed.ts +++ b/apps/smtp/src/pages/api/webhooks/order-confirmed.ts @@ -13,11 +13,11 @@ import { SmtpConfigurationService } from "../../../modules/smtp/configuration/sm import { FeatureFlagService } from "../../../modules/feature-flag-service/feature-flag-service"; import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-metadata-manager"; import { createSettingsManager } from "../../../lib/metadata-manager"; -import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; -import { EmailCompiler } from "../../../modules/smtp/email-compiler"; -import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; -import { HtmlToTextCompiler } from "../../../modules/smtp/html-to-plaintext"; -import { MjmlCompiler } from "../../../modules/smtp/compile-mjml"; +import { SmtpEmailSender } from "../../../modules/smtp/services/smtp-email-sender"; +import { EmailCompiler } from "../../../modules/smtp/services/email-compiler"; +import { HandlebarsTemplateCompiler } from "../../../modules/smtp/services/handlebars-template-compiler"; +import { HtmlToTextCompiler } from "../../../modules/smtp/services/html-to-text-compiler"; +import { MjmlCompiler } from "../../../modules/smtp/services/mjml-compiler"; const OrderConfirmedWebhookPayload = gql` ${OrderDetailsFragmentDoc} diff --git a/apps/smtp/src/pages/api/webhooks/order-created.ts b/apps/smtp/src/pages/api/webhooks/order-created.ts index 08b5fa33c..879e0a44b 100644 --- a/apps/smtp/src/pages/api/webhooks/order-created.ts +++ b/apps/smtp/src/pages/api/webhooks/order-created.ts @@ -11,11 +11,11 @@ import { SmtpConfigurationService } from "../../../modules/smtp/configuration/sm import { FeatureFlagService } from "../../../modules/feature-flag-service/feature-flag-service"; import { createSettingsManager } from "../../../lib/metadata-manager"; import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-metadata-manager"; -import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; -import { EmailCompiler } from "../../../modules/smtp/email-compiler"; -import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; -import { HtmlToTextCompiler } from "../../../modules/smtp/html-to-plaintext"; -import { MjmlCompiler } from "../../../modules/smtp/compile-mjml"; +import { SmtpEmailSender } from "../../../modules/smtp/services/smtp-email-sender"; +import { EmailCompiler } from "../../../modules/smtp/services/email-compiler"; +import { HandlebarsTemplateCompiler } from "../../../modules/smtp/services/handlebars-template-compiler"; +import { HtmlToTextCompiler } from "../../../modules/smtp/services/html-to-text-compiler"; +import { MjmlCompiler } from "../../../modules/smtp/services/mjml-compiler"; const OrderCreatedWebhookPayload = gql` ${OrderDetailsFragmentDoc} diff --git a/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts b/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts index c720ecd0d..31b9f7e7a 100644 --- a/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts +++ b/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts @@ -13,11 +13,11 @@ import { SmtpConfigurationService } from "../../../modules/smtp/configuration/sm import { FeatureFlagService } from "../../../modules/feature-flag-service/feature-flag-service"; import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-metadata-manager"; import { createSettingsManager } from "../../../lib/metadata-manager"; -import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; -import { EmailCompiler } from "../../../modules/smtp/email-compiler"; -import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; -import { HtmlToTextCompiler } from "../../../modules/smtp/html-to-plaintext"; -import { MjmlCompiler } from "../../../modules/smtp/compile-mjml"; +import { SmtpEmailSender } from "../../../modules/smtp/services/smtp-email-sender"; +import { EmailCompiler } from "../../../modules/smtp/services/email-compiler"; +import { HandlebarsTemplateCompiler } from "../../../modules/smtp/services/handlebars-template-compiler"; +import { HtmlToTextCompiler } from "../../../modules/smtp/services/html-to-text-compiler"; +import { MjmlCompiler } from "../../../modules/smtp/services/mjml-compiler"; const OrderFulfilledWebhookPayload = gql` ${OrderDetailsFragmentDoc} diff --git a/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts b/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts index def2a95f2..29c07871c 100644 --- a/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts +++ b/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts @@ -13,11 +13,11 @@ import { SmtpConfigurationService } from "../../../modules/smtp/configuration/sm import { FeatureFlagService } from "../../../modules/feature-flag-service/feature-flag-service"; import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-metadata-manager"; import { createSettingsManager } from "../../../lib/metadata-manager"; -import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; -import { EmailCompiler } from "../../../modules/smtp/email-compiler"; -import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; -import { HtmlToTextCompiler } from "../../../modules/smtp/html-to-plaintext"; -import { MjmlCompiler } from "../../../modules/smtp/compile-mjml"; +import { SmtpEmailSender } from "../../../modules/smtp/services/smtp-email-sender"; +import { EmailCompiler } from "../../../modules/smtp/services/email-compiler"; +import { HandlebarsTemplateCompiler } from "../../../modules/smtp/services/handlebars-template-compiler"; +import { HtmlToTextCompiler } from "../../../modules/smtp/services/html-to-text-compiler"; +import { MjmlCompiler } from "../../../modules/smtp/services/mjml-compiler"; const OrderFullyPaidWebhookPayload = gql` ${OrderDetailsFragmentDoc} diff --git a/apps/smtp/src/pages/api/webhooks/order-refunded.ts b/apps/smtp/src/pages/api/webhooks/order-refunded.ts index c6db8603b..cdc7d0f4e 100644 --- a/apps/smtp/src/pages/api/webhooks/order-refunded.ts +++ b/apps/smtp/src/pages/api/webhooks/order-refunded.ts @@ -13,11 +13,11 @@ import { SmtpConfigurationService } from "../../../modules/smtp/configuration/sm import { FeatureFlagService } from "../../../modules/feature-flag-service/feature-flag-service"; import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-metadata-manager"; import { createSettingsManager } from "../../../lib/metadata-manager"; -import { SmtpEmailSender } from "../../../modules/smtp/smtp-email-sender"; -import { EmailCompiler } from "../../../modules/smtp/email-compiler"; -import { HandlebarsTemplateCompiler } from "../../../modules/smtp/template-compiler"; -import { HtmlToTextCompiler } from "../../../modules/smtp/html-to-plaintext"; -import { MjmlCompiler } from "../../../modules/smtp/compile-mjml"; +import { SmtpEmailSender } from "../../../modules/smtp/services/smtp-email-sender"; +import { EmailCompiler } from "../../../modules/smtp/services/email-compiler"; +import { HandlebarsTemplateCompiler } from "../../../modules/smtp/services/handlebars-template-compiler"; +import { HtmlToTextCompiler } from "../../../modules/smtp/services/html-to-text-compiler"; +import { MjmlCompiler } from "../../../modules/smtp/services/mjml-compiler"; const OrderRefundedWebhookPayload = gql` ${OrderDetailsFragmentDoc} From d53e7355da30688de5480cf05b7823fc364de422 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Mon, 13 May 2024 20:19:26 +0200 Subject: [PATCH 08/14] handlebars template compiler test --- .../handlebars-template-compiler.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 apps/smtp/src/modules/smtp/services/handlebars-template-compiler.test.ts diff --git a/apps/smtp/src/modules/smtp/services/handlebars-template-compiler.test.ts b/apps/smtp/src/modules/smtp/services/handlebars-template-compiler.test.ts new file mode 100644 index 000000000..86bc4d80f --- /dev/null +++ b/apps/smtp/src/modules/smtp/services/handlebars-template-compiler.test.ts @@ -0,0 +1,18 @@ +import { describe, it, vi, expect } from "vitest"; +import { HandlebarsTemplateCompiler } from "./handlebars-template-compiler"; + +describe("HandlebarsTemplateCompiler", () => { + it("Compiles template", () => { + const compiler = new HandlebarsTemplateCompiler(); + const result = compiler.compile("Template {{foo}}", { foo: "bar" }); + + expect(result._unsafeUnwrap()).toEqual({ template: "Template bar" }); + }); + + it("Returns error if compilation failed", () => { + const compiler = new HandlebarsTemplateCompiler(); + const result = compiler.compile("{{{", { foo: "bar" }); + + expect(result._unsafeUnwrapErr()).toBeInstanceOf(HandlebarsTemplateCompiler.FailedCompileError); + }); +}); From f95791b4595767cf0ae7431c05436286f5e5d3e7 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Mon, 13 May 2024 20:38:00 +0200 Subject: [PATCH 09/14] tests --- .../services/html-to-text-compiler.test.ts | 20 +++ .../smtp/services/html-to-text-compiler.ts | 16 +- .../smtp/services/mjml-compiler.test.ts | 161 ++++++++++++++++++ 3 files changed, 189 insertions(+), 8 deletions(-) create mode 100644 apps/smtp/src/modules/smtp/services/html-to-text-compiler.test.ts create mode 100644 apps/smtp/src/modules/smtp/services/mjml-compiler.test.ts diff --git a/apps/smtp/src/modules/smtp/services/html-to-text-compiler.test.ts b/apps/smtp/src/modules/smtp/services/html-to-text-compiler.test.ts new file mode 100644 index 000000000..c832de307 --- /dev/null +++ b/apps/smtp/src/modules/smtp/services/html-to-text-compiler.test.ts @@ -0,0 +1,20 @@ +import { it, vi, expect, describe } from "vitest"; +import { HtmlToTextCompiler } from "./html-to-text-compiler"; + +describe("HtmlToTextCompiler", () => { + it("Compiles HTML to plain text", () => { + const compiler = new HtmlToTextCompiler(); + + const result = compiler.compile("
Foo: bar
"); + + expect(result._unsafeUnwrap()).toEqual("Foo: bar"); + }); + + it("Fallbacks to empty string when HTML is broken", () => { + const compiler = new HtmlToTextCompiler(); + + const result = compiler.compile("> { this.logger.debug("Trying to convert HTML to plaintext"); - return fromThrowable( - convert, - (err) => - new HtmlToTextCompiler.FailedToConvertError("Failed to compile HTML to plain text", { - errors: [err], - }), - )(html); + return fromThrowable(convert, (err) => { + this.logger.debug("Failed to compile HTML to plain text", { error: err }); + + return new HtmlToTextCompiler.FailedToCompileError("Failed to compile HTML to plain text", { + errors: [err], + }); + })(html); } } diff --git a/apps/smtp/src/modules/smtp/services/mjml-compiler.test.ts b/apps/smtp/src/modules/smtp/services/mjml-compiler.test.ts new file mode 100644 index 000000000..1c9778679 --- /dev/null +++ b/apps/smtp/src/modules/smtp/services/mjml-compiler.test.ts @@ -0,0 +1,161 @@ +import { it, expect, vi, describe } from "vitest"; +import { MjmlCompiler } from "./mjml-compiler"; + +describe("MjmlCompiler", () => { + it("Compiles MJML syntax to HTML", () => { + const compiler = new MjmlCompiler(); + + const result = compiler.compile(` + + + + + {{injectedValue}} + + + + + `); + + expect(result._unsafeUnwrap()).toMatchInlineSnapshot(` + " + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
{{injectedValue}}
+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + " + `); + }); + + it("Returns error if mjml syntax is broken", () => { + const compiler = new MjmlCompiler(); + + const result = compiler.compile(` + 2137 + `); + + expect(result._unsafeUnwrapErr()).toBeInstanceOf(MjmlCompiler.FailedToCompileError); + }); +}); From 471f05b54377a160c2fde63795939dca6fe8e345 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Mon, 13 May 2024 21:15:59 +0200 Subject: [PATCH 10/14] email compiler refactor --- .../modules/smtp/services/email-compiler.ts | 169 ++++++++++-------- 1 file changed, 90 insertions(+), 79 deletions(-) diff --git a/apps/smtp/src/modules/smtp/services/email-compiler.ts b/apps/smtp/src/modules/smtp/services/email-compiler.ts index ecba8e930..50404a050 100644 --- a/apps/smtp/src/modules/smtp/services/email-compiler.ts +++ b/apps/smtp/src/modules/smtp/services/email-compiler.ts @@ -5,6 +5,7 @@ import { IHtmlToTextCompiler } from "./html-to-text-compiler"; import { createLogger } from "../../../logger"; import { BaseError } from "../../../errors"; import { err, ok, Result } from "neverthrow"; +import combine = Result.combine; interface CompileArgs { recipientEmail: string; @@ -40,6 +41,72 @@ export class EmailCompiler implements IEmailCompiler { private mjmlCompiler: IMjmlCompiler, ) {} + private resolveEmailSubject = (subjectTemplate: string, payload: unknown) => + this.templateCompiler + .compile(subjectTemplate, payload) + .orElse((error) => + err( + new EmailCompiler.CompilationFailedError("Failed to compile email subject template", { + errors: [error], + }), + ), + ) + .andThen((value) => { + if (!value.template || !value.template?.length) { + return err( + new EmailCompiler.EmptyEmailSubjectError("Mjml subject message is empty, skipping", { + props: { + subject: value.template, + }, + }), + ); + } + + return ok(value); + }); + + private resolveBodyTemplate( + bodyTemplate: string, + payload: unknown, + ): Result<{ template: string }, InstanceType> { + return this.templateCompiler + .compile(bodyTemplate, payload) + .orElse((error) => { + return err( + new EmailCompiler.CompilationFailedError("Failed to compile email body template", { + errors: [error], + }), + ); + }) + .andThen((value) => { + if (!value.template || !value.template?.length) { + return err(new EmailCompiler.EmptyEmailBodyError("MJML template body is empty")); + } + + return ok(value); + }); + } + + private resolveBodyMjml = (emailTemplate: string) => + this.mjmlCompiler.compile(emailTemplate).orElse((error) => { + return err( + new EmailCompiler.CompilationFailedError("Failed to compile MJML", { + errors: [error], + }), + ); + }); + + private resolveBodyPlainText = ( + html: string, + ): Result> => + this.htmlToTextCompiler.compile(html).orElse((error) => + err( + new EmailCompiler.CompilationFailedError("Failed to compile body to plain text", { + errors: [error], + }), + ), + ); + compile({ payload, recipientEmail, @@ -49,87 +116,31 @@ export class EmailCompiler implements IEmailCompiler { senderEmail, senderName, }: CompileArgs): Result> { - const logger = createLogger("sendSmtp", { - name: "sendSmtp", + const logger = createLogger("EmailCompiler", { event, }); - logger.debug("Compiling an email using MJML"); - - const subjectCompilationResult = this.templateCompiler.compile(subjectTemplate, payload); - - if (subjectCompilationResult.isErr()) { - return err( - new EmailCompiler.CompilationFailedError("Failed to compile email subject template", { - errors: [subjectCompilationResult.error], - }), - ); - } - - const { template: emailSubject } = subjectCompilationResult.value; - - if (!emailSubject || !emailSubject?.length) { - logger.error("Mjml subject message is empty, skipping"); - - return err( - new EmailCompiler.EmptyEmailSubjectError("Mjml subject message is empty, skipping", { - props: { - subject: emailSubject, - }, - }), - ); - } - - logger.debug({ emailSubject }, "Subject compiled"); - - const bodyCompilationResult = this.templateCompiler.compile(bodyTemplate, payload); - - if (bodyCompilationResult.isErr()) { - return err( - new EmailCompiler.CompilationFailedError("Failed to compile email body template", { - errors: [bodyCompilationResult.error], - }), - ); - } - - const { template: emailTemplate } = bodyCompilationResult.value; - - if (!emailTemplate || !emailTemplate?.length) { - return err(new EmailCompiler.EmptyEmailBodyError("MJML template body is empty")); - } - - logger.debug("Handlebars template compiled"); - - const mjmlCompilationResult = this.mjmlCompiler.compile(emailTemplate); - - if (mjmlCompilationResult.isErr()) { - return err( - new EmailCompiler.CompilationFailedError("Failed to compile MJML", { - errors: [mjmlCompilationResult.error], - }), - ); - } - - logger.debug("MJML template compiled"); - - const plainTextCompilationResult = this.htmlToTextCompiler.compile(mjmlCompilationResult.value); - - if (plainTextCompilationResult.isErr()) { - return err( - new EmailCompiler.CompilationFailedError("Failed to compile body to plain text", { - errors: [plainTextCompilationResult.error], - }), - ); - } - - logger.debug("Email body converted to plaintext"); - - return ok({ - text: plainTextCompilationResult.value, - html: mjmlCompilationResult.value, - from: `${senderName} <${senderEmail}>`, - to: recipientEmail, - subject: emailSubject, - }); + const subjectCompilationResult = this.resolveEmailSubject(subjectTemplate, payload); + const bodyCompilationInHtmlResult = this.resolveBodyTemplate(bodyTemplate, payload).andThen( + (value) => { + return this.resolveBodyMjml(value.template); + }, + ); + + return combine([subjectCompilationResult, bodyCompilationInHtmlResult]).andThen( + ([subjectCompiled, bodyCompiledHtml]) => { + return this.resolveBodyPlainText(bodyCompiledHtml).andThen((bodyPlainText) => { + logger.debug("Resolved compiled email template"); + + return ok({ + text: bodyPlainText, + html: bodyCompiledHtml, + from: `${senderName} <${senderEmail}>`, + to: recipientEmail, + subject: subjectCompiled.template, + }); + }); + }, + ); } } From d3a55dfd15ce10903493534661905d3008cdae66 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Mon, 13 May 2024 21:17:33 +0200 Subject: [PATCH 11/14] cleanup --- .../modules/event-handlers/send-event-messages.use-case.ts | 4 ++-- apps/smtp/src/modules/smtp/services/mjml-compiler.ts | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts index 66dc7f098..df85a4d42 100644 --- a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts +++ b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts @@ -22,8 +22,8 @@ export class SendEventMessagesUseCase { recipientEmail, channelSlug, }: { - channelSlug: string; // todo: id or slug - payload: any; // todo can be narrowed? + channelSlug: string; + payload: unknown; recipientEmail: string; event: MessageEventTypes; }) { diff --git a/apps/smtp/src/modules/smtp/services/mjml-compiler.ts b/apps/smtp/src/modules/smtp/services/mjml-compiler.ts index fdb7bcd7e..9bbcf1a3f 100644 --- a/apps/smtp/src/modules/smtp/services/mjml-compiler.ts +++ b/apps/smtp/src/modules/smtp/services/mjml-compiler.ts @@ -7,9 +7,6 @@ export interface IMjmlCompiler { compile(mjml: string): Result>; } -/** - * todo test - */ export class MjmlCompiler implements IMjmlCompiler { static MjmlCompilerError = BaseError.subclass("MjmlCompilerError"); static FailedToCompileError = this.MjmlCompilerError.subclass("FailedToCompileError"); From 48391cb70b4221b62df7897288a10f27b4ec5015 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Mon, 13 May 2024 21:24:41 +0200 Subject: [PATCH 12/14] factory for use case --- .../send-event-messages.use-case.factory.ts | 37 +++++++++++++++++++ .../src/pages/api/webhooks/gift-card-sent.ts | 35 ++---------------- .../src/pages/api/webhooks/invoice-sent.ts | 34 ++--------------- apps/smtp/src/pages/api/webhooks/notify.ts | 35 ++---------------- .../src/pages/api/webhooks/order-cancelled.ts | 34 ++--------------- .../src/pages/api/webhooks/order-confirmed.ts | 34 ++--------------- .../src/pages/api/webhooks/order-created.ts | 34 ++--------------- .../src/pages/api/webhooks/order-fulfilled.ts | 36 +++--------------- .../pages/api/webhooks/order-fully-paid.ts | 36 +++--------------- .../src/pages/api/webhooks/order-refunded.ts | 34 ++--------------- 10 files changed, 75 insertions(+), 274 deletions(-) create mode 100644 apps/smtp/src/modules/event-handlers/send-event-messages.use-case.factory.ts diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.factory.ts b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.factory.ts new file mode 100644 index 000000000..9899da8e6 --- /dev/null +++ b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.factory.ts @@ -0,0 +1,37 @@ +import { AuthData } from "@saleor/app-sdk/APL"; +import { SendEventMessagesUseCase } from "./send-event-messages.use-case"; +import { SmtpEmailSender } from "../smtp/services/smtp-email-sender"; +import { EmailCompiler } from "../smtp/services/email-compiler"; +import { HandlebarsTemplateCompiler } from "../smtp/services/handlebars-template-compiler"; +import { HtmlToTextCompiler } from "../smtp/services/html-to-text-compiler"; +import { MjmlCompiler } from "../smtp/services/mjml-compiler"; +import { SmtpConfigurationService } from "../smtp/configuration/smtp-configuration.service"; +import { FeatureFlagService } from "../feature-flag-service/feature-flag-service"; +import { SmtpMetadataManager } from "../smtp/configuration/smtp-metadata-manager"; +import { createSettingsManager } from "../../lib/metadata-manager"; +import { createInstrumentedGraphqlClient } from "../../lib/create-instrumented-graphql-client"; + +export class SendEventMessagesUseCaseFactory { + createFromAuthData(authData: AuthData) { + const client = createInstrumentedGraphqlClient({ + saleorApiUrl: authData.saleorApiUrl, + token: authData.token, + }); + + return new SendEventMessagesUseCase({ + emailSender: new SmtpEmailSender(), + emailCompiler: new EmailCompiler( + new HandlebarsTemplateCompiler(), + new HtmlToTextCompiler(), + new MjmlCompiler(), + ), + smtpConfigurationService: new SmtpConfigurationService({ + featureFlagService: new FeatureFlagService({ client }), + metadataManager: new SmtpMetadataManager( + createSettingsManager(client, authData.appId), + authData.saleorApiUrl, + ), + }), + }); + } +} diff --git a/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts b/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts index 698747f72..70cd0ac57 100644 --- a/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts +++ b/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts @@ -2,19 +2,9 @@ import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handl import { gql } from "urql"; import { saleorApp } from "../../../saleor-app"; import { GiftCardSentWebhookPayloadFragment } from "../../../../generated/graphql"; -import { SendEventMessagesUseCase } from "../../../modules/event-handlers/send-event-messages.use-case"; import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; -import { createInstrumentedGraphqlClient } from "../../../lib/create-instrumented-graphql-client"; -import { SmtpConfigurationService } from "../../../modules/smtp/configuration/smtp-configuration.service"; -import { FeatureFlagService } from "../../../modules/feature-flag-service/feature-flag-service"; -import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-metadata-manager"; -import { createSettingsManager } from "../../../lib/metadata-manager"; -import { SmtpEmailSender } from "../../../modules/smtp/services/smtp-email-sender"; -import { EmailCompiler } from "../../../modules/smtp/services/email-compiler"; -import { HandlebarsTemplateCompiler } from "../../../modules/smtp/services/handlebars-template-compiler"; -import { HtmlToTextCompiler } from "../../../modules/smtp/services/html-to-text-compiler"; -import { MjmlCompiler } from "../../../modules/smtp/services/mjml-compiler"; +import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/send-event-messages.use-case.factory"; const GiftCardSentWebhookPayload = gql` fragment GiftCardSentWebhookPayload on GiftCardSent { @@ -78,6 +68,8 @@ export const giftCardSentWebhook = new SaleorAsyncWebhook = async ( req, res, @@ -109,26 +101,7 @@ const handler: NextWebhookApiHandler = async return res.status(200).end(); } - const client = createInstrumentedGraphqlClient({ - saleorApiUrl: authData.saleorApiUrl, - token: authData.token, - }); - - const useCase = new SendEventMessagesUseCase({ - emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler( - new HandlebarsTemplateCompiler(), - new HtmlToTextCompiler(), - new MjmlCompiler(), - ), - smtpConfigurationService: new SmtpConfigurationService({ - featureFlagService: new FeatureFlagService({ client }), - metadataManager: new SmtpMetadataManager( - createSettingsManager(client, authData.appId), - authData.saleorApiUrl, - ), - }), - }); + const useCase = useCaseFactory.createFromAuthData(authData); await useCase.sendEventMessages({ channelSlug: channel, diff --git a/apps/smtp/src/pages/api/webhooks/invoice-sent.ts b/apps/smtp/src/pages/api/webhooks/invoice-sent.ts index e43fc2e12..514aafbe3 100644 --- a/apps/smtp/src/pages/api/webhooks/invoice-sent.ts +++ b/apps/smtp/src/pages/api/webhooks/invoice-sent.ts @@ -5,19 +5,9 @@ import { InvoiceSentWebhookPayloadFragment, OrderDetailsFragmentDoc, } from "../../../../generated/graphql"; -import { SendEventMessagesUseCase } from "../../../modules/event-handlers/send-event-messages.use-case"; import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; -import { createInstrumentedGraphqlClient } from "../../../lib/create-instrumented-graphql-client"; -import { SmtpConfigurationService } from "../../../modules/smtp/configuration/smtp-configuration.service"; -import { FeatureFlagService } from "../../../modules/feature-flag-service/feature-flag-service"; -import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-metadata-manager"; -import { createSettingsManager } from "../../../lib/metadata-manager"; -import { SmtpEmailSender } from "../../../modules/smtp/services/smtp-email-sender"; -import { EmailCompiler } from "../../../modules/smtp/services/email-compiler"; -import { HandlebarsTemplateCompiler } from "../../../modules/smtp/services/handlebars-template-compiler"; -import { HtmlToTextCompiler } from "../../../modules/smtp/services/html-to-text-compiler"; -import { MjmlCompiler } from "../../../modules/smtp/services/mjml-compiler"; +import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/send-event-messages.use-case.factory"; const InvoiceSentWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -64,6 +54,8 @@ export const invoiceSentWebhook = new SaleorAsyncWebhook = async ( req, res, @@ -89,26 +81,8 @@ const handler: NextWebhookApiHandler = async } const channel = order.channel.slug; - const client = createInstrumentedGraphqlClient({ - saleorApiUrl: authData.saleorApiUrl, - token: authData.token, - }); - const useCase = new SendEventMessagesUseCase({ - emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler( - new HandlebarsTemplateCompiler(), - new HtmlToTextCompiler(), - new MjmlCompiler(), - ), - smtpConfigurationService: new SmtpConfigurationService({ - featureFlagService: new FeatureFlagService({ client }), - metadataManager: new SmtpMetadataManager( - createSettingsManager(client, authData.appId), - authData.saleorApiUrl, - ), - }), - }); + const useCase = useCaseFactory.createFromAuthData(authData); await useCase.sendEventMessages({ channelSlug: channel, diff --git a/apps/smtp/src/pages/api/webhooks/notify.ts b/apps/smtp/src/pages/api/webhooks/notify.ts index 6d69909e1..c2aeab886 100644 --- a/apps/smtp/src/pages/api/webhooks/notify.ts +++ b/apps/smtp/src/pages/api/webhooks/notify.ts @@ -1,19 +1,9 @@ import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; import { saleorApp } from "../../../saleor-app"; -import { SendEventMessagesUseCase } from "../../../modules/event-handlers/send-event-messages.use-case"; import { notifyEventMapping, NotifySubscriptionPayload } from "../../../lib/notify-event-types"; import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; -import { createInstrumentedGraphqlClient } from "../../../lib/create-instrumented-graphql-client"; -import { SmtpConfigurationService } from "../../../modules/smtp/configuration/smtp-configuration.service"; -import { FeatureFlagService } from "../../../modules/feature-flag-service/feature-flag-service"; -import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-metadata-manager"; -import { createSettingsManager } from "../../../lib/metadata-manager"; -import { SmtpEmailSender } from "../../../modules/smtp/services/smtp-email-sender"; -import { EmailCompiler } from "../../../modules/smtp/services/email-compiler"; -import { HandlebarsTemplateCompiler } from "../../../modules/smtp/services/handlebars-template-compiler"; -import { HtmlToTextCompiler } from "../../../modules/smtp/services/html-to-text-compiler"; -import { MjmlCompiler } from "../../../modules/smtp/services/mjml-compiler"; +import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/send-event-messages.use-case.factory"; /* * The Notify webhook is triggered on multiple Saleor events. @@ -30,6 +20,8 @@ export const notifyWebhook = new SaleorAsyncWebhook({ const logger = createLogger(notifyWebhook.webhookPath); +const useCaseFactory = new SendEventMessagesUseCaseFactory(); + const handler: NextWebhookApiHandler = async (req, res, context) => { logger.debug("Webhook received"); @@ -54,26 +46,7 @@ const handler: NextWebhookApiHandler = async (req, re return res.status(200).json({ message: `${payload.notify_event} event is not supported.` }); } - const client = createInstrumentedGraphqlClient({ - saleorApiUrl: authData.saleorApiUrl, - token: authData.token, - }); - - const useCase = new SendEventMessagesUseCase({ - emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler( - new HandlebarsTemplateCompiler(), - new HtmlToTextCompiler(), - new MjmlCompiler(), - ), - smtpConfigurationService: new SmtpConfigurationService({ - featureFlagService: new FeatureFlagService({ client }), - metadataManager: new SmtpMetadataManager( - createSettingsManager(client, authData.appId), - authData.saleorApiUrl, - ), - }), - }); + const useCase = useCaseFactory.createFromAuthData(authData); await useCase.sendEventMessages({ channelSlug: channel, diff --git a/apps/smtp/src/pages/api/webhooks/order-cancelled.ts b/apps/smtp/src/pages/api/webhooks/order-cancelled.ts index a5b1d6164..86bb24efa 100644 --- a/apps/smtp/src/pages/api/webhooks/order-cancelled.ts +++ b/apps/smtp/src/pages/api/webhooks/order-cancelled.ts @@ -5,19 +5,9 @@ import { OrderCancelledWebhookPayloadFragment, OrderDetailsFragmentDoc, } from "../../../../generated/graphql"; -import { SendEventMessagesUseCase } from "../../../modules/event-handlers/send-event-messages.use-case"; import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; -import { createInstrumentedGraphqlClient } from "../../../lib/create-instrumented-graphql-client"; -import { SmtpConfigurationService } from "../../../modules/smtp/configuration/smtp-configuration.service"; -import { FeatureFlagService } from "../../../modules/feature-flag-service/feature-flag-service"; -import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-metadata-manager"; -import { createSettingsManager } from "../../../lib/metadata-manager"; -import { SmtpEmailSender } from "../../../modules/smtp/services/smtp-email-sender"; -import { EmailCompiler } from "../../../modules/smtp/services/email-compiler"; -import { HandlebarsTemplateCompiler } from "../../../modules/smtp/services/handlebars-template-compiler"; -import { HtmlToTextCompiler } from "../../../modules/smtp/services/html-to-text-compiler"; -import { MjmlCompiler } from "../../../modules/smtp/services/mjml-compiler"; +import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/send-event-messages.use-case.factory"; const OrderCancelledWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -47,6 +37,8 @@ export const orderCancelledWebhook = new SaleorAsyncWebhook = async ( req, res, @@ -72,26 +64,8 @@ const handler: NextWebhookApiHandler = asy } const channel = order.channel.slug; - const client = createInstrumentedGraphqlClient({ - saleorApiUrl: authData.saleorApiUrl, - token: authData.token, - }); - const useCase = new SendEventMessagesUseCase({ - emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler( - new HandlebarsTemplateCompiler(), - new HtmlToTextCompiler(), - new MjmlCompiler(), - ), - smtpConfigurationService: new SmtpConfigurationService({ - featureFlagService: new FeatureFlagService({ client }), - metadataManager: new SmtpMetadataManager( - createSettingsManager(client, authData.appId), - authData.saleorApiUrl, - ), - }), - }); + const useCase = useCaseFactory.createFromAuthData(authData); await useCase.sendEventMessages({ channelSlug: channel, diff --git a/apps/smtp/src/pages/api/webhooks/order-confirmed.ts b/apps/smtp/src/pages/api/webhooks/order-confirmed.ts index e8cde0dfe..7de0973b8 100644 --- a/apps/smtp/src/pages/api/webhooks/order-confirmed.ts +++ b/apps/smtp/src/pages/api/webhooks/order-confirmed.ts @@ -5,19 +5,9 @@ import { OrderConfirmedWebhookPayloadFragment, OrderDetailsFragmentDoc, } from "../../../../generated/graphql"; -import { SendEventMessagesUseCase } from "../../../modules/event-handlers/send-event-messages.use-case"; import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; -import { createInstrumentedGraphqlClient } from "../../../lib/create-instrumented-graphql-client"; -import { SmtpConfigurationService } from "../../../modules/smtp/configuration/smtp-configuration.service"; -import { FeatureFlagService } from "../../../modules/feature-flag-service/feature-flag-service"; -import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-metadata-manager"; -import { createSettingsManager } from "../../../lib/metadata-manager"; -import { SmtpEmailSender } from "../../../modules/smtp/services/smtp-email-sender"; -import { EmailCompiler } from "../../../modules/smtp/services/email-compiler"; -import { HandlebarsTemplateCompiler } from "../../../modules/smtp/services/handlebars-template-compiler"; -import { HtmlToTextCompiler } from "../../../modules/smtp/services/html-to-text-compiler"; -import { MjmlCompiler } from "../../../modules/smtp/services/mjml-compiler"; +import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/send-event-messages.use-case.factory"; const OrderConfirmedWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -48,6 +38,8 @@ export const orderConfirmedWebhook = new SaleorAsyncWebhook = async ( req, res, @@ -73,26 +65,8 @@ const handler: NextWebhookApiHandler = asy } const channel = order.channel.slug; - const client = createInstrumentedGraphqlClient({ - saleorApiUrl: authData.saleorApiUrl, - token: authData.token, - }); - const useCase = new SendEventMessagesUseCase({ - emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler( - new HandlebarsTemplateCompiler(), - new HtmlToTextCompiler(), - new MjmlCompiler(), - ), - smtpConfigurationService: new SmtpConfigurationService({ - featureFlagService: new FeatureFlagService({ client }), - metadataManager: new SmtpMetadataManager( - createSettingsManager(client, authData.appId), - authData.saleorApiUrl, - ), - }), - }); + const useCase = useCaseFactory.createFromAuthData(authData); await useCase.sendEventMessages({ channelSlug: channel, diff --git a/apps/smtp/src/pages/api/webhooks/order-created.ts b/apps/smtp/src/pages/api/webhooks/order-created.ts index 879e0a44b..6e6464c3e 100644 --- a/apps/smtp/src/pages/api/webhooks/order-created.ts +++ b/apps/smtp/src/pages/api/webhooks/order-created.ts @@ -3,19 +3,9 @@ import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handl import { gql } from "urql"; import { saleorApp } from "../../../saleor-app"; import { OrderCreatedWebhookPayloadFragment } from "../../../../generated/graphql"; -import { SendEventMessagesUseCase } from "../../../modules/event-handlers/send-event-messages.use-case"; import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; -import { createInstrumentedGraphqlClient } from "../../../lib/create-instrumented-graphql-client"; -import { SmtpConfigurationService } from "../../../modules/smtp/configuration/smtp-configuration.service"; -import { FeatureFlagService } from "../../../modules/feature-flag-service/feature-flag-service"; -import { createSettingsManager } from "../../../lib/metadata-manager"; -import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-metadata-manager"; -import { SmtpEmailSender } from "../../../modules/smtp/services/smtp-email-sender"; -import { EmailCompiler } from "../../../modules/smtp/services/email-compiler"; -import { HandlebarsTemplateCompiler } from "../../../modules/smtp/services/handlebars-template-compiler"; -import { HtmlToTextCompiler } from "../../../modules/smtp/services/html-to-text-compiler"; -import { MjmlCompiler } from "../../../modules/smtp/services/mjml-compiler"; +import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/send-event-messages.use-case.factory"; const OrderCreatedWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -45,6 +35,8 @@ export const orderCreatedWebhook = new SaleorAsyncWebhook = async ( req, res, @@ -70,26 +62,8 @@ const handler: NextWebhookApiHandler = async } const channel = order.channel.slug; - const client = createInstrumentedGraphqlClient({ - saleorApiUrl: authData.saleorApiUrl, - token: authData.token, - }); - const useCase = new SendEventMessagesUseCase({ - emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler( - new HandlebarsTemplateCompiler(), - new HtmlToTextCompiler(), - new MjmlCompiler(), - ), - smtpConfigurationService: new SmtpConfigurationService({ - featureFlagService: new FeatureFlagService({ client }), - metadataManager: new SmtpMetadataManager( - createSettingsManager(client, authData.appId), - authData.saleorApiUrl, - ), - }), - }); + const useCase = useCaseFactory.createFromAuthData(authData); await useCase.sendEventMessages({ channelSlug: channel, diff --git a/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts b/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts index 31b9f7e7a..86247a028 100644 --- a/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts +++ b/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts @@ -5,19 +5,9 @@ import { OrderDetailsFragmentDoc, OrderFulfilledWebhookPayloadFragment, } from "../../../../generated/graphql"; -import { SendEventMessagesUseCase } from "../../../modules/event-handlers/send-event-messages.use-case"; import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; -import { createInstrumentedGraphqlClient } from "../../../lib/create-instrumented-graphql-client"; -import { SmtpConfigurationService } from "../../../modules/smtp/configuration/smtp-configuration.service"; -import { FeatureFlagService } from "../../../modules/feature-flag-service/feature-flag-service"; -import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-metadata-manager"; -import { createSettingsManager } from "../../../lib/metadata-manager"; -import { SmtpEmailSender } from "../../../modules/smtp/services/smtp-email-sender"; -import { EmailCompiler } from "../../../modules/smtp/services/email-compiler"; -import { HandlebarsTemplateCompiler } from "../../../modules/smtp/services/handlebars-template-compiler"; -import { HtmlToTextCompiler } from "../../../modules/smtp/services/html-to-text-compiler"; -import { MjmlCompiler } from "../../../modules/smtp/services/mjml-compiler"; +import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/send-event-messages.use-case.factory"; const OrderFulfilledWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -43,11 +33,13 @@ export const orderFulfilledWebhook = new SaleorAsyncWebhook = async ( req, res, @@ -73,26 +65,8 @@ const handler: NextWebhookApiHandler = asy } const channel = order.channel.slug; - const client = createInstrumentedGraphqlClient({ - saleorApiUrl: authData.saleorApiUrl, - token: authData.token, - }); - const useCase = new SendEventMessagesUseCase({ - emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler( - new HandlebarsTemplateCompiler(), - new HtmlToTextCompiler(), - new MjmlCompiler(), - ), - smtpConfigurationService: new SmtpConfigurationService({ - featureFlagService: new FeatureFlagService({ client }), - metadataManager: new SmtpMetadataManager( - createSettingsManager(client, authData.appId), - authData.saleorApiUrl, - ), - }), - }); + const useCase = useCaseFactory.createFromAuthData(authData); await useCase.sendEventMessages({ channelSlug: channel, diff --git a/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts b/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts index 29c07871c..551d62cf2 100644 --- a/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts +++ b/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts @@ -5,19 +5,9 @@ import { OrderDetailsFragmentDoc, OrderFullyPaidWebhookPayloadFragment, } from "../../../../generated/graphql"; -import { SendEventMessagesUseCase } from "../../../modules/event-handlers/send-event-messages.use-case"; import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; -import { createInstrumentedGraphqlClient } from "../../../lib/create-instrumented-graphql-client"; -import { SmtpConfigurationService } from "../../../modules/smtp/configuration/smtp-configuration.service"; -import { FeatureFlagService } from "../../../modules/feature-flag-service/feature-flag-service"; -import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-metadata-manager"; -import { createSettingsManager } from "../../../lib/metadata-manager"; -import { SmtpEmailSender } from "../../../modules/smtp/services/smtp-email-sender"; -import { EmailCompiler } from "../../../modules/smtp/services/email-compiler"; -import { HandlebarsTemplateCompiler } from "../../../modules/smtp/services/handlebars-template-compiler"; -import { HtmlToTextCompiler } from "../../../modules/smtp/services/html-to-text-compiler"; -import { MjmlCompiler } from "../../../modules/smtp/services/mjml-compiler"; +import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/send-event-messages.use-case.factory"; const OrderFullyPaidWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -43,11 +33,13 @@ export const orderFullyPaidWebhook = new SaleorAsyncWebhook = async ( req, res, @@ -73,26 +65,8 @@ const handler: NextWebhookApiHandler = asy } const channel = order.channel.slug; - const client = createInstrumentedGraphqlClient({ - saleorApiUrl: authData.saleorApiUrl, - token: authData.token, - }); - const useCase = new SendEventMessagesUseCase({ - emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler( - new HandlebarsTemplateCompiler(), - new HtmlToTextCompiler(), - new MjmlCompiler(), - ), - smtpConfigurationService: new SmtpConfigurationService({ - featureFlagService: new FeatureFlagService({ client }), - metadataManager: new SmtpMetadataManager( - createSettingsManager(client, authData.appId), - authData.saleorApiUrl, - ), - }), - }); + const useCase = useCaseFactory.createFromAuthData(authData); await useCase.sendEventMessages({ channelSlug: channel, diff --git a/apps/smtp/src/pages/api/webhooks/order-refunded.ts b/apps/smtp/src/pages/api/webhooks/order-refunded.ts index cdc7d0f4e..44eac1357 100644 --- a/apps/smtp/src/pages/api/webhooks/order-refunded.ts +++ b/apps/smtp/src/pages/api/webhooks/order-refunded.ts @@ -5,19 +5,9 @@ import { import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; import { gql } from "urql"; import { saleorApp } from "../../../saleor-app"; -import { SendEventMessagesUseCase } from "../../../modules/event-handlers/send-event-messages.use-case"; import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; -import { createInstrumentedGraphqlClient } from "../../../lib/create-instrumented-graphql-client"; -import { SmtpConfigurationService } from "../../../modules/smtp/configuration/smtp-configuration.service"; -import { FeatureFlagService } from "../../../modules/feature-flag-service/feature-flag-service"; -import { SmtpMetadataManager } from "../../../modules/smtp/configuration/smtp-metadata-manager"; -import { createSettingsManager } from "../../../lib/metadata-manager"; -import { SmtpEmailSender } from "../../../modules/smtp/services/smtp-email-sender"; -import { EmailCompiler } from "../../../modules/smtp/services/email-compiler"; -import { HandlebarsTemplateCompiler } from "../../../modules/smtp/services/handlebars-template-compiler"; -import { HtmlToTextCompiler } from "../../../modules/smtp/services/html-to-text-compiler"; -import { MjmlCompiler } from "../../../modules/smtp/services/mjml-compiler"; +import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/send-event-messages.use-case.factory"; const OrderRefundedWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -47,6 +37,8 @@ export const orderRefundedWebhook = new SaleorAsyncWebhook = async ( req, res, @@ -72,26 +64,8 @@ const handler: NextWebhookApiHandler = asyn } const channel = order.channel.slug; - const client = createInstrumentedGraphqlClient({ - saleorApiUrl: authData.saleorApiUrl, - token: authData.token, - }); - const useCase = new SendEventMessagesUseCase({ - emailSender: new SmtpEmailSender(), - emailCompiler: new EmailCompiler( - new HandlebarsTemplateCompiler(), - new HtmlToTextCompiler(), - new MjmlCompiler(), - ), - smtpConfigurationService: new SmtpConfigurationService({ - featureFlagService: new FeatureFlagService({ client }), - metadataManager: new SmtpMetadataManager( - createSettingsManager(client, authData.appId), - authData.saleorApiUrl, - ), - }), - }); + const useCase = useCaseFactory.createFromAuthData(authData); await useCase.sendEventMessages({ channelSlug: channel, From 6caf81e2800639d36a8bf2863d7bfe5e48cfa4ba Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Mon, 13 May 2024 21:39:24 +0200 Subject: [PATCH 13/14] test scenarios for use case --- .../send-event-messages.use-case.factory.ts | 2 +- .../send-event-messages.use-case.test.ts | 39 +++++++++++++++++++ .../send-event-messages.use-case.ts | 5 ++- .../smtp-configuration.service.ts | 2 +- .../configuration/smtp-metadata-manager.ts | 2 +- .../smtp/services/email-compiler.test.ts | 6 +-- .../handlebars-template-compiler.test.ts | 2 +- .../services/html-to-text-compiler.test.ts | 2 +- .../smtp/services/mjml-compiler.test.ts | 2 +- 9 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.factory.ts b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.factory.ts index 9899da8e6..a608b1a90 100644 --- a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.factory.ts +++ b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.factory.ts @@ -12,7 +12,7 @@ import { createSettingsManager } from "../../lib/metadata-manager"; import { createInstrumentedGraphqlClient } from "../../lib/create-instrumented-graphql-client"; export class SendEventMessagesUseCaseFactory { - createFromAuthData(authData: AuthData) { + createFromAuthData(authData: AuthData): SendEventMessagesUseCase { const client = createInstrumentedGraphqlClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token, diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts new file mode 100644 index 000000000..2cdbf287a --- /dev/null +++ b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import { SendEventMessagesUseCase } from "./send-event-messages.use-case"; +import { SendEventMessagesUseCaseFactory } from "./send-event-messages.use-case.factory"; + +describe("SendEventMessagesUseCase", () => { + describe("Factory", () => { + it("Build with default dependencies from AuthData", () => { + const instance = new SendEventMessagesUseCaseFactory().createFromAuthData({ + saleorApiUrl: "https://demo.saleor.cloud/graphql/", + token: "foo", + appId: "1", + }); + + expect(instance).toBeDefined(); + }); + }); + + describe("sendEventMessages method", () => { + it.todo("Returns error when no active configurations are available for selected channel"); + + describe("Multiple configurations assigned for the same event", () => { + it.todo("Calls SMTP service to send email for each configuration"); + }); + + describe("Single configuration assigned for the event", () => { + it.todo("Does nothing (?) if config is missing for this event"); + + it.todo("Does nothing (?) if event is set to not active"); + + it.todo("Does nothing (?) if configuration sender name is missing"); + + it.todo("Does nothing (?) if configuration sender email is missing"); + + it.todo("Does nothing (?) if email compilation fails"); + + it.todo("Calls SMTP service to send email"); + }); + }); +}); diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts index df85a4d42..57cdbd440 100644 --- a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts +++ b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts @@ -4,7 +4,10 @@ import { MessageEventTypes } from "./message-event-types"; import { createLogger } from "../../logger"; import { ISMTPEmailSender, SendMailArgs } from "../smtp/services/smtp-email-sender"; -// todo test +/* + * todo test + * todo: how this service should handle error for one config and success for another? + */ export class SendEventMessagesUseCase { private logger = createLogger("SendEventMessagesUseCase"); diff --git a/apps/smtp/src/modules/smtp/configuration/smtp-configuration.service.ts b/apps/smtp/src/modules/smtp/configuration/smtp-configuration.service.ts index 3e035f1fd..907f4641d 100644 --- a/apps/smtp/src/modules/smtp/configuration/smtp-configuration.service.ts +++ b/apps/smtp/src/modules/smtp/configuration/smtp-configuration.service.ts @@ -7,7 +7,7 @@ import { filterConfigurations } from "../../app-configuration/filter-configurati import { FeatureFlagService } from "../../feature-flag-service/feature-flag-service"; import { createLogger } from "../../../logger"; import { BaseError } from "../../../errors"; -import { err, errAsync, fromAsyncThrowable, ok, okAsync, Result, ResultAsync } from "neverthrow"; +import { err, errAsync, fromAsyncThrowable, ok, okAsync, ResultAsync } from "neverthrow"; const logger = createLogger("SmtpConfigurationService"); diff --git a/apps/smtp/src/modules/smtp/configuration/smtp-metadata-manager.ts b/apps/smtp/src/modules/smtp/configuration/smtp-metadata-manager.ts index 6dc20773a..9c9aa41f9 100644 --- a/apps/smtp/src/modules/smtp/configuration/smtp-metadata-manager.ts +++ b/apps/smtp/src/modules/smtp/configuration/smtp-metadata-manager.ts @@ -1,6 +1,6 @@ import { SettingsManager } from "@saleor/app-sdk/settings-manager"; import { SmtpConfig } from "./smtp-config-schema"; -import { fromAsyncThrowable, fromThrowable, ok, okAsync, Result, ResultAsync } from "neverthrow"; +import { fromAsyncThrowable, fromThrowable, ok, ResultAsync } from "neverthrow"; import { BaseError } from "../../../errors"; export class SmtpMetadataManager { diff --git a/apps/smtp/src/modules/smtp/services/email-compiler.test.ts b/apps/smtp/src/modules/smtp/services/email-compiler.test.ts index 00063612f..228d98e1d 100644 --- a/apps/smtp/src/modules/smtp/services/email-compiler.test.ts +++ b/apps/smtp/src/modules/smtp/services/email-compiler.test.ts @@ -1,7 +1,7 @@ -import { describe, it, expect, beforeEach, vi, Mock } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { EmailCompiler } from "./email-compiler"; -import { HandlebarsTemplateCompiler, ITemplateCompiler } from "./handlebars-template-compiler"; -import { err, ok, Result } from "neverthrow"; +import { HandlebarsTemplateCompiler } from "./handlebars-template-compiler"; +import { err, Result } from "neverthrow"; import { HtmlToTextCompiler } from "./html-to-text-compiler"; import { MjmlCompiler } from "./mjml-compiler"; diff --git a/apps/smtp/src/modules/smtp/services/handlebars-template-compiler.test.ts b/apps/smtp/src/modules/smtp/services/handlebars-template-compiler.test.ts index 86bc4d80f..460b0f12f 100644 --- a/apps/smtp/src/modules/smtp/services/handlebars-template-compiler.test.ts +++ b/apps/smtp/src/modules/smtp/services/handlebars-template-compiler.test.ts @@ -1,4 +1,4 @@ -import { describe, it, vi, expect } from "vitest"; +import { describe, expect, it } from "vitest"; import { HandlebarsTemplateCompiler } from "./handlebars-template-compiler"; describe("HandlebarsTemplateCompiler", () => { diff --git a/apps/smtp/src/modules/smtp/services/html-to-text-compiler.test.ts b/apps/smtp/src/modules/smtp/services/html-to-text-compiler.test.ts index c832de307..5d6d3d82c 100644 --- a/apps/smtp/src/modules/smtp/services/html-to-text-compiler.test.ts +++ b/apps/smtp/src/modules/smtp/services/html-to-text-compiler.test.ts @@ -1,4 +1,4 @@ -import { it, vi, expect, describe } from "vitest"; +import { describe, expect, it } from "vitest"; import { HtmlToTextCompiler } from "./html-to-text-compiler"; describe("HtmlToTextCompiler", () => { diff --git a/apps/smtp/src/modules/smtp/services/mjml-compiler.test.ts b/apps/smtp/src/modules/smtp/services/mjml-compiler.test.ts index 1c9778679..720a4cc14 100644 --- a/apps/smtp/src/modules/smtp/services/mjml-compiler.test.ts +++ b/apps/smtp/src/modules/smtp/services/mjml-compiler.test.ts @@ -1,4 +1,4 @@ -import { it, expect, vi, describe } from "vitest"; +import { describe, expect, it } from "vitest"; import { MjmlCompiler } from "./mjml-compiler"; describe("MjmlCompiler", () => { From f17a2cbaee4a6e1c5e1f9a1f0e31f0462e768e74 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Mon, 13 May 2024 21:44:46 +0200 Subject: [PATCH 14/14] Apply shared packages --- apps/smtp/src/lib/is-in-iframe.ts | 7 ------- apps/smtp/src/lib/no-ssr-wrapper.tsx | 19 ------------------- apps/smtp/src/pages/_app.tsx | 2 +- apps/smtp/src/pages/index.tsx | 2 +- apps/smtp/src/public/sendgrid.png | Bin 332 -> 0 bytes 5 files changed, 2 insertions(+), 28 deletions(-) delete mode 100644 apps/smtp/src/lib/is-in-iframe.ts delete mode 100644 apps/smtp/src/lib/no-ssr-wrapper.tsx delete mode 100644 apps/smtp/src/public/sendgrid.png diff --git a/apps/smtp/src/lib/is-in-iframe.ts b/apps/smtp/src/lib/is-in-iframe.ts deleted file mode 100644 index e1f481c92..000000000 --- a/apps/smtp/src/lib/is-in-iframe.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function isInIframe() { - try { - return window.self !== window.top; - } catch (e) { - return true; - } -} diff --git a/apps/smtp/src/lib/no-ssr-wrapper.tsx b/apps/smtp/src/lib/no-ssr-wrapper.tsx deleted file mode 100644 index f1954399c..000000000 --- a/apps/smtp/src/lib/no-ssr-wrapper.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React, { PropsWithChildren } from "react"; -import dynamic from "next/dynamic"; - -const Wrapper = (props: PropsWithChildren<{}>) => {props.children}; - -/** - * Saleor App can be rendered only as a Saleor Dashboard iframe. - * All content is rendered after Dashboard exchanges auth with the app. - * Hence, there is no reason to render app server side. - * - * This component forces app to work in SPA-mode. It simplifies browser-only code and reduces need - * of using dynamic() calls - * - * You can use this wrapper selectively for some pages or remove it completely. - * It doesn't affect Saleor communication, but may cause problems with some client-only code. - */ -export const NoSSRWrapper = dynamic(() => Promise.resolve(Wrapper), { - ssr: false, -}); diff --git a/apps/smtp/src/pages/_app.tsx b/apps/smtp/src/pages/_app.tsx index 7d501905a..5b847e632 100644 --- a/apps/smtp/src/pages/_app.tsx +++ b/apps/smtp/src/pages/_app.tsx @@ -8,7 +8,7 @@ import { AppProps } from "next/app"; import { ThemeProvider } from "@saleor/macaw-ui"; import { ThemeSynchronizer } from "../lib/theme-synchronizer"; -import { NoSSRWrapper } from "../lib/no-ssr-wrapper"; +import { NoSSRWrapper } from "@saleor/apps-shared"; import { trpcClient } from "../modules/trpc/trpc-client"; /** diff --git a/apps/smtp/src/pages/index.tsx b/apps/smtp/src/pages/index.tsx index fd0d93dc1..4134d5ef0 100644 --- a/apps/smtp/src/pages/index.tsx +++ b/apps/smtp/src/pages/index.tsx @@ -3,7 +3,7 @@ import { useAppBridge } from "@saleor/app-sdk/app-bridge"; import { useEffect } from "react"; import { useIsMounted } from "usehooks-ts"; import { useRouter } from "next/router"; -import { isInIframe } from "../lib/is-in-iframe"; +import { isInIframe } from "@saleor/apps-shared"; import { appName } from "../const"; import { appUrls } from "../modules/app-configuration/urls"; diff --git a/apps/smtp/src/public/sendgrid.png b/apps/smtp/src/public/sendgrid.png deleted file mode 100644 index ede13eb1cec8e92e0aeed674210454a1ec70180d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 332 zcmeAS@N?(olHy`uVBq!ia0vp^3P3E+!3HGT!p`>qDb50q$YKTtZeb8+WSBKa0w~B> z9OUlAu|M+GDqhz zp)F+#CcZEYOJpxg;8hV@AZ)wT{c)i1`5n`Dwnv>)S*BFDujpLO(5i!ynFR6Wzot^uRpZBKDV?;`?I{pZ)Ov!m@R) zu$$WVK7MANW8yP1LV{OJ{kcwaY1`UG*IQVD${m