GithubHelp home page GithubHelp logo

juicycleff / nestjs-event-store Goto Github PK

View Code? Open in Web Editor NEW
198.0 8.0 28.0 563 KB

NestJS CQRS module for EventStore.org. It requires @nestjs/cqrs

Home Page: https://xraph.com

License: MIT License

TypeScript 100.00%
cqrs-pattern cqrs event-sourcing event-store event-storage nestjs nest library nats-streaming nats kafka-streams kafka

nestjs-event-store's Introduction

NestJs Event Store

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

NPM Version License Code Size Top Language Top Language

Installation

$ yarn add @juicycleff/nestjs-event-store
$ yarn add node-nats-streaming node-eventstore-client

Description

This module aims to bridge the gap between NestJs and popular event store brokers like Event Store and NATS Streaming with support for kafka coming.

It supports all different subscription strategies in EventStore.Org, such as Volatile, CatchUp and Persistent subscriptions fairly easily. There is support for a storage adapter interface for storing catchup events type last checkpoint position, so the checkpoint can be read on start up; The adapter interface is very slim and easy and can be assigned preferably using the EventStoreModule.registerFeatureAsync method. Adapter data store examples coming soon.

Note: if your event broker type is Event Store then featureStreamName should look like '$ce-user', then you should name your domain argument should be user without $ce, for example.

export class UserCreatedEvent implements IEvent {
    constructor(
        public readonly user: any // This what im talking about.
    )  { }
}

The way this works is we group the event based the first argument in the constructor name and this argument name must be a substring of featureStreamName. I'm sorry you can't pass you your own unique name at the moment, but I will add support for it

It supports all both durable/persistent subscription with shared subscription and volatile. It does not have the limitations of Event Store stated above.

Note: if your event broker type is NATS then featureStreamName should look like 'user'.

Setup from versions from v3.1.15

Setup NATS

Setup root app module for NATS
import { Module } from '@nestjs/common';
import { EventStoreModule } from '@juicycleff/nestjs-event-store';

@Module({
  imports: [
    EventStoreModule.register({
      type: 'nats',
      groupId: 'groupId',
      clusterId: 'clusterId',
      clientId: 'clientId', // Optional (Auto generated with uuid)
      options: {
        url: 'nats://localhost:4222',
        reconnect: true,
        maxReconnectAttempts: -1,
      },
    }),
  ]
})
export class AppModule {}
Setup async root app module
import { Module } from '@nestjs/common';
import { EventStoreModule } from '@juicycleff/nestjs-event-store';
import { EventStoreConfigService } from './eventstore-config.service';

@Module({
  imports: [
    EventStoreModule.registerAsync({
      type: 'nats',
      useClass: EventStoreConfigService
    }),
  ]
})
export class AppModule {}
Setup feature module
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { AccountEventHandlers, UserLoggedInEvent } from '@ultimatebackend/core';
import { AccountSagas } from '../common';
import { EventStoreModule, EventStoreSubscriptionType } from '@juicycleff/nestjs-event-store';

@Module({
  imports: [
    EventStoreModule.registerFeature({
      featureStreamName: 'user',
      type: 'nats',
      subscriptions: [
        {
          type: EventStoreSubscriptionType.Persistent,
          stream: 'account',
          durableName: 'svc-user',
        }
      ],
      eventHandlers: {
        UserLoggedInEvent: (data) => new UserLoggedInEvent(data),
      },
    })
  ],
  providers: [...AccountEventHandlers, AccountSagas],
  controllers: [UsersController],
})
export class UsersModule {}

Setup EventStore

Setup root app module for EventStore
import { Module } from '@nestjs/common';
import { EventStoreModule } from '@juicycleff/nestjs-event-store';

@Module({
  imports: [
    EventStoreModule.register({
      type: 'event-store',
      tcpEndpoint: {
        host: 'localhost',
        port: 1113,
      },
      options: {
        maxRetries: 1000, // Optional
        maxReconnections: 1000,  // Optional
        reconnectionDelay: 1000,  // Optional
        heartbeatInterval: 1000,  // Optional
        heartbeatTimeout: 1000,  // Optional
        defaultUserCredentials: {
          password: 'admin',
          username: 'chnageit',
        },
      },
    }),
  ]
})
export class AppModule {}
Setup async root app module
import { Module } from '@nestjs/common';
import { EventStoreModule } from '@juicycleff/nestjs-event-store';
import { EventStoreConfigService } from './eventstore-config.service';

@Module({
  imports: [
    EventStoreModule.registerAsync({
      type: 'event-store',
      useClass: EventStoreConfigService
    }),
  ]
})
export class AppModule {}
Setup feature module
import { Module } from '@nestjs/common';
import { CommandBus, CqrsModule, EventBus } from '@nestjs/cqrs';
import { EventStoreModule, EventStore, EventStoreSubscriptionType } from '@juicycleff/nestjs-event-store';

import {
  UserCommandHandlers,
  UserLoggedInEvent,
  UserEventHandlers,
  UserQueryHandlers,
} from '../cqrs';
import { UserSagas } from './sagas';
import { MongoStore } from './mongo-eventstore-adapter';

@Module({
  imports: [
    CqrsModule,
    EventStoreModule.registerFeature({
      featureStreamName: '$ce-user',
      type: 'event-store',
      store: MongoStore, // Optional mongo store for persisting catchup events position for microservices to mitigate failures. Must implement IAdapterStore
      subscriptions: [
        {
          type: EventStoreSubscriptionType.CatchUp,
          stream: '$ce-user',
          resolveLinkTos: true, // Default is true (Optional)
          lastCheckpoint: 13, // Default is 0 (Optional)
        },
      ],
      eventHandlers: {
        UserLoggedInEvent: (data) => new UserLoggedInEvent(data),
      },
    }),
  ],
  
  providers: [
    UserSagas,
    ...UserQueryHandlers,
    ...UserCommandHandlers,
    ...UserEventHandlers,
  ],
})
export class UserModule {}

Setup from versions from v3.0.0 to 3.0.5

Setup root app module
import { Module } from '@nestjs/common';
import { EventStoreModule } from '@juicycleff/nestjs-event-store';

@Module({
  imports: [
    EventStoreModule.register({
      tcpEndpoint: {
        host: process.env.ES_TCP_HOSTNAME || AppConfig.eventstore?.hostname,
        port: parseInt(process.env.ES_TCP_PORT, 10) || AppConfig.eventstore?.tcpPort,
      },
      options: {
        maxRetries: 1000, // Optional
        maxReconnections: 1000,  // Optional
        reconnectionDelay: 1000,  // Optional
        heartbeatInterval: 1000,  // Optional
        heartbeatTimeout: 1000,  // Optional
        defaultUserCredentials: {
          password: AppConfig.eventstore?.tcpPassword,
          username: AppConfig.eventstore?.tcpUsername,
        },
      },
    }),
  ]
})
export class AppModule {}
Setup async root app module
import { Module } from '@nestjs/common';
import { EventStoreModule } from '@juicycleff/nestjs-event-store';
import { EventStoreConfigService } from './eventstore-config.service';

@Module({
  imports: [
    EventStoreModule.registerAsync({
      useClass: EventStoreConfigService
    }),
  ]
})
export class AppModule {}

Setup module

Note featureStreamName field is not important if you're subscription type is persistent'

Setup feature module
import { Module } from '@nestjs/common';
import { CommandBus, CqrsModule, EventBus } from '@nestjs/cqrs';
import { EventStoreModule, EventStore, EventStoreSubscriptionType } from '@juicycleff/nestjs-event-store';

import {
  UserCommandHandlers,
  UserCreatedEvent,
  UserEventHandlers,
  UserQueryHandlers,
} from '../cqrs';
import { UserSagas } from './sagas';
import { MongoStore } from './mongo-eventstore-adapter';

@Module({
  imports: [
    CqrsModule,
    EventStoreModule.registerFeature({
      featureStreamName: '$ce-user',
      store: MongoStore, // Optional mongo store for persisting catchup events position for microservices to mitigate failures. Must implement IAdapterStore
      subscriptions: [
        {
          type: EventStoreSubscriptionType.CatchUp,
          stream: '$ce-user',
          resolveLinkTos: true, // Default is true (Optional)
          lastCheckpoint: 13, // Default is 0 (Optional)
        },
        {
          type: EventStoreSubscriptionType.Volatile,
          stream: '$ce-user',
        },
        {
          type: EventStoreSubscriptionType.Persistent,
          stream: '$ce-user',
          persistentSubscriptionName: 'steamName',
          resolveLinkTos: true,  // Default is true (Optional)
        },
      ],
      eventHandlers: {
        UserLoggedInEvent: (data) => new UserLoggedInEvent(data),
        UserRegisteredEvent: (data) => new UserRegisteredEvent(data),
        EmailVerifiedEvent: (data) => new EmailVerifiedEvent(data),
      },
    }),
  ],
  
  providers: [
    UserSagas,
    ...UserQueryHandlers,
    ...UserCommandHandlers,
    ...UserEventHandlers,
  ],
})
export class UserModule {}
Setup async feature module
import { Module } from '@nestjs/common';
import { EventStoreModule } from '@juicycleff/nestjs-event-store';
import { EventStoreFeatureService } from './user-eventstore-feature.service';

@Module({
  imports: [
    EventStoreModule.registerFeatureAsync({
      useClass: EventStoreFeatureService
    }),
  ]
})
export class AppModule {}

Setup from versions below v2.0.0

Setup root app module

import { Module } from '@nestjs/common';
import { NestjsEventStoreModule } from '@juicycleff/nestjs-event-store';

@Module({
  imports: [
    NestjsEventStoreModule.forRoot({
      http: {
        port: parseInt(process.env.ES_HTTP_PORT, 10),
        protocol: process.env.ES_HTTP_PROTOCOL,
      },
      tcp: {
        credentials: {
          password: process.env.ES_TCP_PASSWORD,
          username: process.env.ES_TCP_USERNAME,
        },
        hostname: process.env.ES_TCP_HOSTNAME,
        port: parseInt(process.env.ES_TCP_PORT, 10),
        protocol: process.env.ES_TCP_PROTOCOL,
      },
    }),
  ]
})
export class AppModule {}

Setup module

import { Module } from '@nestjs/common';
import { CommandBus, CqrsModule, EventBus } from '@nestjs/cqrs';
import { NestjsEventStoreModule, EventStore } from '@juicycleff/nestjs-event-store';

import {
  UserCommandHandlers,
  UserCreatedEvent,
  UserEventHandlers,
  UserQueryHandlers,
} from '../cqrs';
import { UserSagas } from './sagas';

@Module({
  imports: [
    CqrsModule,
    NestjsEventStoreModule.forFeature({
      name: 'user',
      resolveLinkTos: false,
    }),
  ],
  
  providers: [
    UserSagas,
    ...UserQueryHandlers,
    ...UserCommandHandlers,
    ...UserEventHandlers,
  ],
})
export class UserModule {
  constructor(
    private readonly command$: CommandBus,
    private readonly event$: EventBus,
    private readonly eventStore: EventStore,
  ) {}

  onModuleInit(): any {
    this.eventStore.setEventHandlers(this.eventHandlers);
    this.eventStore.bridgeEventsTo((this.event$ as any).subject$);
    this.event$.publisher = this.eventStore;
  }

  eventHandlers = {
    UserCreatedEvent: (data) => new UserCreatedEvent(data),
  };
}

Notice

2.0.0 release inspired by nestjs-eventstore

License

This project is MIT licensed.

nestjs-event-store's People

Contributors

0xb4lamx avatar dependabot[bot] avatar juicycleff avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

nestjs-event-store's Issues

EventStore closed Connection

I have a problem, I don't know exactly why, but the client only manages to connect to the EventStore every now and then in a docker-compose. In the client log I always see the following:

[Nest] 39 - 03/04/2020, 1:48:06 PM [NestjsEventStore] EventStore closed! +28745ms [Nest] 39 - 03/04/2020, 1:48:06 PM [EventStore] Connection 'ES-d4ed476d-4d76-45c2-91f0-3f21da4db8da' was closed. +2ms [Nest] 39 - 03/04/2020, 1:48:35 PM [NestjsEventStore] EventStore closed! +28650ms [Nest] 39 - 03/04/2020, 1:49:03 PM [NestjsEventStore] EventStore closed! +28659ms

I use the EventStore in version 5.0.6.0
My guess is it's the heartbeat timeout. Is it possible to change this on the client side?

Multiple Feature Registrations in One App Module

Hi, I'm running into an issue registering the EventStoreModule within each domain module of my application. Events are publishing to Event Store, but regardless of module is publishing the event, the stream name is always the first module that's imported in the app.module.ts.

Users Domain Module:

