├── src ├── index.ts └── lib │ ├── adapter │ ├── index.ts │ └── adapter.interface.ts │ ├── contract │ ├── shared-store.interface.ts │ ├── index.ts │ ├── nestjs-event-store.interface.ts │ ├── nestjs-event-store.constant.ts │ ├── event-store-connect-config.interface.ts │ └── event-store-option.config.ts │ ├── stores │ ├── index.ts │ ├── nats-event-store.ts │ └── event-store.ts │ ├── brokers │ ├── index.ts │ ├── event-store.broker.ts │ └── nats-event-store.broker.ts │ ├── index.ts │ ├── event-store.module.ts │ └── event-store-core.module.ts ├── .prettierignore ├── .gitignore ├── .vscode ├── settings.json ├── debug-ts.js └── launch.json ├── .npmignore ├── tsconfig.module.json ├── .travis.yml ├── .editorconfig ├── tslint.json ├── tsconfig.json ├── LICENSE ├── .circleci └── config.yml ├── .github └── workflows │ └── codeql-analysis.yml ├── CHANGELOG.md ├── package.json └── README.md /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib'; 2 | -------------------------------------------------------------------------------- /src/lib/adapter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapter.interface'; 2 | -------------------------------------------------------------------------------- /src/lib/contract/shared-store.interface.ts: -------------------------------------------------------------------------------- 1 | export type BrokerTypes = 'event-store' | 'nats'; 2 | -------------------------------------------------------------------------------- /src/lib/stores/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nats-event-store'; 2 | export * from './event-store'; 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # package.json is formatted by package managers, so we ignore it here 2 | package.json -------------------------------------------------------------------------------- /src/lib/brokers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event-store.broker'; 2 | export * from './nats-event-store.broker'; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | test 4 | src/**.js 5 | .idea/* 6 | 7 | coverage 8 | .nyc_output 9 | *.log 10 | 11 | package-lock.json -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | // "typescript.implementationsCodeLens.enabled": true 4 | // "typescript.referencesCodeLens.enabled": true 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | export * from './event-store.module'; 4 | export * from './contract'; 5 | export * from './adapter'; 6 | export * from './stores/event-store'; 7 | -------------------------------------------------------------------------------- /src/lib/adapter/adapter.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IAdapterStore { 2 | storeKey: string; 3 | write(key: string, value: number): Promise; 4 | read(key: string): Promise; 5 | clear(): number; 6 | } 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | tsconfig.json 4 | tsconfig.module.json 5 | tslint.json 6 | .travis.yml 7 | .github 8 | .prettierignore 9 | .vscode 10 | build/docs 11 | **/*.spec.* 12 | coverage 13 | .nyc_output 14 | *.log 15 | -------------------------------------------------------------------------------- /tsconfig.module.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "outDir": "build/module", 6 | "module": "esnext" 7 | }, 8 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '10' 5 | - '12' 6 | # keep the npm cache to speed up installs 7 | cache: 8 | directories: 9 | - '$HOME/.npm' 10 | after_success: 11 | - npm run cov:send 12 | - npm run cov:check 13 | -------------------------------------------------------------------------------- /src/lib/contract/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nestjs-event-store.constant'; 2 | export * from './event-store-connect-config.interface'; 3 | export * from './nestjs-event-store.constant'; 4 | export * from './event-store-option.config'; 5 | export * from './shared-store.interface'; 6 | -------------------------------------------------------------------------------- /src/lib/contract/nestjs-event-store.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IEventStoreMessage { 2 | streamId: string; 3 | eventId: string; 4 | eventNumber: number; 5 | eventType: string; 6 | created: Date; 7 | metadata: object; 8 | isJson: boolean; 9 | data: object; 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "jsRules": { 4 | "no-unused-expression": true 5 | }, 6 | "rules": { 7 | "quotemark": [true, "single"], 8 | "member-access": [false], 9 | "ordered-imports": [false], 10 | "max-line-length": [true, 150], 11 | "member-ordering": [false], 12 | "interface-name": [false], 13 | "arrow-parens": false, 14 | "object-literal-sort-keys": false 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/contract/nestjs-event-store.constant.ts: -------------------------------------------------------------------------------- 1 | export const ProvidersConstants = { 2 | EVENT_STORE_PROVIDER: 'EVENT_STORE_PROVIDER', 3 | EVENT_STORE_STREAM_CONFIG_PROVIDER: 'EsStreamConfig', 4 | EVENT_STORE_CONNECTION_CONFIG_PROVIDER: 'EsConnectionConfig' 5 | }; 6 | 7 | export const NEST_EVENTSTORE_OPTION = 'NEST_EVENTSTORE_OPTION'; 8 | 9 | export const NEST_EVENTSTORE_FEATURE_OPTION = 'NEST_EVENTSTORE_FEATURE_OPTION'; 10 | 11 | export const NEST_EVENTSTORE_MODULE_ID = 'NEST_EVENTSTORE_MODULE_ID'; 12 | export const EVENT_STORE_CONFIG = 'EVENT_STORE_CONFIG'; 13 | 14 | export const EVENT_STORE_STORAGE_ADAPTER = 'EVENT_STORE_STORAGE_ADAPTER'; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "removeComments": true, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "sourceMap": true, 8 | "target": "es2017", 9 | "outDir": "build/main", 10 | "rootDir": "src", 11 | "incremental": true, 12 | "moduleResolution": "node", 13 | 14 | "module": "commonjs", 15 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 16 | 17 | /* Experimental Options */ 18 | // "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 19 | // "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, 20 | 21 | "lib": ["es2017"], 22 | "types": ["node"], 23 | "typeRoots": ["node_modules/@types", "src/types"] 24 | }, 25 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"], 26 | "compileOnSave": false 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Rex Isaac Raphael 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 | -------------------------------------------------------------------------------- /.vscode/debug-ts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const meow = require('meow'); 3 | const path = require('path'); 4 | 5 | const tsFile = getTSFile(); 6 | const jsFile = TS2JS(tsFile); 7 | 8 | replaceCLIArg(tsFile, jsFile); 9 | 10 | // Ava debugger 11 | require('ava/profile'); 12 | 13 | /** 14 | * get ts file path from CLI args 15 | * 16 | * @return string path 17 | */ 18 | function getTSFile() { 19 | const cli = meow(); 20 | return cli.input[0]; 21 | } 22 | 23 | /** 24 | * get associated compiled js file path 25 | * 26 | * @param tsFile path 27 | * @return string path 28 | */ 29 | function TS2JS(tsFile) { 30 | const srcFolder = path.join(__dirname, '..', 'src'); 31 | const distFolder = path.join(__dirname, '..', 'build', 'main'); 32 | 33 | const tsPathObj = path.parse(tsFile); 34 | 35 | return path.format({ 36 | dir: tsPathObj.dir.replace(srcFolder, distFolder), 37 | ext: '.js', 38 | name: tsPathObj.name, 39 | root: tsPathObj.root 40 | }); 41 | } 42 | 43 | /** 44 | * replace a value in CLI args 45 | * 46 | * @param search value to search 47 | * @param replace value to replace 48 | * @return void 49 | */ 50 | function replaceCLIArg(search, replace) { 51 | process.argv[process.argv.indexOf(search)] = replace; 52 | } -------------------------------------------------------------------------------- /src/lib/event-store.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module } from '@nestjs/common'; 2 | import { CqrsModule } from '@nestjs/cqrs'; 3 | import { 4 | EventStoreModuleOptions, 5 | EventStoreModuleAsyncOptions, 6 | EventStoreOptionConfig, 7 | EventStoreFeatureAsyncOptions 8 | } from './contract'; 9 | import { EventStoreCoreModule } from './event-store-core.module'; 10 | 11 | @Module({ 12 | imports: [CqrsModule] 13 | }) 14 | export class EventStoreModule { 15 | static register(option: EventStoreModuleOptions): DynamicModule { 16 | return { 17 | module: EventStoreModule, 18 | imports: [EventStoreCoreModule.register(option)] 19 | }; 20 | } 21 | 22 | static registerAsync(option: EventStoreModuleAsyncOptions): DynamicModule { 23 | return { 24 | module: EventStoreModule, 25 | imports: [EventStoreCoreModule.registerAsync(option)] 26 | }; 27 | } 28 | 29 | static registerFeature(config: EventStoreOptionConfig): DynamicModule { 30 | return { 31 | module: EventStoreModule, 32 | imports: [EventStoreCoreModule.registerFeature(config)] 33 | }; 34 | } 35 | 36 | static registerFeatureAsync( 37 | options: EventStoreFeatureAsyncOptions 38 | ): DynamicModule { 39 | return { 40 | module: EventStoreModule, 41 | imports: [EventStoreCoreModule.registerFeatureAsync(options)] 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [{ 4 | "type": "node", 5 | "request": "launch", 6 | "name": "Debug Project", 7 | // we test in `build` to make cleanup fast and easy 8 | "cwd": "${workspaceFolder}/build", 9 | // Replace this with your project root. If there are multiple, you can 10 | // automatically run the currently visible file with: "program": ${file}" 11 | "program": "${workspaceFolder}/src/cli/cli.ts", 12 | // "args": ["--no-install"], 13 | "outFiles": ["${workspaceFolder}/build/main/**/*.js"], 14 | "skipFiles": [ 15 | "/**/*.js", 16 | "${workspaceFolder}/node_modules/**/*.js" 17 | ], 18 | "preLaunchTask": "npm: build", 19 | "stopOnEntry": true, 20 | "smartStep": true, 21 | "runtimeArgs": ["--nolazy"], 22 | "env": { 23 | "TYPESCRIPT_STARTER_REPO_URL": "${workspaceFolder}" 24 | }, 25 | "console": "externalTerminal" 26 | }, 27 | { 28 | "type": "node", 29 | "request": "launch", 30 | "name": "Debug Spec", 31 | "program": "${workspaceRoot}/.vscode/debug-ts.js", 32 | "args": ["${file}"], 33 | "skipFiles": ["/**/*.js"], 34 | // Consider using `npm run watch` or `yarn watch` for faster debugging 35 | // "preLaunchTask": "npm: build", 36 | // "smartStep": true, 37 | "runtimeArgs": ["--nolazy"] 38 | }] 39 | } -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # https://circleci.com/docs/2.0/language-javascript/ 2 | version: 2 3 | jobs: 4 | 'node-10': 5 | docker: 6 | - image: circleci/node:10 7 | working_directory: ~/typescript-starter 8 | steps: 9 | - checkout 10 | # Download and cache dependencies 11 | - restore_cache: 12 | keys: 13 | - v1-dependencies-{{ checksum "package.json" }} 14 | # fallback to using the latest cache if no exact match is found 15 | - v1-dependencies- 16 | - run: npm install 17 | - save_cache: 18 | paths: 19 | - node_modules 20 | key: v1-dependencies-{{ checksum "package.json" }} 21 | - run: npm test 22 | - run: npm run cov:send 23 | - run: npm run cov:check 24 | 'node-latest': 25 | docker: 26 | - image: circleci/node:latest 27 | working_directory: ~/typescript-starter 28 | steps: 29 | - checkout 30 | - restore_cache: 31 | keys: 32 | - v1-dependencies-{{ checksum "package.json" }} 33 | - v1-dependencies- 34 | - run: npm install 35 | - save_cache: 36 | paths: 37 | - node_modules 38 | key: v1-dependencies-{{ checksum "package.json" }} 39 | - run: npm test 40 | - run: npm run cov:send 41 | - run: npm run cov:check 42 | 43 | workflows: 44 | version: 2 45 | build: 46 | jobs: 47 | - 'node-10' 48 | - 'node-latest' 49 | -------------------------------------------------------------------------------- /src/lib/contract/event-store-connect-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { ModuleMetadata, Type } from '@nestjs/common/interfaces'; 2 | import { ConnectionSettings, TcpEndPoint } from 'node-eventstore-client'; 3 | import { EventStoreOptionConfig } from './event-store-option.config'; 4 | import { ClientOpts } from 'node-nats-streaming'; 5 | import { BrokerTypes } from './shared-store.interface'; 6 | 7 | export type EventStoreModuleOptions = 8 | | { 9 | type: 'event-store'; 10 | options: ConnectionSettings; 11 | tcpEndpoint: TcpEndPoint; 12 | } 13 | | { 14 | type: 'nats'; 15 | clusterId: string; 16 | clientId?: string; 17 | groupId?: string; 18 | options: ClientOpts; 19 | }; 20 | 21 | export interface EventStoreOptionsFactory { 22 | createEventStoreOptions( 23 | connectionName?: string 24 | ): Promise | EventStoreModuleOptions; 25 | } 26 | 27 | export interface EventStoreModuleAsyncOptions 28 | extends Pick { 29 | type: BrokerTypes; 30 | name?: string; 31 | useExisting?: Type; 32 | useClass?: Type; 33 | useFactory?: ( 34 | ...args: any[] 35 | ) => Promise | EventStoreModuleOptions; 36 | inject?: any[]; 37 | } 38 | 39 | export interface EventStoreFeatureOptionsFactory { 40 | createFeatureOptions( 41 | connectionName?: string 42 | ): Promise | EventStoreOptionConfig; 43 | } 44 | 45 | export interface EventStoreFeatureAsyncOptions 46 | extends Pick { 47 | name?: string; 48 | type: BrokerTypes; 49 | useExisting?: Type; 50 | useClass?: Type; 51 | useFactory?: ( 52 | ...args: any[] 53 | ) => Promise | EventStoreOptionConfig; 54 | inject?: any[]; 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/brokers/event-store.broker.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import assert from 'assert'; 3 | import * as uuid from 'uuid'; 4 | import { 5 | EventStoreNodeConnection, 6 | ConnectionSettings, 7 | TcpEndPoint, 8 | createConnection 9 | } from 'node-eventstore-client'; 10 | import { BrokerTypes } from '../contract'; 11 | 12 | /** 13 | * @description Event store setup from eventstore.org 14 | */ 15 | export class EventStoreBroker { 16 | [x: string]: any; 17 | private logger: Logger = new Logger(this.constructor.name); 18 | private client: EventStoreNodeConnection; 19 | public isConnected: boolean; 20 | type: BrokerTypes; 21 | 22 | constructor() { 23 | this.type = 'event-store'; 24 | } 25 | 26 | connect(options: ConnectionSettings, endpoint: TcpEndPoint) { 27 | try { 28 | this.client = createConnection(options, endpoint); 29 | this.client.connect(); 30 | 31 | this.client.on('connected', () => { 32 | this.isConnected = true; 33 | this.logger.log('EventStore connected!'); 34 | }); 35 | this.client.on('closed', () => { 36 | this.isConnected = false; 37 | this.logger.error('EventStore closed!'); 38 | this.connect(options, endpoint); 39 | }); 40 | 41 | return this; 42 | } catch (e) { 43 | this.logger.error(e); 44 | throw new Error(e); 45 | } 46 | } 47 | 48 | getClient(): EventStoreNodeConnection { 49 | return this.client; 50 | } 51 | 52 | newEvent(name, payload) { 53 | return this.newEventBuilder(name, payload); 54 | } 55 | 56 | private newEventBuilder(eventType, data, metadata?, eventId?) { 57 | assert(eventType); 58 | assert(data); 59 | 60 | const event: { 61 | eventId: string | any; 62 | eventType?: string | any; 63 | data?: any; 64 | metadata?: any; 65 | } = { 66 | eventId: eventId || uuid.v4(), 67 | eventType, 68 | data 69 | }; 70 | 71 | if (metadata !== undefined) { 72 | event.metadata = metadata; 73 | } 74 | return event; 75 | } 76 | 77 | /** 78 | * Close event store client 79 | */ 80 | close() { 81 | this.client.close(); 82 | return this; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/lib/brokers/nats-event-store.broker.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import assert from 'assert'; 3 | import * as uuid from 'uuid'; 4 | import { ClientOpts, connect as stanConnect, Stan } from 'node-nats-streaming'; 5 | import { BrokerTypes } from '../contract'; 6 | 7 | /** 8 | * @description Event store setup from NATS 9 | */ 10 | export class NatsEventStoreBroker { 11 | [x: string]: any; 12 | private logger: Logger = new Logger(this.constructor.name); 13 | private client: Stan; 14 | public isConnected: boolean; 15 | type: BrokerTypes; 16 | 17 | constructor() { 18 | this.type = 'nats'; 19 | this.clientId = uuid.v4(); 20 | } 21 | 22 | connect(clusterId: string, clientId: string, options: ClientOpts) { 23 | try { 24 | if (clientId) { 25 | this.clientId = clientId; 26 | } 27 | 28 | this.client = stanConnect(clusterId, this.clientId, options); 29 | 30 | this.client.on('connect', () => { 31 | this.isConnected = true; 32 | this.logger.log('Nats Streaming EventStore connected!'); 33 | }); 34 | this.client.on('disconnect:', () => { 35 | this.isConnected = false; 36 | this.logger.error('Nats Streaming EventStore disconnected!'); 37 | this.connect(clusterId, this.clientId, options); 38 | }); 39 | this.client.on('close:', () => { 40 | this.isConnected = false; 41 | this.logger.error('Nats Streaming EventStore closed!'); 42 | this.connect(clusterId, this.clientId, options); 43 | }); 44 | 45 | return this; 46 | } catch (e) { 47 | this.logger.error(e); 48 | throw new Error(e); 49 | } 50 | } 51 | 52 | getClient(): Stan { 53 | return this.client; 54 | } 55 | 56 | newEvent(name, payload) { 57 | return this.newEventBuilder(name, payload); 58 | } 59 | 60 | private newEventBuilder(eventType, data, metadata?, eventId?) { 61 | assert(eventType); 62 | assert(data); 63 | 64 | const event: { 65 | eventId: string | any; 66 | eventType?: string | any; 67 | data?: any; 68 | metadata?: any; 69 | } = { 70 | eventId: eventId || uuid.v4(), 71 | eventType, 72 | data 73 | }; 74 | 75 | if (metadata !== undefined) { 76 | event.metadata = metadata; 77 | } 78 | return event; 79 | } 80 | 81 | /** 82 | * Close event store connection 83 | */ 84 | close() { 85 | this.client.close(); 86 | return this; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '25 15 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [3.1.17](https://github.com/juicycleff/nestjs-event-store/compare/v3.1.14...v3.1.17) (2020-08-09) 6 | 7 | 8 | 9 | ### [3.1.15](https://github.com/juicycleff/nestjs-event-store/compare/v3.1.12...v3.1.15) (2020-08-09) 10 | 11 | 12 | 13 | ### [3.1.13](https://github.com/juicycleff/nestjs-event-store/compare/v3.1.10...v3.1.13) (2020-08-09) 14 | 15 | 16 | 17 | ### [3.1.11](https://github.com/juicycleff/nestjs-event-store/compare/v3.1.8...v3.1.11) (2020-08-09) 18 | 19 | 20 | 21 | ### [3.1.9](https://github.com/juicycleff/nestjs-event-store/compare/v3.1.6...v3.1.9) (2020-08-09) 22 | 23 | 24 | 25 | ### [3.1.7](https://github.com/juicycleff/nestjs-event-store/compare/v3.1.4...v3.1.7) (2020-08-09) 26 | 27 | 28 | 29 | ### [3.1.5](https://github.com/juicycleff/nestjs-event-store/compare/v3.1.2...v3.1.5) (2020-08-09) 30 | 31 | 32 | 33 | ### [3.1.3](https://github.com/juicycleff/nestjs-event-store/compare/v3.1.0...v3.1.3) (2020-08-09) 34 | 35 | 36 | 37 | ### [3.1.1](https://github.com/juicycleff/nestjs-event-store/compare/v3.0.29...v3.1.1) (2020-08-09) 38 | 39 | 40 | 41 | ### [3.0.30](https://github.com/juicycleff/nestjs-event-store/compare/v3.0.27...v3.0.30) (2020-08-09) 42 | 43 | 44 | 45 | ### [3.0.28](https://github.com/juicycleff/nestjs-event-store/compare/v3.0.25...v3.0.28) (2020-08-09) 46 | 47 | 48 | 49 | ### [3.0.26](https://github.com/juicycleff/nestjs-event-store/compare/v3.0.23...v3.0.26) (2020-08-09) 50 | 51 | 52 | 53 | ### [3.0.24](https://github.com/juicycleff/nestjs-event-store/compare/v3.0.22...v3.0.24) (2020-08-09) 54 | 55 | 56 | 57 | ### [3.0.22](https://github.com/juicycleff/nestjs-event-store/compare/v3.0.20...v3.0.22) (2020-08-09) 58 | 59 | 60 | 61 | ### [3.0.20](https://github.com/juicycleff/nestjs-event-store/compare/v3.0.18...v3.0.20) (2020-08-09) 62 | 63 | 64 | 65 | ### [3.0.18](https://github.com/juicycleff/nestjs-event-store/compare/v3.0.16...v3.0.18) (2020-08-09) 66 | 67 | 68 | 69 | ### [3.0.16](https://github.com/juicycleff/nestjs-event-store/compare/v3.0.14...v3.0.16) (2020-08-09) 70 | 71 | 72 | 73 | ### [3.0.14](https://github.com/juicycleff/nestjs-event-store/compare/v3.0.12...v3.0.14) (2020-08-09) 74 | 75 | 76 | 77 | ### [3.0.12](https://github.com/juicycleff/nestjs-event-store/compare/v3.0.10...v3.0.12) (2020-08-09) 78 | 79 | 80 | 81 | ### [3.0.10](https://github.com/juicycleff/nestjs-event-store/compare/v3.0.8...v3.0.10) (2020-08-09) 82 | 83 | 84 | 85 | ### [3.0.8](https://github.com/juicycleff/nestjs-event-store/compare/v3.0.6...v3.0.8) (2020-08-09) 86 | 87 | 88 | 89 | ### [3.0.6](https://github.com/juicycleff/nestjs-event-store/compare/v3.0.7...v3.0.6) (2020-08-09) 90 | 91 | 92 | 93 | ### 3.0.7 (2020-08-09) 94 | 95 | 96 | 97 | ### [3.0.5](https://github.com/juicycleff/nestjs-event-store/compare/v3.0.3...v3.0.5) (2020-04-20) 98 | 99 | 100 | 101 | ### [3.0.3](https://github.com/juicycleff/nestjs-event-store/compare/v3.0.0...v3.0.3) (2020-03-25) 102 | 103 | 104 | 105 | ### 3.0.1 (2020-03-25) 106 | -------------------------------------------------------------------------------- /src/lib/contract/event-store-option.config.ts: -------------------------------------------------------------------------------- 1 | import { IEvent } from '@nestjs/cqrs'; 2 | import * as Long from 'long'; 3 | import { 4 | EventStoreCatchUpSubscription, 5 | EventStorePersistentSubscription as ESStorePersistentSub, 6 | EventStoreSubscription as ESVolatileSubscription 7 | } from 'node-eventstore-client'; 8 | import { IAdapterStore } from '../adapter'; 9 | import { Subscription } from 'node-nats-streaming'; 10 | 11 | export enum EventStoreSubscriptionType { 12 | Persistent, 13 | CatchUp, 14 | Volatile 15 | } 16 | 17 | export interface EventStorePersistentSubscription { 18 | type: EventStoreSubscriptionType.Persistent; 19 | stream: string; 20 | persistentSubscriptionName: string; 21 | resolveLinkTos?: boolean; 22 | } 23 | 24 | export interface EventStoreCatchupSubscription { 25 | type: EventStoreSubscriptionType.CatchUp; 26 | stream: string; 27 | resolveLinkTos?: boolean; 28 | lastCheckpoint?: Long | number | null; 29 | } 30 | 31 | export interface EventStoreVolatileSubscription { 32 | type: EventStoreSubscriptionType.Volatile; 33 | stream: string; 34 | resolveLinkTos?: boolean; 35 | } 36 | 37 | export interface NatsEventStorePersistentSubscription { 38 | type: EventStoreSubscriptionType.Persistent; 39 | stream: string; 40 | durableName: string; 41 | maxInflight?: number; 42 | startAt?: number; 43 | ackWait?: number, 44 | manualAcks?: boolean 45 | } 46 | 47 | export interface NatsEventStoreVolatileSubscription { 48 | type: EventStoreSubscriptionType.Volatile; 49 | stream: string; 50 | startAt?: number, 51 | maxInflight?: number, 52 | ackWait?: number, 53 | manualAcks?: boolean 54 | } 55 | 56 | export type EventStoreSubscription = 57 | | EventStorePersistentSubscription 58 | | EventStoreCatchupSubscription 59 | | EventStoreVolatileSubscription; 60 | 61 | export type NatsEventStoreSubscription = 62 | | NatsEventStorePersistentSubscription 63 | | NatsEventStoreVolatileSubscription; 64 | 65 | export interface IEventConstructors { 66 | [key: string]: (...args: any[]) => IEvent; 67 | } 68 | 69 | export interface ExtendedCatchUpSubscription 70 | extends EventStoreCatchUpSubscription { 71 | type: 'catch'; 72 | isLive: boolean | undefined; 73 | } 74 | 75 | export interface ExtendedPersistentSubscription extends ESStorePersistentSub { 76 | type: 'persistent'; 77 | isLive?: boolean | undefined; 78 | } 79 | 80 | export interface ExtendedVolatileSubscription extends ESVolatileSubscription { 81 | type: 'volatile'; 82 | isLive?: boolean | undefined; 83 | } 84 | 85 | export interface ExtendedNatsPersistentSubscription extends Subscription { 86 | type: 'persistent'; 87 | isLive?: boolean | undefined; 88 | } 89 | 90 | export interface ExtendedNatsVolatileSubscription extends Subscription { 91 | type: 'volatile'; 92 | isLive?: boolean | undefined; 93 | } 94 | 95 | export type EventStoreOptionConfig = 96 | | { 97 | type: 'event-store'; 98 | subscriptions: EventStoreSubscription[]; 99 | eventHandlers: IEventConstructors; 100 | featureStreamName?: string; 101 | store?: IAdapterStore; 102 | } 103 | | { 104 | type: 'nats'; 105 | subscriptions: NatsEventStoreSubscription[]; 106 | eventHandlers: IEventConstructors; 107 | featureStreamName?: string; 108 | }; 109 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@juicycleff/nestjs-event-store", 3 | "version": "4.0.0", 4 | "description": "NestJS CQRS module for EventStore.org CQRS", 5 | "author": { 6 | "name": "Rex Isaac Raphael", 7 | "email": "rex.rahael@outlook.com", 8 | "url": "https://xraph.com" 9 | }, 10 | "main": "build/main/index.js", 11 | "typings": "build/main/index.d.ts", 12 | "module": "build/module/index.js", 13 | "repository": "https://github.com/juicycleff/nestjs-event-store", 14 | "license": "MIT", 15 | "keywords": [], 16 | "scripts": { 17 | "describe": "npm-scripts-info", 18 | "build": "run-s clean && run-p build:*", 19 | "build:main": "tsc -p tsconfig.json", 20 | "build:module": "tsc -p tsconfig.module.json", 21 | "fix": "run-s fix:*", 22 | "fix:prettier": "prettier \"src/**/*.ts\" --write", 23 | "fix:tslint": "tslint --fix --project .", 24 | "test:lint": "tslint --project . && prettier \"src/**/*.ts\" --list-different", 25 | "test": "jest", 26 | "test:watch": "jest --watch", 27 | "test:cov": "jest --coverage && cat ./coverage/lcov.info | codacy-coverage --token a5ea7c309f7543a3b7cfc662c1c88d67 --language typescript", 28 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 29 | "watch": "run-s clean build:main && run-p \"build:main -- -w\" \"test:unit -- --watch\"", 30 | "doc": "run-s doc:html && open-cli build/docs/index.html", 31 | "doc:html": "typedoc src/ --exclude **/*.spec.ts --target ES6 --mode file --out build/docs", 32 | "doc:json": "typedoc src/ --exclude **/*.spec.ts --target ES6 --mode file --json build/docs/typedoc.json", 33 | "doc:publish": "gh-pages -m \"[ci skip] Updates\" -d build/docs", 34 | "version": "standard-version", 35 | "reset": "git clean -dfx && git reset --hard && npm i", 36 | "clean": "trash build test", 37 | "prepare-release": "run-s reset test cov:check doc:html version doc:publish" 38 | }, 39 | "scripts-info": { 40 | "info": "Display information about the package scripts", 41 | "build": "Clean and rebuild the project", 42 | "fix": "Try to automatically fix any linting problems", 43 | "test": "Lint and unit test the project", 44 | "watch": "Watch and rebuild the project on save, then rerun relevant tests", 45 | "cov": "Rebuild, run tests, then create and open the coverage report", 46 | "doc": "Generate HTML API documentation and open it in a browser", 47 | "doc:json": "Generate API documentation in typedoc JSON format", 48 | "version": "Bump package.json version, update CHANGELOG.md, tag release", 49 | "reset": "Delete all untracked files and reset the repo to the last commit", 50 | "prepare-release": "One-step: clean, build, test, publish docs, and prep a release" 51 | }, 52 | "engines": { 53 | "node": ">=14" 54 | }, 55 | "dependencies": { 56 | "node-eventstore-client": "^0.2.16", 57 | "node-nats-streaming": "^0.3.2", 58 | "uuid": "^8.3.0" 59 | }, 60 | "peerDependencies": { 61 | "@nestjs/common": "^9.0.0", 62 | "@nestjs/core": "^9.0.0", 63 | "@nestjs/cqrs": "^9.0.0", 64 | "reflect-metadata": "^0.1.13", 65 | "rxjs": "^7.1.0" 66 | }, 67 | "devDependencies": { 68 | "@bitjson/npm-scripts-info": "^1.0.0", 69 | "@bitjson/typedoc": "^0.15.0-0", 70 | "@nestjs/cli": "^9.0.0", 71 | "@nestjs/testing": "^9.0.0", 72 | "@types/jest": "^28.0.0", 73 | "@types/lodash": "^4.14.144", 74 | "@types/node": "^12.7.5", 75 | "@types/supertest": "^2.0.8", 76 | "codecov": "^3.5.0", 77 | "cz-conventional-changelog": "^2.1.0", 78 | "gh-pages": "^2.0.1", 79 | "jest": "^28.1.2", 80 | "npm-run-all": "^4.1.5", 81 | "nyc": "^14.1.1", 82 | "open-cli": "^5.0.0", 83 | "prettier": "^1.18.2", 84 | "remark-cli": "^7.0.0", 85 | "remark-lint-emphasis-marker": "^1.0.3", 86 | "remark-lint-strong-marker": "^1.0.3", 87 | "remark-preset-lint-recommended": "^3.0.3", 88 | "standard-version": "^6.0.1", 89 | "supertest": "^4.0.2", 90 | "trash-cli": "^3.0.0", 91 | "ts-jest": "^28.0.0", 92 | "ts-loader": "^6.1.1", 93 | "ts-node": "^8.4.1", 94 | "tsconfig-paths": "^3.9.0", 95 | "tslint": "^5.20.0", 96 | "tslint-config-prettier": "^1.18.0", 97 | "tslint-immutable": "^6.0.1", 98 | "typescript": "^4.7.4" 99 | }, 100 | "jest": { 101 | "globals": { 102 | "ts-jest": { 103 | "diagnostics": false 104 | } 105 | }, 106 | "moduleFileExtensions": [ 107 | "js", 108 | "json", 109 | "ts" 110 | ], 111 | "rootDir": ".", 112 | "testRegex": ".spec.ts$", 113 | "transform": { 114 | "^.+\\.(t|j)s$": "ts-jest" 115 | }, 116 | "coverageDirectory": "./coverage", 117 | "testEnvironment": "node" 118 | }, 119 | "config": { 120 | "commitizen": { 121 | "path": "cz-conventional-changelog" 122 | } 123 | }, 124 | "prettier": { 125 | "singleQuote": true 126 | }, 127 | "nyc": { 128 | "extends": "@istanbuljs/nyc-config-typescript", 129 | "exclude": [ 130 | "**/*.spec.js" 131 | ] 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/lib/event-store-core.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Global, Module, Provider, Type } from '@nestjs/common'; 2 | import { 3 | EventStoreFeatureAsyncOptions, 4 | EventStoreFeatureOptionsFactory, 5 | EventStoreModuleAsyncOptions, 6 | EventStoreModuleOptions, 7 | EventStoreOptionConfig, 8 | EventStoreOptionsFactory, 9 | NEST_EVENTSTORE_FEATURE_OPTION, 10 | NEST_EVENTSTORE_OPTION, 11 | ProvidersConstants 12 | } from './contract'; 13 | import { EventStore, NatsEventStore } from './stores'; 14 | import { EventStoreBroker, NatsEventStoreBroker } from './brokers'; 15 | import { CqrsModule } from '@nestjs/cqrs'; 16 | 17 | @Global() 18 | @Module({ 19 | imports: [CqrsModule] 20 | }) 21 | export class EventStoreCoreModule { 22 | static register(option: EventStoreModuleOptions): DynamicModule { 23 | const eventStoreProviders = { 24 | provide: ProvidersConstants.EVENT_STORE_PROVIDER, 25 | useFactory: (): any => { 26 | switch (option.type) { 27 | case 'event-store': 28 | return new EventStoreBroker(); 29 | case 'nats': 30 | return new NatsEventStoreBroker(); 31 | default: 32 | throw new Error('event store broker type missing'); 33 | } 34 | } 35 | }; 36 | 37 | const configProv = { 38 | provide: ProvidersConstants.EVENT_STORE_CONNECTION_CONFIG_PROVIDER, 39 | useValue: { 40 | ...option 41 | } 42 | }; 43 | 44 | return { 45 | module: EventStoreCoreModule, 46 | providers: [eventStoreProviders, configProv], 47 | exports: [eventStoreProviders, configProv] 48 | }; 49 | } 50 | 51 | static registerAsync(options: EventStoreModuleAsyncOptions): DynamicModule { 52 | const eventStoreProviders = { 53 | provide: ProvidersConstants.EVENT_STORE_PROVIDER, 54 | useFactory: (): any => { 55 | switch (options.type) { 56 | case 'event-store': 57 | return new EventStoreBroker(); 58 | case 'nats': 59 | return new NatsEventStoreBroker(); 60 | default: 61 | throw new Error('event store broker type missing'); 62 | } 63 | } 64 | }; 65 | 66 | const configProv: Provider = { 67 | provide: ProvidersConstants.EVENT_STORE_CONNECTION_CONFIG_PROVIDER, 68 | useFactory: async (esOptions: EventStoreModuleOptions) => { 69 | return { 70 | ...esOptions 71 | }; 72 | }, 73 | inject: [NEST_EVENTSTORE_OPTION] 74 | }; 75 | 76 | const asyncProviders = this.createAsyncProviders(options); 77 | 78 | return { 79 | module: EventStoreCoreModule, 80 | imports: options.imports, 81 | providers: [...asyncProviders, eventStoreProviders, configProv], 82 | exports: [eventStoreProviders, configProv] 83 | }; 84 | } 85 | 86 | static registerFeature(config: EventStoreOptionConfig): DynamicModule { 87 | if (config === undefined || config === null) { 88 | throw new Error('Config missing'); 89 | } 90 | 91 | const CurrentStore = 92 | config.type === 'event-store' ? EventStore : NatsEventStore; 93 | 94 | return { 95 | module: EventStoreCoreModule, 96 | providers: [ 97 | { 98 | provide: ProvidersConstants.EVENT_STORE_STREAM_CONFIG_PROVIDER, 99 | useValue: { 100 | ...config 101 | } 102 | }, 103 | CurrentStore 104 | ], 105 | exports: [CurrentStore] 106 | }; 107 | } 108 | 109 | static registerFeatureAsync( 110 | options: EventStoreFeatureAsyncOptions 111 | ): DynamicModule { 112 | const configProv: Provider = { 113 | provide: ProvidersConstants.EVENT_STORE_STREAM_CONFIG_PROVIDER, 114 | useFactory: async (config: EventStoreOptionConfig) => { 115 | return { 116 | ...config 117 | }; 118 | }, 119 | inject: [NEST_EVENTSTORE_FEATURE_OPTION] 120 | }; 121 | 122 | const asyncProviders = this.createFeatureAsyncProviders(options); 123 | const CurrentStore = 124 | options.type === 'event-store' ? EventStore : NatsEventStore; 125 | 126 | return { 127 | module: EventStoreCoreModule, 128 | providers: [...asyncProviders, configProv, CurrentStore], 129 | exports: [CurrentStore] 130 | }; 131 | } 132 | 133 | private static createAsyncProviders( 134 | options: EventStoreModuleAsyncOptions 135 | ): Provider[] { 136 | if (options.useExisting || options.useFactory) { 137 | return [this.createAsyncOptionsProvider(options)]; 138 | } 139 | const useClass = options.useClass as Type; 140 | return [ 141 | this.createAsyncOptionsProvider(options), 142 | { 143 | provide: useClass, 144 | useClass 145 | } 146 | ]; 147 | } 148 | 149 | private static createAsyncOptionsProvider( 150 | options: EventStoreModuleAsyncOptions 151 | ): Provider { 152 | if (options.useFactory) { 153 | return { 154 | provide: NEST_EVENTSTORE_OPTION, 155 | useFactory: options.useFactory, 156 | inject: options.inject || [] 157 | }; 158 | } 159 | const inject = [ 160 | (options.useClass || options.useExisting) as Type< 161 | EventStoreOptionsFactory 162 | > 163 | ]; 164 | return { 165 | provide: NEST_EVENTSTORE_OPTION, 166 | useFactory: async (optionsFactory: EventStoreOptionsFactory) => 167 | await optionsFactory.createEventStoreOptions(options.name), 168 | inject 169 | }; 170 | } 171 | 172 | private static createFeatureAsyncProviders( 173 | options: EventStoreFeatureAsyncOptions 174 | ): Provider[] { 175 | if (options.useExisting || options.useFactory) { 176 | return [this.createFeatureAsyncOptionsProvider(options)]; 177 | } 178 | const useClass = options.useClass as Type; 179 | return [ 180 | this.createFeatureAsyncOptionsProvider(options), 181 | { 182 | provide: useClass, 183 | useClass 184 | } 185 | ]; 186 | } 187 | 188 | private static createFeatureAsyncOptionsProvider( 189 | options: EventStoreFeatureAsyncOptions 190 | ): Provider { 191 | if (options.useFactory) { 192 | return { 193 | provide: NEST_EVENTSTORE_FEATURE_OPTION, 194 | useFactory: options.useFactory, 195 | inject: options.inject || [] 196 | }; 197 | } 198 | const inject = [ 199 | (options.useClass || options.useExisting) as Type< 200 | EventStoreFeatureOptionsFactory 201 | > 202 | ]; 203 | return { 204 | provide: NEST_EVENTSTORE_FEATURE_OPTION, 205 | useFactory: async (optionsFactory: EventStoreFeatureOptionsFactory) => 206 | await optionsFactory.createFeatureOptions(options.name), 207 | inject 208 | }; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/lib/stores/nats-event-store.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:variable-name */ 2 | 3 | import { 4 | Inject, 5 | Injectable, 6 | Logger, 7 | OnModuleDestroy, 8 | OnModuleInit 9 | } from '@nestjs/common'; 10 | import { 11 | IEvent, 12 | IEventPublisher, 13 | EventBus, 14 | IMessageSource 15 | } from '@nestjs/cqrs'; 16 | import { Subject } from 'rxjs'; 17 | import { 18 | EventStoreOptionConfig, 19 | IEventConstructors, 20 | EventStoreSubscriptionType, 21 | NatsEventStorePersistentSubscription as ESPersistentSubscription, 22 | NatsEventStoreVolatileSubscription as ESVolatileSubscription, 23 | ExtendedNatsVolatileSubscription, 24 | ExtendedNatsPersistentSubscription, 25 | ProvidersConstants, 26 | EventStoreModuleOptions 27 | } from '../contract'; 28 | import { NatsEventStoreBroker } from '../brokers'; 29 | import { Message } from 'node-nats-streaming'; 30 | 31 | /** 32 | * @class EventStore 33 | */ 34 | @Injectable() 35 | export class NatsEventStore 36 | implements IEventPublisher, OnModuleDestroy, OnModuleInit, IMessageSource { 37 | private logger = new Logger(this.constructor.name); 38 | private eventStore: NatsEventStoreBroker; 39 | private eventHandlers: IEventConstructors; 40 | private subject$: Subject; 41 | private readonly featureStream?: string; 42 | 43 | private persistentSubscriptions: ExtendedNatsPersistentSubscription[] = []; 44 | private persistentSubscriptionsCount: number; 45 | 46 | private volatileSubscriptions: ExtendedNatsVolatileSubscription[] = []; 47 | private volatileSubscriptionsCount: number; 48 | 49 | constructor( 50 | @Inject(ProvidersConstants.EVENT_STORE_PROVIDER) eventStore: any, 51 | @Inject(ProvidersConstants.EVENT_STORE_CONNECTION_CONFIG_PROVIDER) 52 | private readonly configService: EventStoreModuleOptions, 53 | @Inject(ProvidersConstants.EVENT_STORE_STREAM_CONFIG_PROVIDER) 54 | private readonly esStreamConfig: EventStoreOptionConfig, 55 | private readonly eventsBus: EventBus 56 | ) { 57 | this.eventStore = eventStore; 58 | this.featureStream = esStreamConfig.featureStreamName; 59 | this.addEventHandlers(esStreamConfig.eventHandlers); 60 | 61 | if (configService.type === 'nats') { 62 | this.eventStore.connect( 63 | configService.clusterId, 64 | configService.clientId, 65 | configService.options 66 | ); 67 | } else { 68 | throw new Error( 69 | 'Event store type is not supported - (nats-event-store.ts)' 70 | ); 71 | } 72 | 73 | this.initSubs(); 74 | this.eventStore.getClient().on('connect', () => { 75 | this.initSubs(); 76 | }); 77 | } 78 | 79 | private initSubs() { 80 | if (this.esStreamConfig.type === 'nats') { 81 | const persistentSubscriptions = this.esStreamConfig.subscriptions.filter( 82 | sub => { 83 | return sub.type === EventStoreSubscriptionType.Persistent; 84 | } 85 | ); 86 | const volatileSubscriptions = this.esStreamConfig.subscriptions.filter( 87 | sub => { 88 | return sub.type === EventStoreSubscriptionType.Volatile; 89 | } 90 | ); 91 | 92 | this.subscribeToPersistentSubscriptions( 93 | persistentSubscriptions as ESPersistentSubscription[] 94 | ); 95 | this.subscribeToVolatileSubscriptions( 96 | volatileSubscriptions as ESVolatileSubscription[] 97 | ); 98 | } else { 99 | throw new Error( 100 | 'Event store type is not supported for feature - nats-event-store.ts' 101 | ); 102 | } 103 | } 104 | 105 | async publish(event: IEvent & { handlerType?: string }, stream?: string) { 106 | if (event === undefined) { 107 | return; 108 | } 109 | if (event === null) { 110 | return; 111 | } 112 | 113 | event.handlerType = event?.constructor?.name; 114 | const payload = Buffer.from(JSON.stringify(event)); 115 | 116 | const streamId = this.getStreamId(stream ? stream : this.featureStream); 117 | 118 | try { 119 | await this.eventStore.getClient().publish(streamId, payload); 120 | } catch (err) { 121 | this.logger.error(err); 122 | } 123 | } 124 | 125 | private getStreamId(stream) { 126 | return `nest-event-store-${stream}`; 127 | } 128 | 129 | async subscribeToPersistentSubscriptions( 130 | subscriptions: ESPersistentSubscription[] 131 | ) { 132 | if (!this.eventStore.isConnected) { 133 | return; 134 | } 135 | this.persistentSubscriptionsCount = subscriptions.length; 136 | this.persistentSubscriptions = await Promise.all( 137 | subscriptions.map(async subscription => { 138 | return await this.subscribeToPersistentSubscription( 139 | this.getStreamId(subscription.stream), 140 | subscription.durableName, 141 | subscription?.startAt, 142 | subscription.maxInflight, 143 | subscription?.ackWait, 144 | subscription?.manualAcks 145 | ); 146 | }) 147 | ); 148 | } 149 | 150 | async subscribeToVolatileSubscriptions( 151 | subscriptions: ESVolatileSubscription[] 152 | ) { 153 | this.volatileSubscriptionsCount = subscriptions.length; 154 | this.volatileSubscriptions = await Promise.all( 155 | subscriptions.map(async subscription => { 156 | return await this.subscribeToVolatileSubscription( 157 | this.getStreamId(subscription.stream), 158 | subscription.startAt, 159 | subscription.maxInflight, 160 | subscription?.ackWait, 161 | subscription?.manualAcks 162 | ); 163 | }) 164 | ); 165 | } 166 | 167 | async subscribeToVolatileSubscription( 168 | stream: string, 169 | startAt?: number, 170 | maxInFlight?: number, 171 | ackWait?: number, 172 | manualAcks?: boolean 173 | ): Promise { 174 | this.logger.log(`Volatile and subscribing to stream ${stream}!`); 175 | try { 176 | const opts = this.eventStore.getClient().subscriptionOptions(); 177 | opts.setAckWait(ackWait); 178 | opts.setMaxInFlight(maxInFlight); 179 | opts.setManualAckMode(manualAcks); 180 | 181 | if (startAt) { 182 | opts.setStartAtSequence(startAt); 183 | } 184 | 185 | if (this.configService.type === 'event-store') { 186 | return; 187 | } 188 | 189 | const resolved = (await this.eventStore 190 | .getClient() 191 | .subscribe( 192 | stream, 193 | this.configService.groupId, 194 | opts 195 | )) as ExtendedNatsVolatileSubscription; 196 | resolved.on('message', msg => this.onEvent(msg)); 197 | resolved.on('error', err => this.onDropped(err)); 198 | this.logger.log('Volatile processing of EventStore events started!'); 199 | resolved.isLive = true; 200 | return resolved; 201 | } catch (err) { 202 | this.logger.error(err); 203 | } 204 | } 205 | 206 | get allVolatileSubscriptionsLive(): boolean { 207 | const initialized = 208 | this.volatileSubscriptions.length === this.volatileSubscriptionsCount; 209 | return ( 210 | initialized && 211 | this.volatileSubscriptions.every(subscription => { 212 | return !!subscription && subscription.isLive; 213 | }) 214 | ); 215 | } 216 | 217 | get allPersistentSubscriptionsLive(): boolean { 218 | const initialized = 219 | this.persistentSubscriptions.length === this.persistentSubscriptionsCount; 220 | return ( 221 | initialized && 222 | this.persistentSubscriptions.every(subscription => { 223 | return !!subscription && subscription.isLive; 224 | }) 225 | ); 226 | } 227 | 228 | async subscribeToPersistentSubscription( 229 | stream: string, 230 | durableName: string, 231 | startAt?: number, 232 | maxInFlight?: number, 233 | ackWait?: number, 234 | manualAcks?: boolean 235 | ): Promise { 236 | try { 237 | this.logger.log(` 238 | Connecting to persistent subscription ${durableName} on stream ${stream}! 239 | `); 240 | 241 | const opts = this.eventStore.getClient().subscriptionOptions(); 242 | opts.setDurableName(durableName); 243 | 244 | if (ackWait) { 245 | opts.setAckWait(ackWait); 246 | } 247 | if (maxInFlight) { 248 | opts.setMaxInFlight(maxInFlight); 249 | } 250 | if (manualAcks) { 251 | opts.setManualAckMode(manualAcks); 252 | } 253 | 254 | if (startAt) { 255 | opts.setStartAtSequence(startAt); 256 | } else { 257 | opts.setStartWithLastReceived(); 258 | } 259 | 260 | if (this.configService.type === 'event-store') { 261 | return; 262 | } 263 | 264 | const resolved = (await this.eventStore 265 | .getClient() 266 | .subscribe( 267 | stream, 268 | this.configService.groupId, 269 | opts 270 | )) as ExtendedNatsPersistentSubscription; 271 | resolved.isLive = true; 272 | resolved.on('message', msg => this.onEvent(msg)); 273 | resolved.on('error', err => this.onDropped(err)); 274 | 275 | return resolved; 276 | } catch (err) { 277 | this.logger.error(err); 278 | } 279 | } 280 | 281 | async onEvent(payload: Message) { 282 | const data: any & { handlerType?: string } = JSON.parse( 283 | payload.getRawData().toString() 284 | ); 285 | const handlerType = data.handlerType; 286 | delete data.handlerType; 287 | const handler = this.eventHandlers[handlerType]; 288 | if (!handler) { 289 | this.logger.error('Received event that could not be handled!'); 290 | return; 291 | } 292 | 293 | const eventType = payload.getSubject(); 294 | if (this.eventHandlers && this.eventHandlers[handlerType]) { 295 | this.subject$.next(this.eventHandlers[handlerType](...Object.values(data))); 296 | } else { 297 | Logger.warn( 298 | `Event of type ${eventType} not handled`, 299 | this.constructor.name 300 | ); 301 | } 302 | } 303 | 304 | onDropped(error) { 305 | // subscription.isLive = false; 306 | this.logger.error('onDropped => ' + error.message); 307 | } 308 | 309 | get isLive(): boolean { 310 | /* return ( 311 | this.allCatchUpSubscriptionsLive && this.allPersistentSubscriptionsLive && this.allVolatileSubscriptionsLive 312 | ); */ 313 | return false; 314 | } 315 | 316 | addEventHandlers(eventHandlers: IEventConstructors) { 317 | this.eventHandlers = { ...this.eventHandlers, ...eventHandlers }; 318 | } 319 | onModuleInit(): any { 320 | this.subject$ = (this.eventsBus as any).subject$; 321 | this.bridgeEventsTo((this.eventsBus as any).subject$); 322 | this.eventsBus.publisher = this; 323 | } 324 | 325 | onModuleDestroy(): any { 326 | this.eventStore.close(); 327 | } 328 | 329 | async bridgeEventsTo(subject: Subject): Promise { 330 | this.subject$ = subject; 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/lib/stores/event-store.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:variable-name */ 2 | // 3 | // Modified version of this https://github.com/daypaio/nestjs-eventstore/blob/master/src/event-store/eventstore-cqrs/event-store.bus.ts 4 | // special thanks to him. 5 | // 6 | 7 | import { 8 | Inject, 9 | Injectable, 10 | Logger, 11 | OnModuleDestroy, 12 | OnModuleInit 13 | } from '@nestjs/common'; 14 | import { 15 | IEvent, 16 | IEventPublisher, 17 | EventBus, 18 | IMessageSource 19 | } from '@nestjs/cqrs'; 20 | import * as Long from 'long'; 21 | import { Subject } from 'rxjs'; 22 | import { v4 } from 'uuid'; 23 | import { 24 | EventData, 25 | EventStorePersistentSubscription, 26 | ResolvedEvent, 27 | EventStoreCatchUpSubscription, 28 | EventStoreSubscription as EventStoreVolatileSubscription, 29 | expectedVersion, 30 | createJsonEventData 31 | } from 'node-eventstore-client'; 32 | import { IAdapterStore } from '../adapter'; 33 | import { 34 | EventStoreOptionConfig, 35 | IEventConstructors, 36 | EventStoreSubscriptionType, 37 | EventStorePersistentSubscription as ESPersistentSubscription, 38 | EventStoreCatchupSubscription as ESCatchUpSubscription, 39 | EventStoreVolatileSubscription as ESVolatileSubscription, 40 | ExtendedCatchUpSubscription, 41 | ExtendedVolatileSubscription, 42 | ExtendedPersistentSubscription, 43 | ProvidersConstants, 44 | EventStoreModuleOptions 45 | } from '../contract'; 46 | import { EventStoreBroker } from '../brokers'; 47 | 48 | /** 49 | * @class EventStore 50 | */ 51 | @Injectable() 52 | export class EventStore 53 | implements IEventPublisher, OnModuleDestroy, OnModuleInit, IMessageSource { 54 | private logger = new Logger(this.constructor.name); 55 | private eventStore: EventStoreBroker; 56 | private store: IAdapterStore; 57 | private eventHandlers: IEventConstructors; 58 | private subject$: Subject; 59 | private readonly featureStream?: string; 60 | private catchupSubscriptions: Array< 61 | Promise 62 | > = []; 63 | private catchupSubscriptionsCount: number; 64 | 65 | private persistentSubscriptions: ExtendedPersistentSubscription[] = []; 66 | private persistentSubscriptionsCount: number; 67 | 68 | private volatileSubscriptions: ExtendedVolatileSubscription[] = []; 69 | private volatileSubscriptionsCount: number; 70 | 71 | constructor( 72 | @Inject(ProvidersConstants.EVENT_STORE_PROVIDER) eventStore: any, 73 | @Inject(ProvidersConstants.EVENT_STORE_CONNECTION_CONFIG_PROVIDER) 74 | configService: EventStoreModuleOptions, 75 | @Inject(ProvidersConstants.EVENT_STORE_STREAM_CONFIG_PROVIDER) 76 | esStreamConfig: EventStoreOptionConfig, 77 | private readonly eventsBus: EventBus 78 | ) { 79 | this.eventStore = eventStore; 80 | this.featureStream = esStreamConfig.featureStreamName; 81 | if (esStreamConfig.type === 'event-store') { 82 | this.store = esStreamConfig.store; 83 | } else { 84 | throw new Error('Event store type is not supported - (event-tore.ts)'); 85 | } 86 | this.addEventHandlers(esStreamConfig.eventHandlers); 87 | if (configService.type === 'event-store') { 88 | this.eventStore.connect(configService.options, configService.tcpEndpoint); 89 | 90 | const catchupSubscriptions = esStreamConfig.subscriptions.filter(sub => { 91 | return sub.type === EventStoreSubscriptionType.CatchUp; 92 | }); 93 | const persistentSubscriptions = esStreamConfig.subscriptions.filter( 94 | sub => { 95 | return sub.type === EventStoreSubscriptionType.Persistent; 96 | } 97 | ); 98 | const volatileSubscriptions = esStreamConfig.subscriptions.filter(sub => { 99 | return sub.type === EventStoreSubscriptionType.Volatile; 100 | }); 101 | 102 | this.subscribeToCatchUpSubscriptions( 103 | catchupSubscriptions as ESCatchUpSubscription[] 104 | ); 105 | this.subscribeToPersistentSubscriptions( 106 | persistentSubscriptions as ESPersistentSubscription[] 107 | ); 108 | this.subscribeToVolatileSubscriptions( 109 | volatileSubscriptions as ESVolatileSubscription[] 110 | ); 111 | } else { 112 | throw new Error('Event store type is not supported for feature - (event-tore.ts)'); 113 | } 114 | } 115 | 116 | async publish(event: IEvent, stream?: string) { 117 | if (event === undefined) { 118 | return; 119 | } 120 | if (event === null) { 121 | return; 122 | } 123 | 124 | const eventPayload: EventData = createJsonEventData( 125 | v4(), 126 | event, 127 | null, 128 | stream 129 | ); 130 | 131 | const streamId = stream ? stream : this.featureStream; 132 | 133 | try { 134 | await this.eventStore 135 | .getClient() 136 | .appendToStream(streamId, expectedVersion.any, [eventPayload]); 137 | } catch (err) { 138 | this.logger.error(err); 139 | } 140 | } 141 | 142 | async subscribeToPersistentSubscriptions( 143 | subscriptions: ESPersistentSubscription[] 144 | ) { 145 | this.persistentSubscriptionsCount = subscriptions.length; 146 | this.persistentSubscriptions = await Promise.all( 147 | subscriptions.map(async subscription => { 148 | return await this.subscribeToPersistentSubscription( 149 | subscription.stream, 150 | subscription.persistentSubscriptionName 151 | ); 152 | }) 153 | ); 154 | } 155 | 156 | async subscribeToCatchUpSubscriptions( 157 | subscriptions: ESCatchUpSubscription[] 158 | ) { 159 | this.catchupSubscriptionsCount = subscriptions.length; 160 | this.catchupSubscriptions = subscriptions.map(async subscription => { 161 | let lcp = subscription.lastCheckpoint; 162 | if (this.store) { 163 | lcp = await this.store.read(this.store.storeKey); 164 | } 165 | return this.subscribeToCatchupSubscription( 166 | subscription.stream, 167 | subscription.resolveLinkTos, 168 | lcp 169 | ); 170 | }); 171 | } 172 | 173 | async subscribeToVolatileSubscriptions( 174 | subscriptions: ESVolatileSubscription[] 175 | ) { 176 | this.volatileSubscriptionsCount = subscriptions.length; 177 | this.volatileSubscriptions = await Promise.all( 178 | subscriptions.map(async subscription => { 179 | return await this.subscribeToVolatileSubscription( 180 | subscription.stream, 181 | subscription.resolveLinkTos 182 | ); 183 | }) 184 | ); 185 | } 186 | 187 | subscribeToCatchupSubscription( 188 | stream: string, 189 | resolveLinkTos: boolean = true, 190 | lastCheckpoint: number | Long | null = 0 191 | ): ExtendedCatchUpSubscription { 192 | this.logger.log(`Catching up and subscribing to stream ${stream}!`); 193 | try { 194 | return this.eventStore.getClient().subscribeToStreamFrom( 195 | stream, 196 | lastCheckpoint, 197 | resolveLinkTos, 198 | (sub, payload) => this.onEvent(sub, payload), 199 | subscription => 200 | this.onLiveProcessingStarted( 201 | subscription as ExtendedCatchUpSubscription 202 | ), 203 | (sub, reason, error) => 204 | this.onDropped(sub as ExtendedCatchUpSubscription, reason, error) 205 | ) as ExtendedCatchUpSubscription; 206 | } catch (err) { 207 | this.logger.error(err); 208 | } 209 | } 210 | 211 | async subscribeToVolatileSubscription( 212 | stream: string, 213 | resolveLinkTos: boolean = true 214 | ): Promise { 215 | this.logger.log(`Volatile and subscribing to stream ${stream}!`); 216 | try { 217 | const resolved = (await this.eventStore.getClient().subscribeToStream( 218 | stream, 219 | resolveLinkTos, 220 | (sub, payload) => this.onEvent(sub, payload), 221 | (sub, reason, error) => 222 | this.onDropped(sub as ExtendedVolatileSubscription, reason, error) 223 | )) as ExtendedVolatileSubscription; 224 | 225 | this.logger.log('Volatile processing of EventStore events started!'); 226 | resolved.isLive = true; 227 | return resolved; 228 | } catch (err) { 229 | this.logger.error(err); 230 | } 231 | } 232 | 233 | get allCatchUpSubscriptionsLive(): boolean { 234 | const initialized = 235 | this.catchupSubscriptions.length === this.catchupSubscriptionsCount; 236 | return ( 237 | initialized && 238 | this.catchupSubscriptions.every(async subscription => { 239 | const s = await subscription; 240 | return !!s && s.isLive; 241 | }) 242 | ); 243 | } 244 | 245 | get allVolatileSubscriptionsLive(): boolean { 246 | const initialized = 247 | this.volatileSubscriptions.length === this.volatileSubscriptionsCount; 248 | return ( 249 | initialized && 250 | this.volatileSubscriptions.every(subscription => { 251 | return !!subscription && subscription.isLive; 252 | }) 253 | ); 254 | } 255 | 256 | get allPersistentSubscriptionsLive(): boolean { 257 | const initialized = 258 | this.persistentSubscriptions.length === this.persistentSubscriptionsCount; 259 | return ( 260 | initialized && 261 | this.persistentSubscriptions.every(subscription => { 262 | return !!subscription && subscription.isLive; 263 | }) 264 | ); 265 | } 266 | 267 | async subscribeToPersistentSubscription( 268 | stream: string, 269 | subscriptionName: string 270 | ): Promise { 271 | try { 272 | this.logger.log(` 273 | Connecting to persistent subscription ${subscriptionName} on stream ${stream}! 274 | `); 275 | 276 | const resolved = (await this.eventStore 277 | .getClient() 278 | .connectToPersistentSubscription( 279 | stream, 280 | subscriptionName, 281 | (sub, payload) => this.onEvent(sub, payload), 282 | (sub, reason, error) => 283 | this.onDropped(sub as ExtendedPersistentSubscription, reason, error) 284 | )) as ExtendedPersistentSubscription; 285 | 286 | resolved.isLive = true; 287 | 288 | return resolved; 289 | } catch (err) { 290 | this.logger.error(err.message); 291 | } 292 | } 293 | 294 | async onEvent( 295 | _subscription: 296 | | EventStorePersistentSubscription 297 | | EventStoreCatchUpSubscription 298 | | EventStoreVolatileSubscription, 299 | payload: ResolvedEvent 300 | ) { 301 | const { event } = payload; 302 | 303 | if (!event || !event.isJson) { 304 | this.logger.error('Received event that could not be resolved!'); 305 | return; 306 | } 307 | 308 | const handler = this.eventHandlers[event.eventType]; 309 | if (!handler) { 310 | this.logger.error('Received event that could not be handled!'); 311 | return; 312 | } 313 | 314 | const rawData = JSON.parse(event.data.toString()); 315 | const data = Object.values(rawData); 316 | 317 | const eventType = event.eventType || rawData.content.eventType; 318 | if (this.eventHandlers && this.eventHandlers[eventType]) { 319 | this.subject$.next(this.eventHandlers[event.eventType](...data)); 320 | if ( 321 | this.store && 322 | _subscription.constructor.name === 'EventStoreStreamCatchUpSubscription' 323 | ) { 324 | await this.store.write( 325 | this.store.storeKey, 326 | payload.event.eventNumber.toInt() 327 | ); 328 | } 329 | } else { 330 | Logger.warn( 331 | `Event of type ${eventType} not handled`, 332 | this.constructor.name 333 | ); 334 | } 335 | } 336 | 337 | onDropped( 338 | subscription: 339 | | ExtendedPersistentSubscription 340 | | ExtendedCatchUpSubscription 341 | | ExtendedVolatileSubscription, 342 | _reason: string, 343 | error: Error 344 | ) { 345 | subscription.isLive = false; 346 | this.logger.error('onDropped => ' + error); 347 | } 348 | 349 | onLiveProcessingStarted(subscription: ExtendedCatchUpSubscription) { 350 | subscription.isLive = true; 351 | this.logger.log('Live processing of EventStore events started!'); 352 | } 353 | 354 | get isLive(): boolean { 355 | return ( 356 | this.allCatchUpSubscriptionsLive && 357 | this.allPersistentSubscriptionsLive && 358 | this.allVolatileSubscriptionsLive 359 | ); 360 | } 361 | 362 | addEventHandlers(eventHandlers: IEventConstructors) { 363 | this.eventHandlers = { ...this.eventHandlers, ...eventHandlers }; 364 | } 365 | onModuleInit(): any { 366 | this.subject$ = (this.eventsBus as any).subject$; 367 | this.bridgeEventsTo((this.eventsBus as any).subject$); 368 | this.eventsBus.publisher = this; 369 | } 370 | 371 | onModuleDestroy(): any { 372 | this.eventStore.close(); 373 | } 374 | 375 | async bridgeEventsTo(subject: Subject): Promise { 376 | this.subject$ = subject; 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | NestJs Event Store 3 |

