imago-sql
Version:
Small library to work with Azure SQL and MySQL via a single interface.
195 lines (170 loc) • 6.41 kB
JavaScript
/**
* @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;