diff --git a/HISTORY.md b/HISTORY.md index aea1dfc..5662115 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,10 @@ +unreleased +========== + + * Change identifier escaping to always be literal + * Remove array escape behavior + * Remove object key-value-pair escape behavior + 2.3.3 / 2022-03-06 ================== diff --git a/README.md b/README.md index a00c560..8f645b1 100644 --- a/README.md +++ b/README.md @@ -70,29 +70,16 @@ Different value types are escaped differently, here is how: * Date objects are converted to `'YYYY-mm-dd HH:ii:ss'` strings * Buffers are converted to hex strings, e.g. `X'0fa5'` * Strings are safely escaped -* Arrays are turned into list, e.g. `['a', 'b']` turns into `'a', 'b'` -* Nested arrays are turned into grouped lists (for bulk inserts), e.g. `[['a', - 'b'], ['c', 'd']]` turns into `('a', 'b'), ('c', 'd')` * Objects that have a `toSqlString` method will have `.toSqlString()` called and the returned value is used as the raw SQL. -* Objects are turned into `key = 'val'` pairs for each enumerable property on - the object. If the property's value is a function, it is skipped; if the - property's value is an object, toString() is called on it and the returned - value is used. * `undefined` / `null` are converted to `NULL` * `NaN` / `Infinity` are left as-is. MySQL does not support these, and trying to insert them as values will trigger MySQL errors until they implement support. +* All other values types are converted to a string using the global `String()` + and the resulting value is escaped. -You may have noticed that this escaping allows you to do neat things like this: - -```js -var post = {id: 1, title: 'Hello MySQL'}; -var sql = SqlString.format('INSERT INTO posts SET ?', post); -console.log(sql); // INSERT INTO posts SET `id` = 1, `title` = 'Hello MySQL' -``` - -And the `toSqlString` method allows you to form complex queries with functions: +The `toSqlString` method allows you to form complex queries with functions: ```js var CURRENT_TIMESTAMP = { toSqlString: function() { return 'CURRENT_TIMESTAMP()'; } }; @@ -132,30 +119,12 @@ var sql = 'SELECT * FROM posts ORDER BY ' + SqlString.escapeId(sorter); console.log(sql); // SELECT * FROM posts ORDER BY `date` ``` -It also supports adding qualified identifiers. It will escape both parts. - -```js -var sorter = 'date'; -var sql = 'SELECT * FROM posts ORDER BY ' + SqlString.escapeId('posts.' + sorter); -console.log(sql); // SELECT * FROM posts ORDER BY `posts`.`date` -``` - -If you do not want to treat `.` as qualified identifiers, you can set the second -argument to `true` in order to keep the string as a literal identifier: - -```js -var sorter = 'date.2'; -var sql = 'SELECT * FROM posts ORDER BY ' + SqlString.escapeId(sorter, true); -console.log(sql); // SELECT * FROM posts ORDER BY `date.2` -``` - Alternatively, you can use `??` characters as placeholders for identifiers you would like to have escaped like this: ```js var userId = 1; -var columns = ['username', 'email']; -var sql = SqlString.format('SELECT ?? FROM ?? WHERE id = ?', [columns, 'users', userId]); +var sql = SqlString.format('SELECT ??, ?? FROM ?? WHERE id = ?', ['username', 'email', 'users', userId]); console.log(sql); // SELECT `username`, `email` FROM `users` WHERE id = 1 ``` **Please note that this last character sequence is experimental and syntax might change** @@ -176,8 +145,7 @@ console.log(sql); // SELECT * FROM `users` WHERE `id` = 1 Following this you then have a valid, escaped query that you can then send to the database safely. This is useful if you are looking to prepare the query before actually sending it to the database. -You also have the option (but are not required) to pass in `stringifyObject` and `timeZone`, -allowing you provide a custom means of turning objects into strings, as well as a +You also have the option (but are not required) to pass in `timeZone`, allowing you provide a location-specific/timezone-aware `Date`. This can be further combined with the `SqlString.raw()` helper to generate SQL @@ -185,8 +153,9 @@ that includes MySQL functions as dynamic vales: ```js var userId = 1; -var data = { email: 'foobar@example.com', modified: SqlString.raw('NOW()') }; -var sql = SqlString.format('UPDATE ?? SET ? WHERE `id` = ?', ['users', data, userId]); +var email = 'foobar@example.com'; +var sql = SqlString.format('UPDATE ?? SET `email` = ?, `modified` = ? WHERE `id` = ?', + ['users', email, SqlString.raw('NOW()'), userId]); console.log(sql); // UPDATE `users` SET `email` = 'foobar@example.com', `modified` = NOW() WHERE `id` = 1 ``` diff --git a/lib/SqlString.js b/lib/SqlString.js index 8206dad..14babef 100644 --- a/lib/SqlString.js +++ b/lib/SqlString.js @@ -1,7 +1,6 @@ var SqlString = exports; var ID_GLOBAL_REGEXP = /`/g; -var QUAL_GLOBAL_REGEXP = /\./g; var CHARS_GLOBAL_REGEXP = /[\0\b\t\n\r\x1a\"\'\\]/g; // eslint-disable-line no-control-regex var CHARS_ESCAPE_MAP = { '\0' : '\\0', @@ -15,62 +14,30 @@ var CHARS_ESCAPE_MAP = { '\\' : '\\\\' }; -SqlString.escapeId = function escapeId(val, forbidQualified) { - if (Array.isArray(val)) { - var sql = ''; - - for (var i = 0; i < val.length; i++) { - sql += (i === 0 ? '' : ', ') + SqlString.escapeId(val[i], forbidQualified); - } - - return sql; - } else if (forbidQualified) { - return '`' + String(val).replace(ID_GLOBAL_REGEXP, '``') + '`'; - } else { - return '`' + String(val).replace(ID_GLOBAL_REGEXP, '``').replace(QUAL_GLOBAL_REGEXP, '`.`') + '`'; - } +SqlString.escapeId = function escapeId(val) { + return '`' + String(val).replace(ID_GLOBAL_REGEXP, '``') + '`'; }; -SqlString.escape = function escape(val, stringifyObjects, timeZone) { +SqlString.escape = function escape(val, timeZone) { if (val === undefined || val === null) { return 'NULL'; } switch (typeof val) { case 'boolean': return (val) ? 'true' : 'false'; - case 'number': return val + ''; + case 'number': return String(val); case 'object': if (Object.prototype.toString.call(val) === '[object Date]') { return SqlString.dateToString(val, timeZone || 'local'); - } else if (Array.isArray(val)) { - return SqlString.arrayToList(val, timeZone); } else if (Buffer.isBuffer(val)) { return SqlString.bufferToString(val); } else if (typeof val.toSqlString === 'function') { return String(val.toSqlString()); - } else if (stringifyObjects) { - return escapeString(val.toString()); } else { - return SqlString.objectToValues(val, timeZone); + return escapeString(String(val)); } - default: return escapeString(val); - } -}; - -SqlString.arrayToList = function arrayToList(array, timeZone) { - var sql = ''; - - for (var i = 0; i < array.length; i++) { - var val = array[i]; - - if (Array.isArray(val)) { - sql += (i === 0 ? '' : ', ') + '(' + SqlString.arrayToList(val, timeZone) + ')'; - } else { - sql += (i === 0 ? '' : ', ') + SqlString.escape(val, true, timeZone); - } + default: return escapeString(String(val)); } - - return sql; }; SqlString.format = function format(sql, values, stringifyObjects, timeZone) { @@ -167,22 +134,6 @@ SqlString.bufferToString = function bufferToString(buffer) { return 'X' + escapeString(buffer.toString('hex')); }; -SqlString.objectToValues = function objectToValues(object, timeZone) { - var sql = ''; - - for (var key in object) { - var val = object[key]; - - if (typeof val === 'function') { - continue; - } - - sql += (sql.length === 0 ? '' : ', ') + SqlString.escapeId(key) + ' = ' + SqlString.escape(val, true, timeZone); - } - - return sql; -}; - SqlString.raw = function raw(sql) { if (typeof sql !== 'string') { throw new TypeError('argument sql must be a string'); diff --git a/test/unit/test-SqlString.js b/test/unit/test-SqlString.js index 580aa4e..3f5b086 100644 --- a/test/unit/test-SqlString.js +++ b/test/unit/test-SqlString.js @@ -29,23 +29,15 @@ test('SqlString.escapeId', { }, 'value containing separator is quoted': function() { - assert.equal(SqlString.escapeId('id1.id2'), '`id1`.`id2`'); - }, - - 'value containing separator and escapes is quoted': function() { - assert.equal(SqlString.escapeId('id`1.i`d2'), '`id``1`.`i``d2`'); + assert.equal(SqlString.escapeId('id1.id2'), '`id1.id2`'); }, 'value containing separator is fully escaped when forbidQualified': function() { assert.equal(SqlString.escapeId('id1.id2', true), '`id1.id2`'); }, - 'arrays are turned into lists': function() { - assert.equal(SqlString.escapeId(['a', 'b', 't.c']), '`a`, `b`, `t`.`c`'); - }, - - 'nested arrays are flattened': function() { - assert.equal(SqlString.escapeId(['a', ['b', ['t.c']]]), '`a`, `b`, `t`.`c`'); + 'arrays are stringified and then escaped': function() { + assert.equal(SqlString.escapeId(['a', 'b', 'c']), '`a,b,c`'); } }); @@ -71,16 +63,9 @@ test('SqlString.escape', { assert.equal(SqlString.escape(SqlString.raw('NOW()')), 'NOW()'); }, - 'objects are turned into key value pairs': function() { - assert.equal(SqlString.escape({a: 'b', c: 'd'}), "`a` = 'b', `c` = 'd'"); - }, - - 'objects function properties are ignored': function() { - assert.equal(SqlString.escape({a: 'b', c: function() {}}), "`a` = 'b'"); - }, - - 'object values toSqlString is called': function() { - assert.equal(SqlString.escape({id: { toSqlString: function() { return 'LAST_INSERT_ID()'; } }}), '`id` = LAST_INSERT_ID()'); + 'objects are turned into string value': function() { + assert.equal(SqlString.escape({ 'hello': 'world' }), "'[object Object]'"); + assert.equal(SqlString.escape({ toString: function () { return 'hello'; } }), "'hello'"); }, 'objects toSqlString is called': function() { @@ -91,32 +76,8 @@ test('SqlString.escape', { assert.equal(SqlString.escape({ toSqlString: function() { return 'CURRENT_TIMESTAMP()'; } }), 'CURRENT_TIMESTAMP()'); }, - 'nested objects are cast to strings': function() { - assert.equal(SqlString.escape({a: {nested: true}}), "`a` = '[object Object]'"); - }, - - 'nested objects use toString': function() { - assert.equal(SqlString.escape({a: { toString: function() { return 'foo'; } }}), "`a` = 'foo'"); - }, - - 'nested objects use toString is quoted': function() { - assert.equal(SqlString.escape({a: { toString: function() { return "f'oo"; } }}), "`a` = 'f\\'oo'"); - }, - - 'arrays are turned into lists': function() { - assert.equal(SqlString.escape([1, 2, 'c']), "1, 2, 'c'"); - }, - - 'nested arrays are turned into grouped lists': function() { - assert.equal(SqlString.escape([[1, 2, 3], [4, 5, 6], ['a', 'b', {nested: true}]]), "(1, 2, 3), (4, 5, 6), ('a', 'b', '[object Object]')"); - }, - - 'nested objects inside arrays are cast to strings': function() { - assert.equal(SqlString.escape([1, {nested: true}, 2]), "1, '[object Object]', 2"); - }, - - 'nested objects inside arrays use toString': function() { - assert.equal(SqlString.escape([1, { toString: function() { return 'foo'; } }, 2]), "1, 'foo', 2"); + 'arrays are stringified and escaped': function() { + assert.equal(SqlString.escape([1, 2, 'c']), "'1,2,c'"); }, 'strings are quoted': function() { @@ -179,7 +140,7 @@ test('SqlString.escape', { '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 = SqlString.escape(date, false, 'Z'); + var string = SqlString.escape(date, 'Z'); assert.strictEqual(string, "'" + expected + "'"); }, @@ -187,7 +148,7 @@ test('SqlString.escape', { '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 = SqlString.escape(date, false, '+01'); + var string = SqlString.escape(date, '+01'); assert.strictEqual(string, "'" + expected + "'"); }, @@ -195,7 +156,7 @@ test('SqlString.escape', { 'dates are converted to specified time zone "+0200"': function() { var expected = '2012-05-07 13:42:03.002'; var date = new Date(Date.UTC(2012, 4, 7, 11, 42, 3, 2)); - var string = SqlString.escape(date, false, '+0200'); + var string = SqlString.escape(date, '+0200'); assert.strictEqual(string, "'" + expected + "'"); }, @@ -203,15 +164,15 @@ test('SqlString.escape', { 'dates are converted to specified time zone "-05:00"': function() { var expected = '2012-05-07 06:42:03.002'; var date = new Date(Date.UTC(2012, 4, 7, 11, 42, 3, 2)); - var string = SqlString.escape(date, false, '-05:00'); + var string = SqlString.escape(date, '-05:00'); assert.strictEqual(string, "'" + expected + "'"); }, 'dates are converted to UTC for unknown time zone': function() { var date = new Date(Date.UTC(2012, 4, 7, 11, 42, 3, 2)); - var expected = SqlString.escape(date, false, 'Z'); - var string = SqlString.escape(date, false, 'foo'); + var expected = SqlString.escape(date, 'Z'); + var string = SqlString.escape(date, 'foo'); assert.strictEqual(string, expected); }, @@ -291,13 +252,8 @@ test('SqlString.format', { assert.equal(sql, '?'); }, - 'objects is converted to values': function () { + 'objects is converted to string value': function () { var sql = SqlString.format('?', { 'hello': 'world' }, false); - assert.equal(sql, "`hello` = 'world'"); - }, - - 'objects is not converted to values': function () { - var sql = SqlString.format('?', { 'hello': 'world' }, true); assert.equal(sql, "'[object Object]'"); var sql = SqlString.format('?', { toString: function () { return 'hello'; } }, true);
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: