Skip to content

Instantly share code, notes, and snippets.

@eneuhauser
Created October 31, 2019 01:15
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eneuhauser/d749e533e6b9e10fcd658cc94b96c8c6 to your computer and use it in GitHub Desktop.
Save eneuhauser/d749e533e6b9e10fcd658cc94b96c8c6 to your computer and use it in GitHub Desktop.
Connection URL
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();
});
});
});
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);
};
}
export interface ConnectionDetails {
protocol?: string;
driver?: string;
host: string;
port?: number;
username?: string;
password?: string;
database?: string;
schema?: string;
}
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('');
});
});
});
});
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