UNPKG

dynamoose

Version:

Dynamoose is a modeling tool for Amazon's DynamoDB (inspired by Mongoose)

237 lines (200 loc) 7.38 kB
'use strict'; 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) { return new AWS.DynamoDB({ endpoint: new AWS.Endpoint(endpointURL) }); } 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; let self = this; 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 }; 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}`); function getModelSchemaFromIndex(index) { const requestItem = items[index]; const requestItemProperty = Object.keys(items[index])[0]; const tableName = requestItem[requestItemProperty].TableName; const TheModel = self.models[tableName]; if (!TheModel) { const errorMessage = `${tableName} is not a registered model. You can only use registered Dynamoose models when using a RAW transaction object.`; throw new errors.TransactionError(errorMessage); } const TheModel$ = TheModel.$__; const schema = TheModel$.schema; return {TheModel, TheModel$, schema}; } const transact = () => { debugTransaction('transact', transactionReq); this.dynamoDB[transactionMethodName](transactionReq, async 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((await Promise.all(data.Responses.map(async function (item, index) { const {TheModel, schema} = getModelSchemaFromIndex(index); Object.keys(item).forEach(function (prop) { if (item[prop] instanceof DynamoDBSet) { item[prop] = item[prop].values; } }); let model = new TheModel(); model.$__.isNew = false; // Destruct 'item' DynamoDB's returned structure. await schema.parseDynamo(model, item.Item); debugTransaction(`${transactionMethodName} parsed model`, model); return model; }))).filter((item, index) => { const {schema} = getModelSchemaFromIndex(index); 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, index) => getModelSchemaFromIndex(index).TheModel.$__.table.options.waitForActive)) { const waitForActivePromises = Promise.all(items.filter((item, index) => getModelSchemaFromIndex(index).TheModel.$__.table.options.waitForActive).map((item, index) => getModelSchemaFromIndex(index).TheModel.$__.table.waitForActive())); waitForActivePromises.then(transact).catch(deferred.reject); } else { transact(); } return deferred.promise.nodeify(next); }; module.exports = new Dynamoose();