UNPKG

@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.

684 lines (583 loc) 19.8 kB
var debug = require('debug')('domain'), async = require('async'), util = require('util'), EventEmitter = require('events').EventEmitter, _ = require('lodash'), eventstore = require('@orchestrated-io/eventstore'), aggregatelock = require('./lock'), commandBumper = require('./bumper'), customLoader = require('./structure/customLoader'), structureLoader = require('./structure/structureLoader'), attachLookupFunctions = require('./structure/treeExtender'), ValidationError = require('./errors/validationError'), BusinessRuleError = require('./errors/businessRuleError'), AggregateConcurrencyError = require('./errors/aggregateConcurrencyError'), AggregateDestroyedError = require('./errors/aggregateDestroyedError'), DuplicateCommandError = require('./errors/duplicateCommandError'), CommandDispatcher = require('./commandDispatcher'), uuid = require('uuid').v4, dotty = require('dotty'); function isValidEventStore (obj) { // TODO: check each method's signature? return !!obj && _.every([ obj.init, obj.on, obj.getNewId, obj.getFromSnapshot, obj.createSnapshot, obj.setEventToDispatched], function (o) { return _.isFunction(o); } ); } function createEventStore (options) { if ( _.isFunction(options)) { // This is a factory method. var eventStore = options(); if (!isValidEventStore(eventStore)) { var err = new Error('"options.eventStore" is not a valid event store factory'); debug(err); throw err; } return eventStore; } return eventstore(options); } function createStructureLoader (options) { if (options) { if (!_.isFunction(options)) { var err = new Error('"options.structureLoader" is not a valid structure loader method'); debug(err); throw err; }; return customLoader(options); } return structureLoader; } function isValidAggregateLock (obj) { // TODO: check each method's signature? return !!obj && _.every([ obj.connect, obj.disconnect, obj.on, obj.getNewId, obj.reserve, obj.getAll, obj.resolve], function (o) { return _.isFunction(o); } ); } function isValidCommandBumper (obj) { // TODO: check each method's signature? return !!obj && _.every([ obj.connect, obj.disconnect, obj.on, obj.getNewId, obj.add], function (o) { return _.isFunction(o); } ); } function createAggregateLock (options) { if (_.isFunction(options)) { // This is a factory method. var lock = options(); if (!isValidAggregateLock(lock)) { var err = new Error('"options.aggregateLock" is not a valid aggregate lock factory'); debug(err); throw err; } } return aggregatelock.create(options); } function createCommandBumper (options) { if (_.isFunction(options)) { // This is a factory method. var bumper = options(); if (!isValidCommandBumper(bumper)) { var err = new Error('"options.deduplication" is not a valid deduplication store factory'); debug(err); throw err; } } return commandBumper.create(options); } /** * Domain constructor * @param {Object} options The options. * @constructor */ function Domain(options) { EventEmitter.call(this); options = options || {}; if (!options.domainPath) { var err = new Error('Please provide domainPath or domainLoader in options'); debug(err); throw err; } options.retryOnConcurrencyTimeout = options.retryOnConcurrencyTimeout || 800; options.commandRejectedEventName = options.commandRejectedEventName || 'commandRejected'; options.snapshotThreshold = options.snapshotThreshold || 100; options.useLoaderExtensions = options.useLoaderExtensions || false; this.structureLoader = createStructureLoader(options.structureLoader); this.eventStore = createEventStore(options.eventStore); this.aggregateLock = createAggregateLock(options.aggregateLock); if (options.deduplication) { this.commandBumper = createCommandBumper(options.deduplication); } this.options = options; this.definitions = { command: { id: 'id', name: 'name', aggregateId: 'aggregate.id' // context: 'context.name', // optional // aggregate: 'aggregate.name', // optional // payload: 'payload', // optional // revision: 'revision', // optional // version: 'version', // optional // meta: 'meta' // optional (will be passed directly to corresponding event(s)) }, event: { correlationId: 'correlationId', id: 'id', name: 'name', aggregateId: 'aggregate.id', // context: 'context.name', // optional // aggregate: 'aggregate.name', // optional payload: 'payload', // optional revision: 'revision' // optional // version: 'version', // optional // meta: 'meta' // optional (will be passed directly from corresponding command) } }; this.idGenerator(function () { return uuid().toString(); }); this.onEvent(function (evt) { debug('emit:', evt); }); this.extendValidator(function (validator) { debug('no validator extension defined'); }); } util.inherits(Domain, EventEmitter); _.extend(Domain.prototype, { /** * Inject definition for command structure. * @param {Object} definition the definition to be injected * @returns {Domain} to be able to chain... */ defineCommand: function (definition) { if (!definition || !_.isObject(definition)) { var err = new Error('Please pass a valid definition!'); debug(err); throw err; } this.definitions.command = _.defaults(definition, this.definitions.command); return this; }, /** * Inject definition for event structure. * @param {Object} definition the definition to be injected * @returns {Domain} to be able to chain... */ defineEvent: function (definition) { if (!definition || !_.isObject(definition)) { var err = new Error('Please pass a valid definition!'); debug(err); throw err; } this.definitions.event = _.defaults(definition, this.definitions.event); if (this.definitions.event.commitStamp || this.definitions.event.position) { var mappings = {}; if (this.definitions.event.commitStamp) { mappings.commitStamp = this.definitions.event.commitStamp; } if (this.definitions.event.position) { mappings.position = this.definitions.event.position; } this.eventStore.defineEventMappings(mappings); } return this; }, /** * Inject idGenerator function. * @param {Function} fn The function to be injected. * @returns {Domain} to be able to chain... */ idGenerator: function (fn) { if (!fn || !_.isFunction(fn)) { var err = new Error('Please pass a valid function!'); debug(err); throw err; } if (fn.length === 1) { this.getNewId = fn; return this; } this.getNewId = function (callback) { callback(null, fn()); }; return this; }, /** * Inject idGenerator function for aggregate id. * @param {Function} fn The function to be injected. * @returns {Domain} to be able to chain... */ aggregateIdGenerator: function (fn) { if (!fn || !_.isFunction(fn)) { var err = new Error('Please pass a valid function!'); debug(err); throw err; } if (fn.length === 1) { this.getNewAggregateId = fn; return this; } this.getNewAggregateId = function (callback) { callback(null, fn()); }; return this; }, /** * Inject custom structure loader function * @param {Function} fn The function to be injected. * @returns {Domain} to be able to chain... */ defineStructureLoader: function (fn) { if (!fn || !_.isFunction(fn)) { var err = new Error('Please pass a valid function!'); debug(err); throw err; } this.structureLoader = customLoader(fn); return this; }, /** * Inject function for for event notification. * @param {Function} fn the function to be injected * @returns {Domain} to be able to chain... */ onEvent: function (fn) { if (!fn || !_.isFunction(fn)) { var err = new Error('Please pass a valid function!'); debug(err); throw err; } if (fn.length === 1) { fn = _.wrap(fn, function(func, evt, callback) { func(evt); callback(null); }); } this.onEventHandle = fn; return this; }, /** * Converts an error to the commandRejected event * @param {Object} cmd The command that was handled. * @param {Error} err The error that occurs. * @returns {Object} The resulting event. */ createCommandRejectedEvent: function (cmd, err) { if (!cmd || !_.isObject(cmd)) { var err = new Error('Please pass a valid command!'); debug(err); throw err; } if (!err || !_.isObject(err)) { var err = new Error('Please pass a valid error!'); debug(err); throw err; } var evt = {}; if (!!this.definitions.command.meta && !!this.definitions.event.meta) { dotty.put(evt, this.definitions.event.meta, dotty.get(cmd, this.definitions.command.meta)); } dotty.put(evt, this.definitions.event.correlationId, dotty.get(cmd, this.definitions.command.id)); dotty.put(evt, this.definitions.event.name, this.options.commandRejectedEventName); dotty.put(evt, this.definitions.event.id, dotty.get(cmd, this.definitions.command.id) + '_rejected'); dotty.put(evt, this.definitions.event.aggregateId, dotty.get(cmd, this.definitions.command.aggregateId)); if (!!this.definitions.command.aggregate && !!this.definitions.event.aggregate) { dotty.put(evt, this.definitions.event.aggregate, dotty.get(cmd, this.definitions.command.aggregate)); } if (!!this.definitions.command.context && !!this.definitions.event.context) { dotty.put(evt, this.definitions.event.context, dotty.get(cmd, this.definitions.command.context)); } if (err instanceof ValidationError || err instanceof BusinessRuleError || err instanceof AggregateDestroyedError || err instanceof AggregateConcurrencyError || err instanceof DuplicateCommandError) { dotty.put(evt, this.definitions.event.payload, { command: cmd, reason: { name: err.name, message: err.message, more: err.more } }); if (!!this.definitions.event.revision && dotty.get(evt, this.definitions.event.revision) === undefined && err.more && err.more.aggregateRevision > -1) { dotty.put(evt, this.definitions.event.revision, err.more.aggregateRevision); } } else { evt = null; } return evt; }, /** * Returns the domain information. * @returns {Object} */ getInfo: function () { if (!this.tree) { var err = new Error('Not initialized!'); debug(err); throw err; } return this.tree.getInfo(); }, /** * Extends the validator instance. * @param {Function} fn the function to be injected * @returns {Domain} to be able to chain... */ extendValidator: function (fn) { if (!fn || !_.isFunction(fn) || fn.length !== 1) { var err = new Error('Please pass a valid function!'); debug(err); throw err; } this.validatorExtension = fn; return this; }, /** * Call this function to initialize the domain. * @param {Function} callback the function that will be called when this action has finished [optional] * `function(err, warnings){}` */ init: function (callback) { var self = this; var warnings = null; async.series([ // load domain files... function (callback) { debug('load domain files..'); self.structureLoader(self.options.domainPath, self.validatorExtension, self.options.useLoaderExtensions, function (err, tree, warns) { if (err) { return callback(err); } warnings = warns; if (!tree || _.isEmpty(tree)) return callback(new Error('No structure loaded for ' + self.options.domainPath + '!')); self.tree = attachLookupFunctions(tree); callback(null); }); }, // prepare infrastructure... function (callback) { debug('prepare infrastructure...'); async.parallel([ // prepare eventStore... function (callback) { debug('prepare eventStore...'); self.eventStore.on('connect', function () { self.emit('connect'); }); self.eventStore.on('disconnect', function () { self.emit('disconnect'); }); self.eventStore.init(callback); }, // prepare aggregateLock... function (callback) { debug('prepare aggregateLock...'); self.aggregateLock.on('connect', function () { self.emit('connect'); }); self.aggregateLock.on('disconnect', function () { self.emit('disconnect'); }); self.aggregateLock.connect(callback); }, // prepare commandBumper... function (callback) { if (!self.commandBumper) { return callback(null); } debug('prepare commandBumper...'); self.commandBumper.on('connect', function () { self.emit('connect'); }); self.commandBumper.on('disconnect', function () { self.emit('disconnect'); }); self.commandBumper.connect(callback); } ], callback); }, // inject all needed dependencies... function (callback) { debug('inject all needed dependencies...'); self.commandDispatcher = new CommandDispatcher(self.tree, self.definitions.command, self.commandBumper); self.tree.defineOptions({ retryOnConcurrencyTimeout: self.options.retryOnConcurrencyTimeout, snapshotThreshold: self.options.snapshotThreshold, snapshotThresholdMs: self.options.snapshotThresholdMs }) .defineCommand(self.definitions.command) .defineEvent(self.definitions.event) .idGenerator(self.getNewId) .useEventStore(self.eventStore) .useAggregateLock(self.aggregateLock); if (self.getNewAggregateId) { self.tree.aggregateIdGenerator(self.getNewAggregateId); } callback(null); } ], function (err) { if (err) { debug(err); } else { debug('domain inited'); } if (callback) callback(err, warnings); }); }, /** * Is called when dispatched a command. * @param {Object} cmd the command object * @param {Error} err the error * @param {Array} eventsToDispatch the events to dispatch * @param {Object} aggregateData the aggregate data * @param {Object} meta the meta infos * @param {Function} callback the function that will be called when this action has finished [optional] * `function(err, evts, aggregateData, meta){}` evts is of type Array, aggregateData and meta are an object */ onDispatched: function (cmd, err, eventsToDispatch, aggregateData, meta, callback) { var self = this; if (err) { debug(err); var cmdRejEvt = this.createCommandRejectedEvent(cmd, err); if (cmdRejEvt) { this.onEventHandle(cmdRejEvt, function (err) { if (err) { debug(err); } }); if (callback) { try { callback(err, [cmdRejEvt], aggregateData || null, meta || null); } catch (e) { debug(e); console.log(e.stack); process.emit('uncaughtException', e); } } return; } if (callback) { try { callback(err, null, aggregateData, meta); } catch (e) { debug(e); console.log(e.stack); process.emit('uncaughtException', e); } } return; } var evts = []; if (!eventsToDispatch || !_.isArray(eventsToDispatch)) { debug('seams to be something from a custom command handler'); if (callback) { try { callback.apply(callback, _.toArray(arguments)); } catch (e) { debug(e); console.log(e.stack); process.emit('uncaughtException', e); } } return; } async.each(eventsToDispatch, function (evt, callback) { function setEventToDispatched (e, clb) { if (!e.payload || !e.id || e.disablePersistence) { evts.push(e.payload || e); return callback(null); } debug('set event to dispatched'); self.eventStore.setEventToDispatched(e, function (err) { if (err) { return callback(err); } evts.push(e.payload); clb(null); }); } if (self.onEventHandle) { debug('publish an event'); if (evt.payload && evt.id) { self.onEventHandle(evt.payload, function (err) { if (err) { debug(err); return callback(err); } setEventToDispatched(evt, function (err) { if (err) { return callback(err); } callback(null); }); }); } else { // seams that custom command handler has done some strange stuff!!! self.onEventHandle(evt, function (err) { if (err) { debug(err); return callback(err); } evts.push(evt); callback(null); }); } } else { setEventToDispatched(evt, callback); } }, function (err) { if (err) { debug(err); } if (callback) { try { callback(err, evts, aggregateData, meta); } catch (e) { debug(e); console.log(e.stack); process.emit('uncaughtException', e); } } }); }, /** * Call this function to let the domain handle it. * @param {Object} cmd the command object * @param {Function} callback the function that will be called when this action has finished [optional] * `function(err, evts, aggregateData, meta){}` evts is of type Array, aggregateData and meta are an object */ handle: function (cmd, callback) { if (!cmd || !_.isObject(cmd) || !dotty.exists(cmd, this.definitions.command.name)) { var err = new Error('Please pass a valid command!'); debug(err); if (callback) callback(err); return; } var self = this; process.nextTick(function () { try { if (callback && callback.length > 2) { self.commandDispatcher.dispatch(cmd, function (err, eventsToDispatch, aggregateData, meta) { self.onDispatched(cmd, err, eventsToDispatch, aggregateData, meta, callback); }); return; } self.commandDispatcher.dispatch(cmd, function (err, eventsToDispatch) { self.onDispatched(cmd, err, eventsToDispatch, null, null, callback); }); } catch (e) { if (callback) callback(e); } }); } }); module.exports = Domain;