From fd060c108e7efe2f87c50709a902d41877729553 Mon Sep 17 00:00:00 2001 From: David Hochbaum Date: Wed, 9 Oct 2024 15:17:36 -0400 Subject: [PATCH] Refactored Sendgrid API calls as a service --- server/src/_utils/validate-email.ts | 1 - .../src/subscriber/subscriber.controller.ts | 110 ++---------------- server/src/subscriber/subscriber.module.ts | 6 +- server/src/subscriber/subscriber.service.ts | 81 +++++++++++++ 4 files changed, 96 insertions(+), 102 deletions(-) create mode 100644 server/src/subscriber/subscriber.service.ts diff --git a/server/src/_utils/validate-email.ts b/server/src/_utils/validate-email.ts index cdcb2db5..6bc0db14 100644 --- a/server/src/_utils/validate-email.ts +++ b/server/src/_utils/validate-email.ts @@ -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; diff --git a/server/src/subscriber/subscriber.controller.ts b/server/src/subscriber/subscriber.controller.ts index 8931c68c..b9ef8299 100644 --- a/server/src/subscriber/subscriber.controller.ts +++ b/server/src/subscriber/subscriber.controller.ts @@ -1,28 +1,20 @@ -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"); @@ -30,32 +22,14 @@ export class SubscriberController { @Post("/subscribers") async subscribe(@Req() request: Request, @Res() response) { - const searchRequest = { - url: "/v3/marketing/contacts/search/emails", - method: 'POST', - body: { - "emails": [request.body.email] - } - } - const addRequest = { - url: "/v3/marketing/contacts", - method: '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 }) @@ -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({ @@ -100,69 +77,4 @@ export class SubscriberController { return; } - - async searchByEmail(email: string) { - const searchRequest = { - url: "/v3/marketing/contacts/search/emails", - method: '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: '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); - } - } \ No newline at end of file diff --git a/server/src/subscriber/subscriber.module.ts b/server/src/subscriber/subscriber.module.ts index ab518594..4f9b1956 100644 --- a/server/src/subscriber/subscriber.module.ts +++ b/server/src/subscriber/subscriber.module.ts @@ -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 {} diff --git a/server/src/subscriber/subscriber.service.ts b/server/src/subscriber/subscriber.service.ts new file mode 100644 index 00000000..3cd7c368 --- /dev/null +++ b/server/src/subscriber/subscriber.service.ts @@ -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: '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: '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); + } +}