wr-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.
596 lines (498 loc) • 19 kB
JavaScript
var util = require('util'),
Store = require('../base'),
_ = require('lodash'),
async = require('async'),
azure = Store.use('azure-storage'),
eg = azure.TableUtilities.entityGenerator,
debug = require('debug')('eventstore:store:azuretable');
function AzureTable(options) {
options = options || {};
var azureConf = {
storageAccount: 'nodeeventstore',
storageAccessKey: 'aXJaod96t980AbNwG9Vh6T3ewPQnvMWAn289Wft9RTv+heXQBxLsY3Z4w66CI7NN12+1HUnHM8S3sUbcI5zctg==',
storageTableHost: 'https://nodeeventstore.table.core.windows.net/'
};
this.options = _.defaults(options, azureConf);
var defaults = {
eventsTableName: 'events',
undispatchedEventsTableName: 'undispatchedevents',
snapshotsTableName: 'snapshots'
};
this.options = _.defaults(this.options, defaults);
}
util.inherits(AzureTable, Store);
_.extend(AzureTable.prototype, {
connect: function (callback) {
var self = this;
var retryOperations = new azure.ExponentialRetryPolicyFilter();
var server = azure.createTableService(this.options.storageAccount, this.options.storageAccessKey, this.options.storageTableHost).withFilter(retryOperations);
self.client = server;
self.isConnected = true;
var createEventsTable = function (callback) {
self.client.createTableIfNotExists(self.options.eventsTableName, callback);
};
var createSnapshotTable = function (callback) {
self.client.createTableIfNotExists(self.options.snapshotsTableName, callback);
};
var createUndispatchedEventsTable = function (done) {
self.client.createTableIfNotExists(self.options.undispatchedEventsTableName, done)
};
async.parallel([
createEventsTable,
createSnapshotTable,
createUndispatchedEventsTable
], function (err) {
if (err) {
if (callback) callback(err);
} else {
self.emit('connect');
if (callback) callback(null, self);
}
});
},
disconnect: function (callback) {
this.emit('disconnect');
if (callback) callback(null);
},
clear: function (done) {
var self = this;
var query = new azure.TableQuery();
var clearEventsTable = function (callback) {
self.client.queryEntities(self.options.eventsTableName, query, null, function (err, entities) {
if (err) {
return callback(err);
}
async.each(entities.entries, function (entity, callback) {
self.client.deleteEntity(self.options.eventsTableName, entity, function (error, response) {
callback(error);
});
},
callback);
});
};
var clearSnapshotsTable = function (callback) {
self.client.queryEntities(self.options.snapshotsTableName, query, null, function (err, entities) {
if (err) {
return callback(err);
}
async.each(entities.entries, function (entity, callback) {
self.client.deleteEntity(self.options.snapshotsTableName, entity, function (error, response) {
callback(error);
});
},
callback);
});
};
var clearUndispatchedEventsTable = function (callback) {
self.client.queryEntities(self.options.undispatchedEventsTableName, query, null, function (err, entities) {
if (err) {
return callback(err);
}
async.each(entities.entries, function (entity, callback) {
self.client.deleteEntity(self.options.undispatchedEventsTableName, entity, function (error, response) {
callback(error);
});
},
callback);
});
};
async.parallel([
clearEventsTable,
clearSnapshotsTable,
clearUndispatchedEventsTable
], function (err) {
if (err) {
debug(err);
if (done) done(err);
// if (done) done(null); // strange on azure...
return;
}
if (done) done(null, self);
});
},
addEvents: function (events, callback) {
var self = this;
var batch = new azure.TableBatch();
var noAggId = _.every(events, function (event) {
return !event.aggregateId
});
if (noAggId) {
var errMsg = 'aggregateId not defined!';
debug(errMsg);
if (callback) callback(new Error(errMsg));
return;
}
async.concat(events,
function (event, callback) {
var results = [
function (callback) {
var storedEvent = new StoredEvent(event);
self.client.insertEntity(self.options.eventsTableName, storedEvent, callback);
},
function (callback) {
var storedUndispatchedEvent = new StoredUndispatchedEvent(event);
self.client.insertEntity(self.options.undispatchedEventsTableName, storedUndispatchedEvent, callback);
}
]
callback(null, results);
},
function (err, results) {
async.parallel(results, callback);
}
);
},
getEvents: function (query, skip, limit, callback) {
var self = this;
var tableQuery = new azure.TableQuery();
var continuationToken = null;
var entities = [];
var pageSize = skip + limit;
tableQuery = _(query)
.pick(['aggregate', 'context', 'aggregateId'])
.reduce(function (result, val, key) {
key = key === 'aggregateId' ? 'PartitionKey' : key;
if (result._where.length === 0) return tableQuery.where(key + ' eq ?', val);
return result.and(key + ' eq ?', val)
}, tableQuery);
if (limit !== -1) {
tableQuery = tableQuery.top(pageSize);
}
async.doWhilst(function (end) {
// retrieve entities
self.client.queryEntities(self.options.eventsTableName, tableQuery, continuationToken, function (err, results) {
if (err) {
debug(err);
return end(err);
}
continuationToken = results.continuationToken;
entities = entities.concat(results.entries);
end(null);
});
}, function () {
// test if we need to load more
return (entities.length < pageSize || pageSize == -1) ? continuationToken !== null : false;
}, function (err) {
// return results
if (err) {
debug(err);
return callback(err);
}
entities = entities.map(MapStoredEventToEvent);
entities = _.sortBy(entities, function (e) {
return e.commitStamp.getTime();
});
if (limit === -1) {
entities = entities.slice(skip);
} else {
entities = entities.slice(skip, skip + limit);
}
callback(null, entities);
});
},
getEventsSince: function (date, skip, limit, callback) {
var self = this;
var tableQuery = new azure.TableQuery();
var continuationToken = null;
var entities = [];
var pageSize = skip + limit;
tableQuery.where('commitStamp >= ?', date);
async.doWhilst(function (end) {
// retrieve entities
self.client.queryEntities(self.options.eventsTableName, tableQuery, continuationToken, function (err, results) {
if (err) {
debug(err);
return end(err);
}
continuationToken = results.continuationToken;
entities = entities.concat(results.entries);
end(null);
});
}, function () {
// test if we need to load more
return (entities.length < pageSize || pageSize == -1) ? continuationToken !== null : false;
}, function (err) {
// return results
if (err) {
debug(err);
return callback(err);
}
entities = entities.map(MapStoredEventToEvent);
entities = _.sortBy(entities, function (e) {
return e.commitStamp.getTime();
});
if (limit === -1) {
entities = entities.slice(skip);
} else {
entities = entities.slice(skip, pageSize);
}
callback(null, entities);
});
},
getEventsByRevision: function (query, revMin, revMax, callback) {
var self = this;
var tableQuery = new azure.TableQuery();
var continuationToken = null;
var entities = [];
if (!query.aggregateId) {
var errMsg = 'aggregateId not defined!';
debug(errMsg);
if (callback) callback(new Error(errMsg));
return;
}
tableQuery = _(query)
.pick(['aggregate', 'context', 'aggregateId'])
.reduce(function (result, val, key) {
key = key === 'aggregateId' ? 'PartitionKey' : key;
if (result._where.length === 0) return tableQuery.where(key + ' eq ?', val);
return result.and(key + ' eq ?', val)
}, tableQuery);
tableQuery = tableQuery.and('streamRevision >= ?', revMin);
if (revMax != -1) tableQuery = tableQuery.and('streamRevision < ?', revMax);
async.doWhilst(function (end) {
// retrieve entities
self.client.queryEntities(self.options.eventsTableName, tableQuery, continuationToken, function (err, results) {
if (err) {
debug(err);
return end(err);
}
continuationToken = results.continuationToken;
entities = entities.concat(results.entries);
end(null);
});
}, function () {
// test if we need to load more
return continuationToken !== null;
}, function (err) {
// return results
if (err) {
debug(err);
return callback(err);
}
entities = entities.map(MapStoredEventToEvent);
entities = _.sortBy(entities, function (e) {
return e.commitStamp.getTime();
});
callback(null, entities);
});
},
getUndispatchedEvents: function (query, callback) {
var self = this;
var tableQuery = new azure.TableQuery();
var continuationToken = null;
var entities = [];
if (query && query.aggregate) tableQuery.where('aggregate eq ?', query.aggregate);
if (query && query.context) tableQuery.where('context eq ?', query.context);
if (query && query.aggregateId) tableQuery.where('aggregateId eq ?', query.aggregateId);
async.doWhilst(function (end) {
// retrieve entities
self.client.queryEntities(self.options.undispatchedEventsTableName, tableQuery, continuationToken, function (err, results) {
if (err) {
debug(err);
return end(err);
}
continuationToken = results.continuationToken;
entities = entities.concat(results.entries);
end(null);
});
}, function () {
// test if we need to load more
return continuationToken !== null;
}, function (err) {
// return results
if (err) {
debug(err);
return callback(err);
}
entities = entities.map(MapStoredEventToEvent);
entities = _.sortBy(entities, function (e) {
return e.commitStamp.getTime();
});
callback(null, entities);
});
},
getLastEvent: function (query, callback) {
var self = this;
var tableQuery = new azure.TableQuery();
var continuationToken = null;
var entities = [];
tableQuery = _(query)
.pick(['aggregate', 'context', 'aggregateId'])
.reduce(function (result, val, key) {
key = key === 'aggregateId' ? 'PartitionKey' : key;
if (result._where.length === 0) return tableQuery.where(key + ' eq ?', val);
return result.and(key + ' eq ?', val)
}, tableQuery);
async.doWhilst(function (end) {
// retrieve entities
self.client.queryEntities(self.options.eventsTableName, tableQuery, continuationToken, function (err, results) {
if (err) {
debug(err);
return end(err);
}
continuationToken = results.continuationToken;
entities = entities.concat(results.entries);
end(null);
});
}, function () {
// test if we need to load more
return continuationToken !== null;
}, function (err) {
// return results
if (err) {
debug(err);
return callback(err);
}
entities = entities.map(MapStoredEventToEvent);
entities = _.sortBy(entities, function (e) {
return [e.commitStamp.getTime(),
e.streamRevision,
e.commitSequence];
}).reverse();
callback(null, entities[0]);
});
//this.events.findOne(findStatement, {sort: [['commitStamp', 'desc'], ['streamRevision', 'desc'], ['commitSequence', 'desc']]}, callback);
},
setEventToDispatched: function (id, callback) {
var self = this;
var objDescriptor = {
PartitionKey: eg.String(id),
RowKey: eg.String(id)
};
self.client.deleteEntity(self.options.undispatchedEventsTableName, objDescriptor, null, callback);
},
addSnapshot: function (snap, callback) {
var self = this;
if (!snap.aggregateId) {
var errMsg = 'aggregateId not defined!';
debug(errMsg);
if (callback) callback(new Error(errMsg));
return;
}
self.client.insertEntity(self.options.snapshotsTableName, new StoredSnapshot(snap), callback);
},
getSnapshot: function (query, revMax, callback) {
var self = this;
var tableQuery = new azure.TableQuery();
var continuationToken = null;
var entities = [];
if (!query.aggregateId) {
var errMsg = 'aggregateId not defined!';
debug(errMsg);
if (callback) callback(new Error(errMsg));
return;
}
tableQuery = _(query)
.pick(['aggregate', 'context', 'aggregateId'])
.reduce(function (result, val, key) {
key = key === 'aggregateId' ? 'PartitionKey' : key;
if (result._where.length === 0) return tableQuery.where(key + ' eq ?', val);
return result.and(key + ' eq ?', val)
}, tableQuery);
if (revMax != -1) tableQuery = tableQuery.and('revision le ?', revMax);
async.doWhilst(function (end) {
// retrieve entities
self.client.queryEntities(self.options.snapshotsTableName, tableQuery, continuationToken, function (err, results) {
if (err) {
debug(err);
return end(err);
}
continuationToken = results.continuationToken;
entities = entities.concat(results.entries);
end(null);
});
}, function () {
// test if we need to load more
return continuationToken !== null;
}, function (err) {
// return results
if (err) {
debug(err);
return callback(err);
}
entities = entities.map(MapStoredSnapshotToSnapshot);
entities = _.sortBy(entities, function (e) {
return -e.commitStamp.getTime();
});
callback(null, entities[0]);
});
}
});
var StoredEvent = function (event) {
this.PartitionKey = eg.EntityProperty(event.aggregateId);
this.RowKey = eg.EntityProperty(event.id);
this.aggregateId = eg.EntityProperty(event.aggregateId);
this.id = eg.EntityProperty(event.id);
this.context = eg.EntityProperty(event.context);
this.aggregate = eg.EntityProperty(event.aggregate);
this.streamRevision = eg.EntityProperty(event.streamRevision);
this.commitId = eg.EntityProperty(event.commitId);
this.commitSequence = eg.EntityProperty(event.commitSequence);
this.commitStamp = eg.EntityProperty(event.commitStamp);
this.header = eg.EntityProperty(event.header);
this.dispatched = eg.EntityProperty(event.dispatched || false);
this.payload = eg.EntityProperty(JSON.stringify(event.payload));
};
function MapStoredEventToEvent(storedEvent) {
var event = {
aggregateId: getEntityProperty(storedEvent.aggregateId),
id: getEntityProperty(storedEvent.id),
context: getEntityProperty(storedEvent.context),
aggregate: getEntityProperty(storedEvent.aggregate),
streamRevision: getEntityProperty(storedEvent.streamRevision),
commitId: getEntityProperty(storedEvent.commitId),
commitSequence: getEntityProperty(storedEvent.commitSequence),
commitStamp: getEntityProperty(storedEvent.commitStamp) || null,
header: getEntityProperty(storedEvent.header) || null,
dispatched: getEntityProperty(storedEvent.dispatched),
payload: JSON.parse(getEntityProperty(storedEvent.payload)) || null
};
return event;
}
var StoredUndispatchedEvent = function (event) {
this.PartitionKey = eg.EntityProperty(event.id);
this.RowKey = eg.EntityProperty(event.id);
this.aggregateId = eg.EntityProperty(event.aggregateId);
this.id = eg.EntityProperty(event.id);
this.context = eg.EntityProperty(event.context);
this.aggregate = eg.EntityProperty(event.aggregate);
this.streamRevision = eg.EntityProperty(event.streamRevision);
this.commitId = eg.EntityProperty(event.commitId);
this.commitSequence = eg.EntityProperty(event.commitSequence);
this.commitStamp = eg.EntityProperty(event.commitStamp);
this.header = eg.EntityProperty(event.header);
this.dispatched = eg.EntityProperty(event.dispatched || false);
this.payload = eg.EntityProperty(JSON.stringify(event.payload));
};
var StoredSnapshot = function (snapshot) {
this.PartitionKey = eg.EntityProperty(snapshot.aggregateId);
this.RowKey = eg.EntityProperty(snapshot.id);
this.id = eg.EntityProperty(snapshot.id);
this.aggregateId = eg.EntityProperty(snapshot.aggregateId);
this.aggregate = eg.EntityProperty(snapshot.aggregate) || undefined;
this.context = eg.EntityProperty(snapshot.context) || undefined;
this.revision = eg.EntityProperty(snapshot.revision);
this.version = eg.EntityProperty(snapshot.version);
this.commitStamp = eg.EntityProperty(snapshot.commitStamp);
this.data = eg.EntityProperty(JSON.stringify(snapshot.data));
};
function MapStoredSnapshotToSnapshot(storedSnapshot) {
var snapshot = {
id: getEntityProperty(storedSnapshot.id),
aggregateId: getEntityProperty(storedSnapshot.PartitionKey),
aggregate: getEntityProperty(storedSnapshot.aggregate) || undefined,
context: getEntityProperty(storedSnapshot.context) || undefined,
revision: getEntityProperty(storedSnapshot.revision),
version: getEntityProperty(storedSnapshot.version),
commitStamp: getEntityProperty(storedSnapshot.commitStamp),
data: JSON.parse(getEntityProperty(storedSnapshot.data)) || null
};
return snapshot;
}
var getEntityProperty = function (propertyField) {
if (propertyField != null) {
return propertyField['_'];
} else {
return null;
}
};
module.exports = AzureTable;