Created
October 31, 2019 01:15
-
-
Save eneuhauser/d749e533e6b9e10fcd658cc94b96c8c6 to your computer and use it in GitHub Desktop.
Connection URL
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'reflect-metadata'; | |
import { Type } from '@nestjs/common'; | |
import { plainToClass } from 'class-transformer'; | |
import { Conn } from './conn.decorator'; | |
import { ConnectionDetails } from './connection-details.interface'; | |
jest.mock('./connection.service', () => ({ | |
addConnectionDetails: () => {}, | |
})); | |
function build<T>(metaType: Type<T>, config: object = {}): T { | |
return plainToClass(metaType, config, { | |
enableImplicitConversion: true, | |
excludeExtraneousValues: true, | |
}); | |
} | |
describe('ConnDecorator', () => { | |
describe('BaseUrl', () => { | |
class BaseUrl { | |
@Conn({ | |
url: 'pg://db', | |
}) | |
conn: ConnectionDetails; | |
} | |
it('should handle base url with no variables', () => { | |
const { conn } = build(BaseUrl); | |
expect(conn.protocol).toBeUndefined(); | |
expect(conn.driver).toBe('pg'); | |
expect(conn.host).toBe('db'); | |
expect(conn.port).toBeUndefined(); | |
expect(conn.username).toBeUndefined(); | |
expect(conn.password).toBeUndefined(); | |
expect(conn.database).toBeUndefined(); | |
expect(conn.schema).toBeUndefined(); | |
}); | |
it('should override values from default variable names', () => { | |
const { conn } = build(BaseUrl, { | |
DB_HOST: 'localhost', | |
DB_PORT: '5433', | |
DB_USER: 'postgres', | |
DB_PASS: 's3cr3t!', | |
}); | |
expect(conn.protocol).toBeUndefined(); | |
expect(conn.driver).toBe('pg'); | |
expect(conn.host).toBe('localhost'); | |
expect(conn.port).toBe(5433); | |
expect(conn.username).toBe('postgres'); | |
expect(conn.password).toBe('s3cr3t!'); | |
expect(conn.database).toBeUndefined(); | |
expect(conn.schema).toBeUndefined(); | |
}); | |
}); | |
describe('FullUrl', () => { | |
class FullUrl { | |
@Conn({ | |
url: | |
'jdbc:pg://admin:r3AdM3!@127.0.0.1:2468/data_base?currentSchema=my_schema', | |
}) | |
conn: ConnectionDetails; | |
} | |
it('should handle full url with no variables', () => { | |
const { conn } = build(FullUrl); | |
expect(conn.protocol).toBe('jdbc'); | |
expect(conn.driver).toBe('pg'); | |
expect(conn.host).toBe('127.0.0.1'); | |
expect(conn.port).toBe(2468); | |
expect(conn.username).toBe('admin'); | |
expect(conn.password).toBe('r3AdM3!'); | |
expect(conn.database).toBe('data_base'); | |
expect(conn.schema).toBe('my_schema'); | |
}); | |
it('should override values from default variable names', () => { | |
const { conn } = build(FullUrl, { | |
DB_HOST: 'localhost', | |
DB_PORT: '5433', | |
DB_USER: 'postgres', | |
DB_PASS: 's3cr3t!', | |
}); | |
expect(conn.protocol).toBe('jdbc'); | |
expect(conn.driver).toBe('pg'); | |
expect(conn.host).toBe('localhost'); | |
expect(conn.port).toBe(5433); | |
expect(conn.username).toBe('postgres'); | |
expect(conn.password).toBe('s3cr3t!'); | |
expect(conn.database).toBe('data_base'); | |
expect(conn.schema).toBe('my_schema'); | |
}); | |
}); | |
describe('Variable Prefix', () => { | |
class Prefix { | |
@Conn({ | |
url: 'pg://db', | |
prefix: 'FOO', | |
}) | |
conn: ConnectionDetails; | |
} | |
class PrefixBarHost { | |
@Conn({ | |
url: 'pg://db', | |
prefix: 'FOO', | |
hostKey: 'BAR', | |
}) | |
conn: ConnectionDetails; | |
} | |
it('should apply prefix to variable names', () => { | |
const { conn } = build(Prefix, { | |
FOO_DB_HOST: 'localhost', | |
FOO_DB_PORT: '5433', | |
FOO_DB_USER: 'postgres', | |
FOO_DB_PASS: 's3cr3t!', | |
}); | |
expect(conn.protocol).toBeUndefined(); | |
expect(conn.driver).toBe('pg'); | |
expect(conn.host).toBe('localhost'); | |
expect(conn.port).toBe(5433); | |
expect(conn.username).toBe('postgres'); | |
expect(conn.password).toBe('s3cr3t!'); | |
expect(conn.database).toBeUndefined(); | |
expect(conn.schema).toBeUndefined(); | |
}); | |
it('should not apply prefix to explicit variable names', () => { | |
const { conn } = build(PrefixBarHost, { | |
BAR: '127.0.0.1', | |
FOO_DB_PORT: '5433', | |
FOO_DB_USER: 'postgres', | |
FOO_DB_PASS: 's3cr3t!', | |
}); | |
expect(conn.protocol).toBeUndefined(); | |
expect(conn.driver).toBe('pg'); | |
expect(conn.host).toBe('127.0.0.1'); | |
expect(conn.port).toBe(5433); | |
expect(conn.username).toBe('postgres'); | |
expect(conn.password).toBe('s3cr3t!'); | |
expect(conn.database).toBeUndefined(); | |
expect(conn.schema).toBeUndefined(); | |
}); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Expose, Transform } from 'class-transformer'; | |
import { ConnectionDetails } from './connection-details.interface'; | |
import { parseConnectionString } from './connection-string.model'; | |
// import { addConnectionDetails } from './connection.service'; | |
interface ConnValues { | |
driver?: string; | |
host?: string; | |
port?: number; | |
username?: string; | |
password?: string; | |
database?: string; | |
schema?: string; | |
ssl?: boolean; | |
} | |
interface ConnKeys { | |
/** | |
* Key to the config variable that contains the host value | |
* | |
* DEFAULT: DB_HOST | `${prefix}_DB_HOST` | |
*/ | |
hostKey?: string; | |
/** | |
* Key to the config variable that contains the port value. | |
* | |
* DEFAULT: DB_PORT | `${prefix}_DB_PORT` | |
*/ | |
portKey?: string; | |
/** | |
* Key to the config variable that contains the username value. | |
* | |
* DEFAULT: DB_USER | `${prefix}_DB_USER` | |
*/ | |
userKey?: string; | |
/** | |
* Key to the config variable that contains the password value | |
* | |
* DEFAULT: DB_PASS | `${prefix}_DB_PASS` | |
*/ | |
passKey?: string; | |
sslKey?: string; | |
} | |
interface ConnUrlOpts extends ConnValues, ConnKeys { | |
/** | |
* Defines the default values for the database connection details. | |
* | |
* Details in the URL connection are used as the base, but are overidden by the individiual | |
* properties. For example, if the URL is 'pg://127.0.0.1:5432' and the 'DB_HOST' config value is | |
* set to 'db', the `host` will be 'db' and the `port` will be 5432. | |
* | |
* Min URL Info: | |
* - pg://db/c2fo | |
* - pg://127.0.0.1/c2fo | |
* | |
* Full Example: | |
* - pg://pollenadmin:su!4you11aaAAaa@db:5432/c2fo?currentSchema=public | |
* - pg://pollenadmin:su!4you11aaAAaa@127.0.0.1:5432/c2fo?currentSchema=public | |
*/ | |
url: string; | |
/** | |
* Only set for the second or greater connection as the name of the connection. This is the name | |
* used when injecting a repository. Typically, this will be blank to use the default connection | |
* name that can be omitted when injecting the connection. | |
*/ | |
name?: string; | |
/** | |
* Prefix added to the default key values. For example, if the prefix is 'C2FO' and no `hostKey` | |
* is defined, the `hostKey` will be set to 'C2FO_DB_HOST'. | |
*/ | |
prefix?: string; | |
} | |
const GLOBAL_DEFAULTS: ConnValues = { | |
driver: 'pg', | |
host: 'db', | |
port: 5432, | |
username: 'admin', | |
password: 'admin', | |
database: 'postgres', | |
schema: 'public', | |
ssl: true, | |
}; | |
/** | |
* Defines the default values for the config keys names. If a prefix is defined, this applies the prefix to the default | |
* keys. | |
*/ | |
function applyDefaults(options: ConnUrlOpts): ConnUrlOpts { | |
const p = options.prefix !== undefined ? `${options.prefix}_` : ''; | |
return { | |
hostKey: `${p}DB_HOST`, | |
portKey: `${p}DB_PORT`, | |
userKey: `${p}DB_USER`, | |
passKey: `${p}DB_PASS`, | |
...options, | |
}; | |
} | |
type TransformFn = (obj: object, name: string) => void; | |
export function Conn<T>(options: ConnUrlOpts): TransformFn { | |
const opts = applyDefaults(options); | |
let resolve; | |
const promise = new Promise<ConnectionDetails>(r => { | |
resolve = r; | |
}); | |
// FIXME This would have to change | |
// addConnectionDetails(promise, opts.name); | |
return (target: object, name: string): void => { | |
// eslint-disable-next-line @typescript-eslint/no-unused-vars | |
Transform((value: unknown, config: object) => { | |
function val(keyName: string, defaultValue: string | number): string { | |
const k = opts[keyName]; | |
const v = typeof k === 'string' ? config[k] : undefined; | |
return v !== undefined ? v : defaultValue; | |
} | |
function num(keyName: string, defaultValue: number): number { | |
const v = val(keyName, defaultValue); | |
return v !== undefined ? parseInt(v, 10) : undefined; | |
} | |
const defaultValueKeys = Object.keys(GLOBAL_DEFAULTS); | |
/* | |
const defaultConn = Object.entries(options) | |
.filter(([k, v]) => v !== undefined && defaultValueKeys.includes(k)) | |
.reduce( | |
(acc, [k, v]) => ({ | |
...acc, | |
[k]: v, | |
}), | |
GLOBAL_DEFAULTS, | |
); | |
*/ | |
const defaultConn = parseConnectionString(options.url); | |
const conn: ConnectionDetails = { | |
protocol: defaultConn.protocol, | |
driver: defaultConn.driver, | |
host: val('hostKey', defaultConn.host), | |
port: num('portKey', defaultConn.port), | |
username: val('userKey', defaultConn.username), | |
password: val('passKey', defaultConn.password), | |
database: defaultConn.database, | |
schema: defaultConn.schema, | |
}; | |
resolve(conn); | |
return conn; | |
})(target, name); | |
Expose()(target, name); | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export interface ConnectionDetails { | |
protocol?: string; | |
driver?: string; | |
host: string; | |
port?: number; | |
username?: string; | |
password?: string; | |
database?: string; | |
schema?: string; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { parseConnectionString } from './connection-string.model'; | |
describe('parseConnectionString', () => { | |
describe('base details', () => { | |
it('should handle minimum string driver://host', () => { | |
expect(parseConnectionString('driver://host')).toEqual({ | |
driver: 'driver', | |
host: 'host', | |
}); | |
expect(parseConnectionString('postgresql://127.0.0.1/test_db')).toEqual({ | |
driver: 'postgresql', | |
host: '127.0.0.1', | |
database: 'test_db', | |
}); | |
}); | |
it('should handle protocol:driver://host:port/database', () => { | |
expect( | |
parseConnectionString('protocol:driver://host:0000/database'), | |
).toEqual({ | |
protocol: 'protocol', | |
driver: 'driver', | |
host: 'host', | |
port: 0, | |
database: 'database', | |
}); | |
expect( | |
parseConnectionString('jdbc:postgresql://127.0.0.1:5432/test_db'), | |
).toEqual({ | |
protocol: 'jdbc', | |
driver: 'postgresql', | |
host: '127.0.0.1', | |
port: 5432, | |
database: 'test_db', | |
}); | |
}); | |
it('should handle driver://host:port', () => { | |
expect(parseConnectionString('driver://host:0000')).toEqual({ | |
driver: 'driver', | |
host: 'host', | |
port: 0, | |
}); | |
expect(parseConnectionString('pg://127.0.0.1:5432')).toEqual({ | |
driver: 'pg', | |
host: '127.0.0.1', | |
port: 5432, | |
}); | |
expect(parseConnectionString('mysql://127.0.0.1:3306')).toEqual({ | |
driver: 'mysql', | |
host: '127.0.0.1', | |
port: 3306, | |
}); | |
}); | |
it('should handle driver://user@host:port', () => { | |
expect(parseConnectionString('driver://user@host:0000')).toEqual({ | |
driver: 'driver', | |
username: 'user', | |
host: 'host', | |
port: 0, | |
}); | |
expect(parseConnectionString('pg://admin@127.0.0.1:5432')).toEqual({ | |
driver: 'pg', | |
username: 'admin', | |
host: '127.0.0.1', | |
port: 5432, | |
}); | |
expect(parseConnectionString('mysql://root@127.0.0.1:3306')).toEqual({ | |
driver: 'mysql', | |
username: 'root', | |
host: '127.0.0.1', | |
port: 3306, | |
}); | |
}); | |
it('should handle driver://user:password@host:port', () => { | |
expect(parseConnectionString('driver://user:pass@host:0000')).toEqual({ | |
driver: 'driver', | |
username: 'user', | |
password: 'pass', | |
host: 'host', | |
port: 0, | |
}); | |
expect(parseConnectionString('pg://admin:admin@127.0.0.1:5432')).toEqual({ | |
driver: 'pg', | |
username: 'admin', | |
password: 'admin', | |
host: '127.0.0.1', | |
port: 5432, | |
}); | |
expect( | |
parseConnectionString('mysql://root:faK3Pa$$W0rd!@127.0.0.1:3306'), | |
).toEqual({ | |
driver: 'mysql', | |
username: 'root', | |
password: 'faK3Pa$$W0rd!', | |
host: '127.0.0.1', | |
port: 3306, | |
}); | |
}); | |
it('should handle driver://user:password@host:port/database', () => { | |
expect( | |
parseConnectionString('driver://user:pass@host:0000/database'), | |
).toEqual({ | |
driver: 'driver', | |
username: 'user', | |
password: 'pass', | |
host: 'host', | |
port: 0, | |
database: 'database', | |
}); | |
expect( | |
parseConnectionString('pg://admin:admin@127.0.0.1:5432/db_name'), | |
).toEqual({ | |
driver: 'pg', | |
username: 'admin', | |
password: 'admin', | |
host: '127.0.0.1', | |
port: 5432, | |
database: 'db_name', | |
}); | |
expect( | |
parseConnectionString( | |
'mysql://root:faK3Pa$$W0rd!@127.0.0.1:3306/1table_name2', | |
), | |
).toEqual({ | |
driver: 'mysql', | |
username: 'root', | |
password: 'faK3Pa$$W0rd!', | |
host: '127.0.0.1', | |
port: 3306, | |
database: '1table_name2', | |
}); | |
}); | |
}); | |
describe('queryString', () => { | |
it('should handle currentSchema query parameter as schema', () => { | |
expect( | |
parseConnectionString( | |
'protocol:driver://user:pass@host:0000/database?currentSchema=schema', | |
), | |
).toEqual({ | |
protocol: 'protocol', | |
driver: 'driver', | |
username: 'user', | |
password: 'pass', | |
host: 'host', | |
port: 0, | |
database: 'database', | |
schema: 'schema', | |
}); | |
expect( | |
parseConnectionString( | |
'pg://admin:admin@127.0.0.1:5432/db_name?currentSchema=public', | |
), | |
).toEqual({ | |
driver: 'pg', | |
username: 'admin', | |
password: 'admin', | |
host: '127.0.0.1', | |
port: 5432, | |
database: 'db_name', | |
schema: 'public', | |
}); | |
expect( | |
parseConnectionString( | |
'mysql://root:faK3Pa$$W0rd!@127.0.0.1:3306/1table_name2?currentSchema=foo_bar', | |
), | |
).toEqual({ | |
driver: 'mysql', | |
username: 'root', | |
password: 'faK3Pa$$W0rd!', | |
host: '127.0.0.1', | |
port: 3306, | |
database: '1table_name2', | |
schema: 'foo_bar', | |
}); | |
}); | |
}); | |
describe('function characteristics', () => { | |
it('should not handle strings without ://', () => { | |
const fn = () => parseConnectionString('pg:db:5432'); | |
expect(fn).toThrow(); | |
}); | |
it('should not handle passwords with @ signs', () => { | |
const fn = () => parseConnectionString('pg://admin:p@ssword@db:5432'); | |
expect(fn).toThrow(); | |
}); | |
it('should not return properties with undefined values', () => { | |
const required = new Set(['driver', 'host']); | |
Object.entries(parseConnectionString('driver://host')) | |
// Only check optional fields | |
.filter(([key]) => !required.has(key)) | |
.forEach(([key, value]) => { | |
expect(key).toBeDefined(); | |
expect(key).not.toBeNull(); | |
expect(key).not.toBe(''); | |
expect(value).toBeDefined(); | |
expect(value).not.toBeNull(); | |
expect(value).not.toBe(''); | |
}); | |
}); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ConnectionDetails } from './connection-details.interface'; | |
interface ConnQuery { | |
currentSchema?: string; | |
maxConnections?: string; | |
} | |
function reduceTuples<T>(obj: T, [key, value]): T { | |
if (value === undefined) return obj; | |
return { | |
...obj, | |
[key]: value, | |
}; | |
} | |
/* | |
* The group that captures the host excludes @ in case an @ is in the password it will incorrectly | |
* include in the host. The $ on the end ensures the whole string is valid. | |
* | |
* Minimum: | |
* driver://host | |
* | |
* Maximum: | |
* protocol:driver://username:password@host:0000/database?currentSchema=schema&maxConnections=1 | |
*/ | |
const CONN_REGEX = /^(?:([^:]+):)?([^:]+):\/\/(?:([^:@]+)(?::([^@]+))?@)?([^:/@]+)(?::(\d+))?(?:\/([^?]+))?(?:\?(.*))?$/; | |
export function parseConnectionString(uri): ConnectionDetails { | |
const [ | |
, | |
protocol, | |
driver, | |
username, | |
password, | |
host, | |
port, | |
database, | |
query = '', | |
] = CONN_REGEX.exec(uri); | |
const options: ConnQuery = query | |
.split('&') | |
.map(p => p.split('=')) | |
.reduce(reduceTuples, {}); | |
return Object.entries({ | |
// Optional fields | |
protocol, | |
username, | |
password, | |
port: port !== undefined ? +port : undefined, | |
database, | |
schema: options.currentSchema, | |
}).reduce(reduceTuples, { | |
// Required fields | |
driver, | |
host, | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment