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.
1,196 lines (1,026 loc) • 37.2 kB
JavaScript
var util = require('util'),
Store = require('../base'),
_ = require('lodash'),
async = require('async'),
aws = Store.use('aws-sdk'),
dbg = require('debug');
var debug = dbg('eventstore:store:dynamodb'),
error = dbg("eventstore:store:dynamodb:error");
/*
for information on optimizing access patterns see: https://medium.com/building-timehop/one-year-of-dynamodb-at-timehop-f761d9fe5fa1
- query when possible
- scan when you will really be visiting all items (or almost all, ex: clear, or undispatched)
- secondary index when you need only a subset (instead of scan) and it will still be cheaper to run SELECT N+1 query (get partition keys from 2ndary then get full item using getItem(key) )
*/
function DynamoDB(options) {
options = options || {};
var awsConf = {
// don't put AWS credentials in files. Use IAM, ~/.aws/credentials with $AWS_PROFILE, or env vars
// see: http://docs.aws.amazon.com/AWSJavaScriptSDK/guide/node-configuring.html
// example using credentials file with $AWS_PROFILE:
// $ AWS_PROFILE=my-non-default-profile npm test
region: "us-west-2",
endpointConf: {}
};
// support setting a specific endpoint for dynamodb (ex: DynamoDB local)
// examples usage for testing:
// $ AWS_DYNAMODB_ENDPOINT=http://localhost:8000 npm test
if (process.env["AWS_DYNAMODB_ENDPOINT"]) {
awsConf.endpointConf = { endpoint: process.env["AWS_DYNAMODB_ENDPOINT"] };
}
this.options = _.defaults(options, awsConf);
var defaults = {
eventsTableName: 'events',
undispatchedEventsTableName: 'undispatchedevents',
snapshotsTableName: 'snapshots',
// 3 write units / 1 read unit for events & undispatched is just low enough
// to trigger throttling w/o hitting the 20 test timeout. Takes about 5 minutes to run storeTest.js.
UndispatchedEventsReadCapacityUnits: 1,
UndispatchedEventsWriteCapacityUnits: 3,
EventsReadCapacityUnits: 1,
EventsWriteCapacityUnits: 3,
SnapshotReadCapacityUnits: 1,
SnapshotWriteCapacityUnits: 1,
useUndispatchedEventsTable: true,
eventsTableStreamEnabled: false,
eventsTableStreamViewType: "NEW_IMAGE"
};
this.options = _.defaults(this.options, defaults);
}
util.inherits(DynamoDB, Store);
_.extend(DynamoDB.prototype, {
connect: function (callback) {
var self = this;
self.client = new aws.DynamoDB(self.options.endpointConf);
self.documentClient = new aws.DynamoDB.DocumentClient({ service: self.client });
self.isConnected = true;
var createEventsTable = function (callback) {
createTableIfNotExists(self.client, EventsTableDefinition(self.options), callback);
};
var createSnapshotTable = function (callback) {
createTableIfNotExists(self.client, SnapshotTableDefinition(self.options), callback);
};
var createUndispatchedEventsTable = function (done) {
if (self.options.useUndispatchedEventsTable) {
createTableIfNotExists(self.client, UndispatchedEventsTableDefinition(self.options), done)
} else {
done();
}
};
async.parallel([
createEventsTable,
createSnapshotTable,
createUndispatchedEventsTable
], function (err) {
if (err) {
error("connect error: " + err);
if (callback) callback(err);
} else {
self.emit('connect');
if (callback) callback(null, self);
}
});
},
disconnect: function (callback) {
this.emit('disconnect');
if (callback) callback(null);
},
removeTables: function (done) {
var self = this;
// AWS has a limit on the number of DynamoDB tables for an account. Let's clean up when we're done
debug("remove all tables created for testing");
deleteAllTempTables(self.client, self.options, function (err, result) {
if (err) {
error("removeTables error: " + err);
return done(err);
}
return done(null, result);
});
},
clear: function (done) {
var self = this;
var clearEvents = function (callback) {
clearEventTables(self.options, self.documentClient, function (err) {
if (err) {
error("clearEventTables error: " + err);
return callback(err);
}
callback(null, "events");
});
};
var clearSnapshots = function (callback) {
clearSnapshotsTable(self.options, self.documentClient, function (err) {
if (err) {
error("clearSnapshotsTable error: " + err);
return callback(err);
}
callback(null, "snapshots");
});
};
async.parallel([
clearEvents,
clearSnapshots
], function (err, data) {
if (err) {
error("removeTables error: " + err);
if (done) done(err);
return;
}
if (done) done(null, self);
});
},
addEvents: function (events, callback) {
var self = this;
var noAggId = _.every(events, function (event) {
return !event.aggregateId
});
if (noAggId) {
var errMsg = 'aggregateId not defined!';
error(errMsg);
if (callback) callback(new Error(errMsg));
return;
}
async.concatSeries(events,
function (event, callback) {
var results = [
function (callback) {
var storedEvent = {
TableName: self.options.eventsTableName,
Item: new StoredEvent(event),
ExpressionAttributeNames: {
"#name": "aggregate"
},
ConditionExpression: "attribute_not_exists(aggregateId) and attribute_not_exists(streamRevision) and attribute_not_exists(context) and attribute_not_exists(#name)"
};
debug("Saving event to events table: " + JSON.stringify(storedEvent, null, 2));
self.documentClient.put(storedEvent, function (err, data) {
if (err) {
error("dynamodb.addEvents error: " + JSON.stringify(err));
return callback(err);
} else {
debug("event saved");
callback(null, data);
}
});
}];
if (self.options.useUndispatchedEventsTable) {
debug("using undispatchedevents table");
results.push(function (callback) {
var storedEvent = {
TableName: self.options.undispatchedEventsTableName,
Item: new StoredEvent(event),
ConditionExpression: "attribute_not_exists(id)"
};
debug("Saving event to undispatchedevents table " + JSON.stringify(storedEvent, null, 2));
self.documentClient.put(storedEvent, function (err, data) {
if (err) {
error("dynamodb.addUndispatchedEvents error: " + JSON.stringify(err));
return callback(err);
} else {
debug("undispatched event saved");
callback(null, data);
}
});
});
}
callback(null, results);
},
function (err, results) {
if (err) {
error("addEvents error: " + JSON.stringify(err));
}
async.series(results, callback);
}
);
},
getEvents: function (query, skip, limit, callback) {
var self = this;
var client = new aws.DynamoDB.DocumentClient(self.options.endpointConf);
var exclusiveStartKey = null;
var entities = [];
var tableQuery = {
TableName: self.options.eventsTableName
};
var vals = {};
if (query && query.aggregateId) {
vals[":a"] = query.aggregateId;
tableQuery.KeyConditionExpression = "aggregateId = :a";
}
if (query && query.aggregate) {
vals[":name"] = query.aggregate;
tableQuery.FilterExpression = "#name = :name";
tableQuery.ExpressionAttributeNames = {
"#name": "aggregate"
};
}
if (query && query.context) {
vals[":c"] = query.context;
if (tableQuery.FilterExpression && tableQuery.FilterExpression.length !== 0)
tableQuery.FilterExpression += " and context = :c";
else
tableQuery.FilterExpression = "context = :c";
}
if (Object.keys(vals).length !== 0) {
tableQuery.ExpressionAttributeValues = vals;
}
var pageSize = skip + limit;
if (limit !== -1) {
tableQuery.Limit = pageSize;
}
async.doWhilst(function (end) {
if (exclusiveStartKey) tableQuery.ExclusiveStartKey = exclusiveStartKey;
if (tableQuery.KeyConditionExpression) {
client.query(tableQuery, function (err, results) {
if (err) {
error("getEvents query error: " + err);
return end(err);
}
exclusiveStartKey = results.LastEvaluatedKey || null;
entities = entities.concat(results.Items);
end(null);
});
} else {
// no great 2ndary index possibilities here - avoid calling getItems w/o aggregateId
client.scan(tableQuery, function (err, results) {
if (err) {
error("getEvents scan error: " + err);
return end(err);
}
exclusiveStartKey = results.LastEvaluatedKey || null;
entities = entities.concat(results.Items);
end(null);
})
}
}, function () {
return (entities.length < pageSize || pageSize == -1) ? exclusiveStartKey !== null : false;
}, function (err) {
if (err) {
error("getEvents error: " + err);
return callback(err);
}
entities = entities.map(MapStoredEventToEvent);
entities = _.sortBy(entities, function (e) {
return [new Date(e.commitStamp).getTime(), e.streamRevision];
});
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 client = new aws.DynamoDB.DocumentClient(self.options.endpointConf);
var exclusiveStartKey = null;
var entities = [];
var tableQuery = {
TableName: self.options.eventsTableName,
FilterExpression: "commitStamp >= :date",
ExpressionAttributeValues: { ":date": date.getTime() }
};
var pageSize = skip + limit;
if (limit !== -1) {
tableQuery.Limit = pageSize;
}
async.doWhilst(function (end) {
if (exclusiveStartKey) tableQuery.ExclusiveStartKey = exclusiveStartKey;
// scan is just really inefficient but if you need to do it often a query on a 2ndary IDX *might* help
client.scan(tableQuery, function (err, results) {
if (err) {
error("getEventsSince scan error: " + err);
return end(err);
}
exclusiveStartKey = results.LastEvaluatedKey || null;
entities = entities.concat(results.Items);
end(null);
});
}, function () {
return (entities.length < pageSize || pageSize == -1) ? exclusiveStartKey !== null : false;
}, function (err) {
if (err) {
error("getEventsSince error: " + 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 client = new aws.DynamoDB.DocumentClient(self.options.endpointConf);
var exclusiveStartKey = null;
var entities = [];
if (!query.aggregateId) {
var errMsg = 'aggregateId not defined!';
error(errMsg);
if (callback) callback(new Error(errMsg));
return;
}
var tableQuery = {
TableName: self.options.eventsTableName,
KeyConditionExpression: "aggregateId = :a",
FilterExpression: "streamRevision >= :rmin",
ExpressionAttributeValues: {
":a": query.aggregateId,
":rmin": revMin
}
};
if (revMax !== -1) {
tableQuery.FilterExpression = "streamRevision BETWEEN :rmin AND :rmax";
tableQuery.ExpressionAttributeValues[":rmax"] = revMax;
}
if (query && query.aggregate) {
tableQuery.FilterExpression += " AND #name = :name";
tableQuery.ExpressionAttributeValues[":name"] = query.aggregate;
tableQuery.ExpressionAttributeNames = { "#name": "aggregate" };
}
if (query && query.context) {
tableQuery.FilterExpression += " AND context = :ctx";
tableQuery.ExpressionAttributeValues[":ctx"] = query.context;
}
async.doWhilst(function (end) {
if (exclusiveStartKey) tableQuery.ExclusiveStartKey = exclusiveStartKey;
client.query(tableQuery, function (err, results) {
if (err) {
error("getEventsByRevision query error: " + err);
return end(err);
}
exclusiveStartKey = results.LastEvaluatedKey || null;
entities = entities.concat(results.Items);
end(null);
});
}, function () {
return exclusiveStartKey !== null;
}, function (err) {
if (err) {
error("getEventsByRevision error: " + err);
return callback(err);
}
entities = entities.map(MapStoredEventToEvent);
entities = _.sortBy(entities, function (e) {
return new Date(e.commitStamp).getTime();
});
callback(null, entities);
});
},
getUndispatchedEvents: function (query, callback) {
var self = this;
var client = new aws.DynamoDB.DocumentClient(self.options.endpointConf);
var exclusiveStartKey = null;
var entities = [];
if (!self.options.useUndispatchedEventsTable) return entities;
// TODO: use DynamoDB Streams instead
var tableQuery = {
TableName: self.options.undispatchedEventsTableName
};
if (query && query.aggregateId) {
tableQuery.ExpressionAttributeValues = { ":a": query.aggregateId };
tableQuery.FilterExpression = "aggregateId = :a";
}
if (query && query.context) {
if (tableQuery.FilterExpression && tableQuery.FilterExpression.length !== 0) {
tableQuery.ExpressionAttributeValues[":ctx"] = query.context;
tableQuery.FilterExpression += " and context = :ctx";
} else {
tableQuery.ExpressionAttributeValues = { ":ctx": query.context };
tableQuery.FilterExpression = "context = :ctx";
}
}
if (query && query.aggregate) {
tableQuery.ExpressionAttributeNames = { "#name": "aggregate" };
if (tableQuery.FilterExpression && tableQuery.FilterExpression.length !== 0) {
tableQuery.ExpressionAttributeValues[":name"] = query.aggregate;
tableQuery.FilterExpression += " AND #name = :name";
} else {
tableQuery.ExpressionAttributeValues = { ":name": query.aggregate };
tableQuery.FilterExpression = "#name = :name";
}
}
async.doWhilst(function (end) {
if (exclusiveStartKey) tableQuery.ExclusiveStartKey = exclusiveStartKey;
client.scan(tableQuery, function (err, results) {
if (err) {
error("getUndispatchedEvents scan error: " + err);
return end(err);
}
exclusiveStartKey = results.LastEvaluatedKey || null;
entities = entities.concat(results.Items);
end(null);
});
}, function () {
return exclusiveStartKey !== null;
}, function (err) {
if (err) {
error("getUndispatchedEvents error: " + err);
return callback(err);
}
entities = entities.map(MapStoredEventToEvent);
entities = _.sortBy(entities, [function (e) {
return new Date(e.commitStamp).getTime();
}, 'id']);
callback(null, entities);
});
},
getLastEvent: function (query, callback) {
var self = this;
var client = new aws.DynamoDB.DocumentClient(self.options.endpointConf);
var exclusiveStartKey = null;
var entities = [];
if (!query.aggregateId) {
var errMsg = 'aggregateId not defined!';
error(errMsg);
if (callback) callback(new Error(errMsg));
return;
}
var tableQuery = {
TableName: self.options.eventsTableName,
KeyConditionExpression: "aggregateId = :a",
ExpressionAttributeValues: {
":a": query.aggregateId
}
};
async.doWhilst(function (end) {
if (exclusiveStartKey) tableQuery.ExclusiveStartKey = exclusiveStartKey;
client.query(tableQuery, function (err, results) {
if (err) {
error("getLastEvent query error: " + err);
return end(err);
}
exclusiveStartKey = results.LastEvaluatedKey || null;
entities = entities.concat(results.Items);
end(null);
});
}, function () {
return exclusiveStartKey !== null;
}, function (err) {
if (err) {
error("getLastEvent error: " + err);
return callback(err);
}
entities = entities.map(MapStoredEventToEvent);
entities = _.sortBy(entities, function (e) {
return [new Date(e.commitStamp).getTime(),
e.streamRevision,
e.commitSequence];
}).reverse();
callback(null, entities[0]);
});
},
setEventToDispatched: function (id, callback) {
var self = this;
var client = new aws.DynamoDB.DocumentClient(self.options.endpointConf);
if (!self.options.useUndispatchedEventsTable) return callback();
var objDescriptor = {
TableName: self.options.undispatchedEventsTableName,
Key: {
id: id
}
};
client.delete(objDescriptor, callback);
},
addSnapshot: function (snap, callback) {
var self = this;
var client = new aws.DynamoDB.DocumentClient(self.options.endpointConf);
if (!snap.aggregateId) {
var errMsg = 'aggregateId not defined!';
error(errMsg);
if (callback) callback(new Error(errMsg));
return;
}
var snapshot = {
TableName: self.options.snapshotsTableName,
Item: new StoredSnapshot(snap),
ConditionExpression: "attribute_not_exists(aggregateId) and attribute_not_exists(id)"
}
client.put(snapshot, function (err, data) {
if (err) {
error("addSnapshot error: " + err);
return callback(err);
}
callback(null, data);
});
},
getSnapshot: function (query, revMax, callback) {
var self = this;
var client = new aws.DynamoDB.DocumentClient(self.options.endpointConf);
var exclusiveStartKey = null;
var entities = [];
if (!query.aggregateId) {
var errMsg = 'aggregateId not defined!';
if (callback) callback(new Error(errMsg));
return;
}
var tableQuery = {
TableName: self.options.snapshotsTableName,
KeyConditionExpression: "aggregateId = :a",
ExpressionAttributeValues: {
":a": query.aggregateId
}
};
if (query && query.aggregate) {
tableQuery.ExpressionAttributeNames = { "#name": "aggregate" };
tableQuery.ExpressionAttributeValues[":name"] = query.aggregate;
tableQuery.FilterExpression = "#name = :name";
}
if (query && query.context) {
tableQuery.ExpressionAttributeValues[":ctx"] = query.context;
if (tableQuery.FilterExpression && tableQuery.FilterExpression.length !== 0)
tableQuery.FilterExpression += " AND context = :ctx";
else
tableQuery.FilterExpression = "context = :ctx";
}
if (revMax != -1) {
if (tableQuery.FilterExpression && tableQuery.FilterExpression.length !== 0)
tableQuery.FilterExpression += " AND revision <= :rmax";
else
tableQuery.FilterExpression = "revision <= :rmax";
tableQuery.ExpressionAttributeValues[":rmax"] = revMax;
}
async.doWhilst(function (end) {
if (exclusiveStartKey) tableQuery.ExclusiveStartKey = exclusiveStartKey;
client.query(tableQuery, function (err, results) {
if (err) {
debug("getSnapshot query error: " + err);
return end(err);
}
exclusiveStartKey = results.LastEvaluatedKey || null;
entities = entities.concat(results.Items);
end(null);
});
}, function () {
return exclusiveStartKey !== null;
}, function (err) {
if (err) {
error("getSnapshot error: " + err);
return callback(err);
}
entities = entities.map(MapStoredSnapshotToSnapshot);
entities = _.sortBy(entities, function (e) {
return - new Date(e.commitStamp).getTime();
});
callback(null, entities[0]);
});
}
});
var StoredEvent = function (event) {
debug("Converting event to StoredEvent: " + JSON.stringify(event, null, 2));
this.aggregateId = event.aggregateId;
this.rowKey = (event.context || "") + ":" + (event.aggregate || "") + ":" + _.padStart(event.streamRevision, 16, '0');
this.id = event.id;
this.context = event.context;
this.aggregate = event.aggregate;
this.streamRevision = event.streamRevision;
this.commitId = event.commitId;
this.commitSequence = event.commitSequence;
this.commitStamp = new Date(event.commitStamp).getTime();
this.header = event.header;
this.dispatched = event.dispatched || false;
this.payload = JSON.stringify(event.payload);
debug("Event converted to StoredEvent: " + JSON.stringify(this, null, 2));
};
function MapStoredEventToEvent(storedEvent) {
var event = {
aggregateId: storedEvent.aggregateId,
id: storedEvent.id,
context: storedEvent.context,
aggregate: storedEvent.aggregate,
streamRevision: storedEvent.streamRevision,
commitId: storedEvent.commitId,
commitSequence: storedEvent.commitSequence,
commitStamp: new Date(storedEvent.commitStamp) || null,
header: storedEvent.header || null,
dispatched: storedEvent.dispatched,
payload: JSON.parse(storedEvent.payload) || null
};
return event;
}
var StoredSnapshot = function (snapshot) {
this.id = snapshot.id;
this.aggregateId = snapshot.aggregateId;
this.aggregate = snapshot.aggregate || undefined;
this.context = snapshot.context || undefined;
this.revision = snapshot.revision;
this.version = snapshot.version;
this.commitStamp = new Date(snapshot.commitStamp).getTime();
this.data = JSON.stringify(snapshot.data);
};
function MapStoredSnapshotToSnapshot(storedSnapshot) {
var snapshot = {
id: storedSnapshot.id,
aggregateId: storedSnapshot.aggregateId,
aggregate: storedSnapshot.aggregate || undefined,
context: storedSnapshot.context || undefined,
revision: storedSnapshot.revision,
version: storedSnapshot.version,
commitStamp: new Date(storedSnapshot.commitStamp) || null,
data: JSON.parse(storedSnapshot.data) || null
};
return snapshot;
}
function EventsTableDefinition(opts) {
var def = {
TableName: opts.eventsTableName,
KeySchema: [
{ AttributeName: "aggregateId", KeyType: "HASH" },
{ AttributeName: "rowKey", KeyType: "RANGE" }
],
AttributeDefinitions: [
{ AttributeName: "aggregateId", AttributeType: "S" },
{ AttributeName: "rowKey", AttributeType: "S" }
],
ProvisionedThroughput: {
ReadCapacityUnits: opts.EventsReadCapacityUnits,
WriteCapacityUnits: opts.EventsWriteCapacityUnits
}
};
if(opts.eventsTableStreamEnabled) {
_.assign(def, {
StreamSpecification: {
StreamEnabled: true,
StreamViewType: opts.eventsTableStreamViewType
}
})
};
return def;
}
function SnapshotTableDefinition(opts) {
var def = {
TableName: opts.snapshotsTableName,
KeySchema: [
{ AttributeName: "aggregateId", KeyType: "HASH" },
{ AttributeName: "id", KeyType: "RANGE" }
],
AttributeDefinitions: [
{ AttributeName: "aggregateId", AttributeType: "S" },
{ AttributeName: "id", AttributeType: "S" }
],
ProvisionedThroughput: {
ReadCapacityUnits: opts.SnapshotReadCapacityUnits,
WriteCapacityUnits: opts.SnapshotWriteCapacityUnits
}
};
return def;
}
function UndispatchedEventsTableDefinition(opts) {
var def = {
TableName: opts.undispatchedEventsTableName,
KeySchema: [
{ AttributeName: "id", KeyType: "HASH" }
],
AttributeDefinitions: [
{ AttributeName: "id", AttributeType: "S" }
],
ProvisionedThroughput: {
ReadCapacityUnits: opts.UndispatchedEventsReadCapacityUnits,
WriteCapacityUnits: opts.UndispatchedEventsWriteCapacityUnits
}
};
return def;
}
function isTableAlreadyExistsError (err) {
return err.code === "ResourceInUseException" && err.message === "Cannot create preexisting table";
}
var createTableIfNotExists = function (client, params, callback) {
var exists = function (p, cbExists) {
client.describeTable({ TableName: p.TableName }, function (err, data) {
if (err) {
if (err.code === "ResourceNotFoundException") {
debug("Table " + p.TableName + " doesn't exist yet: " + JSON.stringify(p, null, 2));
cbExists(null, { exists: false, definition: p });
} else {
error("Table " + p.TableName + " doesn't exist yet but describeTable: " + JSON.stringify(err, null, 2));
cbExists(err);
}
} else {
debug("Table " + p.TableName + " already exists.");
cbExists(null, { exists: true, description: data });
}
});
};
var create = function (r, cbCreate) {
if (!r.exists) {
debug("Creating " + r.definition.TableName);
client.createTable(r.definition, function (err, data) {
if (err && !isTableAlreadyExistsError(err)) {
error("Error while creating " + r.definition.TableName + ": " + JSON.stringify(err, null, 2));
cbCreate(err);
} else {
debug(params.TableName + " created. Waiting for activiation.");
cbCreate(null, { Table: { TableName: params.TableName, TableStatus: data ? data.TableDescription.TableStatus : "UNKNOWN"} });
}
});
} else {
cbCreate(null, r.description);
}
};
var active = function (d, cbActive) {
var status = d.Table.TableStatus;
async.until(
function () { return status === "ACTIVE" },
function (cbUntil) {
debug("checking " + d.Table.TableName + " status.");
client.describeTable({ TableName: d.Table.TableName }, function (err, data) {
if (err) {
error("There was an error checking " + d.Table.TableName + " status: " + JSON.stringify(err, null, 2));
cbUntil(err);
} else {
status = data.Table.TableStatus;
setTimeout(cbUntil, 1000);
}
});
},
function (err, r) {
if (err) {
error("connect create table error: " + err);
return cbActive(err);
}
debug("Table " + d.Table.TableName + " is active.");
cbActive(null, r);
});
};
async.compose(active, create, exists)(params, function (err, result) {
if (err) callback(err);
else callback(null, result);
});
};
var deleteTableIfExists = function (client, tableName, callback) {
var exists = function (name, cbExists) {
client.describeTable({ TableName: name }, function (err, data) {
if (err) {
if (err.code === "ResourceNotFoundException") {
cbExists(null, { exists: false, definition: { TableName: name } });
} else {
error("deleteTableIfExists - describeTable error: " + JSON.stringify(err, null, 2));
cbExists(err);
}
} else {
cbExists(null, { exists: true, description: { TableName: data.Table.TableName } });
}
});
};
var deleteTable = function (r, cbDelete) {
if (r.exists) {
client.deleteTable(r.description, function (err, data) {
if (err) {
error("Error deleting '" + r.description.TableName + "': " + JSON.stringify(err, null, 2));
cbDelete(err);
} else {
cbDelete(null, { Table: { TableName: data.TableDescription.TableName, TableStatus: data.TableDescription.TableStatus } });
}
});
} else {
cbDelete(null, r.description);
}
};
var active = function (d, cbActive) {
var status = d.Table.TableStatus;
async.until(
function () { return status === "DELETED" },
function (cbUntil) {
client.describeTable({ TableName: d.Table.TableName }, function (err, data) {
if (err) {
if (err.code === "ResourceNotFoundException") {
status = "DELETED";
return cbUntil(null, d.Table.TableName);
}
error("Error calling describeTable for '" + d.Table.TableName + "'");
cbUntil(err);
} else {
setTimeout(cbUntil, 1000);
}
});
},
function (err, r) {
if (err) {
error("connect delete table error: " + err);
return cbActive(err);
}
cbActive(null, r);
});
};
async.compose(active, deleteTable, exists)(tableName, function (err, result) {
if (err) callback(err);
else callback(null, result);
});
};
var clearEventTables = function (opts, documentClient, cleared) {
var exclusiveStartKey = null;
var retryCount = 0;
debug("clearning events tables");
var maps = [
{
TableName: opts.eventsTableName,
keyMap: function (n) {
return { DeleteRequest: { Key: { aggregateId: n.aggregateId, rowKey: n.rowKey } } };
}
},
];
if (opts.useUndispatchedEventsTable) {
maps.push({
TableName: opts.undispatchedEventsTableName,
keyMap: function (n) {
return { DeleteRequest: { Key: { id: n.id } } };
}
});
}
var read = function (task, callback) {
documentClient.scan(task.params, function (err, page) {
if (err) {
error("clearEventTables scan error: " + err);
return callback(err);
}
retryCount = 0;
exclusiveStartKey = page.LastEvaluatedKey || null;
if (page.Count == 0) {
return callback(null, {});
}
var batch = {
RequestItems: {},
ReturnConsumedCapacity: "INDEXES",
ReturnItemCollectionMetrics: "SIZE"
};
_.forEach(task.maps, function (m) {
var keys = _.map(page.Items, m.keyMap);
batch.RequestItems[m.TableName] = keys;
});
callback(null, batch);
});
};
var write = function (batch, callback) {
if (batch && batch.RequestItems) {
debug("Clear: calling batchWrite: " + JSON.stringify(batch, null, 2));
documentClient.batchWrite(batch, function (err, result) {
if (err) {
error("Clear (batchWrite) error): " + JSON.stringify(batch, null, 2));
return callback(err);
}
if (Object.keys(result.UnprocessedItems).length !== 0) {
retryCount++;
var retry = {
RequestItems: result.UnprocessedItems,
ReturnConsumedCapacity: "INDEXES",
ReturnItemCollectionMetrics: "SIZE"
};
debug("Clear batchWrite throttling: " + JSON.stringify(retry, null, 2));
// retry with exponential backoff
var delay = retryCount > 0 ? (50 * Math.pow(2, retryCount - 1)) : 0;
setTimeout(write, delay, retry, callback);
return;
}
callback(null, result);
});
} else {
callback(null);
}
};
var tasks = [
{
params: {
TableName: opts.eventsTableName,
ProjectionExpression: "aggregateId,rowKey,id",
Limit: 25, // max 25 per batchWrite call
ConsistentRead: false,
ReturnConsumedCapacity: "TOTAL"
},
maps: [maps[0]]
}
];
if (opts.useUndispatchedEventsTable) {
tasks.splice(0, 0, {
params: {
TableName: opts.undispatchedEventsTableName,
ProjectionExpression: "aggregateId,rowKey,id",
Limit: 12, // max 25 per batchWrite call divided by 2 tables
ConsistentRead: false,
ReturnConsumedCapacity: "TOTAL"
},
maps: maps
});
}
async.eachSeries(tasks, function (t, afterEach) {
async.doWhilst(function (next) {
if (exclusiveStartKey)
t.params.ExclusiveStartKey = exclusiveStartKey;
async.seq(read, write)(t, function (err, result) {
if (err) next(err);
else next(null, result);
});
}, function () {
return exclusiveStartKey !== null;
}, function (err, r) {
if (err) {
error("clearEvents error: " + err);
return afterEach(err);
}
return afterEach();
});
}, function (err) {
if (err) {
error("Error while clearning events tables: " + JSON.stringify(err, null, 2));
return cleared(err);
}
debug("Events tables successfully cleared.");
return cleared();
});
};
var clearSnapshotsTable = function (opts, documentClient, cleared) {
var exclusiveStartKey = null;
var query = {
TableName: opts.snapshotsTableName,
ProjectionExpression: "aggregateId,id",
Limit: 25, // max 25 per batchWrite call
ConsistentRead: false,
ReturnConsumedCapacity: "TOTAL"
};
async.doWhilst(function (end) {
if (exclusiveStartKey)
query.ExclusiveStartKey = exclusiveStartKey;
documentClient.scan(query, function (err, page) {
if (err) {
error("clearSnapshotsTable scan error: " + err);
return end(err);
}
exclusiveStartKey = page.LastEvaluatedKey || null;
if (page.Count === 0) {
return end(err);
}
var keys = _.map(page.Items, function (n) {
return { DeleteRequest: { Key: n } }
});
var batch = {
RequestItems: {},
ReturnConsumedCapacity: "TOTAL",
ReturnItemCollectionMetrics: "SIZE"
};
batch.RequestItems[opts.snapshotsTableName] = keys;
documentClient.batchWrite(batch, function (err2, data) {
error("clearSnapshotsTable batchWrite error: " + err2);
return end(err2);
});
});
}, function () {
return exclusiveStartKey !== null;
}, function (err, r) {
if (err) {
error("clearSnapshotsTable error: " + err);
return cleared(error);
}
return cleared(null);;
});
};
var deleteAllTempTables = function (client, opts, done) {
var exclusiveStartTableName = null;
var read = function (query, callback) {
if (exclusiveStartTableName)
query.ExclusiveStartTableName = exclusiveStartTableName;
client.listTables(query, function (err, list) {
if (err) {
error("deleteAllTempTables listTables error: " + err);
return callback(err);
}
exclusiveStartTableName = list.LastEvaluatedTableName || null;
var targets = _.filter(list.TableNames, function (t) {
return t === opts.eventsTableName ||
t === opts.undispatchedEventsTableName ||
t === opts.snapshotsTableName;
});
callback(null, targets);
});
};
var drop = function (targets, callback) {
async.each(targets, function (t, deleted) {
deleteTableIfExists(client, t, deleted);
}, function (err) {
if (err) {
error("deleteAllTempTables drop error: " + err);
return callback(err);
}
return callback(null);
});
};
async.doWhilst(function (next) {
async.compose(drop, read)({}, function (err, result) {
if (err) next(err);
else next(null, result);
});
}, function () {
return exclusiveStartTableName !== null;
}, function (err, result) {
if (err) {
error("deleteAllTempTables error: " + err);
return done(err);
}
done(null, result);
});
};
module.exports = DynamoDB;