UNPKG

imago-azure-storage

Version:

An opinionated async wrapper for azure-storage for working with Azure Storage such as tables and queues.

296 lines (263 loc) 9.84 kB
/* eslint no-param-reassign:0 */ const Azure = require('azure-storage'); const Generator = Azure.TableUtilities.entityGenerator; const { azureTablesArrayToObject, azureTablesRowToObject, } = require('./common'); /** * Handles CRUD operations for Azure Tables, a cheaper and faster alternative * to SQL databases when relational databases are not needed. */ class AzureTableStorage { constructor(storageAccount, storageAccessKey) { try { this.service = Azure.createTableService(storageAccount, storageAccessKey); this.storageAccount = storageAccount; } catch (error) { console.log('Error initializing Azure Table Storage: ', error); throw error; } } /** * Creates tables if they don't exist yet. * @param {string[]} tables - Table names. */ async initializeTables(tables) { for (const table of tables) { /* eslint-disable-next-line no-await-in-loop */ await new Promise((resolve, reject) => { console.log(`Initializing table ${table}...`); this.service.createTableIfNotExists(table, (error) => { if (error) { reject(error); } else { resolve(error); } }); }); } } /** * Reads a specified row from the table. * @param {any} table * @param {any} partition * @param {any} key */ async retrieveEntity(table, partition, key) { if (table === null || table === undefined) { throw new Error('Empty table name when trying to retrieve an entity.'); } if (partition === null || partition === undefined) { throw new Error('Empty partition key when trying to retrieve a Table entity.'); } if (key === null || key === undefined) { throw new Error('Empty row key when trying to retrieve a Table entity.'); } return new Promise((resolve, reject) => { // Numbers are not allowed: partition = partition.toString(); key = key.toString(); this.service.retrieveEntity(table, partition, key, (error, result) => { if (error) { reject(error); } else { // Convert the response format where values are stored in the _ // key into a normal object format: resolve(azureTablesRowToObject(result)); } }); }); } /** * Retrieves all entities from a partition. * @param {string} table - The name of the Azure Table. * @param {string} partition - The PartitionKey. * @param {number} maxPages - Max number of pages to fetch. However, some pages * could be empty due to being on a different database server. */ async retrieveAllEntitiesByPartitionKey(table, partition, maxPages = 0) { return new Promise((resolve, reject) => { const query = new Azure.TableQuery().where('PartitionKey eq ?', partition); this.service.queryEntities(table, query, null, async (error, result) => { if (error) { reject(error); } else { const processedData = await azureTablesArrayToObject(result.entries); let { continuationToken } = result; let pagesFetched = 0; while (continuationToken && (maxPages ? pagesFetched < maxPages : true)) { /* eslint-disable no-await-in-loop */ const data = await this.repeatWithContinuationToken(table, query, continuationToken); ({ continuationToken } = data); processedData.push(...await azureTablesArrayToObject(data.entries)); pagesFetched += 1; } resolve(processedData); } }); }); } /** * Retrieves all records from the table (currently up to 1000, TODO FIXME). * @param {string} table - The name of the table. * @param {number} maxPages - Max number of pages to fetch. However, some pages * could be empty due to being on a different database server. */ async retrieveEntireTable(table, maxPages = 0) { return new Promise((resolve, reject) => { const query = new Azure.TableQuery(); this.service.queryEntities(table, query, null, async (error, result) => { if (error) { reject(error); } else { const processedData = await azureTablesArrayToObject(result.entries); let { continuationToken } = result; let pagesFetched = 0; while (continuationToken && (maxPages ? pagesFetched < maxPages : true)) { /* eslint-disable no-await-in-loop */ const data = await this.repeatWithContinuationToken(table, query, continuationToken); ({ continuationToken } = data); processedData.push(...await azureTablesArrayToObject(data.entries)); pagesFetched += 1; } resolve(processedData); } }); }); } /** * Adds or replaces an entity where all values are strings. * @param {string} table - Table name. * @param {string|object} partition - Partition key, or data if initializing * with two parameters. * @param {string} key - Row key (row ID). * @param {object} data - The data object to be saved, where object keys are * column names. */ async saveStringOnlyEntity(table, partition, key, data) { // Allow calling with just two params, when the two keys are inside // the data: if (typeof partition === 'object') { data = partition; partition = data.PartitionKey; key = data.RowKey; if (!partition || !key) { throw new Error('Empty partition key or row key when trying to save.'); } } const entity = { PartitionKey: Generator.String(partition.toString()), RowKey: Generator.String(key.toString()), }; for (const dataKey of Object.keys(data)) { entity[dataKey] = Generator.String(data[dataKey] && data[dataKey].toString ? data[dataKey].toString() : ''); } return new Promise((resolve, reject) => { this.service.insertOrReplaceEntity(table, entity, (error) => { if (error) { reject(error); } else { resolve(error); } }); }); } /** * Deletes the row with the specified key. * @param {string} table * @param {string} partition * @param {string} key - The row key of the entity to be deleted. */ async deleteEntity(table, partition, key) { return new Promise((resolve, reject) => { this.service.deleteEntity( table, { PartitionKey: partition, RowKey: key, }, (error, success, response) => { if (!error && success) { resolve(success); } else { reject(response); } }, ); }); } /** * Queries a table based on the column values. * @param {string} table * @param {string} partition - The PartitionKey. If null or empty, all * partition keys will be searched (very slow and expensive); * @param {string[][]} conditions - An array of arrays, where each condition * is an array containing two or more elements, for example ['userName eq ?', 'Anton'] * or ['hitCount >= ? and hitCount <= ?', '2', '100']. * @param {number} maxPages - Max number of pages to fetch. However, some pages * could be empty due to being on a different database server. */ async query(table, partition, conditions, maxPages = 0) { conditions = conditions || []; return new Promise((resolve, reject) => { let query; if (partition) { query = new Azure.TableQuery() .where('PartitionKey eq ?', partition); for (const condition of conditions) { query = query.and(...condition); } } else { // Empty partition key: query = new Azure.TableQuery(); let firstIteration = true; for (const condition of conditions) { if (firstIteration) { query = query.where(...condition); firstIteration = false; } else { query = query.and(...condition); } } } this.service.queryEntities(table, query, null, async (error, result) => { if (error) { reject(error); } else { const processedData = await azureTablesArrayToObject(result.entries); // If a continuation token is returned, fetch the remaining items. // Keep querying until the continuation token is null: let { continuationToken } = result; let pagesFetched = 0; while (continuationToken && (maxPages ? pagesFetched < maxPages : true)) { /* eslint-disable no-await-in-loop */ const data = await this.repeatWithContinuationToken(table, query, continuationToken); ({ continuationToken } = data); processedData.push(...await azureTablesArrayToObject(data.entries)); pagesFetched += 1; } resolve(processedData); } }); }); } /** * Repeats a table query with a continuation token which is returned when * there are more than 1000 results or the query spans multiple servers. */ async repeatWithContinuationToken(table, query, continuationToken) { return new Promise((resolve, reject) => { this.service.queryEntities(table, query, continuationToken, async (error, result) => { if (error) { reject(error); } else { resolve(result); } }); }); } } module.exports = AzureTableStorage;