├── .eslintrc.js ├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierrc ├── LICENSE ├── README.md ├── examples └── basics │ ├── .env │ ├── .env.example │ ├── .eslintrc.js │ ├── .gitignore │ ├── .prettierrc │ ├── Dockerfile │ ├── README.md │ ├── components │ ├── redis-pubsub.yaml │ └── resiliency.yaml │ ├── docker-compose.yml │ ├── nest-cli.json │ ├── package-lock.json │ ├── package.json │ ├── rabbitmq-components │ ├── rabbitmq-pubsub.yaml │ └── rabbitmq-queue-binding.yaml │ ├── servicebus-components │ ├── azure-servicebus-pubsub.yaml │ ├── azure-servicebus-queue-binding.yaml │ └── local-secret-store.yaml │ ├── src │ ├── Message.ts │ ├── app.controller.spec.ts │ ├── app.module.ts │ ├── main.ts │ └── pubsub.controller.ts │ ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json │ ├── tsconfig.build.json │ └── tsconfig.json ├── lib ├── constants.ts ├── dapr-binding.decorator.ts ├── dapr-metadata.accessor.ts ├── dapr-pubsub.decorator.ts ├── dapr.loader.ts ├── dapr.module.ts └── index.ts ├── package-lock.json ├── package.json └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: [ 8 | '@typescript-eslint/eslint-plugin', 9 | 'promise', 10 | 'import', 11 | 'jest' 12 | ], 13 | extends: [ 14 | 'eslint:recommended', 15 | 'plugin:@typescript-eslint/recommended', 16 | 'plugin:prettier/recommended', 17 | 'plugin:jest/recommended', 18 | 'plugin:promise/recommended' 19 | ], 20 | root: true, 21 | env: { 22 | node: true, 23 | jest: true, 24 | }, 25 | ignorePatterns: ['.eslintrc.js'], 26 | rules: { 27 | '@typescript-eslint/interface-name-prefix': 'off', 28 | '@typescript-eslint/explicit-function-return-type': 'off', 29 | '@typescript-eslint/explicit-module-boundary-types': 'off', 30 | '@typescript-eslint/no-explicit-any': 'off', 31 | 'import/order': [ 32 | 'error', 33 | { 34 | 'groups': ['builtin', 'external', 'parent', 'sibling', 'index'], 35 | 'pathGroups': [ 36 | { 37 | 'pattern': 'react', 38 | 'group': 'external', 39 | 'position': 'before' 40 | } 41 | ], 42 | 'alphabetize': { 43 | 'order': 'asc', 44 | 'caseInsensitive': false 45 | } 46 | } 47 | ], 48 | 'prettier/prettier': 'error' 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: npm package 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 16 16 | - run: npm ci 17 | 18 | publish-npm: 19 | needs: build 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: 16 26 | registry-url: https://registry.npmjs.org/ 27 | - run: npm ci 28 | - run: npm run build 29 | - run: npm publish 30 | env: 31 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # IDE 5 | /.vscode 6 | 7 | # misc 8 | npm-debug.log 9 | .DS_Store 10 | 11 | # tests 12 | /test 13 | /coverage 14 | /.nyc_output 15 | 16 | # dist 17 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # source 2 | lib 3 | tests 4 | index.ts 5 | package-lock.json 6 | tslint.json 7 | tsconfig.json 8 | .prettierrc 9 | 10 | # github 11 | .github -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "tabWidth": 2 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2023 Deep Blue Company 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Develop NestJs microservices using [Dapr](https://dapr.io/) pubsub and bindings. 2 | 3 | # Description 4 | 5 | Dapr Module for [Nest](https://github.com/nestjs/nest) built on top of the [Dapr JS SDK](https://github.com/dapr/js-sdk). 6 | 7 | # Installation 8 | 9 | ```bash 10 | npm i --save @dbc-tech/nest-dapr 11 | ``` 12 | 13 | # Requirements 14 | 15 | Install [Dapr](https://dapr.io/) as per getting started [guide](https://docs.dapr.io/getting-started/). Ensure Dapr is running with 16 | 17 | ```bash 18 | dapr --version 19 | ``` 20 | 21 | Output: 22 | 23 | ``` 24 | CLI version: 1.7.1 25 | Runtime version: 1.7.4 26 | ``` 27 | 28 | # Quick start 29 | 30 | The following scaffolds a [Nest](https://github.com/nestjs/nest) project with the [nest-dapr](https://www.npmjs.com/package/@dbc-tech/nest-dapr) package and demonstrates using Nest with Dapr using [RabbitMQ](https://www.rabbitmq.com/) pubsub & queue bindings. 31 | 32 | Install Nest [CLI](https://docs.nestjs.com/cli/overview) 33 | 34 | ```bash 35 | npm install -g @nestjs/cli 36 | ``` 37 | 38 | Scaffold Nest project 39 | 40 | ``` 41 | nest new nest-dapr 42 | cd nest-dapr/ 43 | ``` 44 | 45 | Install [nest-dapr](https://www.npmjs.com/package/@dbc-tech/nest-dapr) package 46 | 47 | ```bash 48 | npm i --save @dbc-tech/nest-dapr 49 | ``` 50 | 51 | Import `DaprModule` in `AppModule` class 52 | 53 | ```typescript 54 | @Module({ 55 | imports: [DaprModule.register()], 56 | controllers: [AppController], 57 | providers: [AppService], 58 | }) 59 | export class AppModule {} 60 | ``` 61 | 62 | Import `DaprClient` from `@dapr/dapr` package and add dependency to `AppController` class 63 | 64 | ```typescript 65 | import { DaprClient } from '@dapr/dapr'; 66 | import { Controller, Get } from '@nestjs/common'; 67 | import { AppService } from './app.service'; 68 | 69 | @Controller() 70 | export class AppController { 71 | constructor( 72 | private readonly appService: AppService, 73 | private readonly daprClient: DaprClient, 74 | ) {} 75 | 76 | @Get() 77 | getHello(): string { 78 | return this.appService.getHello(); 79 | } 80 | } 81 | ``` 82 | 83 | Create pubsub & topic names used for pubsub operations and message interface 84 | 85 | ```typescript 86 | const pubSubName = 'my-pubsub'; 87 | const topicName = 'my-topic'; 88 | 89 | interface Message { 90 | hello: string; 91 | } 92 | 93 | @Controller() 94 | ``` 95 | 96 | Create endpoint to publish topic message 97 | 98 | ```typescript 99 | @Post('pubsub') 100 | async pubsub(): Promise { 101 | const message: Message = { hello: 'world' }; 102 | 103 | return this.daprClient.pubsub.publish(pubSubName, topicName, message); 104 | } 105 | ``` 106 | 107 | Create pubsub handler which will subscribe to the topic and log the received message 108 | 109 | ```typescript 110 | @DaprPubSub(pubSubName, topicName) 111 | pubSubHandler(message: Message): void { 112 | console.log(`Received topic:${topicName} message:`, message); 113 | } 114 | ``` 115 | 116 | Create Dapr [pubsub](https://docs.dapr.io/developing-applications/building-blocks/pubsub/) component in `components` folder 117 | 118 | ```yaml 119 | apiVersion: dapr.io/v1alpha1 120 | kind: Component 121 | metadata: 122 | name: my-pubsub 123 | namespace: default 124 | spec: 125 | type: pubsub.rabbitmq 126 | version: v1 127 | metadata: 128 | - name: host 129 | value: amqp://guest:guest@localhost:5674 130 | ``` 131 | Save file as `components/rabbitmq-pubsub.yaml` 132 | 133 | Create `docker-compose.yml` in the project root used to run [RabbitMQ](https://www.rabbitmq.com/) 134 | 135 | ```yaml 136 | version: '3.9' 137 | services: 138 | pubsub: 139 | image: rabbitmq:3-management-alpine 140 | ports: 141 | - 5674:5672 142 | - 15674:15672 143 | ``` 144 | 145 | Start RabbitMQ 146 | 147 | ```bash 148 | docker-compose up 149 | ``` 150 | 151 | Create script to bootstrap your Nest project using Dapr sidecar. Update `package.json` and add script 152 | 153 | ```json 154 | "scripts": { 155 | .. 156 | "start:dapr": "dapr run --app-id nest-dapr --app-protocol http --app-port 50001 --dapr-http-port 50000 --components-path ./components npm run start" 157 | }, 158 | ``` 159 | 160 | Start Nest app with Dapr 161 | 162 | ```bash 163 | npm run start:dapr 164 | ``` 165 | 166 | Invoke endpoint to publish the message 167 | 168 | ```bash 169 | curl -X POST localhost:3000/pubsub 170 | ``` 171 | 172 | This should publish a message to RabbitMQ which should be consumed by the handler and written to the console: 173 | 174 | ``` 175 | == APP == Received topic:my-topic message: { hello: 'world' } 176 | ``` 177 | 178 | Full example 179 | 180 | ```typescript 181 | import { DaprClient } from '@dapr/dapr'; 182 | import { DaprPubSub } from '@dbc-tech/nest-dapr'; 183 | import { Controller, Get, Post } from '@nestjs/common'; 184 | import { AppService } from './app.service'; 185 | 186 | const pubSubName = 'my-pubsub'; 187 | const topicName = 'my-topic'; 188 | 189 | interface Message { 190 | hello: string; 191 | } 192 | 193 | @Controller() 194 | export class AppController { 195 | constructor( 196 | private readonly appService: AppService, 197 | private readonly daprClient: DaprClient, 198 | ) {} 199 | 200 | @Get() 201 | getHello(): string { 202 | return this.appService.getHello(); 203 | } 204 | 205 | @Post('pubsub') 206 | async pubsub(): Promise { 207 | const message: Message = { hello: 'world' }; 208 | 209 | return this.daprClient.pubsub.publish(pubSubName, topicName, message); 210 | } 211 | 212 | @DaprPubSub(pubSubName, topicName) 213 | pubSubHandler(message: Message): void { 214 | console.log(`Received topic:${topicName} message:`, message); 215 | } 216 | } 217 | ``` 218 | 219 | # DaprModule 220 | 221 | `DaprModule` is a global Nest [Module](https://docs.nestjs.com/modules) used to register `DaprServer` & `DaprClient` as [providers](https://docs.nestjs.com/providers) within your project. It also registers all your handlers which listen to Dapr pubsub and input bindings so that when messages are received by Dapr, they are forwarded to the handler. Handler registration occurs during the `onApplicationBootstrap` lifecycle hook. 222 | 223 | To use `nest-dapr`, import the `DaprModule` into the root `AppModule` and run the `register()` static method. 224 | 225 | ```typescript 226 | @Module({ 227 | imports: [DaprModule.register()], 228 | controllers: [AppController], 229 | providers: [AppService], 230 | }) 231 | export class AppModule {} 232 | ``` 233 | 234 | `register()` takes an optional `DaprModuleOptions` object which allows passing arguments to `DaprServer` instance. 235 | 236 | ```typescript 237 | export interface DaprModuleOptions { 238 | serverHost?: string; 239 | serverPort?: string; 240 | daprHost?: string; 241 | daprPort?: string; 242 | communicationProtocol?: CommunicationProtocolEnum; 243 | clientOptions?: DaprClientOptions; 244 | } 245 | ``` 246 | 247 | See Dapr JS [docs](https://docs.dapr.io/developing-applications/sdks/js/js-server/) for more information about these arguments. 248 | 249 | ## Async configuration 250 | 251 | You can pass your options asynchronously instead of statically. In this case, use the `registerAsync()` method, which provides several ways to deal with async configuration. One of which is to use a factory function: 252 | 253 | ```typescript 254 | DaprModule.registerAsync({ 255 | imports: [ConfigModule], 256 | useFactory: (configService: ConfigService) => ({ 257 | serverHost: configService.get('DAPR_SERVER_HOST'), 258 | serverPort: configService.get('DAPR_SERVER_PORT'), 259 | daprHost: configService.get('DAPR_HOST'), 260 | daprPort: configService.get('DAPR_PORT'), 261 | communicationProtocol: CommunicationProtocolEnum.GRPC, 262 | clientOptions: { 263 | logger: { 264 | level: LogLevel.Verbose, 265 | }, 266 | }, 267 | }), 268 | inject: [ConfigService], 269 | }) 270 | ``` 271 | 272 | # DaprServer & DaprClient providers 273 | 274 | `DaprModule` registers [DaprServer](https://docs.dapr.io/developing-applications/sdks/js/js-server/) and [DaprClient](https://docs.dapr.io/developing-applications/sdks/js/js-client/) as Nest [providers](https://docs.nestjs.com/providers). These can be injected into your controllers and services like any other provider. 275 | 276 | ```typescript 277 | import { DaprClient } from '@dapr/dapr'; 278 | import { Controller, Post } from '@nestjs/common'; 279 | 280 | @Controller() 281 | export class AppController { 282 | constructor(readonly daprClient: DaprClient) {} 283 | 284 | @Post() 285 | async pubsub(): Promise { 286 | return this.daprClient.pubsub.publish('my-pub-sub', 'my-topic', { 287 | hello: 'world', 288 | }); 289 | } 290 | } 291 | ``` 292 | 293 | # Dapr decorators 294 | 295 | `nest-dapr` provides two TypeScript [decorators](https://www.typescriptlang.org/docs/handbook/decorators.html#decorators) which are used to declaratively configure subscriptions and bindings. These are used by `DaprModule` in conjunction with the handler method to define the handler implementations. 296 | 297 | ## DaprPubSub decorator 298 | 299 | `DaprPubSub` decorator is used to set-up a handler for receiving pubsub topic messages. The handler has 3 arguments (`name`, `topicName` & `route`). `name` specifies the pubsub component `name` as defined in the Dapr component `metadata` section. `topicName` is the name of the pubsub topic. Route is an optional argument and defines possible [routing](https://docs.dapr.io/developing-applications/building-blocks/pubsub/howto-route-messages/) values. 300 | 301 | Example: 302 | 303 | ```typescript 304 | @DaprPubSub('my-pubsub', 'my-topic') 305 | pubSubHandler(message: any): void { 306 | console.log('Received message:', message); 307 | } 308 | ``` 309 | 310 | RabbitMQ pubsub Component: 311 | 312 | ```yaml 313 | apiVersion: dapr.io/v1alpha1 314 | kind: Component 315 | metadata: 316 | name: my-pubsub 317 | namespace: default 318 | spec: 319 | type: pubsub.rabbitmq 320 | version: v1 321 | metadata: 322 | - name: host 323 | value: amqp://guest:guest@localhost:5674 324 | ``` 325 | 326 | Publish message: 327 | 328 | ```typescript 329 | await this.daprClient.pubsub.publish('my-pubsub', 'my-topic', { hello: 'world' }); 330 | ``` 331 | 332 | In this example the handler `pubSubHandler` method will receive messages from the `my-topic` topic through the `my-pubsub` component which in this case is RabbitMQ. 333 | 334 | ## DaprBinding decorator 335 | 336 | `DaprBinding` decorator is used to set-up a handler for receiving input binding data. The handler has one argument `name` which specifies the binding component `name` as defined in the Dapr component `metadata` section. 337 | 338 | Example: 339 | 340 | ```typescript 341 | @DaprBinding('my-queue-binding') 342 | bindingHandler(message: any): void { 343 | coneole.log('Received message:', message); 344 | } 345 | ``` 346 | 347 | RabbitMQ binding component: 348 | 349 | ```yaml 350 | apiVersion: dapr.io/v1alpha1 351 | kind: Component 352 | metadata: 353 | name: my-queue-binding 354 | namespace: default 355 | spec: 356 | type: bindings.rabbitmq 357 | version: v1 358 | metadata: 359 | - name: queueName 360 | value: queue1 361 | - name: host 362 | value: amqp://guest:guest@localhost:5674 363 | - name: durable 364 | value: true 365 | - name: deleteWhenUnused 366 | value: false 367 | - name: ttlInSeconds 368 | value: 60 369 | - name: prefetchCount 370 | value: 0 371 | - name: exclusive 372 | value: false 373 | - name: maxPriority 374 | value: 5 375 | - name: contentType 376 | value: "text/plain" 377 | ``` 378 | 379 | Send message: 380 | 381 | ```typescript 382 | await this.daprClient.binding.send('my-queue-binding', 'create', { hello: 'world' }); 383 | ``` 384 | 385 | In this example the handler `bindingHandler` method will receive messages from the `queue1` queue defined in the `my-queue-binding` component which in this case is RabbitMQ. 386 | 387 | ## Writing handlers 388 | 389 | `DaprModule` uses reflection to register all handlers found either in [Controller](https://docs.nestjs.com/controllers) or [Provider](https://docs.nestjs.com/providers) classes. These classes must be registered in a Nest [module](https://docs.nestjs.com/modules). Providers must be decorated with the `@Injectable()` decorator at the class level. Once this is done and your provider is added to your module's [providers] array then `nest-dapr` will use Nest dependency injection container to resolve the provider instance and call your handler when the message is received. 390 | 391 | Here's an example of a [Provider](https://docs.nestjs.com/providers) containing a Dapr handler. 392 | 393 | ```typescript 394 | import { DaprPubSub } from '@dbc-tech/nest-dapr'; 395 | import { Injectable, Logger } from '@nestjs/common'; 396 | 397 | @Injectable() 398 | export class AppService { 399 | private readonly logger = new Logger(AppService.name); 400 | 401 | @DaprPubSub('my-pubsub', 'my-topic') 402 | pubSubHandler(message: any): void { 403 | this.logger.log(`Received topic message:`, message); 404 | } 405 | } 406 | ``` 407 | 408 | # Examples 409 | 410 | | Example | Description | 411 | |---|---| 412 | | [Basics](examples/basics/README.md) | Demonstrates pubsub & input binding using RabbitMQ | 413 | 414 | # Troubleshooting 415 | 416 | [Dapr](https://dapr.io/) is a complex set of tools and services and must be set-up and deployed carefully to ensure your system operates correctly. `nest-dapr` is merely [syntactic sugar](https://en.wikipedia.org/wiki/Syntactic_sugar) over the existing Dapr [js-sdk](https://github.com/dapr/js-sdk). If things are not working out for you please review Dapr & SDK documentation and issues. Also please use one of the examples provided in this repo. They are updated and tested regularly and should work out of the box. If you find that both Dapr and the Javascript SDK is both working fine but `nest-dapr` is not working in some way, please file an issue and state clearly the problem and provide a reproducable code example. Filing an issue with something like: "It doesn't work" is likely to be ignored. Thank you. 417 | 418 | # Credits 419 | 420 | Inspiration for this project is taken from [dapr-nestjs-pubsub](https://github.com/avifatal/dapr-nestjs-pubsub) which I believe is the first attempt at integrating [Dapr](https://dapr.io/) with [Nest](https://github.com/nestjs/nest). Unfortunately this repo supports only pubsub messaging and I wanted to support input bindings as well. In the end I adopted the patterns established in Nest's own [event-emitter](https://github.com/nestjs/event-emitter) repo and I pretty much *borrowed* all the metadata, reflection & decorator utils from there. 421 | 422 | So full credit goes to the [Nest](https://github.com/nestjs/nest) development team :heart: 423 | 424 | -------------------------------------------------------------------------------- /examples/basics/.env: -------------------------------------------------------------------------------- 1 | # App server listen port. Used in main.ts and should match expose port in Dockerfile 2 | PORT=3000 3 | 4 | # App environment 5 | APP_ENV=test 6 | APP_VERSION=1.0 7 | 8 | # Dapr 9 | DAPR_PORT=3500 10 | DAPR_SERVER_PORT=50002 11 | 12 | # Logging 13 | LOG_LEVEL=debug -------------------------------------------------------------------------------- /examples/basics/.env.example: -------------------------------------------------------------------------------- 1 | # App server listen port. Used in main.ts and should match expose port in Dockerfile 2 | PORT=3000 3 | 4 | # App environment 5 | APP_ENV=test 6 | APP_VERSION=1.0 7 | 8 | # Dapr 9 | DAPR_PORT=3500 10 | DAPR_SERVER_PORT=50002 11 | 12 | # Logging 13 | LOG_LEVEL=debug -------------------------------------------------------------------------------- /examples/basics/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir : __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 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 | -------------------------------------------------------------------------------- /examples/basics/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | secrets.json -------------------------------------------------------------------------------- /examples/basics/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /examples/basics/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Node.js runtime as a parent image 2 | FROM node:18-alpine 3 | 4 | # Set the working directory to /app 5 | WORKDIR /app 6 | 7 | # Copy package.json and package-lock.json to the container 8 | COPY package*.json ./ 9 | 10 | # Install dependencies 11 | RUN npm install 12 | 13 | # Copy the rest of the application code to the container 14 | COPY . . 15 | 16 | # Expose the port that the application listens on 17 | EXPOSE 3000 18 | 19 | # Start the application 20 | CMD [ "npm", "start" ] -------------------------------------------------------------------------------- /examples/basics/README.md: -------------------------------------------------------------------------------- 1 | # Basics: nest-dapr pubsub example 2 | 3 | Demonstrates pubsub using Redis. 4 | 5 | ## Getting Started 6 | 7 | Install packages 8 | 9 | ```bash 10 | npm i 11 | ``` 12 | 13 | Start docker-compose to app & dapr 14 | 15 | ```bash 16 | docker compose up 17 | ``` 18 | 19 | ## pubsub test 20 | 21 | Invoke endpoint to publish message 22 | 23 | ```bash 24 | curl -X POST localhost:3000/pubsub 25 | ``` 26 | 27 | Observe handler received message 28 | 29 | ## Resiliency 30 | 31 | Un-comment the `BadRequestException` throw to simulate handler failure 32 | 33 | Observe retries defined in the resilience policy 34 | -------------------------------------------------------------------------------- /examples/basics/components/redis-pubsub.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: redis-pubsub 5 | namespace: default 6 | spec: 7 | type: pubsub.redis 8 | version: v1 9 | metadata: 10 | - name: redisHost 11 | value: redis:6379 12 | - name: redisPassword 13 | value: '' 14 | -------------------------------------------------------------------------------- /examples/basics/components/resiliency.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Resiliency 3 | metadata: 4 | name: myresiliency 5 | spec: 6 | policies: 7 | retries: 8 | pubsubRetry: 9 | policy: constant 10 | duration: 5s 11 | maxRetries: 5 12 | targets: 13 | components: 14 | redis-pubsub: 15 | inbound: 16 | retry: pubsubRetry 17 | -------------------------------------------------------------------------------- /examples/basics/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | ############################ 5 | # Basics Handler 6 | ############################ 7 | main: 8 | container_name: basics-main 9 | build: 10 | dockerfile: ./Dockerfile 11 | volumes: 12 | - .:/usr/src/app 13 | command: 14 | [sh, -c, 'cd /usr/src/app && yarn start:debug'] 15 | ports: 16 | - 3000:3000 17 | - 3500:3500 18 | - 50002:50002 19 | - 9229:9229 20 | env_file: 21 | - .env 22 | environment: 23 | PORT: ${PORT} 24 | NODE_ENV: development 25 | networks: 26 | - webnet 27 | depends_on: 28 | - placement 29 | ############################ 30 | # Dapr sidecar for Handler app 31 | ############################ 32 | nodeapp-dapr: 33 | container_name: basics-dapr-sidecar 34 | image: 'daprio/daprd' 35 | command: 36 | [ 37 | './daprd', 38 | '--app-id', 39 | 'nodeapp', 40 | '--app-port', 41 | '50002', 42 | '--app-protocol', 43 | 'http', 44 | '--placement-host-address', 45 | 'placement:50006', 46 | '--components-path', 47 | '/components', 48 | ] 49 | volumes: 50 | - './components/:/components' 51 | depends_on: 52 | - main 53 | network_mode: 'service:main' 54 | ############################ 55 | # Dapr 56 | ############################ 57 | placement: 58 | container_name: basics-dapr 59 | image: 'daprio/dapr' 60 | command: ['./placement', '-port', '50006'] 61 | ports: 62 | - 50006:50006 63 | networks: 64 | - webnet 65 | ############################ 66 | # Redis state store 67 | ############################ 68 | redis: 69 | container_name: basics-redis 70 | image: redis 71 | ports: 72 | - 6380:6379 73 | networks: 74 | - webnet 75 | ############################ 76 | # Azurite Storage Emulator 77 | ############################ 78 | azurite: 79 | container_name: basics-storage 80 | hostname: azurite 81 | image: mcr.microsoft.com/azure-storage/azurite 82 | networks: 83 | - webnet 84 | ports: 85 | - 10000:10000 86 | networks: 87 | webnet: 88 | -------------------------------------------------------------------------------- /examples/basics/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /examples/basics/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basics", 3 | "version": "0.0.2", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json", 22 | "start:dapr": "dapr run --app-id basics --app-protocol http --app-port 50001 --dapr-http-port 50000 --components-path ./rabbitmq-components npm run start" 23 | }, 24 | "dependencies": { 25 | "@dbc-tech/nest-dapr": "^0.8.0", 26 | "@nestjs/common": "^9.0.0", 27 | "@nestjs/config": "^2.2.0", 28 | "@nestjs/core": "^9.0.0", 29 | "@nestjs/platform-express": "^9.0.0", 30 | "reflect-metadata": "^0.1.13", 31 | "rimraf": "^3.0.2", 32 | "rxjs": "^7.2.0" 33 | }, 34 | "devDependencies": { 35 | "@golevelup/ts-jest": "^0.3.2", 36 | "@nestjs/cli": "^9.5.0", 37 | "@nestjs/schematics": "^9.0.0", 38 | "@nestjs/testing": "^9.0.0", 39 | "@types/express": "^4.17.13", 40 | "@types/jest": "27.5.0", 41 | "@types/node": "^16.0.0", 42 | "@types/supertest": "^2.0.11", 43 | "@typescript-eslint/eslint-plugin": "^5.0.0", 44 | "@typescript-eslint/parser": "^5.0.0", 45 | "eslint": "^8.0.1", 46 | "eslint-config-prettier": "^8.3.0", 47 | "eslint-plugin-prettier": "^4.0.0", 48 | "jest": "28.0.3", 49 | "prettier": "^2.3.2", 50 | "source-map-support": "^0.5.20", 51 | "supertest": "^6.1.3", 52 | "ts-jest": "28.0.1", 53 | "ts-loader": "^9.2.3", 54 | "ts-node": "^10.0.0", 55 | "tsconfig-paths": "4.0.0", 56 | "typescript": "^4.3.5" 57 | }, 58 | "jest": { 59 | "moduleFileExtensions": [ 60 | "js", 61 | "json", 62 | "ts" 63 | ], 64 | "rootDir": "src", 65 | "testRegex": ".*\\.spec\\.ts$", 66 | "transform": { 67 | "^.+\\.(t|j)s$": "ts-jest" 68 | }, 69 | "collectCoverageFrom": [ 70 | "**/*.(t|j)s" 71 | ], 72 | "coverageDirectory": "../coverage", 73 | "testEnvironment": "node" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /examples/basics/rabbitmq-components/rabbitmq-pubsub.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: my-pubsub 5 | namespace: default 6 | spec: 7 | type: pubsub.rabbitmq 8 | version: v1 9 | metadata: 10 | - name: host 11 | value: amqp://guest:guest@localhost:5674 -------------------------------------------------------------------------------- /examples/basics/rabbitmq-components/rabbitmq-queue-binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: my-queue-binding 5 | namespace: default 6 | spec: 7 | type: bindings.rabbitmq 8 | version: v1 9 | metadata: 10 | - name: queueName 11 | value: queue1 12 | - name: host 13 | value: amqp://guest:guest@localhost:5674 14 | - name: durable 15 | value: true 16 | - name: deleteWhenUnused 17 | value: false 18 | - name: ttlInSeconds 19 | value: 60 20 | - name: prefetchCount 21 | value: 0 22 | - name: exclusive 23 | value: false 24 | - name: maxPriority 25 | value: 5 26 | - name: contentType 27 | value: "text/plain" -------------------------------------------------------------------------------- /examples/basics/servicebus-components/azure-servicebus-pubsub.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: my-pubsub 5 | namespace: default 6 | spec: 7 | type: pubsub.azure.servicebus 8 | version: v1 9 | metadata: 10 | - name: connectionString 11 | secretKeyRef: 12 | name: asbNsConnstring 13 | key: asbNsConnstring 14 | auth: 15 | secretStore: localsecretstore 16 | -------------------------------------------------------------------------------- /examples/basics/servicebus-components/azure-servicebus-queue-binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: my-queue-binding 5 | namespace: default 6 | spec: 7 | type: bindings.azure.servicebusqueues 8 | version: v1 9 | metadata: 10 | - name: connectionString 11 | secretKeyRef: 12 | name: asbNsConnstring 13 | key: asbNsConnstring 14 | - name: queueName 15 | value: request-payment 16 | auth: 17 | secretStore: localsecretstore 18 | -------------------------------------------------------------------------------- /examples/basics/servicebus-components/local-secret-store.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: localsecretstore 5 | namespace: default 6 | spec: 7 | type: secretstores.local.file 8 | version: v1 9 | metadata: 10 | - name: secretsFile 11 | value: secrets.json 12 | - name: nestedSeparator 13 | value: ":" 14 | -------------------------------------------------------------------------------- /examples/basics/src/Message.ts: -------------------------------------------------------------------------------- 1 | export interface Message { 2 | hello: string; 3 | } 4 | -------------------------------------------------------------------------------- /examples/basics/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { DaprClient } from '@dapr/dapr'; 2 | import { createMock, DeepMocked } from '@golevelup/ts-jest'; 3 | import { Test, TestingModule } from '@nestjs/testing'; 4 | import { PubsubController } from './pubsub.controller'; 5 | import { AppService } from './app.service'; 6 | 7 | describe('AppController', () => { 8 | let appController: PubsubController; 9 | const daprMock: DeepMocked = createMock(); 10 | 11 | beforeEach(async () => { 12 | const app: TestingModule = await Test.createTestingModule({ 13 | controllers: [PubsubController], 14 | providers: [ 15 | AppService, 16 | { 17 | provide: DaprClient, 18 | useValue: daprMock, 19 | }, 20 | ], 21 | }).compile(); 22 | 23 | appController = app.get(PubsubController); 24 | }); 25 | 26 | describe('root', () => { 27 | it('should return "Hello World!"', () => { 28 | expect(appController.getHello()).toBe('Hello World!'); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /examples/basics/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PubsubController } from './pubsub.controller'; 3 | import { DaprPubSubStatusEnum } from '@dapr/dapr'; 4 | import { ConfigModule, ConfigService } from '@nestjs/config'; 5 | import { DaprModule } from '@dbc-tech/nest-dapr'; 6 | 7 | @Module({ 8 | imports: [ 9 | ConfigModule.forRoot({ 10 | isGlobal: true, 11 | cache: true, 12 | }), 13 | DaprModule.registerAsync({ 14 | useFactory: (configService: ConfigService) => { 15 | return { 16 | serverPort: configService.get('DAPR_SERVER_PORT'), 17 | onError: () => DaprPubSubStatusEnum.RETRY, 18 | }; 19 | }, 20 | imports: [], 21 | inject: [ConfigService], 22 | }), 23 | ], 24 | controllers: [PubsubController], 25 | }) 26 | export class AppModule {} 27 | -------------------------------------------------------------------------------- /examples/basics/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(3000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /examples/basics/src/pubsub.controller.ts: -------------------------------------------------------------------------------- 1 | import { DaprClient } from '@dapr/dapr'; 2 | import { BadRequestException, Controller, Logger, Post } from '@nestjs/common'; 3 | import { Message } from './Message'; 4 | import { DaprPubSub } from '@dbc-tech/nest-dapr'; 5 | 6 | @Controller() 7 | export class PubsubController { 8 | private readonly logger = new Logger(PubsubController.name); 9 | 10 | constructor(readonly daprClient: DaprClient) { 11 | this.logger.log(`Dapr Client running on ${daprClient.options.daprPort}`); 12 | } 13 | 14 | @Post('pubsub') 15 | async pubsub() { 16 | const message: Message = { hello: Date.now().toString(36) }; 17 | 18 | return this.daprClient.pubsub.publish('redis-pubsub', 'myqueue', message); 19 | } 20 | 21 | @DaprPubSub('redis-pubsub', 'myqueue') 22 | pubSubHandler(message: Message): void { 23 | this.logger.log(`Received topic message:`, message); 24 | //throw new BadRequestException(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/basics/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from '../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/basics/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/basics/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/basics/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 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const DAPR_BINDING_METADATA = 'DAPR_BINDING_METADATA'; 2 | export const DAPR_PUBSUB_METADATA = 'DAPR_PUBSUB_METADATA'; 3 | -------------------------------------------------------------------------------- /lib/dapr-binding.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { DAPR_BINDING_METADATA } from './constants'; 3 | 4 | /** 5 | * `@DaprBinding` decorator metadata 6 | */ 7 | export interface DaprBindingMetadata { 8 | /** 9 | * Name of binding to receive data. 10 | */ 11 | name: string; 12 | } 13 | 14 | /** 15 | * Dapr Binding decorator. 16 | * Receives data from Dapr input bindings. 17 | * 18 | * @param name name of binding 19 | */ 20 | export const DaprBinding = (name: string): MethodDecorator => 21 | SetMetadata(DAPR_BINDING_METADATA, { name } as DaprBindingMetadata); 22 | -------------------------------------------------------------------------------- /lib/dapr-metadata.accessor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Type } from '@nestjs/common'; 2 | import { Reflector } from '@nestjs/core'; 3 | import { DAPR_BINDING_METADATA, DAPR_PUBSUB_METADATA } from './constants'; 4 | import { DaprBindingMetadata } from './dapr-binding.decorator'; 5 | import { DaprPubSubMetadata } from './dapr-pubsub.decorator'; 6 | 7 | @Injectable() 8 | export class DaprMetadataAccessor { 9 | constructor(private readonly reflector: Reflector) {} 10 | 11 | getDaprPubSubHandlerMetadata( 12 | target: Type, 13 | ): DaprPubSubMetadata | undefined { 14 | return this.reflector.get(DAPR_PUBSUB_METADATA, target); 15 | } 16 | 17 | getDaprBindingHandlerMetadata( 18 | target: Type, 19 | ): DaprBindingMetadata | undefined { 20 | return this.reflector.get(DAPR_BINDING_METADATA, target); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/dapr-pubsub.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { DAPR_PUBSUB_METADATA } from './constants'; 3 | 4 | /** 5 | * `@DaprBinding` decorator metadata 6 | */ 7 | export interface DaprPubSubMetadata { 8 | /** 9 | * Name of pubsub component. 10 | */ 11 | name: string; 12 | 13 | /** 14 | * Topic name to subscribe. 15 | */ 16 | topicName: string; 17 | 18 | /** 19 | * Route to use. 20 | */ 21 | route?: string; 22 | } 23 | 24 | /** 25 | * Dapr pubsub decorator. 26 | * Subscribes to Dapr pubsub topics. 27 | * 28 | * @param name name of pubsub component 29 | * @param topicName topic name to subscribe 30 | */ 31 | export const DaprPubSub = ( 32 | name: string, 33 | topicName: string, 34 | route?: string, 35 | ): MethodDecorator => 36 | SetMetadata(DAPR_PUBSUB_METADATA, { 37 | name, 38 | topicName, 39 | route, 40 | } as DaprPubSubMetadata); 41 | -------------------------------------------------------------------------------- /lib/dapr.loader.ts: -------------------------------------------------------------------------------- 1 | import { DaprPubSubStatusEnum, DaprServer } from '@dapr/dapr'; 2 | import { 3 | Inject, 4 | Injectable, 5 | Logger, 6 | OnApplicationBootstrap, 7 | OnApplicationShutdown, 8 | } from '@nestjs/common'; 9 | import { DiscoveryService, MetadataScanner } from '@nestjs/core'; 10 | import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; 11 | import { DaprMetadataAccessor } from './dapr-metadata.accessor'; 12 | import { DAPR_MODULE_OPTIONS_TOKEN, DaprModuleOptions } from './dapr.module'; 13 | 14 | @Injectable() 15 | export class DaprLoader 16 | implements OnApplicationBootstrap, OnApplicationShutdown 17 | { 18 | private readonly logger = new Logger(DaprLoader.name); 19 | 20 | constructor( 21 | private readonly discoveryService: DiscoveryService, 22 | private readonly daprServer: DaprServer, 23 | private readonly daprMetadataAccessor: DaprMetadataAccessor, 24 | private readonly metadataScanner: MetadataScanner, 25 | @Inject(DAPR_MODULE_OPTIONS_TOKEN) 26 | private readonly options: DaprModuleOptions, 27 | ) {} 28 | 29 | async onApplicationBootstrap() { 30 | this.loadDaprHandlers(); 31 | this.logger.log('Starting Dapr server'); 32 | await this.daprServer.start(); 33 | this.logger.log('Dapr server started'); 34 | } 35 | 36 | async onApplicationShutdown() { 37 | this.logger.log('Stopping Dapr server'); 38 | await this.daprServer.stop(); 39 | this.logger.log('Dapr server stopped'); 40 | } 41 | 42 | loadDaprHandlers() { 43 | const providers = this.discoveryService.getProviders(); 44 | const controllers = this.discoveryService.getControllers(); 45 | [...providers, ...controllers] 46 | .filter((wrapper) => wrapper.isDependencyTreeStatic()) 47 | .filter((wrapper) => wrapper.instance) 48 | .forEach(async (wrapper: InstanceWrapper) => { 49 | const { instance } = wrapper; 50 | const prototype = Object.getPrototypeOf(instance) || {}; 51 | this.metadataScanner.scanFromPrototype( 52 | instance, 53 | prototype, 54 | async (methodKey: string) => { 55 | await this.subscribeToDaprPubSubEventIfListener( 56 | instance, 57 | methodKey, 58 | ); 59 | await this.subscribeToDaprBindingEventIfListener( 60 | instance, 61 | methodKey, 62 | ); 63 | }, 64 | ); 65 | }); 66 | } 67 | 68 | private async subscribeToDaprPubSubEventIfListener( 69 | instance: Record, 70 | methodKey: string, 71 | ) { 72 | const daprPubSubMetadata = 73 | this.daprMetadataAccessor.getDaprPubSubHandlerMetadata( 74 | instance[methodKey], 75 | ); 76 | if (!daprPubSubMetadata) { 77 | return; 78 | } 79 | const { name, topicName, route } = daprPubSubMetadata; 80 | 81 | this.logger.log( 82 | `Subscribing to Dapr: ${name}, Topic: ${topicName}${ 83 | route ? ' on route ' + route : '' 84 | }`, 85 | ); 86 | await this.daprServer.pubsub.subscribe( 87 | name, 88 | topicName, 89 | async (data: any) => { 90 | try { 91 | await instance[methodKey].call(instance, data); 92 | } catch (err) { 93 | if (this.options.onError) { 94 | const response = this.options.onError(name, topicName, err); 95 | if (response == DaprPubSubStatusEnum.RETRY) { 96 | this.logger.debug('Retrying pubsub handler operation'); 97 | } else if (response == DaprPubSubStatusEnum.DROP) { 98 | this.logger.debug('Dropping message'); 99 | } 100 | return response; 101 | } 102 | } 103 | return DaprPubSubStatusEnum.SUCCESS; 104 | }, 105 | route, 106 | ); 107 | } 108 | 109 | private async subscribeToDaprBindingEventIfListener( 110 | instance: Record, 111 | methodKey: string, 112 | ) { 113 | const daprBindingMetadata = 114 | this.daprMetadataAccessor.getDaprBindingHandlerMetadata( 115 | instance[methodKey], 116 | ); 117 | if (!daprBindingMetadata) { 118 | return; 119 | } 120 | const { name } = daprBindingMetadata; 121 | 122 | this.logger.log(`Registering Dapr binding: ${name}`); 123 | await this.daprServer.binding.receive(name, async (data: any) => { 124 | await instance[methodKey].call(instance, data); 125 | }); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /lib/dapr.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CommunicationProtocolEnum, 3 | DaprClient, 4 | DaprPubSubStatusEnum, 5 | DaprServer, 6 | } from '@dapr/dapr'; 7 | import { DaprClientOptions } from '@dapr/dapr/types/DaprClientOptions'; 8 | import { 9 | DynamicModule, 10 | Module, 11 | ModuleMetadata, 12 | Provider, 13 | Type, 14 | } from '@nestjs/common'; 15 | import { DiscoveryModule } from '@nestjs/core'; 16 | import { DaprMetadataAccessor } from './dapr-metadata.accessor'; 17 | import { DaprLoader } from './dapr.loader'; 18 | 19 | export const DAPR_MODULE_OPTIONS_TOKEN = 'DAPR_MODULE_OPTIONS_TOKEN'; 20 | 21 | export interface DaprModuleOptions { 22 | serverHost?: string; 23 | serverPort?: string; 24 | communicationProtocol?: CommunicationProtocolEnum; 25 | clientOptions?: DaprClientOptions; 26 | onError?: ( 27 | name: string, 28 | topicName: string, 29 | error: any, 30 | ) => DaprPubSubStatusEnum; 31 | } 32 | 33 | export interface DaprModuleOptionsFactory { 34 | createDaprModuleOptions(): Promise | DaprModuleOptions; 35 | } 36 | 37 | export function createOptionsProvider(options: DaprModuleOptions): any { 38 | return { provide: DAPR_MODULE_OPTIONS_TOKEN, useValue: options || {} }; 39 | } 40 | 41 | export interface DaprModuleAsyncOptions 42 | extends Pick { 43 | useExisting?: Type; 44 | useClass?: Type; 45 | useFactory?: ( 46 | ...args: any[] 47 | ) => Promise | DaprModuleOptions; 48 | inject?: any[]; 49 | extraProviders?: Provider[]; 50 | } 51 | 52 | @Module({}) 53 | export class DaprModule { 54 | static register(options?: DaprModuleOptions): DynamicModule { 55 | return { 56 | global: true, 57 | module: DaprModule, 58 | imports: [DiscoveryModule], 59 | providers: [ 60 | createOptionsProvider(options), 61 | { 62 | provide: DaprServer, 63 | useValue: new DaprServer({ 64 | serverHost: options.serverHost, 65 | serverPort: options.serverPort, 66 | clientOptions: options.clientOptions, 67 | communicationProtocol: options.communicationProtocol, 68 | }), 69 | }, 70 | { 71 | provide: DaprClient, 72 | useFactory: (daprServer: DaprServer) => daprServer.client, 73 | inject: [DaprServer], 74 | }, 75 | DaprLoader, 76 | DaprMetadataAccessor, 77 | ], 78 | exports: [DaprClient], 79 | }; 80 | } 81 | 82 | static registerAsync(options: DaprModuleAsyncOptions): DynamicModule { 83 | return { 84 | global: true, 85 | module: DaprModule, 86 | imports: [...options.imports, DiscoveryModule], 87 | providers: [ 88 | ...this.createAsyncProviders(options), 89 | { 90 | provide: DaprServer, 91 | useFactory: ({ 92 | serverHost, 93 | serverPort, 94 | communicationProtocol, 95 | clientOptions, 96 | }: DaprModuleOptions) => 97 | new DaprServer({ 98 | serverHost, 99 | serverPort, 100 | clientOptions, 101 | communicationProtocol, 102 | }), 103 | inject: [DAPR_MODULE_OPTIONS_TOKEN], 104 | }, 105 | { 106 | provide: DaprClient, 107 | useFactory: (daprServer: DaprServer) => daprServer.client, 108 | inject: [DaprServer], 109 | }, 110 | DaprLoader, 111 | DaprMetadataAccessor, 112 | ...(options.extraProviders || []), 113 | ], 114 | exports: [DaprClient], 115 | }; 116 | } 117 | 118 | private static createAsyncProviders( 119 | options: DaprModuleAsyncOptions, 120 | ): Provider[] { 121 | if (options.useExisting || options.useFactory) { 122 | return [this.createAsyncOptionsProvider(options)]; 123 | } 124 | return [ 125 | this.createAsyncOptionsProvider(options), 126 | { 127 | provide: options.useClass, 128 | useClass: options.useClass, 129 | }, 130 | ]; 131 | } 132 | 133 | private static createAsyncOptionsProvider( 134 | options: DaprModuleAsyncOptions, 135 | ): Provider { 136 | if (options.useFactory) { 137 | return { 138 | provide: DAPR_MODULE_OPTIONS_TOKEN, 139 | useFactory: options.useFactory, 140 | inject: options.inject || [], 141 | }; 142 | } 143 | return { 144 | provide: DAPR_MODULE_OPTIONS_TOKEN, 145 | useFactory: async (optionsFactory: DaprModuleOptionsFactory) => 146 | optionsFactory.createDaprModuleOptions(), 147 | inject: [options.useExisting || options.useClass], 148 | }; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import { DAPR_BINDING_METADATA, DAPR_PUBSUB_METADATA } from './constants'; 2 | import { DaprBinding, DaprBindingMetadata } from './dapr-binding.decorator'; 3 | import { DaprMetadataAccessor } from './dapr-metadata.accessor'; 4 | import { DaprPubSub, DaprPubSubMetadata } from './dapr-pubsub.decorator'; 5 | import { DaprLoader } from './dapr.loader'; 6 | import { DaprModule } from './dapr.module'; 7 | 8 | export { 9 | DAPR_BINDING_METADATA, 10 | DAPR_PUBSUB_METADATA, 11 | DaprMetadataAccessor, 12 | DaprBindingMetadata, 13 | DaprBinding, 14 | DaprPubSubMetadata, 15 | DaprPubSub, 16 | DaprLoader, 17 | DaprModule, 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dbc-tech/nest-dapr", 3 | "version": "0.8.0", 4 | "description": "Develop NestJs microservices using Dapr pubsub and bindings", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "build": "rimraf -rf dist && tsc -p tsconfig.json", 9 | "lint": "eslint \"{lib,tests}/**/*.ts\" --fix", 10 | "test": "echo todo", 11 | "push:version": "git push --follow-tags" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/dbc-tech/nest-dapr.git" 16 | }, 17 | "author": "Neil Dobson", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/dbc-tech/nest-dapr/issues" 21 | }, 22 | "homepage": "https://github.com/dbc-tech/nest-dapr#readme", 23 | "peerDependencies": { 24 | "@nestjs/common": "^9.0.0", 25 | "@nestjs/core": "^9.0.0" 26 | }, 27 | "devDependencies": { 28 | "@typescript-eslint/eslint-plugin": "^5.29.0", 29 | "eslint": "^8.4.1", 30 | "eslint-config-prettier": "^8.3.0", 31 | "eslint-plugin-import": "^2.25.4", 32 | "eslint-plugin-jest": "^25.7.0", 33 | "eslint-plugin-prettier": "^4.0.0", 34 | "eslint-plugin-promise": "^6.0.0", 35 | "jest": "^27.4.4", 36 | "rimraf": "^3.0.2", 37 | "typescript": "^4.7.4" 38 | }, 39 | "dependencies": { 40 | "@dapr/dapr": "^3.1.1" 41 | }, 42 | "publishConfig": { 43 | "access": "public", 44 | "registry": "https://registry.npmjs.org/" 45 | }, 46 | "files": [ 47 | "dist" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "strict": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "emitDecoratorMetadata": true, 9 | "esModuleInterop": true, 10 | "experimentalDecorators": true, 11 | "target": "es6", 12 | "sourceMap": false, 13 | "outDir": "./dist", 14 | "rootDir": "./lib", 15 | "skipLibCheck": true 16 | }, 17 | "include": ["lib/**/*"], 18 | "exclude": ["node_modules", "**/*.spec.ts", "tests"] 19 | } --------------------------------------------------------------------------------