moleculer-cqrs
TypeScript icon, indicating that this package has built-in type declarations

0.1.0-beta.5 • Public • Published

Moleculer logo

Build Status Coverage Status Code Climate maintainability David Known Vulnerabilities

moleculer-cqrs NPM version

CQRS and Event sourcing module for moleculerjs

Getting started

If you want skip next steps and start playing with moleculer & moleculer-cqrs clone moleculer-cqrs-skeleton repository

Create node project

npx moleculer init project moleculer-cqrs-skeleton

Initialize git repository

git init

Install dependencies

npm install --save moleculer-db moleculer-cqrs

Create domain code (aggregate)

node node_modules/moleculer-cqrs/bin/cqrs-generator.js
  local@notebook~$ cqrs generate
  Aggregate directory: ./aggregates
  Aggregate name: todo
  ? Do you want generate a view model service?  Yes
  Services directory: ./services
  View model name: todo-list

Add aggregate path to jest roots

diff --git a/package.json b/package.json
index 3b31276..c82b69d 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,8 @@ 
     "testEnvironment": "node",
     "rootDir": "./services",
     "roots": [
-      "../test"
+      "../test",
+      "../aggregates"
     ]
   }
 }

Run test

npm run ci

Install Event Sourcing storage adapter

mkdir event-sourcing-storage
mkdir data
touch event-sourcing-storage/index.js
npm install --save resolve-storage-lite
// event-sourcing-storage/index.js
 
const createEsStorage = require("resolve-storage-lite").default;
 
const eventStore = createEsStorage({
  databaseFile: "./data/event-store.sqlite",
});
 
module.exports = eventStore;

Staring up and playing with moleculer services

npm run dev
 
# Moleculer repl 
mol $
 
# Dispatch commands 
call todo.command '{"aggregateId":"uuid-todo-1", "type":"createTodo", "payload":{"title": "Buy Milk"}}'
call todo.command '{"aggregateId":"uuid-todo-2", "type":"createTodo", "payload":{"title": "Buy Eggs"}}'
call todo.command '{"aggregateId":"uuid-todo-3", "type":"createTodo", "payload":{"title": "Buy a new Google Pixel 4 XL"}}'
 
# Query view-model todo-list (MoleculerDb service) 
call todo-list.list
 
# Query read-model materialized aggregate on-the-fly from event-sourcing 
call todo.read-model '{"aggregateId":"uuid-todo-2"}'
 
# Dispatch command 
call todo.command '{"aggregateId":"uuid-todo-2", "type":"deleteTodo", "payload":{"message": "Alredy bought"}}'
 
# Query read-model (state after deleted command) 
call todo.read-model '{"aggregateId":"uuid-todo-2"}'
 
# Query view-model (after delete event dipatched by deleted command) 
call todo-list.list
 
# Call MoleculerDb remove, delete data 
call todo-list.remove '{"id":"uuid-todo-1"}'
call todo-list.remove '{"id":"uuid-todo-3"}'
 
# Query view-model after manually deleted data 
call todo-list.list
 
# Regenerate view-model from saved events 
call todo.replay '{"viewModels":["todo-list"]}'
 
# Query view-model after regeneration from events 
call todo-list.list
 
# Query aggregate event history (with or without payload) 
call todo.history '{"aggregateId":"uuid-todo-2"}'
call todo.history '{"aggregateId":"uuid-todo-2", "payload": true}'
 
# Query read-model (using history events timestamp + 1 millis) 
# Note: add 1 millis to history timestamp because finishTime in not included 
call todo.read-model '{"aggregateId":"uuid-todo-2", "finishTime":1572097057195}'

Aggregate service source code

CQRSEventSourcing service expose four actions but command, read-model and history are avaiable only if the mixin recieved and aggregate as parameter.

Actions:

  • command
    • command action needs aggregateId, type and payload parameters
  • read-model
    • read-model action needs aggregateId parameter and accept finishTime (timestamp) parameter to load only events untill the specificated datetime
  • history
    • history action needs aggregateId parameter and accept payload (boolean) parameter to load payload data as well
  • replay
    • replay action needs viewModels (array of view-model name) parameter

EventSourcingStorage

  • $ npm install --save resolve-storage-lite - Adapter info: SQLite
  • $ npm install --save resolve-storage-mongo - Adapter info: Mongo DB
  • $ npm install --save resolve-storage-mysql - Adapter info: MySQL
  • $ npm install --save resolve-storage-postgresql-serverless - Adapter info: Postgresql serverless