@Module({
    imports: [
        CqrsModule,
        EventStoreModule.registerFeature({
            type: 'event-store',
            featureStreamName: '$svc-identity-user',
            eventHandlers: {
                UserCreatedEvent: (user: UserCreatedPayload) => new UserCreatedEvent(user),
            },
            subscriptions: []
        }),
        MongooseModule.forFeature([{ name: UserDto.name, schema: UserSchema }], connectionName),
        forwardRef(() => AuthModule),
    ],
    providers: [
        UserResolver,
        ...QueryHandlers,
        ...CommandHandlers,
        UsersService,
    ],
    exports: [
        UsersService,
    ],
})
export class UsersModule { }

Organizations Domain Module

@Module({
  imports: [
    CqrsModule,
    EventStoreModule.registerFeature({
      type: 'event-store',
      featureStreamName: '$svc-identity-organization',
      eventHandlers: {
        OrganizationCreatedEvent: (payload: OrganizationCreatedPayload) => new OrganizationCreatedEvent(payload),
        OrganizationUpdatedEvent: (payload: OrganizationUpdatedPayload) => new OrganizationUpdatedEvent(payload),
      },
      subscriptions: []
    }),
    MongooseModule.forFeature([{ name: OrganizationDto.name, schema: OrganizationSchema }], connectionName),
    UsersModule,
  ],
  providers: [
    OrganizationsResolver,
    ...QueryHandlers,
    ...CommandHandlers,
    OrganizationsService
  ],
  exports: [
    OrganizationsService
  ],
})
export class OrganizationsModule { }

App Module

const modules = [
  UsersModule,
  OrganizationsModule,
  AuthModule,
  PluginsModule,
];

const graphQlConfig = GraphQLModule.forRoot({
  include: [...modules],
  debug: process.env.ENVIRONMENT === 'local',
  playground: process.env.ENVIRONMENT === 'local',
  autoSchemaFile: true,
  context: ({ req }) => ({ req }),
});

@Module({
  imports: [
    MongooseModule.forRoot(connString, {
      connectionName: connectionName,
      dbName: dbName,
      useNewUrlParser: true,
      useUnifiedTopology: true,
      readPreference: ReadPreference.SECONDARY_PREFERRED,
    }),
    EventStoreModule.register({
      type: 'event-store',
      tcpEndpoint: {
        host: process.env.EVENT_STORE_HOSTNAME,
        port: +process.env.EVENT_STORE_PORT,
      },
      options: {
        maxRetries: 10,
        maxReconnections: 10,
        reconnectionDelay: 1000,
        heartbeatInterval: 5000,
        heartbeatTimeout: 1000,
        defaultUserCredentials: {
          password: process.env.EVENT_STORE_PASSWORD,
          username: process.env.EVENT_STORE_USERNAME,
        },
      },
    }),
    ...modules,
    graphQlConfig,
  ],
  providers: [
    AuthStartResolver,
    AuthCompleteResolver,
    GlobalExceptionFilter,
  ],
})
export class AppModule {
  async onModuleInit() { }
}

Regardless of the type of event that occurs, everything is being published to Event Store in the $svc-identity-user stream, but if I were to change the orders of the modules imported within the AppModule so that the OrganizationsModule was imported first then everything would publish to the $svc-identity-organization stream.

I may be misunderstanding the feature registrations and/or eventHandlers within each registration but ideally I'll be able to publish different streams per microservice, and similarly then subscribe to those different streams within my other microservices (as well as register and publish events the same way in those microservices). Any guidance would be greatly appreciated.

Maintained ?

Hi,

Is this still maintained ?

I was wondering if this has been done yet ?

"Adapter data store examples coming soon."

Thanks in advance

serious problem !

hello,

when i use this package i got this error :

[Nest] 22613  - 07/25/2021, 4:34:24 PM     LOG [InstanceLoader] EventStoreModule dependencies initialized +1ms
[Nest] 22613  - 07/25/2021, 4:34:24 PM   ERROR [ExceptionHandler] Nest can't resolve dependencies of the CommandBus (?). Please make sure that the argument ModuleRef at index [0] is available in the CqrsModule context.

Potential solutions:
- If ModuleRef is a provider, is it part of the current CqrsModule?
- If ModuleRef is exported from a separate @Module, is that module imported within CqrsModule?
  @Module({
    imports: [ /* the Module containing ModuleRef */ ]
  })

Error: Nest can't resolve dependencies of the CommandBus (?). Please make sure that the argument ModuleRef at index [0] is available in the CqrsModule context.

