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:
0
packages/modules/providers/notification-local/.gitignore
vendored
Normal file
0
packages/modules/providers/notification-local/.gitignore
vendored
Normal 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"}'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "@swc/jest",
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
|
||||
}
|
||||
39
packages/modules/providers/notification-local/package.json
Normal file
39
packages/modules/providers/notification-local/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
10
packages/modules/providers/notification-local/src/index.ts
Normal file
10
packages/modules/providers/notification-local/src/index.ts
Normal 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
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
32
packages/modules/providers/notification-local/tsconfig.json
Normal file
32
packages/modules/providers/notification-local/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
0
packages/modules/providers/notification-sendgrid/.gitignore
vendored
Normal file
0
packages/modules/providers/notification-sendgrid/.gitignore
vendored
Normal 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."
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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`],
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ModuleProviderExports } from "@medusajs/types"
|
||||
import { SendgridNotificationService } from "./services/sendgrid"
|
||||
|
||||
const services = [SendgridNotificationService]
|
||||
|
||||
const providerExport: ModuleProviderExports = {
|
||||
services,
|
||||
}
|
||||
|
||||
export default providerExport
|
||||
@@ -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"
|
||||
}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user