├── .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 | });
--------------------------------------------------------------------------------