All files / lib Driver.js

100% Statements 71/71
100% Branches 20/20
100% Functions 7/7
100% Lines 71/71
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 2276x             6x                           1344x                               60x   60x 52x     60x                       241x                     1487x           1487x 321x       1166x     1166x 4x 4x       1166x 1166x     1166x 4x     4x     4x 4x     1166x                     5x       5x   5x                       1x 1x 1x       1x 3x 9x           1x   1x     1x 1x 1x   1x   1x                               4x 4x 4x   4x       4x 8x 8x 24x   8x       4x 8x 8x 8x 16x 16x   16x 16x     8x 8x   8x     4x   4x 4x 8x 8x 8x       4x 4x   4x 4x   4x       6x  
const Helpers = require('./Helpers');
 
/**
 * Base Database Driver
 *
 * @private
 */
const Driver = {
	identifierStartChar: '"',
	identifierEndChar: '"',
	tablePrefix: null,
	hasTruncate: true,
 
	/**
	 * Low level function for naive quoting of strings
	 *
	 * @param {String} str - The sql fragment to quote
	 * @return {String} - The quoted sql fragment
	 * @private
	 */
	_quote (str) {
		return (Helpers.isString(str) &&
			!(str.startsWith(Driver.identifierStartChar) || str.endsWith(Driver.identifierEndChar))
		)
			? `${Driver.identifierStartChar}${str}${Driver.identifierEndChar}`
			: str;
	},
 
	/**
	 * Set the limit clause
	 * @private
	 * @param {String} sql - SQL statement to modify
	 * @param {Number} limit - Maximum number of rows to fetch
	 * @param {Number} [offset] - Number of rows to skip
	 * @return {String} - Modified SQL statement
	 */
	limit (sql, limit, offset) {
		sql += ` LIMIT ${limit}`;
 
		if (Helpers.isNumber(offset)) {
			sql += ` OFFSET ${offset}`;
		}
 
		return sql;
	},
 
	/**
	 * Quote database table name, and set prefix
	 *
	 * @private
	 * @param {String} table - Table name to quote
	 * @return {String} - Quoted table name
	 */
	quoteTable (table) {
		// Quote after prefix
		return Driver.quoteIdentifiers(table);
	},
 
	/**
	 * Use the driver's escape character to quote identifiers
	 *
	 * @private
	 * @param {String|Array} str - String or array of strings to quote identifiers
	 * @return {String|Array} - Quoted identifier(s)
	 */
	quoteIdentifiers (str) {
		const pattern = new RegExp(
			`${Driver.identifierStartChar}(` +
				'([a-zA-Z0-9_]+)' + '(((.*?)))' +
				`)${Driver.identifierEndChar}`, 'ig');
 
		// Recurse for arrays of identifiers
		if (Array.isArray(str)) {
			return str.map(Driver.quoteIdentifiers);
		}
 
		// cast to string so that you don't have undefined method errors with junk data
		str = String(str);
 
		// Handle commas
		if (str.includes(',')) {
			const parts = str.split(',').map(Helpers.stringTrim);
			str = parts.map(Driver.quoteIdentifiers).join(',');
		}
 
		// Split identifiers by period
		const hierarchies = str.split('.').map(Driver._quote);
		let raw = hierarchies.join('.');
 
		// Fix functions
		if (raw.includes('(') && raw.includes(')')) {
			const functionCalls = pattern.exec(raw);
 
			// Unquote the function
			raw = raw.replace(functionCalls[0], functionCalls[1]);
 
			// Quote the identifiers inside of the parens
			const inParens = functionCalls[3].substring(1, functionCalls[3].length - 1);
			raw = raw.replace(inParens, Driver.quoteIdentifiers(inParens));
		}
 
		return raw;
	},
 
	/**
	 * Generate SQL to truncate the passed table
	 *
	 * @private
	 * @param {String} table - Table to truncate
	 * @return {String} - Truncation SQL
	 */
	truncate (table) {
		let sql = (Driver.hasTruncate)
			? 'TRUNCATE '
			: 'DELETE FROM ';
 
		sql += Driver.quoteTable(table);
 
		return sql;
	},
 
	/**
	 * Generate SQL to insert a group of rows
	 *
	 * @private
	 * @param {String} table - The table to insert to
	 * @param {Array} [data] - The array of object containing data to insert
	 * @return {String} - Query and data to insert
	 */
	insertBatch (table, data) {
		const values = [];
		const fields = Object.keys(data[0]);
		let sql = '';
 
		// Get the data values to insert, so they can
		// be parameterized
		data.forEach(obj => {
			Object.keys(obj).forEach(key => {
				values.push(obj[key]);
			});
		});
 
		// Get the field names from the keys of the first
		// object inserted
		table = Driver.quoteTable(table);
 
		sql += `INSERT INTO ${table} (${Driver.quoteIdentifiers(fields).join(',')}) VALUES `;
 
		// Create placeholder groups
		const params = Array(fields.length).fill('?');
		const paramString = `(${params.join(',')})`;
		const paramList = Array(data.length).fill(paramString);
 
		sql += paramList.join(',');
 
		return {
			sql: sql,
			values: values
		};
	},
 
	/**
	 * Creates a batch update sql statement
	 *
	 * @private
	 * @param {String} table - The name of the table to update
	 * @param {Array<Object>} data -  Array of objects containing the update data
	 * @param {String} updateKey - the field name to update based on
	 * @return {Array<String,Object,Number>} - array of parameters passed to run the query
	 */
	updateBatch (table, data, updateKey) {
		let affectedRows = 0;
		let insertData = [];
		const fieldLines = [];
 
		let sql = `UPDATE ${Driver.quoteTable(table)} SET `;
 
		// get the keys of the current set of data, except the one used to
		// set the update condition
		const fields = data.reduce((previous, current) => {
			affectedRows++;
			const keys = Object.keys(current).filter(key => {
				return key !== updateKey && !previous.includes(key);
			});
			return previous.concat(keys);
		}, []);
 
		// Create the CASE blocks for each data set
		fields.forEach(field => {
			let line = `${Driver.quoteIdentifiers(field)} = CASE\n`;
			const cases = [];
			data.forEach(currentCase => {
				insertData.push(currentCase[updateKey]);
				insertData.push(currentCase[field]);
 
				const newCase = `WHEN ${Driver.quoteIdentifiers(updateKey)} =? THEN ? `;
				cases.push(newCase);
			});
 
			line += `${cases.join('\n')}\n`;
			line += `ELSE ${Driver.quoteIdentifiers(field)} END`;
 
			fieldLines.push(line);
		});
 
		sql += `${fieldLines.join(',\n')}\n`;
 
		const whereValues = [];
		data.forEach(entry => {
			const insertValue = entry[updateKey];
			whereValues.push(insertValue);
			insertData.push(insertValue);
		});
 
		// Create the placeholders for the WHERE IN clause
		const placeholders = Array(whereValues.length);
		placeholders.fill('?');
 
		sql += `WHERE ${Driver.quoteIdentifiers(updateKey)} IN `;
		sql += `( ${placeholders.join(',')} )`;
 
		return [sql, insertData, affectedRows];
	}
};
 
module.exports = Driver;