Potential solutions:
- If ModuleRef is a provider, is it part of the current CqrsModule?
- If ModuleRef is exported from a separate @Module, is that module imported within CqrsModule?
  @Module({
    imports: [ /* the Module containing ModuleRef */ ]
  })

    at Injector.lookupComponentInParentModules (/home/pedram/my-project/github-samples/nestjs-cqrs-starter/node_modules/@nestjs/core/injector/injector.js:189:19)
    at Injector.resolveComponentInstance (/home/pedram/my-project/github-samples/nestjs-cqrs-starter/node_modules/@nestjs/core/injector/injector.js:145:33)
    at resolveParam (/home/pedram/my-project/github-samples/nestjs-cqrs-starter/node_modules/@nestjs/core/injector/injector.js:99:38)
    at async Promise.all (index 0)
    at Injector.resolveConstructorParams (/home/pedram/my-project/github-samples/nestjs-cqrs-starter/node_modules/@nestjs/core/injector/injector.js:114:27)
    at Injector.loadInstance (/home/pedram/my-project/github-samples/nestjs-cqrs-starter/node_modules/@nestjs/core/injector/injector.js:47:9)
    at Injector.loadProvider (/home/pedram/my-project/github-samples/nestjs-cqrs-starter/node_modules/@nestjs/core/injector/injector.js:69:9)
    at async Promise.all (index 3)
    at InstanceLoader.createInstancesOfProviders (/home/pedram/my-project/github-samples/nestjs-cqrs-starter/node_modules/@nestjs/core/injector/instance-loader.js:44:9)
    at /home/pedram/my-project/github-samples/nestjs-cqrs-starter/node_modules/@nestjs/core/injector/instance-loader.js:29:13

package.json :

{
  "name": "product-service",
  "version": "0.0.1",
  "description": "",
  "author": "",
  "private": true,
  "license": "UNLICENSED",
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "node dist/main",
    "listen": "nest start --watch --config listener.json",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
  },
  "dependencies": {
    "@akanass/nestjsx-crypto": "^2.0.0",
    "@juicycleff/nestjs-event-store": "^3.1.18",
    "@nestjs/common": "^8.0.4",
    "@nestjs/core": "^8.0.4",
    "@nestjs/cqrs": "^8.0.0",
    "@nestjs/microservices": "^8.0.4",
    "@nestjs/mongoose": "^8.0.0",
    "@nestjs/platform-express": "^8.0.4",
    "amqp-connection-manager": "^3.2.2",
    "amqplib": "^0.8.0",
    "class-validator": "^0.13.1",
    "mongoose": "^5.13.3",
    "nestjs-console": "^6.0.0",
    "node-eventstore-client": "^0.2.18",
    "node-nats-streaming": "^0.3.2",
    "reflect-metadata": "^0.1.13",
    "rimraf": "^3.0.2",
    "rxjs": "^7.2.0"
  },
  "devDependencies": {
    "@nestjs/cli": "^7.6.0",
    "@nestjs/schematics": "^7.3.0",
    "@nestjs/testing": "^7.6.15",
    "@types/express": "^4.17.11",
    "@types/jest": "^26.0.22",
    "@types/node": "^14.14.36",
    "@types/supertest": "^2.0.10",
    "@typescript-eslint/eslint-plugin": "^4.19.0",
    "@typescript-eslint/parser": "^4.19.0",
    "eslint": "^7.22.0",
    "eslint-config-prettier": "^8.1.0",
    "eslint-plugin-prettier": "^3.3.1",
    "jest": "^26.6.3",
    "prettier": "^2.2.1",
    "supertest": "^6.1.3",
    "ts-jest": "^26.5.4",
    "ts-loader": "^8.0.18",
    "ts-node": "^9.1.1",
    "tsconfig-paths": "^3.9.0",
    "typescript": "^4.2.3"
  },
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      "ts"
    ],
    "rootDir": "src",
    "testRegex": ".*\\.spec\\.ts$",
    "transform": {
      "^.+\\.(t|j)s$": "ts-jest"
    },
    "collectCoverageFrom": [
      "**/*.(t|j)s"
    ],
    "coverageDirectory": "../coverage",
    "testEnvironment": "node"
  }
}

my app.module.ts :

import { CqrsModule } from '@nestjs/cqrs';
import { ProductModule } from './product/product.module';
import { Module } from '@nestjs/common';
import { EventStoreModule } from '@juicycleff/nestjs-event-store';
@Module({
  imports: [
    CqrsModule,

    EventStoreModule.register({
      type: 'event-store',
      tcpEndpoint: {
        host: 'localhost',
        port: 1113,
      },
      options: {
        defaultUserCredentials: {
          username: 'admin',
          password: 'changeit',
        },
      },
    }),
    ProductModule,
  ],
})
export class AppModule {}