4 | 5 |

6 | NestJS CQRS module with support EventStore.org and NATS Streaming. It requires @nestjs/cqrs. 7 |

8 |

9 |

10 | 11 |

12 | NPM Version 13 | License 14 | Code Size 15 | Top Language 16 | Top Language 17 |

18 | 19 | ## Installation 20 | 21 | ```bash 22 | $ yarn add @juicycleff/nestjs-event-store 23 | $ yarn add node-nats-streaming node-eventstore-client 24 | ``` 25 | 26 | ## Description 27 | This module aims to bridge the gap between NestJs and popular event store brokers like [Event Store](https://eventstore.org) and [NATS Streaming](https://nats.io) with support for kafka coming. 28 | 29 | #### [Event Store](https://eventstore.org) 30 | It supports all different subscription strategies in EventStore.Org, 31 | such as Volatile, CatchUp and Persistent subscriptions fairly easily. There is support for a storage adapter interface for storing catchup events type last checkpoint position, so 32 | the checkpoint can be read on start up; The adapter interface is very slim and easy and can be assigned preferably using the `EventStoreModule.registerFeatureAsync` method. 33 | Adapter data store examples coming soon. 34 | 35 | Note: if your event broker type is Event Store then featureStreamName should look like `'$ce-user'`, then you should name your domain argument should be `user` without `$ce`, for example. 36 | 37 | ```typescript 38 | export class UserCreatedEvent implements IEvent { 39 | constructor( 40 | public readonly user: any // This what im talking about. 41 | ) { } 42 | } 43 | ``` 44 | The way this works is we group the event based the first argument in the constructor name and this argument name must be a substring of featureStreamName. I'm sorry you can't pass you your own unique name at the moment, but I will add support for it 45 | 46 | #### [NATS Streaming](https://nats.io) 47 | It supports all both durable/persistent subscription with shared subscription and volatile. It does not have the limitations of [Event Store](https://eventstore.org) stated above. 48 | 49 | Note: if your event broker type is NATS then featureStreamName should look like `'user'`. 50 | 51 | ### Setup from versions from `v3.1.15` 52 | #### Setup NATS 53 | ##### Setup root app module for NATS 54 | 55 | ```typescript 56 | import { Module } from '@nestjs/common'; 57 | import { EventStoreModule } from '@juicycleff/nestjs-event-store'; 58 | 59 | @Module({ 60 | imports: [ 61 | EventStoreModule.register({ 62 | type: 'nats', 63 | groupId: 'groupId', 64 | clusterId: 'clusterId', 65 | clientId: 'clientId', // Optional (Auto generated with uuid) 66 | options: { 67 | url: 'nats://localhost:4222', 68 | reconnect: true, 69 | maxReconnectAttempts: -1, 70 | }, 71 | }), 72 | ] 73 | }) 74 | export class AppModule {} 75 | ``` 76 | 77 | ##### Setup async root app module 78 | ```typescript 79 | import { Module } from '@nestjs/common'; 80 | import { EventStoreModule } from '@juicycleff/nestjs-event-store'; 81 | import { EventStoreConfigService } from './eventstore-config.service'; 82 | 83 | @Module({ 84 | imports: [ 85 | EventStoreModule.registerAsync({ 86 | type: 'nats', 87 | useClass: EventStoreConfigService 88 | }), 89 | ] 90 | }) 91 | export class AppModule {} 92 | ``` 93 | 94 | ##### Setup feature module 95 | ```typescript 96 | import { Module } from '@nestjs/common'; 97 | import { UsersController } from './users.controller'; 98 | import { AccountEventHandlers, UserLoggedInEvent } from '@ultimatebackend/core'; 99 | import { AccountSagas } from '../common'; 100 | import { EventStoreModule, EventStoreSubscriptionType } from '@juicycleff/nestjs-event-store'; 101 | 102 | @Module({ 103 | imports: [ 104 | EventStoreModule.registerFeature({ 105 | featureStreamName: 'user', 106 | type: 'nats', 107 | subscriptions: [ 108 | { 109 | type: EventStoreSubscriptionType.Persistent, 110 | stream: 'account', 111 | durableName: 'svc-user', 112 | } 113 | ], 114 | eventHandlers: { 115 | UserLoggedInEvent: (data) => new UserLoggedInEvent(data), 116 | }, 117 | }) 118 | ], 119 | providers: [...AccountEventHandlers, AccountSagas], 120 | controllers: [UsersController], 121 | }) 122 | export class UsersModule {} 123 | ``` 124 | 125 | #### Setup EventStore 126 | ##### Setup root app module for EventStore 127 | 128 | ```typescript 129 | import { Module } from '@nestjs/common'; 130 | import { EventStoreModule } from '@juicycleff/nestjs-event-store'; 131 | 132 | @Module({ 133 | imports: [ 134 | EventStoreModule.register({ 135 | type: 'event-store', 136 | tcpEndpoint: { 137 | host: 'localhost', 138 | port: 1113, 139 | }, 140 | options: { 141 | maxRetries: 1000, // Optional 142 | maxReconnections: 1000, // Optional 143 | reconnectionDelay: 1000, // Optional 144 | heartbeatInterval: 1000, // Optional 145 | heartbeatTimeout: 1000, // Optional 146 | defaultUserCredentials: { 147 | password: 'admin', 148 | username: 'chnageit', 149 | }, 150 | }, 151 | }), 152 | ] 153 | }) 154 | export class AppModule {} 155 | ``` 156 | 157 | ##### Setup async root app module 158 | ```typescript 159 | import { Module } from '@nestjs/common'; 160 | import { EventStoreModule } from '@juicycleff/nestjs-event-store'; 161 | import { EventStoreConfigService } from './eventstore-config.service'; 162 | 163 | @Module({ 164 | imports: [ 165 | EventStoreModule.registerAsync({ 166 | type: 'event-store', 167 | useClass: EventStoreConfigService 168 | }), 169 | ] 170 | }) 171 | export class AppModule {} 172 | ``` 173 | 174 | ##### Setup feature module 175 | ```typescript 176 | import { Module } from '@nestjs/common'; 177 | import { CommandBus, CqrsModule, EventBus } from '@nestjs/cqrs'; 178 | import { EventStoreModule, EventStore, EventStoreSubscriptionType } from '@juicycleff/nestjs-event-store'; 179 | 180 | import { 181 | UserCommandHandlers, 182 | UserLoggedInEvent, 183 | UserEventHandlers, 184 | UserQueryHandlers, 185 | } from '../cqrs'; 186 | import { UserSagas } from './sagas'; 187 | import { MongoStore } from './mongo-eventstore-adapter'; 188 | 189 | @Module({ 190 | imports: [ 191 | CqrsModule, 192 | EventStoreModule.registerFeature({ 193 | featureStreamName: '$ce-user', 194 | type: 'event-store', 195 | store: MongoStore, // Optional mongo store for persisting catchup events position for microservices to mitigate failures. Must implement IAdapterStore 196 | subscriptions: [ 197 | { 198 | type: EventStoreSubscriptionType.CatchUp, 199 | stream: '$ce-user', 200 | resolveLinkTos: true, // Default is true (Optional) 201 | lastCheckpoint: 13, // Default is 0 (Optional) 202 | }, 203 | ], 204 | eventHandlers: { 205 | UserLoggedInEvent: (data) => new UserLoggedInEvent(data), 206 | }, 207 | }), 208 | ], 209 | 210 | providers: [ 211 | UserSagas, 212 | ...UserQueryHandlers, 213 | ...UserCommandHandlers, 214 | ...UserEventHandlers, 215 | ], 216 | }) 217 | export class UserModule {} 218 | ``` 219 | 220 | ### Setup from versions from `v3.0.0 to 3.0.5` 221 | ##### Setup root app module 222 | 223 | ```typescript 224 | import { Module } from '@nestjs/common'; 225 | import { EventStoreModule } from '@juicycleff/nestjs-event-store'; 226 | 227 | @Module({ 228 | imports: [ 229 | EventStoreModule.register({ 230 | tcpEndpoint: { 231 | host: process.env.ES_TCP_HOSTNAME || AppConfig.eventstore?.hostname, 232 | port: parseInt(process.env.ES_TCP_PORT, 10) || AppConfig.eventstore?.tcpPort, 233 | }, 234 | options: { 235 | maxRetries: 1000, // Optional 236 | maxReconnections: 1000, // Optional 237 | reconnectionDelay: 1000, // Optional 238 | heartbeatInterval: 1000, // Optional 239 | heartbeatTimeout: 1000, // Optional 240 | defaultUserCredentials: { 241 | password: AppConfig.eventstore?.tcpPassword, 242 | username: AppConfig.eventstore?.tcpUsername, 243 | }, 244 | }, 245 | }), 246 | ] 247 | }) 248 | export class AppModule {} 249 | ``` 250 | 251 | ##### Setup async root app module 252 | ```typescript 253 | import { Module } from '@nestjs/common'; 254 | import { EventStoreModule } from '@juicycleff/nestjs-event-store'; 255 | import { EventStoreConfigService } from './eventstore-config.service'; 256 | 257 | @Module({ 258 | imports: [ 259 | EventStoreModule.registerAsync({ 260 | useClass: EventStoreConfigService 261 | }), 262 | ] 263 | }) 264 | export class AppModule {} 265 | ``` 266 | 267 | ## Setup module 268 | *Note* `featureStreamName` field is not important if you're subscription type is persistent' 269 | 270 | ##### Setup feature module 271 | ```typescript 272 | import { Module } from '@nestjs/common'; 273 | import { CommandBus, CqrsModule, EventBus } from '@nestjs/cqrs'; 274 | import { EventStoreModule, EventStore, EventStoreSubscriptionType } from '@juicycleff/nestjs-event-store'; 275 | 276 | import { 277 | UserCommandHandlers, 278 | UserCreatedEvent, 279 | UserEventHandlers, 280 | UserQueryHandlers, 281 | } from '../cqrs'; 282 | import { UserSagas } from './sagas'; 283 | import { MongoStore } from './mongo-eventstore-adapter'; 284 | 285 | @Module({ 286 | imports: [ 287 | CqrsModule, 288 | EventStoreModule.registerFeature({ 289 | featureStreamName: '$ce-user', 290 | store: MongoStore, // Optional mongo store for persisting catchup events position for microservices to mitigate failures. Must implement IAdapterStore 291 | subscriptions: [ 292 | { 293 | type: EventStoreSubscriptionType.CatchUp, 294 | stream: '$ce-user', 295 | resolveLinkTos: true, // Default is true (Optional) 296 | lastCheckpoint: 13, // Default is 0 (Optional) 297 | }, 298 | { 299 | type: EventStoreSubscriptionType.Volatile, 300 | stream: '$ce-user', 301 | }, 302 | { 303 | type: EventStoreSubscriptionType.Persistent, 304 | stream: '$ce-user', 305 | persistentSubscriptionName: 'steamName', 306 | resolveLinkTos: true, // Default is true (Optional) 307 | }, 308 | ], 309 | eventHandlers: { 310 | UserLoggedInEvent: (data) => new UserLoggedInEvent(data), 311 | UserRegisteredEvent: (data) => new UserRegisteredEvent(data), 312 | EmailVerifiedEvent: (data) => new EmailVerifiedEvent(data), 313 | }, 314 | }), 315 | ], 316 | 317 | providers: [ 318 | UserSagas, 319 | ...UserQueryHandlers, 320 | ...UserCommandHandlers, 321 | ...UserEventHandlers, 322 | ], 323 | }) 324 | export class UserModule {} 325 | ``` 326 | 327 | ##### Setup async feature module 328 | ```typescript 329 | import { Module } from '@nestjs/common'; 330 | import { EventStoreModule } from '@juicycleff/nestjs-event-store'; 331 | import { EventStoreFeatureService } from './user-eventstore-feature.service'; 332 | 333 | @Module({ 334 | imports: [ 335 | EventStoreModule.registerFeatureAsync({ 336 | useClass: EventStoreFeatureService 337 | }), 338 | ] 339 | }) 340 | export class AppModule {} 341 | ``` 342 | 343 | ### Setup from versions below `v2.0.0` 344 | #### Setup root app module 345 | 346 | ```typescript 347 | import { Module } from '@nestjs/common'; 348 | import { NestjsEventStoreModule } from '@juicycleff/nestjs-event-store'; 349 | 350 | @Module({ 351 | imports: [ 352 | NestjsEventStoreModule.forRoot({ 353 | http: { 354 | port: parseInt(process.env.ES_HTTP_PORT, 10), 355 | protocol: process.env.ES_HTTP_PROTOCOL, 356 | }, 357 | tcp: { 358 | credentials: { 359 | password: process.env.ES_TCP_PASSWORD, 360 | username: process.env.ES_TCP_USERNAME, 361 | }, 362 | hostname: process.env.ES_TCP_HOSTNAME, 363 | port: parseInt(process.env.ES_TCP_PORT, 10), 364 | protocol: process.env.ES_TCP_PROTOCOL, 365 | }, 366 | }), 367 | ] 368 | }) 369 | export class AppModule {} 370 | ``` 371 | 372 | #### Setup module 373 | 374 | ```typescript 375 | import { Module } from '@nestjs/common'; 376 | import { CommandBus, CqrsModule, EventBus } from '@nestjs/cqrs'; 377 | import { NestjsEventStoreModule, EventStore } from '@juicycleff/nestjs-event-store'; 378 | 379 | import { 380 | UserCommandHandlers, 381 | UserCreatedEvent, 382 | UserEventHandlers, 383 | UserQueryHandlers, 384 | } from '../cqrs'; 385 | import { UserSagas } from './sagas'; 386 | 387 | @Module({ 388 | imports: [ 389 | CqrsModule, 390 | NestjsEventStoreModule.forFeature({ 391 | name: 'user', 392 | resolveLinkTos: false, 393 | }), 394 | ], 395 | 396 | providers: [ 397 | UserSagas, 398 | ...UserQueryHandlers, 399 | ...UserCommandHandlers, 400 | ...UserEventHandlers, 401 | ], 402 | }) 403 | export class UserModule { 404 | constructor( 405 | private readonly command$: CommandBus, 406 | private readonly event$: EventBus, 407 | private readonly eventStore: EventStore, 408 | ) {} 409 | 410 | onModuleInit(): any { 411 | this.eventStore.setEventHandlers(this.eventHandlers); 412 | this.eventStore.bridgeEventsTo((this.event$ as any).subject$); 413 | this.event$.publisher = this.eventStore; 414 | } 415 | 416 | eventHandlers = { 417 | UserCreatedEvent: (data) => new UserCreatedEvent(data), 418 | }; 419 | } 420 | ``` 421 | 422 | 423 | ## Notice 424 | `2.0.0` release inspired by [nestjs-eventstore](https://github.com/daypaio/nestjs-eventstore) 425 | 426 | ## License 427 | 428 | This project is [MIT licensed](LICENSE). 429 | --------------------------------------------------------------------------------