diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c5323da0c0..9dc42643afe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5700,9 +5700,9 @@ importers: '@novu/stateless': specifier: ^0.21.0 version: link:../../packages/stateless - sparkpost: - specifier: ^2.1.4 - version: 2.1.4 + axios: + specifier: ^1.6.0 + version: 1.6.0 devDependencies: '@istanbuljs/nyc-config-typescript': specifier: ^1.0.1 @@ -5745,7 +5745,7 @@ importers: version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 - version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) + version: 10.9.1(@types/node@20.5.1)(typescript@4.9.5) typedoc: specifier: ^0.24.0 version: 0.24.6(typescript@4.9.5) @@ -7895,7 +7895,7 @@ packages: '@babel/traverse': 7.23.2 '@babel/types': 7.23.0 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -11531,7 +11531,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.0 '@babel/types': 7.23.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -24135,7 +24135,6 @@ packages: resolution: {integrity: sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==} requiresBuild: true dev: true - optional: true /@types/nodemailer@6.4.11: resolution: {integrity: sha512-Ld2c0frwpGT4VseuoeboCXQ7UJIkK3X7Lx/4YsZEiUHtHsthWAOCYtf6PAiLhMtfwV0cWJRabLBS3+LD8x6Nrw==} @@ -25722,7 +25721,7 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -30104,6 +30103,17 @@ packages: ms: 2.1.2 dev: false + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + /debug@4.3.4(supports-color@8.1.1): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -34430,7 +34440,7 @@ packages: dependencies: '@tootallnate/once': 1.1.2 agent-base: 6.0.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 transitivePeerDependencies: - supports-color dev: true @@ -34547,7 +34557,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -35688,7 +35698,7 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 istanbul-lib-coverage: 3.2.0 source-map: 0.6.1 transitivePeerDependencies: @@ -35937,7 +35947,7 @@ packages: pretty-format: 27.5.1 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1(@types/node@16.11.7)(typescript@4.9.5) + ts-node: 10.9.1(@types/node@20.5.1)(typescript@4.9.5) transitivePeerDependencies: - bufferutil - canvas @@ -46565,14 +46575,6 @@ packages: resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==} dev: false - /sparkpost@2.1.4: - resolution: {integrity: sha512-7yvpfLVCnCVaE1yexemHNDX2bLkG3Lel14aCwj1ZCAX8Aa4OjYCP7uWPOkSwwXLYfQZRUgdwxgzXwaIDiYVx7A==} - engines: {node: '>=4'} - dependencies: - lodash: 4.17.21 - request: 2.88.2 - dev: false - /sparse-bitfield@3.0.3: resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} requiresBuild: true @@ -48512,7 +48514,6 @@ packages: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 dev: true - optional: true /ts-node@9.1.1(typescript@4.9.5): resolution: {integrity: sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==} diff --git a/providers/sparkpost/jest.config.js b/providers/sparkpost/jest.config.js index e86e13bab91..61faa20934a 100644 --- a/providers/sparkpost/jest.config.js +++ b/providers/sparkpost/jest.config.js @@ -2,4 +2,7 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', + moduleNameMapper: { + axios: 'axios/dist/node/axios.cjs', + }, }; diff --git a/providers/sparkpost/package.json b/providers/sparkpost/package.json index dc8ad4e652d..272778788b8 100644 --- a/providers/sparkpost/package.json +++ b/providers/sparkpost/package.json @@ -33,7 +33,7 @@ }, "dependencies": { "@novu/stateless": "^0.21.0", - "sparkpost": "^2.1.4" + "axios": "^1.6.0" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", diff --git a/providers/sparkpost/src/lib/sparkpost.error.ts b/providers/sparkpost/src/lib/sparkpost.error.ts new file mode 100644 index 00000000000..2396d5edf33 --- /dev/null +++ b/providers/sparkpost/src/lib/sparkpost.error.ts @@ -0,0 +1,16 @@ +export interface ISparkPostErrorResponse { + errors: Array<{ + description: string; + code: string; + message: string; + }>; +} + +export class SparkPostError extends Error implements ISparkPostErrorResponse { + readonly errors: ISparkPostErrorResponse['errors']; + + constructor(response: ISparkPostErrorResponse, readonly statusCode: number) { + super(); + this.errors = response.errors; + } +} diff --git a/providers/sparkpost/src/lib/sparkpost.provider.ts b/providers/sparkpost/src/lib/sparkpost.provider.ts index b8159467827..84493283c50 100644 --- a/providers/sparkpost/src/lib/sparkpost.provider.ts +++ b/providers/sparkpost/src/lib/sparkpost.provider.ts @@ -6,13 +6,22 @@ import { ICheckIntegrationResponse, CheckIntegrationResponseEnum, } from '@novu/stateless'; +import axios, { AxiosError } from 'axios'; import { randomUUID } from 'crypto'; -import SparkPost from 'sparkpost'; +import { ISparkPostErrorResponse, SparkPostError } from './sparkpost.error'; + +interface ISparkPostResponse { + results: { + total_rejected_recipients: number; + total_accepted_recipients: number; + id: string; + }; +} export class SparkPostEmailProvider implements IEmailProvider { readonly id = 'sparkpost'; - readonly channelType = ChannelTypeEnum.EMAIL as ChannelTypeEnum.EMAIL; - private readonly client: SparkPost; + readonly channelType = ChannelTypeEnum.EMAIL; + private readonly endpoint: string; constructor( private config: { @@ -22,10 +31,7 @@ export class SparkPostEmailProvider implements IEmailProvider { senderName: string; } ) { - const endpoint = this.getEndpoint(config.region); - this.client = new SparkPost(config.apiKey, { - endpoint, - }); + this.endpoint = this.getEndpoint(config.region); } async sendMessage({ @@ -50,7 +56,7 @@ export class SparkPostEmailProvider implements IEmailProvider { }); }); - const sent = await this.client.transmissions.send({ + const data = { recipients, content: { from: from || this.config.from, @@ -59,12 +65,29 @@ export class SparkPostEmailProvider implements IEmailProvider { html, attachments: files, }, - }); - - return { - id: sent.results.id, - date: new Date().toISOString(), }; + + try { + const sent = await axios.post( + '/transmissions', + data, + { + headers: { + 'Content-Type': 'application/json', + Authorization: this.config.apiKey, + }, + baseURL: this.endpoint, + } + ); + + return { + id: sent.data.results.id, + date: new Date().toISOString(), + }; + } catch (err) { + this.createSparkPostError(err); + throw err; + } } async checkIntegration( @@ -93,6 +116,16 @@ export class SparkPostEmailProvider implements IEmailProvider { } } + private createSparkPostError(err: unknown) { + if (axios.isAxiosError(err)) { + const response = (err as AxiosError).response; + + if (response && response.data && response.data.errors) { + throw new SparkPostError(response.data, response.status); + } + } + } + private transformLegacyRegion(region: string | boolean) { if (region === 'true' || region === true) return 'eu'; @@ -104,9 +137,9 @@ export class SparkPostEmailProvider implements IEmailProvider { switch (region) { case 'eu': - return 'https://api.eu.sparkpost.com:443'; + return 'https://api.eu.sparkpost.com/api/v1'; default: - return; + return 'https://api.sparkpost.com/api/v1'; } } }