my product.module.ts :

import { Module } from '@nestjs/common';
import { ProductController } from './controllers/products.controller';
import { ProductRepository } from './repository/product.repository';
import { ProductService } from './services/products.service';
import { ProductsSagas } from './sagas/products.sagas';
import { CommandHandlers } from './commands/handlers';
import { EventHandlers } from './events/handlers';
import { ProductCreatedEvent } from './events/impl/product-created.event';
import { CqrsModule } from '@nestjs/cqrs';
import {
  EventStoreModule,
  EventStoreSubscriptionType,
} from '@juicycleff/nestjs-event-store';


@Module({
  imports: [
    CqrsModule,
    EventStoreModule.registerFeature({
      type: 'event-store',
      featureStreamName: '$svc-product',
      subscriptions: [
        {
          type: EventStoreSubscriptionType.Persistent,
          stream: '$svc-product',
          persistentSubscriptionName: 'product',
        },
      ],
      eventHandlers: {
        ProductCreatedEvent: (data) => new ProductCreatedEvent(data),
      },
    }),
  ],
  controllers: [ProductController],
  providers: [
    ProductService,
    ProductRepository,
    ProductsSagas,
    ...CommandHandlers,
    ...EventHandlers,
  ],
})
export class ProductModule {}

EventHanlers not called

Hello,

I am trying to hook up your module to handle event sourcing in my project, but event handlers are never called. Events are being persisted in the event store, but I never get to the handle method in my cqrs event handlers. Do you use custom event publisher as daypaio does in his command handlers?

Below are snippets from my code:

command handler:

    const userAggregate = this.publisher.mergeObjectContext(
      Object.assign(new AuthUserDto(), userDto)
    );

    userAggregate.createUser();
    userAggregate.commit();

event impl:

export class UserCreatedEvent implements IEvent {
  constructor(
    public readonly userDto: AuthUserDto) {}
}

event handler:

@EventsHandler(UserCreatedEvent)
export class UserCreatedHandler
  implements IEventHandler<UserCreatedEvent> {
  handle(event: UserCreatedEvent) {
    Logger.log(event, 'UserCreatedEvent');
  }
}

es module register:

 EventStoreModule.registerFeature({
       ....
       ....
      eventHandlers: {
        UserCreatedEvent: (data) => new UserCreatedEvent(data),
       ....
      },
    }),

Edit:

  1. I can confirm that normally without event-sourcing module events are being handled correctly - handle method in event handler is being called as expected. This is when using this.publisher.mergeObjectContext() and then commit() on aggregate root.

  2. If I use EventBus directly and publish events from it, they are being handled as well:
    this.eventBus.publish(new UserCreatedEvent(userAggregate));

So the issue has something to do with EventPublisher from @nestjs/cqrs.

make properties of EventStore protected

If I wanted to extend EventStore and modify the publish function for example to include metadata, I can't extend the EventStore class because all the properties are private, can we make them protected?

position of events

Hi,
I can't find an option how to receive the position # of the event, it only passes data in the eventhandlers - when I do I can store the last checkpoint and pass that in the registerFeature subscription options.

Is it possible to get the position number of the events?

Cannot publish to stream with aggregate ID in stream name

I am not able to publish to a stream with the related aggregate ID as part of the stream name. I would like to publish to a stream with the name "widget-223434", where 223434 is the aggregate root ID and widget is the aggregate class. This is handled by @daypaio in the nestjs-eventstore in his EventPublisher implementation.

It seems the only way to set the stream name when publishing is when importing NestjsEventStoreModule with the forFeature method and passing in config with the featureStreamName set. When specifying the stream name here it is constant for the module and there is no way to reference the aggregateID as part of the stream.

Is this use case being considered? Am I misunderstanding how to implement this?

Catchup Checkpoint Not Updating

When using a catchup subscription with a store for persisting checkpoints the store only gets read from on start and never written to. This occurs because _subscription doesn't have a type property.
https://github.com/juicycleff/nestjs-event-store/blob/master/src/lib/event-store.ts#L285

Solution:
Check the constructor name instead of type property.

Currently, this shim works for me.

