├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── index.ts ├── nest-cli.json ├── package-lock.json ├── package.json ├── src ├── channels │ ├── custom.channel.e2e.spec.ts │ ├── http │ │ ├── http-notification.interface.ts │ │ ├── http.channel.e2e.spec.ts │ │ ├── http.channel.spec.ts │ │ ├── http.channel.ts │ │ └── index.ts │ ├── index.ts │ ├── notification-channel.interface.ts │ └── sendgrid │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── interfaces │ │ ├── sendgrid-attachment.interface.ts │ │ ├── sendgrid-recipient.interface.ts │ │ └── sendgrid-request-body.interface.ts │ │ ├── sendgrid-notification.interface.ts │ │ ├── sendgrid.channel.e2e.spec.ts │ │ ├── sendgrid.channel.spec.ts │ │ └── sendgrid.channel.ts ├── constants.ts ├── index.ts ├── interfaces.ts ├── nestjs-notifications.module.ts ├── nestjs-notifications.service.spec.ts ├── nestjs-notifications.service.ts └── notification │ ├── notification.example.ts │ ├── notification.interface.ts │ └── notification.spec.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | index.js 2 | index.ts 3 | index.d.ts -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'prettier/@typescript-eslint', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ed Stephenson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NestJs Notifications 2 | 3 | NestJs Notifications is a flexible multi-channel notification service inspired by Laravel Notifications: https://github.com/illuminate/notifications 4 | 5 | This module is designed for sending short informational messages across a variety of delivery channels that notify users of something that occurred in your application. For example, if you are writing a billing application, you might send an "Invoice Paid" notification to your users via email and SMS channels. 6 | 7 | You can use pre-built delivery channels in this package, or you can create your own custom channels that can be easily integrated with this package. 8 | 9 | ## Installation 10 | 11 | ```bash 12 | $ npm install nestjs-notifications --save 13 | ``` 14 | 15 | ## Usage 16 | 17 | To make use of this package, you will need a Notification and at least one Channel. You can create your own custom channels, or you can use pre-built ones that come with this package. 18 | 19 | Example Channels and example Notifications are below. 20 | 21 | Once this has been done, you can be trigger notifications to be sent like so: 22 | 23 | ``` 24 | import { HttpService, Injectable } from '@nestjs/common'; 25 | import { NestJsNotificationsService } from 'nestjs-notifications'; 26 | import { ExampleNotification } from 'xxxx'; 27 | 28 | @Injectable() 29 | export class ExampleService { 30 | constructor(private readonly notifications: NestJsNotificationsService) {} 31 | 32 | /** 33 | * Send the given notification 34 | */ 35 | public async send(): Promise { 36 | const notification = new ExampleNotification(); 37 | this.notifications.send(notification); 38 | } 39 | } 40 | ``` 41 | 42 | ### Example Channel 43 | 44 | All channels are resolved inside the IOC container, so you can import services, such as the HttpService as you need them. 45 | 46 | ``` 47 | import { HttpService, Injectable } from '@nestjs/common'; 48 | import { NestJsNotificationChannel, NestJsNotification } from 'nestjs-notifications'; 49 | 50 | @Injectable() 51 | export class ExampleHttpChannel implements NestJsNotificationChannel { 52 | constructor(private readonly httpService: HttpService) {} 53 | 54 | /** 55 | * Send the given notification 56 | * @param notification 57 | */ 58 | public async send(notification: NestJsNotification): Promise { 59 | const data = this.getData(notification); 60 | await this.httpService.post(notification.httpUrl(), data).toPromise(); 61 | } 62 | 63 | /** 64 | * Get the data for the notification. 65 | * @param notification 66 | */ 67 | getData(notification: NestJsNotification) { 68 | return notification.toPayload(); 69 | } 70 | } 71 | ``` 72 | 73 | ### Example Notification 74 | 75 | You can specify as many channels as you want to in the `broadcastOn()` function. 76 | 77 | When constructing payloads, you can specify functions to create customised payloads for each channel, or fallback to the default `toPayload()` method. 78 | 79 | You can also pass any data you need into the constructor of the notification to pass to the payload constructors. 80 | 81 | ``` 82 | import { Type } from '@nestjs/common'; 83 | import { HttpChannel, NestJsNotificationChannel, NestJsNotification } from 'nestjs-notifications'; 84 | import { CustomChannel } from './src/your-project/custom-channel'; 85 | import { EmailChannel } from './src/your-project/email-channel'; 86 | 87 | export class ExampleNotification implements NestJsNotification { 88 | 89 | /** 90 | * Data passed into the notification to be used when 91 | * constructing the different payloads 92 | */ 93 | private data: any; 94 | 95 | constructor(data: any) { 96 | this.data = data; 97 | } 98 | 99 | /** 100 | * Get the channels the notification should broadcast on 101 | * @returns {Type[]} array 102 | */ 103 | public broadcastOn(): Type[] { 104 | return [ 105 | HttpChannel, 106 | CustomChannel, 107 | EmailChannel 108 | ]; 109 | } 110 | 111 | toHttp() { } 112 | 113 | toCustom() { } 114 | 115 | toEmail() { } 116 | 117 | /** 118 | * Get the json representation of the notification. 119 | * @returns {} 120 | */ 121 | toPayload(): any { 122 | return this.data; 123 | } 124 | } 125 | ``` 126 | 127 | ## Channels Included in this Package 128 | 129 | ### HttpChannel 130 | 131 | The HttpChannel is designed to post data to an external URL/webhook. In order to utilise the channel correctly, you need specify the `httpUrl()` function and return the URL. You can then choose between using the standard `toPayload()` method or `toHttp()` method to the payload specifically for this channel. 132 | 133 | You can implement the `HttpNotification` interface on your Notification Class to ensure you include the right methods. 134 | 135 | #### HttpChannel Class 136 | 137 | ``` 138 | import { 139 | HttpService, 140 | Injectable, 141 | InternalServerErrorException, 142 | } from '@nestjs/common'; 143 | import { HttpNotification, NestJsNotificationChannel } from 'nestjs-notifications'; 144 | 145 | @Injectable() 146 | export class HttpChannel implements NestJsNotificationChannel { 147 | 148 | constructor(private readonly httpService: HttpService) {} 149 | 150 | /** 151 | * Send the given notification 152 | * @param notification 153 | */ 154 | public async send(notification: HttpNotification): Promise { 155 | const message = this.getData(notification); 156 | await this.httpService.post(notification.httpUrl(), message).toPromise(); 157 | } 158 | 159 | /** 160 | * Get the data for the notification. 161 | * @param notification 162 | */ 163 | getData(notification: HttpNotification) { 164 | if (typeof notification.toHttp === 'function') { 165 | return notification.toHttp(); 166 | } 167 | 168 | if (typeof notification.toPayload === 'function') { 169 | return notification.toPayload(); 170 | } 171 | 172 | throw new InternalServerErrorException( 173 | 'Notification is missing toPayload method.', 174 | ); 175 | } 176 | } 177 | ``` 178 | 179 | #### HttpNotification Interface 180 | 181 | ``` 182 | import { NestJsNotification } from 'nestjs-notifications'; 183 | 184 | export interface HttpNotification extends NestJsNotification { 185 | /** 186 | * Define the Http url to send the notification to 187 | * @returns {string} 188 | */ 189 | httpUrl(): string; 190 | 191 | /** 192 | * Get the Http representation of the notification. 193 | * @returns {any} http payload data 194 | */ 195 | toHttp?(): any; 196 | } 197 | ``` 198 | 199 | ## Test 200 | 201 | ```bash 202 | # unit tests 203 | $ npm run test 204 | 205 | # e2e tests 206 | $ npm run test:e2e 207 | 208 | # test coverage 209 | $ npm run test:cov 210 | ``` 211 | 212 | ## Collaborating 213 | 214 | Would appreciate any support anyone may wish to offer. Please get in contact. 215 | 216 | ## License 217 | 218 | NestJs Notifications is [MIT licensed](LICENSE). 219 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist'; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function __export(m) { 3 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; 4 | } 5 | exports.__esModule = true; 6 | __export(require("./dist")); -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './dist'; 2 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-notifications", 3 | "version": "0.1.17", 4 | "description": "NestJs Notifications is a flexible multi-channel notification service inspired by Laravel Notifications designed for sending notifications across a variety of delivery channels.", 5 | "author": "Ed Stephenson", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/edstevo/nestjs-notifications" 10 | }, 11 | "keywords": [ 12 | "nest", 13 | "nestjs", 14 | "notifications" 15 | ], 16 | "files": [ 17 | "dist", 18 | "index.js", 19 | "index.d.ts" 20 | ], 21 | "main": "dist/index.js", 22 | "scripts": { 23 | "prebuild": "rimraf dist", 24 | "fix": "rm -rf node_modules && rm package-lock.json && npm install", 25 | "build": "rimraf -rf dist && tsc -p tsconfig.json", 26 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 27 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 28 | "test": "jest --force-exit --runInBand", 29 | "test:watch": "jest --watch", 30 | "test:cov": "jest --coverage", 31 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 32 | "test:e2e": "jest --config ./test/jest-e2e.json", 33 | "prepub": "npm ci && npm run build", 34 | "pub": "np patch --no-yarn" 35 | }, 36 | "peerDependencies": { 37 | "@nestjs/common": ">=7.5.1", 38 | "@nestjs/core": ">=7.5.1" 39 | }, 40 | "dependencies": { 41 | "@nestjs/common": "^7.5.1", 42 | "@nestjs/core": "^7.5.1", 43 | "@nestjs/platform-express": "^7.5.1", 44 | "@types/axios": "^0.14.0", 45 | "reflect-metadata": "^0.1.13", 46 | "rimraf": "^3.0.2", 47 | "rxjs": "^6.6.3" 48 | }, 49 | "devDependencies": { 50 | "@nestjs/bull": "^0.3.1", 51 | "@nestjs/cli": "^7.5.1", 52 | "@nestjs/schematics": "^7.1.3", 53 | "@nestjs/testing": "^7.5.1", 54 | "@types/bull": "^3.15.0", 55 | "@types/express": "^4.17.8", 56 | "@types/jest": "^26.0.15", 57 | "@types/node": "^14.14.6", 58 | "@types/supertest": "^2.0.10", 59 | "@typescript-eslint/eslint-plugin": "^4.6.1", 60 | "@typescript-eslint/parser": "^4.6.1", 61 | "bull": "^3.22.0", 62 | "eslint": "^7.12.1", 63 | "eslint-config-prettier": "7.2.0", 64 | "eslint-plugin-prettier": "^3.1.4", 65 | "jest": "^26.6.3", 66 | "prettier": "^2.1.2", 67 | "supertest": "^6.0.0", 68 | "ts-jest": "^26.4.3", 69 | "ts-loader": "^8.0.8", 70 | "ts-node": "^9.0.0", 71 | "tsconfig-paths": "^3.9.0", 72 | "typescript": "^4.0.5" 73 | }, 74 | "jest": { 75 | "moduleFileExtensions": [ 76 | "js", 77 | "json", 78 | "ts" 79 | ], 80 | "rootDir": "src", 81 | "testRegex": ".*\\.spec\\.ts$", 82 | "transform": { 83 | "^.+\\.(t|j)s$": "ts-jest" 84 | }, 85 | "collectCoverageFrom": [ 86 | "**/*.(t|j)s" 87 | ], 88 | "coverageDirectory": "../coverage", 89 | "testEnvironment": "node" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/channels/custom.channel.e2e.spec.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { NestJsNotificationsModule } from '../nestjs-notifications.module'; 4 | import { NestJsNotification } from '../notification/notification.interface'; 5 | import { NestJsNotificationsService } from '../nestjs-notifications.service'; 6 | import { NestJsNotificationChannel } from './notification-channel.interface'; 7 | 8 | const testSendFn = jest.fn().mockImplementation(() => Promise.resolve()); 9 | 10 | class CustomChannel implements NestJsNotificationChannel { 11 | send = testSendFn; 12 | } 13 | 14 | class CustomNotification implements NestJsNotification { 15 | 16 | /** 17 | * Get the channels the notification should broadcast on. 18 | * @returns {Type[]} array 19 | */ 20 | public broadcastOn(): Type[] { 21 | return [CustomChannel]; 22 | } 23 | } 24 | 25 | describe('CustomChannel E2E', () => { 26 | let module: TestingModule; 27 | let service: NestJsNotificationsService; 28 | let notification: CustomNotification; 29 | 30 | beforeEach(async () => { 31 | module = await Test.createTestingModule({ 32 | imports: [NestJsNotificationsModule.forRoot({})], 33 | }).compile(); 34 | 35 | service = module.get(NestJsNotificationsService); 36 | notification = new CustomNotification(); 37 | }); 38 | 39 | describe('Send Notification Via Webhook Channel', () => { 40 | it('should post webhook correctly', async () => { 41 | await service.send(notification); 42 | expect(testSendFn).toHaveBeenCalled(); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/channels/http/http-notification.interface.ts: -------------------------------------------------------------------------------- 1 | import { NestJsNotification } from '../../notification/notification.interface'; 2 | 3 | export interface HttpNotification extends NestJsNotification { 4 | /** 5 | * Define the Http url to send the notification to 6 | * @returns {string} 7 | */ 8 | httpUrl(): string; 9 | 10 | /** 11 | * Get the Http representation of the notification. 12 | * @returns {any} http payload data 13 | */ 14 | toHttp?(): any; 15 | } 16 | -------------------------------------------------------------------------------- /src/channels/http/http.channel.e2e.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule, HttpService, Type } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { HttpChannel } from './http.channel'; 4 | import { NestJsNotificationsService } from '../../nestjs-notifications.service'; 5 | import { NestJsNotificationsModule } from '../../nestjs-notifications.module'; 6 | import { HttpNotification } from './http-notification.interface'; 7 | import { of } from 'rxjs'; 8 | import { NestJsNotificationChannel } from '../notification-channel.interface'; 9 | 10 | const testUrl = 'testUrl'; 11 | const testData = { test: true }; 12 | 13 | class TestNotification implements HttpNotification { 14 | data: any; 15 | 16 | constructor(data: any) { 17 | this.data = data; 18 | } 19 | 20 | /** 21 | * Get the channels the notification should broadcast on. 22 | * @returns {Type[]} array 23 | */ 24 | public broadcastOn(): Type[] { 25 | return [HttpChannel]; 26 | } 27 | 28 | httpUrl(): string { 29 | return testUrl; 30 | } 31 | 32 | toHttp() { 33 | return this.data; 34 | } 35 | } 36 | 37 | describe('HttpChannel E2E', () => { 38 | let module: TestingModule; 39 | let service: NestJsNotificationsService; 40 | let httpService: HttpService; 41 | let notification: TestNotification; 42 | 43 | beforeEach(async () => { 44 | module = await Test.createTestingModule({ 45 | imports: [ 46 | NestJsNotificationsModule.forRootAsync({ 47 | imports: [HttpModule] 48 | }) 49 | ] 50 | }).compile(); 51 | 52 | service = module.get(NestJsNotificationsService); 53 | httpService = module.get(HttpService); 54 | notification = new TestNotification(testData); 55 | }); 56 | 57 | describe('Send Notification Via Http Channel', () => { 58 | it('should post Http correctly', async () => { 59 | jest.spyOn(httpService, 'post').mockImplementation(() => of(null)); 60 | 61 | await service.send(notification); 62 | expect(httpService.post).toHaveBeenCalledWith(testUrl, testData); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/channels/http/http.channel.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule, HttpService, Type } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { HttpChannel } from './http.channel'; 4 | import { HttpNotification } from './http-notification.interface'; 5 | import { of } from 'rxjs'; 6 | import { NestJsNotificationChannel } from '../notification-channel.interface'; 7 | 8 | const testUrl = 'testUrl'; 9 | const testToHttpData = { test: 'toHttp' }; 10 | const testToPayloadData = { test: 'toPayload' }; 11 | 12 | class TestNotification implements HttpNotification { 13 | public broadcastOn(): any[] { 14 | return [HttpChannel]; 15 | } 16 | 17 | public httpUrl(): string { 18 | return testUrl; 19 | } 20 | 21 | public toHttp() { 22 | return testToHttpData; 23 | } 24 | 25 | public toPayload() { 26 | return testToPayloadData; 27 | } 28 | } 29 | 30 | class TestToPayloadNotification implements HttpNotification { 31 | httpUrl(): string { 32 | return testUrl; 33 | } 34 | toPayload?(): Record { 35 | return testToPayloadData; 36 | } 37 | public broadcastOn(): Type[] { 38 | return [HttpChannel]; 39 | } 40 | } 41 | 42 | class TestToHttpNotification implements HttpNotification { 43 | httpUrl(): string { 44 | return testUrl; 45 | } 46 | toHttp(): Record { 47 | return testToHttpData; 48 | } 49 | public broadcastOn(): Type[] { 50 | return [HttpChannel]; 51 | } 52 | } 53 | 54 | describe('HttpChannel', () => { 55 | let service: HttpChannel; 56 | let httpService: HttpService; 57 | let testNotification: TestNotification; 58 | 59 | beforeEach(async () => { 60 | const module: TestingModule = await Test.createTestingModule({ 61 | imports: [HttpModule], 62 | providers: [HttpChannel], 63 | }).compile(); 64 | 65 | service = module.get(HttpChannel); 66 | httpService = module.get(HttpService); 67 | testNotification = new TestNotification(); 68 | }); 69 | 70 | it('should be defined', () => { 71 | expect(service).toBeDefined(); 72 | }); 73 | 74 | describe('getData', () => { 75 | it('should return toHttp data when only toHttp present', () => { 76 | const notification = new TestToHttpNotification(); 77 | const res = service.getData(notification); 78 | expect(res).toEqual(testToHttpData); 79 | }); 80 | 81 | it('should return toHttp data when not only toHttp present', () => { 82 | const res = service.getData(testNotification); 83 | expect(res).toEqual(testToHttpData); 84 | }); 85 | 86 | it('should return toPayload data when toHttp not present', () => { 87 | const notification = new TestToPayloadNotification(); 88 | const res = service.getData(notification); 89 | expect(res).toEqual(testToPayloadData); 90 | }); 91 | }); 92 | 93 | describe('send', () => { 94 | it('should post data correctly', async () => { 95 | const testGetData = { getData: true }; 96 | service.getData = jest.fn().mockImplementation(() => testGetData); 97 | 98 | jest 99 | .spyOn(httpService, 'post') 100 | .mockImplementation(() => of({ test: true } as any)); 101 | 102 | await service.send(testNotification); 103 | expect(httpService.post).toHaveBeenCalledWith(testUrl, testGetData); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /src/channels/http/http.channel.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpService, 3 | Injectable, 4 | InternalServerErrorException, 5 | } from '@nestjs/common'; 6 | import { HttpNotification } from './http-notification.interface'; 7 | import { NestJsNotificationChannel } from '../notification-channel.interface'; 8 | import { AxiosResponse } from 'axios'; 9 | 10 | @Injectable() 11 | export class HttpChannel implements NestJsNotificationChannel { 12 | constructor(private readonly httpService: HttpService) {} 13 | 14 | /** 15 | * Send the given notification 16 | * @param notification 17 | */ 18 | public async send( 19 | notification: HttpNotification, 20 | ): Promise> { 21 | const message = this.getData(notification); 22 | return this.httpService.post(notification.httpUrl(), message).toPromise(); 23 | } 24 | 25 | /** 26 | * Get the data for the notification. 27 | * @param notification 28 | */ 29 | getData(notification: HttpNotification) { 30 | if (typeof notification.toHttp === 'function') { 31 | return notification.toHttp(); 32 | } 33 | 34 | if (typeof notification.toPayload === 'function') { 35 | return notification.toPayload(); 36 | } 37 | 38 | throw new InternalServerErrorException( 39 | 'Notification is missing toPayload method.', 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/channels/http/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http-notification.interface'; 2 | export * from './http.channel'; -------------------------------------------------------------------------------- /src/channels/index.ts: -------------------------------------------------------------------------------- 1 | export * from './notification-channel.interface'; 2 | export * from './http'; 3 | export * from './sendgrid'; -------------------------------------------------------------------------------- /src/channels/notification-channel.interface.ts: -------------------------------------------------------------------------------- 1 | import { NestJsNotification } from '../notification/notification.interface'; 2 | 3 | export interface NestJsNotificationChannel { 4 | /** 5 | * Send the given notification 6 | * @param notification 7 | */ 8 | send(notification: NestJsNotification): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /src/channels/sendgrid/constants.ts: -------------------------------------------------------------------------------- 1 | export const SendGridApiUrl = 'https://api.sendgrid.com/v3/mail/send'; 2 | -------------------------------------------------------------------------------- /src/channels/sendgrid/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sendgrid-notification.interface'; 2 | export * from './sendgrid.channel'; 3 | 4 | export * from './interfaces/sendgrid-request-body.interface'; 5 | export * from './interfaces/sendgrid-attachment.interface'; 6 | export * from './interfaces/sendgrid-recipient.interface'; -------------------------------------------------------------------------------- /src/channels/sendgrid/interfaces/sendgrid-attachment.interface.ts: -------------------------------------------------------------------------------- 1 | export interface SendGridAttachment { 2 | content: string; 3 | type?: string; 4 | filename: string; 5 | disposition?: string; 6 | content_id?: string; 7 | } -------------------------------------------------------------------------------- /src/channels/sendgrid/interfaces/sendgrid-recipient.interface.ts: -------------------------------------------------------------------------------- 1 | export interface SendGridRecipient { 2 | email: string; 3 | name?: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/channels/sendgrid/interfaces/sendgrid-request-body.interface.ts: -------------------------------------------------------------------------------- 1 | import { SendGridAttachment } from './sendgrid-attachment.interface'; 2 | import { SendGridRecipient } from './sendgrid-recipient.interface'; 3 | 4 | export interface SendGridRequestBody { 5 | personalizations: [ 6 | { 7 | from?: SendGridRecipient | string; 8 | to: SendGridRecipient[] | string[]; 9 | cc?: SendGridRecipient[] | string[]; 10 | bcc?: SendGridRecipient[] | string[]; 11 | subject?: string; 12 | headers?: Record; 13 | substitutions?: Record; 14 | dynamic_template_data?: Record; 15 | custom_args?: Record; 16 | send_at?: number; 17 | }, 18 | ]; 19 | from: SendGridRecipient | string; 20 | reply_to?: SendGridRecipient | string; 21 | subject: string; 22 | content: { 23 | type: string; 24 | value: string; 25 | }[]; 26 | attachments?: SendGridAttachment[]; 27 | template_id?: string; 28 | headers?: Record; 29 | categories?: string[]; 30 | custom_args?: string; 31 | send_at?: number; 32 | batch_id?: string; 33 | asm?: { 34 | group_id: number; 35 | groups_to_display?: number[]; 36 | }; 37 | ip_pool_name?: string; 38 | mail_settings?: { 39 | bypass_list_management?: { 40 | enable?: boolean; 41 | }; 42 | bypass_spam_management?: { 43 | enable?: boolean; 44 | }; 45 | bypass_bounce_management?: { 46 | enable?: boolean; 47 | }; 48 | bypass_unsubscribe_management?: { 49 | enable?: boolean; 50 | }; 51 | footer?: { 52 | enable?: boolean; 53 | text?: string; 54 | html?: string; 55 | }; 56 | sandbox_mode?: { 57 | enable?: boolean; 58 | }; 59 | spam_check?: { 60 | enable?: boolean; 61 | threshold?: number; 62 | post_to_url?: string; 63 | }; 64 | }; 65 | tracking_settings?: { 66 | click_tracking?: { 67 | enable?: boolean; 68 | enable_text?: string; 69 | }; 70 | open_tracking?: { 71 | enable?: boolean; 72 | substitution_tag?: string; 73 | }; 74 | subscription_tracking?: { 75 | enable?: boolean; 76 | text?: string; 77 | html?: string; 78 | substitution_tag?: string; 79 | }; 80 | ganalytics?: { 81 | enable?: boolean; 82 | utm_source?: string; 83 | utm_medium?: string; 84 | utm_term?: string; 85 | utm_content?: string; 86 | utm_campaign?: string; 87 | }; 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /src/channels/sendgrid/sendgrid-notification.interface.ts: -------------------------------------------------------------------------------- 1 | import { NestJsNotification } from '../../notification/notification.interface'; 2 | import { SendGridRequestBody } from './interfaces/sendgrid-request-body.interface'; 3 | 4 | export interface SendGridNotification extends NestJsNotification { 5 | /** 6 | * Define the your api key 7 | * @returns {string} 8 | */ 9 | sendGridApiKey(): string; 10 | 11 | /** 12 | * Get the SendGrid representation of the notification. 13 | * @returns {SendGridRequestBody} 14 | */ 15 | toSendGrid?(): SendGridRequestBody; 16 | } 17 | -------------------------------------------------------------------------------- /src/channels/sendgrid/sendgrid.channel.e2e.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule, HttpService, Type } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { SendGridChannel } from './sendgrid.channel'; 4 | import { NestJsNotificationsService } from '../../nestjs-notifications.service'; 5 | import { NestJsNotificationsModule } from '../../nestjs-notifications.module'; 6 | import { SendGridNotification } from './sendgrid-notification.interface'; 7 | import { of } from 'rxjs'; 8 | import { NestJsNotificationChannel } from '../notification-channel.interface'; 9 | import { SendGridApiUrl } from './constants'; 10 | import { NestJsNotificationsModuleOptions } from '../../interfaces'; 11 | 12 | const testApiKey = 'testApiKey'; 13 | const testData = { test: true }; 14 | 15 | class TestNotification implements SendGridNotification { 16 | data: any; 17 | 18 | constructor(data: any) { 19 | this.data = data; 20 | } 21 | 22 | /** 23 | * Get the channels the notification should broadcast on. 24 | * @returns {Type[]} array 25 | */ 26 | public broadcastOn(): Type[] { 27 | return [SendGridChannel]; 28 | } 29 | 30 | sendGridApiKey(): string { 31 | return testApiKey; 32 | } 33 | 34 | toSendGrid() { 35 | return this.data; 36 | } 37 | } 38 | 39 | describe('SendGridChannel E2E', () => { 40 | let module: TestingModule; 41 | let service: NestJsNotificationsService; 42 | let httpService: HttpService; 43 | let notification: TestNotification; 44 | 45 | beforeEach(async () => { 46 | module = await Test.createTestingModule({ 47 | imports: [ 48 | NestJsNotificationsModule.forRootAsync({ 49 | imports: [HttpModule] 50 | }) 51 | ] 52 | }).compile(); 53 | 54 | service = module.get(NestJsNotificationsService); 55 | httpService = module.get(HttpService); 56 | notification = new TestNotification(testData); 57 | }); 58 | 59 | describe('Send Notification Via Http Channel', () => { 60 | it('should post Http correctly', async () => { 61 | jest.spyOn(httpService, 'post').mockImplementation(() => of(null)); 62 | 63 | await service.send(notification); 64 | expect(httpService.post).toHaveBeenCalledWith(SendGridApiUrl, testData, { 65 | headers: { 66 | Authorization: testApiKey 67 | } 68 | }); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/channels/sendgrid/sendgrid.channel.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule, HttpService, Type } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { SendGridChannel } from './sendgrid.channel'; 4 | import { SendGridNotification } from './sendgrid-notification.interface'; 5 | import { of } from 'rxjs'; 6 | import { NestJsNotificationChannel } from '../notification-channel.interface'; 7 | import { SendGridApiUrl } from './constants'; 8 | import { SendGridRequestBody } from './interfaces/sendgrid-request-body.interface'; 9 | 10 | const testApiKey = 'testUrl'; 11 | const testToSendGridData: SendGridRequestBody = { 12 | personalizations: [ 13 | { 14 | to: ['test@email.com'] 15 | } 16 | ], 17 | from: 'from@email.com', 18 | subject: "Test Email", 19 | content: [ 20 | { 21 | type: 'text/plain', 22 | value: 'Some content', 23 | } 24 | ] 25 | }; 26 | const testToPayloadData = { test: 'toPayload' }; 27 | 28 | class TestNotification implements SendGridNotification { 29 | public broadcastOn(): any[] { 30 | return [SendGridChannel]; 31 | } 32 | 33 | public sendGridApiKey(): string { 34 | return testApiKey; 35 | } 36 | 37 | public toSendGrid() { 38 | return testToSendGridData; 39 | } 40 | 41 | public toPayload() { 42 | return testToPayloadData; 43 | } 44 | } 45 | 46 | class TestToPayloadNotification implements SendGridNotification { 47 | sendGridApiKey(): string { 48 | return testApiKey; 49 | } 50 | toPayload?(): any { 51 | return testToPayloadData; 52 | } 53 | public broadcastOn(): Type[] { 54 | return [SendGridChannel]; 55 | } 56 | } 57 | 58 | class TestToSendGridNotification implements SendGridNotification { 59 | sendGridApiKey(): string { 60 | return testApiKey; 61 | } 62 | toSendGrid(): SendGridRequestBody { 63 | return testToSendGridData; 64 | } 65 | public broadcastOn(): Type[] { 66 | return [SendGridChannel]; 67 | } 68 | } 69 | 70 | describe('SendGridChannel', () => { 71 | let service: SendGridChannel; 72 | let httpService: HttpService; 73 | let testNotification: TestNotification; 74 | 75 | beforeEach(async () => { 76 | const module: TestingModule = await Test.createTestingModule({ 77 | imports: [HttpModule], 78 | providers: [SendGridChannel], 79 | }).compile(); 80 | 81 | service = module.get(SendGridChannel); 82 | httpService = module.get(HttpService); 83 | testNotification = new TestNotification(); 84 | }); 85 | 86 | it('should be defined', () => { 87 | expect(service).toBeDefined(); 88 | }); 89 | 90 | describe('getData', () => { 91 | it('should return toSendGrid data when only toSendGrid present', () => { 92 | const notification = new TestToSendGridNotification(); 93 | const res = service.getData(notification); 94 | expect(res).toEqual(testToSendGridData); 95 | }); 96 | 97 | it('should return toSendGrid data when not only toSendGrid present', () => { 98 | const res = service.getData(testNotification); 99 | expect(res).toEqual(testToSendGridData); 100 | }); 101 | 102 | it('should return toPayload data when toSendGrid not present', () => { 103 | const notification = new TestToPayloadNotification(); 104 | const res = service.getData(notification); 105 | expect(res).toEqual(testToPayloadData); 106 | }); 107 | }); 108 | 109 | describe('send', () => { 110 | it('should post data correctly', async () => { 111 | const testGetData = { getData: true }; 112 | service.getData = jest.fn().mockImplementation(() => testGetData); 113 | 114 | jest 115 | .spyOn(httpService, 'post') 116 | .mockImplementation(() => of({ test: true } as any)); 117 | 118 | await service.send(testNotification); 119 | expect(httpService.post).toHaveBeenCalledWith(SendGridApiUrl, testGetData, { 120 | headers: { 121 | Authorization: testApiKey, 122 | } 123 | }); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /src/channels/sendgrid/sendgrid.channel.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpService, 3 | Injectable, 4 | InternalServerErrorException, 5 | } from '@nestjs/common'; 6 | import { SendGridNotification } from './sendgrid-notification.interface'; 7 | import { NestJsNotificationChannel } from '../notification-channel.interface'; 8 | import { SendGridApiUrl } from './constants'; 9 | import { AxiosResponse } from 'axios'; 10 | 11 | @Injectable() 12 | export class SendGridChannel implements NestJsNotificationChannel { 13 | constructor(private readonly httpService: HttpService) {} 14 | 15 | /** 16 | * Send the given notification 17 | * @param notification 18 | */ 19 | public async send( 20 | notification: SendGridNotification, 21 | ): Promise> { 22 | const data = this.getData(notification); 23 | return this.httpService 24 | .post(SendGridApiUrl, data, { 25 | headers: { 26 | Authorization: notification.sendGridApiKey(), 27 | }, 28 | }) 29 | .toPromise(); 30 | } 31 | 32 | /** 33 | * Get the data for the notification. 34 | * @param notification 35 | */ 36 | getData(notification: SendGridNotification) { 37 | if (typeof notification.toSendGrid === 'function') { 38 | return notification.toSendGrid(); 39 | } 40 | 41 | if (typeof notification.toPayload === 'function') { 42 | return notification.toPayload(); 43 | } 44 | 45 | throw new InternalServerErrorException( 46 | 'Notification is missing toPayload method.', 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const NESTJS_NOTIFICATIONS_OPTIONS = 'NESTJS_NOTIFICATIONS_OPTIONS'; 2 | export const NESTJS_NOTIFICATIONS_QUEUE = 'NESTJS_NOTIFICATIONS_QUEUE'; 3 | export const NESTJS_NOTIFICATIONS_JOB_OPTIONS = 'NESTJS_NOTIFICATIONS_JOB_OPTIONS'; 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nestjs-notifications.module'; 2 | export * from './nestjs-notifications.service'; 3 | export * from './notification/notification.interface'; 4 | export * from './channels'; 5 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { ModuleMetadata, Type } from '@nestjs/common'; 2 | import { JobOptions, Queue } from 'bull'; 3 | 4 | export type NestJsNotificationsModuleOptions = { 5 | queue?: Queue; 6 | defaultJobOptions?: JobOptions; 7 | }; 8 | 9 | export interface NestJsNotificationsModuleOptionsFactory { 10 | createNestJsNotificationsModuleOptions(): 11 | | Promise 12 | | NestJsNotificationsModuleOptions; 13 | } 14 | 15 | export interface NestJsNotificationsModuleAsyncOptions 16 | extends Pick { 17 | inject?: any[]; 18 | useExisting?: Type; 19 | useClass?: Type; 20 | useFactory?: ( 21 | ...args: any[] 22 | ) => 23 | | Promise 24 | | NestJsNotificationsModuleOptions; 25 | } 26 | -------------------------------------------------------------------------------- /src/nestjs-notifications.module.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@nestjs/common'; 2 | import { DynamicModule, Global, Module, ValueProvider } from '@nestjs/common'; 3 | import { JobOptions, Queue } from 'bull'; 4 | import { 5 | NESTJS_NOTIFICATIONS_JOB_OPTIONS, 6 | NESTJS_NOTIFICATIONS_OPTIONS, 7 | NESTJS_NOTIFICATIONS_QUEUE, 8 | } from './constants'; 9 | import { 10 | NestJsNotificationsModuleAsyncOptions, 11 | NestJsNotificationsModuleOptions, 12 | NestJsNotificationsModuleOptionsFactory, 13 | } from './interfaces'; 14 | import { NestJsNotificationsService } from './nestjs-notifications.service'; 15 | 16 | @Global() 17 | @Module({}) 18 | export class NestJsNotificationsModule { 19 | public static forRoot( 20 | options: NestJsNotificationsModuleOptions, 21 | ): DynamicModule { 22 | const queueProvider: ValueProvider = { 23 | provide: NESTJS_NOTIFICATIONS_QUEUE, 24 | useValue: options.queue ? options.queue : null, 25 | }; 26 | 27 | const jobOptionsProvider: ValueProvider = { 28 | provide: NESTJS_NOTIFICATIONS_JOB_OPTIONS, 29 | useValue: options.defaultJobOptions ? options.defaultJobOptions : {}, 30 | }; 31 | 32 | return { 33 | global: true, 34 | module: NestJsNotificationsModule, 35 | providers: [ 36 | queueProvider, 37 | jobOptionsProvider, 38 | NestJsNotificationsService, 39 | ], 40 | exports: [ 41 | NESTJS_NOTIFICATIONS_QUEUE, 42 | NESTJS_NOTIFICATIONS_JOB_OPTIONS, 43 | NestJsNotificationsService, 44 | ], 45 | }; 46 | } 47 | 48 | public static forRootAsync( 49 | asyncOptions: NestJsNotificationsModuleAsyncOptions, 50 | ): DynamicModule { 51 | return { 52 | global: true, 53 | module: NestJsNotificationsModule, 54 | imports: asyncOptions.imports || [], 55 | providers: [ 56 | ...this.createAsyncProviders(asyncOptions), 57 | ...this.createQueueProvider(), 58 | NestJsNotificationsService, 59 | ], 60 | exports: [ 61 | NESTJS_NOTIFICATIONS_QUEUE, 62 | NESTJS_NOTIFICATIONS_JOB_OPTIONS, 63 | NestJsNotificationsService, 64 | ], 65 | }; 66 | } 67 | 68 | private static createQueueProvider(): Provider[] { 69 | return [ 70 | { 71 | provide: NESTJS_NOTIFICATIONS_QUEUE, 72 | inject: [NESTJS_NOTIFICATIONS_OPTIONS], 73 | useFactory(options: NestJsNotificationsModuleOptions): Queue { 74 | return options.queue ? options.queue : null; 75 | }, 76 | }, 77 | { 78 | provide: NESTJS_NOTIFICATIONS_JOB_OPTIONS, 79 | inject: [NESTJS_NOTIFICATIONS_OPTIONS], 80 | useFactory(options: NestJsNotificationsModuleOptions): JobOptions { 81 | return options.defaultJobOptions ? options.defaultJobOptions : {}; 82 | }, 83 | }, 84 | ]; 85 | } 86 | 87 | static createAsyncProviders( 88 | options: NestJsNotificationsModuleAsyncOptions, 89 | ): Provider[] { 90 | if (options.useExisting || options.useFactory) { 91 | return [this.createAsyncOptionsProvider(options)]; 92 | } else if (!options.useClass) { 93 | return [ 94 | { 95 | provide: NESTJS_NOTIFICATIONS_OPTIONS, 96 | useValue: {}, 97 | inject: options.inject || [], 98 | }, 99 | ]; 100 | } 101 | 102 | return [ 103 | this.createAsyncOptionsProvider(options), 104 | { 105 | provide: options.useClass, 106 | useClass: options.useClass, 107 | }, 108 | ]; 109 | } 110 | 111 | static createAsyncOptionsProvider( 112 | options: NestJsNotificationsModuleAsyncOptions, 113 | ): Provider { 114 | if (options.useFactory) { 115 | return { 116 | provide: NESTJS_NOTIFICATIONS_OPTIONS, 117 | useFactory: options.useFactory, 118 | inject: options.inject || [], 119 | }; 120 | } 121 | 122 | const inject = options.useClass || options.useExisting; 123 | 124 | if (!inject) { 125 | throw new Error( 126 | 'Invalid configuration. Must provide useFactory, useClass or useExisting', 127 | ); 128 | } 129 | 130 | return { 131 | provide: NESTJS_NOTIFICATIONS_OPTIONS, 132 | async useFactory( 133 | optionsFactory: NestJsNotificationsModuleOptionsFactory, 134 | ): Promise { 135 | const opts = await optionsFactory.createNestJsNotificationsModuleOptions(); 136 | return opts; 137 | }, 138 | inject: [inject], 139 | }; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/nestjs-notifications.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { ModuleRef } from '@nestjs/core'; 3 | import { Test, TestingModule } from '@nestjs/testing'; 4 | import { NestJsNotificationChannel } from './channels/notification-channel.interface'; 5 | import { HttpChannel } from './channels/http/http.channel'; 6 | import { NestJsNotification, NestJsQueuedNotification } from './notification/notification.interface'; 7 | import { NestJsNotificationsService } from './nestjs-notifications.service'; 8 | import { NestJsNotificationsModule } from './nestjs-notifications.module'; 9 | import { BullModule, getQueueToken, InjectQueue } from '@nestjs/bull'; 10 | import { Queue } from 'bull'; 11 | import { NestJsNotificationsModuleOptions, NestJsNotificationsModuleOptionsFactory } from './interfaces'; 12 | 13 | jest.mock('./channels/http/http.channel'); 14 | 15 | class TestNotification implements NestJsNotification, NestJsQueuedNotification { 16 | public broadcastOn(): Type[] { 17 | return [HttpChannel]; 18 | } 19 | 20 | getJobOptions() { return null } 21 | } 22 | 23 | export class NestJsNotificationConfigService 24 | implements NestJsNotificationsModuleOptionsFactory { 25 | constructor( 26 | @InjectQueue('notifications_queue') 27 | private readonly notificationsQueue: Queue, 28 | ) {} 29 | 30 | createNestJsNotificationsModuleOptions = (): NestJsNotificationsModuleOptions => { 31 | return { 32 | queue: this.notificationsQueue, 33 | defaultJobOptions: { 34 | attempts: 5, 35 | backoff: { 36 | type: 'exponential', 37 | delay: 1000, 38 | }, 39 | }, 40 | } 41 | }; 42 | } 43 | 44 | describe('NestJsNotificationsService', () => { 45 | let module: TestingModule; 46 | let moduleRef: ModuleRef; 47 | let service: NestJsNotificationsService; 48 | let notification: TestNotification; 49 | 50 | beforeEach(async () => { 51 | module = await Test.createTestingModule({ 52 | imports: [NestJsNotificationsModule.forRoot({})] 53 | }).compile(); 54 | 55 | service = module.get(NestJsNotificationsService); 56 | moduleRef = module.get(ModuleRef); 57 | notification = new TestNotification(); 58 | }); 59 | 60 | it('should be defined', () => { 61 | expect(service).toBeDefined(); 62 | }); 63 | 64 | describe('send', () => { 65 | it('should call sendOnChannel correctly', async () => { 66 | service.sendOnChannel = jest.fn(); 67 | 68 | await service.send(notification); 69 | 70 | expect(service.sendOnChannel).toBeCalledWith(notification, HttpChannel); 71 | }); 72 | }); 73 | 74 | describe('sendOnChannel', () => { 75 | it('should send via channel correctly', async () => { 76 | const channelService = ((await moduleRef.create( 77 | HttpChannel, 78 | )) as unknown) as jest.Mocked; 79 | 80 | service.resolveChannel = jest 81 | .fn() 82 | .mockImplementation(() => channelService); 83 | 84 | await service.sendOnChannel(notification, HttpChannel); 85 | expect(channelService.send).toHaveBeenCalledWith(notification); 86 | }); 87 | }); 88 | 89 | describe('resolveChannel', () => { 90 | it('should resolve channel correctly', async () => { 91 | const res = await service.resolveChannel(HttpChannel); 92 | expect(res).toBeInstanceOf(HttpChannel); 93 | }); 94 | }); 95 | 96 | describe('queuing',()=>{ 97 | let queue: Queue; 98 | 99 | beforeEach(async () => { 100 | module = await Test.createTestingModule({ 101 | imports: [ 102 | NestJsNotificationsModule.forRootAsync({ 103 | imports: [ 104 | BullModule.forRoot({}), 105 | BullModule.registerQueue({ 106 | name: 'notifications_queue', 107 | }), 108 | ], 109 | useClass: NestJsNotificationConfigService 110 | }) 111 | ], 112 | providers: [ 113 | 114 | ] 115 | }).compile(); 116 | 117 | service = module.get(NestJsNotificationsService); 118 | moduleRef = module.get(ModuleRef); 119 | notification = new TestNotification(); 120 | queue = module.get(getQueueToken('notifications_queue')) 121 | }); 122 | 123 | it('should be defined',()=>{ 124 | expect(service).toBeDefined(); 125 | }); 126 | 127 | describe('queue',()=>{ 128 | fit('should add to the queue correctly', async ()=>{ 129 | jest.spyOn(queue, 'add').mockImplementation(()=>true as any); 130 | 131 | await service.queue(notification); 132 | 133 | expect(queue.add).toHaveBeenCalled() 134 | }) 135 | }); 136 | }) 137 | }); 138 | -------------------------------------------------------------------------------- /src/nestjs-notifications.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, OnModuleInit, Type } from '@nestjs/common'; 2 | import { ModuleRef } from '@nestjs/core'; 3 | import { JobOptions, Queue } from 'bull'; 4 | import { NestJsNotificationChannel } from './channels/notification-channel.interface'; 5 | import { 6 | NESTJS_NOTIFICATIONS_JOB_OPTIONS, 7 | NESTJS_NOTIFICATIONS_QUEUE, 8 | } from './constants'; 9 | import { 10 | NestJsNotification, 11 | NestJsQueuedNotification, 12 | } from './notification/notification.interface'; 13 | 14 | @Injectable() 15 | export class NestJsNotificationsService implements OnModuleInit { 16 | processor_name = 'nestjs_notification'; 17 | 18 | constructor( 19 | private moduleRef: ModuleRef, 20 | @Inject(NESTJS_NOTIFICATIONS_QUEUE) 21 | private notificationsQueue: Queue, 22 | @Inject(NESTJS_NOTIFICATIONS_JOB_OPTIONS) 23 | private defaultJobOptions: JobOptions, 24 | ) { } 25 | 26 | onModuleInit() { 27 | if (this.notificationsQueue) { 28 | this.notificationsQueue.process( 29 | this.processor_name, 30 | async (job: { data: { notification; callback } }, done) => { 31 | try { 32 | await job.data.callback(job.data.notification).then(done()); 33 | } catch (e) { 34 | throw e; 35 | } 36 | }, 37 | ); 38 | } 39 | } 40 | 41 | /** 42 | * Process a notification and send via designated channel 43 | * @param notification 44 | */ 45 | public send(notification: NestJsNotification): Promise { 46 | const channels = notification.broadcastOn(); 47 | return Promise.all( 48 | channels.map((channel: Type) => 49 | this.sendOnChannel(notification, channel), 50 | ), 51 | ); 52 | } 53 | 54 | /** 55 | * Push a job to the queue 56 | * @param notification 57 | */ 58 | public queue(notification: NestJsQueuedNotification): any { 59 | if (!this.notificationsQueue) throw new Error('No Queue Specified'); 60 | 61 | return this.notificationsQueue.add( 62 | this.processor_name, 63 | { 64 | notification, 65 | callback: this.send, 66 | }, 67 | notification.getJobOptions() 68 | ? notification.getJobOptions() 69 | : this.defaultJobOptions, 70 | ); 71 | } 72 | 73 | /** 74 | * Send notification on designated channel 75 | * @param notification 76 | * @param channel 77 | */ 78 | async sendOnChannel( 79 | notification: NestJsNotification, 80 | channel: Type, 81 | ): Promise { 82 | const chann = await this.resolveChannel(channel); 83 | await chann.send(notification); 84 | } 85 | 86 | /** 87 | * Resolve the channel needed to send the Notification 88 | * @param channel 89 | */ 90 | resolveChannel = (channel: Type) => 91 | this.moduleRef.create(channel); 92 | } 93 | -------------------------------------------------------------------------------- /src/notification/notification.example.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { HttpChannel } from '../channels/http/http.channel'; 3 | import { NestJsNotificationChannel } from '../channels/notification-channel.interface'; 4 | import { NestJsNotification } from './notification.interface'; 5 | 6 | export class ExampleNotification implements NestJsNotification { 7 | private data: any; 8 | 9 | constructor(data: any) { 10 | this.data = data; 11 | } 12 | 13 | /** 14 | * Get the channels the notification should broadcast on 15 | * @returns {Type[]} array 16 | */ 17 | public broadcastOn(): Type[] { 18 | return [HttpChannel]; 19 | } 20 | 21 | /** 22 | * Get the json representation of the notification. 23 | * @returns {} 24 | */ 25 | toPayload(): any { 26 | return this.data; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/notification/notification.interface.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { JobOptions } from 'bull'; 3 | import { NestJsNotificationChannel } from '../channels/notification-channel.interface'; 4 | 5 | export interface NestJsNotification { 6 | /** 7 | * Get the channels the notification should broadcast on. 8 | * @returns {Type[]} array 9 | */ 10 | broadcastOn(): Type[]; 11 | 12 | /** 13 | * Get the json representation of the notification. 14 | * @returns json 15 | */ 16 | toPayload?(): Record; 17 | } 18 | 19 | export interface NestJsQueuedNotification { 20 | /** 21 | * Return any job options for this Notification 22 | * @returns {JobOptions | null} 23 | */ 24 | getJobOptions(): JobOptions | null; 25 | } 26 | -------------------------------------------------------------------------------- /src/notification/notification.spec.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { HttpChannel } from '../channels/http/http.channel'; 3 | import { NestJsNotificationChannel } from '../channels/notification-channel.interface'; 4 | import { NestJsNotification } from './notification.interface'; 5 | 6 | class TestNotification implements NestJsNotification { 7 | /** 8 | * Get the channels the notification should broadcast on 9 | * @returns {Type[]} array 10 | */ 11 | public broadcastOn(): Type[] { 12 | return [HttpChannel]; 13 | } 14 | } 15 | 16 | describe('Notification', () => { 17 | let notification: TestNotification; 18 | 19 | beforeEach(() => { 20 | notification = new TestNotification(); 21 | }); 22 | 23 | it('should be defined', () => { 24 | expect(notification).toBeDefined(); 25 | }); 26 | 27 | describe('broadcastOn', () => { 28 | it('should return Type correctly', () => { 29 | const res = notification.broadcastOn(); 30 | expect(res).toBeInstanceOf(Array); 31 | expect(res[0]).toEqual(HttpChannel); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 17 | } 18 | --------------------------------------------------------------------------------