Skip to content

Instantly share code, notes, and snippets.

@panzi
Last active August 30, 2022 20:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save panzi/7e300b1535e53cc4e52638be005281ee to your computer and use it in GitHub Desktop.
Save panzi/7e300b1535e53cc4e52638be005281ee to your computer and use it in GitHub Desktop.
Simple parser for common database URLs. (MIT License)
// Copyright 2022 Mathias Panzenböck
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
export interface DatabaseConfig {
protocol: string;
}
export interface RemoteDatabaseConfig {
host: string;
port?: number;
database: string;
tableName?: string;
username?: string;
password?: string;
}
export interface SQLiteConfig extends DatabaseConfig {
protocol: 'sqlite';
path: string;
tableName?: string;
}
export interface MemoryConfig extends DatabaseConfig {
protocol: 'memory';
}
export interface PostgreSQLConfig extends DatabaseConfig, RemoteDatabaseConfig {
protocol: 'postgres';
}
export interface MySQLConfig extends DatabaseConfig, RemoteDatabaseConfig {
protocol: 'mysql';
}
export interface MongoDBConfig extends DatabaseConfig {
protocol: 'mongodb';
host: string;
port?: number;
database: string;
collectionName?: string;
username?: string;
password?: string;
}
export interface RedisConfig extends DatabaseConfig {
protocol: 'redis';
host: string;
port?: number;
database: number;
keyPrefix?: string;
username?: string;
password?: string;
}
export type AnyDatabaseConfig = SQLiteConfig | PostgreSQLConfig | MemoryConfig | RedisConfig | MySQLConfig | MongoDBConfig;
export function parseRemoteDatabasePayload(payload: string): RemoteDatabaseConfig {
const match = /^(?:([-_%.a-z0-9]+)(?::([-_%.a-z0-9]+))?@)?([-_%.a-z0-9]+)(?::([0-9]+))?\/([-_%.\/:,a-z0-9]+)(?:#([-_%.\/:,a-z0-9]+))?$/i.exec(payload);
if (!match) {
throw new SyntaxError(`illegal database URL: ${payload}`);
}
const username = decodeURIComponent(match[1]);
const password = decodeURIComponent(match[2]);
const host = match[3];
const port = match[4];
const database = decodeURIComponent(match[5]);
const tableName = match[6];
const config: RemoteDatabaseConfig = {
host,
database,
};
if (tableName) {
config.tableName = decodeURIComponent(tableName);
}
if (username) {
config.username = username;
}
if (password) {
config.password = password;
}
if (port !== undefined) {
const portNumber = parseInt(port, 10);
if (!isFinite(portNumber) || portNumber <= 0) {
throw new SyntaxError(`illegal port number in database URL: ${payload}`);
}
config.port = portNumber;
}
return config;
}
/**
* Parse database URL.
*
* URL format:
*
* <protocol>://[<username>:<password>@]<host>[:<port>]/<database>[#<table-or-collection>]
* sqlite://<path>[#<table>]
*
* <protocol> ::= postgres | memory | redis | mysql | mongodb
*
* For non-SQLite protocols the components <username>, <password>, <database> and <table-or-collection>
* are URL encoded. See: encodeURIComponent()
*
*/
export function parseDatabaseUrl(url: string): AnyDatabaseConfig {
const match = /^([-_a-z0-9]+):\/\/(.*)$/i.exec(url);
if (!match) {
throw new SyntaxError(`illegal database URL: ${url}`);
}
const protocol = match[1].toLowerCase();
const payload = match[2];
switch (protocol) {
case 'sqlite':
{
const [ path, tableName ] = payload.split('#');
return tableName ? { protocol, path, tableName } : { protocol, path };
}
case 'postgres':
case 'mysql':
return { protocol, ...parseRemoteDatabasePayload(payload) };
case 'memory':
if (payload) {
throw new SyntaxError(`illegal database URL: ${url}`);
}
return { protocol };
case 'redis':
{
const opts = parseRemoteDatabasePayload(payload);
const database = parseInt(opts.database, 10);
if (!isFinite(database) || database < 0 || database > 15) {
throw new SyntaxError(`illegal database URL: ${url}`);
}
const keyPrefix = opts.tableName;
delete opts.tableName;
return keyPrefix ? { protocol, ...opts, database, keyPrefix } : { protocol, ...opts, database };
}
case 'mongodb':
{
const opts = parseRemoteDatabasePayload(payload);
const collectionName = opts.tableName;
delete opts.tableName;
return collectionName ? { protocol, ...opts, collectionName } : { protocol, ...opts };
}
default:
throw new SyntaxError(`unsupported database URL: ${url}`);
}
}
export function toURLString(config: AnyDatabaseConfig): string {
switch (config.protocol) {
case 'sqlite':
{
let url = `sqlite://${config.path}`;
if (config.tableName) {
url = `${url}#${config.tableName}`;
}
return url;
}
case 'memory':
return 'memory://';
default:
{
const { protocol, username, password, port } = config;
const host = port !== undefined ? `${config.host}:${port}` : config.host;
let auth = username ? encodeURIComponent(username) : '';
if (password) {
auth = `${auth}:${encodeURIComponent(password)}@`;
} else if (auth) {
auth += '@';
}
let url = `${protocol}://${auth}${host}/${encodeURIComponent(config.database)}`;
const hash =
protocol === 'mongodb' ? config.collectionName :
protocol === 'redis' ? config.keyPrefix :
config.tableName;
if (hash) {
url = `${url}#${encodeURIComponent(hash)}`;
}
return url;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment