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.

515 lines (414 loc) 13.6 kB
var util = require('util'), Store = require('../base'), _ = require('lodash'), async = require('async'), tingodb = Store.use('tingodb')(), ObjectID = tingodb.ObjectID, debug = require('debug')('eventstore:store:tingodb'); function Tingo(options) { options = options || {}; Store.call(this, options); var defaults = { dbPath: require('path').join(__dirname, '../../'), eventsCollectionName: 'events', snapshotsCollectionName: 'snapshots', transactionsCollectionName: 'transactions' }; _.defaults(options, defaults); this.options = options; } util.inherits(Tingo, Store); _.extend(Tingo.prototype, { connect: function (callback) { var options = this.options; this.db = new tingodb.Db(options.dbPath, {}); // this.db.on('close', function() { // self.emit('disconnect'); // }); this.events = this.db.collection(options.eventsCollectionName + '.tingo'); this.events.ensureIndex({ aggregateId: 1, streamRevision: 1 }, function (err) { if (err) { debug(err); } }); this.events.ensureIndex({ commitStamp: 1 }, function (err) { if (err) { debug(err); } }); this.events.ensureIndex({ dispatched: 1 }, { sparse: true }, function (err) { if (err) { debug(err); } }); this.events.ensureIndex({ commitStamp: 1, streamRevision: 1, commitSequence: 1 }, function (err) { if (err) { debug(err); } }); this.events.ensureIndex({ aggregate: 1, aggregateId: 1, commitStamp: -1, streamRevision: -1, commitSequence: -1 }, function (err) { if (err) { debug(err); } }); this.snapshots = this.db.collection(options.snapshotsCollectionName + '.tingo'); this.snapshots.ensureIndex({ aggregateId: 1, revision: -1 }, function (err) { if (err) { debug(err); } }); this.transactions = this.db.collection(options.transactionsCollectionName + '.tingo'); this.transactions.ensureIndex({ aggregateId: 1, 'events.streamRevision': 1 }, function (err) { if (err) { debug(err); } }); this.emit('connect'); if (callback) callback(null, this); }, disconnect: function (callback) { if (!this.db) { if (callback) callback(null); return; } this.emit('disconnect'); this.db.close(callback || function () {}); }, clear: function (callback) { var self = this; async.parallel([ function (callback) { self.events.remove({}, callback); }, function (callback) { self.snapshots.remove({}, callback); }, function (callback) { self.transactions.remove({}, callback); } ], function (err) { if (err) { debug(err); } if (callback) callback(err); }); }, // getNewId: function(callback) { // callback(null, new ObjectID().toString()); // }, addEvents: function (events, callback) { var noAggId = false; _.each(events, function (evt) { if (!evt.aggregateId) { noAggId = true; } evt._id = evt.id; evt.dispatched = false; }); if (noAggId) { var errMsg = 'aggregateId not defined!'; debug(errMsg); if (callback) callback(new Error(errMsg)); return; } var self = this; if (events.length === 1) { return this.events.insert(events, callback); } var tx = { _id: events[0].commitId, events: events, aggregateId: events[0].aggregateId, aggregate: events[0].aggregate, context: events[0].context }; this.transactions.insert(tx, function (err) { if (err) { debug(err); if (callback) callback(err); return; } self.events.insert(events, function (err) { if (err) { debug(err); if (callback) callback(err); return; } self.removeTransactions(events[events.length - 1]); if (callback) { callback(null); } }); }); }, getEvents: function (query, skip, limit, callback) { var findStatement = {}; if (query.aggregate) { findStatement.aggregate = query.aggregate; } if (query.context) { findStatement.context = query.context; } if (query.aggregateId) { findStatement['$or'] = [ { aggregateId: query.aggregateId }, { streamId: query.aggregateId } // just for compatability of < 1.0.0 ]; } if (limit === -1) { return this.events.find(findStatement, { sort: [['commitStamp', 'asc'], ['streamRevision', 'asc'], ['commitSequence', 'asc']] }).skip(skip).toArray(callback); } this.events.find(findStatement, { sort: [['commitStamp', 'asc'], ['streamRevision', 'asc'], ['commitSequence', 'asc']] }).skip(skip).limit(limit).toArray(callback); }, getEventsSince: function (date, skip, limit, callback) { var findStatement = { commitStamp: { '$gte': date } }; if (limit === -1) { return this.events.find(findStatement, { sort: [['commitStamp', 'asc'], ['streamRevision', 'asc'], ['commitSequence', 'asc']] }).skip(skip).toArray(callback); } this.events.find(findStatement, { sort: [['commitStamp', 'asc'], ['streamRevision', 'asc'], ['commitSequence', 'asc']] }).skip(skip).limit(limit).toArray(callback); }, getEventsByRevision: function (query, revMin, revMax, callback) { if (!query.aggregateId) { var errMsg = 'aggregateId not defined!'; debug(errMsg); if (callback) callback(new Error(errMsg)); return; } var streamRevOptions = { '$gte': revMin, '$lt': revMax }; if (revMax == -1) { streamRevOptions = { '$gte': revMin }; } var findStatement = { '$or': [ { aggregateId: query.aggregateId }, { streamId: query.aggregateId } // just for compatability of < 1.0.0 ], streamRevision: streamRevOptions }; if (query.aggregate) { findStatement.aggregate = query.aggregate; } if (query.context) { findStatement.context = query.context; } var self = this; this.events.find(findStatement, { sort: [['commitStamp', 'asc'], ['streamRevision', 'asc'], ['commitSequence', 'asc']] }).toArray(function (err, res) { if (err) { debug(err); return callback(err); } if (!res || res.length === 0) { return callback(null, []); } var lastEvt = res[res.length - 1]; var txOk = (revMax === -1 && !lastEvt.restInCommitStream) || (revMax !== -1 && (lastEvt.streamRevision === revMax - 1 || !lastEvt.restInCommitStream)); if (txOk) { // the following is usually unnecessary self.removeTransactions(lastEvt); return callback(null, res); } self.repairFailedTransaction(lastEvt, function (err) { if (err) { if (err.message.indexOf('missing tx entry') >= 0) { return callback(null, res); } debug(err); return callback(err); } self.getEventsByRevision(query, revMin, revMax, callback); }); }); }, getUndispatchedEvents: function (query, callback) { var findStatement = { dispatched: false }; if (query && query.aggregate) { findStatement.aggregate = query.aggregate; } if (query && query.context) { findStatement.context = query.context; } if (query && query.aggregateId) { findStatement.aggregateId = query.aggregateId; } this.events.find(findStatement, { sort: [['commitStamp', 'asc'], ['streamRevision', 'asc'], ['commitSequence', 'asc']] }).toArray(callback); }, setEventToDispatched: function (id, callback) { var updateCommand = { '$unset' : { 'dispatched': null } }; this.events.update({'_id' : id}, updateCommand, callback); }, addSnapshot: function(snap, callback) { if (!snap.aggregateId) { var errMsg = 'aggregateId not defined!'; debug(errMsg); if (callback) callback(new Error(errMsg)); return; } snap._id = snap.id; this.snapshots.insert(snap, callback); }, cleanSnapshots: function (query, callback) { if (!query.aggregateId) { var errMsg = 'aggregateId not defined!'; debug(errMsg); if (callback) callback(new Error(errMsg)); return; } var findStatement = { '$or': [ { aggregateId: query.aggregateId }, { streamId: query.aggregateId } // just for compatability of < 1.0.0 ] }; if (query.aggregate) { findStatement.aggregate = query.aggregate; } if (query.context) { findStatement.context = query.context; } this.snapshots.find(findStatement, { sort: [['revision', 'desc'], ['version', 'desc'], ['commitStamp', 'desc']] }) .skip(this.options.maxSnapshotsCount) .toArray(removeElements(this.snapshots, callback)); }, getSnapshot: function (query, revMax, callback) { if (!query.aggregateId) { var errMsg = 'aggregateId not defined!'; debug(errMsg); if (callback) callback(new Error(errMsg)); return; } var findStatement = { '$or': [ { aggregateId: query.aggregateId }, { streamId: query.aggregateId } // just for compatability of < 1.0.0 ] }; if (query.aggregate) { findStatement.aggregate = query.aggregate; } if (query.context) { findStatement.context = query.context; } if (revMax > -1) { findStatement.revision = { '$lte': revMax }; } this.snapshots.findOne(findStatement, { sort: [['revision', 'desc'], ['version', 'desc'], ['commitStamp', 'desc']] }, callback); }, removeTransactions: function (evt, callback) { if (!evt.aggregateId) { var errMsg = 'aggregateId not defined!'; debug(errMsg); if (callback) callback(new Error(errMsg)); return; } var findStatement = { aggregateId: evt.aggregateId }; if (evt.aggregate) { findStatement.aggregate = evt.aggregate; } if (evt.context) { findStatement.context = evt.context; } // the following is usually unnecessary this.transactions.remove(findStatement, function (err) { if (err) { debug(err); } if (callback) { callback(err); } }); }, getPendingTransactions: function (callback) { var self = this; this.transactions.find({}).toArray(function (err, txs) { if (err) { debug(err); return callback(err); } if (txs.length === 0) { return callback(null, txs); } var goodTxs = []; async.map(txs, function (tx, clb) { var findStatement = { commitId: tx._id, aggregateId: tx.aggregateId }; if (tx.aggregate) { findStatement.aggregate = tx.aggregate; } if (tx.context) { findStatement.context = tx.context; } self.events.findOne(findStatement, function (err, evt) { if (err) { return clb(err); } if (evt) { goodTxs.push(evt); } else { self.transactions.remove({ _id: tx._id }, function (err) { if (err) { debug(err); } }); } clb(null); }); }, function (err) { if (err) { debug(err); return callback(err); } callback(null, goodTxs); }) }); }, getLastEvent: function (query, callback) { if (!query.aggregateId) { var errMsg = 'aggregateId not defined!'; debug(errMsg); if (callback) callback(new Error(errMsg)); return; } var findStatement = { aggregateId: query.aggregateId }; if (query.aggregate) { findStatement.aggregate = query.aggregate; } if (query.context) { findStatement.context = query.context; } this.events.findOne(findStatement, { sort: [['commitStamp', 'desc'], ['streamRevision', 'desc'], ['commitSequence', 'desc']] }, callback); }, repairFailedTransaction: function (lastEvt, callback) { var self = this; //var findStatement = { // aggregateId: lastEvt.aggregateId, // 'events.streamRevision': lastEvt.streamRevision + 1 //}; // //if (lastEvt.aggregate) { // findStatement.aggregate = lastEvt.aggregate; //} // //if (lastEvt.context) { // findStatement.context = lastEvt.context; //} //this.transactions.findOne(findStatement, function (err, tx) { this.transactions.findOne({ _id: lastEvt.commitId }, function (err, tx) { if (err) { debug(err); return callback(err); } if (!tx) { var err = new Error('missing tx entry for aggregate ' + lastEvt.aggregateId); debug(err); return callback(err); } var missingEvts = tx.events.slice(tx.events.length - lastEvt.restInCommitStream); self.events.insert(missingEvts, function (err) { if (err) { debug(err); return callback(err); } self.removeTransactions(lastEvt); callback(null); }); }); } }); function removeElements(collection, callback) { return function (error, elements) { if (error) { debug(error); return callback(error); } async.each(elements, function (element, callback) { try { collection.remove({_id: element._id}); callback(); } catch (error) { callback(error); } }, function(error) { callback(error, elements.length); }); } } module.exports = Tingo;