UNPKG

imago-sql

Version:

Small library to work with Azure SQL and MySQL via a single interface.

195 lines (170 loc) 6.41 kB
/** * @fileoverview An interface to various SQL databases. Allows to plug in * various databases to switch between them instantly, e.g. Azure MS SQL. * @author Anton 2017-09-12 */ const fs = require('fs').promises; const Handlebars = require('handlebars'); const path = require('path'); const DEFAULT_ENGINE = 'azure'; const ENGINES = { azure: './sql/azure-sql', mysql: './sql/mysql', }; /** Caches compiled templates for faster access. */ const handlebarsCache = {}; class Sql { /** * @constructor * @param {string} connectionString - The connection string for the DB, for * example 'mssql://username:password@localhost/database'. * @param {object} [options] - Options. * @param {boolean} [options.debug] - Show debug messages. False by default. * @param {boolean} [options.swallowExceptions] - Do not throw an exception * if a transaction fails, and return null instead. * @param {string} [options.engine] - 'azure' or 'mysql'. * @param {string} [options.templatePath] - The folder containing the SQL * template files. */ constructor(connectionString, options) { if (!connectionString) { throw new Error('Empty database connection string (SQL).'); } options = options || {}; // Auto-select engine type as mysql when the connection string // indicates that. if (connectionString.toLowerCase().startsWith('mysql')) { options.engine = 'mysql'; } // eslint-disable-next-line global-require, import/no-dynamic-require this.Db = require(ENGINES[options.engine || DEFAULT_ENGINE]); this.db = new this.Db(connectionString, options); this.options = options; } /** * Selects all items from a table. * @param {string} tableName - The name of the SQL table to return all items * from. Must be SQL-safe. * @returns {object[]} - An array of all items from the SQL table. */ async selectAll(tableName) { return this.db.selectAll(tableName); } /** * Executes the specified SQL command. * @param {string} statement - An SQL statement, e.g. "SELECT * FROM ...". */ async query(statement) { return this.db.query(statement); } /** * Executes the specified SQL command, returns raw results. * @param {string} statement - An SQL statement, e.g. "SELECT * FROM ...". */ async runSqlQuery(statement) { return this.db.runSqlQuery(statement); } /** * Runs a query with SQL parameters. * @param {string} statement - A statement that includes parameters, e.g. * "SELECT * FROM table WHER ID = @user_id". * @param {object[]} parameters - An array of objects, each of which includes * `name`, `type` (like sql.Int) and `value`. * @returns {object} */ async runParameterizedQuery(statement, parameters) { return this.db.runParameterizedQuery(statement, parameters); } /** * Inserts multiple rows into a table simultaneously. * @param {string} table - The name of the table to insert the data into. * @param {string[]} columns - Names of columns to insert the data into. * The order of columns in `rows` must be the same as here. * @param {Array.<string[]>} rows - An array of rows, where each row is an array * of values in columns. The columns are the same as described in `columns`. * @returns {Promise<string>} 'OK' if successful. */ async insertMultipleRows(table, columns, rows) { return this.db.insertMultipleRows(table, columns, rows); } /** * Escapes a string to make it SQL-safe. * @param {string} str - A string to be saved in SQL, potentially unsafe. * @returns {string} The escaped SQL-safe string. */ escapeString(str) { return this.db.escapeString(str); } /** * Truncates the specified table (removes all rows, resets the auto-increment * counter). * @param {string} table - The name of the table to be truncated. * @returns {string} 'OK' if truncated successfully. */ async truncate(table) { return this.db.truncate(table); } /** * Saves the entire object as a row into the database. The object's property * names must match the column names. * @param {string} table - The table name. * @param {object} data - The object to be saved as a row. */ async save(table, data) { return this.db.save(table, data); } /** * Executes a stored procedure. * @param {string} procedure - Name of the stored procedure. * @param {object} data - The object to be saved as a row. */ async execute(procedure, data) { return this.db.execute(procedure, data); } /** * Bulk import huge of dataset to database * @param {*} table - The table object contains the dataset */ async bulkInsert(table) { return this.db.bulkInsert(table); } /** * Reads the SQL statement template from disk and replaces the * @param {string} name - Name of the SQL template file without the .sql * or .sql.template extension. * @param {object} params - A params object to be passed to handlebars. * Values inside this object will replace expressions like {{user.name}} * inside the SQL statement template. */ async getStatement(name, params) { if (!fs) { throw new Error('ImagoSql.getStatement() is only supported on Node.js 10.6.0 or above.'); } // Read SQL template from either .sql or .sql.template file: try { const filename = path.join(this.options.templatePath, `${name}.sql`); let template; try { template = await fs.readFile(filename, 'utf8'); } catch (e) { try { template = await fs.readFile(`${filename}.template`, 'utf8'); } catch (e2) { console.log(e2); throw new Error(`Unable to read SQL template file from "${filename}" ` + `or "${filename}.template".`); } } // If it's the first time we are running this statement, compile it // as handlebars template: if (!handlebarsCache[template]) { handlebarsCache[template] = Handlebars.compile(template); } return handlebarsCache[template](params); } catch (error) { console.log(error); throw error; } } } module.exports = Sql;