├── .jshintignore ├── .gitignore ├── package.json ├── LICENSE ├── main.js ├── README.md └── test └── test.js /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | *.swp 4 | *.swo 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-postgres-named", 3 | "description": "Named parameters for node-postgres.", 4 | "version": "2.4.1", 5 | "private": false, 6 | "main": "./main", 7 | "engines": { 8 | "iojs": ">= 1", 9 | "node": ">= 0.10", 10 | "npm": ">= 1" 11 | }, 12 | "dependencies": { 13 | "lodash": "^4.0.0" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/bwestergard/node-postgres-named.git" 18 | }, 19 | "devDependencies": { 20 | "mocha": "~3.1.2", 21 | "chai": "~3.4.1", 22 | "jshint": "~2.9.1-rc1" 23 | }, 24 | "scripts": { 25 | "test": "./node_modules/mocha/bin/mocha", 26 | "test-watch": "./node_modules/mocha/bin/mocha --watch", 27 | "test-debug": "./node_modules/mocha/bin/mocha debug", 28 | "lint": "./node_modules/jshint/bin/jshint ." 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | var tokenPattern = /\$[a-zA-Z]([a-zA-Z0-9_]*)\b/g; 4 | 5 | function numericFromNamed(sql, parameters) { 6 | var fillableTokens = Object.keys(parameters); 7 | var matchedTokens = _.uniq(_.map(sql.match(tokenPattern), function (token) { 8 | return token.substring(1); // Remove leading dollar sign 9 | })); 10 | 11 | var fillTokens = _.intersection(fillableTokens, matchedTokens).sort(); 12 | var fillValues = _.map(fillTokens, function (token) { 13 | return parameters[token]; 14 | }); 15 | 16 | var unmatchedTokens = _.difference(matchedTokens, fillableTokens); 17 | 18 | if (unmatchedTokens.length) { 19 | var missing = unmatchedTokens.join(", "); 20 | throw new Error("Missing Parameters: " + missing); 21 | } 22 | 23 | var interpolatedSql = _.reduce(fillTokens, 24 | function (partiallyInterpolated, token, index) { 25 | var replaceAllPattern = new RegExp('\\$' + fillTokens[index] + '\\b', "g"); 26 | return partiallyInterpolated 27 | .replace(replaceAllPattern, 28 | '$' + (index+1)); // PostGreSQL parameters are inexplicably 1-indexed. 29 | }, sql); 30 | 31 | var out = {}; 32 | out.sql = interpolatedSql; 33 | out.values = fillValues; 34 | 35 | return out; 36 | } 37 | 38 | function patch (client) { 39 | var originalQuery = client.query; 40 | 41 | if (originalQuery.patched) return client; 42 | 43 | originalQuery = originalQuery.bind(client); 44 | 45 | var patchedQuery = function(config, values, callback) { 46 | var reparameterized; 47 | if (_.isPlainObject(config) && _.isPlainObject(config.values)) { 48 | reparameterized = numericFromNamed(config.text, config.values); 49 | config.text = reparameterized.sql; 50 | config.values = reparameterized.values; 51 | } 52 | 53 | if (arguments.length === 1) { 54 | return originalQuery(config); 55 | } 56 | else if (arguments.length === 2 && _.isFunction(values)) { 57 | return originalQuery(config, values); 58 | } 59 | else if (_.isUndefined(values) || _.isNull(values) || _.isArray(values)) { 60 | return originalQuery(config, values, callback); 61 | } else { 62 | reparameterized = numericFromNamed(config, values); 63 | return originalQuery(reparameterized.sql, reparameterized.values, callback); 64 | } 65 | }; 66 | 67 | client.query = patchedQuery; 68 | client.query.patched = true; 69 | 70 | return client; 71 | } 72 | 73 | module.exports.patch = patch; 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-postgres-named 2 | =================== 3 | 4 | Named parameters for node-postgres. 5 | 6 | [![Circle CI](https://circleci.com/gh/bwestergard/node-postgres-named/tree/master.svg?style=svg)](https://circleci.com/gh/bwestergard/node-postgres-named/tree/master) 7 | [![npm version](https://badge.fury.io/js/node-postgres-named.svg)](https://badge.fury.io/js/node-postgres-named) 8 | [![dependencies](https://david-dm.org/bwestergard/node-postgres-named.svg)](https://david-dm.org/bwestergard/node-postgres-named) 9 | 10 | Why node-postgres-named? 11 | ------------------------ 12 | 13 | Want to use postgres with node? [node-postgres](https://github.com/brianc/node-postgres) has you covered. Want [named parameters](https://github.com/brianc/node-postgres/issues/268)? 14 | 15 | Well, postgres itself doesn't support them, and [brianc](https://github.com/brianc) has sagely opted to keep his library small and close to the postgres specification. 16 | 17 | But SQL replete with meaningless numeric tokens (e.g. `$1`) isn't very readable. This module lets you monkeypatch [node-postgres](https://github.com/brianc/node-postgres) or [node-postgres-pure](https://github.com/brianc/node-postgres-pure) to support named parameters. 18 | 19 | Go from this... 20 | 21 | ```javascript 22 | 23 | client.query('SELECT name FROM person WHERE name = $1 AND tenure <= $2 AND age <= $3', 24 | ['Ursus', 2.5, 24], 25 | function (results) { console.log(results); }); 26 | 27 | ``` 28 | 29 | to this... 30 | 31 | ```javascript 32 | 33 | client.query('SELECT name FROM person WHERE name = $name AND tenure <= $tenure AND age <= $age', 34 | {'name': 'Ursus', 'tenure': 2.5, 'age': 24}, 35 | function (results) { console.log(results); }); 36 | 37 | ``` 38 | 39 | Tokens are identified with `\$[a-zA-Z]([a-zA-Z0-9_\-]*)\b`. In other words: they must begin with a letter, and can contain only alphanumerics, underscores, and dashes. 40 | 41 | Execution of [prepared statements](https://github.com/brianc/node-postgres/wiki/Prepared-Statements) is also supported: 42 | 43 | ```javascript 44 | client.query({ 45 | name : 'select.person.byNameTenureAge', 46 | text : "SELECT name FORM person WHERE name = $name AND tenure <= $tenure AND age <= $age", 47 | values : { 'name': 'Ursus Oestergardii', 48 | 'tenure': 3, 49 | 'age': 24 } 50 | }, function (results) { console.log(results); }); 51 | ``` 52 | 53 | Usage 54 | ----- 55 | 56 | Create a client as usual, then call the patch function on it. It will be patched in-place. 57 | 58 | ```javascript 59 | var pg = require('pg'); 60 | var named = require('node-postgres-named'); 61 | var client = new pg.Client(conString); 62 | named.patch(client); 63 | ``` 64 | 65 | Now both of the above call styles (with a list of values, or a dictionary of named parameters) will work. 66 | 67 | Contributors 68 | --------- 69 | 70 | Inspiration provided by a conversation with [Mike "ApeChimp" Atkins](https://github.com/apechimp). Support for prepared statements added by [nuarhu](https://github.com/nuarhu). Critical connection-pooling bugfix tediously diagnosed and patched by [Tony "tone81" Nguyen](https://github.com/tone81). [Mike "mfine15" Fine](https://github.com/mfine15) righted my unaesthetic mixing of double and single-quotes, and [Victor Quinn](https://github.com/victorquinn) fixed a spelling error. 71 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* globals it: false, describe: false */ 2 | var _ = require('lodash'); 3 | var assert = require("assert"); 4 | var chai = require("chai"); 5 | var named = require("../main.js"); 6 | 7 | // Dummy Client class for testing purposes. No methods except query, which returns its arguments 8 | 9 | function Client() { 10 | } 11 | 12 | Client.prototype.query = function(sql, values, callback) { 13 | var out = {}; 14 | out.sql = sql; 15 | out.values = values; 16 | out.callback = callback; 17 | 18 | // prepared statement call with 2 arguments 19 | if (_.isUndefined(callback) && _.isFunction(values) && _.isPlainObject(sql)) { 20 | out.callback = values; 21 | out.values = sql.values; 22 | out.sql = sql.text; 23 | } 24 | return out; 25 | }; 26 | 27 | var client = new Client(); 28 | named.patch(client); 29 | 30 | describe('node-postgres-named', function () { 31 | describe('Parameter translation', function () { 32 | it('Basic Interpolation', function () { 33 | var results = client.query("$a $b $c", {'a': 10, 'b': 20, 'c': 30}); 34 | assert.deepEqual(results.values, [ 10, 20, 30 ]); 35 | assert.equal(results.sql, "$1 $2 $3"); 36 | }); 37 | 38 | it('Lexicographic order of parameter keys differs from order of appearance in SQL string', function () { 39 | var results = client.query("$z $y $x", {'z': 10, 'y': 20, 'x': 30}); 40 | assert.deepEqual(results.values, [ 30, 20, 10 ]); 41 | assert.equal(results.sql, "$3 $2 $1"); 42 | }); 43 | 44 | it('Missing Parameters', function () { 45 | var flawedCall = function () { 46 | client.query("$z $y $x", {'z': 10, 'y': 20}); 47 | }; 48 | chai.expect(flawedCall).to.throw('Missing Parameters: x'); 49 | }); 50 | 51 | it('Extra Parameters', function () { 52 | var okayCall = function () { 53 | client.query("$x $y $z", {'w': 0, 'x': 10, 'y': 20, 'z': 30}); 54 | }; 55 | chai.expect(okayCall).not.to.throw(); 56 | }); 57 | 58 | it('Handles word boundaries', function() { 59 | var results = client.query("$a $aa", { a: 5, aa: 23 }); 60 | assert.deepEqual(results.values, [5, 23]); 61 | assert.equal(results.sql, ["$1 $2"]); 62 | }); 63 | }); 64 | 65 | describe('Monkeypatched Dispatch', function () { 66 | it('Call with original signature results in unchanged call to original function', function () { 67 | var sql = "SELECT name FORM person WHERE name = $1 AND tenure <= $2 AND age <= $3"; 68 | var values = ['Ursus Oestergardii', 3, 24]; 69 | var callback = function () { }; 70 | var results = client.query(sql, values, callback); 71 | assert.equal(results.sql, sql); 72 | assert.deepEqual(results.values, values); 73 | assert.equal(callback, callback); 74 | }); 75 | it('Call with no values results in unchanged call to original function', function () { 76 | var sql = "SELECT name FORM person WHERE name = $1 AND tenure <= $2 AND age <= $3"; 77 | var results = client.query(sql); 78 | assert.equal(results.sql, sql); 79 | assert.strictEqual(results.values, undefined); 80 | assert.strictEqual(results.callback, undefined); 81 | }); 82 | it('Named parameter call dispatched correctly', function () { 83 | var sql = "SELECT name FORM person WHERE name = $name AND tenure <= $tenure AND age <= $age"; 84 | var values = { 'name': 'Ursus Oestergardii', 85 | 'tenure': 3, 86 | 'age': 24 }; 87 | var callback = function () { }; 88 | var results = client.query(sql, values, callback); 89 | assert.equal(results.sql, 'SELECT name FORM person WHERE name = $2 AND tenure <= $3 AND age <= $1'); 90 | assert.deepEqual(results.values, [24, "Ursus Oestergardii", 3]); 91 | assert.equal(callback, callback); 92 | }); 93 | it('Prepared statement call', function() { 94 | var prepStmt = { 95 | name : 'select.person.byNameTenureAge', 96 | text : "SELECT name FORM person WHERE name = $name AND tenure <= $tenure AND age <= $age", 97 | values : { 'name': 'Ursus Oestergardii', 98 | 'tenure': 3, 99 | 'age': 24 } 100 | }; 101 | var callback = function () { }; 102 | var results = client.query(prepStmt, callback); 103 | assert.equal(results.sql, 'SELECT name FORM person WHERE name = $2 AND tenure <= $3 AND age <= $1'); 104 | assert.deepEqual(results.values, [24, "Ursus Oestergardii", 3]); 105 | assert.equal(callback, callback); 106 | }); 107 | }); 108 | }); 109 | --------------------------------------------------------------------------------