diff --git a/.gitignore b/.gitignore index f28f01f..5478e79 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,7 @@ build/Release # Deployed apps should consider commenting this line out: # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git node_modules -package-lock.json \ No newline at end of file +package-lock.json + +# IDE files and folders: +.idea/ diff --git a/.travis.yml b/.travis.yml index daf50ba..ed2578d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: - - '0.10' - - '6.9' + - '6' - '8' + - '12' after_success: 'npm run coveralls' diff --git a/index.js b/index.js index 3cabb37..3bcd37a 100644 --- a/index.js +++ b/index.js @@ -1,78 +1,58 @@ 'use strict'; -var url = require('url'); -var fs = require('fs'); +const fs = require('fs'); +const ConnectionString = require('connection-string').ConnectionString; -//Parse method copied from https://github.com/brianc/node-postgres -//Copyright (c) 2010-2014 Brian Carlson (brian.m.carlson@gmail.com) -//MIT License - -//parses a connection string +// parses a connection string function parse(str) { - //unix socket - if(str.charAt(0) === '/') { - var config = str.split(' '); - return { host: config[0], database: config[1] }; - } - - // url parse expects spaces encoded as %20 - var result = url.parse(/ |%[^a-f0-9]|%[a-f0-9][^a-f0-9]/i.test(str) ? encodeURI(str).replace(/\%25(\d\d)/g, "%$1") : str, true); - var config = result.query; - for (var k in config) { - if (Array.isArray(config[k])) { - config[k] = config[k][config[k].length-1]; + + const cs = new ConnectionString(str); + + const config = cs.params ? Object.assign({}, cs.params) : {}; + + // In this version we ignore multi-host support, using compatibility properties for the first host + port: + config.host = cs.hostname; + config.port = cs.port; + + config.database = cs.path && cs.path[0]; + config.user = cs.user; + config.password = cs.password; + + if (config.encoding) { + config.client_encoding = config.encoding; + } + + if (cs.hosts && cs.hosts[0].type === 'socket') { + return config; + } + + if (config.ssl === 'true' || config.ssl === '1') { + config.ssl = true; } - } - var auth = (result.auth || ':').split(':'); - config.user = auth[0]; - config.password = auth.splice(1).join(':'); + if (config.ssl === '0') { + config.ssl = false; + } + + if (config.sslcert || config.sslkey || config.sslrootcert) { + config.ssl = {}; + } + + if (config.sslcert) { + config.ssl.cert = fs.readFileSync(config.sslcert).toString(); + } + + if (config.sslkey) { + config.ssl.key = fs.readFileSync(config.sslkey).toString(); + } + + if (config.sslrootcert) { + config.ssl.ca = fs.readFileSync(config.sslrootcert).toString(); + } - config.port = result.port; - if(result.protocol == 'socket:') { - config.host = decodeURI(result.pathname); - config.database = result.query.db; - config.client_encoding = result.query.encoding; return config; - } - config.host = result.hostname; - - // result.pathname is not always guaranteed to have a '/' prefix (e.g. relative urls) - // only strip the slash if it is present. - var pathname = result.pathname; - if (pathname && pathname.charAt(0) === '/') { - pathname = result.pathname.slice(1) || null; - } - config.database = pathname && decodeURI(pathname); - - if (config.ssl === 'true' || config.ssl === '1') { - config.ssl = true; - } - - if (config.ssl === '0') { - config.ssl = false; - } - - if (config.sslcert || config.sslkey || config.sslrootcert) { - config.ssl = {}; - } - - if (config.sslcert) { - config.ssl.cert = fs.readFileSync(config.sslcert).toString(); - } - - if (config.sslkey) { - config.ssl.key = fs.readFileSync(config.sslkey).toString(); - } - - if (config.sslrootcert) { - config.ssl.ca = fs.readFileSync(config.sslrootcert).toString(); - } - - return config; } - module.exports = parse; parse.parse = parse; diff --git a/package.json b/package.json index 415638f..2ba59a5 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "pg-connection-string", - "version": "2.1.0", + "version": "3.0.0", "description": "Functions for dealing with a PostgresSQL connection string", "main": "./index.js", "types": "./index.d.ts", "scripts": { - "test": "istanbul cover _mocha && npm run check-coverage", + "test": "istanbul cover node_modules/mocha/bin/_mocha && npm run check-coverage", "check-coverage": "istanbul check-coverage --statements 100 --branches 100 --lines 100 --functions 100", "coveralls": "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls" }, @@ -25,12 +25,14 @@ "url": "https://github.com/iceddev/pg-connection-string/issues" }, "homepage": "https://github.com/iceddev/pg-connection-string", - "dependencies": {}, + "dependencies": { + "connection-string": "^3.0.2" + }, "devDependencies": { "chai": "^4.1.1", "coveralls": "^3.0.4", "istanbul": "^0.4.5", - "mocha": "^3.5.0" + "mocha": "^6.1.4" }, "files": [ "index.js", diff --git a/test/parse.js b/test/parse.js index 4de2871..b1f1935 100644 --- a/test/parse.js +++ b/test/parse.js @@ -1,220 +1,218 @@ 'use strict'; -var chai = require('chai'); -var expect = chai.expect; +const path = require('path'); +const EOL = require('os').EOL; +const chai = require('chai'); chai.should(); -var parse = require('../').parse; - -describe('parse', function(){ - - it('using connection string in client constructor', function(){ - var subject = parse('postgres://brian:pw@boom:381/lala'); - subject.user.should.equal('brian'); - subject.password.should.equal( 'pw'); - subject.host.should.equal( 'boom'); - subject.port.should.equal( '381'); - subject.database.should.equal( 'lala'); - }); - - it('escape spaces if present', function(){ - var subject = parse('postgres://localhost/post gres'); - subject.database.should.equal('post gres'); - }); - - it('do not double escape spaces', function(){ - var subject = parse('postgres://localhost/post%20gres'); - subject.database.should.equal('post gres'); - }); - - it('initializing with unix domain socket', function(){ - var subject = parse('/var/run/'); - subject.host.should.equal('/var/run/'); - }); - - it('initializing with unix domain socket and a specific database, the simple way', function(){ - var subject = parse('/var/run/ mydb'); - subject.host.should.equal('/var/run/'); - subject.database.should.equal('mydb'); - }); - - it('initializing with unix domain socket, the health way', function(){ - var subject = parse('socket:/some path/?db=my[db]&encoding=utf8'); - subject.host.should.equal('/some path/'); - subject.database.should.equal('my[db]', 'must to be escaped and unescaped trough "my%5Bdb%5D"'); - subject.client_encoding.should.equal('utf8'); - }); - - it('initializing with unix domain socket, the escaped health way', function(){ - var subject = parse('socket:/some%20path/?db=my%2Bdb&encoding=utf8'); - subject.host.should.equal('/some path/'); - subject.database.should.equal('my+db'); - subject.client_encoding.should.equal('utf8'); - }); - - it('initializing with unix domain socket, username and password', function(){ - var subject = parse('socket://brian:pw@/var/run/?db=mydb'); - subject.user.should.equal('brian'); - subject.password.should.equal('pw'); - subject.host.should.equal('/var/run/'); - subject.database.should.equal('mydb'); - }); - - it('password contains < and/or > characters', function(){ - var sourceConfig = { - user:'brian', - password: 'helloe', - port: 5432, - host: 'localhost', - database: 'postgres' - }; - var connectionString = 'postgres://' + sourceConfig.user + ':' + sourceConfig.password + '@' + sourceConfig.host + ':' + sourceConfig.port + '/' + sourceConfig.database; - var subject = parse(connectionString); - subject.password.should.equal(sourceConfig.password); - }); - - it('password contains colons', function(){ - var sourceConfig = { - user:'brian', - password: 'hello:pass:world', - port: 5432, - host: 'localhost', - database: 'postgres' - }; - var connectionString = 'postgres://' + sourceConfig.user + ':' + sourceConfig.password + '@' + sourceConfig.host + ':' + sourceConfig.port + '/' + sourceConfig.database; - var subject = parse(connectionString); - subject.password.should.equal(sourceConfig.password); - }); - - it('username or password contains weird characters', function(){ - var strang = 'pg://my f%irst name:is&%awesome!@localhost:9000'; - var subject = parse(strang); - subject.user.should.equal('my f%irst name'); - subject.password.should.equal('is&%awesome!'); - subject.host.should.equal('localhost'); - }); - - it('url is properly encoded', function(){ - var encoded = 'pg://bi%25na%25%25ry%20:s%40f%23@localhost/%20u%2520rl'; - var subject = parse(encoded); - subject.user.should.equal('bi%na%%ry '); - subject.password.should.equal('s@f#'); - subject.host.should.equal('localhost'); - subject.database.should.equal(' u%20rl'); - }); - - it('relative url sets database', function(){ - var relative = 'different_db_on_default_host'; - var subject = parse(relative); - subject.database.should.equal('different_db_on_default_host'); - }); - - it('no pathname returns null database', function () { - var subject = parse('pg://myhost'); - (subject.database === null).should.equal(true); - }); - - it('pathname of "/" returns null database', function () { - var subject = parse('pg://myhost/'); - subject.host.should.equal('myhost'); - (subject.database === null).should.equal(true); - }); - - it('configuration parameter application_name', function(){ - var connectionString = 'pg:///?application_name=TheApp'; - var subject = parse(connectionString); - subject.application_name.should.equal('TheApp'); - }); - - it('configuration parameter fallback_application_name', function(){ - var connectionString = 'pg:///?fallback_application_name=TheAppFallback'; - var subject = parse(connectionString); - subject.fallback_application_name.should.equal('TheAppFallback'); - }); - - it('configuration parameter fallback_application_name', function(){ - var connectionString = 'pg:///?fallback_application_name=TheAppFallback'; - var subject = parse(connectionString); - subject.fallback_application_name.should.equal('TheAppFallback'); - }); - - it('configuration parameter ssl=true', function(){ - var connectionString = 'pg:///?ssl=true'; - var subject = parse(connectionString); - subject.ssl.should.equal(true); - }); - - it('configuration parameter ssl=1', function(){ - var connectionString = 'pg:///?ssl=1'; - var subject = parse(connectionString); - subject.ssl.should.equal(true); - }); - - it('configuration parameter ssl=0', function(){ - var connectionString = 'pg:///?ssl=0'; - var subject = parse(connectionString); - subject.ssl.should.equal(false); - }); - - it('set ssl', function () { - var subject = parse('pg://myhost/db?ssl=1'); - subject.ssl.should.equal(true); - }); - - it('configuration parameter sslcert=/path/to/cert', function(){ - var connectionString = 'pg:///?sslcert=' + __dirname + '/example.cert'; - var subject = parse(connectionString); - subject.ssl.should.eql({ - cert: 'example cert\n' - }); - }); - - it('configuration parameter sslkey=/path/to/key', function(){ - var connectionString = 'pg:///?sslkey=' + __dirname + '/example.key'; - var subject = parse(connectionString); - subject.ssl.should.eql({ - key: 'example key\n' - }); - }); - - it('configuration parameter sslrootcert=/path/to/ca', function(){ - var connectionString = 'pg:///?sslrootcert=' + __dirname + '/example.ca'; - var subject = parse(connectionString); - subject.ssl.should.eql({ - ca: 'example ca\n' - }); - }); - - it('allow other params like max, ...', function () { - var subject = parse('pg://myhost/db?max=18&min=4'); - subject.max.should.equal('18'); - subject.min.should.equal('4'); - }); - - - it('configuration parameter keepalives', function(){ - var connectionString = 'pg:///?keepalives=1'; - var subject = parse(connectionString); - subject.keepalives.should.equal('1'); - }); - - it('unknown configuration parameter is passed into client', function(){ - var connectionString = 'pg:///?ThereIsNoSuchPostgresParameter=1234'; - var subject = parse(connectionString); - subject.ThereIsNoSuchPostgresParameter.should.equal('1234'); - }); - - it('do not override a config field with value from query string', function(){ - var subject = parse('socket:/some path/?db=my[db]&encoding=utf8&client_encoding=bogus'); - subject.host.should.equal('/some path/'); - subject.database.should.equal('my[db]', 'must to be escaped and unescaped through "my%5Bdb%5D"'); - subject.client_encoding.should.equal('utf8'); - }); - - - it('return last value of repeated parameter', function(){ - var connectionString = 'pg:///?keepalives=1&keepalives=0'; - var subject = parse(connectionString); - subject.keepalives.should.equal('0'); - }); +const expect = chai.expect; +const parse = require('../').parse; + +describe('parse', () => { + + it('using connection string in client constructor', () => { + const subject = parse('postgres://brian:pw@boom:381/lala'); + subject.user.should.equal('brian'); + subject.password.should.equal('pw'); + subject.host.should.equal('boom'); + subject.port.should.equal(381); + subject.database.should.equal('lala'); + }); + + it('escape spaces and pluses if present', () => { + const subject1 = parse('postgres://localhost/post+gres'); + subject1.database.should.equal('post gres'); + const subject2 = parse('postgres://localhost/post%20gres'); + subject2.database.should.equal('post gres'); + }); + + it('do not double escape spaces', () => { + const subject = parse('postgres://localhost/post%20gres'); + subject.database.should.equal('post gres'); + }); + + it('initializing with unix domain socket', () => { + const subject1 = parse('%2Fconst%2Frun%2F'); + subject1.host.should.equal('/const/run/'); + const subject2 = parse('test.domain.sock'); + subject2.host.should.equal('test.domain.sock'); + }); + + it('initializing with unix domain socket and a specific database, the simple way', () => { + const subject = parse('%2Fconst%2Frun%2F/mydb'); + subject.host.should.equal('/const/run/'); + subject.database.should.equal('mydb'); + }); + + it('initializing with unix domain socket, the healthy way', () => { + const subject = parse('%2Fsome%20path%2F/my%5Bdb%5D?encoding=utf8'); + subject.host.should.equal('/some path/'); + subject.database.should.equal('my[db]', 'must be escaped and unescaped trough "my%5Bdb%5D"'); + subject.client_encoding.should.equal('utf8'); + }); + + it('initializing with unix domain socket, the escaped healthy way', () => { + const subject = parse('%2Fsome%20path%2F/my%2Bdb?encoding=utf8'); + subject.host.should.equal('/some path/'); + subject.database.should.equal('my+db'); + subject.client_encoding.should.equal('utf8'); + }); + + it('initializing with unix domain socket, username and password', () => { + const subject = parse('postgres://brian:pw@%2Fconst%2Frun%2F/mydb'); + subject.user.should.equal('brian'); + subject.password.should.equal('pw'); + subject.host.should.equal('/const/run/'); + subject.database.should.equal('mydb'); + }); + + it('password contains < and/or > characters', () => { + const sourceConfig = { + user: 'brian', + password: 'helloe', + port: 5432, + host: 'localhost', + database: 'postgres' + }; + const connectionString = 'postgres://' + sourceConfig.user + ':' + encodeURIComponent(sourceConfig.password) + '@' + sourceConfig.host + ':' + sourceConfig.port + '/' + sourceConfig.database; + const subject = parse(connectionString); + subject.password.should.equal(sourceConfig.password); + }); + + it('password contains colons', () => { + const sourceConfig = { + user: 'brian', + password: 'hello:pass:world', + port: 5432, + host: 'localhost', + database: 'postgres' + }; + const connectionString = 'postgres://' + sourceConfig.user + ':' + encodeURIComponent(sourceConfig.password) + '@' + sourceConfig.host + ':' + sourceConfig.port + '/' + sourceConfig.database; + const subject = parse(connectionString); + subject.password.should.equal(sourceConfig.password); + }); + + it('url is properly encoded', () => { + const encoded = 'pg://bi%25na%25%25ry%20:s%40f%23@localhost/%20u%2520rl'; + const subject = parse(encoded); + subject.user.should.equal('bi%na%%ry '); + subject.password.should.equal('s@f#'); + subject.host.should.equal('localhost'); + subject.database.should.equal(' u%20rl'); + }); + + it('relative url sets database', () => { + const relative = '/different_db_on_default_host'; + const subject = parse(relative); + subject.database.should.equal('different_db_on_default_host'); + }); + + it('no pathname returns undefined database', () => { + const subject = parse('pg://myhost'); + (subject.database === undefined).should.equal(true); + }); + + it('pathname of "/" returns undefined database', () => { + const subject = parse('pg://myhost/'); + subject.host.should.equal('myhost'); + (subject.database === undefined).should.equal(true); + }); + + it('configuration parameter application_name', () => { + const connectionString = 'pg:///?application_name=TheApp'; + const subject = parse(connectionString); + subject.application_name.should.equal('TheApp'); + }); + + it('configuration parameter fallback_application_name', () => { + const connectionString = 'pg:///?fallback_application_name=TheAppFallback'; + const subject = parse(connectionString); + subject.fallback_application_name.should.equal('TheAppFallback'); + }); + + it('configuration parameter fallback_application_name', () => { + const connectionString = 'pg:///?fallback_application_name=TheAppFallback'; + const subject = parse(connectionString); + subject.fallback_application_name.should.equal('TheAppFallback'); + }); + + it('configuration parameter ssl=true', () => { + const connectionString = 'pg:///?ssl=true'; + const subject = parse(connectionString); + subject.ssl.should.equal(true); + }); + + it('configuration parameter ssl=1', () => { + const connectionString = 'pg:///?ssl=1'; + const subject = parse(connectionString); + subject.ssl.should.equal(true); + }); + + it('configuration parameter ssl=0', () => { + const connectionString = 'pg:///?ssl=0'; + const subject = parse(connectionString); + subject.ssl.should.equal(false); + }); + + it('set ssl', () => { + const subject = parse('pg://myhost/db?ssl=1'); + subject.ssl.should.equal(true); + }); + + it('configuration parameter sslcert=/path/to/cert', () => { + const connectionString = 'pg:///?sslcert=' + encodeURIComponent(path.join(__dirname, '/example.cert')); + const subject = parse(connectionString); + subject.ssl.should.eql({ + cert: 'example cert' + EOL + }); + }); + + it('configuration parameter sslkey=/path/to/key', () => { + const connectionString = 'pg:///?sslkey=' + encodeURIComponent(path.join(__dirname, 'example.key')); + const subject = parse(connectionString); + subject.ssl.should.eql({ + key: 'example key' + EOL + }); + }); + + it('configuration parameter sslrootcert=/path/to/ca', () => { + const connectionString = 'pg:///?sslrootcert=' + encodeURIComponent(path.join(__dirname, 'example.ca')); + const subject = parse(connectionString); + subject.ssl.should.eql({ + ca: 'example ca' + EOL + }); + }); + + it('allow other params like max, ...', () => { + const subject = parse('pg://myhost/db?max=18&min=4'); + subject.max.should.equal('18'); + subject.min.should.equal('4'); + }); + + + it('configuration parameter keepalives', () => { + const connectionString = 'pg:///?keepalives=1'; + const subject = parse(connectionString); + subject.keepalives.should.equal('1'); + }); + + it('unknown configuration parameter is passed into client', () => { + const connectionString = 'pg:///?ThereIsNoSuchPostgresParameter=1234'; + const subject = parse(connectionString); + subject.ThereIsNoSuchPostgresParameter.should.equal('1234'); + }); + + it('do not override a config field with value from query string', () => { + const subject = parse('postgres://some-path.socket/my%5Bdb%5D?encoding=utf8&client_encoding=bogus'); + subject.host.should.equal('some-path.socket'); + subject.database.should.equal('my[db]', 'must to be escaped and unescaped through "my%5Bdb%5D"'); + subject.client_encoding.should.equal('utf8'); + }); + + it('must throw on repeated parameters', () => { + const connectionString = 'pg:///?keepalives=1&keepalives=0'; + expect(() => { + parse(connectionString); + }).throw('Parameter "keepalives" is repeated.'); + }); });