From e725bb113816624d0d9e5bd15c851ca4dfb0012e Mon Sep 17 00:00:00 2001 From: tommaso1 Date: Thu, 21 Nov 2024 15:01:48 +0100 Subject: [PATCH] [DEV-2026] Refactor Active Campaign package (#1252) * refactor * refactor * Removed axios, removed .env except than for the test, create new main handler to execute function in aws lambda * Update packages/active-campaign-client/jest.config.ts --------- Co-authored-by: t Co-authored-by: Marco Ponchia --- .changeset/ten-trains-grin.md | 5 + package-lock.json | 16 +-- .../active-campaign-client/jest.config.ts | 5 + packages/active-campaign-client/package.json | 3 +- .../src/__tests__/handlers/addContact.test.ts | 5 +- .../src/__tests__/handlers/createList.test.ts | 4 +- .../__tests__/handlers/deleteContact.test.ts | 6 +- .../src/__tests__/handlers/deleteList.test.ts | 4 +- .../__tests__/handlers/updateContact.test.ts | 5 +- .../src/activeCampaignClient.ts | 82 ------------ .../src/handlers/addContact.ts | 8 +- .../src/handlers/createList.ts | 8 +- .../src/handlers/deleteContact.ts | 8 +- .../src/handlers/deleteList.ts | 4 +- .../src/handlers/updateContact.ts | 6 +- packages/active-campaign-client/src/index.ts | 12 ++ .../src/types/webinarPayload.ts | 5 + .../src/utils/activeCampaignClient.ts | 121 ++++++++++++++++++ 18 files changed, 184 insertions(+), 123 deletions(-) create mode 100644 .changeset/ten-trains-grin.md delete mode 100644 packages/active-campaign-client/src/activeCampaignClient.ts create mode 100644 packages/active-campaign-client/src/index.ts create mode 100644 packages/active-campaign-client/src/types/webinarPayload.ts create mode 100644 packages/active-campaign-client/src/utils/activeCampaignClient.ts diff --git a/.changeset/ten-trains-grin.md b/.changeset/ten-trains-grin.md new file mode 100644 index 000000000..1951726bf --- /dev/null +++ b/.changeset/ten-trains-grin.md @@ -0,0 +1,5 @@ +--- +"active-campaign-client": patch +--- + +Removed axios, removed .env except than for the test, create new main handler to execute function in aws lambda diff --git a/package-lock.json b/package-lock.json index 5c56c36ae..5ed9e06ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51795,8 +51795,6 @@ "@types/aws-lambda": "^8.10.126", "@types/jest": "^29.5.1", "aws-lambda": "^1.0.7", - "axios": "^1.7.7", - "dotenv": "16.4.5", "jest": "^29.7.0", "ts-jest": "^29.1.1", "typescript": "^5.0.2" @@ -51804,6 +51802,7 @@ "devDependencies": { "@eslint/eslintrc": "^2.1.4", "@eslint/js": "^8.57.1", + "dotenv": "16.4.5", "eslint": "^8.57.1", "eslint-config-custom": "^0.1.0" } @@ -51842,18 +51841,11 @@ "dev": true, "license": "BSD-3-Clause" }, - "packages/active-campaign-client/node_modules/axios": { - "version": "1.7.7", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "packages/active-campaign-client/node_modules/dotenv": { "version": "16.4.5", - "license": "BSD-2-Clause", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, "engines": { "node": ">=12" }, diff --git a/packages/active-campaign-client/jest.config.ts b/packages/active-campaign-client/jest.config.ts index 4a5b465ec..066e81b16 100644 --- a/packages/active-campaign-client/jest.config.ts +++ b/packages/active-campaign-client/jest.config.ts @@ -1,3 +1,8 @@ +import * as dotenv from 'dotenv'; + +// Load environment variables from .env file +dotenv.config({ path: '.env' }); + module.exports = { preset: 'ts-jest', testEnvironment: 'node', diff --git a/packages/active-campaign-client/package.json b/packages/active-campaign-client/package.json index bd757f53e..ef972019b 100644 --- a/packages/active-campaign-client/package.json +++ b/packages/active-campaign-client/package.json @@ -10,14 +10,13 @@ "@eslint/eslintrc": "^2.1.4", "@eslint/js": "^8.57.1", "eslint": "^8.57.1", + "dotenv": "16.4.5", "eslint-config-custom": "^0.1.0" }, "dependencies": { "@types/aws-lambda": "^8.10.126", "@types/jest": "^29.5.1", "aws-lambda": "^1.0.7", - "axios": "^1.7.7", - "dotenv": "16.4.5", "jest": "^29.7.0", "ts-jest": "^29.1.1", "typescript": "^5.0.2" diff --git a/packages/active-campaign-client/src/__tests__/handlers/addContact.test.ts b/packages/active-campaign-client/src/__tests__/handlers/addContact.test.ts index 0851ab6f0..f912c7cc3 100644 --- a/packages/active-campaign-client/src/__tests__/handlers/addContact.test.ts +++ b/packages/active-campaign-client/src/__tests__/handlers/addContact.test.ts @@ -1,4 +1,4 @@ -import { handler } from '../../handlers/addContact'; +import { addContact } from '../../handlers/addContact'; import { SQSEvent } from 'aws-lambda'; // remove .skip to run the test, be aware it does a real API call so it will create a contact in the active campaign account @@ -12,6 +12,7 @@ describe.skip('addContact handler', () => { body: JSON.stringify({ username: `test@example${new Date().getTime()}e.com`, firstName: 'Giovanni', + cognitoId: '466e0280-9061-7007-c3e0-beb6be672f68', lastName: 'Doe', company: 'Test Co', role: 'Developer', @@ -32,7 +33,7 @@ describe.skip('addContact handler', () => { ], }; - const response = await handler(event); + const response = await addContact(event); expect(response.statusCode).toBe(200); }); }); diff --git a/packages/active-campaign-client/src/__tests__/handlers/createList.test.ts b/packages/active-campaign-client/src/__tests__/handlers/createList.test.ts index 011b7f652..1c8d722ac 100644 --- a/packages/active-campaign-client/src/__tests__/handlers/createList.test.ts +++ b/packages/active-campaign-client/src/__tests__/handlers/createList.test.ts @@ -1,4 +1,4 @@ -import { handler } from '../../handlers/createList'; +import { createList } from '../../handlers/createList'; import { SQSEvent } from 'aws-lambda'; describe.skip('createList handler', () => { @@ -31,7 +31,7 @@ describe.skip('createList handler', () => { ], }; - const response = await handler(event); + const response = await createList(event); expect(response.statusCode).toBe(200); }); }); diff --git a/packages/active-campaign-client/src/__tests__/handlers/deleteContact.test.ts b/packages/active-campaign-client/src/__tests__/handlers/deleteContact.test.ts index d3c58faa5..f6b809496 100644 --- a/packages/active-campaign-client/src/__tests__/handlers/deleteContact.test.ts +++ b/packages/active-campaign-client/src/__tests__/handlers/deleteContact.test.ts @@ -1,4 +1,4 @@ -import { handler } from '../../handlers/deleteContact'; +import { deleteContact } from '../../handlers/deleteContact'; import { SQSEvent } from 'aws-lambda'; // remove .skip to run the test, be aware it does a real API call so it will create a contact in the active campaign account @@ -11,7 +11,7 @@ describe.skip('deleteContact handler', () => { receiptHandle: '1', body: JSON.stringify({ // Replace this with the existing email of the contact you want to delete, otherwise the test will fail - username: `test@example1731961388029e.com`, + cognitoId: `466e0280-9061-7007-c3e0-beb6be672f68`, }), attributes: { ApproximateReceiveCount: '1', @@ -28,7 +28,7 @@ describe.skip('deleteContact handler', () => { ], }; - const response = await handler(event); + const response = await deleteContact(event); expect(response.statusCode).toBe(200); }); }); diff --git a/packages/active-campaign-client/src/__tests__/handlers/deleteList.test.ts b/packages/active-campaign-client/src/__tests__/handlers/deleteList.test.ts index a4dc143be..90ffd8ce8 100644 --- a/packages/active-campaign-client/src/__tests__/handlers/deleteList.test.ts +++ b/packages/active-campaign-client/src/__tests__/handlers/deleteList.test.ts @@ -1,4 +1,4 @@ -import { handler } from '../../handlers/deleteList'; +import { deleteList } from '../../handlers/deleteList'; import { SQSEvent } from 'aws-lambda'; describe.skip('deleteList handler', () => { @@ -25,7 +25,7 @@ describe.skip('deleteList handler', () => { }, ], }; - const response = await handler(event); + const response = await deleteList(event); expect(response.statusCode).toBe(200); }); }); diff --git a/packages/active-campaign-client/src/__tests__/handlers/updateContact.test.ts b/packages/active-campaign-client/src/__tests__/handlers/updateContact.test.ts index 954df1d01..521c774cf 100644 --- a/packages/active-campaign-client/src/__tests__/handlers/updateContact.test.ts +++ b/packages/active-campaign-client/src/__tests__/handlers/updateContact.test.ts @@ -1,4 +1,4 @@ -import { handler } from '../../handlers/updateContact'; +import { updateContact } from '../../handlers/updateContact'; import { SQSEvent } from 'aws-lambda'; // remove .skip to run the test, be aware it does a real API call so it will create a contact in the active campaign account @@ -16,6 +16,7 @@ describe.skip('updateContact handler', () => { company: 'Test Co', role: 'Developer', mailinglistAccepted: true, + cognitoId: '466e0280-9061-7007-c3e0-beb6be672f68', }), attributes: { ApproximateReceiveCount: '1', @@ -32,7 +33,7 @@ describe.skip('updateContact handler', () => { ], }; - const response = await handler(event); + const response = await updateContact(event); expect(response.statusCode).toBe(200); }); }); diff --git a/packages/active-campaign-client/src/activeCampaignClient.ts b/packages/active-campaign-client/src/activeCampaignClient.ts deleted file mode 100644 index f92b3c0af..000000000 --- a/packages/active-campaign-client/src/activeCampaignClient.ts +++ /dev/null @@ -1,82 +0,0 @@ -import axios from 'axios'; -import * as dotenv from 'dotenv'; -import { ContactPayload } from './types/contactPayload'; -import { ListPayload } from './types/listPayload'; - -dotenv.config({ path: '.env' }); - -export class ActiveCampaignClient { - private readonly baseUrl: string; - private readonly apiKey: string; - - constructor(baseUrl: string, apiKey: string) { - this.baseUrl = baseUrl; - this.apiKey = apiKey; - } - - private getHeaders() { - return { - 'Api-Token': this.apiKey, - 'Content-Type': 'application/json', - }; - } - - async createContact(data: ContactPayload) { - const response = await axios.post(`${this.baseUrl}/api/3/contacts`, data, { - headers: this.getHeaders(), - }); - return response.data; - } - - async updateContact(contactId: string, data: ContactPayload) { - const response = await axios.put( - `${this.baseUrl}/api/3/contacts/${contactId}`, - data, - { headers: this.getHeaders() } - ); - return response.data; - } - - async deleteContact(contactId: string) { - const response = await axios.delete( - `${this.baseUrl}/api/3/contacts/${contactId}`, - { headers: this.getHeaders() } - ); - return response.data; - } - - async getContactByEmail(email: string) { - const response = await axios.get(`${this.baseUrl}/api/3/contacts`, { - headers: this.getHeaders(), - params: { email }, - }); - return response.data?.contacts?.[0]?.id; - } - - async createList(data: ListPayload) { - const response = await axios.post(`${this.baseUrl}/api/3/lists`, data, { - headers: this.getHeaders(), - }); - return response.data; - } - - async getListIdByName(name: string) { - const response = await axios.get(`${this.baseUrl}/api/3/lists`, { - headers: this.getHeaders(), - params: { 'filters[name][eq]': name }, - }); - return response.data?.lists[0]?.id; - } - - async deleteList(id: number) { - const response = await axios.delete(`${this.baseUrl}/api/3/lists/${id}`, { - headers: this.getHeaders(), - }); - return response.data; - } -} - -export const acClient = new ActiveCampaignClient( - process.env.AC_BASE_URL!, - process.env.AC_API_KEY! -); diff --git a/packages/active-campaign-client/src/handlers/addContact.ts b/packages/active-campaign-client/src/handlers/addContact.ts index b27373886..e6bdadb7c 100644 --- a/packages/active-campaign-client/src/handlers/addContact.ts +++ b/packages/active-campaign-client/src/handlers/addContact.ts @@ -1,15 +1,16 @@ import { APIGatewayProxyResult, SQSEvent } from 'aws-lambda'; -import { acClient } from '../activeCampaignClient'; +import { acClient } from '../utils/activeCampaignClient'; import { SignUpUserData } from 'nextjs-website/src/lib/types/sign-up'; import { ContactPayload } from '../types/contactPayload'; -export async function handler(event: { +export async function addContact(event: { readonly Records: SQSEvent['Records']; }): Promise { try { const firstMessage = event.Records[0] ?? { body: '{}' }; // Parse request body - const userData: SignUpUserData = JSON.parse(firstMessage.body); + const userData: SignUpUserData & { readonly cognitoId: string } = + JSON.parse(firstMessage.body); // Transform to AC payload const acPayload: ContactPayload = { @@ -17,6 +18,7 @@ export async function handler(event: { email: userData.username, firstName: userData.firstName, lastName: userData.lastName, + phone: `cognito:${userData.cognitoId}`, fieldValues: [ { field: '2', diff --git a/packages/active-campaign-client/src/handlers/createList.ts b/packages/active-campaign-client/src/handlers/createList.ts index cd8dddd20..db5996dc5 100644 --- a/packages/active-campaign-client/src/handlers/createList.ts +++ b/packages/active-campaign-client/src/handlers/createList.ts @@ -1,14 +1,14 @@ import { APIGatewayProxyResult, SQSEvent } from 'aws-lambda'; -import { acClient } from '../activeCampaignClient'; -import { Webinar } from 'nextjs-website/src/lib/types/webinar'; +import { acClient } from '../utils/activeCampaignClient'; import { ListPayload } from '../types/listPayload'; +import { WebinarPayload } from '../types/webinarPayload'; -export async function handler(event: { +export async function createList(event: { readonly Records: SQSEvent['Records']; }): Promise { try { const firstMessage = event.Records[0] ?? { body: '{}' }; - const webinarData: Webinar = JSON.parse(firstMessage.body); + const webinarData: WebinarPayload = JSON.parse(firstMessage.body); const acPayload: ListPayload = { list: { diff --git a/packages/active-campaign-client/src/handlers/deleteContact.ts b/packages/active-campaign-client/src/handlers/deleteContact.ts index c29493f8e..1d72f6d69 100644 --- a/packages/active-campaign-client/src/handlers/deleteContact.ts +++ b/packages/active-campaign-client/src/handlers/deleteContact.ts @@ -1,15 +1,15 @@ import { APIGatewayProxyResult, SQSEvent } from 'aws-lambda'; -import { acClient } from '../activeCampaignClient'; +import { acClient } from '../utils/activeCampaignClient'; -export async function handler(event: { +export async function deleteContact(event: { readonly Records: SQSEvent['Records']; }): Promise { try { const firstMessage = event.Records[0] ?? { body: '{}' }; // Parse request body - const { username } = JSON.parse(firstMessage.body); + const { cognitoId } = JSON.parse(firstMessage.body); - const contactId = await acClient.getContactByEmail(username); + const contactId = await acClient.getContactByCognitoId(cognitoId); if (!contactId) { return { diff --git a/packages/active-campaign-client/src/handlers/deleteList.ts b/packages/active-campaign-client/src/handlers/deleteList.ts index 5e4ab9659..22ea9ac9c 100644 --- a/packages/active-campaign-client/src/handlers/deleteList.ts +++ b/packages/active-campaign-client/src/handlers/deleteList.ts @@ -1,7 +1,7 @@ import { APIGatewayProxyResult, SQSEvent } from 'aws-lambda'; -import { acClient } from '../activeCampaignClient'; +import { acClient } from '../utils/activeCampaignClient'; -export async function handler(event: { +export async function deleteList(event: { readonly Records: SQSEvent['Records']; }): Promise { try { diff --git a/packages/active-campaign-client/src/handlers/updateContact.ts b/packages/active-campaign-client/src/handlers/updateContact.ts index 6b896dcec..dff29e4bd 100644 --- a/packages/active-campaign-client/src/handlers/updateContact.ts +++ b/packages/active-campaign-client/src/handlers/updateContact.ts @@ -1,8 +1,8 @@ import { APIGatewayProxyResult, SQSEvent } from 'aws-lambda'; -import { acClient } from '../activeCampaignClient'; +import { acClient } from '../utils/activeCampaignClient'; import { ContactPayload } from '../types/contactPayload'; -export async function handler(event: { +export async function updateContact(event: { readonly Records: SQSEvent['Records']; }): Promise { try { @@ -17,7 +17,7 @@ export async function handler(event: { }; } - const contactId = await acClient.getContactByEmail(userData.username); + const contactId = await acClient.getContactByCognitoId(userData.cognitoId); if (!contactId) { return { diff --git a/packages/active-campaign-client/src/index.ts b/packages/active-campaign-client/src/index.ts new file mode 100644 index 000000000..87b5a2fda --- /dev/null +++ b/packages/active-campaign-client/src/index.ts @@ -0,0 +1,12 @@ +import { APIGatewayProxyResult, SQSEvent } from 'aws-lambda'; +import { addContact } from './handlers/addContact'; + +export async function handler(event: { + readonly Records: SQSEvent['Records']; +}): Promise { + console.log('Event:', event); + return { + statusCode: 200, + body: JSON.stringify(event), + }; +} diff --git a/packages/active-campaign-client/src/types/webinarPayload.ts b/packages/active-campaign-client/src/types/webinarPayload.ts new file mode 100644 index 000000000..93e64b469 --- /dev/null +++ b/packages/active-campaign-client/src/types/webinarPayload.ts @@ -0,0 +1,5 @@ +export type WebinarPayload = { + readonly slug: string; + readonly title: string; + readonly subscribeCtaLabel: string; +}; diff --git a/packages/active-campaign-client/src/utils/activeCampaignClient.ts b/packages/active-campaign-client/src/utils/activeCampaignClient.ts new file mode 100644 index 000000000..53cd3ccaa --- /dev/null +++ b/packages/active-campaign-client/src/utils/activeCampaignClient.ts @@ -0,0 +1,121 @@ +import * as https from 'https'; +import { ContactPayload } from '../types/contactPayload'; +import { ListPayload } from '../types/listPayload'; + +export class ActiveCampaignClient { + private readonly baseUrl: string; + private readonly apiKey: string; + + constructor(baseUrl: string, apiKey: string) { + // Remove any trailing slashes from the baseUrl + this.baseUrl = baseUrl.replace(/\/$/, ''); + this.apiKey = apiKey; + } + + private getHeaders() { + return { + 'Api-Token': this.apiKey, + 'Content-Type': 'application/json', + }; + } + + private async makeRequest( + method: string, + path: string, + data?: any, + params?: Record + ): Promise { + return new Promise((resolve, reject) => { + // Parse the base URL to get hostname and path + const url = new URL(path, this.baseUrl); + + // Add query parameters if they exist + if (params) { + Object.entries(params).forEach(([key, value]) => { + url.searchParams.append(key, value); + }); + } + + const options = { + method, + hostname: url.hostname, + path: `${url.pathname}${url.search}`, + headers: this.getHeaders(), + }; + + const req = https.request(options, (res) => { + // eslint-disable-next-line functional/no-let + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { + try { + resolve(JSON.parse(data)); + } catch (error) { + reject(new Error('Failed to parse response data')); + } + } else { + reject( + new Error(`Request failed with status code ${res.statusCode}`) + ); + } + }); + }); + + req.on('error', (error) => { + reject(error); + }); + + if (data) { + req.write(JSON.stringify(data)); + } + + req.end(); + }); + } + + async createContact(data: ContactPayload) { + return this.makeRequest('POST', '/api/3/contacts', data); + } + + async updateContact(contactId: string, data: ContactPayload) { + return this.makeRequest('PUT', `/api/3/contacts/${contactId}`, data); + } + + async deleteContact(contactId: string) { + return this.makeRequest('DELETE', `/api/3/contacts/${contactId}`); + } + + async getContactByCognitoId(cognitoId: string) { + const response = await this.makeRequest<{ + readonly contacts: ReadonlyArray<{ readonly id: string }>; + }>('GET', '/api/3/contacts', undefined, { phone: `cognito:${cognitoId}` }); + return response?.contacts?.[0]?.id; + } + + async createList(data: ListPayload) { + return this.makeRequest('POST', '/api/3/lists', data); + } + + async getListIdByName(name: string) { + const response = await this.makeRequest<{ + readonly lists: ReadonlyArray<{ readonly id: number }>; + }>('GET', '/api/3/lists', undefined, { 'filters[name][eq]': name }); + return response?.lists[0]?.id; + } + + async deleteList(id: number) { + return this.makeRequest('DELETE', `/api/3/lists/${id}`); + } +} + +export const acClient = new ActiveCampaignClient( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + process.env.AC_BASE_URL!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + process.env.AC_API_KEY! +);