const _onEvent = Reflect.get(EventStore.prototype, 'onEvent');
Reflect.set(EventStore.prototype, 'onEvent', async function(_sub, event) {
    console.log(_sub.constructor.name === 'EventStoreStreamCatchUpSubscription'); // true
    return _onEvent.call(this, Object.assign(_sub, { type: 'catch' }), event);
});

What is the plan for this Repo

Hi Rex,

I was reviewing your Ultimate backend and it helped connect a number of dots WRT connecting GraphQL, CQRS and EventStore. There are now two nestjs-event-store modules; yours and the one from daypaio.

How are the two different?
Is this a repo for casual use, or do you plan to use it in a production setting.

Thanks, either way.

Examples?

Looks like a great library.

Any physical examples or tutorials?

I noticed some examples in the Readme but physical files are missing.

For example the async register with useclass

Event not handled by event store.

As soon as I implement the "nestjs-event-store" like in your branche "next" in 'ultimate-backend' the handler is not called + the event is not received in the event store

app.module.ts

@Module({
  imports: [
    ProductModule,
    EventStoreModule.register({
      tcpEndpoint: {
        host: 'http://localhost',
        port: 1113,
      },
      options: {
        defaultUserCredentials: {
          username: 'admin'
          password: 'changeit',
',
        },
      },
    }),
  ],
  controllers: [],
  providers: [],
})
export class AppModule { }

product.module.ts

const productEventHandlers = [
  ProductCreatedHandler
];

@Module({
  imports: [
    CqrsModule,
    EventStoreModule.registerFeature({
      subscriptions: [
        {
          type: EventStoreSubscriptionType.CatchUp,
          stream: '$ce-product',
          resolveLinkTos: true, // Default is true (Optional)
          lastCheckpoint: 13, // Default is 0 (Optional)
        },
        {
          type: EventStoreSubscriptionType.Volatile,
          stream: '$ce-product',
        },
        {
          type: EventStoreSubscriptionType.Persistent,
          stream: '$ce-product',
          persistentSubscriptionName: 'steamName',
        },
      ],
      eventHandlers: {
        ProductCreatedEvent: (id, data) => new ProductCreatedEvent(id, data),
      },
    }),
  ],
  providers: [
    ProductService,
    ...commandHandler,
    ...queryHandler,
    ...productEventHandlers,
  ],
  controllers: [],
  exports: [ProductService]
})
export class ProductModule { }

product.handler.command.create.ts

import { CommandHandler, ICommandHandler, EventBus } from "@nestjs/cqrs";

@CommandHandler(CreateProductCommand)
export class CreateProductCommandHandler implements ICommandHandler<CreateProductCommand> {

  constructor(private readonly eventBus: EventBus) { }

  async execute(command: CreateProductCommand) {
    const { dto } = command;
    this.eventBus.publish(new ProductCreatedEvent("1", dto));
    return {};
  }

}

product-created-event.ts

import { IEvent } from '@nestjs/cqrs';

export class ProductCreatedEvent implements IEvent {
    constructor(
        public _id: string,
        public readonly data: any
    )  { }
}

product-created.handler.ts

import { IEventHandler, EventsHandler } from '@nestjs/cqrs';

@EventsHandler(ProductCreatedEvent)
export class ProductCreatedHandler
  implements IEventHandler<ProductCreatedEvent> {
  handle(event: ProductCreatedEvent) {
    Logger.log(event, 'ProductCreatedEvent'); // write here
  }
}

Do I miss something or is this a bug?

Nice job btw!

Security - Axios

# npm audit report

axios  <0.21.1
Severity: high
Server-Side Request Forgery - https://npmjs.com/advisories/1594
No fix available
node_modules/axios
  @juicycleff/nestjs-event-store  *
  Depends on vulnerable versions of axios
  node_modules/@juicycleff/nestjs-event-store

Axios needs updating @juicycleff

ERROR [EventStore] ProtocolError: missing required 'result' | How to fix this error!

I using this lib for storing application events.
Storing process is success but got this error from internal of this lib. How to fix it. I think it throwing from protobufjs

full log:
[Nest] 2912 - 01/25/2023, 4:48:08 PM LOG [NestFactory] Starting Nest application...
[Nest] 2912 - 01/25/2023, 4:48:08 PM LOG [InstanceLoader] AppModule dependencies initialized +25ms
[Nest] 2912 - 01/25/2023, 4:48:08 PM LOG [InstanceLoader] EventStoreModule dependencies initialized +1ms
[Nest] 2912 - 01/25/2023, 4:48:08 PM LOG [InstanceLoader] EventStoreCoreModule dependencies initialized +0ms
[Nest] 2912 - 01/25/2023, 4:48:08 PM LOG [InstanceLoader] EventStoreModule dependencies initialized +0ms
[Nest] 2912 - 01/25/2023, 4:48:08 PM LOG [EventStore]
Connecting to persistent subscription hero on stream $ce-hero!

