diff --git a/.travis.yml b/.travis.yml index 7d53cfa..7f55508 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,8 @@ cache: - node_modules before_install: # Setup Node.js version-specific dependencies + - "test $TRAVIS_NODE_VERSION '>' '1.0' || export FILTER='SqlString'" + - "test $TRAVIS_NODE_VERSION != '3.3' || export FILTER='SqlString'" - "test $TRAVIS_NODE_VERSION != '0.6' || npm rm --save-dev nyc" - "test $TRAVIS_NODE_VERSION != '0.8' || npm rm --save-dev nyc" - "test $(echo $TRAVIS_NODE_VERSION | cut -d'.' -f1) -ge 4 || npm rm --save-dev eslint eslint-plugin-markdown" diff --git a/README.md b/README.md index 898e582..b1fae4f 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,19 @@ var sql = SqlString.format('INSERT INTO posts SET ?', post); console.log(sql); // INSERT INTO posts SET `id` = 1, `title` = 'Hello MySQL' ``` +There are also tags to be used with template strings in `sqlstring/tags`: + +```js +/*eslint-env es6*/ +var SqlStringTags = require('sqlstring/tags'); +var userId = 1; +var bar = {bar: 'b'}; +var sql = SqlStringTags.escape `UPDATE users SET foo = ${'a'}, ${bar}, baz = ${'c'} WHERE id = ${userId}`; +console.log(sql); // UPDATE users SET foo = 'a', bar = 'b', baz = 'c' WHERE id = 1 +var sql = SqlStringTags.escapeStringifyObjects `UPDATE users SET foo = ${'a'}, ${bar}, baz = ${'c'} WHERE id = ${userId}`; +console.log(sql); // UPDATE users SET foo = 'a', '[object Object]', baz = 'c' WHERE id = 1 +``` + If you feel the need to escape queries by yourself, you can also use the escaping function directly: @@ -135,6 +148,18 @@ console.log(sql); // SELECT `username`, `email` FROM `users` WHERE id = 1 ``` **Please note that this last character sequence is experimental and syntax might change** +There are also tags to be used with template strings in `sqlstring/tags`: + +```js +/*eslint-env es6*/ +var SqlStringTags = require('sqlstring/tags'); +var columns = ['users.username', 'email']; +var sql = SqlStringTags.escapeId `SELECT ${columns} FROM ${'users'}`; +console.log(sql); // SELECT `users`.`username`, `email` FROM `users` +sql = SqlStringTags.escapeIdForbidQualified `SELECT ${columns} FROM ${'users'}`; +console.log(sql); // SELECT `users.username`, `email` FROM `users` +``` + When you pass an Object to `.escape()` or `.format()`, `.escapeId()` is used to avoid SQL injection in object keys. ### Formatting queries @@ -155,6 +180,22 @@ You also have the option (but are not required) to pass in `stringifyObject` and allowing you provide a custom means of turning objects into strings, as well as a location-specific/timezone-aware `Date`. +There are also tags to be used with template strings in `sqlstring/tags`: + +```js +/*eslint-env es6*/ +var SqlStringTags = require('sqlstring/tags'); +var userId = 1; +var columns = ['users.username', 'email']; +var values = [userId, {givenname: 'example'}]; +var sql = (SqlStringTags.generateFormatFunction `SELECT ${columns} FROM ${'users'} WHERE ${'id'} = ? AND ?`)(values); +console.log(sql); // SELECT `users`.`username`, `email` FROM `users` WHERE id = 1 AND givenname = 'example' +var sql = (SqlStringTags.generateFormatFunction(true) `SELECT ${columns} FROM ${'users'} WHERE ${'id'} = ? AND ?`)(values); +console.log(sql); // SELECT `users.username`, `email` FROM `users` WHERE id = 1 AND givenname = 'example' +var sql = (SqlStringTags.generateFormatFunction `SELECT ${columns} FROM ${'users'} WHERE ${'id'} = ? AND ?`)(values, true); +console.log(sql); // SELECT `users`.`username`, `email` FROM `users` WHERE id = 1 AND '[object Object]' +``` + ## License [MIT](LICENSE) diff --git a/lib/tags.js b/lib/tags.js new file mode 100644 index 0000000..a592ad5 --- /dev/null +++ b/lib/tags.js @@ -0,0 +1,70 @@ +var SqlString = require('./SqlString'); + +module.exports.escapeId = function escapeId(strings) { + var values = [].slice.call(arguments).slice(1); + + return values.map(function(value, i) { + return strings[i] + SqlString.escapeId(value); + }).join('') + strings[strings.length - 1]; +}; + +module.exports.escapeIdForbidQualified = function escapeIdForbidQualified(strings) { + var values = [].slice.call(arguments).slice(1); + + return values.map(function(value, i) { + return strings[i] + SqlString.escapeId(value, true); + }).join('') + strings[strings.length - 1]; +}; + +module.exports.escapeWithOptions = function escapeWithOptions(stringifyObjects, timeZone) { + return function(strings) { + var values = [].slice.call(arguments).slice(1); + + return values.map(function(value, i) { + return strings[i] + SqlString.escape(value, stringifyObjects, timeZone); + }).join('') + strings[strings.length - 1]; + }; +}; +module.exports.escape = module.exports.escapeWithOptions(); +module.exports.escapeStringifyObjects = module.exports.escapeWithOptions(true); + +module.exports.generateFormatFunctionWithOptions = function generateFormatFunctionWithOptions(forbidQualified) { + return function generateFormatFunction(strings) { + var values = [].slice.call(arguments).slice(1); + + var parts = values.map(function(value, i) { + var stringParts = strings[i].split('?'); + stringParts[stringParts.length - 1] += SqlString.escapeId(value, forbidQualified); + return stringParts; + }); + parts.push(strings[strings.length - 1].split('?')); + + var prepared = parts.reduce(function(result, part) { + result[result.length - 1] += part.shift(); + result.push.apply(result, part); + return result; + }, parts.shift()); + var lastPrepared = prepared.pop(); + + return function(values, stringifyObjects, timeZone) { + if (!values) { + values = []; + } + + if (!Array.isArray(values)) { + values = [values]; + } + + return prepared.map(function(string, i) { + var value = '?'; + if (i < values.length) { + value = SqlString.escape(values[i], stringifyObjects, timeZone); + } + + return string + value; + }).join('') + lastPrepared; + }; + }; +}; +module.exports.generateFormatFunction = module.exports.generateFormatFunctionWithOptions(); +module.exports.generateFormatFunctionForbidQualified = module.exports.generateFormatFunctionWithOptions(true); diff --git a/tags.js b/tags.js new file mode 100644 index 0000000..669840e --- /dev/null +++ b/tags.js @@ -0,0 +1 @@ +module.exports = require('./lib/tags'); diff --git a/test/unit/test-tags.js b/test/unit/test-tags.js new file mode 100644 index 0000000..18fd5c1 --- /dev/null +++ b/test/unit/test-tags.js @@ -0,0 +1,281 @@ +/*eslint-env es6*/ +var assert = require('assert'); +var tags = require('../../tags'); +var test = require('utest'); + +test('SqlString.tags.escapeId', { + 'value is quoted': function() { + assert.equal('`id`', tags.escapeId `${'id'}`); + }, + + 'value can be a number': function() { + assert.equal('`42`', tags.escapeId `${42}`); + }, + + 'value containing escapes is quoted': function() { + assert.equal('`i``d`', tags.escapeId `${'i`d'}`); + }, + + 'value containing separator is quoted': function() { + assert.equal('`id1`.`id2`', tags.escapeId `${'id1.id2'}`); + }, + + 'value containing separator and escapes is quoted': function() { + assert.equal('`id``1`.`i``d2`', tags.escapeId `${'id`1.i`d2'}`); + }, + + 'arrays are turned into lists': function() { + assert.equal("`a`, `b`, `t`.`c`", tags.escapeId `${['a', 'b', 't.c']}`); + }, + + 'nested arrays are flattened': function() { + assert.equal("`a`, `b`, `t`.`c`", tags.escapeId `${['a', ['b', ['t.c']]]}`); + } +}); + +test('SqlString.tags.escapeIdForbidQualified', { + 'value is quoted': function() { + assert.equal('`id`', tags.escapeIdForbidQualified `${'id'}`); + }, + + 'value can be a number': function() { + assert.equal('`42`', tags.escapeIdForbidQualified `${42}`); + }, + + 'value containing escapes is quoted': function() { + assert.equal('`i``d`', tags.escapeIdForbidQualified `${'i`d'}`); + }, + + 'value containing separator is quoted': function() { + assert.equal('`id1.id2`', tags.escapeIdForbidQualified `${'id1.id2'}`); + }, + + 'value containing separator and escapes is quoted': function() { + assert.equal('`id``1.i``d2`', tags.escapeIdForbidQualified `${'id`1.i`d2'}`); + }, + + 'arrays are turned into lists': function() { + assert.equal("`a`, `b`, `t.c`", tags.escapeIdForbidQualified `${['a', 'b', 't.c']}`); + }, + + 'nested arrays are flattened': function() { + assert.equal("`a`, `b`, `t.c`", tags.escapeIdForbidQualified `${['a', ['b', ['t.c']]]}`); + } +}); + +test('SqlString.tags.escape', { + 'question marks are replaced with escaped array values': function() { + var sql = tags.escape `${'a'} and ${'b'}`; + assert.equal(sql, "'a' and 'b'"); + }, + + 'double quest marks are ignored': function () { + var sql = tags.escape `SELECT * FROM ?? WHERE id = ${42}`; + assert.equal(sql, 'SELECT * FROM ?? WHERE id = 42'); + }, + + 'single question marks are ignored': function() { + var sql = tags.escape `${'a'} and ?`; + assert.equal(sql, "'a' and ?"); + }, + + 'question marks within values do not cause issues': function() { + var sql = tags.escape `${'hello?'} and ${'b'}`; + assert.equal(sql, "'hello?' and 'b'"); + }, + + 'undefined is converted to NULL': function () { + var sql = tags.escape `${undefined}`; + assert.equal(sql, 'NULL'); + }, + + 'objects are converted to values': function () { + var sql = tags.escape `${{ 'hello': 'world' }}`; + assert.equal(sql, "`hello` = 'world'"); + } +}); + +test('SqlString.tags.escapeStringifyObjects', { + 'question marks are replaced with escaped array values': function() { + var sql = tags.escapeStringifyObjects `${'a'} and ${'b'}`; + assert.equal(sql, "'a' and 'b'"); + }, + + 'double quest marks are ignored': function () { + var sql = tags.escapeStringifyObjects `SELECT * FROM ?? WHERE id = ${42}`; + assert.equal(sql, 'SELECT * FROM ?? WHERE id = 42'); + }, + + 'single question marks are ignored': function() { + var sql = tags.escapeStringifyObjects `${'a'} and ?`; + assert.equal(sql, "'a' and ?"); + }, + + 'question marks within values do not cause issues': function() { + var sql = tags.escapeStringifyObjects `${'hello?'} and ${'b'}`; + assert.equal(sql, "'hello?' and 'b'"); + }, + + 'undefined is converted to NULL': function () { + var sql = tags.escapeStringifyObjects `${undefined}`; + assert.equal(sql, 'NULL'); + }, + + 'objects are converted to string': function () { + var sql = tags.escapeStringifyObjects `${{ 'hello': 'world' }}`; + assert.equal(sql, "'[object Object]'"); + + var sql = tags.escapeStringifyObjects `${{ toString: function () { return 'hello'; } }}`; + assert.equal(sql, "'hello'"); + } +}); + +test('SqlString.tags.escapeWithOptions', { + 'question marks are replaced with escaped array values': function() { + var sql = tags.escapeWithOptions(false, 'Z') `${'a'} and ${'b'}`; + assert.equal(sql, "'a' and 'b'"); + + var sql = tags.escapeWithOptions(false, '+01') `${'a'} and ${'b'}`; + assert.equal(sql, "'a' and 'b'"); + }, + + 'double quest marks are ignored': function () { + var sql = tags.escapeWithOptions(false, 'Z') `SELECT * FROM ?? WHERE id = ${42}`; + assert.equal(sql, 'SELECT * FROM ?? WHERE id = 42'); + + var sql = tags.escapeWithOptions(false, '+01') `SELECT * FROM ?? WHERE id = ${42}`; + assert.equal(sql, 'SELECT * FROM ?? WHERE id = 42'); + }, + + 'single question marks are ignored': function() { + var sql = tags.escapeWithOptions(false, 'Z') `${'a'} and ?`; + assert.equal(sql, "'a' and ?"); + + var sql = tags.escapeWithOptions(false, '+01') `${'a'} and ?`; + assert.equal(sql, "'a' and ?"); + }, + + 'question marks within values do not cause issues': function() { + var sql = tags.escapeWithOptions(false, 'Z') `${'hello?'} and ${'b'}`; + assert.equal(sql, "'hello?' and 'b'"); + + var sql = tags.escapeWithOptions(false, '+01') `${'hello?'} and ${'b'}`; + assert.equal(sql, "'hello?' and 'b'"); + }, + + 'dates are converted to specified time zone "Z"': function() { + var expected = '2012-05-07 11:42:03.002'; + var date = new Date(Date.UTC(2012, 4, 7, 11, 42, 3, 2)); + var string = tags.escapeWithOptions(false, 'Z') `${date}`; + + assert.strictEqual(string, "'" + expected + "'"); + }, + + 'dates are converted to specified time zone "+01"': function() { + var expected = '2012-05-07 12:42:03.002'; + var date = new Date(Date.UTC(2012, 4, 7, 11, 42, 3, 2)); + var string = tags.escapeWithOptions(false, '+01') `${date}`; + + assert.strictEqual(string, "'" + expected + "'"); + } +}); + +test('SqlString.tags.generateFormatFunction', { + 'question marks are replaced with escaped array values': function() { + var sql = (tags.generateFormatFunction `? and ?`)(['a', 'b']); + assert.equal(sql, "'a' and 'b'"); + }, + + 'double quest marks are replaced with escaped id': function () { + var sql = (tags.generateFormatFunction `SELECT * FROM ${'db.table'} WHERE id = ?`)([42]); + assert.equal(sql, 'SELECT * FROM `db`.`table` WHERE id = 42'); + }, + + 'extra question marks are left untouched': function() { + var sql = (tags.generateFormatFunction `? and ?`)(['a']); + assert.equal(sql, "'a' and ?"); + }, + + 'extra arguments are not used': function() { + var sql = (tags.generateFormatFunction `? and ?`)(['a', 'b', 'c']); + assert.equal(sql, "'a' and 'b'"); + }, + + 'question marks within values do not cause issues': function() { + var sql = (tags.generateFormatFunction `? and ?`)(['hello?', 'b']); + assert.equal(sql, "'hello?' and 'b'"); + }, + + 'undefined is ignored': function () { + var sql = (tags.generateFormatFunction `?`)(undefined, false); + assert.equal(sql, '?'); + }, + + 'objects is converted to values': function () { + var sql = (tags.generateFormatFunction `?`)({ 'hello': 'world' }, false); + assert.equal(sql, "`hello` = 'world'"); + }, + + 'objects is not converted to values': function () { + var sql = (tags.generateFormatFunction `?`)({ 'hello': 'world' }, true); + assert.equal(sql, "'[object Object]'"); + + var sql = (tags.generateFormatFunction `?`)({ toString: function () { return 'hello'; } }, true); + assert.equal(sql, "'hello'"); + }, + + 'sql is untouched if values are provided but there are no placeholders': function () { + var sql = (tags.generateFormatFunction `SELECT COUNT(*) FROM table`)(['a', 'b']); + assert.equal(sql, 'SELECT COUNT(*) FROM table'); + } +}); + +test('SqlString.tags.generateFormatFunctionWithOptions(true)', { + 'question marks are replaced with escaped array values': function() { + var sql = (tags.generateFormatFunctionWithOptions(true) `? and ?`)(['a', 'b']); + assert.equal(sql, "'a' and 'b'"); + }, + + 'double quest marks are replaced with escaped id': function () { + var sql = (tags.generateFormatFunctionWithOptions(true) `SELECT * FROM ${'db.table'} WHERE id = ?`)([42]); + assert.equal(sql, 'SELECT * FROM `db.table` WHERE id = 42'); + }, + + 'extra question marks are left untouched': function() { + var sql = (tags.generateFormatFunctionWithOptions(true) `? and ?`)(['a']); + assert.equal(sql, "'a' and ?"); + }, + + 'extra arguments are not used': function() { + var sql = (tags.generateFormatFunctionWithOptions(true) `? and ?`)(['a', 'b', 'c']); + assert.equal(sql, "'a' and 'b'"); + }, + + 'question marks within values do not cause issues': function() { + var sql = (tags.generateFormatFunctionWithOptions(true) `? and ?`)(['hello?', 'b']); + assert.equal(sql, "'hello?' and 'b'"); + }, + + 'undefined is ignored': function () { + var sql = (tags.generateFormatFunctionWithOptions(true) `?`)(undefined, false); + assert.equal(sql, '?'); + }, + + 'objects is converted to values': function () { + var sql = (tags.generateFormatFunctionWithOptions(true) `?`)({ 'hello': 'world' }, false); + assert.equal(sql, "`hello` = 'world'"); + }, + + 'objects is not converted to values': function () { + var sql = (tags.generateFormatFunctionWithOptions(true) `?`)({ 'hello': 'world' }, true); + assert.equal(sql, "'[object Object]'"); + + var sql = (tags.generateFormatFunctionWithOptions(true) `?`)({ toString: function () { return 'hello'; } }, true); + assert.equal(sql, "'hello'"); + }, + + 'sql is untouched if values are provided but there are no placeholders': function () { + var sql = (tags.generateFormatFunctionWithOptions(true) `SELECT COUNT(*) FROM table`)(['a', 'b']); + assert.equal(sql, 'SELECT COUNT(*) FROM table'); + } +});