UNPKG

sort-eventstore

Version:

Node-eventstore is a node.js module for multiple databases. It can be very useful as eventstore if you work with (d)ddd, cqrs, eventsourcing, commands and events, etc.

633 lines (532 loc) 18.7 kB
var debug = require('debug')('eventstore'), util = require('util'), EventEmitter = require('events').EventEmitter, _ = require('lodash'), async = require('async'), tolerate = require('tolerance'), EventDispatcher = require('./eventDispatcher'), EventStream = require('./eventStream'), Snapshot = require('./snapshot'); /** * Eventstore constructor * @param {Object} options The options. * @param {Store} store The db implementation. * @constructor */ function Eventstore(options, store) { this.options = options || {}; this.store = store; this.eventMappings = {}; EventEmitter.call(this); } util.inherits(Eventstore, EventEmitter); _.extend(Eventstore.prototype, { /** * Inject function for event publishing. * @param {Function} fn the function to be injected * @returns {Eventstore} to be able to chain... */ useEventPublisher: function (fn) { if (fn.length === 1) { fn = _.wrap(fn, function(func, evt, callback) { func(evt); callback(null); }); } this.publisher = fn; return this; }, /** * Define which values should be mapped/copied to the payload event. [optional] * @param {Object} mappings the mappings in dotty notation * { * id: 'id', * commitId: 'commitId', * commitSequence: 'commitSequence', * commitStamp: 'commitStamp', * streamRevision: 'streamRevision' * } * @returns {Eventstore} to be able to chain... */ defineEventMappings: function (mappings) { if (!mappings || !_.isObject(mappings)) { var err = new Error('Please pass a valid mapping values!'); debug(err); throw err; } this.eventMappings = mappings; return this; }, /** * Call this function to initialize the eventstore. * If an event publisher function was injected it will additionally initialize an event dispatcher. * @param {Function} callback the function that will be called when this action has finished [optional] */ init: function (callback) { var self = this; function initDispatcher() { debug('init event dispatcher'); self.dispatcher = new EventDispatcher(self.publisher, self); self.dispatcher.start(callback); } this.store.on('connect', function () { self.emit('connect'); }); this.store.on('disconnect', function () { self.emit('disconnect'); }); process.nextTick(function() { tolerate(function(callback) { self.store.connect(callback); }, self.options.timeout || 0, function (err) { if (err) { debug(err); if (callback) callback(err); return; } if (!self.publisher) { debug('no publisher defined'); if (callback) callback(null); return; } initDispatcher(); }); }); }, // streaming api /** * streams the events * @param {Object || String} query the query object [optional] * @param {Number} skip how many events should be skipped? [optional] * @param {Number} limit how many events do you want in the result? [optional] * @returns {Stream} a stream with the events */ streamEvents: function (query, skip, limit) { if (!this.store.streamEvents) { throw new Error('Streaming API is not suppoted by '+(this.options.type || 'inmemory') +' db implementation.'); } if (typeof query === 'number') { limit = skip; skip = query; query = {}; }; if (typeof query === 'string') { query = { aggregateId: query }; } return this.store.streamEvents(query, skip, limit); }, /** * streams all the events since passed commitStamp * @param {Date} commitStamp the date object * @param {Number} skip how many events should be skipped? [optional] * @param {Number} limit how many events do you want in the result? [optional] * @returns {Stream} a stream with the events */ streamEventsSince: function (commitStamp, skip, limit) { if (!this.store.streamEvents) { throw new Error('Streaming API is not suppoted by '+(this.options.type || 'inmemory') +' db implementation.'); } if (!commitStamp) { var err = new Error('Please pass in a date object!'); debug(err); throw err; } var self = this; commitStamp = new Date(commitStamp); return this.store.streamEventsSince(commitStamp, skip, limit); }, /** * stream events by revision * @param {Object || String} query the query object * @param {Number} revMin revision start point [optional] * @param {Number} revMax revision end point (hint: -1 = to end) [optional] * @returns {Stream} a stream with the events */ streamEventsByRevision: function (query, revMin, revMax) { if (typeof query === 'string') { query = { aggregateId: query }; } if (!query.aggregateId) { var err = new Error('An aggregateId should be passed!'); debug(err); if (callback) callback(err); return; } return this.store.streamEventsByRevision(query, revMin, revMax); }, /** * loads the events * @param {Object || String} query the query object [optional] * @param {Number} skip how many events should be skipped? [optional] * @param {Number} limit how many events do you want in the result? [optional] * @param {Function} callback the function that will be called when this action has finished * `function(err, events){}` */ getEvents: function (query, skip, limit, callback) { if (typeof query === 'function') { callback = query; skip = 0; limit = -1; query = {}; } else if (typeof skip === 'function') { callback = skip; skip = 0; limit = -1; if (typeof query === 'number') { skip = query; query = {}; } } else if (typeof limit === 'function') { callback = limit; limit = -1; if (typeof query === 'number') { limit = skip; skip = query; query = {}; } } if (typeof query === 'string') { query = { aggregateId: query }; } var self = this; function nextFn(callback) { if (limit < 0) { var resEvts = []; resEvts.next = nextFn; return process.nextTick(function () { callback(null, resEvts) }); } skip += limit; _getEvents(query, skip, limit, callback); } function _getEvents(query, skip, limit, callback) { self.store.getEvents(query, skip, limit, function (err, evts) { if (err) return callback(err); evts.next = nextFn; callback(null, evts); }); } _getEvents(query, skip, limit, callback); }, /** * loads all the events since passed commitStamp * @param {Date} commitStamp the date object * @param {Number} skip how many events should be skipped? [optional] * @param {Number} limit how many events do you want in the result? [optional] * @param {Function} callback the function that will be called when this action has finished * `function(err, events){}` */ getEventsSince: function (commitStamp, skip, limit, callback) { if (!commitStamp) { var err = new Error('Please pass in a date object!'); debug(err); throw err; } if (typeof skip === 'function') { callback = skip; skip = 0; limit = -1; } else if (typeof limit === 'function') { callback = limit; limit = -1; } var self = this; function nextFn(callback) { if (limit < 0) { var resEvts = []; resEvts.next = nextFn; return process.nextTick(function () { callback(null, resEvts) }); } skip += limit; _getEventsSince(commitStamp, skip, limit, callback); } commitStamp = new Date(commitStamp); function _getEventsSince(commitStamp, skip, limit, callback) { self.store.getEventsSince(commitStamp, skip, limit, function (err, evts) { if (err) return callback(err); evts.next = nextFn; callback(null, evts); }); } _getEventsSince(commitStamp, skip, limit, callback); }, /** * loads the events * @param {Object || String} query the query object * @param {Number} revMin revision start point [optional] * @param {Number} revMax revision end point (hint: -1 = to end) [optional] * @param {Function} callback the function that will be called when this action has finished * `function(err, events){}` */ getEventsByRevision: function (query, revMin, revMax, callback) { if (typeof revMin === 'function') { callback = revMin; revMin = 0; revMax = -1; } else if (typeof revMax === 'function') { callback = revMax; revMax = -1; } if (typeof query === 'string') { query = { aggregateId: query }; } if (!query.aggregateId) { var err = new Error('An aggregateId should be passed!'); debug(err); if (callback) callback(err); return; } this.store.getEventsByRevision(query, revMin, revMax, callback); }, /** * loads the event stream * @param {Object || String} query the query object * @param {Number} revMin revision start point [optional] * @param {Number} revMax revision end point (hint: -1 = to end) [optional] * @param {Function} callback the function that will be called when this action has finished * `function(err, eventstream){}` */ getEventStream: function (query, revMin, revMax, callback) { if (typeof revMin === 'function') { callback = revMin; revMin = 0; revMax = -1; } else if (typeof revMax === 'function') { callback = revMax; revMax = -1; } if (typeof query === 'string') { query = { aggregateId: query }; } if (!query.aggregateId) { var err = new Error('An aggregateId should be passed!'); debug(err); if (callback) callback(err); return; } var self = this; this.getEventsByRevision(query, revMin, revMax, function(err, evts) { if (err) { return callback(err); } callback(null, new EventStream(self, query, evts)); }); }, /** * loads the next snapshot back from given max revision * @param {Object || String} query the query object * @param {Number} revMax revision end point (hint: -1 = to end) [optional] * @param {Function} callback the function that will be called when this action has finished * `function(err, snapshot, eventstream){}` */ getFromSnapshot: function (query, revMax, callback) { if (typeof revMax === 'function') { callback = revMax; revMax = -1; } if (typeof query === 'string') { query = { aggregateId: query }; } if (!query.aggregateId) { var err = new Error('An aggregateId should be passed!'); debug(err); if (callback) callback(err); return; } var self = this; async.waterfall([ function getSnapshot(callback) { self.store.getSnapshot(query, revMax, callback); }, function getEventStream(snap, callback) { var rev = 0; if (snap && (snap.revision !== undefined && snap.revision !== null)) { rev = snap.revision + 1; } self.getEventStream(query, rev, revMax, function(err, stream) { if (err) { return callback(err); } if (rev > 0 && stream.lastRevision == -1) { stream.lastRevision = snap.revision; } callback(null, snap, stream); }); }], callback ); }, /** * stores a new snapshot * @param {Object} obj the snapshot data * @param {Function} callback the function that will be called when this action has finished [optional] */ createSnapshot: function(obj, callback) { if (obj.streamId && !obj.aggregateId) { obj.aggregateId = obj.streamId; } if (!obj.aggregateId) { var err = new Error('An aggregateId should be passed!'); debug(err); if (callback) callback(err); return; } obj.streamId = obj.aggregateId; if (obj.revision) { if (typeof (obj.revision) === 'string') { const castedRevision = parseFloat(obj.revision); if (castedRevision && castedRevision.toString() === obj.revision) { // Determines if the revision was parsed correctly, for the cases where user using custom typed revisions that's not in valid float format like: obj.revision = '1,2,3' obj.revision = castedRevision; } } } var self = this; async.waterfall([ function getNewIdFromStorage(callback) { self.getNewId(callback); }, function commit(id, callback) { try { var snap = new Snapshot(id, obj); snap.commitStamp = new Date(); } catch (err) { return callback(err); } self.store.addSnapshot(snap, function(error) { if (self.options.maxSnapshotsCount) { self.store.cleanSnapshots(_.pick(obj, 'aggregateId', 'aggregate', 'context'), callback); } else { callback(error); } }); }], callback ); }, /** * commits all uncommittedEvents in the eventstream * @param eventstream the eventstream that should be saved (hint: directly use the commit function on eventstream) * @param {Function} callback the function that will be called when this action has finished * `function(err, eventstream){}` (hint: eventstream.eventsToDispatch) */ commit: function(eventstream, callback) { var self = this; async.waterfall([ function getNewCommitId(callback) { self.getNewId(callback); }, function commitEvents(id, callback) { // start committing. var event, currentRevision = eventstream.currentRevision(), uncommittedEvents = [].concat(eventstream.uncommittedEvents); eventstream.uncommittedEvents = []; self.store.getNextPositions(uncommittedEvents.length, function(err, positions) { if (err) return callback(err) for (var i = 0, len = uncommittedEvents.length; i < len; i++) { event = uncommittedEvents[i]; event.id = id + i.toString(); event.commitId = id; event.commitSequence = i; event.restInCommitStream = len - 1 - i; event.commitStamp = new Date(); currentRevision++; event.streamRevision = currentRevision; if (positions) event.position = positions[i]; event.applyMappings(); } self.store.addEvents(uncommittedEvents, function(err) { if (err) { // add uncommitted events back to eventstream eventstream.uncommittedEvents = uncommittedEvents.concat(eventstream.uncommittedEvents); return callback(err); } if (self.publisher && self.dispatcher) { // push to undispatchedQueue self.dispatcher.addUndispatchedEvents(uncommittedEvents); } else { eventstream.eventsToDispatch = [].concat(uncommittedEvents); } // move uncommitted events to events eventstream.events = eventstream.events.concat(uncommittedEvents); eventstream.currentRevision(); callback(null, eventstream); }); }); }], callback ); }, /** * loads all undispatched events * @param {Object || String} query the query object [optional] * @param {Function} callback the function that will be called when this action has finished * `function(err, events){}` */ getUndispatchedEvents: function (query, callback) { if (!callback) { callback = query; query = null; } if (typeof query === 'string') { query = { aggregateId: query }; } this.store.getUndispatchedEvents(query, callback); }, /** * loads the last event * @param {Object || String} query the query object [optional] * @param {Function} callback the function that will be called when this action has finished * `function(err, event){}` */ getLastEvent: function (query, callback) { if (!callback) { callback = query; query = null; } if (typeof query === 'string') { query = { aggregateId: query }; } this.store.getLastEvent(query, callback); }, /** * loads the last event in a stream * @param {Object || String} query the query object [optional] * @param {Function} callback the function that will be called when this action has finished * `function(err, eventstream){}` */ getLastEventAsStream: function (query, callback) { if (!callback) { callback = query; query = null; } if (typeof query === 'string') { query = { aggregateId: query }; } var self = this; this.store.getLastEvent(query, function (err, evt) { if (err) return callback(err); callback(null, new EventStream(self, query, evt ? [evt] : [])); }); }, /** * Sets the given event to dispatched. * @param {Object || String} evtOrId the event object or its id * @param {Function} callback the function that will be called when this action has finished [optional] */ setEventToDispatched: function (evtOrId, callback) { if (typeof evtOrId === 'object') { evtOrId = evtOrId.id; } this.store.setEventToDispatched(evtOrId, callback); }, /** * loads a new id from store * @param {Function} callback the function that will be called when this action has finished */ getNewId: function (callback) { this.store.getNewId(callback); } }); module.exports = Eventstore;