Skip to content

Commit

Permalink
Refactored Sendgrid API calls as a service
Browse files Browse the repository at this point in the history
  • Loading branch information
dhochbaum-dcp committed Oct 9, 2024
1 parent 9c62de9 commit fd060c1
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 102 deletions.
1 change: 0 additions & 1 deletion server/src/_utils/validate-email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ var tester = /^[-!#$%&'*+\/0-9=?A-Z^_a-z{|}~](\.?[-!#$%&'*+\/0-9=?A-Z^_a-z`{|}~]
* @returns {boolean}
*/
export default function validateEmail(email: string) {

if (!email)
return false;

Expand Down
110 changes: 11 additions & 99 deletions server/src/subscriber/subscriber.controller.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,35 @@
import { Controller, Get, Post, Param, Query, Req, Res } from "@nestjs/common";
import { Controller, Post, Req, Res } from "@nestjs/common";
import { ConfigService } from "../config/config.service";
import { SubscriberService } from "./subscriber.service";
import { Request } from "express";
import validateEmail from "../_utils/validate-email";
import client from "@sendgrid/client";

type HttpMethod = 'get'|'GET'|'post'|'POST'|'put'|'PUT'|'patch'|'PATCH'|'delete'|'DELETE';

const PAUSE_BETWEEN_CHECKS = 3000;
const CHECKS_BEFORE_FAIL = 10;

function delay(milliseconds){
return new Promise(resolve => {
setTimeout(resolve, milliseconds);
});
}

@Controller()
export class SubscriberController {
apiKey = "";
list = "";

constructor(
private readonly config: ConfigService

private readonly config: ConfigService,
private readonly subscriberService: SubscriberService
) {
this.apiKey = this.config.get("SENDGRID_API_KEY");
this.list = this.config.get("SENDGRID_LIST");
}

@Post("/subscribers")
async subscribe(@Req() request: Request, @Res() response) {
const searchRequest = {
url: "/v3/marketing/contacts/search/emails",
method:<HttpMethod> 'POST',
body: {
"emails": [request.body.email]
}
}
const addRequest = {
url: "/v3/marketing/contacts",
method:<HttpMethod> 'PUT',
body: {
"list_ids": [this.list],
"contacts": [{"email": request.body.email}]
}
}

if (!validateEmail(request.body.email)) {
response.status(400).send({
error: "Invalid email address."
})
return;
}

client.setApiKey(this.apiKey);

const existingUser = await this.searchByEmail(request.body.email)
const existingUser = await this.subscriberService.findByEmail(request.body.email)
if(![200, 404].includes(existingUser.code)) {
console.error(existingUser.code, existingUser.message);
response.status(existingUser.code).send({ error: existingUser.message })
Expand All @@ -72,12 +46,15 @@ export class SubscriberController {
}

// If we have reached this point, the user either doesn't exist or isn't signed up for the list
const addToQueue = await this.addSubscriber(request.body.email, response)
const addToQueue = await this.subscriberService.create(request.body.email, this.list, response)

if(addToQueue.isError) { return; }
if(addToQueue.isError) {
response.status(addToQueue.code).send({errors: addToQueue.response.body.errors})
return;
}

// Now we keep checking to make sure the import was successful
const importConfirmation = await this.checkSuccessfulImport(request.body.email, response, 0)
const importConfirmation = await this.subscriberService.checkCreate(request.body.email, response, 0, CHECKS_BEFORE_FAIL, PAUSE_BETWEEN_CHECKS, this.list)

if(importConfirmation.isError && (importConfirmation.code === 408)) {
response.status(408).send({
Expand All @@ -100,69 +77,4 @@ export class SubscriberController {
return;

}

async searchByEmail(email: string) {
const searchRequest = {
url: "/v3/marketing/contacts/search/emails",
method:<HttpMethod> 'POST',
body: {
"emails": [email]
}
}

// https://www.twilio.com/docs/sendgrid/api-reference/contacts/get-contacts-by-emails
try {
const user = await client.request(searchRequest);
return {isError: false, code: user[0].statusCode, ...user};
} catch(error) {
return {isError: true, ...error};
}
}


async addSubscriber(email: string, @Res() response) {
const addRequest = {
url: "/v3/marketing/contacts",
method:<HttpMethod> 'PUT',
body: {
"list_ids": [this.list],
"contacts": [{"email": email}]
}
}

// If successful, this will add the request to the queue and return a 202
// https://www.twilio.com/docs/sendgrid/api-reference/contacts/add-or-update-a-contact
try {
const result = await client.request(addRequest);
return {isError: false, result: result};
} catch(error) {
response.status(error.code).send({errors: error.response.body.errors})
return {isError: true, ...error};
}
}


async checkSuccessfulImport(email: string, @Res() response, counter = 0) {
if(counter >= CHECKS_BEFORE_FAIL) {
return { isError: true, code: 408 }
}

await delay(PAUSE_BETWEEN_CHECKS);

const existingUser = await this.searchByEmail(email);

if(![200, 404].includes(existingUser.code)) {
console.error(existingUser.code, existingUser.message);
return { isError: true, code: existingUser.code, message: existingUser.message }
}


if(existingUser[0] && existingUser[0].body.result[email].contact["list_ids"].includes(this.list)) {
// Success!
return { isError: false, code: 200, ...existingUser };
}

return await this.checkSuccessfulImport(email, response, counter + 1);
}

}
6 changes: 4 additions & 2 deletions server/src/subscriber/subscriber.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Module } from "@nestjs/common";
import { SubscriberController } from "./subscriber.controller";
import { SubscriberService } from "./subscriber.service";
import { ConfigModule } from "../config/config.module";
import { Client } from "@sendgrid/client";

@Module({
imports: [ConfigModule],
providers: [],
exports: [],
providers: [SubscriberService, Client],
exports: [SubscriberService],
controllers: [SubscriberController]
})
export class SubscriberModule {}
81 changes: 81 additions & 0 deletions server/src/subscriber/subscriber.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Injectable, Res } from "@nestjs/common";
import { ConfigService } from "../config/config.service";
import { Client } from "@sendgrid/client";

type HttpMethod = 'get'|'GET'|'post'|'POST'|'put'|'PUT'|'patch'|'PATCH'|'delete'|'DELETE';

function delay(milliseconds){
return new Promise(resolve => {
setTimeout(resolve, milliseconds);
});
}

@Injectable()
export class SubscriberService {
constructor(
private readonly config: ConfigService,
private client: Client
) {
this.client.setApiKey(this.config.get("SENDGRID_API_KEY"));
}

async findByEmail(email: string) {
const searchRequest = {
url: "/v3/marketing/contacts/search/emails",
method:<HttpMethod> 'POST',
body: {
"emails": [email]
}
}

// https://www.twilio.com/docs/sendgrid/api-reference/contacts/get-contacts-by-emails
try {
const user = await this.client.request(searchRequest);
return {isError: false, code: user[0].statusCode, ...user};
} catch(error) {
return {isError: true, ...error};
}
}

async create(email: string, list: string, @Res() response) {
const addRequest = {
url: "/v3/marketing/contacts",
method:<HttpMethod> 'PUT',
body: {
"list_ids": [list],
"contacts": [{"email": email}]
}
}

// If successful, this will add the request to the queue and return a 202
// https://www.twilio.com/docs/sendgrid/api-reference/contacts/add-or-update-a-contact
try {
const result = await this.client.request(addRequest);
return {isError: false, result: result};
} catch(error) {
return {isError: true, ...error};
}
}

async checkCreate(email: string, @Res() response, counter: number = 0, checksBeforeFail: number, pauseBetweenChecks: number, list: string) {
if(counter >= checksBeforeFail) {
return { isError: true, code: 408 }
}

await delay(pauseBetweenChecks);

const existingUser = await this.findByEmail(email);

if(![200, 404].includes(existingUser.code)) {
console.error(existingUser.code, existingUser.message);
return { isError: true, code: existingUser.code, message: existingUser.message }
}

if(existingUser[0] && existingUser[0].body.result[email].contact["list_ids"].includes(list)) {
// Success!
return { isError: false, code: 200, ...existingUser };
}

return await this.checkCreate(email, response, counter + 1, checksBeforeFail, pauseBetweenChecks, list);
}
}

0 comments on commit fd060c1

Please sign in to comment.