├── .github └── workflows │ ├── lint_and_build_workflow.yml │ └── publish_workflow.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── examples └── eventstore.cqrs │ ├── app.module.ts │ ├── config │ └── eventstore.ts │ ├── event-bus.provider.ts │ └── persons │ ├── commands │ ├── handlers │ │ ├── add-person.command.handler.ts │ │ ├── delete-person.command.handler.ts │ │ ├── edit-person.command.handler.ts │ │ └── index.ts │ └── impl │ │ ├── add-person.command.ts │ │ ├── delete-person.command.ts │ │ └── edit-person.command.ts │ ├── controllers │ └── persons.controller.ts │ ├── dto │ └── update-person.dto.ts │ ├── events │ ├── handlers │ │ ├── add-person.event.handler.ts │ │ ├── delete-person.event.handler.ts │ │ ├── edit-person.event.handler.ts │ │ ├── index.ts │ │ └── person.event.handlers.spec.ts │ ├── impl │ │ └── index.ts │ └── instantiators │ │ └── index.ts │ ├── models │ ├── person.aggregate.ts │ ├── person.model.interface.ts │ └── person.model.ts │ ├── persons.module.ts │ ├── queries │ ├── handlers │ │ ├── browse-person.query.handler.ts │ │ ├── index.ts │ │ └── read-person.query.handler.ts │ └── impl │ │ ├── browse-person.query.ts │ │ └── read-person.query.ts │ ├── repositories │ └── person.repository.ts │ └── services │ └── persons.service.ts ├── package-lock.json ├── package.json ├── src ├── event-store │ ├── event-store.class.ts │ ├── event-store.module.ts │ ├── eventstore-cqrs │ │ ├── event-bus.provider.ts │ │ ├── event-publisher.ts │ │ ├── event-store.bus.ts │ │ ├── eventstore-cqrs.module.ts │ │ └── index.ts │ └── shared │ │ └── aggregate-event.interface.ts └── index.ts ├── tsconfig.build.json ├── tsconfig.json └── tslint.json /.github/workflows/lint_and_build_workflow.yml: -------------------------------------------------------------------------------- 1 | name: lint & build 2 | # This workflow is triggered on pushes to the repository. 3 | on: 4 | push: 5 | 6 | jobs: 7 | lint: 8 | name: lint 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: '10.x' 15 | - run: npm install 16 | - run: npm run lint 17 | build: 18 | name: build 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@master 22 | - uses: actions/setup-node@v1 23 | with: 24 | node-version: '10.x' 25 | - run: npm install 26 | - run: npm run build 27 | -------------------------------------------------------------------------------- /.github/workflows/publish_workflow.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | lint: 9 | name: lint 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@master 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: '10.x' 16 | - run: npm install 17 | - run: npm run lint 18 | build: 19 | name: build 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@master 23 | - uses: actions/setup-node@v1 24 | with: 25 | node-version: '10.x' 26 | - run: npm install 27 | - run: npm run build 28 | publish: 29 | name: publish 30 | runs-on: ubuntu-latest 31 | needs: [lint, build] 32 | steps: 33 | - uses: actions/checkout@v1 34 | - uses: actions/setup-node@v1 35 | with: 36 | node-version: '10.x' 37 | registry-url: 'https://registry.npmjs.org' 38 | - run: npm install 39 | - run: npm publish 40 | env: 41 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | 36 | # Certificates 37 | *.key 38 | 39 | #Config 40 | .env -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 daypaio GmbH 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 | --- 3 | 4 |

5 | GitHub Actions status 6 |

7 | 8 |

9 | Nest Logo 10 |

