├── .gitignore ├── test ├── helpers │ └── setup.js ├── parse_test.js └── parse_cases.json ├── .travis.yml ├── package.json ├── LICENSE.txt ├── README.md └── lib └── parse-database-url.js /.gitignore: -------------------------------------------------------------------------------- 1 | # npm 2 | /node_modules 3 | parse-database-url-*.tgz 4 | 5 | # Vim. 6 | *.sw* 7 | -------------------------------------------------------------------------------- /test/helpers/setup.js: -------------------------------------------------------------------------------- 1 | global.parseDatabaseUrl = require("../../lib/parse-database-url"); 2 | 3 | global.chai = require("chai"); 4 | global.asset = global.chai.assert; 5 | global.expect = global.chai.expect; 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "0.12" 5 | - "4" 6 | - "5" 7 | compiler: clang-3.7 8 | env: 9 | - CXX=clang-3.7 10 | addons: 11 | apt: 12 | sources: 13 | - llvm-toolchain-precise-3.7 14 | - ubuntu-toolchain-r-test 15 | packages: 16 | - clang-3.6 17 | - g++-5 18 | 19 | -------------------------------------------------------------------------------- /test/parse_test.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var path = require("path"); 3 | 4 | describe("parseDatabaseUrl", function() { 5 | var data = fs.readFileSync(path.join(__dirname, "parse_cases.json"), "utf8"); 6 | var parseCases = JSON.parse(data); 7 | var _length = parseCases.length; 8 | for (var i = 0; i < _length; ++i) { 9 | var _parseCase = parseCases[i]; 10 | 11 | (function(parseCase) { 12 | it("parses a " + parseCase.desc, function() { 13 | expect(parseDatabaseUrl(parseCase.url)).to.deep. 14 | equal(parseCase.config); 15 | }); 16 | })(_parseCase); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parse-database-url", 3 | "version": "0.3.0", 4 | "description": "Database configuration URL parser for node.js", 5 | "keywords": ["database", "configuration", "url", "heroku", "database_url"], 6 | "homepage": "https://github.com/pwnall/node-parse-database-url", 7 | "author": "Victor Costan (http://www.costan.us)", 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/pwnall/node-parse-database-url.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/pwnall/node-parse-database-url/issues" 15 | }, 16 | "engines": { 17 | "node": ">= 0.6" 18 | }, 19 | "dependencies": { 20 | "mongodb-uri": ">= 0.9.7" 21 | }, 22 | "devDependencies": { 23 | "chai": ">= 3.4.1", 24 | "mocha": ">= 2.3.4" 25 | }, 26 | "main": "lib/parse-database-url.js", 27 | "directories": { 28 | "lib": "lib", 29 | "test": "test" 30 | }, 31 | "scripts": { 32 | "test": "node_modules/mocha/bin/mocha --ui bdd --require test/helpers/setup.js --recursive test/" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Victor Costan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DATABASE_URL parser for node.js 2 | 3 | [![Build Status](https://travis-ci.org/pwnall/node-parse-database-url.svg)](https://travis-ci.org/pwnall/node-parse-database-url) 4 | [![NPM Version](http://img.shields.io/npm/v/parse-database-url.svg)](https://www.npmjs.org/package/parse-database-url) 5 | 6 | This is an [npm](https://npmjs.org/) package that parses database 7 | configurations passed in as URLs. This is typically useful in Heroku 8 | deployments, where the database configuration is given in the `DATABASE_URL` 9 | environment variable. 10 | 11 | 12 | ## Supported Platforms 13 | 14 | This library is tested against the following platforms. 15 | 16 | * [node.js](http://nodejs.org/) 0.6 17 | * [node.js](http://nodejs.org/) 0.8 18 | * [node.js](http://nodejs.org/) 0.10 19 | 20 | Keep in mind that the versions above are not hard requirements. 21 | 22 | 23 | ## Installation and Usage 24 | 25 | The preferred installation method is to add the library to the `dependencies` 26 | section in your `package.json`. 27 | 28 | ```json 29 | { 30 | "dependencies": { 31 | "parse-database-url": "*" 32 | } 33 | } 34 | ``` 35 | 36 | Alternatively, `npm` can be used to install the library directly. 37 | 38 | ```bash 39 | npm install parse-database-url 40 | ``` 41 | 42 | Once the library is installed, `require`-ing it returns the function that 43 | parses URLs. 44 | 45 | ```javascript 46 | var parseDbUrl = require("parse-database-url"); 47 | ``` 48 | 49 | And you can use that to parse URLs :) 50 | 51 | ```javascript 52 | var dbConfig = parseDbUrl(process.env["DATABASE_URL"]); 53 | ``` 54 | 55 | 56 | ## Development 57 | 58 | The library comes with unit tests. Please make sure they pass before submitting 59 | a pull request. 60 | 61 | ```bash 62 | npm install 63 | npm test 64 | ``` 65 | 66 | If you modify the parser, please add test cases showing your changes to 67 | [test/parse_cases.json](test/parse_cases.json). 68 | 69 | 70 | ## Copyright and License 71 | 72 | The library is Copyright (c) 2013 Victor Costan, and distributed under the MIT 73 | License. 74 | -------------------------------------------------------------------------------- /lib/parse-database-url.js: -------------------------------------------------------------------------------- 1 | var url = require("url"); 2 | var querystring = require("querystring"); 3 | 4 | var mongodbUri = require("mongodb-uri"); 5 | 6 | /** 7 | * This is the exported function that parses database URLs. 8 | * 9 | * @param {String} databaseUrl the URL to be parsed 10 | * @return {Object} the database configuration; this will 11 | * always have the "driver" key pointing to a database driver, and may 12 | * have some of the following keys: "host", "port", "user", "password", 13 | * "database", "filename" 14 | */ 15 | module.exports = function (databaseUrl) { 16 | var parsedUrl = url.parse(databaseUrl, false, true); 17 | 18 | // Query parameters end up directly in the configuration. 19 | var config = querystring.parse(parsedUrl.query); 20 | 21 | config.driver = (parsedUrl.protocol || "sqlite3:") 22 | // The protocol coming from url.parse() has a trailing : 23 | .replace(/\:$/, ""); 24 | 25 | // Cloud Foundry will sometimes set a 'mysql2' scheme instead of 'mysql'. 26 | if (config.driver == "mysql2") 27 | config.driver = "mysql"; 28 | 29 | // url.parse() produces an "auth" that looks like "user:password". No 30 | // individual fields, unfortunately. 31 | if (parsedUrl.auth) { 32 | var userPassword = parsedUrl.auth.split(":", 2); 33 | config.user = userPassword[0]; 34 | if (userPassword.length > 1) { 35 | config.password = userPassword[1]; 36 | } 37 | } 38 | 39 | if (config.driver === "sqlite3") { 40 | if (parsedUrl.hostname) { 41 | if (parsedUrl.pathname) { 42 | // Relative path. 43 | config.filename = parsedUrl.hostname + parsedUrl.pathname; 44 | } else { 45 | // Just a filename. 46 | config.filename = parsedUrl.hostname; 47 | } 48 | } else { 49 | // Absolute path. 50 | config.filename = parsedUrl.pathname; 51 | } 52 | } else { 53 | if (config.driver === "mongodb") { 54 | // MongoDB URLs can have multiple comma-separated host:port pairs. This 55 | // trips up the standard URL parser. 56 | var mongoParsedUrl = mongodbUri.parse(databaseUrl); 57 | parsedUrl = {}; 58 | if (mongoParsedUrl.hosts) { 59 | config.hosts = mongoParsedUrl.hosts; 60 | for (var i = 0; i < config.hosts.length; i += 1) { 61 | if (config.hosts[i].port) 62 | config.hosts[i].port = config.hosts[i].port.toString(); 63 | } 64 | if (config.hosts.length === 1) { 65 | if (config.hosts[0].host) 66 | config.host = config.hosts[0].host; 67 | if (config.hosts[0].port) 68 | config.port = config.hosts[0].port.toString(); 69 | } 70 | } 71 | if (mongoParsedUrl.database) 72 | config.database = mongoParsedUrl.database; 73 | } else { 74 | // Some drivers (e.g., redis) don't have database names. 75 | if (parsedUrl.pathname) { 76 | config.database = 77 | parsedUrl.pathname.replace(/^\//, "").replace(/\/$/, ""); 78 | } 79 | } 80 | 81 | if (parsedUrl.hostname) config.host = parsedUrl.hostname; 82 | if (parsedUrl.port) config.port = parsedUrl.port; 83 | } 84 | 85 | return config; 86 | }; 87 | -------------------------------------------------------------------------------- /test/parse_cases.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "desc": "PostgreSQL URL with username/password", 4 | "url": "postgres://pwnall:sekret@localhost/pwnbase", 5 | "config": { 6 | "driver": "postgres", 7 | "user": "pwnall", 8 | "password": "sekret", 9 | "host": "localhost", 10 | "database": "pwnbase" 11 | } 12 | }, 13 | { 14 | "desc": "MySQL URL with username/password and host/port", 15 | "url": "mysql://someuser@server.heroku.com:1337/herokudb", 16 | "config": { 17 | "driver": "mysql", 18 | "user": "someuser", 19 | "host": "server.heroku.com", 20 | "port": "1337", 21 | "database": "herokudb" 22 | } 23 | }, 24 | { 25 | "desc": "SQLite URL with file name", 26 | "url": "sqlite3://development.sqlite", 27 | "config": { 28 | "driver": "sqlite3", 29 | "filename": "development.sqlite" 30 | } 31 | }, 32 | { 33 | "desc": "SQLite URL with relative path", 34 | "url": "sqlite3://path/to/test.sqlite", 35 | "config": { 36 | "driver": "sqlite3", 37 | "filename": "path/to/test.sqlite" 38 | } 39 | }, 40 | { 41 | "desc": "SQLite URL with absolute path", 42 | "url": "sqlite3:///path/to/test.sqlite", 43 | "config": { 44 | "driver": "sqlite3", 45 | "filename": "/path/to/test.sqlite" 46 | } 47 | }, 48 | { 49 | "desc": "URL with query parameters (Issue #3)", 50 | "url": "postgres://user:pass@localhost/database?native=true", 51 | "config": { 52 | "driver": "postgres", 53 | "user": "user", 54 | "password": "pass", 55 | "host": "localhost", 56 | "database": "database", 57 | "native": "true" 58 | } 59 | }, 60 | { 61 | "desc": "Heroku ClearDB connection string (Issue #1)", 62 | "url": "mysql://user:pass@host/database?reconnect=true", 63 | "config": { 64 | "driver": "mysql", 65 | "user": "user", 66 | "password": "pass", 67 | "host": "host", 68 | "database": "database", 69 | "reconnect": "true" 70 | } 71 | }, 72 | { 73 | "desc": "Cloud Foundry ClearDB connection string with mysql2 scheme", 74 | "url": "mysql2://user:pass@host/database?reconnect=true", 75 | "config": { 76 | "driver": "mysql", 77 | "user": "user", 78 | "password": "pass", 79 | "host": "host", 80 | "database": "database", 81 | "reconnect": "true" 82 | } 83 | }, 84 | { 85 | "desc": "Redis URL, which doesn't have a path (Issue #4)", 86 | "url": "redis://localhost:6379", 87 | "config": { 88 | "driver": "redis", 89 | "host": "localhost", 90 | "port": "6379" 91 | } 92 | }, 93 | { 94 | "desc": "MongoDB URL with username and host", 95 | "url": "mongodb://someuser@server.heroku.com/herokudb", 96 | "config": { 97 | "driver": "mongodb", 98 | "user": "someuser", 99 | "host": "server.heroku.com", 100 | "hosts": [ 101 | { 102 | "host": "server.heroku.com" 103 | } 104 | ], 105 | "database": "herokudb" 106 | } 107 | }, 108 | { 109 | "desc": "MongoDB URL with username/password and host/port", 110 | "url": "mongodb://user:pass@server.heroku.com:1337/herokudb", 111 | "config": { 112 | "driver": "mongodb", 113 | "user": "user", 114 | "password": "pass", 115 | "host": "server.heroku.com", 116 | "port": "1337", 117 | "hosts": [ 118 | { 119 | "host": "server.heroku.com", 120 | "port": "1337" 121 | } 122 | ], 123 | "database": "herokudb" 124 | } 125 | }, 126 | { 127 | "desc": "MongoDB URL with username/password and multiple hosts/ports (Issue #6)", 128 | "url": "mongodb://user:pass@server1.heroku.com:1337,server2.heroku.com:1338,server3.heroku.com:1339/herokudb", 129 | "config": { 130 | "driver": "mongodb", 131 | "user": "user", 132 | "password": "pass", 133 | "hosts": [ 134 | { 135 | "host": "server1.heroku.com", 136 | "port": "1337" 137 | }, 138 | { 139 | "host": "server2.heroku.com", 140 | "port": "1338" 141 | }, 142 | { 143 | "host": "server3.heroku.com", 144 | "port": "1339" 145 | } 146 | ], 147 | "database": "herokudb" 148 | } 149 | } 150 | ] 151 | --------------------------------------------------------------------------------