@orchestrated-io/cqrs-domain
Version:
Node-cqrs-domain is a node.js module based on node-eventstore. It can be very useful as domain component if you work with (d)ddd, cqrs, eventdenormalizer, host, etc.
1,218 lines (1,021 loc) • 39.4 kB
JavaScript
var Definition = require('./definitionBase'),
util = require('util'),
_ = require('lodash'),
debug = require('debug')('domain:defaultCommandHandler'),
dotty = require('dotty'),
async = require('async'),
uuid = require('uuid').v4,
ConcurrencyError = require('./errors/concurrencyError'),
AggregateDestroyedError = require('./errors/aggregateDestroyedError'),
AggregateConcurrencyError = require('./errors/aggregateConcurrencyError'),
EventFromEventStore = require('@orchestrated-io/eventstore/lib/event');
/**
* Returns a random number between passed values of min and max.
* @param {Number} min The minimum value of the resulting random number.
* @param {Number} max The maximum value of the resulting random number.
* @returns {Number}
*/
function randomBetween(min, max) {
return Math.round(min + Math.random() * (max - min));
}
/**
* DefaultCommandHandler constructor
* @param {Object} meta Meta infos like: { name: 'name', version: 1 }
* @constructor
*/
function DefaultCommandHandler (meta) {
Definition.call(this, meta);
this.id = uuid().toString();
this.queue = {};
}
util.inherits(DefaultCommandHandler, Definition);
_.extend(DefaultCommandHandler.prototype, {
/**
* Injects the needed aggregate.
* @param {Aggregate} aggregate The aggregate object to inject.
*/
useAggregate: function (aggregate) {
if (!aggregate || !_.isObject(aggregate)) {
var err = new Error('Please pass a valid aggregate!');
debug(err);
throw err;
}
this.aggregate = aggregate;
},
/**
* Injects the needed eventStore.
* @param {Object} eventStore The eventStore object to inject.
*/
useEventStore: function (eventStore) {
if (!eventStore || !_.isObject(eventStore)) {
var err = new Error('Please pass a valid eventStore!');
debug(err);
throw err;
}
this.eventStore = eventStore;
},
/**
* Injects the needed aggregateLock.
* @param {Object} aggregateLock The aggregateLock object to inject.
*/
useAggregateLock: function (aggregateLock) {
if (!aggregateLock || !_.isObject(aggregateLock)) {
var err = new Error('Please pass a valid aggregateLock!');
debug(err);
throw err;
}
this.aggregateLock = aggregateLock;
},
/**
* Queues the passed command and its callback.
* @param {String} aggId The passed aggregate id.
* @param {Object} cmd The command to be queued.
* @param {Function} clb The callback of this command.
*/
queueCommand: function (aggId, cmd, clb) {
if (!aggId || !_.isString(aggId)) {
var err = new Error('Please pass a valid aggregate id!');
debug(err);
throw err;
}
if (!cmd || !_.isObject(cmd)) {
var err = new Error('Please pass a valid command!');
debug(err);
throw err;
}
if (!clb || !_.isFunction(clb)) {
var err = new Error('Please pass a valid callback!');
debug(err);
throw err;
}
this.queue[aggId] = this.queue[aggId] || [];
this.queue[aggId].push({ command: cmd, callback: clb })
},
/**
* Returns next command in the queue
* @param {String} aggId The passed aggregate id.
* @returns {Object}
*/
getNextCommandInQueue: function (aggId) {
if (!aggId || !_.isString(aggId)) {
var err = new Error('Please pass a valid aggregate id!');
debug(err);
throw err;
}
if (this.queue[aggId] && this.queue[aggId].length > 0) {
var nextCmd = this.queue[aggId][0];
return nextCmd;
}
return null;
},
/**
* Removes the passed command from the queue.
* @param {String} aggId The passed aggregate id.
* @param {Object} cmd The command to be queued.
*/
removeCommandFromQueue: function (aggId, cmd) {
if (!aggId || !_.isString(aggId)) {
var err = new Error('Please pass a valid aggregate id!');
debug(err);
throw err;
}
_.remove(this.queue[aggId], function (c) {
return c.command === cmd;
});
},
/**
* Locks the aggregate.
* @param {String} aggregateId The passed aggregateId.
* @param {Function} callback The function, that will be called when this action is completed.
* `function(err){}`
*/
lockAggregate: function (aggregateId, callback) {
if (!aggregateId || !_.isString(aggregateId)) {
var err = new Error('Please pass a valid aggregateId!');
debug(err);
throw err;
}
if (!callback || !_.isFunction(callback)) {
var err = new Error('Please pass a valid callback!');
debug(err);
throw err;
}
this.aggregateLock.reserve(this.id, aggregateId, callback);
},
/**
* Loads the aggregate.
* @param {Object} cmd The command to be queued.
* @param {String} aggregateId The passed aggregateId (default).
* @param {Function} callback The function, that will be called when this action is completed.
* `function(err, aggregate, stream, isNewSnapshotNeeded){}`
*/
loadAggregate: function (cmd, aggregateId, callback) {
if (!cmd || !_.isObject(cmd)) {
var err = new Error('Please pass a valid command!');
debug(err);
throw err;
}
if (!aggregateId || !_.isString(aggregateId)) {
var err = new Error('Please pass a valid aggregateId!');
debug(err);
throw err;
}
if (!callback || !_.isFunction(callback)) {
var err = new Error('Please pass a valid callback!');
debug(err);
throw err;
}
var hasAggregateName = !!this.definitions.command.aggregate;
var hasContextName = !!this.definitions.command.context;
var toLoad = this.aggregate.getLoadInfo(cmd);
var self = this;
var firstToLoad = toLoad.shift();
firstToLoad.aggregateId = firstToLoad.aggregateId || aggregateId;
var aggregate = this.aggregate.create(aggregateId);
var streams = [];
var isNewSnapShotNeeded = false;
if (this.aggregate.skipHistory) {
debug('skip history for ', firstToLoad);
if (this.aggregate.disablePersistence) {
debug('persistency is disabled for ', firstToLoad);
return callback(null, aggregate, streams);
}
var startLoadingOne = Date.now();
// load aggregate with every stream
this.eventStore.getLastEventAsStream(firstToLoad, function(err, stream) {
if (err) {
return callback(err);
}
var lastStreamRevInLoop = null;
var events = _.map(stream.events, function (streamEvent) {
if (lastStreamRevInLoop !== null && lastStreamRevInLoop + 1 !== streamEvent.streamRevision) {
var msg = 'WARNING!!! Inconsistent event stream! Event with id (' + streamEvent.id + ') has a streamRevision of ' + streamEvent.streamRevision + ', but streamRevision in last event was ' + lastStreamRevInLoop + '!';
var e = new Error(msg);
debug(e);
console.log(msg);
}
lastStreamRevInLoop = streamEvent.streamRevision;
return streamEvent.payload;
});
var loadingTime = Date.now() - startLoadingOne;
debug('needed ' + loadingTime + 'ms to load last event from the eventstore for: ', firstToLoad);
streams.push(stream);
async.map(events, function (evt, clb) {
var evtName = dotty.get(evt, self.definitions.event.name);
if (!evtName) {
var err = new Error('event has no event name in ' + self.definitions.event.name);
debug(err);
throw err;
}
var version = 0;
if (!!self.definitions.event.version) {
version = dotty.get(evt, self.definitions.event.version);
}
var evtTransformer = self.aggregate.getLoadingEventTransformer(evtName, version) || { transform: function (e, cb) { cb(null, e); } };
evtTransformer.transform(evt, clb);
}, function (err, events) {
if (err) return callback(err);
self.aggregate.loadFromHistory(aggregate, null, events, loadingTime, stream, streams);
// callback with the aggregate and the streams
callback(null, aggregate, streams);
});
});
return;
}
function regularLoad (callback) {
if (!hasAggregateName) {
delete firstToLoad.aggregate;
}
if (!hasContextName) {
delete firstToLoad.context;
}
var startLoading = Date.now();
// load aggregate with every stream
self.eventStore.getFromSnapshot(firstToLoad, function(err, snapshot, stream) {
if (err) {
return callback(err);
}
var ignoreSnapshot = false;
if (snapshot) ignoreSnapshot = self.aggregate.shouldIgnoreSnapshot(snapshot);
if (!ignoreSnapshot) {
var lastStreamRevInLoop = null;
var events = _.map(stream.events, function (streamEvent) {
if (lastStreamRevInLoop !== null && lastStreamRevInLoop + 1 !== streamEvent.streamRevision) {
var msg = 'WARNING!!! Inconsistent event stream! Event with id (' + streamEvent.id + ') has a streamRevision of ' + streamEvent.streamRevision + ', but streamRevision in last event was ' + lastStreamRevInLoop + '!';
var e = new Error(msg);
debug(e);
console.log(msg);
}
lastStreamRevInLoop = streamEvent.streamRevision;
return streamEvent.payload;
});
var loadingTime = Date.now() - startLoading;
var addon = '';
if (snapshot) {
addon = ' and snapshot';
}
debug('needed ' + loadingTime + 'ms to load events' + addon + ' from the eventstore for: ', firstToLoad);
if (!snapshot) {
isNewSnapShotNeeded = self.aggregate.loadFromHistory(aggregate, snapshot, [], loadingTime, stream);
streams.push(stream);
callback(null, events, stream, startLoading);
return;
}
var transformer = self.aggregate.loadingSnapshotTransformers[self.aggregate.context.name + '.' + self.aggregate.name + '.' + self.aggregate.version] || function (sn, cb) { cb(null, sn); };
transformer(snapshot.data, function (err, snapData) {
if (err) return callback(err);
snapshot.data = snapData;
isNewSnapShotNeeded = self.aggregate.loadFromHistory(aggregate, snapshot, [], loadingTime, stream);
streams.push(stream);
callback(null, events, stream, startLoading);
});
return;
}
debug('skipping snapshot, and load whole event stream');
startLoading = Date.now();
self.eventStore.getEventStream(firstToLoad, function(err, stream) {
if (err) {
return callback(err);
}
var lastStreamRevInLoop = null;
var events = _.map(stream.events, function (streamEvent) {
if (lastStreamRevInLoop !== null && lastStreamRevInLoop + 1 !== streamEvent.streamRevision) {
var msg = 'WARNING!!! Inconsistent event stream! Event with id (' + streamEvent.id + ') has a streamRevision of ' + streamEvent.streamRevision + ', but streamRevision in last event was ' + lastStreamRevInLoop + '!';
var e = new Error(msg);
debug(e);
console.log(msg);
}
lastStreamRevInLoop = streamEvent.streamRevision;
return streamEvent.payload;
});
loadingTime = Date.now() - startLoading;
debug('needed ' + loadingTime + 'ms to load events from the eventstore');
async.map(events, function (evt, clb) {
var evtName = dotty.get(evt, self.definitions.event.name);
if (!evtName) {
var err = new Error('event has no event name in ' + self.definitions.event.name);
debug(err);
throw err;
}
var version = 0;
if (!!self.definitions.event.version) {
version = dotty.get(evt, self.definitions.event.version);
}
var evtTransformer = self.aggregate.getLoadingEventTransformer(evtName, version) || { transform: function (e, cb) { cb(null, e); } };
evtTransformer.transform(evt, clb);
}, function (err, events) {
if (err) return callback(err);
isNewSnapShotNeeded = self.aggregate.loadFromHistory(aggregate, null, events, loadingTime, stream);
streams.push(stream);
callback(null, events, stream, startLoading);
});
});
});
}
regularLoad(function (err, events, stream, totalLoadingTime) {
if (err) {
return callback(err);
}
async.eachSeries(toLoad, function (loadInfo, callback) {
loadInfo.aggregateId = loadInfo.aggregateId || aggregateId;
if (!hasAggregateName) {
delete loadInfo.aggregate;
}
if (!hasContextName) {
delete loadInfo.context;
}
var rev = aggregate.getRevision(loadInfo);
var startLoading = Date.now();
// load aggregate with every stream
self.eventStore.getEventStream(loadInfo, rev, function(err, str) {
if (err) {
return callback(err);
}
var lastStreamRevInLoop = null;
var evts = _.map(str.events, function (streamEvent) {
if (lastStreamRevInLoop !== null && lastStreamRevInLoop + 1 !== streamEvent.streamRevision) {
var msg = 'WARNING!!! Inconsistent event stream! Event with id (' + streamEvent.id + ') has a streamRevision of ' + streamEvent.streamRevision + ', but streamRevision in last event was ' + lastStreamRevInLoop + '!';
var e = new Error(msg);
debug(e);
console.log(msg);
}
lastStreamRevInLoop = streamEvent.streamRevision;
return streamEvent.payload;
});
var loadingTime = Date.now() - startLoading;
debug('needed ' + loadingTime + 'ms to load events from the eventstore for: ', loadInfo);
var notSameOrigin = loadInfo.aggregate && loadInfo.aggregate !== self.aggregate.name ||
loadInfo.context && loadInfo.context !== self.aggregate.context.name;
async.map(evts, function (evt, clb) {
var evtName = dotty.get(evt, self.definitions.event.name);
if (!evtName) {
var err = new Error('event has no event name in ' + self.definitions.event.name);
debug(err);
throw err;
}
var version = 0;
if (!!self.definitions.event.version) {
version = dotty.get(evt, self.definitions.event.version);
}
var evtTransformer = self.aggregate.getLoadingEventTransformer(evtName, version) || { transform: function (e, cb) { cb(null, e); } };
evtTransformer.transform(evt, clb);
}, function (err, evts) {
if (err) return callback(err);
var snapNeed = self.aggregate.loadFromHistory(aggregate, null, evts, loadingTime, str, null, notSameOrigin);
if (!isNewSnapShotNeeded) {
isNewSnapShotNeeded = snapNeed;
}
streams.push(str);
callback(null);
});
});
}, function (err) {
if (err) {
return callback(err);
}
var loadingTime = Date.now() - totalLoadingTime;
async.map(events, function (evt, clb) {
var evtName = dotty.get(evt, self.definitions.event.name);
if (!evtName) {
var err = new Error('event has no event name in ' + self.definitions.event.name);
debug(err);
throw err;
}
var version = 0;
if (!!self.definitions.event.version) {
version = dotty.get(evt, self.definitions.event.version);
}
var evtTransformer = self.aggregate.getLoadingEventTransformer(evtName, version) || { transform: function (e, cb) { cb(null, e); } };
evtTransformer.transform(evt, clb);
}, function (err, events) {
if (err) return callback(err);
var snapNeed = self.aggregate.loadFromHistory(aggregate, null, events, loadingTime, stream, streams);
if (!isNewSnapShotNeeded) {
isNewSnapShotNeeded = snapNeed;
}
debug('check if new snapshot is needed');
if (isNewSnapShotNeeded) {
self.createSnapshot(aggregate, stream);
}
// callback with the aggregate and the streams
callback(null, aggregate, streams);
});
});
});
},
/**
* Creates a new snapshot.
* @param {AggregateModel} aggregate The passed aggregate.
* @param {Object} stream The event stream.
* @param {Function} callback The function, that will be called when this action is completed. [optional]
* `function(err){}`
*/
createSnapshot: function (aggregate, stream, callback) {
if (!aggregate || !_.isObject(aggregate)) {
var err = new Error('Please pass a valid aggregate!');
debug(err);
throw err;
}
if (!stream || !_.isObject(stream)) {
var err = new Error('Please pass a valid aggregate!');
debug(err);
throw err;
}
var hasAggregateName = !!this.definitions.command.aggregate;
var hasContextName = !!this.definitions.command.context;
var query = {
aggregateId: aggregate.id
};
if (hasAggregateName) {
query.aggregate = this.aggregate.name;
}
if (hasContextName) {
query.context = this.aggregate.context.name;
}
query.data = aggregate.toJSON();
query.revision = stream.lastRevision;
query.version = this.aggregate.version;
var self = this;
process.nextTick(function() {
var transformer = self.aggregate.committingSnapshotTransformers[self.aggregate.context.name + '.' + self.aggregate.name + '.' + self.aggregate.version] || function (sn, cb) { cb(null, sn); };
transformer(query.data, function (err, snapData) {
if (err) {
debug(err);
if (callback) callback(err);
return;
}
query.data = snapData;
debug('cerate new snapshot');
self.eventStore.createSnapshot(query, function (err) {
if (err) {
debug(err);
if (callback) callback(err);
return;
}
debug('snapshot created');
if (callback) callback(null);
});
});
});
},
/**
* Returns an error if the aggregate is destroyed.
* @param {AggregateModel} aggregate The passed aggregate.
* @param {Object} cmd The command.
* @returns {AggregateDestroyedError}
*/
isAggregateDestroyed: function (aggregate, cmd) {
if (!aggregate || !_.isObject(aggregate)) {
var err = new Error('Please pass a valid aggregate!');
debug(err);
throw err;
}
if (!cmd || !_.isObject(cmd)) {
var err = new Error('Please pass a valid command!');
debug(err);
throw err;
}
var contextName, aggregateName, aggregateId;
if (!!this.definitions.command.context) {
contextName = dotty.get(cmd, this.definitions.command.context);
}
if (!!this.definitions.command.aggregate) {
aggregateName = dotty.get(cmd, this.definitions.command.aggregate);
}
if (!!this.definitions.command.aggregateId) {
aggregateId = dotty.get(cmd, this.definitions.command.aggregateId);
}
var streamInfo = {
context: contextName,
aggregate: aggregateName,
aggregateId: aggregateId
};
if (aggregate.isDestroyed()) {
return new AggregateDestroyedError('Aggregate has already been destroyed!', {
aggregateId: aggregate.id,
aggregateRevision: aggregate.getRevision(streamInfo)
});
}
return null;
},
/**
* Returns an error if the revision does not match.
* @param {AggregateModel} aggregate The passed aggregate.
* @param {Object} cmd The command.
* @returns {AggregateConcurrencyError}
*/
isRevisionWrong: function (aggregate, cmd) {
if (!aggregate || !_.isObject(aggregate)) {
var err = new Error('Please pass a valid aggregate!');
debug(err);
throw err;
}
if (!cmd || !_.isObject(cmd)) {
var err = new Error('Please pass a valid command!');
debug(err);
throw err;
}
var hasRevision = !!this.definitions.command.revision;
if (!hasRevision) {
return null;
}
var revisionInCommand = dotty.get(cmd, this.definitions.command.revision);
if (revisionInCommand === null || revisionInCommand === undefined) {
return null;
}
var contextName, aggregateName, aggregateId;
if (!!this.definitions.command.context) {
contextName = dotty.get(cmd, this.definitions.command.context);
}
if (!!this.definitions.command.aggregate) {
aggregateName = dotty.get(cmd, this.definitions.command.aggregate);
}
if (!!this.definitions.command.aggregateId) {
aggregateId = dotty.get(cmd, this.definitions.command.aggregateId);
}
var streamInfo = {
context: contextName,
aggregate: aggregateName,
aggregateId: aggregateId
};
if (revisionInCommand === aggregate.getRevision(streamInfo)) {
return null;
}
return new AggregateConcurrencyError('Actual revision in command is "' + revisionInCommand + '", but expected is "' + aggregate.getRevision(streamInfo) + '"!', {
aggregateId: aggregate.id,
aggregateRevision: aggregate.getRevision(streamInfo),
commandRevision: revisionInCommand
});
},
/**
* Returns an error if the command is not valid.
* @param {Object} cmd The command.
* @returns {ValidationError}
*/
validateCommand: function (cmd, callback) {
if (!cmd || !_.isObject(cmd)) {
debug(err);
return callback(new Error('Please pass a valid command!'), null);
}
return this.aggregate.validateCommand(cmd, callback);
},
checkPreLoadConditions: function (cmd, clb) {
this.aggregate.checkPreLoadConditions(cmd, clb);
},
/**
* Returns an error if verification fails.
* @param {AggregateModel} aggregate The passed aggregate.
* @param {Object} cmd The command.
* @returns {Error}
*/
verifyAggregate: function (aggregate, cmd) {
if (!aggregate || !_.isObject(aggregate)) {
var err = new Error('Please pass a valid aggregate!');
debug(err);
throw err;
}
if (!cmd || !_.isObject(cmd)) {
var err = new Error('Please pass a valid command!');
debug(err);
throw err;
}
var reason = this.isAggregateDestroyed(aggregate, cmd);
if (reason) {
return reason;
}
if (this.aggregate.disablePersistence) return;
reason = this.isRevisionWrong(aggregate, cmd);
if (reason) {
return reason;
}
},
/**
* Handles the command by passing it to the handle function of the aggregate.
* @param {AggregateModel} aggregate The passed aggregate.
* @param {Object} cmd The command.
* @param {Function} callback The function, that will be called when this action is completed.
* `function(err){}`
*/
letHandleCommandByAggregate: function (aggregate, cmd, callback) {
if (!aggregate || !_.isObject(aggregate)) {
var err = new Error('Please pass a valid aggregate!');
debug(err);
throw err;
}
if (!cmd || !_.isObject(cmd)) {
var err = new Error('Please pass a valid command!');
debug(err);
throw err;
}
if (!callback || !_.isFunction(callback)) {
var err = new Error('Please pass a valid function!');
debug(err);
throw err;
}
this.aggregate.handle(aggregate, cmd, callback);
},
/**
* Checks if the aggregate lock is ok.
* @param {String} aggregateId The passed aggregateId.
* @param {Function} callback The function, that will be called when this action is completed.
* `function(err){}`
*/
checkAggregateLock: function (aggregateId, callback) {
if (!aggregateId || !_.isString(aggregateId)) {
var err = new Error('Please pass a valid aggregateId!');
debug(err);
throw err;
}
if (!callback || !_.isFunction(callback)) {
var err = new Error('Please pass a valid callback!');
debug(err);
throw err;
}
var self = this;
this.aggregateLock.getAll(aggregateId, function (err, workerIds) {
if (err) {
return callback(err);
}
if (workerIds.length === 1 && workerIds[0] === self.id) {
return callback(null);
}
var err = new ConcurrencyError('Aggregate is locked by an other command handler!');
debug(err);
callback(err);
});
},
/**
* Resolves if the aggregate lock.
* @param {String} aggregateId The passed aggregateId.
* @param {Function} callback The function, that will be called when this action is completed.
* `function(err){}`
*/
resolveAggregateLock: function (aggregateId, callback) {
if (!aggregateId || !_.isString(aggregateId)) {
var err = new Error('Please pass a valid aggregateId!');
debug(err);
throw err;
}
if (!callback || !_.isFunction(callback)) {
var err = new Error('Please pass a valid callback!');
debug(err);
throw err;
}
this.aggregateLock.resolve(aggregateId, callback);
},
/**
* Saves the uncommitted events of an aggregate in the eventstore.
* @param {AggregateModel} aggregate The passed aggregate.
* @param {Array} streams The event streams.
* @param {Function} callback The function, that will be called when this action is completed.
* `function(err, eventsToDispatch){}`
*/
commit: function (aggregate, streams, callback) {
if (!aggregate || !_.isObject(aggregate)) {
var err = new Error('Please pass a valid aggregate!');
debug(err);
throw err;
}
if (!streams || !_.isArray(streams)) {
var err = new Error('Please pass valid streams!');
debug(err);
throw err;
}
if (!callback || !_.isFunction(callback)) {
var err = new Error('Please pass a valid callback!');
debug(err);
throw err;
}
var uncommitedEvents = aggregate.getUncommittedEvents();
if (uncommitedEvents.length === 0) {
debug('no events generated');
return callback(null, []);
}
var self = this;
var evtsToDispatch = [];
var fakeEventStream = {
aggregateId: dotty.get(uncommitedEvents[0], self.definitions.event.aggregateId),
aggregate: dotty.get(uncommitedEvents[0], self.definitions.event.aggregate),
context: dotty.get(uncommitedEvents[0], self.definitions.event.context),
uncommittedEvents: []
};
for (var e in uncommitedEvents) {
var evt = uncommitedEvents[e];
var stream = _.find(streams, function (s) {
return s.context === dotty.get(evt, self.definitions.event.context) &&
s.aggregate === dotty.get(evt, self.definitions.event.aggregate) &&
s.aggregateId === dotty.get(evt, self.definitions.event.aggregateId);
});
if (!stream && !self.aggregate.disablePersistence) {
debug('no stream found for:', evt);
return callback(new Error('No event stream found for evt with id:' + dotty.get(evt, self.definitions.event.id)));
}
if (stream) stream.addEvent(evt);
if (!stream && self.aggregate.disablePersistence) {
var eventStoreEvent = new EventFromEventStore(fakeEventStream, evt, self.eventStore.eventMappings);
eventStoreEvent.disablePersistence = true;
eventStoreEvent.id = uuid().toString();
evtsToDispatch.push(eventStoreEvent);
}
}
async.each(streams, function (stream, callback) {
if (self.aggregate.disablePersistence) {
for (var ie = 0, evtLength = stream.uncommittedEvents.length; ie < evtLength; ie++) {
stream.uncommittedEvents[ie].disablePersistence = true;
stream.uncommittedEvents[ie].id = uuid().toString();
evtsToDispatch.push(stream.uncommittedEvents[ie]);
}
return callback(null);
}
async.each(stream.uncommittedEvents, function (streamEvent, clb) {
var evt = streamEvent.payload;
var evtName = dotty.get(evt, self.definitions.event.name);
if (!evtName) {
var err = new Error('event has no event name in ' + self.definitions.event.name);
debug(err);
throw err;
}
var version = 0;
if (!!self.definitions.event.version) {
version = dotty.get(evt, self.definitions.event.version);
}
var evtTransformer = self.aggregate.getCommittingEventTransformer(evtName, version) || { transform: function (e, cb) { cb(null, e); } };
evtTransformer.transform(evt, function (err, evt) {
if (err) return clb(err);
streamEvent.payload = evt;
clb(null);
});
}, function (err) {
if (err) return callback(err);
stream.commit(function (err, stream) {
if (err) return callback(err);
evtsToDispatch = evtsToDispatch.concat(stream.eventsToDispatch);
callback(null);
});
});
}, function (err) {
if (err) return callback(err);
async.map(evtsToDispatch, function (streamEvent, clb) {
var evt = streamEvent.payload;
var evtName = dotty.get(evt, self.definitions.event.name);
if (!evtName) {
var err = new Error('event has no event name in ' + self.definitions.event.name);
debug(err);
throw err;
}
var version = 0;
if (!!self.definitions.event.version) {
version = dotty.get(evt, self.definitions.event.version);
}
var evtTransformer = self.aggregate.getLoadingEventTransformer(evtName, version) || { transform: function (e, cb) { cb(null, e); } };
evtTransformer.transform(evt, function (err, evt) {
if (err) return clb(err);
var clonedStreamEvent = _.clone(streamEvent);
clonedStreamEvent.payload = evt;
clb(null, clonedStreamEvent);
});
}, function (err, evtsToDispatch) {
if (err) return callback(err);
callback(null, evtsToDispatch);
});
});
},
/**
* Executes the default workflow to handle a command.
* @param {String} aggId The passed aggregate id.
* @param {Object} cmd The passed command.
* @param {Function} callback The function, that will be called when this action is completed.
* `function(err, eventsToDispatch){}`
*/
workflow: function (aggId, cmd, callback) {
if (!aggId || !_.isString(aggId)) {
var err = new Error('Please pass a valid aggregate id!');
debug(err);
throw err;
}
if (!cmd || !_.isObject(cmd)) {
var err = new Error('Please pass a valid command!');
debug(err);
throw err;
}
if (!callback || !_.isFunction(callback)) {
var err = new Error('Please pass a valid callback!');
debug(err);
throw err;
}
var self = this;
var agg = null;
var meta = {
aggregateId: aggId,
aggregate: this.aggregate ? this.aggregate.name : undefined,
context: this.aggregate && this.aggregate.context ? this.aggregate.context.name : undefined
};
var concatenatedId = this.getConcatenatedId(aggId, cmd);
var hadNoAggregateId = !dotty.exists(cmd, this.definitions.command.aggregateId);
async.waterfall([
// validate command
function (clb) {
debug('validate command');
self.validateCommand(cmd, clb);
},
// // check aggregate lock
// function (clb) {
// if (hadNoAggregateId) return clb(null);
// debug('check aggregate lock');
// self.checkAggregateLock(aggId, clb);
// },
// check the pre-load conditions: this is run before the aggregate is locked to allow non-(b)locking checks
function (clb) {
debug('check the pre-load conditions');
self.checkPreLoadConditions(cmd, clb);
},
// lock aggregate
function (clb) {
if (hadNoAggregateId) return clb(null);
debug('lock aggregate');
self.lockAggregate(concatenatedId, clb);
},
// load aggregate
function (clb) {
debug('load aggregate');
self.loadAggregate(cmd, aggId, clb);
},
// verify aggregate
function (aggregate, streams, clb) {
agg = aggregate; // save it temporary so we can use it in the callback
debug('verify aggregate');
var err = self.verifyAggregate(aggregate, cmd);
if (err) {
return clb(err);
}
clb(null, aggregate, streams);
},
// handle command and check business rules
function (aggregate, streams, clb) {
debug('handle command');
self.letHandleCommandByAggregate(aggregate, cmd, function (err) { // err is a business rule error
if (err) {
return clb(err);
}
clb(null, aggregate, streams);
});
},
// check aggregate lock
function (aggregate, streams, clb) {
if (hadNoAggregateId) return clb(null, aggregate, streams);
debug('check aggregate lock');
self.checkAggregateLock(concatenatedId, function (err) {
clb(err, aggregate, streams);
});
},
// commit new aggregate events
function (aggregate, streams, clb) {
debug('commit new aggregate events');
self.commit(aggregate, streams, clb);
}
], function (err, eventsToDispatch) {
if (agg && agg.getRevision() === 0) {
agg = null;
}
if (hadNoAggregateId) {
if (callback.length > 2) {
return callback(err, eventsToDispatch || null, agg ? agg.toJSON() : null, meta);
}
return callback(err, eventsToDispatch || null);
}
// unlock...
debug('unlock aggregate');
self.resolveAggregateLock(concatenatedId, function (errLock) {
if (errLock) {
debug(errLock);
return callback(errLock);
}
if (err) {
debug(err);
if (err instanceof ConcurrencyError) {
var retryIn = randomBetween(0, self.options.retryOnConcurrencyTimeout); // could be overwritten in a custom commandHandler?
debug('retry in ' + retryIn + 'ms');
setTimeout(function() {
self.workflow(aggId, cmd, callback);
}, retryIn);
return;
}
if (callback.length > 2) {
return callback(err, null, agg ? agg.toJSON() : null, meta);
}
return callback(err, null);
}
if (callback.length > 2) {
return callback(null, eventsToDispatch || null, agg ? agg.toJSON() : null, meta);
}
callback(null, eventsToDispatch || null);
});
});
},
/**
* Returns the concatenated id (more unique)
* @param {String} aggId The passed aggregate id.
* @param {Object} cmd The passed command.
* @returns {string}
*/
getConcatenatedId: function (aggId, cmd) {
var aggregate = '';
if (dotty.exists(cmd, this.definitions.command.aggregate)) {
aggregate = dotty.get(cmd, this.definitions.command.aggregate);
}
var context = '';
if (dotty.exists(cmd, this.definitions.command.context)) {
context = dotty.get(cmd, this.definitions.command.context);
}
return context + aggregate + aggId;
},
/**
* Handles the passed command
* @param {Object} cmd The passed command
* @param {Function} callback The function, that will be called when this action is completed.
* `function(err, evts){}`
*/
handle: function (cmd, callback) {
if (!cmd || !_.isObject(cmd)) {
var err = new Error('Please pass a valid command!');
debug(err);
throw err;
}
if (!callback || !_.isFunction(callback)) {
var err = new Error('Please pass a valid callback!');
debug(err);
throw err;
}
var self = this;
function _handle (aggId) {
var concatenatedId = self.getConcatenatedId(aggId, cmd);
var isFirst = !self.getNextCommandInQueue(concatenatedId);
self.queueCommand(concatenatedId, cmd, callback);
if (!isFirst) {
return;
}
(function handleNext (aggregateId, c) {
var concId = self.getConcatenatedId(aggregateId, c);
var cmdEntry = self.getNextCommandInQueue(concId);
if (cmdEntry) {
if (cmdEntry.callback.length > 2) {
self.workflow(aggregateId, cmdEntry.command, function (err, evts, aggData, meta) {
self.removeCommandFromQueue(concId, cmdEntry.command);
handleNext(aggregateId, cmdEntry.command);
cmdEntry.callback(err, evts, aggData, meta);
});
return;
}
self.workflow(aggregateId, cmdEntry.command, function (err, evts) {
self.removeCommandFromQueue(concId, cmdEntry.command);
handleNext(aggregateId, cmdEntry.command);
cmdEntry.callback(err, evts);
});
}
})(aggId, cmd);
}
if (dotty.exists(cmd, this.definitions.command.aggregateId)) {
return _handle(dotty.get(cmd, this.definitions.command.aggregateId));
}
debug('no aggregateId in command, so generate a new one');
this.getNewAggregateId(cmd, function (err, id) {
if (err) {
debug(err);
return callback(err);
}
return _handle(id);
});
},
/**
* Inject idGenerator function for aggregate id.
* @param {Function} fn The function to be injected.
* @returns {DefaultCommandHandler} to be able to chain...
*/
aggregateIdGenerator: function (fn) {
if (fn.length === 0) {
fn = _.wrap(fn, function(func, callback) {
callback(null, func());
});
}
this.getNewAggregateIdFn = fn;
return this;
},
/**
* IdGenerator function for aggregate id.
* @param {Function} callback The function, that will be called when this action is completed.
* `function(err, newId){}`
*/
getNewAggregateId: function (cmd, callback) {
var getNewIdFn = this.eventStore.getNewId.bind(this.eventStore);
if (this.aggregate && this.aggregate.getNewAggregateId) {
getNewIdFn = this.aggregate.getNewAggregateId.bind(this.aggregate);
} else if (this.getNewAggregateIdFn) {
getNewIdFn = this.getNewAggregateIdFn.bind(this);
}
if (getNewIdFn.length === 2) {
return getNewIdFn(cmd, callback);
}
getNewIdFn(callback);
}
});
module.exports = DefaultCommandHandler;