chore: Email + mailer refactor (#3342)

* Huge email refactor

* fix: One rename too many

* comments
This commit is contained in:
Tom Moor
2022-04-07 16:50:04 -07:00
committed by GitHub
parent 15375bf199
commit 5c24f9e1d5
39 changed files with 879 additions and 870 deletions

View File

@@ -1,8 +1,9 @@
import fs from "fs";
import invariant from "invariant";
import ExportFailureEmail from "@server/emails/templates/ExportFailureEmail";
import ExportSuccessEmail from "@server/emails/templates/ExportSuccessEmail";
import Logger from "@server/logging/logger";
import { FileOperation, Collection, Event, Team, User } from "@server/models";
import EmailTask from "@server/queues/tasks/EmailTask";
import { Event as TEvent } from "@server/types";
import { uploadToS3FromBuffer } from "@server/utils/s3";
import { archiveCollections } from "@server/utils/zip";
@@ -88,21 +89,15 @@ export default class ExportsProcessor extends BaseProcessor {
});
if (state === "error") {
await EmailTask.schedule({
type: "exportFailure",
options: {
to: user.email,
teamUrl: team.url,
},
await ExportFailureEmail.schedule({
to: user.email,
teamUrl: team.url,
});
} else {
await EmailTask.schedule({
type: "exportSuccess",
options: {
to: user.email,
id: fileOperation.id,
teamUrl: team.url,
},
await ExportSuccessEmail.schedule({
to: user.email,
id: fileOperation.id,
teamUrl: team.url,
});
}
}

View File

@@ -1,5 +1,5 @@
import DocumentNotificationEmail from "@server/emails/templates/DocumentNotificationEmail";
import { View, NotificationSetting } from "@server/models";
import EmailTask from "@server/queues/tasks/EmailTask";
import {
buildDocument,
buildCollection,
@@ -8,7 +8,7 @@ import {
import { flushdb } from "@server/test/support";
import NotificationsProcessor from "./NotificationsProcessor";
jest.mock("@server/queues/tasks/EmailTask");
jest.mock("@server/emails/templates/DocumentNotificationEmail");
const ip = "127.0.0.1";
beforeEach(() => flushdb());
@@ -39,7 +39,7 @@ describe("documents.publish", () => {
},
ip,
});
expect(EmailTask.schedule).not.toHaveBeenCalled();
expect(DocumentNotificationEmail.schedule).not.toHaveBeenCalled();
});
test("should send a notification to other users in team", async () => {
@@ -65,7 +65,7 @@ describe("documents.publish", () => {
},
ip,
});
expect(EmailTask.schedule).toHaveBeenCalled();
expect(DocumentNotificationEmail.schedule).toHaveBeenCalled();
});
test("should not send a notification to users without collection access", async () => {
@@ -95,7 +95,7 @@ describe("documents.publish", () => {
},
ip,
});
expect(EmailTask.schedule).not.toHaveBeenCalled();
expect(DocumentNotificationEmail.schedule).not.toHaveBeenCalled();
});
});
@@ -119,7 +119,7 @@ describe("revisions.create", () => {
collectionId: document.collectionId,
teamId: document.teamId,
});
expect(EmailTask.schedule).toHaveBeenCalled();
expect(DocumentNotificationEmail.schedule).toHaveBeenCalled();
});
test("should not send a notification if viewed since update", async () => {
@@ -143,7 +143,7 @@ describe("revisions.create", () => {
collectionId: document.collectionId,
teamId: document.teamId,
});
expect(EmailTask.schedule).not.toHaveBeenCalled();
expect(DocumentNotificationEmail.schedule).not.toHaveBeenCalled();
});
test("should not send a notification to last editor", async () => {
@@ -164,6 +164,6 @@ describe("revisions.create", () => {
collectionId: document.collectionId,
teamId: document.teamId,
});
expect(EmailTask.schedule).not.toHaveBeenCalled();
expect(DocumentNotificationEmail.schedule).not.toHaveBeenCalled();
});
});

View File

@@ -1,4 +1,6 @@
import { Op } from "sequelize";
import CollectionNotificationEmail from "@server/emails/templates/CollectionNotificationEmail";
import DocumentNotificationEmail from "@server/emails/templates/DocumentNotificationEmail";
import Logger from "@server/logging/logger";
import { APM } from "@server/logging/tracing";
import {
@@ -15,7 +17,6 @@ import {
RevisionEvent,
Event,
} from "@server/types";
import EmailTask from "../tasks/EmailTask";
import BaseProcessor from "./BaseProcessor";
@APM.trace()
@@ -123,17 +124,14 @@ export default class NotificationsProcessor extends BaseProcessor {
continue;
}
await EmailTask.schedule({
type: "documentNotification",
options: {
to: setting.user.email,
eventName,
documentId: document.id,
teamUrl: team.url,
actorName: document.updatedBy.name,
collectionName: collection.name,
unsubscribeUrl: setting.unsubscribeUrl,
},
await DocumentNotificationEmail.schedule({
to: setting.user.email,
eventName,
documentId: document.id,
teamUrl: team.url,
actorName: document.updatedBy.name,
collectionName: collection.name,
unsubscribeUrl: setting.unsubscribeUrl,
});
}
}
@@ -177,14 +175,11 @@ export default class NotificationsProcessor extends BaseProcessor {
continue;
}
await EmailTask.schedule({
type: "collectionNotification",
options: {
to: setting.user.email,
eventName: "created",
collectionId: collection.id,
unsubscribeUrl: setting.unsubscribeUrl,
},
await CollectionNotificationEmail.schedule({
to: setting.user.email,
eventName: "created",
collectionId: collection.id,
unsubscribeUrl: setting.unsubscribeUrl,
});
}
}

View File

@@ -9,6 +9,12 @@ export enum TaskPriority {
}
export default abstract class BaseTask<T> {
/**
* Schedule this task type to be processed asyncronously by a worker.
*
* @param props Properties to be used by the task
* @returns A promise that resolves once the job is placed on the task queue
*/
public static schedule<T>(props: T) {
// @ts-expect-error cannot create an instance of an abstract class, we wont
const task = new this();
@@ -21,8 +27,17 @@ export default abstract class BaseTask<T> {
);
}
/**
* Execute the task.
*
* @param props Properties to be used by the task
* @returns A promise that resolves once the task has completed.
*/
public abstract perform(props: T): Promise<void>;
/**
* Job options such as priority and retry strategy, as defined by Bull.
*/
public get options(): JobOptions {
return {
priority: TaskPriority.Normal,

View File

@@ -1,15 +1,23 @@
import emails from "@server/emails/templates";
import { APM } from "@server/logging/tracing";
import mailer, { EmailSendOptions, EmailTypes } from "../../mailer";
import BaseTask from "./BaseTask";
type Props = {
type: EmailTypes;
options: EmailSendOptions;
templateName: string;
props: Record<string, any>;
};
@APM.trace()
export default class EmailTask extends BaseTask<Props> {
public async perform(props: Props) {
await mailer[props.type](props.options);
public async perform({ templateName, props }: Props) {
const EmailClass = emails[templateName];
if (!EmailClass) {
throw new Error(
`Email task "${templateName}" template does not exist. Check the file name matches the class name.`
);
}
const email = new EmailClass(props);
return email.send();
}
}