feat: Add support for sendgrid and logger notification providers (#7290)

* feat: Add support for sendgrid and logger notification providers

* fix: changes based on PR review
This commit is contained in:
Stevche Radevski
2024-05-11 17:40:03 +02:00
committed by GitHub
parent 1a68f4602c
commit 79758c46b6
24 changed files with 631 additions and 1 deletions

View File

@@ -0,0 +1,36 @@
import { LocalNotificationService } from "../../src/services/local"
jest.setTimeout(100000)
describe("Local notification provider", () => {
let localService: LocalNotificationService
beforeAll(() => {
localService = new LocalNotificationService(
{
logger: console as any,
},
{}
)
})
afterEach(() => {
jest.restoreAllMocks()
})
it("sends logs to the console output with the notification details", async () => {
const logSpy = jest.spyOn(console, "info")
await localService.send({
to: "test@medusajs.com",
channel: "email",
template: "some-template",
data: {
username: "john-doe",
},
})
expect(logSpy).toHaveBeenCalled()
expect(logSpy).toHaveBeenCalledWith(
'Attempting to send a notification to: test@medusajs.com on the channel: email with template: some-template and data: {"username":"john-doe"}'
)
})
})

View File

@@ -0,0 +1,7 @@
module.exports = {
transform: {
"^.+\\.[jt]s?$": "@swc/jest",
},
testEnvironment: `node`,
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
}

View File

@@ -0,0 +1,39 @@
{
"name": "@medusajs/notification-local",
"version": "0.0.1",
"description": "Local (logging) notification provider for Medusa, useful for testing purposes and log audits",
"main": "dist/index.js",
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/modules/providers/notification-local"
},
"files": [
"dist"
],
"engines": {
"node": ">=16"
},
"author": "Medusa",
"license": "MIT",
"scripts": {
"prepublishOnly": "cross-env NODE_ENV=production tsc --build",
"test": "jest --passWithNoTests src",
"test:integration": "jest --forceExit -- integration-tests/**/__tests__/**/*.spec.ts",
"build": "rimraf dist && tsc -p ./tsconfig.json",
"watch": "tsc --watch"
},
"devDependencies": {
"cross-env": "^5.2.1",
"jest": "^25.5.4",
"rimraf": "^5.0.1",
"typescript": "^4.9.5"
},
"dependencies": {
"@medusajs/utils": "^1.11.7"
},
"keywords": [
"medusa-provider",
"medusa-provider-local"
]
}

View File

@@ -0,0 +1,10 @@
import { ModuleProviderExports } from "@medusajs/types"
import { LocalNotificationService } from "./services/local"
const services = [LocalNotificationService]
const providerExport: ModuleProviderExports = {
services,
}
export default providerExport

View File

@@ -0,0 +1,48 @@
import {
Logger,
NotificationTypes,
LocalNotificationServiceOptions,
} from "@medusajs/types"
import {
AbstractNotificationProviderService,
MedusaError,
} from "@medusajs/utils"
type InjectedDependencies = {
logger: Logger
}
interface LocalServiceConfig {}
export class LocalNotificationService extends AbstractNotificationProviderService {
protected config_: LocalServiceConfig
protected logger_: Logger
constructor(
{ logger }: InjectedDependencies,
options: LocalNotificationServiceOptions
) {
super()
this.config_ = options
this.logger_ = logger
}
async send(
notification: NotificationTypes.ProviderSendNotificationDTO
): Promise<NotificationTypes.ProviderSendNotificationResultsDTO> {
if (!notification) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`No notification information provided`
)
}
const message =
`Attempting to send a notification to: ${notification.to}` +
` on the channel: ${notification.channel} with template: ${notification.template}` +
` and data: ${JSON.stringify(notification.data)}`
this.logger_.info(message)
return {}
}
}

View File

@@ -0,0 +1,32 @@
{
"compilerOptions": {
"lib": ["es2020"],
"target": "es2020",
"jsx": "react-jsx" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
"outDir": "./dist",
"esModuleInterop": true,
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"noImplicitReturns": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"allowJs": true,
"skipLibCheck": true,
"downlevelIteration": true, // to use ES5 specific tooling
"inlineSourceMap": true /* Emit a single file with source maps instead of having a separate file. */
},
"include": ["src"],
"exclude": [
"dist",
"build",
"src/**/__tests__",
"src/**/__mocks__",
"src/**/__fixtures__",
"node_modules",
".eslintrc.js"
]
}

View File

