Skip to content

Commit

Permalink
feat: Add support for sendgrid and logger notification providers (#7290)
Browse files Browse the repository at this point in the history
* feat: Add support for sendgrid and logger notification providers

* fix: changes based on PR review
  • Loading branch information
sradevski committed May 11, 2024
1 parent 1a68f46 commit 79758c4
Show file tree
Hide file tree
Showing 24 changed files with 631 additions and 1 deletion.
7 changes: 7 additions & 0 deletions .changeset/perfect-fishes-listen.md
@@ -0,0 +1,7 @@
---
"@medusajs/notification-sendgrid": patch
"@medusajs/notification-logger": patch
"@medusajs/types": patch
---

Add sendgrid and logger notification providers
@@ -0,0 +1,128 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
CreateNotificationDTO,
INotificationModuleService,
Logger,
} from "@medusajs/types"
import { ContainerRegistrationKeys } from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "medusa-test-utils"

jest.setTimeout(50000)

const env = { MEDUSA_FF_MEDUSA_V2: true }
medusaIntegrationTestRunner({
env,
testSuite: ({ getContainer }) => {
describe("Notification module", () => {
let service: INotificationModuleService
let logger: Logger

beforeAll(async () => {
service = getContainer().resolve(ModuleRegistrationName.NOTIFICATION)
logger = getContainer().resolve(ContainerRegistrationKeys.LOGGER)
})

afterEach(() => {
jest.restoreAllMocks()
})

it("should successfully send a notification for an available channel", async () => {
const logSpy = jest.spyOn(logger, "info")
const notification = {
to: "[email protected]",
channel: "email",
template: "order-created",
data: { username: "john-doe" },
trigger_type: "order-created",
resource_id: "order-id",
resource_type: "order",
} as CreateNotificationDTO

const result = await service.create(notification)
const fromDB = await service.retrieve(result.id)

expect(result).toEqual(
expect.objectContaining({
id: expect.any(String),
to: "[email protected]",
provider_id: "local-notification-provider",
})
)

delete fromDB.original_notification_id
delete fromDB.external_id
delete fromDB.receiver_id
delete (fromDB as any).idempotency_key
delete (fromDB as any).provider

expect(result).toEqual(fromDB)
expect(logSpy).toHaveBeenCalledWith(
'Attempting to send a notification to: [email protected] on the channel: email with template: order-created and data: {"username":"john-doe"}'
)
})

it("should throw an exception if there is no provider for the channel", async () => {
const notification = {
to: "[email protected]",
channel: "sms",
} as CreateNotificationDTO

const error = await service.create(notification).catch((e) => e)
expect(error.message).toEqual(
"Could not find a notification provider for channel: sms"
)
})

it("should allow listing all notifications with filters", async () => {
const notification1 = {
to: "[email protected]",
channel: "email",
template: "order-created",
} as CreateNotificationDTO

const notification2 = {
to: "[email protected]",
channel: "log",
template: "product-created",
} as CreateNotificationDTO

await service.create([notification1, notification2])

const notifications = await service.list({ channel: "log" })
expect(notifications).toHaveLength(1)
expect(notifications[0]).toEqual(
expect.objectContaining({
to: "[email protected]",
channel: "log",
template: "product-created",
})
)
})

it("should allow retrieving a notification", async () => {
const notification1 = {
to: "[email protected]",
channel: "email",
template: "order-created",
} as CreateNotificationDTO

const notification2 = {
to: "[email protected]",
channel: "log",
template: "product-created",
} as CreateNotificationDTO

const [first] = await service.create([notification1, notification2])

const notification = await service.retrieve(first.id)
expect(notification).toEqual(
expect.objectContaining({
to: "[email protected]",
channel: "email",
template: "order-created",
})
)
})
})
},
})
18 changes: 18 additions & 0 deletions integration-tests/modules/medusa-config.js
Expand Up @@ -104,5 +104,23 @@ module.exports = {
providers: [customFulfillmentProvider],
},
},
[Modules.NOTIFICATION]: {
/** @type {import('@medusajs/types').LocalNotificationServiceOptions} */
options: {
providers: [
{
resolve: "@medusajs/notification-local",
options: {
config: {
"local-notification-provider": {
name: "Local Notification Provider",
channels: ["log", "email"],
},
},
},
},
],
},
},
},
}
3 changes: 2 additions & 1 deletion packages/core/types/src/notification/providers/index.ts
@@ -1 +1,2 @@
export * from "./local"
export * from "./logger"
export * from "./sendgrid"
4 changes: 4 additions & 0 deletions packages/core/types/src/notification/providers/sendgrid.ts
@@ -0,0 +1,4 @@
export interface SendgridNotificationServiceOptions {
api_key: string
from: string
}
Empty file.
Empty 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: "[email protected]",
channel: "email",
template: "some-template",
data: {
username: "john-doe",
},
})

expect(logSpy).toHaveBeenCalled()
expect(logSpy).toHaveBeenCalledWith(
'Attempting to send a notification to: [email protected] on the channel: email with template: some-template and data: {"username":"john-doe"}'
)
})
})
7 changes: 7 additions & 0 deletions packages/modules/providers/notification-local/jest.config.js
@@ -0,0 +1,7 @@
module.exports = {
transform: {
"^.+\\.[jt]s?$": "@swc/jest",
},
testEnvironment: `node`,
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
}
39 changes: 39 additions & 0 deletions packages/modules/providers/notification-local/package.json
@@ -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 changes: 10 additions & 0 deletions packages/modules/providers/notification-local/src/index.ts
@@ -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 changes: 32 additions & 0 deletions packages/modules/providers/notification-local/tsconfig.json
@@ -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"
]
}
Empty file.
Empty file.

0 comments on commit 79758c4

Please sign in to comment.