├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── lib ├── index.js └── reserved.js ├── package.json └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Datalanche, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: publish test 2 | 3 | publish: 4 | npm publish . 5 | 6 | test: 7 | @./node_modules/.bin/mocha --require should --reporter dot --bail -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-pg-format 2 | ============== 3 | 4 | Node.js implementation of [PostgreSQL format()](http://www.postgresql.org/docs/9.3/static/functions-string.html#FUNCTIONS-STRING-FORMAT) to safely create dynamic SQL queries. SQL identifiers and literals are escaped to help prevent SQL injection. The behavior is equivalent to [PostgreSQL format()](http://www.postgresql.org/docs/9.3/static/functions-string.html#FUNCTIONS-STRING-FORMAT). This module also supports Node buffers, arrays, and objects which is explained [below](#arrobject). 5 | 6 | ## Install 7 | 8 | npm install pg-format 9 | 10 | ## Example 11 | ```js 12 | var format = require('pg-format'); 13 | var sql = format('SELECT * FROM %I WHERE my_col = %L %s', 'my_table', 34, 'LIMIT 10'); 14 | console.log(sql); // SELECT * FROM my_table WHERE my_col = '34' LIMIT 10 15 | ``` 16 | 17 | ## API 18 | 19 | ### format(fmt, ...) 20 | Returns a formatted string based on ```fmt``` which has a style similar to the C function ```sprintf()```. 21 | * ```%%``` outputs a literal ```%``` character. 22 | * ```%I``` outputs an escaped SQL identifier. 23 | * ```%L``` outputs an escaped SQL literal. 24 | * ```%s``` outputs a simple string. 25 | 26 | #### Argument position 27 | You can define where an argument is positioned using ```n$``` where ```n``` is the argument index starting at 1. 28 | ```js 29 | var format = require('pg-format'); 30 | var sql = format('SELECT %1$L, %1$L, %L', 34, 'test'); 31 | console.log(sql); // SELECT '34', '34', 'test' 32 | ``` 33 | 34 | ### format.config(cfg) 35 | Changes the global configuration. You can change which letters are used to denote identifiers, literals, and strings in the formatted string. This is useful when the formatted string contains a PL/pgSQL function which calls [PostgreSQL format()](http://www.postgresql.org/docs/9.3/static/functions-string.html#FUNCTIONS-STRING-FORMAT) itself. 36 | ```js 37 | var format = require('pg-format'); 38 | format.config({ 39 | pattern: { 40 | ident: 'V', 41 | literal: 'C', 42 | string: 't' 43 | } 44 | }); 45 | format.config(); // reset to default 46 | ``` 47 | 48 | ### format.ident(input) 49 | Returns the input as an escaped SQL identifier string. ```undefined```, ```null```, and objects will throw an error. 50 | 51 | ### format.literal(input) 52 | Returns the input as an escaped SQL literal string. ```undefined``` and ```null``` will return ```'NULL'```; 53 | 54 | ### format.string(input) 55 | Returns the input as a simple string. ```undefined``` and ```null``` will return an empty string. If an array element is ```undefined``` or ```null```, it will be removed from the output string. 56 | 57 | ### format.withArray(fmt, array) 58 | Same as ```format(fmt, ...)``` except parameters are provided in an array rather than as function arguments. This is useful when dynamically creating a SQL query and the number of parameters is unknown or variable. 59 | 60 | ## Node Buffers 61 | Node buffers can be used for literals (```%L```) and strings (```%s```), and will be converted to [PostgreSQL bytea hex format](http://www.postgresql.org/docs/9.3/static/datatype-binary.html). 62 | 63 | ## Arrays and Objects 64 | For arrays, each element is escaped when appropriate and concatenated to a comma-delimited string. Nested arrays are turned into grouped lists (for bulk inserts), e.g. [['a', 'b'], ['c', 'd']] turns into ('a', 'b'), ('c', 'd'). Nested array expansion can be used for literals (```%L```) and strings (```%s```), but not identifiers (```%I```). 65 | For objects, ```JSON.stringify()``` is called and the resulting string is escaped if appropriate. Objects can be used for literals (```%L```) and strings (```%s```), but not identifiers (```%I```). See the example below. 66 | 67 | ```js 68 | var format = require('pg-format'); 69 | 70 | var myArray = [ 1, 2, 3 ]; 71 | var myObject = { a: 1, b: 2 }; 72 | var myNestedArray = [['a', 1], ['b', 2]]; 73 | 74 | var sql = format('SELECT * FROM t WHERE c1 IN (%L) AND c2 = %L', myArray, myObject); 75 | console.log(sql); // SELECT * FROM t WHERE c1 IN ('1','2','3') AND c2 = '{"a":1,"b":2}' 76 | 77 | sql = format('INSERT INTO t (name, age) VALUES %L', myNestedArray); 78 | console.log(sql); // INSERT INTO t (name, age) VALUES ('a', '1'), ('b', '2') 79 | ``` 80 | 81 | ## Testing 82 | 83 | ``` 84 | npm install 85 | npm test 86 | ``` -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // reserved Postgres words 4 | var reservedMap = require(__dirname + '/reserved.js'); 5 | 6 | var fmtPattern = { 7 | ident: 'I', 8 | literal: 'L', 9 | string: 's', 10 | }; 11 | 12 | // convert to Postgres default ISO 8601 format 13 | function formatDate(date) { 14 | date = date.replace('T', ' '); 15 | date = date.replace('Z', '+00'); 16 | return date; 17 | } 18 | 19 | function isReserved(value) { 20 | if (reservedMap[value.toUpperCase()]) { 21 | return true; 22 | } 23 | return false; 24 | } 25 | 26 | function arrayToList(useSpace, array, formatter) { 27 | var sql = ''; 28 | var temp = []; 29 | 30 | sql += useSpace ? ' (' : '('; 31 | for (var i = 0; i < array.length; i++) { 32 | sql += (i === 0 ? '' : ', ') + formatter(array[i]); 33 | } 34 | sql += ')'; 35 | 36 | return sql; 37 | } 38 | 39 | // Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c 40 | function quoteIdent(value) { 41 | 42 | if (value === undefined || value === null) { 43 | throw new Error('SQL identifier cannot be null or undefined'); 44 | } else if (value === false) { 45 | return '"f"'; 46 | } else if (value === true) { 47 | return '"t"'; 48 | } else if (value instanceof Date) { 49 | return '"' + formatDate(value.toISOString()) + '"'; 50 | } else if (value instanceof Buffer) { 51 | throw new Error('SQL identifier cannot be a buffer'); 52 | } else if (Array.isArray(value) === true) { 53 | var temp = []; 54 | for (var i = 0; i < value.length; i++) { 55 | if (Array.isArray(value[i]) === true) { 56 | throw new Error('Nested array to grouped list conversion is not supported for SQL identifier'); 57 | } else { 58 | temp.push(quoteIdent(value[i])); 59 | } 60 | } 61 | return temp.toString(); 62 | } else if (value === Object(value)) { 63 | throw new Error('SQL identifier cannot be an object'); 64 | } 65 | 66 | var ident = value.toString().slice(0); // create copy 67 | 68 | // do not quote a valid, unquoted identifier 69 | if (/^[a-z_][a-z0-9_$]*$/.test(ident) === true && isReserved(ident) === false) { 70 | return ident; 71 | } 72 | 73 | var quoted = '"'; 74 | 75 | for (var i = 0; i < ident.length; i++) { 76 | var c = ident[i]; 77 | if (c === '"') { 78 | quoted += c + c; 79 | } else { 80 | quoted += c; 81 | } 82 | } 83 | 84 | quoted += '"'; 85 | 86 | return quoted; 87 | }; 88 | 89 | // Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c 90 | function quoteLiteral(value) { 91 | 92 | var literal = null; 93 | var explicitCast = null; 94 | 95 | if (value === undefined || value === null) { 96 | return 'NULL'; 97 | } else if (value === false) { 98 | return "'f'"; 99 | } else if (value === true) { 100 | return "'t'"; 101 | } else if (value instanceof Date) { 102 | return "'" + formatDate(value.toISOString()) + "'"; 103 | } else if (value instanceof Buffer) { 104 | return "E'\\\\x" + value.toString('hex') + "'"; 105 | } else if (Array.isArray(value) === true) { 106 | var temp = []; 107 | for (var i = 0; i < value.length; i++) { 108 | if (Array.isArray(value[i]) === true) { 109 | temp.push(arrayToList(i !== 0, value[i], quoteLiteral)) 110 | } else { 111 | temp.push(quoteLiteral(value[i])); 112 | } 113 | } 114 | return temp.toString(); 115 | } else if (value === Object(value)) { 116 | explicitCast = 'jsonb'; 117 | literal = JSON.stringify(value); 118 | } else { 119 | literal = value.toString().slice(0); // create copy 120 | } 121 | 122 | var hasBackslash = false; 123 | var quoted = '\''; 124 | 125 | for (var i = 0; i < literal.length; i++) { 126 | var c = literal[i]; 127 | if (c === '\'') { 128 | quoted += c + c; 129 | } else if (c === '\\') { 130 | quoted += c + c; 131 | hasBackslash = true; 132 | } else { 133 | quoted += c; 134 | } 135 | } 136 | 137 | quoted += '\''; 138 | 139 | if (hasBackslash === true) { 140 | quoted = 'E' + quoted; 141 | } 142 | 143 | if (explicitCast) { 144 | quoted += '::' + explicitCast; 145 | } 146 | 147 | return quoted; 148 | }; 149 | 150 | function quoteString(value) { 151 | 152 | if (value === undefined || value === null) { 153 | return ''; 154 | } else if (value === false) { 155 | return 'f'; 156 | } else if (value === true) { 157 | return 't'; 158 | } else if (value instanceof Date) { 159 | return formatDate(value.toISOString()); 160 | } else if (value instanceof Buffer) { 161 | return '\\x' + value.toString('hex'); 162 | } else if (Array.isArray(value) === true) { 163 | var temp = []; 164 | for (var i = 0; i < value.length; i++) { 165 | if (value[i] !== null && value[i] !== undefined) { 166 | if (Array.isArray(value[i]) === true) { 167 | temp.push(arrayToList(i !== 0, value[i], quoteString)); 168 | } else { 169 | temp.push(quoteString(value[i])); 170 | } 171 | } 172 | } 173 | return temp.toString(); 174 | } else if (value === Object(value)) { 175 | return JSON.stringify(value); 176 | } 177 | 178 | return value.toString().slice(0); // return copy 179 | } 180 | 181 | function config(cfg) { 182 | 183 | // default 184 | fmtPattern.ident = 'I'; 185 | fmtPattern.literal = 'L'; 186 | fmtPattern.string = 's'; 187 | 188 | if (cfg && cfg.pattern) { 189 | if (cfg.pattern.ident) { fmtPattern.ident = cfg.pattern.ident; } 190 | if (cfg.pattern.literal) { fmtPattern.literal = cfg.pattern.literal; } 191 | if (cfg.pattern.string) { fmtPattern.string = cfg.pattern.string; } 192 | } 193 | } 194 | 195 | function formatWithArray(fmt, parameters) { 196 | 197 | var index = 0; 198 | var params = parameters; 199 | 200 | var re = '%(%|(\\d+\\$)?['; 201 | re += fmtPattern.ident; 202 | re += fmtPattern.literal; 203 | re += fmtPattern.string; 204 | re += '])'; 205 | re = new RegExp(re, 'g'); 206 | 207 | return fmt.replace(re, function(_, type) { 208 | 209 | if (type === '%') { 210 | return '%'; 211 | } 212 | 213 | var position = index; 214 | var tokens = type.split('$'); 215 | 216 | if (tokens.length > 1) { 217 | position = parseInt(tokens[0]) - 1; 218 | type = tokens[1]; 219 | } 220 | 221 | if (position < 0) { 222 | throw new Error('specified argument 0 but arguments start at 1'); 223 | } else if (position > params.length - 1) { 224 | throw new Error('too few arguments'); 225 | } 226 | 227 | index = position + 1; 228 | 229 | if (type === fmtPattern.ident) { 230 | return quoteIdent(params[position]); 231 | } else if (type === fmtPattern.literal) { 232 | return quoteLiteral(params[position]); 233 | } else if (type === fmtPattern.string) { 234 | return quoteString(params[position]); 235 | } 236 | }); 237 | } 238 | 239 | function format(fmt) { 240 | var args = Array.prototype.slice.call(arguments); 241 | args = args.slice(1); // first argument is fmt 242 | return formatWithArray(fmt, args); 243 | } 244 | 245 | exports = module.exports = format; 246 | exports.config = config; 247 | exports.ident = quoteIdent; 248 | exports.literal = quoteLiteral; 249 | exports.string = quoteString; 250 | exports.withArray = formatWithArray; -------------------------------------------------------------------------------- /lib/reserved.js: -------------------------------------------------------------------------------- 1 | // 2 | // PostgreSQL reserved words 3 | // 4 | module.exports = { 5 | "AES128": true, 6 | "AES256": true, 7 | "ALL": true, 8 | "ALLOWOVERWRITE": true, 9 | "ANALYSE": true, 10 | "ANALYZE": true, 11 | "AND": true, 12 | "ANY": true, 13 | "ARRAY": true, 14 | "AS": true, 15 | "ASC": true, 16 | "AUTHORIZATION": true, 17 | "BACKUP": true, 18 | "BETWEEN": true, 19 | "BINARY": true, 20 | "BLANKSASNULL": true, 21 | "BOTH": true, 22 | "BYTEDICT": true, 23 | "CASE": true, 24 | "CAST": true, 25 | "CHECK": true, 26 | "COLLATE": true, 27 | "COLUMN": true, 28 | "CONSTRAINT": true, 29 | "CREATE": true, 30 | "CREDENTIALS": true, 31 | "CROSS": true, 32 | "CURRENT_DATE": true, 33 | "CURRENT_TIME": true, 34 | "CURRENT_TIMESTAMP": true, 35 | "CURRENT_USER": true, 36 | "CURRENT_USER_ID": true, 37 | "DEFAULT": true, 38 | "DEFERRABLE": true, 39 | "DEFLATE": true, 40 | "DEFRAG": true, 41 | "DELTA": true, 42 | "DELTA32K": true, 43 | "DESC": true, 44 | "DISABLE": true, 45 | "DISTINCT": true, 46 | "DO": true, 47 | "ELSE": true, 48 | "EMPTYASNULL": true, 49 | "ENABLE": true, 50 | "ENCODE": true, 51 | "ENCRYPT": true, 52 | "ENCRYPTION": true, 53 | "END": true, 54 | "EXCEPT": true, 55 | "EXPLICIT": true, 56 | "FALSE": true, 57 | "FOR": true, 58 | "FOREIGN": true, 59 | "FREEZE": true, 60 | "FROM": true, 61 | "FULL": true, 62 | "GLOBALDICT256": true, 63 | "GLOBALDICT64K": true, 64 | "GRANT": true, 65 | "GROUP": true, 66 | "GZIP": true, 67 | "HAVING": true, 68 | "IDENTITY": true, 69 | "IGNORE": true, 70 | "ILIKE": true, 71 | "IN": true, 72 | "INITIALLY": true, 73 | "INNER": true, 74 | "INTERSECT": true, 75 | "INTO": true, 76 | "IS": true, 77 | "ISNULL": true, 78 | "JOIN": true, 79 | "LEADING": true, 80 | "LEFT": true, 81 | "LIKE": true, 82 | "LIMIT": true, 83 | "LOCALTIME": true, 84 | "LOCALTIMESTAMP": true, 85 | "LUN": true, 86 | "LUNS": true, 87 | "LZO": true, 88 | "LZOP": true, 89 | "MINUS": true, 90 | "MOSTLY13": true, 91 | "MOSTLY32": true, 92 | "MOSTLY8": true, 93 | "NATURAL": true, 94 | "NEW": true, 95 | "NOT": true, 96 | "NOTNULL": true, 97 | "NULL": true, 98 | "NULLS": true, 99 | "OFF": true, 100 | "OFFLINE": true, 101 | "OFFSET": true, 102 | "OLD": true, 103 | "ON": true, 104 | "ONLY": true, 105 | "OPEN": true, 106 | "OR": true, 107 | "ORDER": true, 108 | "OUTER": true, 109 | "OVERLAPS": true, 110 | "PARALLEL": true, 111 | "PARTITION": true, 112 | "PERCENT": true, 113 | "PLACING": true, 114 | "PRIMARY": true, 115 | "RAW": true, 116 | "READRATIO": true, 117 | "RECOVER": true, 118 | "REFERENCES": true, 119 | "REJECTLOG": true, 120 | "RESORT": true, 121 | "RESTORE": true, 122 | "RIGHT": true, 123 | "SELECT": true, 124 | "SESSION_USER": true, 125 | "SIMILAR": true, 126 | "SOME": true, 127 | "SYSDATE": true, 128 | "SYSTEM": true, 129 | "TABLE": true, 130 | "TAG": true, 131 | "TDES": true, 132 | "TEXT255": true, 133 | "TEXT32K": true, 134 | "THEN": true, 135 | "TO": true, 136 | "TOP": true, 137 | "TRAILING": true, 138 | "TRUE": true, 139 | "TRUNCATECOLUMNS": true, 140 | "UNION": true, 141 | "UNIQUE": true, 142 | "USER": true, 143 | "USING": true, 144 | "VERBOSE": true, 145 | "WALLET": true, 146 | "WHEN": true, 147 | "WHERE": true, 148 | "WITH": true, 149 | "WITHOUT": true, 150 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Datalanche, Inc.", 4 | "url": "https://www.datalanche.com" 5 | }, 6 | "name": "pg-format", 7 | "license": "MIT", 8 | "homepage": "https://github.com/datalanche/node-pg-format", 9 | "description": "Node.js implementation of PostgreSQL's format() to safely create dynamic SQL queries.", 10 | "version": "1.0.4", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/datalanche/node-pg-format.git" 14 | }, 15 | "main": "lib/index.js", 16 | "directories": { 17 | "lib": "./lib" 18 | }, 19 | "engines": { 20 | "node": ">=4.0" 21 | }, 22 | "dependencies": { 23 | }, 24 | "devDependencies": { 25 | "istanbul": "0.4.2", 26 | "mocha": "2.4.5", 27 | "should": "8.2.1" 28 | }, 29 | "scripts": { 30 | "test": "node ./node_modules/mocha/bin/mocha", 31 | "cover-test": "node_modules/.bin/istanbul cover node_modules/.bin/_mocha" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // Original source from https://github.com/segmentio/pg-escape 3 | // 4 | var assert = require('assert'); 5 | var format = require(__dirname + '/../lib'); 6 | var should = require('should'); 7 | 8 | var testDate = new Date(Date.UTC(2012, 11, 14, 13, 6, 43, 152)); 9 | var testArray = [ 'abc', 1, true, null, testDate ]; 10 | var testIdentArray = [ 'abc', 'AbC', 1, true, testDate ]; 11 | var testObject = { a: 1, b: 2 }; 12 | var testNestedArray = [ [1, 2], [3, 4], [5, 6] ]; 13 | 14 | describe('format(fmt, ...)', function() { 15 | describe('%s', function() { 16 | it('should format as a simple string', function() { 17 | format('some %s here', 'thing').should.equal('some thing here'); 18 | format('some %s thing %s', 'long', 'here').should.equal('some long thing here'); 19 | }); 20 | 21 | it('should format array of array as simple string', function() { 22 | format('many %s %s', 'things', testNestedArray).should.equal('many things (1, 2), (3, 4), (5, 6)'); 23 | }); 24 | 25 | it('should format string using position field', function() { 26 | format('some %1$s', 'thing').should.equal('some thing'); 27 | format('some %1$s %1$s', 'thing').should.equal('some thing thing'); 28 | format('some %1$s %s', 'thing', 'again').should.equal('some thing again'); 29 | format('some %1$s %2$s', 'thing', 'again').should.equal('some thing again'); 30 | format('some %1$s %2$s %1$s', 'thing', 'again').should.equal('some thing again thing'); 31 | format('some %1$s %2$s %s %1$s', 'thing', 'again', 'some').should.equal('some thing again some thing'); 32 | }); 33 | 34 | it('should not format string using position 0', function() { 35 | (function() { 36 | format('some %0$s', 'thing'); 37 | }).should.throw(Error); 38 | }); 39 | 40 | it('should not format string using position field with too few arguments', function() { 41 | (function() { 42 | format('some %2$s', 'thing'); 43 | }).should.throw(Error); 44 | }); 45 | }); 46 | 47 | describe('%%', function() { 48 | it('should format as %', function() { 49 | format('some %%', 'thing').should.equal('some %'); 50 | }); 51 | 52 | it('should not eat args', function() { 53 | format('just %% a %s', 'test').should.equal('just % a test'); 54 | }); 55 | 56 | it('should not format % using position field', function() { 57 | format('%1$%', 'thing').should.equal('%1$%'); 58 | }); 59 | }); 60 | 61 | describe('%I', function() { 62 | it('should format as an identifier', function() { 63 | format('some %I', 'foo/bar/baz').should.equal('some "foo/bar/baz"'); 64 | }); 65 | 66 | it('should not format array of array as an identifier', function() { 67 | (function() { 68 | format('many %I %I', 'foo/bar/baz', testNestedArray); 69 | }).should.throw(Error); 70 | }); 71 | 72 | it('should format identifier using position field', function() { 73 | format('some %1$I', 'thing').should.equal('some thing'); 74 | format('some %1$I %1$I', 'thing').should.equal('some thing thing'); 75 | format('some %1$I %I', 'thing', 'again').should.equal('some thing again'); 76 | format('some %1$I %2$I', 'thing', 'again').should.equal('some thing again'); 77 | format('some %1$I %2$I %1$I', 'thing', 'again').should.equal('some thing again thing'); 78 | format('some %1$I %2$I %I %1$I', 'thing', 'again', 'huh').should.equal('some thing again huh thing'); 79 | }); 80 | 81 | it('should not format identifier using position 0', function() { 82 | (function() { 83 | format('some %0$I', 'thing'); 84 | }).should.throw(Error); 85 | }); 86 | 87 | it('should not format identifier using position field with too few arguments', function() { 88 | (function() { 89 | format('some %2$I', 'thing'); 90 | }).should.throw(Error); 91 | }); 92 | }); 93 | 94 | describe('%L', function() { 95 | it('should format as a literal', function() { 96 | format('%L', "Tobi's").should.equal("'Tobi''s'"); 97 | }); 98 | 99 | it('should format array of array as a literal', function() { 100 | format('%L', testNestedArray).should.equal("('1', '2'), ('3', '4'), ('5', '6')"); 101 | }); 102 | 103 | it('should format literal using position field', function() { 104 | format('some %1$L', 'thing').should.equal("some 'thing'"); 105 | format('some %1$L %1$L', 'thing').should.equal("some 'thing' 'thing'"); 106 | format('some %1$L %L', 'thing', 'again').should.equal("some 'thing' 'again'"); 107 | format('some %1$L %2$L', 'thing', 'again').should.equal("some 'thing' 'again'"); 108 | format('some %1$L %2$L %1$L', 'thing', 'again').should.equal("some 'thing' 'again' 'thing'"); 109 | format('some %1$L %2$L %L %1$L', 'thing', 'again', 'some').should.equal("some 'thing' 'again' 'some' 'thing'"); 110 | }); 111 | 112 | it('should not format literal using position 0', function() { 113 | (function() { 114 | format('some %0$L', 'thing'); 115 | }).should.throw(Error); 116 | }); 117 | 118 | it('should not format literal using position field with too few arguments', function() { 119 | (function() { 120 | format('some %2$L', 'thing'); 121 | }).should.throw(Error); 122 | }); 123 | }); 124 | }); 125 | 126 | describe('format.withArray(fmt, args)', function() { 127 | describe('%s', function() { 128 | it('should format as a simple string', function() { 129 | format.withArray('some %s here', [ 'thing' ]).should.equal('some thing here'); 130 | format.withArray('some %s thing %s', [ 'long', 'here' ]).should.equal('some long thing here'); 131 | }); 132 | 133 | it('should format array of array as simple string', function() { 134 | format.withArray('many %s %s', ['things', testNestedArray]).should.equal('many things (1, 2), (3, 4), (5, 6)'); 135 | }); 136 | }); 137 | 138 | describe('%%', function() { 139 | it('should format as %', function() { 140 | format.withArray('some %%', [ 'thing' ]).should.equal('some %'); 141 | }); 142 | 143 | it('should not eat args', function() { 144 | format.withArray('just %% a %s', [ 'test' ]).should.equal('just % a test'); 145 | format.withArray('just %% a %s %s %s', [ 'test', 'again', 'and again' ]).should.equal('just % a test again and again'); 146 | }); 147 | }); 148 | 149 | describe('%I', function() { 150 | it('should format as an identifier', function() { 151 | format.withArray('some %I', [ 'foo/bar/baz' ]).should.equal('some "foo/bar/baz"'); 152 | format.withArray('some %I and %I', [ 'foo/bar/baz', '#hey' ]).should.equal('some "foo/bar/baz" and "#hey"'); 153 | }); 154 | 155 | it('should not format array of array as an identifier', function() { 156 | (function() { 157 | format.withArray('many %I %I', ['foo/bar/baz', testNestedArray]); 158 | }).should.throw(Error); 159 | }); 160 | }); 161 | 162 | describe('%L', function() { 163 | it('should format as a literal', function() { 164 | format.withArray('%L', [ "Tobi's" ]).should.equal("'Tobi''s'"); 165 | format.withArray('%L %L', [ "Tobi's", "birthday" ]).should.equal("'Tobi''s' 'birthday'"); 166 | }); 167 | 168 | it('should format array of array as a literal', function() { 169 | format.withArray('%L', [testNestedArray]).should.equal("('1', '2'), ('3', '4'), ('5', '6')"); 170 | }); 171 | }); 172 | }); 173 | 174 | describe('format.string(val)', function() { 175 | it('should coerce to a string', function() { 176 | format.string(undefined).should.equal(''); 177 | format.string(null).should.equal(''); 178 | format.string(true).should.equal('t'); 179 | format.string(false).should.equal('f'); 180 | format.string(0).should.equal('0'); 181 | format.string(15).should.equal('15'); 182 | format.string(-15).should.equal('-15'); 183 | format.string(45.13).should.equal('45.13'); 184 | format.string(-45.13).should.equal('-45.13'); 185 | format.string('something').should.equal('something'); 186 | format.string(testArray).should.equal('abc,1,t,2012-12-14 13:06:43.152+00'); 187 | format.string(testNestedArray).should.equal('(1, 2), (3, 4), (5, 6)'); 188 | format.string(testDate).should.equal('2012-12-14 13:06:43.152+00'); 189 | format.string(testObject).should.equal('{"a":1,"b":2}'); 190 | }); 191 | }); 192 | 193 | describe('format.ident(val)', function() { 194 | it('should quote when necessary', function() { 195 | format.ident('foo').should.equal('foo'); 196 | format.ident('_foo').should.equal('_foo'); 197 | format.ident('_foo_bar$baz').should.equal('_foo_bar$baz'); 198 | format.ident('test.some.stuff').should.equal('"test.some.stuff"'); 199 | format.ident('test."some".stuff').should.equal('"test.""some"".stuff"'); 200 | }); 201 | 202 | it('should quote reserved words', function() { 203 | format.ident('desc').should.equal('"desc"'); 204 | format.ident('join').should.equal('"join"'); 205 | format.ident('cross').should.equal('"cross"'); 206 | }); 207 | 208 | it('should quote', function() { 209 | format.ident(true).should.equal('"t"'); 210 | format.ident(false).should.equal('"f"'); 211 | format.ident(0).should.equal('"0"'); 212 | format.ident(15).should.equal('"15"'); 213 | format.ident(-15).should.equal('"-15"'); 214 | format.ident(45.13).should.equal('"45.13"'); 215 | format.ident(-45.13).should.equal('"-45.13"'); 216 | format.ident(testIdentArray).should.equal('abc,"AbC","1","t","2012-12-14 13:06:43.152+00"'); 217 | (function() { 218 | format.ident(testNestedArray) 219 | }).should.throw(Error); 220 | format.ident(testDate).should.equal('"2012-12-14 13:06:43.152+00"'); 221 | }); 222 | 223 | it('should throw when undefined', function (done) { 224 | try { 225 | format.ident(undefined); 226 | } catch (err) { 227 | assert(err.message === 'SQL identifier cannot be null or undefined'); 228 | done(); 229 | } 230 | }); 231 | 232 | it('should throw when null', function (done) { 233 | try { 234 | format.ident(null); 235 | } catch (err) { 236 | assert(err.message === 'SQL identifier cannot be null or undefined'); 237 | done(); 238 | } 239 | }); 240 | 241 | it('should throw when object', function (done) { 242 | try { 243 | format.ident({}); 244 | } catch (err) { 245 | assert(err.message === 'SQL identifier cannot be an object'); 246 | done(); 247 | } 248 | }); 249 | }); 250 | 251 | describe('format.literal(val)', function() { 252 | it('should return NULL for null', function() { 253 | format.literal(null).should.equal('NULL'); 254 | format.literal(undefined).should.equal('NULL'); 255 | }); 256 | 257 | it('should quote', function() { 258 | format.literal(true).should.equal("'t'"); 259 | format.literal(false).should.equal("'f'"); 260 | format.literal(0).should.equal("'0'"); 261 | format.literal(15).should.equal("'15'"); 262 | format.literal(-15).should.equal("'-15'"); 263 | format.literal(45.13).should.equal("'45.13'"); 264 | format.literal(-45.13).should.equal("'-45.13'"); 265 | format.literal('hello world').should.equal("'hello world'"); 266 | format.literal(testArray).should.equal("'abc','1','t',NULL,'2012-12-14 13:06:43.152+00'"); 267 | format.literal(testNestedArray).should.equal("('1', '2'), ('3', '4'), ('5', '6')"); 268 | format.literal(testDate).should.equal("'2012-12-14 13:06:43.152+00'"); 269 | format.literal(testObject).should.equal("'{\"a\":1,\"b\":2}'::jsonb"); 270 | }); 271 | 272 | it('should format quotes', function() { 273 | format.literal("O'Reilly").should.equal("'O''Reilly'"); 274 | }); 275 | 276 | it('should format backslashes', function() { 277 | format.literal('\\whoop\\').should.equal("E'\\\\whoop\\\\'"); 278 | }); 279 | }); --------------------------------------------------------------------------------