UNPKG

dynamoose

Version:

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

586 lines (505 loc) 15.8 kB
'use strict'; const Q = require('q'); const debug = require('debug')('dynamoose:table'); const deepEqual = require('deep-equal'); function Table(name, schema, options, base) { debug('new Table (%s)', name, schema); this.name = name; this.schema = schema; this.options = options || {}; this.base = base; if (this.options.create === undefined || this.options.create === null) { this.options.create = true; } } const compareIndexes = function compareIndexes(local, remote) { let indexes = { delete: [], create: [] }; let localTableReq = local; let remoteTableReq = remote; let i; let j; let localIndexes = localTableReq.GlobalSecondaryIndexes || []; let remoteIndexes = remoteTableReq.GlobalSecondaryIndexes || []; debug('compareIndexes'); // let's see what remote indexes we need to sync or create for (i = 0; i < localIndexes.length; i++) { let remoteIndexFound = false; for (j = 0; j < remoteIndexes.length; j++) { if (remoteIndexes[j].IndexName === localIndexes[i].IndexName) { // let's see if the core data matches. if it doesn't, // we may need to delete the remote GSI and rebuild. let localIndex = (({ IndexName, KeySchema, Projection }) => ({ IndexName, KeySchema, Projection }))(localIndexes[i]); if (Array.isArray(localIndex && localIndex.Projection && localIndex.Projection.NonKeyAttributes)) { localIndex.Projection.NonKeyAttributes.sort(); } let remoteIndex = (({ IndexName, KeySchema, Projection }) => ({ IndexName, KeySchema, Projection }))(remoteIndexes[j]); if (Array.isArray(remoteIndex && remoteIndex.Projection && remoteIndex.Projection.NonKeyAttributes)) { remoteIndex.Projection.NonKeyAttributes.sort(); } debug('indexes being compared'); debug('local: ', localIndex); debug('remote: ', remoteIndex); if (!deepEqual(remoteIndex, localIndex)) { localIndex.ProvisionedThroughput = localIndexes[i].ProvisionedThroughput; indexes.delete.push(localIndex); indexes.create.push(localIndex); remoteIndexFound = true; } else { remoteIndexFound = true; } } } if (!remoteIndexFound) { indexes.create.push(localIndexes[i]); } } for (j = 0; j < remoteIndexes.length; j++) { let localExists = false; for (i = 0; i < localIndexes.length; i++) { if (remoteIndexes[j].IndexName === localIndexes[i].IndexName) { localExists = true; } } if (!localExists) { indexes.delete.push(remoteIndexes[j]); } } // now let's see what remote indexes exist that shouldn't exist return indexes; }; Table.prototype.deleteIndex = function deleteIndex(indexName) { const deferred = Q.defer(); let table = this; table.active = false; let params = { TableName: table.name, GlobalSecondaryIndexUpdates: [ { Delete: { IndexName: indexName } } ] }; table.base.ddb().updateTable(params, function (err, data) { debug('deleteIndex handler running'); if (err) { deferred.reject(err); } else { setTimeout(function () { table.waitForActive() .then(function () { deferred.resolve(data); }); }, 300); } }); return deferred.promise; }; Table.prototype.createIndex = function createIndex(attributes, indexSpec) { const deferred = Q.defer(); let table = this; table.active = false; let params = { TableName: this.name, AttributeDefinitions: attributes, GlobalSecondaryIndexUpdates: [ { Create: indexSpec } ] }; this.base.ddb().updateTable(params, function (err, data) { if (err) { deferred.reject(err); } else { setTimeout(function () { table.waitForActive() .then(function () { deferred.resolve(data); }); }, 300); } }); return deferred.promise; }; Table.prototype.init = function (next) { debug('initializing table, %s, %j', this.name, this.options); const deferred = Q.defer(); let table = this; let localTableReq; if (this.options.create) { this.describe() .then(function (data) { debug('table exist -- initialization done'); localTableReq = table.buildTableReq(table.name, table.schema); let indexes = compareIndexes(localTableReq, data.Table); debug('%s', JSON.stringify(indexes, null, 2)); if (table.options.update) { debug('updating indexes sequentially'); Q() .then(function() { return indexes.delete.reduce(function(cur, next) { return cur.then(function() { return table.deleteIndex(next.IndexName); }); }, Q()); }) .then(function() { return indexes.create.reduce(function(cur, next) { return cur.then(function() { return table.createIndex(localTableReq.AttributeDefinitions, next); }); }, Q()); }) .catch(function(e) { debug(e); }); } else { if (indexes.delete.length > 0 || indexes.create.length > 0) { const mess = `${table.name} indexes are not synchronized and update flag is set to false`; debug(mess); deferred.reject(new Error(mess)); } } table.initialized = true; return table.waitForActive() .then(function() { return table.updateTTL(); }) .then(function () { return deferred.resolve(); }); }) .catch(function (err) { if (err && err.code === 'ResourceNotFoundException') { debug('table does not exist -- creating'); return deferred.resolve( table.create() .then(function () { table.initialized = true; }) .then(function () { if (table.options.waitForActive) { return table.waitForActive(); } }) .then(function () { return table.updateTTL(); }) ); } if (err) { debug('error initializing', err.stack); return deferred.reject(err); } }); } else { table.initialized = true; return table.updateTTL(); } return deferred.promise.nodeify(next); }; Table.prototype.waitForActive = function (timeout, next) { debug('Waiting for Active table, %s, %j', this.name, this.options); const deferred = Q.defer(); if (typeof timeout === 'function') { next = timeout; timeout = null; } if (!timeout) { timeout = this.options.waitForActiveTimeout; } let table = this; let timeoutAt = Date.now() + timeout; function waitForActive() { debug('Waiting...'); if (Date.now() > timeoutAt) { return deferred.reject( new Error(`Wait for Active timed out after ${timeout} ms.`) ); } if (!table.initialized) { return setTimeout(waitForActive, 10); } table.describe() .then(function (data) { let active = (data.Table.TableStatus === 'ACTIVE'); let indexes = data.Table.GlobalSecondaryIndexes || []; indexes.forEach(function (gsi) { //debug('waitForActive Index Check: %s', JSON.stringify(gsi, null, 2)); debug('index %s.IndexStatus is %s', gsi.IndexName, gsi.IndexStatus); if (gsi.IndexStatus !== 'ACTIVE') { active = false; } }); if (!active) { debug('Waiting for Active again - %s', table.name); setTimeout(waitForActive, 500); } else { table.active = true; deferred.resolve(); } }) .catch(function (err) { if (err && err.code === 'ResourceNotFoundException') { return setTimeout(waitForActive, 10); } debug('Error waiting for active', err.stack); return deferred.reject(err); }); } waitForActive(); return deferred.promise.nodeify(next); }; Table.prototype.describeTTL = function (next) { debug('Describing ttl for table, %s', this.name); const deferred = Q.defer(); let ddb = this.base.ddb(); let params = { TableName: this.name }; ddb.describeTimeToLive(params, function(err, ttlDescription) { if (err) { return deferred.reject(err); } return deferred.resolve(ttlDescription); }); return deferred.promise.nodeify(next); }; Table.prototype.updateTTL = function (next) { debug('Updating ttl for table, %s', this.name); const deferred = Q.defer(); let table = this; if(this.schema.expires && !this.base.endpointURL) { this.describeTTL() .then(function (ttlDesc) { let status = ttlDesc.TimeToLiveDescription.TimeToLiveStatus; if(status === 'ENABLING' || status === 'ENABLED') { return deferred.resolve(); } let params = { TableName: table.name, TimeToLiveSpecification: { AttributeName: table.schema.expires.attribute, Enabled: true } }; let ddb = table.base.ddb(); ddb.updateTimeToLive(params, function(err) { if (err) { return deferred.reject(err); } return deferred.resolve(); }); }); } else { deferred.resolve(); } return deferred.promise.nodeify(next); }; Table.prototype.describe = function (next) { let describeTableReq = { TableName: this.name }; let deferred = Q.defer(); let ddb = this.base.ddb(); ddb.describeTable(describeTableReq, function (err, data) { if (err) { debug('error describing table', err); return deferred.reject(err); } deferred.resolve(data); }); return deferred.promise.nodeify(next); }; Table.prototype.buildTableReq = function buildTableReq(name, schema) { let attrDefs = []; let keyAttr = {}; function addKeyAttr(attr) { if (attr) { keyAttr[attr.name] = attr; } } addKeyAttr(schema.hashKey); addKeyAttr(schema.rangeKey); for (const globalIndexName in schema.indexes.global) { addKeyAttr(schema.indexes.global[globalIndexName]); // add the range key to the attribute definitions if specified const rangeKeyName = schema.indexes.global[globalIndexName].indexes[globalIndexName].rangeKey; addKeyAttr(schema.attributes[rangeKeyName]); } for (const indexName in schema.indexes.local) { addKeyAttr(schema.indexes.local[indexName]); } for (const keyAttrName in keyAttr) { attrDefs.push({ AttributeName: keyAttrName, AttributeType: keyAttr[keyAttrName].type.dynamo }); } let keySchema = [{ AttributeName: schema.hashKey.name, KeyType: 'HASH' }]; if (schema.rangeKey) { keySchema.push({ AttributeName: schema.rangeKey.name, KeyType: 'RANGE' }); } let createTableReq = { AttributeDefinitions: attrDefs, TableName: name, KeySchema: keySchema }; if (schema.throughput === 'ON_DEMAND') { debug(`Using PAY_PER_REQUEST BillingMode for ${name} table creation`); createTableReq.BillingMode = 'PAY_PER_REQUEST'; } else { debug(`Using PROVISIONED BillingMode for ${name} table creation`); let provThroughput = { ReadCapacityUnits: schema.throughput.read, WriteCapacityUnits: schema.throughput.write }; createTableReq.ProvisionedThroughput = provThroughput; createTableReq.BillingMode = 'PROVISIONED'; } debug('Creating table local indexes', schema.indexes.local); let localSecIndexes, index; for (const localSecIndexName in schema.indexes.local) { localSecIndexes = localSecIndexes || []; let indexAttr = schema.indexes.local[localSecIndexName]; index = indexAttr.indexes[localSecIndexName]; let localSecIndex = { IndexName: localSecIndexName, KeySchema: [{ AttributeName: schema.hashKey.name, KeyType: 'HASH' }, { AttributeName: indexAttr.name, KeyType: 'RANGE' }] }; if (index.project) { if (Array.isArray(index.project)) { localSecIndex.Projection = { ProjectionType: 'INCLUDE', NonKeyAttributes: index.project }; } else { localSecIndex.Projection = { ProjectionType: 'ALL' }; } } else { localSecIndex.Projection = { ProjectionType: 'KEYS_ONLY' }; } localSecIndexes.push(localSecIndex); } let globalSecIndexes; for (const globalSecIndexName in schema.indexes.global) { globalSecIndexes = globalSecIndexes || []; let globalIndexAttr = schema.indexes.global[globalSecIndexName]; index = globalIndexAttr.indexes[globalSecIndexName]; let globalSecIndex = { IndexName: globalSecIndexName, KeySchema: [{ AttributeName: globalIndexAttr.name, KeyType: 'HASH' }] }; if (createTableReq.BillingMode === 'PROVISIONED') { let provThroughput = { ReadCapacityUnits: index.throughput.read, WriteCapacityUnits: index.throughput.write }; globalSecIndex.ProvisionedThroughput = provThroughput; } if (index.rangeKey) { globalSecIndex.KeySchema.push({ AttributeName: index.rangeKey, KeyType: 'RANGE' }); } if (index.project) { if (Array.isArray(index.project)) { globalSecIndex.Projection = { ProjectionType: 'INCLUDE', NonKeyAttributes: index.project }; } else { globalSecIndex.Projection = { ProjectionType: 'ALL' }; } } else { globalSecIndex.Projection = { ProjectionType: 'KEYS_ONLY' }; } globalSecIndexes.push(globalSecIndex); } if (localSecIndexes) { createTableReq.LocalSecondaryIndexes = localSecIndexes; } if (globalSecIndexes) { createTableReq.GlobalSecondaryIndexes = globalSecIndexes; } if (this && this.options) { if (this.options.streamOptions && this.options.streamOptions.enabled === true) { createTableReq.StreamSpecification = { StreamEnabled: true, StreamViewType: this.options.streamOptions.type }; } if (this.options.serverSideEncryption) { createTableReq.SSESpecification = { 'Enabled': true }; } } return createTableReq; }; Table.prototype.create = function (next) { let ddb = this.base.ddb(); let schema = this.schema; let createTableReq = this.buildTableReq(this.name, schema); debug('ddb.createTable request:', createTableReq); const deferred = Q.defer(); ddb.createTable(createTableReq, function (err, data) { if (err) { debug('error creating table', err); return deferred.reject(err); } debug('table created', data); deferred.resolve(data); }); return deferred.promise.nodeify(next); }; Table.prototype.delete = function (next) { let deleteTableReq = { TableName: this.name }; debug('ddb.deleteTable request:', deleteTableReq); let ddb = this.base.ddb(); const deferred = Q.defer(); ddb.deleteTable(deleteTableReq, function (err, data) { if (err) { debug('error deleting table', err); return deferred.reject(err); } debug('deleted table', data); deferred.resolve(data); }); return deferred.promise.nodeify(next); }; Table.prototype.update = function (next) { // let ddb = this.base.ddb(); // ddb.updateTable(); const deferred = Q.defer(); deferred.reject(new Error('TODO')); return deferred.promise.nodeify(next); }; module.exports = Table;