11 | 12 | NPM Version 13 | Package License 14 | NPM Downloads 15 | 16 | ## Description 17 | Injects eventstore connector modules, components, bus and eventstore config into a nestjs application. An example is provided in the examples folder. 18 | 19 | ### Installation 20 | `npm i --save nestjs-eventstore` 21 | 22 | ### Usage 23 | 24 | #### Using the EventStoreCqrsModule 25 | 26 | `EventStoreCqrsModule` uses `@nestjs/cqrs` module under the hood. It overrides the default eventbus of `@nestjs/cqrs` and pushes the event to the eventstore rather than the internal eventBus. 27 | Therefore the `eventBus.publish(event, streamName)` method takes [two arguments instead of one](https://github.com/daypaio/nestjs-eventstore/blob/2e09dd435c60a1a881b9b012d6c83f810b3c85da/src/event-store/eventstore-cqrs/event-store.bus.ts#L115). The first one is the event itself, and the second one is the stream name. 28 | 29 | Once the event is pushed to the eventStore all the subscriptions listening to that event are pushed that event from the event store. Event handlers can then be triggered to cater for those events. 30 | 31 | **app.module.ts** 32 | 33 | ```typescript 34 | import { 35 | EventStoreBusConfig, 36 | EventStoreSubscriptionType, 37 | } from 'nestjs-eventstore'; 38 | 39 | //linking of events from EventStore to local events 40 | const EventInstantiators = [ 41 | SomeEvent: (_id: any, data: any, loggedInUserId: any) => new SomeEvent(_id, data, loggedInUserId); 42 | ]; 43 | 44 | export const eventStoreBusConfig: EventStoreBusConfig = { 45 | subscriptions: [ 46 | { // persistanct subscription 47 | type: EventStoreSubscriptionType.Persistent, 48 | stream: '$ce-persons', 49 | persistentSubscriptionName: 'contacts', 50 | }, 51 | { // Catchup subscription 52 | type: EventStoreSubscriptionType.CatchUp, 53 | stream: '$ce-users', 54 | }, 55 | ], 56 | eventInstantiators: { 57 | ...EventInstantiators 58 | }, 59 | }; 60 | 61 | @Module({ 62 | imports: [ 63 | ConfigModule.load(path.resolve(__dirname, 'config', '**/!(*.d).{ts,js}')), 64 | EventStoreCqrsModule.forRootAsync( 65 | { 66 | useFactory: async (config: ConfigService) => { 67 | return { 68 | connectionSettings: config.get('eventstore.connectionSettings'), 69 | endpoint: config.get('eventstore.tcpEndpoint'), 70 | }; 71 | }, 72 | inject: [ConfigService], 73 | }, 74 | eventStoreBusConfig, 75 | ), 76 | ], 77 | }) 78 | export class AppModule {} 79 | 80 | ``` 81 | 82 | **custom.command.handler.ts** 83 | 84 | This following is a way to use the command handlers that push to the custom eventBus to the eventstore using aggregate root. 85 | 86 | ```typescript 87 | import { ICommandHandler, CommandHandler } from '@nestjs/cqrs'; 88 | import { SomeCommand } from '../impl/some.command'; 89 | import { EventPublisher } from 'nestjs-eventstore'; //this is necessary as it overrides the default publisher 90 | import { ObjectAggregate } from '../../models/object.aggregate'; 91 | 92 | @CommandHandler(SomeCommand) 93 | export class SomeCommandHandler 94 | implements ICommandHandler { 95 | constructor(private readonly publisher: EventPublisher) {} 96 | 97 | async execute(command: SomeCommand) { 98 | const { object, loggedInUserId } = command; 99 | const objectAggregate = this.publisher.mergeObjectContext( 100 | new ObjectAggregate(object._id, object), 101 | ); 102 | objectAggregate.add(loggedInUserId); 103 | objectAggregate.commit(); 104 | } 105 | } 106 | 107 | ``` 108 | 109 | 110 | #### Using the EventStoreModule 111 | 112 | `EventStoreModule` connects directly to the event store without cqrs implementation. 113 | 114 | **app.module.ts** 115 | 116 | ```typescript 117 | @Module({ 118 | imports: [ 119 | ConfigModule.load(path.resolve(__dirname, 'config', '**/!(*.d).{ts,js}')), 120 | EventStoreCqrsModule.forRootAsync( 121 | { 122 | useFactory: async (config: ConfigService) => { 123 | return { 124 | connectionSettings: config.get('eventstore.connectionSettings'), 125 | endpoint: config.get('eventstore.tcpEndpoint'), 126 | }; 127 | }, 128 | inject: [ConfigService], 129 | }, 130 | ), 131 | ], 132 | }) 133 | export class AppModule {} 134 | 135 | ``` -------------------------------------------------------------------------------- /examples/eventstore.cqrs/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PersonsModule } from './persons/persons.module'; 3 | import { ConfigModule, ConfigService } from 'nestjs-config'; 4 | import * as path from 'path'; 5 | import { EventStoreCqrsModule } from 'nestjs-eventstore'; 6 | import { eventStoreBusConfig } from './event-bus.provider'; 7 | 8 | @Module({ 9 | imports: [ 10 | ConfigModule.load(path.resolve(__dirname, 'config', '**/!(*.d).{ts,js}')), 11 | EventStoreCqrsModule.forRootAsync( 12 | { 13 | useFactory: async (config: ConfigService) => { 14 | return { 15 | connectionSettings: config.get('eventstore.connectionSettings'), 16 | endpoint: config.get('eventstore.tcpEndpoint'), 17 | }; 18 | }, 19 | inject: [ConfigService], 20 | }, 21 | eventStoreBusConfig, 22 | ), 23 | PersonsModule, 24 | 25 | ], 26 | }) 27 | export class AppModule { } 28 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/config/eventstore.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | connectionSettings: { 3 | defaultUserCredentials: { 4 | username: process.env.EVENT_STORE_USERNAME, 5 | password: process.env.EVENT_STORE_PASSWORD, 6 | }, 7 | }, 8 | tcpEndpoint: { 9 | host: process.env.EVENT_STORE_TCP_HOST || 'localhost', 10 | port: process.env.EVENT_STORE_TCP_PORT || 1113, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/event-bus.provider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EventStoreBusConfig, 3 | EventStoreSubscriptionType, 4 | } from 'nestjs-eventstore'; 5 | 6 | export class PersonAddedEvent { 7 | constructor( 8 | public _id: string, 9 | public data: any, 10 | public loggedInUserId: string, 11 | ) { } 12 | 13 | get streamName() { 14 | // this is the stream name to which the event will be pushed. 15 | return `persons-${this._id}`; 16 | } 17 | } 18 | 19 | const PersonEventInstantiators = { 20 | PersonAddedEvent: (_id, data, loggedInUserId) => 21 | new PersonAddedEvent(_id, data, loggedInUserId), 22 | }; 23 | 24 | export const eventStoreBusConfig: EventStoreBusConfig = { 25 | subscriptions: [ 26 | { 27 | type: EventStoreSubscriptionType.Persistent, 28 | stream: '$ce-persons', 29 | persistentSubscriptionName: 'contacts', 30 | }, 31 | ], 32 | eventInstantiators: { 33 | ...PersonEventInstantiators, 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/commands/handlers/add-person.command.handler.ts: -------------------------------------------------------------------------------- 1 | import { ICommandHandler, CommandHandler } from '@nestjs/cqrs'; 2 | import { Logger } from '@nestjs/common'; 3 | import { AddPersonCommand } from '../impl/add-person.command'; 4 | import { EventPublisher } from 'nestjs-eventstore'; 5 | import { PersonAggregate } from '../../models/person.aggregate'; 6 | 7 | @CommandHandler(AddPersonCommand) 8 | export class AddPersonCommandHandler 9 | implements ICommandHandler { 10 | private logger: Logger; 11 | constructor(private readonly publisher: EventPublisher) { 12 | this.logger = new Logger(this.constructor.name); 13 | } 14 | 15 | async execute(command: AddPersonCommand) { 16 | this.logger.log('COMMAND TRIGGERED: AddCommandHandler...'); 17 | const { person, loggedInUserId } = command; 18 | const personAggregate = this.publisher.mergeObjectContext( 19 | new PersonAggregate(person._id, person), 20 | ); 21 | personAggregate.add(loggedInUserId); 22 | personAggregate.commit(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/commands/handlers/delete-person.command.handler.ts: -------------------------------------------------------------------------------- 1 | import { CommandHandler, ICommandHandler, EventBus } from '@nestjs/cqrs'; 2 | import { DeletePersonCommand } from '../impl/delete-person.command'; 3 | import { Logger } from '@nestjs/common'; 4 | import { EventPublisher } from 'nestjs-eventstore'; 5 | import { PersonAggregate } from '../../models/person.aggregate'; 6 | 7 | @CommandHandler(DeletePersonCommand) 8 | export class DeletePersonCommandHandler 9 | implements ICommandHandler { 10 | private logger: Logger; 11 | constructor(private readonly publisher: EventPublisher) { 12 | this.logger = new Logger('DeleteCommandHandler'); 13 | } 14 | 15 | async execute(command: DeletePersonCommand) { 16 | this.logger.log('COMMAND TRIGGERED: DeleteCommandHandler...'); 17 | const { id, loggedInUserId } = command; 18 | const personAggregate = this.publisher.mergeObjectContext( 19 | new PersonAggregate(id), 20 | ); 21 | personAggregate.delete(loggedInUserId); 22 | personAggregate.commit(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/commands/handlers/edit-person.command.handler.ts: -------------------------------------------------------------------------------- 1 | import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; 2 | import { EditPersonCommand } from '../impl/edit-person.command'; 3 | import { Logger } from '@nestjs/common'; 4 | import { EventPublisher } from 'nestjs-eventstore'; 5 | import { PersonAggregate } from '../../models/person.aggregate'; 6 | 7 | @CommandHandler(EditPersonCommand) 8 | export class EditPersonCommandHandler 9 | implements ICommandHandler { 10 | private logger: Logger; 11 | constructor(private readonly publisher: EventPublisher) { 12 | this.logger = new Logger('EditCommandHandler'); 13 | } 14 | 15 | async execute(command: EditPersonCommand) { 16 | this.logger.log('COMMAND TRIGGERED: EditCommandHandler...'); 17 | const { id, person, loggedInUserId } = command; 18 | person._id = id; 19 | const personAggregate = this.publisher.mergeObjectContext( 20 | new PersonAggregate(person._id, person), 21 | ); 22 | personAggregate.edit(loggedInUserId); 23 | personAggregate.commit(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/commands/handlers/index.ts: -------------------------------------------------------------------------------- 1 | import { AddPersonCommandHandler } from './add-person.command.handler'; 2 | import { DeletePersonCommandHandler } from './delete-person.command.handler'; 3 | import { EditPersonCommandHandler } from './edit-person.command.handler'; 4 | 5 | export const PersonCommandHandlers = [ 6 | AddPersonCommandHandler, 7 | DeletePersonCommandHandler, 8 | EditPersonCommandHandler, 9 | ]; 10 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/commands/impl/add-person.command.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '@nestjs/cqrs'; 2 | import { IPerson } from '../../models/person.model.interface'; 3 | 4 | export class AddPersonCommand implements ICommand { 5 | constructor( 6 | public readonly person: IPerson, 7 | public readonly loggedInUserId: string, 8 | ) {} 9 | } 10 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/commands/impl/delete-person.command.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '@nestjs/cqrs'; 2 | 3 | export class DeletePersonCommand implements ICommand { 4 | constructor( 5 | public readonly id: string, 6 | public readonly loggedInUserId: string, 7 | ) {} 8 | } 9 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/commands/impl/edit-person.command.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '@nestjs/cqrs'; 2 | import { IPerson } from '../../models/person.model.interface'; 3 | 4 | export class EditPersonCommand implements ICommand { 5 | constructor( 6 | public readonly id: string, 7 | public readonly person: Partial, 8 | public readonly loggedInUserId: string, 9 | ) {} 10 | } 11 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/controllers/persons.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Param, 5 | Patch, 6 | Body, 7 | Post, 8 | Delete, 9 | } from '@nestjs/common'; 10 | import { PersonsService } from '../services/persons.service'; 11 | import { UpdatePersonDto } from '../dto/update-person.dto'; 12 | import { IPerson } from '../models/person.model.interface'; 13 | 14 | @Controller('persons') 15 | export class PersonsController { 16 | constructor(private service: PersonsService) { } 17 | 18 | async browse(): Promise { 19 | return await this.service.browse(); 20 | } 21 | 22 | @Get('/:id') 23 | async read( 24 | @Param('id') id: string, 25 | ): Promise { 26 | return await this.service.read(id); 27 | } 28 | 29 | @Patch('/:id') 30 | async edit( 31 | @Param('id') id: string, 32 | @Body() object: UpdatePersonDto, 33 | ): Promise { 34 | await this.service.edit(id, object); 35 | } 36 | 37 | @Post() 38 | async add( 39 | @Body() object: IPerson 40 | ): Promise { 41 | await this.service.add(object); 42 | } 43 | 44 | @Delete('/:id') 45 | async delete( 46 | @Param('id') id: string 47 | ): Promise { 48 | await this.service.delete(id); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/dto/update-person.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiModelPropertyOptional } from '@nestjs/swagger'; 2 | import { IsOptional, IsEmail, IsString, IsNumberString } from 'class-validator'; 3 | import { Dto } from '../../shared/dtos/dto.interface'; 4 | 5 | export class UpdatePersonDto implements Dto { 6 | constructor(person?: any) { 7 | if (person) { 8 | this.email = person.email; 9 | this.firstName = person.firstName; 10 | this.lastName = person.lastName; 11 | this.phoneNumber = person.phoneNumber; 12 | } 13 | } 14 | @ApiModelPropertyOptional({ 15 | description: 'The email of the person', 16 | }) 17 | @IsOptional() 18 | @IsEmail() 19 | @IsString() 20 | email: string; 21 | 22 | @ApiModelPropertyOptional({ 23 | description: 'The first name of the person', 24 | }) 25 | @IsOptional() 26 | @IsString() 27 | firstName: string; 28 | 29 | @ApiModelPropertyOptional({ 30 | description: 'The last name of the person', 31 | }) 32 | @IsOptional() 33 | @IsString() 34 | lastName: string; 35 | 36 | @ApiModelPropertyOptional({ 37 | description: 'The phone number of the person', 38 | }) 39 | @IsOptional() 40 | @IsNumberString() 41 | phoneNumber: string; 42 | } 43 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/events/handlers/add-person.event.handler.ts: -------------------------------------------------------------------------------- 1 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; 2 | import { Logger } from '@nestjs/common'; 3 | import { PersonRepository } from '../../repositories/person.repository'; 4 | import { PersonAddedEvent } from '../impl'; 5 | 6 | @EventsHandler(PersonAddedEvent) 7 | export class AddPersonEventHandler implements IEventHandler { 8 | private logger = new Logger('AddEventHandler'); 9 | 10 | constructor(private repository: PersonRepository) {} 11 | async handle(event: PersonAddedEvent) { 12 | this.logger.verbose(`EVENT TRIGGERED: ${event.constructor.name}}`); 13 | const { _id, data } = event; 14 | try { 15 | await this.repository.create(data); 16 | } catch (error) { 17 | this.logger.error(`Failed to create person of id ${_id}`); 18 | this.logger.log(error.message); 19 | this.logger.debug(error.stack); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/events/handlers/delete-person.event.handler.ts: -------------------------------------------------------------------------------- 1 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; 2 | import { Logger } from '@nestjs/common'; 3 | import { PersonRepository } from '../../repositories/person.repository'; 4 | import { PersonDeletedEvent } from '../impl'; 5 | 6 | @EventsHandler(PersonDeletedEvent) 7 | export class DeletePersonEventHandler 8 | implements IEventHandler { 9 | private logger = new Logger('DeleteEventHandler'); 10 | constructor(private repository: PersonRepository) {} 11 | async handle(event: PersonDeletedEvent) { 12 | this.logger.verbose(`EVENT TRIGGERED: ${event.constructor.name}}`); 13 | const { _id, loggedInUserId } = event; 14 | try { 15 | const result = await this.repository.deleteById(_id); 16 | return result; 17 | } catch (error) { 18 | this.logger.error(`Cannot delete person of id ${_id}`); 19 | this.logger.log(error.message); 20 | this.logger.debug(error.stack); 21 | // Retry event possibly 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/events/handlers/edit-person.event.handler.ts: -------------------------------------------------------------------------------- 1 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; 2 | import { Logger } from '@nestjs/common'; 3 | import { PersonRepository } from '../../repositories/person.repository'; 4 | import { PersonEditedEvent } from '../impl'; 5 | 6 | @EventsHandler(PersonEditedEvent) 7 | export class EditPersonEventHandler 8 | implements IEventHandler { 9 | private logger = new Logger('EditEventHandler'); 10 | constructor(private repository: PersonRepository) {} 11 | async handle(event: PersonEditedEvent) { 12 | this.logger.verbose(`EVENT TRIGGERED: ${event.constructor.name}`); 13 | const { _id, data, loggedInUserId } = event; 14 | try { 15 | const result = await this.repository.updateById(_id, data); 16 | return result; 17 | } catch (error) { 18 | this.logger.error(`Failed to update person of id: ${_id}`); 19 | this.logger.log(error.message); 20 | this.logger.debug(error.stack); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/events/handlers/index.ts: -------------------------------------------------------------------------------- 1 | import { AddPersonEventHandler } from './add-person.event.handler'; 2 | import { DeletePersonEventHandler } from './delete-person.event.handler'; 3 | import { EditPersonEventHandler } from './edit-person.event.handler'; 4 | 5 | export const PersonEventHandlers = [ 6 | AddPersonEventHandler, 7 | DeletePersonEventHandler, 8 | EditPersonEventHandler, 9 | ]; 10 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/events/handlers/person.event.handlers.spec.ts: -------------------------------------------------------------------------------- 1 | import { Person } from '../../models/person.model'; 2 | import { EventBus, CqrsModule } from '@nestjs/cqrs'; 3 | import { AddPersonEventHandler } from './add-person.event.handler'; 4 | import { EditPersonEventHandler } from './edit-person.event.handler'; 5 | import { DeletePersonEventHandler } from './delete-person.event.handler'; 6 | import { TestingModule, Test } from '@nestjs/testing'; 7 | import { PersonEventHandlers } from '.'; 8 | import { PersonRepository } from '../../repositories/person.repository'; 9 | import { 10 | PersonAddedEvent, 11 | PersonEditedEvent, 12 | PersonDeletedEvent, 13 | } from '../impl'; 14 | 15 | const mockPersonRepository = () => ({ 16 | create: jest.fn(), 17 | updateById: jest.fn(), 18 | deleteById: jest.fn(), 19 | }); 20 | 21 | const mockUserId = 'mock123'; 22 | 23 | function createMockPersons(): Person[] { 24 | const firstPerson = new Person('org1', { 25 | firstName: 'T', 26 | lastName: 'Alam', 27 | email: 't@example.com', 28 | phoneNumber: 12345, 29 | }); 30 | 31 | const secondPerson = new Person('org2', { 32 | firstName: 'J', 33 | lastName: 'Grosch', 34 | email: 'j@example.com', 35 | phoneNumber: 12345, 36 | }); 37 | 38 | const arrayOfPersons: Person[] = []; 39 | arrayOfPersons.push(firstPerson); 40 | arrayOfPersons.push(secondPerson); 41 | 42 | return arrayOfPersons; 43 | } 44 | 45 | describe('PersonEventHandlers', () => { 46 | const mockPersons: Person[] = createMockPersons(); 47 | let eventBus: EventBus; 48 | let repo; 49 | 50 | let addPersonEventHandler: AddPersonEventHandler; 51 | let editPersonEventHandler: EditPersonEventHandler; 52 | let deletePersonEventHandler: DeletePersonEventHandler; 53 | 54 | beforeEach(async () => { 55 | const module: TestingModule = await Test.createTestingModule({ 56 | imports: [CqrsModule], 57 | providers: [ 58 | ...PersonEventHandlers, 59 | { 60 | provide: PersonRepository, 61 | useFactory: mockPersonRepository, 62 | }, 63 | ], 64 | }).compile(); 65 | eventBus = module.get(EventBus); 66 | repo = module.get(PersonRepository); 67 | 68 | addPersonEventHandler = module.get( 69 | AddPersonEventHandler, 70 | ); 71 | editPersonEventHandler = module.get( 72 | EditPersonEventHandler, 73 | ); 74 | deletePersonEventHandler = module.get( 75 | DeletePersonEventHandler, 76 | ); 77 | }); 78 | 79 | describe(' for AddPersonEventHandler', () => { 80 | it('should successfully return an array of persons', async () => { 81 | repo.create.mockResolvedValue(null); 82 | eventBus.register([AddPersonEventHandler]); 83 | expect(repo.create).not.toHaveBeenCalled(); 84 | await eventBus.publish( 85 | new PersonAddedEvent(mockPersons[0]._id, mockPersons[0], mockUserId), 86 | ); 87 | expect(repo.create).toHaveBeenCalledWith(mockPersons[0]); 88 | }); 89 | 90 | it('should log the error if repo.create() fails', async () => { 91 | const logger = (addPersonEventHandler as any).logger; 92 | repo.create.mockReturnValue( 93 | Promise.reject(new Error('This is an auto generated error')), 94 | ); 95 | eventBus.register([AddPersonEventHandler]); 96 | jest.spyOn(logger, 'error'); 97 | expect(repo.create).not.toHaveBeenCalled(); 98 | await eventBus.publish( 99 | new PersonAddedEvent(mockPersons[0]._id, mockPersons[0], mockUserId), 100 | ); 101 | expect(repo.create).toHaveBeenCalledWith(mockPersons[0]); 102 | expect(logger.error).toHaveBeenCalledTimes(1); 103 | }); 104 | }); 105 | 106 | describe(' for EditOrganizationEventHandler', () => { 107 | it('should successfully repo.updatedById() without any errors', async () => { 108 | repo.updateById.mockResolvedValue(null); 109 | eventBus.register([EditPersonEventHandler]); 110 | expect(repo.updateById).not.toHaveBeenCalled(); 111 | await eventBus.publish( 112 | new PersonEditedEvent(mockPersons[0]._id, mockPersons[0], mockUserId), 113 | ); 114 | expect(repo.updateById).toHaveBeenCalledWith( 115 | mockPersons[0]._id, 116 | mockPersons[0], 117 | ); 118 | }); 119 | 120 | it('should log the error if repo.updateById() fails', async () => { 121 | const logger = (editPersonEventHandler as any).logger; 122 | repo.updateById.mockReturnValue( 123 | Promise.reject(new Error('This is an auto generated error')), 124 | ); 125 | eventBus.register([EditPersonEventHandler]); 126 | jest.spyOn(logger, 'error'); 127 | expect(repo.updateById).not.toHaveBeenCalled(); 128 | await eventBus.publish( 129 | new PersonEditedEvent(mockPersons[0]._id, mockPersons[0], mockUserId), 130 | ); 131 | expect(repo.updateById).toHaveBeenCalledWith( 132 | mockPersons[0]._id, 133 | mockPersons[0], 134 | ); 135 | expect(logger.error).toHaveBeenCalledTimes(1); 136 | }); 137 | }); 138 | 139 | describe(' for DeleteOrganizationEventHandler', () => { 140 | it('should successfully repo.deleteById() without any errors', async () => { 141 | repo.updateById.mockResolvedValue(null); 142 | eventBus.register([DeletePersonEventHandler]); 143 | expect(repo.deleteById).not.toHaveBeenCalled(); 144 | await eventBus.publish( 145 | new PersonDeletedEvent(mockPersons[0]._id, mockUserId), 146 | ); 147 | expect(repo.deleteById).toHaveBeenCalledWith(mockPersons[0]._id); 148 | }); 149 | 150 | it('should log the error if repo.deleteById() fails', async () => { 151 | const logger = (deletePersonEventHandler as any).logger; 152 | repo.deleteById.mockReturnValue( 153 | Promise.reject(new Error('This is an auto generated error')), 154 | ); 155 | eventBus.register([DeletePersonEventHandler]); 156 | jest.spyOn(logger, 'error'); 157 | expect(repo.deleteById).not.toHaveBeenCalled(); 158 | await eventBus.publish( 159 | new PersonDeletedEvent(mockPersons[0]._id, mockUserId), 160 | ); 161 | expect(repo.deleteById).toHaveBeenCalledWith(mockPersons[0]._id); 162 | expect(logger.error).toHaveBeenCalledTimes(1); 163 | }); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/events/impl/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | PersonAddedEvent, 3 | PersonEditedEvent, 4 | PersonDeletedEvent, 5 | } from '@daypaio/domain-events'; 6 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/events/instantiators/index.ts: -------------------------------------------------------------------------------- 1 | export { PersonEventInstantiators } from '@daypaio/domain-events'; 2 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/models/person.aggregate.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from '@nestjs/cqrs'; 2 | import { IPerson } from './person.model.interface'; 3 | import { IsDefined, IsEmail, IsString, IsNumberString } from 'class-validator'; 4 | import { 5 | PersonAddedEvent, 6 | PersonEditedEvent, 7 | PersonDeletedEvent, 8 | } from '../events/impl'; 9 | 10 | export class PersonAggregate extends AggregateRoot implements IPerson { 11 | constructor(id: string, person?: any) { 12 | super(); 13 | this._id = id; 14 | if (person) { 15 | this.email = person.email ? person.email : undefined; 16 | this.firstName = person.firstName ? person.firstName : undefined; 17 | this.lastName = person.lastName ? person.lastName : undefined; 18 | this.phoneNumber = person.phoneNumber ? person.phoneNumber : undefined; 19 | } 20 | } 21 | 22 | @IsDefined() 23 | _id: string; 24 | 25 | @IsEmail() 26 | email: string; 27 | 28 | @IsString() 29 | firstName: string; 30 | 31 | @IsString() 32 | lastName: string; 33 | 34 | @IsNumberString() 35 | phoneNumber: number; 36 | 37 | add(loggedInUserId: string) { 38 | this.apply(new PersonAddedEvent(this._id, this, loggedInUserId)); 39 | } 40 | 41 | edit(loggedInUserId: string) { 42 | this.apply(new PersonEditedEvent(this._id, this, loggedInUserId)); 43 | } 44 | 45 | delete(loggedInUserId: string) { 46 | this.apply(new PersonDeletedEvent(this._id, loggedInUserId)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/models/person.model.interface.ts: -------------------------------------------------------------------------------- 1 | export { IPerson } from '@daypaio/domain-events'; 2 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/models/person.model.ts: -------------------------------------------------------------------------------- 1 | import { prop, Typegoose } from 'typegoose'; 2 | import { ApiModelProperty } from '@nestjs/swagger'; 3 | import { IPerson } from './person.model.interface'; 4 | 5 | export class Person extends Typegoose implements IPerson { 6 | constructor(id: string, person?: any) { 7 | super(); 8 | this._id = id; 9 | if (person) { 10 | this.email = person.email ? person.email : undefined; 11 | this.firstName = person.firstName ? person.firstName : undefined; 12 | this.lastName = person.lastName ? person.lastName : undefined; 13 | this.phoneNumber = person.phoneNumber ? person.phoneNumber : undefined; 14 | } 15 | } 16 | 17 | @ApiModelProperty({ 18 | description: 'The ID of the person', 19 | required: true, 20 | }) 21 | @prop({ required: true, unique: true }) 22 | _id: string; 23 | 24 | @ApiModelProperty({ 25 | description: 'The email of the person', 26 | required: true, 27 | }) 28 | @prop({ required: true, unique: true }) 29 | email: string; 30 | 31 | @ApiModelProperty({ 32 | description: 'The first name of the person', 33 | required: true, 34 | }) 35 | @prop({ required: true }) 36 | firstName: string; 37 | 38 | @ApiModelProperty({ 39 | description: 'The last name of the person', 40 | required: true, 41 | }) 42 | @prop({ required: true }) 43 | lastName: string; 44 | 45 | @ApiModelProperty({ 46 | description: 'The phone number of the person', 47 | required: false, 48 | }) 49 | @prop({ required: false }) 50 | phoneNumber: number; 51 | } 52 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/persons.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PersonsController } from './controllers/persons.controller'; 3 | import { PersonsService } from './services/persons.service'; 4 | import { PersonRepository } from './repositories/person.repository'; 5 | import { PersonCommandHandlers } from './commands/handlers'; 6 | import { PersonQueryHandlers } from './queries/handlers'; 7 | import { PersonEventHandlers } from './events/handlers'; 8 | @Module({ 9 | controllers: [PersonsController], 10 | providers: [ 11 | PersonsService, 12 | PersonRepository, 13 | ...PersonQueryHandlers, 14 | ...PersonCommandHandlers, 15 | ...PersonEventHandlers, 16 | ], 17 | }) 18 | export class PersonsModule { } 19 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/queries/handlers/browse-person.query.handler.ts: -------------------------------------------------------------------------------- 1 | import { QueryHandler, IQueryHandler } from '@nestjs/cqrs'; 2 | import { Logger } from '@nestjs/common'; 3 | import { BrowsePersonQuery } from '../impl/browse-person.query'; 4 | import { Person } from '../../models/person.model'; 5 | import { PersonRepository } from '../../repositories/person.repository'; 6 | 7 | @QueryHandler(BrowsePersonQuery) 8 | export class BrowsePersonHandler implements IQueryHandler { 9 | private logger: Logger; 10 | constructor(private repository: PersonRepository) { 11 | this.logger = new Logger('BrowseQueryHandler'); 12 | } 13 | 14 | async execute(): Promise { 15 | this.logger.log('Async BrowseHandler...'); 16 | try { 17 | const result = await this.repository.find(); 18 | return result; 19 | } catch (error) { 20 | this.logger.error('Failed to browse on Person'); 21 | this.logger.log(error.message); 22 | this.logger.debug(error.stack); 23 | throw error; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/queries/handlers/index.ts: -------------------------------------------------------------------------------- 1 | import { ReadPersonHandler } from './read-person.query.handler'; 2 | import { BrowsePersonHandler } from './browse-person.query.handler'; 3 | 4 | export const PersonQueryHandlers = [ReadPersonHandler, BrowsePersonHandler]; 5 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/queries/handlers/read-person.query.handler.ts: -------------------------------------------------------------------------------- 1 | import { QueryHandler, IQueryHandler } from '@nestjs/cqrs'; 2 | import { ReadPersonQuery } from '../impl/read-person.query'; 3 | import { Logger } from '@nestjs/common'; 4 | import { PersonRepository } from '../../repositories/person.repository'; 5 | 6 | @QueryHandler(ReadPersonQuery) 7 | export class ReadPersonHandler implements IQueryHandler { 8 | private logger: Logger; 9 | constructor(private repository: PersonRepository) { 10 | this.logger = new Logger('ReadQueryHandler'); 11 | } 12 | 13 | async execute(query: ReadPersonQuery) { 14 | this.logger.log('Async ReadHandler...'); 15 | 16 | const { id } = query; 17 | try { 18 | return await this.repository.findById(id); 19 | } catch (error) { 20 | this.logger.error(`Failed to read person of id ${id}`); 21 | this.logger.log(error.message); 22 | this.logger.debug(error.stack); 23 | throw error; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/queries/impl/browse-person.query.ts: -------------------------------------------------------------------------------- 1 | import { IQuery } from '@nestjs/cqrs'; 2 | export class BrowsePersonQuery implements IQuery { 3 | constructor(public user: string) {} 4 | } 5 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/queries/impl/read-person.query.ts: -------------------------------------------------------------------------------- 1 | import { IQuery } from '@nestjs/cqrs'; 2 | 3 | export class ReadPersonQuery implements IQuery { 4 | constructor(public readonly id: string, public loggedInUserId: string) {} 5 | } 6 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/repositories/person.repository.ts: -------------------------------------------------------------------------------- 1 | import { Person } from '../models/person.model'; 2 | import { InjectModel } from 'nestjs-typegoose'; 3 | 4 | import { 5 | Logger, 6 | InternalServerErrorException, 7 | NotFoundException, 8 | } from '@nestjs/common'; 9 | import { ModelType } from 'typegoose'; 10 | 11 | export class PersonRepository { 12 | 13 | private logger: Logger; 14 | constructor(@InjectModel(Person) private model: ModelType) { 15 | this.logger = new Logger(`${this.model.modelName}Repository`); 16 | } 17 | 18 | private generateErrorMessage( 19 | error: any, 20 | operation: string, 21 | id?: string, 22 | data?: any, 23 | ) { 24 | const errorMessage = error.message; 25 | const operationMessage = `${ 26 | this.model.modelName 27 | } could not be ${operation.toLowerCase()}ed}`; 28 | const idMessage = id ? `ID: ${id}` : ''; 29 | const dataMessage = data ? JSON.stringify(data) : ''; 30 | return { 31 | error: operationMessage + errorMessage, 32 | data: idMessage + dataMessage, 33 | verbose: `${error.constructor.name} \n 34 | ${operationMessage} \n 35 | ${errorMessage} \n 36 | ${idMessage} \n 37 | ${dataMessage}`, 38 | }; 39 | } 40 | 41 | async create(data: any): Promise { 42 | this.logger.verbose('CREATE'); 43 | console.table(data); 44 | try { 45 | await this.model.create(data); 46 | } catch (error) { 47 | const message = this.generateErrorMessage( 48 | error, 49 | 'create', 50 | data._id, 51 | data, 52 | ); 53 | this.logger.verbose(message.verbose); 54 | throw error; 55 | } 56 | } 57 | 58 | async updateById(id: string, data: any): Promise { 59 | this.logger.verbose('EDIT'); 60 | console.table({ data, _id: id }); 61 | try { 62 | const result = await this.model.updateOne({ _id: id }, { $set: data }); 63 | const { n, nModified } = result; 64 | if (nModified > 0) { 65 | return; 66 | } 67 | if (n < 1) { 68 | throw new NotFoundException(); 69 | } 70 | if (nModified < 1) { 71 | this.logger.verbose( 72 | `Document for ${this.model.modelName} matched but information was the same`, 73 | ); 74 | return; 75 | } 76 | throw new InternalServerErrorException( 77 | `Failed editing model for ${this.model.modelName}: result: ${result}`, 78 | ); 79 | } catch (error) { 80 | const message = this.generateErrorMessage(error, 'updated', id, data); 81 | this.logger.verbose(message.verbose); 82 | throw error; 83 | } 84 | } 85 | 86 | async deleteById(id: string): Promise { 87 | this.logger.verbose('DELETE'); 88 | console.table({ id }); 89 | try { 90 | const result = await this.model.deleteOne({ _id: id }); 91 | const { n, deletedCount } = result; 92 | if (deletedCount > 0) { 93 | return; 94 | } 95 | if (n < 1) { 96 | throw new NotFoundException(); 97 | } 98 | if (deletedCount < 1) { 99 | this.logger.verbose( 100 | `${this.model.modelName} was found with id: ${id} but could not be deleted, with result: ${result} `, 101 | ); 102 | } 103 | throw new InternalServerErrorException(); 104 | } catch (error) { 105 | const message = this.generateErrorMessage(error, 'delete', id); 106 | this.logger.verbose(message.verbose); 107 | throw error; 108 | } 109 | } 110 | 111 | async find(): Promise { 112 | this.logger.verbose('FIND'); 113 | try { 114 | const result = await this.model.find({}, { __v: 0 }); 115 | return result; 116 | } catch (error) { 117 | const message = this.generateErrorMessage(error, 'find'); 118 | this.logger.verbose(message.verbose); 119 | throw error; 120 | } 121 | } 122 | 123 | async findById(id: string): Promise { 124 | this.logger.verbose('FIND BY ID'); 125 | console.table({ id }); 126 | try { 127 | const result = await this.model.findById(id, { __v: 0 }); 128 | if (result == null) { 129 | throw new NotFoundException(); 130 | } 131 | return result; 132 | } catch (error) { 133 | const message = this.generateErrorMessage(error, 'find', id); 134 | this.logger.verbose(message.verbose); 135 | throw error; 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /examples/eventstore.cqrs/persons/services/persons.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { CommandBus, QueryBus, ICommand, IQuery } from '@nestjs/cqrs'; 3 | import { Person } from '../models/person.model'; 4 | import { BrowsePersonQuery } from '../queries/impl/browse-person.query'; 5 | import { ReadPersonQuery } from '../queries/impl/read-person.query'; 6 | import { EditPersonCommand } from '../commands/impl/edit-person.command'; 7 | import { AddPersonCommand } from '../commands/impl/add-person.command'; 8 | import { DeletePersonCommand } from '../commands/impl/delete-person.command'; 9 | import { UpdatePersonDto } from '../dto/update-person.dto'; 10 | import { IPerson } from '../models/person.model.interface'; 11 | 12 | @Injectable() 13 | export class PersonsService { 14 | constructor(private commandBus: CommandBus, private queryBus: QueryBus) { 15 | } 16 | 17 | protected async executeCommand(command: ICommand): Promise { 18 | if (this.commandBus instanceof CommandBus) { 19 | return await this.commandBus.execute(command); 20 | } 21 | } 22 | 23 | protected async executeQuery(query: IQuery): Promise { 24 | if (this.queryBus instanceof QueryBus) { 25 | return await this.queryBus.execute(query); 26 | } 27 | } 28 | 29 | async browse(userId: string): Promise { 30 | return await this.executeQuery(new BrowsePersonQuery(userId)); 31 | } 32 | 33 | async read(id: string, userId: string): Promise { 34 | return await this.executeQuery(new ReadPersonQuery(id, userId)); 35 | } 36 | 37 | async edit( 38 | id: string, 39 | object: UpdatePersonDto, 40 | userId: string, 41 | ): Promise { 42 | return await this.executeCommand( 43 | new EditPersonCommand(id, object as any, userId), 44 | ); 45 | } 46 | 47 | async add(object: IPerson, userId: string): Promise { 48 | return await this.executeCommand(new AddPersonCommand(object, userId)); 49 | } 50 | 51 | async delete(id: string, userId: string): Promise { 52 | return await this.executeCommand(new DeletePersonCommand(id, userId)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-eventstore", 3 | "version": "2.1.0", 4 | "description": "Event Store connector for Nest js", 5 | "author": "Jonas Grosch ", 6 | "license": "MIT", 7 | "readmeFilename": "README.md", 8 | "main": "dist/index.js", 9 | "files": [ 10 | "dist/**/*", 11 | "*.md" 12 | ], 13 | "scripts": { 14 | "start:dev": "tsc -w", 15 | "build": "tsc", 16 | "prepare": "npm run build", 17 | "format": "prettier --write \"src/**/*.ts\"", 18 | "lint": "tslint -p tsconfig.json -c tslint.json", 19 | "lint:fix": "tslint -p tsconfig.json -c tslint.json --fix", 20 | "check-lite": "npm run lint:fix && npm run prepare", 21 | "test": "jest", 22 | "test:watch": "jest --watch", 23 | "test:cov": "jest --coverage", 24 | "test:e2e": "jest --config ./test/jest-e2e.json" 25 | }, 26 | "keywords": [ 27 | "nestjs" 28 | ], 29 | "publishConfig": { 30 | "access": "public" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/daypaio/nestjs-eventstore" 35 | }, 36 | "bugs": "https://github.com/daypaio/nestjs-eventstore/issues", 37 | "peerDependencies": { 38 | "rxjs": "^6.3.3" 39 | }, 40 | "dependencies": { 41 | "@nestjs/common": "^7.6.17", 42 | "@nestjs/core": "^7.6.17", 43 | "@nestjs/cqrs": "^7.0.1", 44 | "node-eventstore-client": "^0.2.18", 45 | "reflect-metadata": "^0.1.13", 46 | "rxjs": "^6.3.3" 47 | }, 48 | "devDependencies": { 49 | "@nestjs/platform-express": "^7.6.17", 50 | "@nestjs/testing": "^7.6.17", 51 | "@types/express": "^4.17.12", 52 | "@types/jest": "^26.0.23", 53 | "@types/node": "^15.9.0", 54 | "@types/supertest": "^2.0.11", 55 | "jest": "^27.0.4", 56 | "prettier": "^2.3.0", 57 | "supertest": "^6.1.3", 58 | "ts-jest": "^27.0.2", 59 | "ts-node": "^10.0.0", 60 | "tsc-watch": "^4.4.0", 61 | "tsconfig-paths": "^3.9.0", 62 | "tslint": "^6.1.3", 63 | "tslint-config-airbnb": "^5.11.2", 64 | "typescript": "^4.3.2" 65 | }, 66 | "jest": { 67 | "moduleFileExtensions": [ 68 | "js", 69 | "json", 70 | "ts" 71 | ], 72 | "rootDir": "src", 73 | "testRegex": ".spec.ts$", 74 | "transform": { 75 | "^.+\\.(t|j)s$": "ts-jest" 76 | }, 77 | "coverageDirectory": "../coverage", 78 | "testEnvironment": "node" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/event-store/event-store.class.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createConnection, 3 | EventStoreNodeConnection, 4 | ConnectionSettings, 5 | TcpEndPoint, 6 | } from 'node-eventstore-client'; 7 | import { Logger } from '@nestjs/common'; 8 | 9 | export class EventStore { 10 | connection: EventStoreNodeConnection; 11 | 12 | isConnected: boolean = false; 13 | retryAttempts: number; 14 | 15 | private logger: Logger = new Logger(this.constructor.name); 16 | 17 | constructor( 18 | private settings: ConnectionSettings, 19 | private endpoint: TcpEndPoint, 20 | ) { 21 | this.retryAttempts = 0; 22 | this.connect(); 23 | } 24 | 25 | async connect() { 26 | this.connection = createConnection(this.settings, this.endpoint); 27 | this.connection.connect(); 28 | this.connection.on('connected', () => { 29 | this.logger.log('Connection to EventStore established!'); 30 | this.retryAttempts = 0; 31 | this.isConnected = true; 32 | }); 33 | this.connection.on('closed', () => { 34 | this.logger.error(`Connection to EventStore closed! reconnecting attempt(${this.retryAttempts})...`); 35 | this.retryAttempts += 1; 36 | this.isConnected = false; 37 | this.connect(); 38 | }); 39 | } 40 | 41 | getConnection(): EventStoreNodeConnection { 42 | return this.connection; 43 | } 44 | 45 | close() { 46 | this.connection.close(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/event-store/event-store.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module, DynamicModule } from '@nestjs/common'; 2 | import { EventStore } from './event-store.class'; 3 | import { ConnectionSettings, TcpEndPoint } from 'node-eventstore-client'; 4 | 5 | export interface EventStoreModuleOptions { 6 | connectionSettings: ConnectionSettings; 7 | endpoint: TcpEndPoint; 8 | } 9 | 10 | export interface EventStoreModuleAsyncOptions { 11 | useFactory: (...args: any[]) => Promise | any; 12 | inject?: any[]; 13 | } 14 | 15 | @Global() 16 | @Module({ 17 | providers: [EventStore], 18 | exports: [EventStore], 19 | }) 20 | export class EventStoreModule { 21 | static forRoot( 22 | settings: ConnectionSettings, 23 | endpoint: TcpEndPoint, 24 | ): DynamicModule { 25 | return { 26 | module: EventStoreModule, 27 | providers: [ 28 | { 29 | provide: EventStore, 30 | useFactory: () => { 31 | return new EventStore(settings, endpoint); 32 | }, 33 | }, 34 | ], 35 | exports: [EventStore], 36 | }; 37 | } 38 | 39 | static forRootAsync(options: EventStoreModuleAsyncOptions): DynamicModule { 40 | return { 41 | module: EventStoreModule, 42 | providers: [ 43 | { 44 | provide: EventStore, 45 | useFactory: async (...args) => { 46 | const { connectionSettings, endpoint } = await options.useFactory( 47 | ...args, 48 | ); 49 | return new EventStore(connectionSettings, endpoint); 50 | }, 51 | inject: options.inject, 52 | }, 53 | ], 54 | exports: [EventStore], 55 | }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/event-store/eventstore-cqrs/event-bus.provider.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleDestroy, Type } from '@nestjs/common'; 2 | import { ModuleRef } from '@nestjs/core'; 3 | import { Observable, Subscription } from 'rxjs'; 4 | import { filter } from 'rxjs/operators'; 5 | import { isFunction } from 'util'; 6 | import { 7 | IEventHandler, 8 | IEvent, 9 | ObservableBus, 10 | CommandBus, 11 | InvalidSagaException, 12 | ISaga, 13 | } from '@nestjs/cqrs'; 14 | import { 15 | SAGA_METADATA, 16 | EVENTS_HANDLER_METADATA, 17 | } from '@nestjs/cqrs/dist/decorators/constants'; 18 | import { EventStoreBus, IEventConstructors } from './event-store.bus'; 19 | import { EventStore } from '../event-store.class'; 20 | import { CqrsOptions } from '@nestjs/cqrs/dist/interfaces/cqrs-options.interface'; 21 | 22 | export enum EventStoreSubscriptionType { 23 | Persistent, 24 | CatchUp, 25 | } 26 | 27 | export type EventStorePersistentSubscription = { 28 | type: EventStoreSubscriptionType.Persistent; 29 | stream: string; 30 | persistentSubscriptionName: string; 31 | }; 32 | 33 | export type EventStoreCatchupSubscription = { 34 | type: EventStoreSubscriptionType.CatchUp; 35 | stream: string; 36 | }; 37 | 38 | export type EventStoreSubscriptionConfig = { 39 | persistentSubscriptionName: string; 40 | }; 41 | 42 | export type EventStoreSubscription = 43 | | EventStorePersistentSubscription 44 | | EventStoreCatchupSubscription; 45 | 46 | export type EventStoreBusConfig = { 47 | subscriptions: EventStoreSubscription[]; 48 | events: IEventConstructors; 49 | }; 50 | 51 | export type EventHandlerType = Type>; 52 | 53 | @Injectable() 54 | export class EventBusProvider extends ObservableBus 55 | implements OnModuleDestroy { 56 | private _publisher: EventStoreBus; 57 | private readonly subscriptions: Subscription[]; 58 | private readonly cqrsOptions: CqrsOptions; 59 | 60 | constructor( 61 | private readonly commandBus: CommandBus, 62 | private readonly moduleRef: ModuleRef, 63 | private readonly eventStore: EventStore, 64 | private config: EventStoreBusConfig, 65 | ) { 66 | super(); 67 | this.subscriptions = []; 68 | this.useDefaultPublisher(); 69 | } 70 | 71 | get publisher(): EventStoreBus { 72 | return this._publisher; 73 | } 74 | 75 | set publisher(_publisher: EventStoreBus) { 76 | this._publisher = _publisher; 77 | } 78 | 79 | onModuleDestroy() { 80 | this.subscriptions.forEach(subscription => subscription.unsubscribe()); 81 | } 82 | 83 | publish(event: T, stream: string) { 84 | this._publisher.publish(event, stream); 85 | } 86 | 87 | publishAll(events: IEvent[]) { 88 | (events || []).forEach(event => this._publisher.publish(event)); 89 | } 90 | 91 | bind(handler: IEventHandler, name: string) { 92 | const stream$ = name ? this.ofEventName(name) : this.subject$; 93 | const subscription = stream$.subscribe(event => handler.handle(event)); 94 | this.subscriptions.push(subscription); 95 | } 96 | 97 | registerSagas(types: Type[] = []) { 98 | const sagas = types 99 | .map((target) => { 100 | const metadata = Reflect.getMetadata(SAGA_METADATA, target) || []; 101 | const instance = this.moduleRef.get(target, { strict: false }); 102 | if (!instance) { 103 | throw new InvalidSagaException(); 104 | } 105 | return metadata.map((key: string) => instance[key]); 106 | }) 107 | .reduce((a, b) => a.concat(b), []); 108 | 109 | sagas.forEach(saga => this.registerSaga(saga)); 110 | } 111 | 112 | register(handlers: EventHandlerType[] = []) { 113 | handlers.forEach(handler => this.registerHandler(handler)); 114 | } 115 | 116 | protected registerHandler(handler: EventHandlerType) { 117 | const instance = this.moduleRef.get(handler, { strict: false }); 118 | if (!instance) { 119 | return; 120 | } 121 | const eventsNames = this.reflectEventsNames(handler); 122 | eventsNames.map(event => 123 | this.bind(instance as IEventHandler, event.name), 124 | ); 125 | } 126 | 127 | protected ofEventName(name: string) { 128 | return this.subject$.pipe( 129 | filter(event => this.getEventName(event) === name), 130 | ); 131 | } 132 | 133 | private getEventName(event): string { 134 | const { constructor } = Object.getPrototypeOf(event); 135 | return constructor.name as string; 136 | } 137 | 138 | protected registerSaga(saga: ISaga) { 139 | if (!isFunction(saga)) { 140 | throw new InvalidSagaException(); 141 | } 142 | const stream$ = saga(this); 143 | if (!(stream$ instanceof Observable)) { 144 | throw new InvalidSagaException(); 145 | } 146 | 147 | const subscription = stream$ 148 | .pipe(filter(e => !!e)) 149 | .subscribe(command => this.commandBus.execute(command)); 150 | 151 | this.subscriptions.push(subscription); 152 | } 153 | 154 | private reflectEventsNames(handler: EventHandlerType): FunctionConstructor[] { 155 | return Reflect.getMetadata(EVENTS_HANDLER_METADATA, handler); 156 | } 157 | 158 | private useDefaultPublisher() { 159 | const pubSub = new EventStoreBus( 160 | this.eventStore, 161 | this.subject$, 162 | this.config, 163 | ); 164 | this._publisher = pubSub; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/event-store/eventstore-cqrs/event-publisher.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AggregateRoot, IEvent } from '@nestjs/cqrs'; 3 | import { EventBusProvider } from './event-bus.provider'; 4 | import { IAggregateEvent } from '../shared/aggregate-event.interface'; 5 | 6 | export interface Constructor { 7 | new(...args: any[]): T; 8 | } 9 | 10 | @Injectable() 11 | export class EventPublisher { 12 | constructor(private readonly eventBus: EventBusProvider) { } 13 | 14 | mergeClassContext>(metatype: T): T { 15 | const eventBus = this.eventBus; 16 | return class extends metatype { 17 | publish(event: IEvent) { 18 | eventBus.publish(event, (event as IAggregateEvent).streamName); 19 | } 20 | }; 21 | } 22 | 23 | mergeObjectContext(object: T): T { 24 | const eventBus = this.eventBus; 25 | object.publish = (event: IEvent) => { 26 | eventBus.publish(event, (event as IAggregateEvent).streamName); 27 | }; 28 | return object; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/event-store/eventstore-cqrs/event-store.bus.ts: -------------------------------------------------------------------------------- 1 | import { IEvent, Constructor } from '@nestjs/cqrs'; 2 | import { Subject } from 'rxjs'; 3 | import { 4 | EventData, 5 | createEventData, 6 | EventStorePersistentSubscription, 7 | ResolvedEvent, 8 | EventStoreCatchUpSubscription, 9 | PersistentSubscriptionSettings, 10 | } from 'node-eventstore-client'; 11 | import { v4 } from 'uuid'; 12 | import { Logger } from '@nestjs/common'; 13 | import { EventStore } from '../event-store.class'; 14 | import { 15 | EventStoreBusConfig, 16 | EventStoreSubscriptionType, 17 | EventStorePersistentSubscription as ESPersistentSubscription, 18 | EventStoreCatchupSubscription as ESCatchUpSubscription, 19 | } from './event-bus.provider'; 20 | 21 | export interface IEventConstructors { 22 | [key: string]: Constructor; 23 | } 24 | 25 | interface ExtendedCatchUpSubscription extends EventStoreCatchUpSubscription { 26 | isLive: boolean | undefined; 27 | } 28 | 29 | interface ExtendedPersistentSubscription 30 | extends EventStorePersistentSubscription { 31 | isLive: boolean | undefined; 32 | } 33 | 34 | export class EventStoreBus { 35 | private eventHandlers: IEventConstructors; 36 | private logger = new Logger('EventStoreBus'); 37 | private catchupSubscriptions: ExtendedCatchUpSubscription[] = []; 38 | private catchupSubscriptionsCount: number; 39 | 40 | private persistentSubscriptions: ExtendedPersistentSubscription[] = []; 41 | private persistentSubscriptionsCount: number; 42 | 43 | constructor( 44 | private eventStore: EventStore, 45 | private subject$: Subject, 46 | config: EventStoreBusConfig, 47 | ) { 48 | this.addEventHandlers(config.events); 49 | 50 | const catchupSubscriptions = config.subscriptions.filter((sub) => { 51 | return sub.type === EventStoreSubscriptionType.CatchUp; 52 | }); 53 | 54 | const persistentSubscriptions = config.subscriptions.filter((sub) => { 55 | return sub.type === EventStoreSubscriptionType.Persistent; 56 | }); 57 | 58 | this.subscribeToCatchUpSubscriptions( 59 | catchupSubscriptions as ESCatchUpSubscription[], 60 | ); 61 | 62 | this.subscribeToPersistentSubscriptions( 63 | persistentSubscriptions as ESPersistentSubscription[], 64 | ); 65 | } 66 | 67 | async subscribeToPersistentSubscriptions( 68 | subscriptions: ESPersistentSubscription[], 69 | ) { 70 | this.persistentSubscriptionsCount = subscriptions.length; 71 | 72 | await this.createMissingPersistentSubscriptions(subscriptions); 73 | 74 | this.persistentSubscriptions = await Promise.all( 75 | subscriptions.map(async (subscription) => { 76 | return await this.subscribeToPersistentSubscription( 77 | subscription.stream, 78 | subscription.persistentSubscriptionName, 79 | ); 80 | }), 81 | ); 82 | } 83 | 84 | async createMissingPersistentSubscriptions( 85 | subscriptions: ESPersistentSubscription[], 86 | ) { 87 | const settings: PersistentSubscriptionSettings = PersistentSubscriptionSettings.create(); 88 | settings['resolveLinkTos'] = true; 89 | 90 | try { 91 | await Promise.all( 92 | subscriptions.map(async (subscription) => { 93 | return this.eventStore.getConnection().createPersistentSubscription( 94 | subscription.stream, 95 | subscription.persistentSubscriptionName, 96 | settings, 97 | ) 98 | .then(() => this.logger.log( 99 | `Created persistent subscription - 100 | ${subscription.persistentSubscriptionName}:${subscription.stream}`, 101 | )) 102 | .catch(() => {}); 103 | }), 104 | ); 105 | } catch (error) { 106 | this.logger.error(error); 107 | } 108 | 109 | } 110 | 111 | subscribeToCatchUpSubscriptions(subscriptions: ESCatchUpSubscription[]) { 112 | this.catchupSubscriptionsCount = subscriptions.length; 113 | this.catchupSubscriptions = subscriptions.map((subscription) => { 114 | return this.subscribeToCatchupSubscription(subscription.stream); 115 | }); 116 | } 117 | 118 | get allCatchUpSubscriptionsLive(): boolean { 119 | const initialized = 120 | this.catchupSubscriptions.length === this.catchupSubscriptionsCount; 121 | return ( 122 | initialized && 123 | this.catchupSubscriptions.every((subscription) => { 124 | return !!subscription && subscription.isLive; 125 | }) 126 | ); 127 | } 128 | 129 | get allPersistentSubscriptionsLive(): boolean { 130 | const initialized = 131 | this.persistentSubscriptions.length === this.persistentSubscriptionsCount; 132 | return ( 133 | initialized && 134 | this.persistentSubscriptions.every((subscription) => { 135 | return !!subscription && subscription.isLive; 136 | }) 137 | ); 138 | } 139 | 140 | get isLive(): boolean { 141 | return ( 142 | this.allCatchUpSubscriptionsLive && this.allPersistentSubscriptionsLive 143 | ); 144 | } 145 | 146 | async publish(event: IEvent, stream?: string) { 147 | const payload: EventData = createEventData( 148 | v4(), 149 | event.constructor.name, 150 | true, 151 | Buffer.from(JSON.stringify(event)), 152 | ); 153 | 154 | try { 155 | await this.eventStore.getConnection().appendToStream(stream, -2, [payload]); 156 | } catch (err) { 157 | this.logger.error(err.message, err.stack); 158 | } 159 | } 160 | 161 | async publishAll(events: IEvent[], stream?: string) { 162 | try { 163 | await this.eventStore.getConnection().appendToStream(stream, -2, (events || []).map( 164 | (event: IEvent) => createEventData( 165 | v4(), 166 | event.constructor.name, 167 | true, 168 | Buffer.from(JSON.stringify(event)), 169 | ), 170 | )); 171 | } catch (err) { 172 | this.logger.error(err); 173 | } 174 | } 175 | 176 | subscribeToCatchupSubscription(stream: string): ExtendedCatchUpSubscription { 177 | this.logger.log(`Catching up and subscribing to stream ${stream}!`); 178 | try { 179 | return this.eventStore.getConnection().subscribeToStreamFrom( 180 | stream, 181 | 0, 182 | true, 183 | (sub, payload) => this.onEvent(sub, payload), 184 | subscription => 185 | this.onLiveProcessingStarted( 186 | subscription as ExtendedCatchUpSubscription, 187 | ), 188 | (sub, reason, error) => 189 | this.onDropped(sub as ExtendedCatchUpSubscription, reason, error), 190 | ) as ExtendedCatchUpSubscription; 191 | } catch (err) { 192 | this.logger.error(err.message, err.stack); 193 | } 194 | } 195 | 196 | async subscribeToPersistentSubscription( 197 | stream: string, 198 | subscriptionName: string, 199 | ): Promise { 200 | try { 201 | const resolved = (await this.eventStore.getConnection().connectToPersistentSubscription( 202 | stream, 203 | subscriptionName, 204 | (sub, payload) => this.onEvent(sub, payload), 205 | (sub, reason, error) => 206 | this.onDropped(sub as ExtendedPersistentSubscription, reason, error), 207 | )) as ExtendedPersistentSubscription; 208 | this.logger.log(`Connection to persistent subscription ${subscriptionName} on stream ${stream} established!`); 209 | resolved.isLive = true; 210 | return resolved; 211 | } catch (err) { 212 | this.logger.error(`[${stream}][${subscriptionName}] ${err.message}`, err.stack); 213 | this.reSubscribeToPersistentSubscription(stream, subscriptionName); 214 | } 215 | } 216 | 217 | async onEvent( 218 | _subscription: 219 | | EventStorePersistentSubscription 220 | | EventStoreCatchUpSubscription, 221 | payload: ResolvedEvent, 222 | ) { 223 | const { event } = payload; 224 | if ((payload.link !== null && !payload.isResolved) || !event || !event.isJson) { 225 | this.logger.error('Received event that could not be resolved!'); 226 | return; 227 | } 228 | const handler = this.eventHandlers[event.eventType]; 229 | if (!handler) { 230 | this.logger.error('Received event that could not be handled!'); 231 | return; 232 | } 233 | const data = Object.values(JSON.parse(event.data.toString())); 234 | this.subject$.next(new this.eventHandlers[event.eventType](...data)); 235 | } 236 | 237 | onDropped( 238 | subscription: ExtendedPersistentSubscription | ExtendedCatchUpSubscription, 239 | _reason: string, 240 | error: Error, 241 | ) { 242 | subscription.isLive = false; 243 | this.logger.error(error.message, error.stack); 244 | if ((subscription as any)._subscriptionId !== undefined) { 245 | this.reSubscribeToPersistentSubscription( 246 | (subscription as any)._streamId, 247 | (subscription as any)._subscriptionId, 248 | ); 249 | } 250 | } 251 | 252 | reSubscribeToPersistentSubscription( 253 | stream: string, 254 | subscriptionName: string, 255 | ) { 256 | this.logger.warn(`connecting to subscription ${subscriptionName} ${stream}. Retrying...`); 257 | setTimeout( 258 | (stream, subscriptionName) => this.subscribeToPersistentSubscription( 259 | stream, subscriptionName, 260 | ), 261 | 3000, 262 | stream, 263 | subscriptionName, 264 | ); 265 | } 266 | 267 | onLiveProcessingStarted(subscription: ExtendedCatchUpSubscription) { 268 | subscription.isLive = true; 269 | this.logger.log('Live processing of EventStore events started!'); 270 | } 271 | 272 | addEventHandlers(eventHandlers: IEventConstructors) { 273 | this.eventHandlers = { ...this.eventHandlers, ...eventHandlers }; 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/event-store/eventstore-cqrs/eventstore-cqrs.module.ts: -------------------------------------------------------------------------------- 1 | import { CqrsModule, EventBus, CommandBus, QueryBus } from '@nestjs/cqrs'; 2 | import { Global, Module, DynamicModule } from '@nestjs/common'; 3 | import { EventBusProvider, EventStoreBusConfig } from './event-bus.provider'; 4 | import { EventStore } from '../event-store.class'; 5 | import { ExplorerService } from '@nestjs/cqrs/dist/services/explorer.service'; 6 | import { ModuleRef } from '@nestjs/core'; 7 | import { 8 | EventStoreModule, 9 | EventStoreModuleAsyncOptions, 10 | } from '../event-store.module'; 11 | import { EventPublisher } from './event-publisher'; 12 | 13 | @Global() 14 | @Module({}) 15 | export class EventStoreCqrsModule { 16 | constructor( 17 | private readonly explorerService: ExplorerService, 18 | private readonly eventsBus: EventBus, 19 | private readonly commandsBus: CommandBus, 20 | private readonly queryBus: QueryBus, 21 | ) { } 22 | 23 | onModuleInit() { 24 | const { events, queries, sagas, commands } = this.explorerService.explore(); 25 | 26 | this.eventsBus.register(events); 27 | this.commandsBus.register(commands); 28 | this.queryBus.register(queries); 29 | this.eventsBus.registerSagas(sagas); 30 | } 31 | 32 | static forRootAsync( 33 | options: EventStoreModuleAsyncOptions, 34 | eventStoreBusConfig: EventStoreBusConfig, 35 | ): DynamicModule { 36 | return { 37 | module: EventStoreCqrsModule, 38 | imports: [EventStoreModule.forRootAsync(options)], 39 | providers: [ 40 | CommandBus, 41 | QueryBus, 42 | EventPublisher, 43 | ExplorerService, 44 | { 45 | provide: EventBus, 46 | useFactory: (commandBus, moduleRef, eventStore) => { 47 | return new EventBusProvider( 48 | commandBus, 49 | moduleRef, 50 | eventStore, 51 | eventStoreBusConfig, 52 | ); 53 | }, 54 | inject: [CommandBus, ModuleRef, EventStore], 55 | }, 56 | { 57 | provide: EventBusProvider, 58 | useExisting: EventBus, 59 | }, 60 | ], 61 | exports: [ 62 | EventStoreModule, 63 | EventBusProvider, 64 | EventBus, 65 | CommandBus, 66 | QueryBus, 67 | ExplorerService, 68 | EventPublisher, 69 | ], 70 | }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/event-store/eventstore-cqrs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event-bus.provider'; 2 | export * from './event-store.bus'; 3 | export * from './eventstore-cqrs.module'; 4 | export * from './eventstore-cqrs.module'; 5 | export * from './event-publisher'; 6 | -------------------------------------------------------------------------------- /src/event-store/shared/aggregate-event.interface.ts: -------------------------------------------------------------------------------- 1 | import { IEvent } from '@nestjs/cqrs'; 2 | 3 | export interface IAggregateEvent extends IEvent { 4 | streamName: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // export public api from here 2 | // for example: 3 | // export * from './decorators'; 4 | export * from './event-store/event-store.module'; 5 | export * from './event-store/event-store.class'; 6 | export * from './event-store/eventstore-cqrs'; 7 | export * from './event-store/shared/aggregate-event.interface'; 8 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es6", 9 | "sourceMap": false, 10 | "outDir": "./dist", 11 | "rootDir": "./src", 12 | "baseUrl": "./", 13 | "noLib": false 14 | }, 15 | "include": ["src/**/*.ts"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-airbnb" 4 | ], 5 | "rules": { 6 | "variable-name": [ 7 | true, 8 | "check-format", 9 | "allow-leading-underscore", 10 | "allow-pascal-case" 11 | ], 12 | "linebreak-style": [ 13 | true, 14 | "LF" 15 | ] 16 | }, 17 | "rulesDirectory": [] 18 | } --------------------------------------------------------------------------------