@@ -0,0 +1,70 @@
import { SendgridNotificationService } from "../../src/services/sendgrid"
jest.setTimeout(100000)
// Note: This test hits the sendgrid service, and it is mainly meant to be run manually after setting all the envvars below.
// We could also setup a sink email service to test this automatically, but it is not necessary for the time being.
describe.skip("Sendgrid notification provider", () => {
let sendgridService: SendgridNotificationService
let emailTemplate = ""
let to = ""
beforeAll(() => {
sendgridService = new SendgridNotificationService(
{
logger: console as any,
},
{
api_key: process.env.SENDGRID_TEST_API_KEY ?? "",
from: process.env.SENDGRID_TEST_FROM ?? "",
}
)
emailTemplate = process.env.SENDGRID_TEST_TEMPLATE ?? ""
to = process.env.SENDGRID_TEST_TO ?? ""
})
it("sends an email with the specified template", async () => {
const resp = await sendgridService.send({
to,
channel: "email",
template: emailTemplate,
data: {
username: "john-doe",
},
})
expect(resp).toEqual({})
})
it("throws an exception if the template does not exist", async () => {
const error = await sendgridService
.send({
to,
channel: "email",
template: "unknown-template",
data: {
username: "john-doe",
},
})
.catch((e) => e)
expect(error.message).toEqual(
"Failed to send email: 400 - The template_id must be a valid GUID, you provided 'unknown-template'."
)
})
it("throws an exception if the to email is not valid", async () => {
const error = await sendgridService
.send({
to: "not-email",
channel: "email",
template: emailTemplate,
data: {
username: "john-doe",
},
})
.catch((e) => e)
expect(error.message).toEqual(
"Failed to send email: 400 - Does not contain a valid address."
)
})
})

View File

@@ -0,0 +1,16 @@
module.exports = {
globals: {
"ts-jest": {
tsconfig: "tsconfig.spec.json",
isolatedModules: false,
},
},
transform: {
"^.+\\.[jt]s?$": "ts-jest",
},
testEnvironment: `node`,
moduleNameMapper: {
"^axios$": "axios/dist/node/axios.cjs",
},
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
}

View File

@@ -0,0 +1,40 @@
{
"name": "@medusajs/notification-sendgrid",
"version": "0.0.1",
"description": "Sendgrid notification provider for Medusa",
"main": "dist/index.js",
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/modules/providers/notification-sendgrid"
},
"files": [
"dist"
],
"engines": {
"node": ">=16"
},
"author": "Medusa",
"license": "MIT",
"scripts": {
"prepublishOnly": "cross-env NODE_ENV=production tsc --build",
"test": "jest --passWithNoTests src",
"test:integration": "jest --forceExit -- integration-tests/**/__tests__/**/*.spec.ts",
"build": "rimraf dist && tsc -p ./tsconfig.json",
"watch": "tsc --watch"
},
"devDependencies": {
"cross-env": "^5.2.1",
"jest": "^25.5.4",
"rimraf": "^5.0.1",
"typescript": "^4.9.5"
},
"dependencies": {
"@medusajs/utils": "^1.11.7",
"@sendgrid/mail": "^8.1.3"
},
"keywords": [
"medusa-provider",
"medusa-provider-sendgrid"
]
}

View File

@@ -0,0 +1,10 @@
import { ModuleProviderExports } from "@medusajs/types"
import { SendgridNotificationService } from "./services/sendgrid"
const services = [SendgridNotificationService]
const providerExport: ModuleProviderExports = {
services,
}
export default providerExport

View File

@@ -0,0 +1,73 @@
import {
Logger,
NotificationTypes,
SendgridNotificationServiceOptions,
} from "@medusajs/types"
import {
AbstractNotificationProviderService,
MedusaError,
} from "@medusajs/utils"
import sendgrid from "@sendgrid/mail"
type InjectedDependencies = {
logger: Logger
}
interface SendgridServiceConfig {
apiKey: string
from: string
}
export class SendgridNotificationService extends AbstractNotificationProviderService {
protected config_: SendgridServiceConfig
protected logger_: Logger
constructor(
{ logger }: InjectedDependencies,
options: SendgridNotificationServiceOptions
) {
super()
this.config_ = {
apiKey: options.api_key,
from: options.from,
}
this.logger_ = logger
sendgrid.setApiKey(this.config_.apiKey)
}
async send(
notification: NotificationTypes.ProviderSendNotificationDTO
): Promise<NotificationTypes.ProviderSendNotificationResultsDTO> {
if (!notification) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`No notification information provided`
)
}
const message = {
to: notification.to,
from: this.config_.from,
templateId: notification.template,
dynamicTemplateData: notification.data as
| { [key: string]: any }
| undefined,
}
try {
// Unfortunately we don't get anything useful back in the response
await sendgrid.send(message)
return {}
} catch (error) {
const errorCode = error.code
const responseError = error.response?.body?.errors?.[0]
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
`Failed to send email: ${errorCode} - ${
responseError?.message ?? "unknown error"
}`
)
}
}
}

View File

@@ -0,0 +1,32 @@
{
"compilerOptions": {
"lib": ["es2020"],
"target": "es2020",
"jsx": "react-jsx" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
"outDir": "./dist",
"esModuleInterop": true,
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"noImplicitReturns": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"allowJs": true,
"skipLibCheck": true,
"downlevelIteration": true, // to use ES5 specific tooling
"inlineSourceMap": true /* Emit a single file with source maps instead of having a separate file. */
},
"include": ["src"],
"exclude": [
"dist",
"build",
"src/**/__tests__",
"src/**/__mocks__",
"src/**/__fixtures__",
"node_modules",
".eslintrc.js"
]
}

View File

@@ -0,0 +1,5 @@
{
"extends": "./tsconfig.json",
"include": ["src"],
"exclude": ["node_modules"]
}