@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,126 lines (946 loc) • 34.3 kB
JavaScript
var Definition = require('../definitionBase'),
util = require('util'),
_ = require('lodash'),
debug = require('debug')('domain:aggregate'),
AggregateModel = require('../aggregateModel'),
dotty = require('dotty'),
DefaultCommandHandler = require('../defaultCommandHandler'),
uuid = require('uuid').v4,
async = require('async');
/**
* Aggregate constructor
* @param {Object} meta Meta infos like: { name: 'name', version: 1 }
* @param {Object} modelInitValues Initialization values for model like: { emails: [] } [optional]
* @constructor
*/
function Aggregate (meta, modelInitValues) {
Definition.call(this, meta);
meta = meta || {};
this.version = meta.version || 0;
this.defaultCommandPayload = meta.defaultCommandPayload || '';
this.defaultEventPayload = meta.defaultEventPayload || '';
this.defaultPreConditionPayload = meta.defaultPreConditionPayload || '';
this.defaultPreLoadConditionPayload = meta.defaultPreLoadConditionPayload || '';
this.skipHistory = meta.skipHistory || false;
this.applyLastEvent = meta.applyLastEvent || false;
this.disablePersistence = meta.disablePersistence || false;
this.commands = [];
this.events = [];
this.businessRules = [];
this.commandHandlers = [];
this.loadingEventTransformers = [];
this.committingEventTransformers = [];
this.snapshotConversions = {};
this.snapshotIgnores = {};
this.loadingSnapshotTransformers = {};
this.committingSnapshotTransformers = {};
this.idGenerator(function () {
return uuid().toString();
});
this.modelInitValues = modelInitValues || {};
this.defaultCommandHandler = new DefaultCommandHandler(this);
this.defaultCommandHandler.useAggregate(this);
this.snapshotConversionRegistrations = [];
this.loadingSnapshotTransformerRegistrations = [];
this.committingSnapshotTransformerRegistrations = [];
}
util.inherits(Aggregate, Definition);
/**
* Returns the apply function for the AggregateModel.
* @param {Aggregate} aggregate The aggregate object.
* @param {AggregateModel} aggregateModel The aggregateModel object.
* @param {Command} cmd The command object that caused this.
* @returns {Function}
*/
function applyHelper (aggregate, aggregateModel, cmd) {
return function (name, payload, version) {
aggregateModel.set = function () {
AggregateModel.prototype.set.apply(aggregateModel, _.toArray(arguments));
};
var evt;
if (!payload) {
if (_.isString(name)) {
evt = {};
dotty.put(evt, aggregate.definitions.event.name, name);
} else if (_.isObject(name)) {
evt = name;
}
} else {
evt = {};
dotty.put(evt, aggregate.definitions.event.name, name);
dotty.put(evt, aggregate.definitions.event.payload, payload);
}
if (!!aggregate.definitions.event.meta && !!aggregate.definitions.command.meta) {
var commandMeta = dotty.get(cmd, aggregate.definitions.command.meta);
var userAddedMeta = dotty.get(evt, aggregate.definitions.event.meta);
var meta = (commandMeta || userAddedMeta) && _.extend({}, commandMeta, userAddedMeta);
dotty.put(evt, aggregate.definitions.event.meta, meta);
}
dotty.put(evt, aggregate.definitions.event.aggregateId, aggregateModel.id);
dotty.put(evt, aggregate.definitions.event.correlationId, dotty.get(cmd, aggregate.definitions.command.id));
if (!!aggregate.definitions.event.version) {
if (_.isNumber(version)) {
dotty.put(evt, aggregate.definitions.event.version, version);
}
if (!dotty.exists(evt, aggregate.definitions.event.version)) {
// if version is not defined in event, search the latest version number...
var evtName = dotty.get(evt, aggregate.definitions.event.name);
var maxVersion = _.reduce(aggregate.getEvents(), function (res, e) {
if (e.name !== evtName) {
return res;
}
var v = e.version || 0;
if (v > res) {
return v;
}
return res;
}, 0);
dotty.put(evt, aggregate.definitions.event.version, maxVersion);
}
}
var aggName, ctxName;
if (!!aggregate.definitions.event.aggregate && !!aggregate.definitions.command.aggregate) {
aggName = dotty.get(cmd, aggregate.definitions.command.aggregate) || aggregate.name;
dotty.put(evt, aggregate.definitions.event.aggregate, aggName);
}
if (!!aggregate.definitions.event.context && !!aggregate.definitions.command.context) {
ctxName = dotty.get(cmd, aggregate.definitions.command.context) || aggregate.context.name;
dotty.put(evt, aggregate.definitions.event.context, ctxName);
}
var aggregateId;
if (!!aggregate.definitions.event.aggregateId) {
aggregateId = dotty.get(evt, aggregate.definitions.event.aggregateId);
}
var streamInfo = {
context: ctxName,// || aggregate.context.name,
aggregate: aggName,// || aggregate.name,
aggregateId: aggregateId || aggregateModel.id
};
if (!aggregate.disablePersistence) {
var revision = aggregateModel.getRevision(streamInfo) + 1;
aggregateModel.setRevision(streamInfo, revision);
dotty.put(evt, aggregate.definitions.event.revision, revision);
}
aggregateModel.addUncommittedEvent(evt);
// apply the event
debug('apply the event');
aggregate.apply(_.cloneDeep(evt), aggregateModel);
aggregateModel.set = function () {
throw new Error('You are not allowed to set a value in this step!');
};
};
}
_.extend(Aggregate.prototype, {
/**
* Inject idGenerator function.
* @param {Function} fn The function to be injected.
* @returns {Aggregate} to be able to chain...
*/
idGenerator: function (fn) {
if (fn.length === 0) {
fn = _.wrap(fn, function(func, callback) {
callback(null, func());
});
}
this.getNewId = fn;
return this;
},
/**
* Inject the context module.
* @param {Context} context The context module to be injected.
*/
defineContext: function (context) {
if (!context || !_.isObject(context)) {
var err = new Error('Please inject a valid context object!');
debug(err);
throw err;
}
this.context = context;
for (var r in this.snapshotConversionRegistrations) {
var reg = this.snapshotConversionRegistrations[r];
var meta = reg.meta;
var fn = reg.fn;
meta.context = meta.context || this.context.name;
meta.aggregate = meta.aggregate || this.name;
this.snapshotConversions[meta.context + '.' + meta.aggregate + '.' + meta.version] = fn;
}
for (var r in this.loadingSnapshotTransformerRegistrations) {
var reg = this.loadingSnapshotTransformerRegistrations[r];
var meta = reg.meta;
var fn = reg.fn;
meta.context = meta.context || this.context.name;
meta.aggregate = meta.aggregate || this.name;
this.loadingSnapshotTransformers[meta.context + '.' + meta.aggregate + '.' + meta.version] = fn;
}
for (var r in this.committingSnapshotTransformerRegistrations) {
var reg = this.committingSnapshotTransformerRegistrations[r];
var meta = reg.meta;
var fn = reg.fn;
meta.context = meta.context || this.context.name;
meta.aggregate = meta.aggregate || this.name;
this.committingSnapshotTransformers[meta.context + '.' + meta.aggregate + '.' + meta.version] = fn;
}
},
/**
* Add command module.
* @param {Command} command The command module to be injected.
*/
addCommand: function (command) {
if (!command || !_.isObject(command)) {
var err = new Error('Please inject a valid command object!');
debug(err);
throw err;
}
if (!command.payload && command.payload !== '') {
command.payload = this.defaultCommandPayload;
}
command.defineAggregate(this);
if (this.commands.indexOf(command) < 0) {
this.commands.push(command);
}
},
/**
* Add event module.
* @param {Event} event The event module to be injected.
*/
addEvent: function (event) {
if (!event || !_.isObject(event)) {
var err = new Error('Please inject a valid event object!');
debug(err);
throw err;
}
if (!event.payload && event.payload !== '') {
event.payload = this.defaultEventPayload;
}
if (this.events.indexOf(event) < 0) {
this.events.push(event);
}
},
/**
* Add businessRule module.
* @param {BusinessRule} businessRule The businessRule module to be injected.
*/
addBusinessRule: function (businessRule) {
if (!businessRule || !_.isObject(businessRule)) {
var err = new Error('Please inject a valid businessRule object!');
debug(err);
throw err;
}
if (this.businessRules.indexOf(businessRule) < 0) {
businessRule.defineAggregate(this);
this.businessRules.push(businessRule);
this.businessRules = _.sortBy(this.businessRules, function(br) {
return br.priority;
});
}
},
/**
* Add loadingEventTransformer module.
* @param {LoadingEventTransformer} loadingEventTransformer The loadingEventTransformer module to be injected.
*/
addLoadingEventTransformer: function (loadingEventTransformer) {
if (!loadingEventTransformer || !_.isObject(loadingEventTransformer)) {
var err = new Error('Please inject a valid loadingEventTransformer object!');
debug(err);
throw err;
}
if (this.loadingEventTransformers.indexOf(loadingEventTransformer) < 0) {
this.loadingEventTransformers.push(loadingEventTransformer);
}
},
/**
* Add committingEventTransformer module.
* @param {CommittingEventTransformer} committingEventTransformer The committingEventTransformer module to be injected.
*/
addCommittingEventTransformer: function (committingEventTransformer) {
if (!committingEventTransformer || !_.isObject(committingEventTransformer)) {
var err = new Error('Please inject a valid committingEventTransformer object!');
debug(err);
throw err;
}
if (this.committingEventTransformers.indexOf(committingEventTransformer) < 0) {
this.committingEventTransformers.push(committingEventTransformer);
}
},
/**
* Add commandHandler module.
* @param {CommandHandler} commandHandler The commandHandler module to be injected.
*/
addCommandHandler: function (commandHandler) {
if (!commandHandler || !_.isObject(commandHandler) || !_.isFunction(commandHandler.useAggregate)) {
var err = new Error('Please inject a valid commandHandler object!');
debug(err);
throw err;
}
commandHandler.useAggregate(this);
if (this.commandHandlers.indexOf(commandHandler) < 0) {
this.commandHandlers.push(commandHandler);
}
},
/**
* Returns the command modules by command name.
* @param {String} name The command name.
* @returns {Array}
*/
getCommandsByName: function (name) {
if (!name || !_.isString(name)) {
var err = new Error('Please pass a valid string as name!');
debug(err);
throw err;
}
return _.filter(this.commands, function (cmd) {
return cmd.name === name;
});
},
/**
* Returns the command module by command name and command version.
* @param {String} name The command name.
* @param {Number} version The command version. [optional; default 0]
* @returns {Command}
*/
getCommand: function (name, version) {
if (!name || !_.isString(name)) {
var err = new Error('Please pass a valid string as name!');
debug(err);
throw err;
}
version = version || 0;
if (!_.isNumber(version)) {
var err = new Error('Please pass a valid number as version!');
debug(err);
throw err;
}
return _.find(this.commands, function (cmd) {
return cmd.name === name && cmd.version === version;
});
},
/**
* Returns all command modules.
* @returns {Array}
*/
getCommands: function () {
return this.commands;
},
/**
* Returns the event module by event name and event version.
* @param {String} name The event name.
* @param {Number} version The event version. [optional; default 0]
* @returns {Event}
*/
getEvent: function (name, version) {
if (!name || !_.isString(name)) {
var err = new Error('Please pass a valid string as name!');
debug(err);
throw err;
}
version = version || 0;
if (!_.isNumber(version)) {
var err = new Error('Please pass a valid number as version!');
debug(err);
throw err;
}
return _.find(this.events, function (evt) {
return evt.name === name && evt.version === version;
});
},
/**
* Returns all event modules.
* @returns {Array}
*/
getEvents: function () {
return this.events;
},
/**
* Returns all business rule modules.
* @returns {Array}
*/
getBusinessRules: function () {
return this.businessRules;
},
/**
* Returns all commandHandler modules.
* @returns {Array}
*/
getCommandHandlers: function () {
return this.commandHandlers;
},
/**
* Returns the commandHandler module by command name and command version.
* @param {String} name The command name.
* @param {Number} version The command version. [optional; default 0]
* @returns {CommandHandler}
*/
getCommandHandler: function (name, version) {
if (!name || !_.isString(name)) {
var err = new Error('Please pass a valid string as name!');
debug(err);
throw err;
}
version = version || 0;
if (!_.isNumber(version)) {
var err = new Error('Please pass a valid number as version!');
debug(err);
throw err;
}
var handler = _.find(this.commandHandlers, function (cmdHnd) {
return cmdHnd.name === name && cmdHnd.version === version;
});
if (handler) {
return handler;
}
if (!this.getCommand(name, version)) {
return null;
}
return this.defaultCommandHandler;
},
/**
* Returns the loadingEventTransformer module by event name and event version.
* @param {String} name The event name.
* @param {Number} version The event version. [optional; default 0]
* @returns {Event}
*/
getLoadingEventTransformer: function (name, version) {
if (!name || !_.isString(name)) {
var err = new Error('Please pass a valid string as name!');
debug(err);
throw err;
}
version = version || 0;
if (!_.isNumber(version)) {
var err = new Error('Please pass a valid number as version!');
debug(err);
throw err;
}
var found = _.find(this.loadingEventTransformers, function (evt) {
return evt.name === name && evt.version === version;
});
if (found) return found;
return _.find(this.loadingEventTransformers, function (evt) {
return evt.name === name && (evt.version === null || evt.version === undefined);
});
},
/**
* Returns the committingEventTransformer module by event name and event version.
* @param {String} name The event name.
* @param {Number} version The event version. [optional; default 0]
* @returns {Event}
*/
getCommittingEventTransformer: function (name, version) {
if (!name || !_.isString(name)) {
var err = new Error('Please pass a valid string as name!');
debug(err);
throw err;
}
version = version || 0;
if (!_.isNumber(version)) {
var err = new Error('Please pass a valid number as version!');
debug(err);
throw err;
}
var found = _.find(this.committingEventTransformers, function (evt) {
return evt.name === name && evt.version === version;
});
if (found) return found;
return _.find(this.committingEventTransformers, function (evt) {
return evt.name === name && (evt.version === null || evt.version === undefined);
});
},
/**
* Returns a new aggregate model, to be used in the command and event functions.
* @param {String} id The aggregate id.
* @returns {AggregateModel}
*/
create: function (id) {
if (!id || !_.isString(id)) {
var err = new Error('Please pass a valid string as id!');
debug(err);
throw err;
}
return new AggregateModel(id, this.modelInitValues);
},
/**
* Validates the requested command.
* @param {Object} cmd The command object
* @returns {ValidationError}
*/
validateCommand: function (cmd, callback) {
var cmdName = dotty.get(cmd, this.definitions.command.name);
var err;
if (!cmdName) {
err = new Error('command has no command name in ' + this.definitions.command.name);
debug(err);
return callback(err);
}
var version = 0;
if (!!this.definitions.command.version) {
version = dotty.get(cmd, this.definitions.command.version);
}
var command = this.getCommand(cmdName, version);
if (!command) {
err = new Error('Command "' + cmdName + '" not found!');
debug(err);
return callback(err);
}
if (command.validate.length >= 2) {
return command.validate(cmd, callback);
}
try {
callback(command.validate(cmd));
} catch (e) {
callback(e);
}
},
/**
* Checks for pre-load-conditions. This check will be done BEFORE the aggregate is locked and loaded.
* @param {Object} cmd The command that was handled.
* @param {Function} callback The function, that will be called when this action is completed.
* `function(err){}`
*/
checkPreLoadConditions: function (cmd, callback) {
var cmdName = dotty.get(cmd, this.definitions.command.name);
if (!cmdName) {
var err = new Error('command has no command name in ' + this.definitions.command.name);
debug(err);
throw err;
}
var version = 0;
if (!!this.definitions.command.version) {
version = dotty.get(cmd, this.definitions.command.version);
}
var command = this.getCommand(cmdName, version);
if (!command) {
var err = new Error('Command "' + cmdName + '" not found!');
debug(err);
throw err;
}
command.checkPreLoadConditions(cmd, callback);
},
/**
* Checks for pre-conditions.
* @param {Object} cmd The command that was handled.
* @param {AggregateModel} aggregateModel The aggregate values.
* @param {Function} callback The function, that will be called when this action is completed.
* `function(err){}`
*/
checkPreConditions: function (cmd, aggregateModel, callback) {
var cmdName = dotty.get(cmd, this.definitions.command.name);
if (!cmdName) {
var err = new Error('command has no command name in ' + this.definitions.command.name);
debug(err);
throw err;
}
var version = 0;
if (!!this.definitions.command.version) {
version = dotty.get(cmd, this.definitions.command.version);
}
var command = this.getCommand(cmdName, version);
if (!command) {
var err = new Error('Command "' + cmdName + '" not found!');
debug(err);
throw err;
}
command.checkPreConditions(cmd, aggregateModel, callback);
},
/**
* Checks business rules.
* @param {Object} changed The new aggregate values.
* @param {Object} previous The previous aggregate values.
* @param {Array} events All new generated events.
* @param {Object} command The command that was handled.
* @param {Function} callback The function, that will be called when this action is completed.
* `function(err){}`
*/
checkBusinessRules: function (changed, previous, events, command, callback) {
async.eachSeries(this.getBusinessRules(), function (rule, callback) {
rule.check(changed, previous, events, command, callback);
}, callback);
},
/**
* Get infos to load correct stream.
* @param {Object} cmd The command object.
* @returns {Array}
*/
getLoadInfo: function (cmd) {
var cmdName = dotty.get(cmd, this.definitions.command.name);
if (!cmdName) {
var err = new Error('command has no command name in ' + this.definitions.command.name);
debug(err);
throw err;
}
var version = 0;
if (!!this.definitions.command.version) {
version = dotty.get(cmd, this.definitions.command.version);
}
var command = this.getCommand(cmdName, version);
if (!command) {
var aggregateId = dotty.get(cmd, this.definitions.command.aggregateId);
var aggregateName = this.name;
var contextName = this.context.name;
return [{
context: contextName,
aggregate: aggregateName,
aggregateId: aggregateId
}];
}
return command.getLoadInfo(cmd);
},
/**
* Handles the passed command and checks the business rules.
* @param {AggregateModel} aggregateModel The aggregateModel that should be used.
* @param {Object} cmd The command that was handled.
* @param {Function} callback The function, that will be called when this action is completed.
* `function(err){}`
*/
handle: function (aggregateModel, cmd, callback) {
var cmdName = dotty.get(cmd, this.definitions.command.name);
if (!cmdName) {
var err = new Error('command has no command name in ' + this.definitions.command.name);
debug(err);
return callback(err);
}
var version = 0;
if (!!this.definitions.command.version) {
version = dotty.get(cmd, this.definitions.command.version);
}
var command = this.getCommand(cmdName, version);
if (!command) {
var err = new Error('Command "' + cmdName + '" not found!');
debug(err);
return callback(err);
}
var self = this;
aggregateModel.set = function () {
throw new Error('You are not allowed to set a value in this step!');
};
var previousModel = new AggregateModel(aggregateModel.id, aggregateModel);
this.checkPreConditions(cmd, aggregateModel, function (err) {
if (err) {
return callback(err);
}
// attach apply function
aggregateModel.apply = applyHelper(self, aggregateModel, cmd);
debug('handle command');
try {
command.handle(cmd, aggregateModel);
} catch (e) {
return callback(e);
}
// remove apply function
delete aggregateModel.apply;
var uncommittedEvents = aggregateModel.getUncommittedEvents();
async.each(uncommittedEvents, function (evt, callback) {
var isEvtIdDefined = !!dotty.get(evt, self.definitions.event.id);
if (isEvtIdDefined) {
debug('event id already defined');
return callback(null);
}
// generate new id for event
debug('generate new id for event');
self.getNewId(function (err, id) {
if (err) {
return callback(err);
}
dotty.put(evt, self.definitions.event.id, id);
callback(null);
});
}, function (err) {
if (err) {
return callback(err);
}
// check business rules
debug('check business rules');
self.checkBusinessRules(aggregateModel, previousModel, uncommittedEvents, cmd, function (err) {
if (!err) {
return callback(null);
}
// clean up...
aggregateModel.reset(previousModel);
aggregateModel.clearUncommittedEvents();
callback(err);
});
});
});
},
/**
* Applies the passed events to the passed aggregateModel.
* @param {Array || Object} events The events that should be applied.
* @param {AggregateModel} aggregateModel The aggregateModel that should be used.
* @param {boolean} notSameOrigin If true it's an indication to not throw if event handler not found.
*/
apply: function (events, aggregateModel, notSameOrigin) {
if (!events) {
return;
}
if (!_.isArray(events)) {
events = [events];
}
var self = this;
events.forEach(function (evt) {
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 event = self.getEvent(evtName, version);
if (!event) {
if (notSameOrigin) {
debug('You don\'t want to handle event "' + evtName + '" of other event stream');
return;
}
var err = new Error('Event "' + evtName + '" not found!');
debug(err);
throw err;
}
event.apply(evt, aggregateModel);
});
},
/**
* Loads the aggregateModel with the data of the snapshot and the events.
* And returns true if a new snapshot should be done.
* @param {AggregateModel} aggregateModel The aggregateModel that should be used.
* @param {Object} snapshot The snapshot object.
* @param {Array} events The events that should be applied.
* @param {Number} loadingTime The loading time in ms of the eventstore data.
* @param {Object} stream The eventstream.
* @param {Array} streams All loaded eventstreams for this aggregate.
* @param {boolean} notSameOrigin If true it's an indication to not throw if event handler not found.
* @returns {boolean}
*/
loadFromHistory: function (aggregateModel, snapshot, events, loadingTime, stream, streams, notSameOrigin) {
var self = this;
var isSnapshotNeeded = false;
if (snapshot) {
// load snapshot
debug('load snapshot ' + snapshot.id + ' from history', _.pick(snapshot, ['context', 'aggregate', 'version']), {
context: this.context.name,
aggregate: this.name,
version: this.version
});
if ((snapshot.context === this.context.name || !snapshot.context && this.context.name === '_general') &&
(snapshot.aggregate === this.name || !snapshot.aggregate) &&
snapshot.version === this.version) {
aggregateModel.set(snapshot.data);
} else {
if (!this.snapshotConversions[snapshot.context + '.' + snapshot.aggregate + '.' + snapshot.version]) {
var err = new Error('No snapshot conversion defined!');
debug(err);
throw err;
}
debug('convert snapshot from history');
this.snapshotConversions[snapshot.context + '.' + snapshot.aggregate + '.' + snapshot.version](snapshot.data, aggregateModel);
isSnapshotNeeded = true;
}
aggregateModel.setRevision({
context: stream.context,
aggregate: stream.aggregate,
aggregateId: stream.aggregateId
}, snapshot.revision + 1);
}
if (events && events.length > 0) {
// load events
debug('load events from history');
var maxRevision = _.reduce(events, function (res, evt) {
var rev = dotty.get(evt, self.definitions.event.revision);
if (rev > res) {
return rev;
}
return res;
}, 0);
if (!this.skipHistory || (this.skipHistory && this.applyLastEvent)) {
this.apply(events, aggregateModel, notSameOrigin);
}
aggregateModel.setRevision({
context: stream.context,
aggregate: stream.aggregate,
aggregateId: stream.aggregateId
}, maxRevision);
if (!isSnapshotNeeded) {
if (this.isSnapshotNeeded.length > 2) {
isSnapshotNeeded = this.isSnapshotNeeded(loadingTime, events, aggregateModel.toJSON(), streams);
} else {
isSnapshotNeeded = this.isSnapshotNeeded(loadingTime, events);
}
}
}
return isSnapshotNeeded;
},
/**
* Defines a new loading transform function for snapshot.
* @param {Object} meta Meta infos like: { version: 10 }
* @param {Function} fn Function containing the transform function
* `function(snapshotData, callback){}`
* @returns {Aggregate}
*/
defineLoadingSnapshotTransformer: function (meta, fn) {
if (!_.isObject(meta) || meta.version === undefined || meta.version === null || !_.isNumber(meta.version)) {
throw new Error('Please pass in a version');
}
if (!_.isFunction(fn)) {
throw new Error('Please pass in a function');
}
var wrappedFn;
if (fn.length === 1) {
wrappedFn = function (s, cb) {
try {
cb(null, fn(s));
} catch (err) {
cb(err);
}
};
} else {
wrappedFn = fn;
}
this.loadingSnapshotTransformerRegistrations.push({ meta: meta, fn: wrappedFn });
return this;
},
/**
* Defines a new committing transform function for snapshot.
* @param {Object} meta Meta infos like: { version: 10 }
* @param {Function} fn Function containing the transform function
* `function(snapshotData, callback){}`
* @returns {Aggregate}
*/
defineCommittingSnapshotTransformer: function (meta, fn) {
if (!_.isObject(meta) || meta.version === undefined || meta.version === null || !_.isNumber(meta.version)) {
throw new Error('Please pass in a version');
}
if (!_.isFunction(fn)) {
throw new Error('Please pass in a function');
}
var wrappedFn;
if (fn.length === 1) {
wrappedFn = function (s, cb) {
try {
cb(null, fn(s));
} catch (err) {
cb(err);
}
};
} else {
wrappedFn = fn;
}
this.committingSnapshotTransformerRegistrations.push({ meta: meta, fn: wrappedFn });
return this;
},
/**
* Returns true if a new snapshot should be done.
* @param {Number} loadingTime The loading time in ms of the eventstore data.
* @param {Array} events The loaded events.
* @param {Object} aggregateModel The aggregate json object. [could be used for other algorithms]
* @param {Array} streams All loaded eventstreams for this aggregate.
* @returns {boolean}
*/
isSnapshotNeeded: function (loadingTime, events) {
var snapshotThreshold = 100;
if (this.options.snapshotThreshold) {
snapshotThreshold = this.options.snapshotThreshold;
}
if (this.options.snapshotThresholdMs) {
return loadingTime >= this.options.snapshotThresholdMs;
}
return events.length >= snapshotThreshold;
},
/**
* Checks if a snapshot should be ignored.
* @param {Object} snapshot The the snapshot.
* @returns {boolean}
*/
shouldIgnoreSnapshot: function (snapshot) {
if (!this.snapshotIgnores[snapshot.version]) {
return false;
}
return this.snapshotIgnores[snapshot.version](snapshot.data);
},
/**
* Defines the algorithm to identify if a snapshot is needed to be done.
* @param {Function} fn Function containing the algorithm. Should return true or false.
* `function(loadingTime, events, aggregateModel){}`
*/
defineSnapshotNeed: function (fn) {
if (!_.isFunction(fn)) {
throw new Error('Please pass in a function');
}
this.isSnapshotNeeded = fn;
return this;
},
/**
* Defines a new conversion function for older snapshot versions.
* @param {Object} meta Meta infos like: { version: 10 }
* @param {Function} fn Function containing the conversion rule
* `function(snapshotData, aggregateModel){}`
* @returns {Aggregate}
*/
defineSnapshotConversion: function (meta, fn) {
if (!_.isObject(meta) || meta.version === undefined || meta.version === null || !_.isNumber(meta.version)) {
throw new Error('Please pass in a version');
}
if (!_.isFunction(fn)) {
throw new Error('Please pass in a function');
}
this.snapshotConversionRegistrations.push({ meta: meta, fn: fn });
return this;
},
/**
* Defines a if a snapshot should be ignored. -> if true it will loads all events
* @param {Object} meta Meta infos like: { version: 10 }
* @param {Function} fn Function containing the check function [optional], default return true
* `function(snapshotData){ return true; }`
* @returns {Aggregate}
*/
defineIgnoreSnapshot: function (meta, fn) {
if (!_.isObject(meta) || meta.version === undefined || meta.version === null || !_.isNumber(meta.version)) {
throw new Error('Please pass in a version');
}
if (!fn) {
this.snapshotIgnores[meta.version] = function () {
return true;
};
return this;
}
if (_.isBoolean(fn)) {
this.snapshotIgnores[meta.version] = function () {
return fn;
};
return this;
}
this.snapshotIgnores[meta.version] = fn;
return this;
},
/**
* Inject idGenerator function for aggregate id.
* @param {Function} fn The function to be injected.
* @returns {Aggregate} to be able to chain...
*/
defineAggregateIdGenerator: function (fn) {
if (fn.length === 0) {
var orgFn = fn;
fn = function(callback) {
callback(null, orgFn());
};
}
this.getNewAggregateId = fn;
return this;
},
/**
* Inject idGenerator function for aggregate id that is command aware.
* @param {Function} fn The function to be injected.
* @returns {Aggregate} to be able to chain...
*/
defineCommandAwareAggregateIdGenerator: function (fn) {
if (fn.length === 0) throw new Error('Please define your function to accept the command!');
if (fn.length === 1) {
var orgFn = fn;
fn = function(cmd, callback) {
callback(null, orgFn(cmd));
};
}
this.getNewAggregateId = fn;
return this;
}
});
module.exports = Aggregate;