├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── example └── example.js ├── index.js ├── lib ├── command.js ├── hub.js ├── observer.js └── speakable.js ├── license ├── package.json └── test ├── evtCmdTest.js └── mocha.opts /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | root = true 3 | 4 | [*.{js,jsx,json}] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | env: 4 | node: true 5 | 6 | globals: 7 | __resourceQuery: false 8 | describe: false 9 | describeSaga: false 10 | describeEvent: false 11 | describeCommand: false 12 | before: false 13 | it: false 14 | xit: false 15 | window : false 16 | beforeEach : false 17 | afterEach : false 18 | after : false 19 | before : false 20 | beforeEachChapter: false 21 | describeScenario: false 22 | describeChapter: false 23 | describeStep: false 24 | document : false 25 | window: false 26 | File : false 27 | FormData: false 28 | QCodeDecoder: false 29 | $: false 30 | L: false 31 | btoa: false 32 | escape: false 33 | angular: false 34 | jQuery: false 35 | 36 | rules: 37 | # ERRORS 38 | no-unused-vars: [2, {vars: all, args: none}] 39 | curly: [2, "multi-line"] 40 | 41 | # WARNINGS 42 | semi-spacing: 1 43 | no-empty: 1 44 | handle-callback-err: 1 45 | eqeqeq: 1 46 | quotes: [1, 'single'] 47 | no-unused-expressions: 1 48 | no-throw-literal: 1 49 | semi: 1 50 | block-scoped-var: 1 51 | no-alert: 1 52 | no-console: 1 53 | new-cap: 1 54 | 55 | # DISABLED 56 | space-after-keywords: 0 57 | dot-notation: 0 58 | consistent-return: 0 59 | brace-style: 0 60 | no-multi-spaces: 0 61 | no-underscore-dangle: 0 62 | key-spacing: 0 63 | comma-spacing: 0 64 | no-shadow: 0 65 | no-mixed-requires: 0 66 | space-infix-ops: 0 67 | strict: 0 68 | camelcase: 0 69 | no-wrap-func: 0 70 | comma-dangle: 0 71 | no-extra-semi: 0 72 | no-use-before-define: [0, "nofunc"] 73 | 74 | # AUTOMATED BY EDITORCONFIG 75 | eol-last: 0 76 | no-trailing-spaces: 0 77 | indent: 0 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | $ cat .gitignore 2 | 3 | # Can ignore specific files 4 | .settings.xml 5 | 6 | # Use wildcards as well 7 | *~ 8 | #*.swp 9 | 10 | # Can also ignore all directories and files in a directory. 11 | ignoreMe 12 | ignoreMe/**/* 13 | 14 | node_modules 15 | node_modules/**/* 16 | 17 | build 18 | build/**/* 19 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example 2 | test -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | - "12" 5 | - "14" 6 | 7 | branches: 8 | only: 9 | - master 10 | 11 | notifications: 12 | email: 13 | - adriano@raiano.ch 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚠️ IMPORTANT NEWS! 📰 2 | 3 | I’ve been dealing with CQRS, event-sourcing and DDD long enough now that I don’t need working with it anymore unfortunately, so at least for now this my formal farewell! 4 | 5 | I want to thank everyone who has contributed in one way or another. 6 | Especially... 7 | 8 | - [Jan](https://github.com/jamuhl), who introduced me to this topic. 9 | - [Dimitar](https://github.com/nanov), one of the last bigger contributors and maintainer. 10 | - My last employer, who gave me the possibility to use all these CQRS modules in a big Cloud-System. 11 | - My family and friends, who very often came up short. 12 | 13 | Finally, I would like to thank [Golo Roden](https://github.com/goloroden), who was there very early at the beginning of my CQRS/ES/DDD journey and is now here again to take over these modules. 14 | 15 | Golo Roden is the founder, CTO and managing director of [the native web](https://www.thenativeweb.io/), a company specializing in native web technologies. Among other things, he also teaches CQRS/ES/DDD etc. and based on his vast knowledge, he brought wolkenkit to life. 16 | [wolkenkit](https://wolkenkit.io) is a CQRS and event-sourcing framework based on Node.js. It empowers you to build and run scalable distributed web and cloud services that process and store streams of domain events. 17 | 18 | With this step, I can focus more on [i18next](https://www.i18next.com), [locize](https://locize.com) and [localistars](https://localistars.com). I'm happy about that. 😊 19 | 20 | So, there is no end, but the start of a new phase for my CQRS modules 😉 21 | 22 | I wish you all good luck on your journey. 23 | 24 | Who knows, maybe we'll meet again in a github issue or PR at [i18next](https://github.com/i18next/i18next) 😉 25 | 26 | 27 | [Adriano Raiano](https://twitter.com/adrirai) 28 | 29 | --- 30 | 31 | # Introduction 32 | 33 | [![travis](https://img.shields.io/travis/adrai/node-evented-command.svg)](https://travis-ci.org/adrai/node-evented-command) [![npm](https://img.shields.io/npm/v/evented-command.svg)](https://npmjs.org/package/evented-command) 34 | 35 | Project goal is to provide a simple command/event handling for evented systems like cqrs. 36 | 37 | # Installation 38 | 39 | npm install evented-command 40 | 41 | # Usage 42 | 43 | var evtCmd = require('evented-command')(); 44 | 45 | ## Define the command structure [optional] 46 | The values describes the path to that property in the command message. 47 | 48 | evtCmd.defineCommand({ 49 | id: 'id', // optional 50 | name: 'name', // optional 51 | context: 'context.name', // optional 52 | aggregate: 'aggregate.name', // optional 53 | aggregateId: 'aggregate.id' // optional 54 | }); 55 | 56 | ## Define the event structure [optional] 57 | The values describes the path to that property in the event message. 58 | 59 | evtCmd.defineEvent({ 60 | correlationId: 'correlationId', // optional 61 | id: 'id', // optional 62 | name: 'name', // optional 63 | context: 'context.name', // optional 64 | aggregate: 'aggregate.name', // optional 65 | aggregateId: 'aggregate.id' // optional 66 | }); 67 | 68 | ## Define the id generator function [optional] 69 | ### you can define a synchronous function 70 | 71 | evtCmd.idGenerator(function() { 72 | var id = require('uuid').v4().toString(); 73 | return id; 74 | }); 75 | 76 | ### or you can define an asynchronous function 77 | 78 | evtCmd.idGenerator(function(callback) { 79 | setTimeout(function() { 80 | var id = require('uuid').v4().toString(); 81 | callback(null, id); 82 | }, 50); 83 | }); 84 | 85 | ## Wire up commands and events 86 | 87 | // pass in events from your bus 88 | bus.on('event', function(data){ 89 | evtCmd.emit('event', data); 90 | }); 91 | 92 | // pass commands to bus 93 | evtCmd.on('command', function(data) { 94 | bus.emit('command', data); 95 | }); 96 | 97 | ## Send commands 98 | 99 | var cmd = new Command({ 100 | // id: 'my onwn command id', // if you don't pass an id it will generate one, when emitting the command... 101 | name: 'changePerson', 102 | payload: { 103 | name: 'my name' 104 | }, 105 | aggregate: { 106 | id: 8, 107 | name: 'jack' 108 | }, 109 | context: { 110 | name: 'hr' 111 | } 112 | }); 113 | 114 | // emit it 115 | cmd.emit(); 116 | 117 | 118 | 119 | // if you want to observe the command pass a callback 120 | cmd.emit(function(evt) { 121 | 122 | }); 123 | 124 | 125 | // if you want to observe the command that generates any events pass an object like this: 126 | cmd.emit({ 127 | 128 | event1: function(evt) { 129 | 130 | }, 131 | 132 | event2: function(evt) { 133 | 134 | } 135 | 136 | }); 137 | 138 | ### Send commands with the speakable api 139 | 140 | evtCmd.send('changePerson') 141 | .for('person') // aggregate name 142 | .instance('8') // aggregate id 143 | .in('hr') // context name 144 | .with({ 145 | // id: 'my onwn command id', // if you don't pass an id it will generate one, when emitting the command... 146 | revision: '12', 147 | payload: { 148 | name: 'jack' 149 | } 150 | }) 151 | .go(function(evt) { 152 | console.log('speakable', evt); 153 | }); 154 | 155 | evtCmd.send('multi') 156 | .for('aggregate') 157 | .instance('instanceId') 158 | .in('context') 159 | .with({ 160 | revision: '43', 161 | payload: 'data2' 162 | }) 163 | .go({ 164 | event1: function(evt) { 165 | console.log('speakable', evt); 166 | }, 167 | event2: function(evt) { 168 | console.log('speakable', evt); 169 | } 170 | }); 171 | 172 | # License 173 | 174 | Copyright (c) 2016 Adriano Raiano 175 | 176 | Permission is hereby granted, free of charge, to any person obtaining a copy 177 | of this software and associated documentation files (the "Software"), to deal 178 | in the Software without restriction, including without limitation the rights 179 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 180 | copies of the Software, and to permit persons to whom the Software is 181 | furnished to do so, subject to the following conditions: 182 | 183 | The above copyright notice and this permission notice shall be included in 184 | all copies or substantial portions of the Software. 185 | 186 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 187 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 188 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 189 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 190 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 191 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 192 | THE SOFTWARE. 193 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | var evtCmd = require('../')(); 2 | 3 | evtCmd.defineCommand({ 4 | id: 'id', // optional 5 | name: 'name', // optional 6 | context: 'context.name', // optional 7 | aggregate: 'aggregate.name', // optional 8 | aggregateId: 'aggregate.id' // optional 9 | }); 10 | 11 | evtCmd.defineEvent({ 12 | correlationId: 'correlationId', // optional 13 | id: 'id', // optional 14 | name: 'name', // optional 15 | context: 'context.name', // optional 16 | aggregate: 'aggregate.name', // optional 17 | aggregateId: 'aggregate.id' // optional 18 | }); 19 | 20 | evtCmd.idGenerator(function(callback) { 21 | setTimeout(function() { 22 | var id = require('uuid').v4().toString(); 23 | callback(null, id); 24 | }, 50); 25 | }); 26 | 27 | evtCmd.idGenerator(function() { 28 | var id = require('uuid').v4().toString(); 29 | return id; 30 | }); 31 | 32 | 33 | evtCmd.on('command', function(cmd) { 34 | if (cmd.name === 'multi') { 35 | evtCmd.emit('event', {name: 'event1', correlationId: cmd.id}); 36 | evtCmd.emit('event', {name: 'event2', correlationId: cmd.id}); 37 | return; 38 | } 39 | 40 | cmd.correlationId = cmd.id; 41 | delete cmd.id; 42 | 43 | evtCmd.emit('event', cmd); 44 | }); 45 | 46 | 47 | 48 | (new evtCmd.Command({ 49 | // id: '12345', 50 | name: 'bla' 51 | })).emit(function(evt) { 52 | console.log(evt); 53 | }); 54 | 55 | 56 | (new evtCmd.Command({ 57 | name: 'multi' 58 | })).emit({ 59 | event1: function(evt) { 60 | console.log(evt); 61 | }, 62 | event2: function(evt) { 63 | console.log(evt); 64 | } 65 | }); 66 | 67 | 68 | setTimeout(function() { 69 | console.log('-------------------'); 70 | 71 | evtCmd.send('command') 72 | .for('aggregate') 73 | .instance('instanceId') 74 | .in('context') 75 | .with({ 76 | revision: '12', 77 | payload: 'data' 78 | }) 79 | .go(function(evt) { 80 | console.log('speakable', evt); 81 | }); 82 | 83 | evtCmd.send('multi') 84 | .for('aggregate') 85 | .instance('instanceId') 86 | .in('context') 87 | .with({ 88 | revision: '43', 89 | payload: 'data2' 90 | }) 91 | .go({ 92 | event1: function(evt) { 93 | console.log('speakable', evt); 94 | }, 95 | event2: function(evt) { 96 | console.log('speakable', evt); 97 | } 98 | }); 99 | }, 500); 100 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Hub = require('./lib/hub'); 4 | 5 | module.exports = function() { 6 | return new Hub(); 7 | }; 8 | -------------------------------------------------------------------------------- /lib/command.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (hub) { 4 | 5 | function Command (data) { 6 | this.data = data; 7 | } 8 | 9 | Command.prototype = { 10 | emit: function(callback) { 11 | hub.sendCommand(this.data, callback); 12 | } 13 | }; 14 | 15 | return Command; 16 | }; -------------------------------------------------------------------------------- /lib/hub.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'), 4 | EventEmitter = require('events').EventEmitter, 5 | _ = require('lodash'), 6 | dotty = require('dotty'), 7 | uuid = require('uuid').v4, 8 | command = require('./command'), 9 | observer = require('./observer'), 10 | speakable = require('./speakable'); 11 | 12 | function Hub(observerTimeout) { 13 | EventEmitter.call(this); 14 | 15 | this.definitions = { 16 | command: { 17 | id: 'id', 18 | name: 'name', 19 | context: 'context.name', 20 | aggregate: 'aggregate.name', 21 | aggregateId: 'aggregate.id' 22 | }, 23 | event: { 24 | correlationId: 'correlationId', 25 | id: 'id', 26 | name: 'name', 27 | context: 'context.name', 28 | aggregate: 'aggregate.name', 29 | aggregateId: 'aggregate.id' 30 | } 31 | }; 32 | 33 | observerTimeout = observerTimeout || 10 * 60 * 1000; 34 | 35 | this.Command = command(this); 36 | 37 | this.observer = observer(observerTimeout); 38 | 39 | this.send = speakable(this).send; 40 | 41 | var self = this; 42 | 43 | this.on('event', function(evt) { 44 | var correlationId = dotty.get(evt, self.definitions.event.correlationId); 45 | 46 | var eventname = dotty.get(evt, self.definitions.event.name); 47 | 48 | var cmdCallback = self.observer.getPendingCommand(correlationId, eventname); 49 | 50 | if (cmdCallback) { 51 | cmdCallback(evt); 52 | } 53 | }); 54 | } 55 | 56 | util.inherits(Hub, EventEmitter); 57 | 58 | _.extend(Hub.prototype, { 59 | 60 | getNewId: function (callback) { 61 | callback(null, uuid().toString()); 62 | }, 63 | 64 | defineCommand: function (definition) { 65 | this.definitions.command = _.defaults(definition, this.definitions.command); 66 | return this; 67 | }, 68 | 69 | defineEvent: function (definition) { 70 | this.definitions.event = _.defaults(definition, this.definitions.event); 71 | return this; 72 | }, 73 | 74 | idGenerator: function (fn) { 75 | if (fn.length === 0) { 76 | fn = _.wrap(fn, function(func, callback) { 77 | callback(null, func()); 78 | }); 79 | } 80 | 81 | this.getNewId = fn; 82 | 83 | return this; 84 | }, 85 | 86 | observe: function (cmd, callback) { 87 | var id = dotty.get(cmd, this.definitions.command.id); 88 | 89 | if (_.isFunction(callback)) { 90 | this.observer.observe(id, callback); 91 | return; 92 | } 93 | 94 | var self = this; 95 | 96 | if (_.isObject(callback)) { 97 | _.each(_.keys(callback), function (eventname) { 98 | self.observer.observe(id, eventname, callback[eventname]); 99 | }); 100 | 101 | return; 102 | } 103 | 104 | this.emit(new Error('Error in command callback! Please pass a function or an object with keys as event name and value as function!')); 105 | }, 106 | 107 | sendCommand: function(cmd, callback) { 108 | var self = this; 109 | 110 | if (!dotty.exists(cmd, this.definitions.command.id)) { 111 | this.getNewId(function (err, id) { 112 | if (err) { 113 | if (callback) callback(err); 114 | self.emit('error', err); 115 | return; 116 | } 117 | 118 | dotty.put(cmd, self.definitions.command.id, id); 119 | 120 | self.observe(cmd, callback); 121 | 122 | self.emit('command', cmd); 123 | }); 124 | 125 | return; 126 | } 127 | 128 | this.observe(cmd, callback); 129 | 130 | this.emit('command', cmd); 131 | } 132 | 133 | }); 134 | 135 | module.exports = Hub; 136 | -------------------------------------------------------------------------------- /lib/observer.js: -------------------------------------------------------------------------------- 1 | module.exports = function (observerTimeout) { 2 | 3 | var commands = {}; 4 | 5 | return { 6 | 7 | observe: function (commandId, eventname, callback) { 8 | if (typeof eventname === 'function') { 9 | callback = eventname; 10 | eventname = ''; 11 | } 12 | commands[commandId + eventname] = callback; 13 | 14 | if (eventname !== '') { 15 | setTimeout(function () { 16 | delete commands[commandId + eventname]; 17 | }, observerTimeout); 18 | } 19 | }, 20 | 21 | getPendingCommand: function(commandId, eventname) { 22 | eventname = eventname || ''; 23 | 24 | var callback = commands[commandId + eventname]; 25 | if (callback) { 26 | delete commands[commandId + eventname]; 27 | return callback; 28 | } 29 | 30 | callback = commands[commandId]; 31 | if (callback) { 32 | delete commands[commandId]; 33 | } 34 | 35 | return callback; 36 | } 37 | 38 | }; 39 | 40 | }; 41 | -------------------------------------------------------------------------------- /lib/speakable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var dotty = require('dotty'); 4 | 5 | module.exports = function(hub) { 6 | 7 | 8 | function Speakable (commandname) { 9 | this.commandname = commandname; 10 | this.data = {}; 11 | } 12 | 13 | Speakable.prototype = { 14 | with: function(data) { 15 | this.data = data; 16 | 17 | return this; 18 | }, 19 | 20 | for: function (aggregatename) { 21 | this.aggregatename = aggregatename; 22 | 23 | return this; 24 | }, 25 | 26 | instance: function (aggregateId) { 27 | this.aggregateId = aggregateId; 28 | 29 | return this; 30 | }, 31 | 32 | in: function (contextname) { 33 | this.contextname = contextname; 34 | 35 | return this; 36 | }, 37 | 38 | go: function (callback) { 39 | dotty.put(this.data, hub.definitions.command.name, this.commandname); 40 | 41 | if (this.aggregatename) { 42 | dotty.put(this.data, hub.definitions.command.aggregate, this.aggregatename); 43 | } 44 | 45 | if (this.aggregateId) { 46 | dotty.put(this.data, hub.definitions.command.aggregateId, this.aggregateId); 47 | } 48 | 49 | if (this.contextname) { 50 | dotty.put(this.data, hub.definitions.command.context, this.contextname); 51 | } 52 | 53 | (new hub.Command(this.data)).emit(callback); 54 | } 55 | }; 56 | 57 | 58 | return { 59 | send: function (commandname) { 60 | return new Speakable(commandname); 61 | } 62 | }; 63 | }; -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Adriano Raiano 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "adrai", 3 | "name": "evented-command", 4 | "version": "1.0.4", 5 | "private": false, 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:adrai/node-evented-command.git" 9 | }, 10 | "description": "Project goal is to provide a simple command/event handling for evented systems like cqrs.", 11 | "keywords": [ 12 | "cqrs", 13 | "evented", 14 | "ddd", 15 | "dddd", 16 | "eventsourcing", 17 | "command", 18 | "observer", 19 | "event" 20 | ], 21 | "main": "./index.js", 22 | "directories": { 23 | "lib": "./lib" 24 | }, 25 | "engines": { 26 | "node": ">= v0.6.5" 27 | }, 28 | "dependencies": { 29 | "dotty": "0.0.2", 30 | "lodash": "4.17.19", 31 | "uuid": "3.0.0" 32 | }, 33 | "devDependencies": { 34 | "expect.js": ">=0.3.1", 35 | "mocha": ">=3.x.x" 36 | }, 37 | "scripts": { 38 | "test": "mocha --exit" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/evtCmdTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'), 2 | _ = require('lodash'), 3 | dotty = require('dotty'), 4 | index = require('../'); 5 | 6 | describe('evented-command', function() { 7 | 8 | var evtCmd; 9 | 10 | describe('creating a new instance', function() { 11 | 12 | it('it should have the correct interface', function() { 13 | 14 | evtCmd = index(); 15 | 16 | expect(evtCmd).to.be.an('object'); 17 | expect(evtCmd.defineCommand).to.be.a('function'); 18 | expect(evtCmd.defineEvent).to.be.a('function'); 19 | expect(evtCmd.idGenerator).to.be.a('function'); 20 | expect(evtCmd.on).to.be.a('function'); 21 | expect(evtCmd.emit).to.be.a('function'); 22 | expect(evtCmd.Command).to.be.a('function'); 23 | 24 | }); 25 | 26 | it('it should have a default id generator function', function(done) { 27 | 28 | evtCmd.getNewId(function(err, id) { 29 | expect(id).to.be.a('string'); 30 | done(); 31 | }); 32 | 33 | }); 34 | 35 | describe('defining the command structure', function() { 36 | 37 | it('it should apply the defaults', function() { 38 | 39 | var defaults = _.cloneDeep(evtCmd.definitions.command); 40 | 41 | evtCmd.defineCommand({ 42 | id: 'commandId', 43 | payload: 'data' 44 | }); 45 | 46 | expect(evtCmd.definitions.command.id).to.eql('commandId'); 47 | expect(defaults.id).not.to.eql(evtCmd.definitions.command.id); 48 | expect(evtCmd.definitions.command.payload).to.eql('data'); 49 | expect(defaults.payload).not.to.eql(evtCmd.definitions.command.payload); 50 | expect(evtCmd.definitions.command.name).to.eql(defaults.name); 51 | expect(evtCmd.definitions.command.aggregate).to.eql(defaults.aggregate); 52 | expect(evtCmd.definitions.command.aggregateId).to.eql(defaults.aggregateId); 53 | expect(evtCmd.definitions.command.context).to.eql(defaults.context); 54 | 55 | }); 56 | 57 | }); 58 | 59 | describe('defining the event structure', function() { 60 | 61 | it('it should apply the defaults', function() { 62 | 63 | var defaults = _.cloneDeep(evtCmd.definitions.event); 64 | 65 | evtCmd.defineEvent({ 66 | id: 'eventId', 67 | payload: 'data' 68 | }); 69 | 70 | expect(evtCmd.definitions.event.id).to.eql('eventId'); 71 | expect(defaults.id).not.to.eql(evtCmd.definitions.event.id); 72 | expect(evtCmd.definitions.event.payload).to.eql('data'); 73 | expect(defaults.payload).not.to.eql(evtCmd.definitions.event.payload); 74 | expect(evtCmd.definitions.event.correlationId).to.eql(defaults.correlationId); 75 | expect(evtCmd.definitions.event.name).to.eql(defaults.name); 76 | expect(evtCmd.definitions.event.aggregate).to.eql(defaults.aggregate); 77 | expect(evtCmd.definitions.event.aggregateId).to.eql(defaults.aggregateId); 78 | expect(evtCmd.definitions.event.context).to.eql(defaults.context); 79 | 80 | }); 81 | 82 | }); 83 | 84 | describe('defining an id generator function', function() { 85 | 86 | beforeEach(function() { 87 | evtCmd.getNewId = null; 88 | }); 89 | 90 | afterEach(function() { 91 | evtCmd = index(); 92 | }); 93 | 94 | describe('in a synchronous way', function() { 95 | 96 | it('it should be transformed internally to an asynchronous way', function(done) { 97 | 98 | evtCmd.idGenerator(function() { 99 | var id = require('uuid').v4().toString(); 100 | return id; 101 | }); 102 | 103 | evtCmd.getNewId(function(err, id) { 104 | expect(id).to.be.a('string'); 105 | done(); 106 | }); 107 | 108 | }); 109 | 110 | }); 111 | 112 | describe('in an synchronous way', function() { 113 | 114 | it('it should be taken as it is', function(done) { 115 | 116 | evtCmd.idGenerator(function(callback) { 117 | setTimeout(function() { 118 | var id = require('uuid').v4().toString(); 119 | callback(null, id); 120 | }, 10); 121 | }); 122 | 123 | evtCmd.getNewId(function(err, id) { 124 | expect(id).to.be.a('string'); 125 | done(); 126 | }); 127 | 128 | }); 129 | 130 | }); 131 | 132 | }); 133 | 134 | describe('executing a command', function() { 135 | 136 | before(function() { 137 | evtCmd = index(); 138 | }); 139 | 140 | describe('via "Command" interface', function() { 141 | 142 | describe('fire and forget', function() { 143 | 144 | it('it should notify correctly', function(done) { 145 | 146 | evtCmd.once('command', function(cmd) { 147 | done(); 148 | }); 149 | 150 | (new evtCmd.Command({ 151 | name: 'changeSomething' 152 | })).emit(); 153 | 154 | }); 155 | 156 | }); 157 | 158 | describe('waiting for an event', function() { 159 | 160 | before(function() { 161 | evtCmd.once('command', function(cmd) { 162 | var evt = { correlationId: cmd.id }; 163 | evtCmd.emit('event', evt); 164 | }); 165 | }); 166 | 167 | it('it should notify correctly', function(done) { 168 | 169 | (new evtCmd.Command({ 170 | name: 'changeSomething' 171 | })).emit(function(evt) { 172 | done(); 173 | }); 174 | 175 | }); 176 | 177 | }); 178 | 179 | describe('waiting for multiple events', function() { 180 | 181 | before(function() { 182 | evtCmd.once('command', function(cmd) { 183 | evtCmd.emit('event', { correlationId: cmd.id, name: 'event1' }); 184 | evtCmd.emit('event', { correlationId: cmd.id, name: 'event2' }); 185 | }); 186 | }); 187 | 188 | it('it should notify correctly', function(done) { 189 | 190 | var finished = 0; 191 | 192 | function check() { 193 | finished++; 194 | if (finished === 2) { 195 | done(); 196 | } 197 | } 198 | 199 | (new evtCmd.Command({ 200 | name: 'changeSomething' 201 | })).emit({ 202 | event1: function(evt) { 203 | check(); 204 | }, 205 | event2: function(evt) { 206 | check(); 207 | } 208 | }); 209 | 210 | }); 211 | 212 | }); 213 | 214 | describe('passing a command id', function() { 215 | 216 | before(function() { 217 | evtCmd.once('command', function(cmd) { 218 | var evt = { correlationId: cmd.id }; 219 | evtCmd.emit('event', evt); 220 | }); 221 | }); 222 | 223 | it('it should use that id command id', function(done) { 224 | 225 | (new evtCmd.Command({ 226 | id: 'my own id!!!', 227 | name: 'changeSomething' 228 | })).emit(function(evt) { 229 | expect(evt.correlationId).to.eql('my own id!!!'); 230 | done(); 231 | }); 232 | 233 | }); 234 | 235 | }); 236 | 237 | describe('not passing a command id', function() { 238 | 239 | before(function() { 240 | evtCmd.once('command', function(cmd) { 241 | var evt = { correlationId: cmd.id }; 242 | evtCmd.emit('event', evt); 243 | }); 244 | }); 245 | 246 | it('it should generate a command id', function(done) { 247 | 248 | (new evtCmd.Command({ 249 | // id: 'no id passed', 250 | name: 'changeSomething' 251 | })).emit(function(evt) { 252 | expect(evt.correlationId).to.be.a('string'); 253 | done(); 254 | }); 255 | 256 | }); 257 | 258 | }); 259 | 260 | }); 261 | 262 | describe('via "speakable" interface', function() { 263 | 264 | describe('fire and forget', function() { 265 | 266 | describe('without data', function() { 267 | 268 | it('it should notify correctly', function(done) { 269 | 270 | evtCmd.once('command', function(cmd) { 271 | done(); 272 | }); 273 | 274 | evtCmd.send('changeSomething') 275 | .go(); 276 | 277 | }); 278 | 279 | }); 280 | 281 | describe('with data', function() { 282 | 283 | it('it should notify correctly', function(done) { 284 | 285 | evtCmd.once('command', function(cmd) { 286 | done(); 287 | }); 288 | 289 | evtCmd.send('changeSomething') 290 | .with({ data: 'hohoho' }) 291 | .go(); 292 | 293 | }); 294 | 295 | }); 296 | 297 | }); 298 | 299 | describe('waiting for an event', function() { 300 | 301 | before(function() { 302 | evtCmd.once('command', function(cmd) { 303 | var evt = { correlationId: cmd.id }; 304 | evtCmd.emit('event', evt); 305 | }); 306 | }); 307 | 308 | it('it should notify correctly', function(done) { 309 | 310 | evtCmd.send('changeSomething') 311 | .with({ data: 'hohoho' }) 312 | .go(function(evt) { 313 | done(); 314 | }); 315 | 316 | }); 317 | 318 | }); 319 | 320 | describe('waiting for multiple events', function() { 321 | 322 | before(function() { 323 | evtCmd.once('command', function(cmd) { 324 | evtCmd.emit('event', { correlationId: cmd.id, name: 'event1' }); 325 | evtCmd.emit('event', { correlationId: cmd.id, name: 'event2' }); 326 | }); 327 | }); 328 | 329 | it('it should notify correctly', function(done) { 330 | 331 | var finished = 0; 332 | 333 | function check() { 334 | finished++; 335 | if (finished === 2) { 336 | done(); 337 | } 338 | } 339 | 340 | evtCmd.send('changeSomething') 341 | .with({ data: 'hohoho' }) 342 | .go({ 343 | event1: function(evt) { 344 | check(); 345 | }, 346 | event2: function(evt) { 347 | check(); 348 | } 349 | }); 350 | }); 351 | 352 | }); 353 | 354 | describe('passing a command id', function() { 355 | 356 | before(function() { 357 | evtCmd.once('command', function(cmd) { 358 | var evt = { correlationId: cmd.id }; 359 | evtCmd.emit('event', evt); 360 | }); 361 | }); 362 | 363 | it('it should use that id command id', function(done) { 364 | 365 | evtCmd.send('changeSomething') 366 | .with({ id: 'my own id!!!' }) 367 | .go(function(evt) { 368 | expect(evt.correlationId).to.eql('my own id!!!'); 369 | done(); 370 | }); 371 | 372 | }); 373 | 374 | }); 375 | 376 | describe('not passing a command id', function() { 377 | 378 | before(function() { 379 | evtCmd.once('command', function(cmd) { 380 | var evt = { correlationId: cmd.id }; 381 | evtCmd.emit('event', evt); 382 | }); 383 | }); 384 | 385 | it('it should generate a command id', function(done) { 386 | 387 | evtCmd.send('changeSomething') 388 | .with({ 389 | // id: 'no id passed', 390 | data: 'hohoho' 391 | }) 392 | .go(function(evt) { 393 | expect(evt.correlationId).to.be.a('string'); 394 | done(); 395 | }); 396 | 397 | }); 398 | 399 | }); 400 | 401 | describe('defining an aggregate name', function() { 402 | 403 | before(function() { 404 | evtCmd.once('command', function(cmd) { 405 | var evt = { correlationId: cmd.id, aggregate: cmd.aggregate }; 406 | evtCmd.emit('event', evt); 407 | }); 408 | }); 409 | 410 | it('it should handle it correctly', function(done) { 411 | 412 | evtCmd.send('changeSomething') 413 | .for('person') 414 | .with({ 415 | data: 'hohoho' 416 | }) 417 | .go(function(evt) { 418 | expect(evt.aggregate.name).to.eql('person'); 419 | done(); 420 | }); 421 | 422 | }); 423 | 424 | }); 425 | 426 | describe('not defining an aggregate name', function() { 427 | 428 | before(function() { 429 | evtCmd.once('command', function(cmd) { 430 | var evt = { correlationId: cmd.id, aggregate: cmd.aggregate }; 431 | evtCmd.emit('event', evt); 432 | }); 433 | }); 434 | 435 | it('it should not handle it', function(done) { 436 | 437 | evtCmd.send('changeSomething') 438 | .with({ 439 | data: 'hohoho' 440 | }) 441 | .go(function(evt) { 442 | expect(dotty.exists(evt, 'aggregate.name')).to.eql(false); 443 | done(); 444 | }); 445 | 446 | }); 447 | 448 | }); 449 | 450 | describe('defining an aggregate id', function() { 451 | 452 | before(function() { 453 | evtCmd.once('command', function(cmd) { 454 | var evt = { correlationId: cmd.id, aggregate: cmd.aggregate }; 455 | evtCmd.emit('event', evt); 456 | }); 457 | }); 458 | 459 | it('it should handle it correctly', function(done) { 460 | 461 | evtCmd.send('changeSomething') 462 | .for('person') 463 | .instance('112233') 464 | .with({ 465 | data: 'hohoho' 466 | }) 467 | .go(function(evt) { 468 | expect(evt.aggregate.id).to.eql('112233'); 469 | done(); 470 | }); 471 | 472 | }); 473 | 474 | }); 475 | 476 | describe('not defining an aggregate id', function() { 477 | 478 | before(function() { 479 | evtCmd.once('command', function(cmd) { 480 | var evt = { correlationId: cmd.id, aggregate: cmd.aggregate }; 481 | evtCmd.emit('event', evt); 482 | }); 483 | }); 484 | 485 | it('it should not handle it', function(done) { 486 | 487 | evtCmd.send('changeSomething') 488 | .with({ 489 | data: 'hohoho' 490 | }) 491 | .go(function(evt) { 492 | expect(dotty.exists(evt, 'aggregate.id')).to.eql(false); 493 | done(); 494 | }); 495 | 496 | }); 497 | 498 | }); 499 | 500 | describe('defining an context name', function() { 501 | 502 | before(function() { 503 | evtCmd.once('command', function(cmd) { 504 | var evt = { correlationId: cmd.id, context: cmd.context }; 505 | evtCmd.emit('event', evt); 506 | }); 507 | }); 508 | 509 | it('it should handle it correctly', function(done) { 510 | 511 | evtCmd.send('changeSomething') 512 | .in('hr') 513 | .with({ 514 | data: 'hohoho' 515 | }) 516 | .go(function(evt) { 517 | expect(evt.context.name).to.eql('hr'); 518 | done(); 519 | }); 520 | 521 | }); 522 | 523 | }); 524 | 525 | describe('not defining an context name', function() { 526 | 527 | before(function() { 528 | evtCmd.once('command', function(cmd) { 529 | var evt = { correlationId: cmd.id, context: cmd.context }; 530 | evtCmd.emit('event', evt); 531 | }); 532 | }); 533 | 534 | it('it should not handle it', function(done) { 535 | 536 | evtCmd.send('changeSomething') 537 | .with({ 538 | data: 'hohoho' 539 | }) 540 | .go(function(evt) { 541 | expect(dotty.exists(evt, 'context.name')).to.eql(false); 542 | done(); 543 | }); 544 | 545 | }); 546 | 547 | }); 548 | 549 | describe('defining all', function() { 550 | 551 | before(function() { 552 | evtCmd.once('command', function(cmd) { 553 | var evt = cmd; 554 | evt.correlationId = cmd.id; 555 | evtCmd.emit('event', evt); 556 | }); 557 | }); 558 | 559 | it('it should handle it correctly', function(done) { 560 | 561 | evtCmd.send('changeSomething') 562 | .with({ 563 | id: 'c1cfe2ba-2f2d-439f-88a2-0ed78c78c827', 564 | data: 'hohoho' 565 | }) 566 | .for('person') 567 | .instance('112233') 568 | .in('hr') 569 | .go(function(evt) { 570 | expect(evt.aggregate.name).to.eql('person'); 571 | expect(evt.aggregate.id).to.eql('112233'); 572 | expect(evt.context.name).to.eql('hr'); 573 | expect(evt.data).to.eql('hohoho'); 574 | expect(evt.correlationId).to.eql('c1cfe2ba-2f2d-439f-88a2-0ed78c78c827'); 575 | done(); 576 | }); 577 | 578 | }); 579 | 580 | }); 581 | 582 | }); 583 | 584 | }); 585 | 586 | }); 587 | 588 | }); 589 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | -R spec --------------------------------------------------------------------------------