const CQRSEventSourcing = require("moleculer-cqrs");
const EventSourcingStorage = require("../event-sourcing-storage");
const aggregate = require("../aggregates/todo");
 
module.exports = {
  name: "todo",
  mixins: [CQRSEventSourcing({ aggregate })],
  storage: EventSourcingStorage,
  settings: {},
  dependencies: [],
  actions: {},
  events: {},
  methods: {},
  created() {},
  started() {},
  stopped() {},
};

Generate aggregate source code

Getting started with aggregate and service skeleton generated by command line.

$ node node_modules/moleculer-cqrs/bin/mol-cqrs-gen.js

Testing

moleculer-cqrs provide a simple CQRSFixture module that let to test domain logic without any type of service.

CQRSFixture accept an aggregate and provide some methods:

  • givenEvents([...]) initialize aggregate state
  • when(command, payload) execute command
  • expectEvent(event) expect event dispatched from command
  • inspectState(state => expectCode) inspectState where state could be verify
  • whenThrow(command, payload) execute command that throw errors
const { CQRSFixture } = require("moleculer-cqrs");
 
const aggregate = require("..");
 
const {
  commands: { createNews, deleteNews, addComment },
  events: { NewsCreatedEvent, NewsDeletedEvent, AddCommentEvent },
} = aggregate;
 
jest
  .spyOn(global.Date, "now")
  .mockImplementation(() => new Date("2019-10-01T11:01:58.135Z").valueOf());
 
const payload = {
  title: "Test document title",
  userId: "user-id-1",
  text: "Asperiores nam tempora qui et provident temporibus illo et fugit.",
};
 
describe("Testing aggregate commands in isolation", () => {
  test("should commands with empty payload throw error", () => {
    expect(() => createNews({}, {})).toThrow("Aggregate validation error");
  });
 
  test("should createNews command return an NewsCreatedEvent", () => {
    expect(createNews({}, { payload })).toMatchSnapshot();
  });
});
 
describe("Testing  aggregate with cqrs fixture", () => {
  let fixture;
 
  beforeEach(() => {
    fixture = new CQRSFixture(aggregate);
  });
 
  test("should call raw command", () => {
    fixture
      .givenEvents([])
      .when({
        aggregateId: "aggregate-uuid-1",
        aggregateName: "news",
        type: "createNews",
        payload,
      })
      .expectEvent(NewsCreatedEvent({ ...payload, createdAt: Date.now() }));
  });
 
  test("should createNews return an NewsCreatedEvent event", () => {
    fixture
      .givenEvents()
      .when(createNews, payload)
      .expectEvent(NewsCreatedEvent({ ...payload, createdAt: Date.now() }));
  });
 
  test("should reject all next commands when aggregate is already deleted", () => {
    const initialEventStream = [
      NewsCreatedEvent({ ...payload, createdAt: Date.now() }),
      NewsDeletedEvent({ deletedAt: Date.now() }),
    ];
    fixture
      .givenEvents(initialEventStream)
      .whenThrow(deleteNews, {})
      .toThrow("Aggregate is already deleted");
 
    fixture
      .givenEvents(initialEventStream)
      .whenThrow(addComment, {})
      .toThrow("Aggregate is already deleted");
  });
 
  test("should add comments to news", () => {
    const initialEventStream = [
      NewsCreatedEvent({ ...payload, createdAt: Date.now() }),
      AddCommentEvent({
        commentId: "uuid-comment-1",
        text: "Comment text 1",
        author: "author 1",
        createdAt: Date.now(),
      }),
    ];
    fixture
      .givenEvents(initialEventStream)
      .when(addComment, {
        commentId: "uuid-comment-2",
        text: "Comment text 2",
        author: "author 2",
      })
      .expectEvent(
        AddCommentEvent({
          commentId: "uuid-comment-2",
          text: "Comment text 2",
          author: "author 2",
          createdAt: Date.now(),
        })
      )
      .inspectState(state =>
        expect(Object.keys(state.comments).length).toEqual(2)
      );
  });
});

Package Sidebar

Install

npm i moleculer-cqrs

Weekly Downloads

4

Version

0.1.0-beta.5

License

MIT

Unpacked Size

39.4 kB

Total Files

23

Last publish

Collaborators

  • davidnussio