dynamoose
Version:
Dynamoose is a modeling tool for Amazon's DynamoDB (inspired by Mongoose)
307 lines (260 loc) • 9.35 kB
JavaScript
;
const Schema = require('./Schema');
const Model = require('./Model');
const https = require('https');
const AWS = require('aws-sdk');
const debug = require('debug')('dynamoose');
const debugTransaction = require('debug')('dynamoose:transaction');
const Q = require('q');
const errors = require('./errors');
function createLocalDb(endpointURL) {
const client = new AWS.DynamoDB({
endpoint: new AWS.Endpoint(endpointURL)
});
if (typeof global.it !== 'function' && typeof global.after !== 'function') {
// We're not running in a test env - just return the client without modifications.
// This ensures that we keep BC with users that might call this method in produciton applications.
// @TODO: Remove once DynamoDB local instance supports transactions.
return client;
}
// @TODO: Remove once DynamoDB local instance supports transactions.
// @HACK
client.transactGetItems = (request, callback) => {
const batchRequest = {
RequestItems: {}
};
request.TransactItems.forEach((item) => {
const operation = Object.keys(item)[0];
const transactItem = item[operation];
if (!batchRequest.RequestItems[transactItem.TableName]) {
batchRequest.RequestItems[transactItem.TableName] = {
Keys: []
};
}
batchRequest.RequestItems[transactItem.TableName].Keys.push(transactItem.Key)
});
return client.batchGetItem(batchRequest, (err, data) => {
if (err) {
return callback(err);
}
// Re-map responses to expected format.
data.Responses = Object.values(data.Responses).reduce(
(list, resp) => ([...list, ...resp.map((item) => ({Item: item}))]),
[]
)
callback(err, data);
});
};
// @TODO: Remove once DynamoDB local instance supports transactions.
// @HACK
client.transactWriteItems = (request, callback) => {
const batchRequest = {
RequestItems: {}
};
request.TransactItems.forEach((item) => {
const operation = Object.keys(item)[0];
const transactItem = item[operation];
if (!batchRequest.RequestItems[transactItem.TableName]) {
batchRequest.RequestItems[transactItem.TableName] = [];
}
// Pick only properties that we're interested in.
const {Key, Item} = transactItem;
const itemValue = {};
if (operation === 'Update') {
// Map Key to update to to item, since Put only supports Item.
itemValue.Item = Key;
} else if (Key) {
itemValue.Key = Key;
} else if (Item) {
itemValue.Item = Item;
}
batchRequest.RequestItems[transactItem.TableName].push({
// Map Update operation to Put.
[`${operation === 'Update' ? 'Put' : operation}Request`]: itemValue
})
});
return client.batchWriteItem(batchRequest, callback);
};
return client;
}
function Dynamoose () {
this.models = {};
this.defaults = {
create: true,
waitForActive: true, // Wait for table to be created
waitForActiveTimeout: 180000, // 3 minutes
prefix: '', // prefix_Table
suffix: '' // Table_suffix
}; // defaults
}
Dynamoose.prototype.model = function(name, schema, options) {
options = options || {};
for(const key in this.defaults) {
options[key] = (typeof options[key] === 'undefined') ? this.defaults[key] : options[key];
}
name = options.prefix + name + options.suffix;
debug('Looking up model %s', name);
if(this.models[name]) {
return this.models[name];
}
if (!(schema instanceof Schema)) {
schema = new Schema(schema, options);
}
const model = Model.compile(name, schema, options, this);
this.models[name] = model;
return model;
};
/**
* The Mongoose [VirtualType](#virtualtype_VirtualType) constructor
*
* @method VirtualType
* @api public
*/
Dynamoose.prototype.VirtualType = require('./VirtualType');
Dynamoose.prototype.AWS = AWS;
Dynamoose.prototype.local = function (url) {
this.endpointURL = url || 'http://localhost:8000';
this.dynamoDB = createLocalDb(this.endpointURL);
debug('Setting DynamoDB to local (%s)', this.endpointURL);
};
/**
* Document client for executing nested scans
*/
Dynamoose.prototype.documentClient = function() {
if (this.dynamoDocumentClient) {
return this.dynamoDocumentClient;
}
if (this.endpointURL) {
debug('Setting dynamodb document client to %s', this.endpointURL);
this.AWS.config.update({ endpoint: this.endpointURL });
} else {
debug('Getting default dynamodb document client');
}
this.dynamoDocumentClient = new this.AWS.DynamoDB.DocumentClient();
return this.dynamoDocumentClient;
};
Dynamoose.prototype.setDocumentClient = function(documentClient) {
debug('Setting dynamodb document client');
this.dynamoDocumentClient = documentClient;
};
Dynamoose.prototype.ddb = function () {
if(this.dynamoDB) {
return this.dynamoDB;
}
if(this.endpointURL) {
debug('Setting DynamoDB to %s', this.endpointURL);
this.dynamoDB = createLocalDb(this.endpointURL);
} else {
debug('Getting default DynamoDB');
this.dynamoDB = new this.AWS.DynamoDB({
httpOptions: {
agent: new https.Agent({
rejectUnauthorized: true,
keepAlive: true
})
}
});
}
return this.dynamoDB;
};
Dynamoose.prototype.setDefaults = function (options) {
for(const key in this.defaults) {
options[key] = (typeof options[key] === 'undefined') ? this.defaults[key] : options[key];
}
this.defaults = options;
};
Dynamoose.prototype.Schema = Schema;
Dynamoose.prototype.Table = require('./Table');
Dynamoose.prototype.Dynamoose = Dynamoose;
Dynamoose.prototype.setDDB = function (ddb) {
debug("Setting custom DDB");
this.dynamoDB = ddb;
};
Dynamoose.prototype.revertDDB = function () {
debug("Reverting to default DDB");
this.dynamoDB = null;
};
Dynamoose.prototype.transaction = async function(items, options, next) {
debugTransaction('Run Transaction');
const deferred = Q.defer();
let dbClient = this.documentClient();
let DynamoDBSet = dbClient.createSet([1, 2, 3]).constructor;
options = options || {};
if(typeof options === 'function') {
next = options;
options = {};
}
if(!Array.isArray(items) || items.length === 0) {
deferred.reject(new errors.TransactionError('Items required to run transaction'));
return deferred.promise.nodeify(next);
}
items = await Promise.all(items);
let transactionReq = {
TransactItems: items.map(item => {
const returnItem = {...item};
delete returnItem.$__;
return returnItem;
})
};
let transactionMethodName;
if (options.type) {
debugTransaction("Using custom transaction method");
if (options.type === "get") {
transactionMethodName = "transactGetItems";
} else if (options.type === "write") {
transactionMethodName = "transactWriteItems";
} else {
deferred.reject(new errors.TransactionError('Invalid type option, please pass in "get" or "write"'));
return deferred.promise.nodeify(next);
}
} else {
debugTransaction("Using predetermined transaction method");
transactionMethodName = items.map(obj => Object.keys(obj)[0]).every(key => key === "Get") ? "transactGetItems" : "transactWriteItems";
}
debugTransaction(`Using transaction method: ${transactionMethodName}`);
const transact = () => {
debugTransaction('transact', transactionReq);
this.dynamoDB[transactionMethodName](transactionReq, function(err, data) {
if(err) {
debugTransaction(`Error returned by ${transactionMethodName}`, err);
return deferred.reject(err);
}
debugTransaction(`${transactionMethodName} response`, data);
if(!data.Responses) {
return deferred.resolve();
}
return deferred.resolve(data.Responses.map(function (item, index) {
let model;
const TheModel = items[index].$__.newModel$;
const TheModel$ = TheModel.$__;
const schema = TheModel$.schema;
Object.keys(item).forEach(function (prop) {
if (item[prop] instanceof DynamoDBSet) {
item[prop] = item[prop].values;
}
});
model = new TheModel();
model.$__.isNew = false;
// Destruct 'item' DynamoDB's returned structure.
schema.parseDynamo(model, item.Item);
debugTransaction(`${transactionMethodName} parsed model`, model);
return model;
}).filter((item, index) => {
const TheModel = items[index].$__.newModel$;
const TheModel$ = TheModel.$__;
const schema = TheModel$.schema;
return !(schema.expires && schema.expires.returnExpiredItems === false && item[schema.expires.attribute] && item[schema.expires.attribute] < new Date());
}));
});
}
if (options.returnRequest) {
deferred.resolve(transactionReq);
} else if (items.some(item => item.$__.newModel$.$__.table.options.waitForActive)) {
const waitForActivePromises = Promise.all(items.filter(item => item.$__.newModel$.$__.table.options.waitForActive).map(item => item.$__.newModel$.$__.table.waitForActive()));
waitForActivePromises.then(transact).catch(deferred.reject);
} else {
transact();
}
return deferred.promise.nodeify(next);
}
module.exports = new Dynamoose();