Coder Social home page Coder Social logo

node-cqrs's Introduction

node-cqrs

NPM Version Build Status Coverage Status NPM Downloads

Overview

The package provides building blocks for making a CQRS-ES application. It was inspired by Lokad.CQRS, but not tied to a specific storage implementation or infrastructure. It favors ES6 classes and dependency injection, so any components can be modified or replaced with your own implementations without hacks to the package codebase.

Documentation at node-cqrs.org

Your app is expected to operate with loosely typed commands and events that match the following interface:

declare interface IMessage {
    type: string,

    aggregateId?: string|number,
    aggregateVersion?: number,

    sagaId?: string|number,
    sagaVersion?: number,

    payload?: any,
    context?: any
}

Domain business logic should be placed in Aggregate, Saga and Projection classes:

  • Aggregates handle commands and emit events
  • Sagas handle events and enqueue commands
  • Projections listen to events and update views

Message delivery is being handled by the following services (in order of appearance):

  • Command Bus delivers commands to command handlers
  • Aggregate Command Handler restores an aggregate state, executes a command
  • Event Store persists events and deliver them to event handlers (saga event handlers, projections or any other custom services)
  • Saga Event Handler restores saga state and applies event

From a high level, this is how the command/event flow looks like:

Overview

Getting Started

You can find sample code of a User domain in the /examples folder.

Your App → Command → Aggregate

Describe an aggregate that handles a command:

const { AbstractAggregate } = require('node-cqrs');

class UserAggregate extends AbstractAggregate {
  static get handles() {
    return ['createUser'];
  }
  
  createUser(commandPayload) {
    // ...
  }
}

Then register aggregate in the DI container. All the wiring can be done manually, without a DI container (you can find it in samples), but with container it’s just easier:

const { Container, InMemoryEventStorage } = require('node-cqrs');

const container = new Container();
container.register(InMemoryEventStorage, 'storage');
container.registerAggregate(UserAggregate);
container.createUnexposedInstances();

Then send a command:

const userAggregateId = undefined;
const payload = {
  username: 'john',
  password: 'test'
};

container.commandBus.send('createUser', userAggregateId, { payload });

Behind the scene, an AggregateCommandHandler will catch the command, try to load an aggregate event stream and project it to aggregate state, then it will pass the command payload to the createUser handler we’ve defined earlier.

The createUser implementation can look like this:

createUser(commandPayload) {
  const { username, password } = commandPayload;

  this.emit('userCreated', {
    username,
    passwordHash: md5Hash(password)
  });
}  

Once the above method is executed, the emitted userCreated event will be persisted and delivered to event handlers (sagas, projections or any other custom event receptors).

Aggregate → Event → Projection → View

Now it’s time to work on a read model. We’ll need a projection that will handle our events. Projection must implement 2 methods: subscribe(eventStore) and project(event) . To make it easier, you can extend an AbstractProjection:

const { AbstractProjection } = require('node-cqrs');

class UsersProjection extends AbstractProjection {
  static get handles() {
    return ['userCreated'];
  }
  
  userCreated(event) {
    // ...
  }
}

By default, projection uses async InMemoryView for inner view, but we’ll use Map to make it more familiar:

class UsersProjection {  
  get view() {
    return this._view || (this._view = new Map());
  }
}

With Map view, our event handler can look this way:

class UsersProjection {
  userCreated(event) {
    this.view.set(event.aggregateId, {
      username: event.payload.username
    });
  }
}

Once the projection is ready, it can be registered in the DI container:

container.registerProjection(UsersProjection, 'users');

And accessed from anywhere in your app:

container.users
// Map { 1 => { username: 'John' } }

Contribution

Dependencies

License

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.