├── .DS_Store ├── .circleci └── config.yml ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── jestconfig.json ├── package-lock.json ├── package.json ├── src ├── client │ ├── event-store.client.spec.ts │ ├── event-store.client.ts │ └── index.ts ├── event-store-core.module.ts ├── event-store.bus.ts ├── event-store.constants.ts ├── event-store.module.ts ├── event-store.publisher.ts ├── index.ts ├── interfaces │ ├── aggregate-event.interface.ts │ ├── event-constructors.interface.ts │ ├── event-store-async-options.interface.ts │ ├── event-store-event.interface.ts │ ├── event-store-feature-options.interface.ts │ ├── event-store-options.interface.ts │ └── index.ts ├── providers │ ├── event-store-bus.provider.ts │ └── index.ts └── types │ ├── constructor.type.ts │ ├── event-store-bus-config.type.ts │ ├── event-store-catchup-subscription.type.ts │ ├── event-store-persistent-subscription.type.ts │ ├── event-store-subscription-config.type.ts │ ├── event-store-subscription.type.ts │ └── index.ts ├── tsconfig.json └── tslint.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renew-app/nestjs-eventstore/85e8cbef841566d2093ab818d0e3d105e5c7ccc7/.DS_Store -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | defaults: &defaults 2 | docker: 3 | - image: node:15.13.0 4 | working_directory: ~/repo 5 | context: Global 6 | 7 | whitelist: &whitelist 8 | paths: 9 | - .npmignore 10 | - .gitignore 11 | - coverage/* 12 | - dist/* 13 | - node_modules/* 14 | - src/* 15 | - test/* 16 | - CODE_OF_CONDUCT.md 17 | - LICENSE.md 18 | - package.json 19 | - README.md 20 | - tsconfig.json 21 | - tslint.json 22 | - .git/* 23 | - jestconfig.json 24 | 25 | ssh_fingerprint: &ssh_fingerprint 26 | run: 27 | name: ssh fingerprint 28 | command: | 29 | mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config 30 | GIT_SSH_COMMAND='ssh -i ~/.ssh/id_rsa_fingerprint' 31 | 32 | version: 2 33 | jobs: 34 | checkout: 35 | <<: *defaults 36 | steps: 37 | - checkout 38 | - restore_cache: 39 | keys: 40 | - v1-dependencies-{{ checksum "package.json" }} 41 | - v1-dependencies- 42 | - run: 43 | name: Install Dependencies 44 | command: npm install 45 | - save_cache: 46 | paths: 47 | - node_modules 48 | key: v1-dependencies-{{ checksum "package.json" }} 49 | - persist_to_workspace: 50 | root: ~/repo 51 | <<: *whitelist 52 | lint: 53 | <<: *defaults 54 | steps: 55 | - attach_workspace: 56 | at: ~/repo 57 | - run: 58 | name: Lint TypeScript code 59 | command: npm run lint 60 | test: 61 | <<: *defaults 62 | steps: 63 | - attach_workspace: 64 | at: ~/repo 65 | - run: 66 | name: Test TypeScript code 67 | command: npm run test 68 | - persist_to_workspace: 69 | root: ~/repo 70 | <<: *whitelist 71 | build: 72 | <<: *defaults 73 | steps: 74 | - attach_workspace: 75 | at: ~/repo 76 | - run: 77 | name: Build TypeScript code 78 | command: npm run build 79 | - persist_to_workspace: 80 | root: ~/repo 81 | <<: *whitelist 82 | release: 83 | <<: *defaults 84 | steps: 85 | - *ssh_fingerprint 86 | - add_ssh_keys: 87 | fingerprints: 88 | - $FINGERPRINT 89 | - attach_workspace: 90 | at: ~/repo 91 | - run: 92 | name: install sudo 93 | command: | 94 | apt-get update && apt-get install -y sudo 95 | - run: 96 | name: install autotag binary 97 | command: | 98 | curl -sL https://git.io/autotag-install | sudo sh -s -- -b /usr/bin 99 | - run: 100 | name: increment version 101 | command: | 102 | AUTOTAG=$(autotag -b ${CIRCLE_BRANCH} -v) 103 | - run: 104 | name: authenticate github 105 | command: | 106 | rm -rf /home/circleci/.git/index.lock 107 | git init 108 | git config --global user.email "${GIT_AUTHOR_EMAIL}" 109 | git config --global user.name "${GIT_AUTHOR_NAME}" 110 | - run: 111 | name: push to github 112 | command: | 113 | git push --tags 114 | deploy: 115 | <<: *defaults 116 | steps: 117 | - attach_workspace: 118 | at: ~/repo 119 | - run: 120 | name: Authenticate with registry 121 | command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc 122 | - run: 123 | name: Publish package 124 | command: npm publish 125 | 126 | workflows: 127 | version: 2 128 | 129 | build-test-release: 130 | jobs: 131 | - checkout: 132 | filters: 133 | tags: 134 | ignore: /v[0-9]+(\.[0-9]+)*/ 135 | branches: 136 | only: 137 | - main 138 | - develop 139 | - test: 140 | requires: 141 | - checkout 142 | filters: 143 | tags: 144 | ignore: /v[0-9]+(\.[0-9]+)*/ 145 | branches: 146 | only: 147 | - main 148 | - develop 149 | - lint: 150 | requires: 151 | - checkout 152 | filters: 153 | tags: 154 | ignore: /v[0-9]+(\.[0-9]+)*/ 155 | branches: 156 | only: 157 | - main 158 | - develop 159 | - build: 160 | requires: 161 | - test 162 | - lint 163 | filters: 164 | tags: 165 | ignore: /v[0-9]+(\.[0-9]+)*/ 166 | branches: 167 | only: 168 | - main 169 | - develop 170 | - release: 171 | requires: 172 | - build 173 | filters: 174 | tags: 175 | ignore: /v[0-9]+(\.[0-9]+)*/ 176 | branches: 177 | only: 178 | - main 179 | - develop 180 | - deploy: 181 | requires: 182 | - build 183 | filters: 184 | tags: 185 | ignore: /v[0-9]+(\.[0-9]+)*/ 186 | branches: 187 | only: 188 | - main -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | build 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | /lib 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # Next.js build output 81 | .next 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "singleQuote": true 5 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 renew-app 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nestjs-eventstore 2 | An implementation of the EventStore nodejs client module within the Nestjs CQRS framework. 3 | -------------------------------------------------------------------------------- /jestconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "transform": { 3 | "^.+\\.(t|j)sx?$": "ts-jest" 4 | }, 5 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 6 | "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"] 7 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@renewapp/nestjs-eventstore", 3 | "version": "0.0.17", 4 | "description": "An implementation of the @eventstore/db-client for the Nestjs framework and working in cooperation with the built-in Nestjs CQRS infrastructure.", 5 | "main": "lib/index.js", 6 | "types": "lib/**/*.d.ts", 7 | "scripts": { 8 | "test": "jest --config jestconfig.json --passWithNoTests", 9 | "build": "tsc", 10 | "format": "prettier --write \"src/**/*.ts\"", 11 | "lint": "tslint -p tsconfig.json", 12 | "prepare": "npm run build", 13 | "prepublishOnly": "npm test && npm run lint", 14 | "preversion": "npm run lint", 15 | "version": "npm run format && git add -A src", 16 | "postversion": "git push && git push --tags" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/renew-app/nestjs-eventstore.git" 21 | }, 22 | "keywords": [ 23 | "event", 24 | "store", 25 | "eventstore", 26 | "nestjs", 27 | "nest", 28 | "cqrs", 29 | "event-streaming" 30 | ], 31 | "author": "Renew Software, LLC", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/renew-app/nestjs-eventstore/issues" 35 | }, 36 | "homepage": "https://github.com/renew-app/nestjs-eventstore#readme", 37 | "devDependencies": { 38 | "@types/debug": "^4.1.5", 39 | "@types/jest": "^26.0.22", 40 | "google-protobuf": "^3.15.7", 41 | "jest": "^26.6.3", 42 | "prettier": "^2.2.1", 43 | "ts-jest": "^26.5.4", 44 | "tslint": "^6.1.3", 45 | "tslint-config-prettier": "^1.18.0", 46 | "typescript": "^4.2.3" 47 | }, 48 | "dependencies": { 49 | "@eventstore/db-client": "1.1.0", 50 | "@grpc/grpc-js": "1.2.12", 51 | "@nestjs/common": "7.6.15", 52 | "@nestjs/core": "7.6.15", 53 | "@nestjs/cqrs": "7.0.1", 54 | "@types/debug": "4.1.5", 55 | "guid-typescript": "1.0.9", 56 | "reflect-metadata": "0.1.13", 57 | "rxjs": "6.6.7" 58 | }, 59 | "files": [ 60 | "lib/**/*" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /src/client/event-store.client.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Credentials, 3 | DnsClusterOptions, 4 | Endpoint, 5 | EventStoreConnectionStringOptions, 6 | EventStoreDnsClusterOptions, 7 | EventStoreGossipClusterOptions, 8 | EventStoreSingleNodeOptions, 9 | GossipClusterOptions, 10 | SingleNodeOptions, 11 | } from '../interfaces'; 12 | 13 | import { EventStoreClient } from './event-store.client'; 14 | 15 | test('instantiate eventstoreclient with insecure connection string', () => { 16 | const esClient = new EventStoreClient({ 17 | connectionString: 'esdb://eventstore:2113?tls=false', 18 | } as EventStoreConnectionStringOptions); 19 | expect(esClient).toBeDefined(); 20 | }); 21 | 22 | test('instantiate eventstoreclient with secure connection string and credentials', () => { 23 | const esClient = new EventStoreClient({ 24 | connectionString: 'esdb://admin:changeit@eventstore:2113?tls=true', 25 | } as EventStoreConnectionStringOptions); 26 | expect(esClient).toBeDefined(); 27 | }); 28 | 29 | test('instantiate eventstoreclient with non-connection string DNS cluster configuration', () => { 30 | const esClient = new EventStoreClient({ 31 | connectionSettings: { 32 | discover: { 33 | address: 'eventstore', 34 | port: 2113, 35 | } as Endpoint, 36 | nodePreference: 'random', 37 | } as DnsClusterOptions, 38 | channelCredentials: { 39 | certChain: undefined, 40 | insecure: true, 41 | privateKey: undefined, 42 | rootCertificate: undefined, 43 | verifyOptions: undefined, 44 | }, 45 | defaultUserCredentials: { 46 | username: 'admin', 47 | password: 'changeit', 48 | } as Credentials, 49 | } as EventStoreDnsClusterOptions); 50 | expect(esClient).toBeDefined(); 51 | }); 52 | 53 | test('instantiate eventstoreclient with non-connection string gossip cluster configuration', () => { 54 | const esClient = new EventStoreClient({ 55 | connectionSettings: { 56 | endpoints: [ 57 | { 58 | address: 'eventstore', 59 | port: 2113, 60 | }, 61 | ], 62 | nodePreference: 'random', 63 | } as GossipClusterOptions, 64 | channelCredentials: { 65 | certChain: undefined, 66 | insecure: true, 67 | privateKey: undefined, 68 | rootCertificate: undefined, 69 | verifyOptions: undefined, 70 | }, 71 | defaultUserCredentials: { 72 | username: 'admin', 73 | password: 'changeit', 74 | } as Credentials, 75 | } as EventStoreGossipClusterOptions); 76 | expect(esClient).toBeDefined(); 77 | }); 78 | 79 | test('instantiate eventstoreclient with non-connection string single node configuration', () => { 80 | const esClient = new EventStoreClient({ 81 | connectionSettings: { 82 | endpoint: { 83 | address: 'eventstore', 84 | port: 2113, 85 | }, 86 | } as SingleNodeOptions, 87 | channelCredentials: { 88 | certChain: undefined, 89 | insecure: true, 90 | privateKey: undefined, 91 | rootCertificate: undefined, 92 | verifyOptions: undefined, 93 | }, 94 | defaultUserCredentials: { 95 | username: 'admin', 96 | password: 'changeit', 97 | } as Credentials, 98 | } as EventStoreSingleNodeOptions); 99 | expect(esClient).toBeDefined(); 100 | }); 101 | -------------------------------------------------------------------------------- /src/client/event-store.client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AppendResult, 3 | END, 4 | EventStoreDBClient, 5 | jsonEvent, 6 | PersistentSubscription, 7 | ReadRevision, 8 | START, 9 | StreamSubscription, 10 | } from '@eventstore/db-client'; 11 | import { GossipClusterOptions, SingleNodeOptions } from '@eventstore/db-client/dist/Client'; 12 | import { PersistentSubscriptionSettings } from '@eventstore/db-client/dist/utils'; 13 | import { Inject, Logger } from '@nestjs/common'; 14 | import { Guid } from 'guid-typescript'; 15 | import { EVENT_STORE_CONNECTION_OPTIONS } from '../event-store.constants'; 16 | import { 17 | DnsClusterOptions, 18 | EventStoreConnectionStringOptions, 19 | EventStoreDnsClusterOptions, 20 | EventStoreEvent, 21 | EventStoreGossipClusterOptions, 22 | EventStoreSingleNodeOptions, 23 | } from '../interfaces'; 24 | 25 | export class EventStoreClient { 26 | [x: string]: any; 27 | private logger: Logger = new Logger(this.constructor.name); 28 | private client: EventStoreDBClient; 29 | 30 | constructor( 31 | @Inject(EVENT_STORE_CONNECTION_OPTIONS) 32 | options: 33 | | EventStoreConnectionStringOptions 34 | | EventStoreDnsClusterOptions 35 | | EventStoreGossipClusterOptions 36 | | EventStoreSingleNodeOptions, 37 | ) { 38 | try { 39 | if (options) { 40 | if ((options as EventStoreConnectionStringOptions).connectionString) { 41 | const { connectionString, parts } = options as EventStoreConnectionStringOptions; 42 | this.client = EventStoreDBClient.connectionString(connectionString, ...(parts || [])); 43 | } else { 44 | const { connectionSettings, channelCredentials, defaultUserCredentials } = options as 45 | | EventStoreDnsClusterOptions 46 | | EventStoreGossipClusterOptions 47 | | EventStoreSingleNodeOptions; 48 | 49 | if ((connectionSettings as DnsClusterOptions).discover) { 50 | this.client = new EventStoreDBClient( 51 | connectionSettings as DnsClusterOptions, 52 | channelCredentials, 53 | defaultUserCredentials, 54 | ); 55 | } else if ((connectionSettings as GossipClusterOptions).endpoints) { 56 | this.client = new EventStoreDBClient( 57 | connectionSettings as GossipClusterOptions, 58 | channelCredentials, 59 | defaultUserCredentials, 60 | ); 61 | } else if ((connectionSettings as SingleNodeOptions).endpoint) { 62 | this.client = new EventStoreDBClient( 63 | connectionSettings as SingleNodeOptions, 64 | channelCredentials, 65 | defaultUserCredentials, 66 | ); 67 | } else { 68 | throw Error('The connectionSettings property appears to be incomplete or malformed.'); 69 | } 70 | } 71 | } else { 72 | throw Error('Connection information not provided.'); 73 | } 74 | } catch (e) { 75 | this.logger.error(e); 76 | throw e; 77 | } 78 | } 79 | 80 | async writeEventToStream(streamName: string, eventType: string, payload: any, metadata?: any): Promise { 81 | const event = jsonEvent({ 82 | id: Guid.create().toString(), 83 | type: eventType, 84 | data: payload, 85 | metadata, 86 | }); 87 | 88 | return this.client.appendToStream(streamName, event); 89 | } 90 | 91 | async writeEventsToStream(streamName: string, events: EventStoreEvent[]): Promise { 92 | const jsonEvents = events.map((e) => { 93 | return jsonEvent({ 94 | id: Guid.create().toString(), 95 | type: e.eventType, 96 | data: e.payload, 97 | metadata: e.metadata, 98 | }); 99 | }); 100 | 101 | return this.client.appendToStream(streamName, [...jsonEvents]); 102 | } 103 | 104 | async createPersistentSubscription( 105 | streamName: string, 106 | persistentSubscriptionName: string, 107 | settings: PersistentSubscriptionSettings, 108 | ): Promise { 109 | return this.client.createPersistentSubscription(streamName, persistentSubscriptionName, settings); 110 | } 111 | 112 | async subscribeToPersistentSubscription( 113 | streamName: string, 114 | persistentSubscriptionName: string, 115 | ): Promise { 116 | return this.client.connectToPersistentSubscription(streamName, persistentSubscriptionName); 117 | } 118 | 119 | async subscribeToCatchupSubscription(streamName: string, fromRevision?: ReadRevision): Promise { 120 | return this.client.subscribeToStream(streamName, { 121 | fromRevision: fromRevision || START, 122 | resolveLinkTos: true, 123 | }); 124 | } 125 | 126 | async subscribeToVolatileSubscription(streamName: string): Promise { 127 | return this.client.subscribeToStream(streamName, { 128 | fromRevision: END, 129 | resolveLinkTos: true, 130 | }); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event-store.client'; 2 | -------------------------------------------------------------------------------- /src/event-store-core.module.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { CommandBus, EventBus, QueryBus } from '@nestjs/cqrs'; 4 | import { DynamicModule, Global, Logger, Module, OnModuleInit, Provider } from '@nestjs/common'; 5 | import { EventStoreBusConfig, EventStoreBusProvider } from '.'; 6 | import { 7 | EventStoreConnectionStringOptions, 8 | EventStoreDnsClusterOptions, 9 | EventStoreGossipClusterOptions, 10 | EventStoreSingleNodeOptions, 11 | IEventConstructors, 12 | } from './interfaces'; 13 | 14 | import { EventStoreClient } from './client'; 15 | import { EventStoreModule } from './event-store.module'; 16 | import { EventStorePublisher } from './event-store.publisher'; 17 | import { ExplorerService } from '@nestjs/cqrs/dist/services/explorer.service'; 18 | import { ModuleRef } from '@nestjs/core'; 19 | 20 | @Global() 21 | @Module({}) 22 | export class EventStoreCoreModule implements OnModuleInit { 23 | private readonly logger = new Logger(this.constructor.name); 24 | 25 | constructor( 26 | private readonly explorerService: ExplorerService, 27 | private readonly eventsBus: EventBus, 28 | private readonly commandsBus: CommandBus, 29 | private readonly queryBus: QueryBus, 30 | ) {} 31 | 32 | onModuleInit() { 33 | const { events, queries, sagas, commands } = this.explorerService.explore(); 34 | this.eventsBus.register(events); 35 | this.commandsBus.register(commands); 36 | this.queryBus.register(queries); 37 | this.eventsBus.registerSagas(sagas); 38 | } 39 | 40 | static forRoot( 41 | options: 42 | | EventStoreConnectionStringOptions 43 | | EventStoreDnsClusterOptions 44 | | EventStoreGossipClusterOptions 45 | | EventStoreSingleNodeOptions, 46 | eventStoreBusConfigs: EventStoreBusConfig[], 47 | ): DynamicModule { 48 | const eventBusProvider = this.createEventBusProviders(eventStoreBusConfigs); 49 | 50 | return { 51 | module: EventStoreCoreModule, 52 | imports: [EventStoreModule.forRoot(options)], 53 | providers: [ 54 | CommandBus, 55 | QueryBus, 56 | EventStorePublisher, 57 | ExplorerService, 58 | eventBusProvider, 59 | { 60 | provide: EventStoreBusProvider, 61 | useExisting: EventBus, 62 | }, 63 | ], 64 | exports: [ 65 | EventStoreModule, 66 | EventStoreBusProvider, 67 | EventBus, 68 | CommandBus, 69 | QueryBus, 70 | ExplorerService, 71 | EventStorePublisher, 72 | ], 73 | }; 74 | } 75 | 76 | private static createEventBusProviders(configs: EventStoreBusConfig[]): Provider { 77 | let events: IEventConstructors = {}; 78 | configs.forEach((c) => { 79 | events = { 80 | ...events, 81 | ...c.events, 82 | }; 83 | }); 84 | 85 | const subscriptions = configs 86 | .map((c) => { 87 | return c.subscriptions; 88 | }) 89 | .reduce((a, b) => a.concat(b), []); 90 | 91 | return { 92 | provide: EventBus, 93 | useFactory: (commandBus, moduleRef, client) => { 94 | return new EventStoreBusProvider(commandBus, moduleRef, client, { 95 | subscriptions, 96 | events, 97 | }); 98 | }, 99 | inject: [CommandBus, ModuleRef, EventStoreClient], 100 | }; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/event-store.bus.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ErrorType, 3 | PersistentSubscriptionSettings, 4 | persistentSubscriptionSettingsFromDefaults, 5 | } from '@eventstore/db-client/dist/utils'; 6 | import { 7 | EventStoreCatchupSubscription as EsCatchUpSubscription, 8 | EventStorePersistentSubscription as EsPersistentSubscription, 9 | ExtendedCatchUpSubscription, 10 | ExtendedPersistentSubscription, 11 | } from './types'; 12 | import { EventStoreBusConfig, IEventConstructors } from '.'; 13 | import { Logger, OnModuleDestroy } from '@nestjs/common'; 14 | import { PersistentAction, ResolvedEvent } from '@eventstore/db-client'; 15 | 16 | import { EventStoreClient } from './client'; 17 | import { EventStoreSubscriptionType } from './event-store.constants'; 18 | import { IEvent } from '@nestjs/cqrs'; 19 | import { Subject } from 'rxjs'; 20 | 21 | export class EventStoreBus implements OnModuleDestroy { 22 | private eventHandlers: IEventConstructors = {}; 23 | private logger = new Logger(this.constructor.name); 24 | 25 | private catchupSubscriptions: ExtendedCatchUpSubscription[] = []; 26 | private catchupSubscriptionCount: number = 0; 27 | 28 | private persistentSubscriptions: ExtendedPersistentSubscription[] = []; 29 | private persistentSubscriptionsCount: number = 0; 30 | 31 | constructor( 32 | private readonly client: EventStoreClient, 33 | private readonly subject$: Subject, 34 | private readonly config: EventStoreBusConfig, 35 | ) { 36 | this.addEventHandlers(this.config.events); 37 | 38 | const catchupSubscriptions = 39 | this.config.subscriptions?.filter((s) => { 40 | return s.type === EventStoreSubscriptionType.CatchUp; 41 | }) || []; 42 | 43 | this.subscribeToCatchUpSubscriptions(catchupSubscriptions as EsCatchUpSubscription[]); 44 | 45 | const persistentSubscriptions = 46 | this.config.subscriptions?.filter((s) => { 47 | return s.type === EventStoreSubscriptionType.Persistent; 48 | }) || []; 49 | 50 | this.subscribeToPersistentSubscriptions(persistentSubscriptions as EsPersistentSubscription[]); 51 | } 52 | 53 | async subscribeToPersistentSubscriptions(subscriptions: EsPersistentSubscription[]) { 54 | this.persistentSubscriptionsCount = subscriptions.length; 55 | 56 | const createSubscriptionResults = await this.createMissingPersistentSubscriptions(subscriptions); 57 | 58 | const availableSubscriptionsCount = createSubscriptionResults.filter((s) => s.isCreated === true).length; 59 | 60 | if (availableSubscriptionsCount === this.persistentSubscriptionsCount) { 61 | this.persistentSubscriptions = await Promise.all( 62 | subscriptions.map(async (sub) => { 63 | return await this.subscribeToPersistentSubscription(sub.stream, sub.persistentSubscriptionName); 64 | }), 65 | ); 66 | } else { 67 | this.logger.error( 68 | `Not proceeding with subscribing to persistent subscriptions. Configured subscriptions ${this.persistentSubscriptionsCount} does not equal the created and available subscriptions ${availableSubscriptionsCount}.`, 69 | ); 70 | } 71 | } 72 | 73 | async createMissingPersistentSubscriptions( 74 | subscriptions: EsPersistentSubscription[], 75 | ): Promise { 76 | const settings: PersistentSubscriptionSettings = persistentSubscriptionSettingsFromDefaults({ 77 | resolveLinkTos: true, 78 | }); 79 | 80 | try { 81 | const subs = subscriptions.map(async (sub) => { 82 | this.logger.verbose( 83 | `Starting to verify and create persistent subscription - [${sub.stream}][${sub.persistentSubscriptionName}]`, 84 | ); 85 | 86 | return this.client 87 | .createPersistentSubscription(sub.stream, sub.persistentSubscriptionName, settings) 88 | .then(() => { 89 | this.logger.verbose(`Created persistent subscription - ${sub.persistentSubscriptionName}:${sub.stream}`); 90 | return { 91 | isLive: false, 92 | isCreated: true, 93 | stream: sub.stream, 94 | subscription: sub.persistentSubscriptionName, 95 | } as ExtendedPersistentSubscription; 96 | }) 97 | .catch((reason) => { 98 | if (reason.type === ErrorType.PERSISTENT_SUBSCRIPTION_EXISTS) { 99 | this.logger.verbose( 100 | `Persistent Subscription - ${sub.persistentSubscriptionName}:${sub.stream} already exists. Skipping creation.`, 101 | ); 102 | 103 | return { 104 | isLive: false, 105 | isCreated: true, 106 | stream: sub.stream, 107 | subscription: sub.persistentSubscriptionName, 108 | } as ExtendedPersistentSubscription; 109 | } else { 110 | this.logger.error(`[${sub.stream}][${sub.persistentSubscriptionName}] ${reason.message} ${reason.stack}`); 111 | 112 | return { 113 | isLive: false, 114 | isCreated: false, 115 | stream: sub.stream, 116 | subscription: sub.persistentSubscriptionName, 117 | } as ExtendedPersistentSubscription; 118 | } 119 | }); 120 | }); 121 | 122 | return await Promise.all(subs); 123 | } catch (e) { 124 | this.logger.error(e); 125 | return []; 126 | } 127 | } 128 | 129 | async subscribeToCatchUpSubscriptions(subscriptions: EsCatchUpSubscription[]) { 130 | this.catchupSubscriptionCount = subscriptions.length; 131 | this.catchupSubscriptions = await Promise.all( 132 | subscriptions.map((sub) => { 133 | return this.subscribeToCatchUpSubscription(sub.stream); 134 | }), 135 | ); 136 | } 137 | 138 | get allCatchupSubsriptionsLive(): boolean { 139 | const initialized = this.catchupSubscriptions.length === this.catchupSubscriptionCount; 140 | 141 | return ( 142 | initialized && 143 | this.catchupSubscriptions.every((sub) => { 144 | return !!sub && sub.isLive; 145 | }) 146 | ); 147 | } 148 | 149 | get allPersistentSubscriptionsLive(): boolean { 150 | const initialized = this.persistentSubscriptions.length === this.persistentSubscriptionsCount; 151 | 152 | return ( 153 | initialized && 154 | this.persistentSubscriptions.every((sub) => { 155 | return !!sub && sub.isLive; 156 | }) 157 | ); 158 | } 159 | 160 | get isLive(): boolean { 161 | return this.allCatchupSubsriptionsLive && this.allPersistentSubscriptionsLive; 162 | } 163 | 164 | async publish(event: IEvent, stream?: string) { 165 | try { 166 | this.logger.debug({ 167 | message: `Publishing event`, 168 | event, 169 | stream, 170 | }); 171 | 172 | this.client.writeEventToStream(stream || '$svc-catch-all', event.constructor.name, event); 173 | } catch (e) { 174 | this.logger.error(e); 175 | throw new Error(e); 176 | } 177 | } 178 | 179 | async publishAll(events: IEvent[], stream?: string) { 180 | try { 181 | this.logger.debug({ 182 | message: `Publishing events`, 183 | events, 184 | stream, 185 | }); 186 | this.client.writeEventsToStream( 187 | stream || '$svc.catch-all', 188 | events.map((ev) => { 189 | return { 190 | contentType: 'application/json', 191 | eventType: event?.constructor.name || '', 192 | payload: event, 193 | }; 194 | }), 195 | ); 196 | } catch (e) { 197 | this.logger.error(e); 198 | throw new Error(e); 199 | } 200 | } 201 | 202 | async subscribeToCatchUpSubscription(stream: string): Promise { 203 | try { 204 | const resolved = (await this.client.subscribeToCatchupSubscription(stream)) as ExtendedCatchUpSubscription; 205 | 206 | resolved 207 | .on('data', (ev: ResolvedEvent) => this.onEvent(ev)) 208 | .on('confirmation', () => this.logger.log(`[${stream}] Catch-Up subscription confirmation`)) 209 | .on('close', () => this.logger.log(`[${stream}] Subscription closed`)) 210 | .on('error', (err: Error) => { 211 | this.logger.error({ stream, error: err, msg: `Subscription error` }); 212 | this.onDropped(resolved); 213 | }); 214 | 215 | this.logger.verbose(`Catching up and subscribing to stream ${stream}`); 216 | resolved.isLive = true; 217 | 218 | return resolved; 219 | } catch (e) { 220 | this.logger.error(`[${stream}] ${e.message} ${e.stack}`); 221 | throw new Error(e); 222 | } 223 | } 224 | 225 | async subscribeToPersistentSubscription( 226 | stream: string, 227 | subscriptionName: string, 228 | ): Promise { 229 | try { 230 | const resolved = (await this.client.subscribeToPersistentSubscription( 231 | stream, 232 | subscriptionName, 233 | )) as ExtendedPersistentSubscription; 234 | 235 | resolved 236 | .on('data', (ev: ResolvedEvent) => { 237 | try { 238 | this.onEvent(ev); 239 | resolved.ack(ev.event?.id || ''); 240 | } catch (err) { 241 | this.logger.error({ 242 | error: err, 243 | msg: `Error handling event`, 244 | event: ev, 245 | stream, 246 | subscriptionName, 247 | }); 248 | resolved.nack('retry', err, ev.event?.id || ''); 249 | } 250 | }) 251 | .on('confirmation', () => 252 | this.logger.log(`[${stream}][${subscriptionName}] Persistent subscription confirmation`), 253 | ) 254 | .on('close', () => { 255 | this.logger.log(`[${stream}][${subscriptionName}] Persistent subscription closed`); 256 | this.onDropped(resolved); 257 | this.reSubscribeToPersistentSubscription(stream, subscriptionName); 258 | }) 259 | .on('error', (err: Error) => { 260 | this.logger.error({ stream, subscriptionName, error: err, msg: `Persistent subscription error` }); 261 | this.onDropped(resolved); 262 | this.reSubscribeToPersistentSubscription(stream, subscriptionName); 263 | }); 264 | 265 | this.logger.verbose(`Connection to persistent subscription ${subscriptionName} on stream ${stream} established.`); 266 | resolved.isLive = true; 267 | 268 | return resolved; 269 | } catch (e) { 270 | this.logger.error(`[${stream}][${subscriptionName}] ${e.message} ${e.stack}`); 271 | throw new Error(e); 272 | } 273 | } 274 | 275 | onEvent(payload: ResolvedEvent) { 276 | const { event } = payload; 277 | 278 | if (!event || !event.isJson) { 279 | this.logger.error(`Received event that could not be resolved: ${event?.id}:${event?.streamId}`); 280 | return; 281 | } 282 | 283 | const { type, id, streamId, data } = event; 284 | 285 | const handler = this.eventHandlers[type]; 286 | 287 | if (!handler) { 288 | this.logger.warn(`Received event that could not be handled: ${type}:${id}:${streamId}`); 289 | return; 290 | } 291 | 292 | const rawData = JSON.parse(JSON.stringify(data)); 293 | const parsedData = Object.values(rawData); 294 | 295 | if (this.eventHandlers && this.eventHandlers[type || rawData.content.eventType]) { 296 | this.subject$.next(this.eventHandlers[type || rawData.content.eventType](...parsedData)); 297 | } else { 298 | this.logger.warn(`Event of type ${type} not able to be handled.`); 299 | } 300 | } 301 | 302 | onDropped(sub: ExtendedCatchUpSubscription | ExtendedPersistentSubscription) { 303 | sub.isLive = false; 304 | } 305 | 306 | reSubscribeToPersistentSubscription(stream: string, subscriptionName: string) { 307 | this.logger.warn(`Reconnecting to subscription ${subscriptionName} ${stream}...`); 308 | setTimeout((st, subName) => this.subscribeToPersistentSubscription(st, subName), 3000, stream, subscriptionName); 309 | } 310 | 311 | addEventHandlers(eventHandlers: IEventConstructors) { 312 | this.eventHandlers = { 313 | ...this.eventHandlers, 314 | ...eventHandlers, 315 | }; 316 | } 317 | 318 | onModuleDestroy() { 319 | this.persistentSubscriptions?.forEach((sub) => { 320 | if (!!sub?.isLive) { 321 | sub.unsubscribe(); 322 | } 323 | }); 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/event-store.constants.ts: -------------------------------------------------------------------------------- 1 | export const EVENT_STORE_CONNECTION_OPTIONS = 'EVENT_STORE_CONNECTION_OPTIONS'; 2 | export const EVENT_STORE_CLIENT = 'EventStoreClient'; 3 | 4 | export enum EventStoreSubscriptionType { 5 | Persistent, 6 | CatchUp, 7 | } 8 | -------------------------------------------------------------------------------- /src/event-store.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Global, Module } from '@nestjs/common'; 2 | import { 3 | EventStoreConnectionStringOptions, 4 | EventStoreDnsClusterOptions, 5 | EventStoreGossipClusterOptions, 6 | EventStoreSingleNodeOptions, 7 | } from './interfaces'; 8 | 9 | import { EVENT_STORE_CONNECTION_OPTIONS } from './event-store.constants'; 10 | import { EventStoreClient } from './client'; 11 | 12 | @Global() 13 | @Module({ 14 | imports: [EventStoreClient], 15 | exports: [EventStoreClient], 16 | }) 17 | export class EventStoreModule { 18 | static forRoot( 19 | options: 20 | | EventStoreConnectionStringOptions 21 | | EventStoreDnsClusterOptions 22 | | EventStoreGossipClusterOptions 23 | | EventStoreSingleNodeOptions, 24 | ): DynamicModule { 25 | const connectionProviders = [ 26 | { 27 | provide: EVENT_STORE_CONNECTION_OPTIONS, 28 | useValue: { 29 | ...options, 30 | }, 31 | }, 32 | ]; 33 | 34 | const clientProvider = { 35 | provide: EventStoreClient, 36 | useClass: EventStoreClient, 37 | }; 38 | 39 | return { 40 | module: EventStoreModule, 41 | providers: [...connectionProviders, clientProvider], 42 | exports: [...connectionProviders, clientProvider], 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/event-store.publisher.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:max-classes-per-file */ 2 | import { AggregateRoot, IEvent } from '@nestjs/cqrs'; 3 | import { Constructor, EventStoreBusProvider } from '.'; 4 | 5 | import { AggregateEvent } from './interfaces'; 6 | import { Injectable } from '@nestjs/common'; 7 | 8 | @Injectable() 9 | export class EventStorePublisher { 10 | constructor(private readonly eventBus: EventStoreBusProvider) {} 11 | 12 | mergeClassContext>(metatype: T): T { 13 | const eventBus = this.eventBus; 14 | return class extends metatype { 15 | publish(event: IEvent) { 16 | eventBus.publish(event, (event as AggregateEvent).streamName); 17 | } 18 | }; 19 | } 20 | 21 | mergeObjectContext(object: T): T { 22 | const eventBus = this.eventBus; 23 | object.publish = (event: IEvent) => { 24 | eventBus.publish(event, (event as AggregateEvent).streamName); 25 | }; 26 | return object; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces'; 2 | export * from './client'; 3 | export * from './providers'; 4 | export * from './types'; 5 | export * from './event-store-core.module'; 6 | export * from './event-store.bus'; 7 | export * from './event-store.constants'; 8 | export * from './event-store.module'; 9 | export * from './event-store.publisher'; 10 | -------------------------------------------------------------------------------- /src/interfaces/aggregate-event.interface.ts: -------------------------------------------------------------------------------- 1 | import { IEvent } from '@nestjs/cqrs'; 2 | 3 | export interface AggregateEvent extends IEvent { 4 | streamName: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/interfaces/event-constructors.interface.ts: -------------------------------------------------------------------------------- 1 | import { IEvent } from '@nestjs/cqrs'; 2 | 3 | export interface IEventConstructors { 4 | [key: string]: (...args: any[]) => IEvent; 5 | } 6 | -------------------------------------------------------------------------------- /src/interfaces/event-store-async-options.interface.ts: -------------------------------------------------------------------------------- 1 | export interface EventStoreAsyncOptions { 2 | useFactory: (...args: any[]) => Promise | any; 3 | inject?: any[]; 4 | } 5 | -------------------------------------------------------------------------------- /src/interfaces/event-store-event.interface.ts: -------------------------------------------------------------------------------- 1 | import { JSONEventData } from '@eventstore/db-client'; 2 | 3 | export interface EventStoreEvent { 4 | contentType: JSONEventData['contentType']; 5 | eventType: string; 6 | payload: any; 7 | metadata?: any; 8 | } 9 | -------------------------------------------------------------------------------- /src/interfaces/event-store-feature-options.interface.ts: -------------------------------------------------------------------------------- 1 | export interface EventStoreFeatureOptions { 2 | streamName: string; 3 | eventsToPublish?: string[]; 4 | } 5 | -------------------------------------------------------------------------------- /src/interfaces/event-store-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { ChannelCredentials } from '@grpc/grpc-js'; 2 | 3 | declare const RANDOM = 'random'; 4 | declare const FOLLOWER = 'follower'; 5 | declare const LEADER = 'leader'; 6 | declare type NodePreference = typeof RANDOM | typeof FOLLOWER | typeof LEADER; 7 | 8 | export interface Endpoint { 9 | address: string; 10 | port: number; 11 | } 12 | 13 | export interface ChannelCredentialOptions { 14 | insecure?: boolean; 15 | rootCertificate?: Buffer; 16 | privateKey?: Buffer; 17 | certChain?: Buffer; 18 | verifyOptions?: Parameters[3]; 19 | } 20 | 21 | export interface Credentials { 22 | username: string; 23 | password: string; 24 | } 25 | 26 | export interface DnsClusterOptions { 27 | discover: Endpoint; 28 | nodePreference?: NodePreference; 29 | } 30 | 31 | export interface GossipClusterOptions { 32 | endpoints: Endpoint[]; 33 | nodePreference?: NodePreference; 34 | } 35 | 36 | export interface SingleNodeOptions { 37 | endpoint: Endpoint | string; 38 | } 39 | 40 | export interface EventStoreConnectionStringOptions { 41 | connectionString: TemplateStringsArray | string; 42 | parts?: string[]; 43 | } 44 | 45 | export interface EventStoreDnsClusterOptions { 46 | connectionSettings: DnsClusterOptions; 47 | channelCredentials?: ChannelCredentialOptions; 48 | defaultUserCredentials?: Credentials; 49 | } 50 | 51 | export interface EventStoreGossipClusterOptions { 52 | connectionSettings: GossipClusterOptions; 53 | channelCredentials?: ChannelCredentialOptions; 54 | defaultUserCredentials?: Credentials; 55 | } 56 | 57 | export interface EventStoreSingleNodeOptions { 58 | connectionSettings: SingleNodeOptions; 59 | channelCredentials?: ChannelCredentialOptions; 60 | defaultUserCredentials?: Credentials; 61 | } 62 | -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event-store-options.interface'; 2 | export * from './event-store-feature-options.interface'; 3 | export * from './event-store-async-options.interface'; 4 | export * from './aggregate-event.interface'; 5 | export * from './event-constructors.interface'; 6 | export * from './event-store-event.interface'; 7 | -------------------------------------------------------------------------------- /src/providers/event-store-bus.provider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CommandBus, 3 | EventHandlerType, 4 | ICommand, 5 | IEvent, 6 | IEventHandler, 7 | ISaga, 8 | InvalidSagaException, 9 | ObservableBus, 10 | } from '@nestjs/cqrs'; 11 | import { EVENTS_HANDLER_METADATA, SAGA_METADATA } from '@nestjs/cqrs/dist/decorators/constants'; 12 | import { Injectable, Logger, OnModuleDestroy, Type } from '@nestjs/common'; 13 | import { Observable, Subscription } from 'rxjs'; 14 | 15 | import { EventStoreBus } from '../event-store.bus'; 16 | import { EventStoreBusConfig } from '..'; 17 | import { EventStoreClient } from '../client'; 18 | import { ModuleRef } from '@nestjs/core'; 19 | import { filter } from 'rxjs/operators'; 20 | import { isFunction } from 'util'; 21 | 22 | @Injectable() 23 | export class EventStoreBusProvider extends ObservableBus implements OnModuleDestroy { 24 | private _publisher: EventStoreBus; 25 | private readonly subscriptions: Subscription[]; 26 | private readonly logger = new Logger(this.constructor.name); 27 | 28 | constructor( 29 | private readonly commandBus: CommandBus, 30 | private readonly moduleRef: ModuleRef, 31 | private readonly client: EventStoreClient, 32 | private readonly config: EventStoreBusConfig, 33 | ) { 34 | super(); 35 | this.subscriptions = []; 36 | 37 | this._publisher = new EventStoreBus(this.client, this.subject$, this.config); 38 | } 39 | 40 | get publisher(): EventStoreBus { 41 | return this._publisher; 42 | } 43 | 44 | set publisher(_publisher: EventStoreBus) { 45 | this._publisher = _publisher; 46 | } 47 | 48 | onModuleDestroy() { 49 | this.subscriptions.forEach((sub) => sub.unsubscribe()); 50 | } 51 | 52 | publish(event: T, stream: string) { 53 | this._publisher.publish(event, stream); 54 | } 55 | 56 | publishAll(events: IEvent[]) { 57 | (events || []).forEach((ev) => this._publisher.publish(ev)); 58 | } 59 | 60 | bind(handler: IEventHandler, name: string) { 61 | const stream$ = name ? this.ofEventName(name) : this.subject$; 62 | const subscription = stream$.subscribe((ev) => handler.handle(ev)); 63 | this.subscriptions.push(subscription); 64 | } 65 | 66 | registerSagas(types: Type[] = []) { 67 | const sagas = types 68 | .map((target) => { 69 | const metadata = Reflect.getMetadata(SAGA_METADATA, target) || []; 70 | const instance = this.moduleRef.get(target, { strict: false }); 71 | if (!instance) { 72 | throw new InvalidSagaException(); 73 | } 74 | return metadata.map((k: string) => instance[k]); 75 | }) 76 | .reduce((a, b) => a.concat(b), []); 77 | 78 | sagas.forEach((saga: ISaga) => this.registerSaga(saga)); 79 | } 80 | 81 | register(handlers: EventHandlerType[] = []) { 82 | handlers.forEach((hand) => this.registerHandler(hand)); 83 | } 84 | 85 | protected registerHandler(handler: EventHandlerType) { 86 | const instance = this.moduleRef.get(handler, { strict: false }); 87 | if (!instance) { 88 | return; 89 | } 90 | 91 | const eventsNames = this.reflectEventsNames(handler); 92 | eventsNames.map((ev) => this.bind(instance as IEventHandler, ev.name)); 93 | } 94 | 95 | protected ofEventName(name: string) { 96 | return this.subject$.pipe(filter((ev) => this.getEventName(ev) === name)); 97 | } 98 | 99 | private getEventName(event: IEvent): string { 100 | const { constructor } = Object.getPrototypeOf(event); 101 | return constructor.name as string; 102 | } 103 | 104 | protected registerSaga(saga: ISaga) { 105 | if (!isFunction(saga)) { 106 | throw new InvalidSagaException(); 107 | } 108 | 109 | const stream$ = saga(this); 110 | 111 | this.logger.log(stream$ instanceof Observable); 112 | 113 | // if (!(stream$ instanceof Observable)) { 114 | // throw new InvalidSagaException(); 115 | // } 116 | 117 | const subscription = stream$.pipe(filter((e) => !!e)).subscribe((command) => this.commandBus.execute(command)); 118 | 119 | this.subscriptions.push(subscription); 120 | } 121 | 122 | private reflectEventsNames(handler: EventHandlerType): FunctionConstructor[] { 123 | return Reflect.getMetadata(EVENTS_HANDLER_METADATA, handler); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/providers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event-store-bus.provider'; 2 | -------------------------------------------------------------------------------- /src/types/constructor.type.ts: -------------------------------------------------------------------------------- 1 | export type Constructor = new (...args: any[]) => T; 2 | -------------------------------------------------------------------------------- /src/types/event-store-bus-config.type.ts: -------------------------------------------------------------------------------- 1 | import { EventStoreSubscription } from './event-store-subscription.type'; 2 | import { IEventConstructors } from '../interfaces'; 3 | 4 | export type EventStoreBusConfig = { 5 | subscriptions: EventStoreSubscription[]; 6 | events: IEventConstructors; 7 | }; 8 | -------------------------------------------------------------------------------- /src/types/event-store-catchup-subscription.type.ts: -------------------------------------------------------------------------------- 1 | import { EventStoreSubscriptionType } from '../event-store.constants'; 2 | import { StreamSubscription } from '@eventstore/db-client'; 3 | 4 | export type EventStoreCatchupSubscription = { 5 | type: EventStoreSubscriptionType.CatchUp; 6 | stream: string; 7 | }; 8 | 9 | export interface ExtendedCatchUpSubscription extends StreamSubscription { 10 | isLive?: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /src/types/event-store-persistent-subscription.type.ts: -------------------------------------------------------------------------------- 1 | import { EventStoreSubscriptionType } from '../event-store.constants'; 2 | import { PersistentSubscription } from '@eventstore/db-client'; 3 | 4 | export type EventStorePersistentSubscription = { 5 | type: EventStoreSubscriptionType.Persistent; 6 | stream: string; 7 | persistentSubscriptionName: string; 8 | }; 9 | 10 | export interface ExtendedPersistentSubscription extends PersistentSubscription { 11 | isLive?: boolean; 12 | isCreated?: boolean; 13 | stream: string; 14 | subscription: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/types/event-store-subscription-config.type.ts: -------------------------------------------------------------------------------- 1 | export type EventStoreSubscriptionConfig = { 2 | persistentSubscriptionName: string; 3 | }; 4 | -------------------------------------------------------------------------------- /src/types/event-store-subscription.type.ts: -------------------------------------------------------------------------------- 1 | import { EventStoreCatchupSubscription } from './event-store-catchup-subscription.type'; 2 | import { EventStorePersistentSubscription } from './event-store-persistent-subscription.type'; 3 | 4 | export type EventStoreSubscription = EventStorePersistentSubscription | EventStoreCatchupSubscription; 5 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event-store-bus-config.type'; 2 | export * from './event-store-catchup-subscription.type'; 3 | export * from './event-store-persistent-subscription.type'; 4 | export * from './event-store-subscription-config.type'; 5 | export * from './event-store-subscription.type'; 6 | export * from './constructor.type'; 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "removeComments": false, 7 | "emitDecoratorMetadata": true, 8 | "outDir": "./lib", 9 | "strict": true, 10 | "experimentalDecorators": true, 11 | "noImplicitAny": false 12 | }, 13 | "include": ["src"], 14 | "exclude": ["node_modules", "**/__tests__/*"] 15 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended", 4 | "tslint-config-prettier" 5 | ] 6 | } --------------------------------------------------------------------------------