[Nest] 2912 - 01/25/2023, 4:48:08 PM LOG [InstanceLoader] CqrsModule dependencies initialized +1ms
[Nest] 2912 - 01/25/2023, 4:48:08 PM LOG [InstanceLoader] EventStoreCoreModule dependencies initialized +0ms
[Nest] 2912 - 01/25/2023, 4:48:08 PM LOG [InstanceLoader] HeroesGameModule dependencies initialized +1ms
[Nest] 2912 - 01/25/2023, 4:48:08 PM LOG [RoutesResolver] HeroesGameController {/hero}: +3ms
[Nest] 2912 - 01/25/2023, 4:48:08 PM LOG [RouterExplorer] Mapped {/hero/:id/kill, POST} route +2ms
[Nest] 2912 - 01/25/2023, 4:48:08 PM LOG [RouterExplorer] Mapped {/hero, GET} route +1ms
[Nest] 2912 - 01/25/2023, 4:48:08 PM LOG [NestApplication] Nest application successfully started +5ms
[Nest] 2912 - 01/25/2023, 4:48:09 PM LOG [EventStoreBroker] EventStore connected!
KillDragonCommand...
[Nest] 2912 - 01/25/2023, 4:48:12 PM ERROR [EventStore] ProtocolError: missing required 'result'
{
eventNumber: 'ID52bbzfgIFuAFYaQu4rNNSvw4QxIIxm',
heroId: '1234',
dragonId: 1
}
HeroKilledDragonEvent...
{"eventNumber":"ID52bbzfgIFuAFYaQu4rNNSvw4QxIIxm","heroId":"1234","dragonId":1}
Inside [HeroesGameSagas] Saga
{"eventNumber":"ID52bbzfgIFuAFYaQu4rNNSvw4QxIIxm","heroId":"1234","dragonId":1}
Async DropAncientItemCommand...
[Nest] 2912 - 01/25/2023, 4:48:13 PM ERROR [EventStore] ProtocolError: missing required 'result'
Async HeroFoundItemEvent...
{"eventNumber":"ORw0k9ywhPNSmS9ZYI9gWWYO8RyRqQEl","hero":"1234","itemId":"0"}

registerFeatureAsync Injects Incorrect Eventstore Options

When calling EventStoreModule.registerFeatureAsync() , it injects NEST_EVENTSTORE_OPTION provider instead of NEST_EVENTSTORE_FEATURE_OPTION which ends up crashing Nest injector.

The following shim works for me:

const _registerFeatureAsync = Reflect.get(EventStoreModule, 'registerFeatureAsync');
Reflect.set(EventStoreModule, 'registerFeatureAsync', function () {
    const module = _registerFeatureAsync.apply(null, arguments);
	module.imports[0].providers.find(p => (
		p.provide === ProvidersConstants.EVENT_STORE_STREAM_CONFIG_PROVIDER
    )).inject = [NEST_EVENTSTORE_FEATURE_OPTION];
	return module;
});

How to store events in the ES right?

Hi I use your repository to store events in the ES. My question: I thought that the events in the ES should look like '0@user-user100'. But when I save the event in the ES it looks like '0@user'. How can I make sure that the userId is part of the stream so that I have all events for one user in one stream?

I think it has something to do with the Publish() method?

image

how to access service in IAdapterStore?

How is it possible to write/ read to/from database in the AdapterStore?
I have a service with methods for reading from and writing the my database to read the last checkpoints position, but I don't have access to that in the Module setup, static method

static register(): DynamicModule {
    return {
      module: GamesModule,
      imports: [
        CqrsModule,
        EventStoreModule.registerFeatureAsync({
          type: 'event-store',
          useFactory: async (...args) => {
            return {
              featureStreamName: '$ce-game',
              type: 'event-store',
              subscriptions: [
                {
                  type: EventStoreSubscriptionType.CatchUp,
                  stream: '$ce-game',
                },
              ],
              eventHandlers: EventStoreInstanciators,
              store: {
                storeKey: 'game',
                write: async (key: string, value: number) => {
                      // service method to write to database
                }

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.