├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
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 | }
--------------------------------------------------------------------------------