Skip to content

Commit

Permalink
Fix individual mode, add more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
arn4v committed Sep 4, 2022
1 parent 351eeb6 commit 1a3996d
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 36 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ jobs:

- name: Run tests
run: yarn test

- name: Code coverage
run: yarn test --coverage
24 changes: 14 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
# Dakiya
# Dakiya ![](https://github.com/arn4v/dakiya/actions/workflows/ci/badge.svg)

Simple email sequence scheduler for Node.js.
_Simple_ email automation for Node.js _made easy_.

## Usage
## Features

- **Zero config management**: Use simple, chainable code to create email sequences.
- **Email platform agnostic**: Works with any email platform, you just need SMTP credentials.

## Example Ussage

```typescript
import { Sequence, Scheduler } from "dakiya";
Expand All @@ -22,16 +27,15 @@ export const onboarding = new Sequence(
welcomeVariablesSchema
)
.waitFor("5m")
.mail({
.sendMail({
key: "welcome",
getSubject() {
return "Welcome to {Product Name}!";
},
getHtml({ name }) {
subject: "Welcome to {Product Name}!",
html({ name }) {
return `Hi ${name}, Welcome to {Product Name}`; // Email HTML
},
})
.mail({
.waitFor("5m")
.sendMail({
key: "verify_email",
getSubject() {
return "Verify Your Email";
Expand All @@ -47,7 +51,7 @@ export const scheduler = new Scheduler([onboarding], {
});

await scheduler.initialize();
await scheduler.exec(
await scheduler.schedule(
EmailSequence.Onboarding,
{ name: "", verificationUrl: "" },
// Nodemailer SendMailOptions
Expand Down
3 changes: 2 additions & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ const config: Config = {
preset: "ts-jest",
roots: ["<rootDir>/src"],
moduleFileExtensions: ["js", "json", "ts"],
forceExit: true
forceExit: true,
verbose: false,
};

export default config;
96 changes: 93 additions & 3 deletions src/scheduler.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it } from "@jest/globals";
import { describe, it, test } from "@jest/globals";
import { MongoClient } from "mongodb";
import { MongoMemoryServer } from "mongodb-memory-server";
import ms from "ms";
Expand All @@ -12,6 +12,7 @@ import {
import { z, ZodError } from "zod";
import { Scheduler } from "./scheduler";
import { Sequence } from "./sequence";
import { sleep } from "./utils";

const testSequence = new Sequence(
"test",
Expand Down Expand Up @@ -66,7 +67,7 @@ describe("Scheduler", () => {
}
});

it("Should connect to MongoDB on initialize", async () => {
it("Should connect to MongoDB & schedule Cron on initialize", async () => {
const cronSpy = jest.spyOn(cron, "schedule");
const mongoSpy = jest.spyOn(mongo, "connect");

Expand All @@ -81,6 +82,30 @@ describe("Scheduler", () => {
expect(cronSpy).toBeCalled();
});

it(
"Should send emails on cron.schedule call",
async () => {
const scheduler = new Scheduler([testSequence], {
mongo,
transporter,
});

const sendPendingSpy = jest.spyOn(scheduler, "sendPendingEmails");
const cronSpy = jest.spyOn(cron, "schedule").mockImplementationOnce(
// @ts-ignore
async (_, __, ___) => await scheduler.sendPendingEmails()
);

await scheduler.initialize();
await scheduler.schedule("test", { name: "" }, { from: "", to: "" });

expect(cronSpy).toBeCalled();
// await sleep(60 * 1000);
expect(sendPendingSpy).toBeCalled();
},
70 * 1000
);

it("Should throw an exception if variables don't match zod spec", async () => {
const scheduler = new Scheduler([testSequence], {
mongo,
Expand Down Expand Up @@ -182,7 +207,7 @@ describe("Scheduler", () => {
expect(jobs?.[1].scheduledFor! - jobs?.[0].scheduledFor!).toBe(ms("2m"));
});

it("Should not stack (individual mode) waitFor delays", async () => {
it("Should not stack waitFor delays (individual mode)", async () => {
const sequence = new Sequence(
"test",
z.object({
Expand All @@ -203,6 +228,7 @@ describe("Scheduler", () => {
const scheduler = new Scheduler([sequence], {
mongo,
transporter,
waitMode: "individual",
});

const { ops } = scheduler.getScheduledJobsOpsObject(sequence);
Expand All @@ -214,4 +240,68 @@ describe("Scheduler", () => {

expect(jobs?.[1].scheduledFor! - jobs?.[0].scheduledFor!).toBe(ms("1m"));
});

test("getScheduledSequence should throw error if invalid _id is passed", async () => {
const sequence = new Sequence(
"test",
z.object({
name: z.string(),
})
)
.waitFor("1m")
.sendMail({
html: "1",
subject: "",
})
.waitFor("2m")
.sendMail({
html: "2",
subject: "",
});

const scheduler = new Scheduler([sequence], {
mongo,
transporter,
waitMode: "individual",
});

await scheduler.initialize();

expect(() => scheduler.getScheduledSequence("invalidId")).rejects.toThrow();
});

it("Should send all pending emails", async () => {
const sequence = new Sequence(
"test",
z.object({
name: z.string(),
})
)
.sendMail({
html: "1",
subject: "",
})
.sendMail({
html: "2",
subject: "",
});

const sendMailSpy = jest
.spyOn(transporter, "sendMail")
.mockImplementation(jest.fn());

const scheduler = new Scheduler([sequence], {
mongo,
transporter,
});

await scheduler.initialize();
await scheduler.schedule("test", { name: "" }, { from: "", to: "" });

expect(await scheduler.getScheduledJobs()).toHaveLength(2);

await scheduler.sendPendingEmails();

expect(sendMailSpy).toBeCalledTimes(2);
});
});
29 changes: 12 additions & 17 deletions src/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ export class Scheduler<
this.sequenceCollection = this.db.collection("sequences");
this.jobsCollection = this.db.collection("jobs");

console.log("Dakiya: Connected to MongoDB");
console.log("Dakiya: Scheduler.initialize: Connected to MongoDB");
} catch (err) {
console.error("Dakiya: Unable to connect to MongoDB");
console.error("Dakiya: Scheduler.initialize: Unable to connect to MongoDB");
throw err;
}

Expand All @@ -72,9 +72,10 @@ export class Scheduler<
}

private startCron() {
cron.schedule("* * * * *", (now) => {
void this.sendPendingEmails();
});
cron.schedule(
this.params.cronStringOverride ?? "* * * * *",
this.sendPendingEmails
);
}

async getScheduledSequence(_id: ObjectId | string) {
Expand All @@ -83,7 +84,7 @@ export class Scheduler<
});

if (!sequence) {
throw "Sequence not found";
throw new Error("Sequence not found");
}

return sequence;
Expand All @@ -101,16 +102,10 @@ export class Scheduler<

async sendPendingEmails() {
const jobs = (await this.getScheduledJobs()) || [];
for (const { _id, key, sequenceId: workflowId } of jobs) {
try {
const scheduledSequence = await this.getScheduledSequence(workflowId);

if (!scheduledSequence) {
console.error(
`sendPendingEmails: Invalid scheduled sequence id ${workflowId}. Not sending email ${key} ${_id}.`
);
continue;
}
for (const { _id, key, sequenceId } of jobs) {
try {
const scheduledSequence = await this.getScheduledSequence(sequenceId);

const sequenceObject =
this.sequences[scheduledSequence.name as unknown as SequenceKeys];
Expand All @@ -135,7 +130,7 @@ export class Scheduler<
} catch (e) {
if (e instanceof Error) {
console.error(
`sendPendingEmails: Failed to send email ${key} of workflow id ${workflowId}. Reason: ${e.message}`
`sendPendingEmails: Failed to send email ${key} of workflow id ${sequenceId}. Reason: ${e.message}`
);
}
continue;
Expand Down Expand Up @@ -171,7 +166,7 @@ export class Scheduler<
} catch (e) {
if (e instanceof ZodError) {
console.error(
`Variables provided for sequence ${String(
`Dakiya: Scheduler.schedule: Variables provided for sequence ${String(
name
)} do not match schema.`,
e.issues
Expand Down
Empty file removed src/sequence.test.ts
Empty file.
6 changes: 1 addition & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,9 @@ type TransporterOrOptions =
transporter: Transporter;
};

enum WaitMode {
Stacked = "stacked",
Indepedent = "independent",
}

export type SchedulerParams = {
waitMode?: "stack" | "individual";
cronStringOverride?: string;
} & TransporterOrOptions &
MongoClientOrUrl;

Expand Down

0 comments on commit 1a3996d

Please sign in to comment.