node-cqs
Version:
Node-cqs is a node.js module that implements the cqrs pattern without eventsourcing. It can be very useful as domain and eventdenormalizer component if you work with (d)ddd, cqrs, host, etc.
207 lines (168 loc) • 5.83 kB
JavaScript
var eventEmitter = require('../eventEmitter'),
async = require('async'),
_ = require('lodash'),
util = require('util'),
EventEmitter2 = require('eventemitter2').EventEmitter2;
var CommandHandler = function() {
EventEmitter2.call(this, {
wildcard: true,
delimiter: ':',
maxListeners: 1000 // default would be 10!
});
this.buffered = {};
};
util.inherits(CommandHandler, EventEmitter2);
_.extend(CommandHandler.prototype, {
defaultHandle: function(id, cmd, callback) {
var self = this;
async.waterfall([
// load aggregate
function(callback) {
self.loadAggregate(id, callback);
},
// reject command if aggregate has already been destroyed
function(aggregate, vm, callback) {
if(aggregate.get('destroyed')) {
var reason = {
name: 'AggregateDestroyed',
message: 'Aggregate has already been destroyed!',
aggregateRevision: aggregate.get('revision'),
aggregateId: aggregate.id
};
return callback(reason);
}
callback(null, aggregate, vm);
},
// check revision
function(aggregate, vm, callback) {
self.checkRevision(cmd, aggregate.get('revision'), function(err) {
callback(err, aggregate, vm);
});
},
// call validate command
function(aggregate, vm, callback) {
self.validate(cmd.command, cmd.payload, function(err) {
callback(err, aggregate, vm);
});
},
// call command function on aggregate
function(aggregate, vm, callback) {
aggregate[cmd.command](cmd.payload, function(err) {
callback(err, aggregate, vm);
});
},
// commit the new events
function(aggregate, vm, callback) {
vm.set(aggregate.toJSON());
self.commit(vm, function(err) {
callback(err, aggregate, cmd);
});
},
// publish events
function(aggregate, cmd, callback) {
self.publish(aggregate.events, cmd, callback);
}
],
// finally publish commandRejected event on error
function(err) {
if (callback) callback(err);
self.finish(id, cmd, err);
});
},
finish: function(id, cmd, err) {
if (err) {
eventEmitter.emit('commandRejected', cmd, err);
}
eventEmitter.emit('handled:' + cmd.command, id, cmd);
this.emit('handled:' + id + ':' + cmd.id, id, cmd);
},
publish: function(evts, cmd, callback) {
var self = this;
async.concat(evts, function(evt, next) {
evt.commandId = cmd.id;
if (cmd.head) {
evt.head = _.extend(_.clone(cmd.head), evt.head);
}
self.getNewId(function(err, id) {
evt.id = id;
self.publisher.publish(evt);
next(err);
});
},
// final
callback);
},
commit: function(vm, callback) {
var self = this;
callback = callback || function(err) {};
self.repository.commit(vm, callback);
},
validate: function(ruleName, data, callback) {
if(this.validationRules && this.validationRules[ruleName]) {
this.validationRules[ruleName].validate(data, callback);
} else {
callback(null);
}
},
handle: function(id, cmd) {
var self = this;
this.buffered[id] = this.buffered[id] || [];
this.buffered[id].push({ id: id, cmd: cmd });
this.on('handled:' + id + ':' + cmd.id, function(id, cmd) {
self.buffered[id] = _.reject(self.buffered[id], function(entry) {
return entry.id === id && entry.cmd === cmd;
});
if (self.buffered[id].length > 0) {
var nextCmd = self.buffered[id][0];
if (self[nextCmd.cmd.command]) {
self[nextCmd.cmd.command](nextCmd.id, nextCmd.cmd);
} else {
self.defaultHandle(nextCmd.id, nextCmd.cmd);
}
}
});
if (this.buffered[id].length === 1) {
if (this[cmd.command]) {
this[cmd.command](id, cmd);
} else {
this.defaultHandle(id, cmd);
}
}
},
loadAggregate: function(id, callback) {
var aggregate = new this.Aggregate(id);
this.repository.get(id, function(err, vm) {
aggregate.load(vm);
callback(err, aggregate, vm);
});
},
checkRevision: function(cmd, aggRev, callback) {
if(!cmd.head || cmd.head.revision === undefined ||
(cmd.head && cmd.head.revision === aggRev)) {
return callback(null);
}
callback('Concurrency exception. Actual ' +
cmd.head.revision + ' expected ' + aggRev);
},
getNewId: function(callback) {
this.repository.getNewId(callback);
},
configure: function(fn) {
fn.call(this);
return this;
},
use: function(module) {
if (!module) return;
if (module.commit) {
this.repository = module;
}
if (module.publish) {
this.publisher = module;
}
}
});
module.exports = {
extend: function(obj) {
return _.extend(new CommandHandler(), obj);
}
};