Skip to content

Commit

Permalink
Support database connection URL
Browse files Browse the repository at this point in the history
  • Loading branch information
ivawzh committed Aug 25, 2020
1 parent 8641955 commit bced4ea
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 48 deletions.
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ API
```ts
function createDatabase(newDbConfig: NewDbConfig, dbCredential?: Partial<DbCredential>): Promise<void>

function dropDatabase(dropDbConfig: DropDbConfig, dbCredential?: DbCredential): Promise<void>
function dropDatabase(dropDbConfig: DropDbConfig, dbCredential?: Partial<DbCredential>): Promise<void>

export type NewDbConfig = {
databaseName: string,
Expand All @@ -59,7 +59,6 @@ export type DbCredential = {
port: number
host: string
password: string
connectionString?: string
}

const defaultDbCred: DbCredential = {
Expand Down Expand Up @@ -96,9 +95,10 @@ USAGE
OPTIONS
-e, --errorIfExist [default: false] whether throw error if DB already exists
-h, --help show CLI help
-h, --host=host [default: localhost] DB host
-o, --host=host [default: localhost] DB host
-i, --initialDb=initialDb [default: postgres] Initial DB name
-n, --databaseName=databaseName (required) new DB name
-n, --databaseName=databaseName new DB name
-l, --url=url new DB URL
-p, --port=port [default: 5432] DB port, default `5432`
-u, --userName=userName [default: postgres] DB user name
-w, --password=password [default: empty] DB password
Expand All @@ -108,6 +108,7 @@ ALIASES
EXAMPLES
$ pg-god db-create --databaseName=bank-db
$ pg-god db-create --url postgresql://localhost:5432/bank-db
$ pg-god db-create --databaseName=bank-db --errorIfExist
$ pg-god db-create --databaseName=bank-db --password=123 --port=5433 --host=a.example.com --userName=beer
```
Expand All @@ -124,9 +125,10 @@ OPTIONS
-e, --errorIfNonExist [default: false] whether throw error if DB doesn't exist
-d, --dropConnections [default: true] whether automatically drop DB connections
-h, --help show CLI help
-h, --host=host [default: localhost] DB host
-o, --host=host [default: localhost] DB host
-i, --initialDb=initialDb [default: postgres] Initial DB name
-n, --databaseName=databaseName (required) name of DB attempt to drop
-n, --databaseName=databaseName name of DB that will be dropped
-l, --url=url URL of DB that will be dropped
-p, --port=port [default: 5432] DB port, default `5432`
-u, --userName=userName [default: postgres] DB user name
-w, --password=password [default: empty] DB password
Expand All @@ -136,6 +138,7 @@ ALIASES
EXAMPLES
$ pg-god db-drop --databaseName=bank-db
$ pg-god db-drop --url postgresql://localhost:5432/bank-db
$ pg-god db-drop --databaseName=bank-db --errorIfNonExist --no-dropConnections
$ pg-god db-drop --databaseName=bank-db --password=123 --port=5433 --host=a.example.com --userName=beer
```
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "pg-god",
"description": "Tiny library that helps create and kill PostgreSQL database.",
"version": "1.0.8",
"version": "1.0.9",
"author": "ivan.wang @ivawzh",
"bin": {
"pg-god": "./bin/run"
Expand Down
45 changes: 31 additions & 14 deletions src/commands/db-create.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Command, flags} from '@oclif/command'
import { createDatabase } from '../god-stuff'
import { createDatabase, parseDbUrl, merge } from '../god-stuff'
import cli from 'cli-ux'

export default class DbCreate extends Command {
Expand All @@ -8,44 +8,61 @@ export default class DbCreate extends Command {

static examples = [
`$ pg-god db-create --databaseName=bank-db`,
`$ pg-god db-create --url postgresql://localhost:5432/bank-db`,
`$ pg-god db-create --databaseName=bank-db --errorIfExist`,
`$ pg-god db-create --databaseName=bank-db --password=123 --port=5433 --host=a.example.com --userName=beer`,
]

static flags = {
help: flags.help({char: 'h'}),
databaseName: flags.string({char: 'n', required: true, description: 'new DB name', env: 'DB_NAME'}),
errorIfExist: flags.boolean({char: 'e', default: false, description: '[default: false] whether throw error if DB already exists', env: 'DB_ERROR_IF_EXIST'}),
userName: flags.string({char: 'u', default: 'postgres', description: 'DB user name', env: 'DB_USERNAME'}),
initialDb: flags.string({char: 'i', default: 'postgres', description: 'Initial DB name', env: 'DB_INITIAL'}),
port: flags.integer({char: 'p', default: 5432, description: 'DB port, default `5432`', env: 'DB_PORT'}),
host: flags.string({char: 'h', default: 'localhost', description: 'DB host', env: 'DB_HOST'}),
password: flags.string({char: 'w', default: '', description: '[default: empty] DB password', env: 'DB_PASSWORD'}),
databaseName: flags.string({char: 'n', description: 'new DB name', env: 'DB_NAME', exclusive: ['url']}),
userName: flags.string({char: 'u', default: 'postgres', description: 'DB user name', env: 'DB_USERNAME', exclusive: ['url']}),
port: flags.integer({char: 'p', default: 5432, description: 'DB port, default `5432`', env: 'DB_PORT', exclusive: ['url']}),
host: flags.string({char: 'o', default: 'localhost', description: 'new DB host', env: 'DB_HOST', exclusive: ['url']}),
password: flags.string({char: 'w', default: '', description: '[default: empty] DB password', env: 'DB_PASSWORD', exclusive: ['url']}),
url: flags.string({char: 'l', description: 'DB URL, e.g. postgres://username:password@localhost:5432/my_db', env: 'DB_URL', exclusive: [
'databaseName',
'userName',
'port',
'host',
'password',
]}),
}

async run() {
const {
flags: {
databaseName,
help,
errorIfExist,
userName,
initialDb,
port,
host,
password,
url: dbUrl,
...flags
}
} = this.parse(DbCreate)
const urlParams = parseDbUrl(dbUrl)
const finalParams = merge(urlParams, flags)
const {
databaseName,
userName,
port,
host,
password,
} = finalParams

cli.action.start(`😇 Create database '${databaseName}'`)

if(!databaseName) throw new Error('Missing required flags/ENV - databaseName("DB_NAME") or url("DB_URL")')

await createDatabase(
{ databaseName, errorIfExist },
{
user: userName,
database: initialDb,
port: port,
host: host,
password: password,
port,
host,
password,
}
)

Expand Down
46 changes: 32 additions & 14 deletions src/commands/db-drop.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Command, flags} from '@oclif/command'
import { dropDatabase } from '../god-stuff'
import { dropDatabase, parseDbUrl, merge } from '../god-stuff'
import cli from 'cli-ux'

export default class DbDrop extends Command {
Expand All @@ -8,38 +8,56 @@ export default class DbDrop extends Command {

static examples = [
`$ pg-god db-drop --databaseName=bank-db`,
`$ pg-god db-drop --url postgresql://localhost:5432/bank-db`,
`$ pg-god db-drop --databaseName=bank-db --errorIfNonExist --no-dropConnections`,
`$ pg-god db-drop --databaseName=bank-db --password=123 --port=5433 --host=a.example.com --userName=beer`,
]

static flags = {
help: flags.help({char: 'h'}),
databaseName: flags.string({char: 'n', required: true, description: 'name of DB attempt to drop', env: 'DB_NAME'}),
errorIfNonExist: flags.boolean({char: 'e', default: false, description: "[default: false] whether throw error if DB doesn't exist", env: 'DB_ERROR_IF_NON_EXIST'}),
dropConnections: flags.boolean({char: 'd', default: true, allowNo: true, description: "[default: true] whether automatically drop DB connections"}),
userName: flags.string({char: 'u', default: 'postgres', description: 'DB user name', env: 'DB_USERNAME'}),
initialDb: flags.string({char: 'i', default: 'postgres', description: 'Initial DB name', env: 'DB_INITIAL'}),
port: flags.integer({char: 'p', default: 5432, description: 'DB port, default `5432`', env: 'DB_PORT'}),
host: flags.string({char: 'h', default: 'localhost', description: 'DB host', env: 'DB_HOST'}),
password: flags.string({char: 'w', default: '', description: '[default: empty] DB password', env: 'DB_PASSWORD'}),
databaseName: flags.string({char: 'n', description: 'name of DB that will be dropped', env: 'DB_NAME', exclusive: ['url']}),
userName: flags.string({char: 'u', default: 'postgres', description: 'DB user name', env: 'DB_USERNAME', exclusive: ['url']}),
port: flags.integer({char: 'p', default: 5432, description: 'DB port, default `5432`', env: 'DB_PORT', exclusive: ['url']}),
host: flags.string({char: 'o', default: 'localhost', description: 'DB host', env: 'DB_HOST', exclusive: ['url']}),
password: flags.string({char: 'w', default: '', description: '[default: empty] DB password', env: 'DB_PASSWORD', exclusive: ['url']}),
url: flags.string({char: 'l', description: 'URL of DB that will be dropped, e.g. postgres://username:password@localhost:5432/my_db', env: 'DB_URL', exclusive: [
'databaseName',
'userName',
'port',
'host',
'password',
]}),
}

async run() {
const {
flags: {
databaseName,
help,
errorIfNonExist,
dropConnections,
userName,
initialDb,
port,
host,
password,
url: dbUrl,
...flags
}
} = this.parse(DbDrop)

const urlParams = parseDbUrl(dbUrl)
const finalParams = merge(urlParams, flags)
const {
databaseName,
userName,
port,
host,
password,
} = finalParams

cli.action.start(`😇 Drop database '${databaseName}'`)

if(!databaseName) throw new Error('Missing required flags/ENV - databaseName("DB_NAME") or url("DB_URL")')

await dropDatabase(
{
databaseName,
Expand All @@ -49,9 +67,9 @@ export default class DbDrop extends Command {
{
user: userName,
database: initialDb,
port: port,
host: host,
password: password,
port,
host,
password,
}
)

Expand Down
47 changes: 34 additions & 13 deletions src/god-stuff.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Client } from 'pg'
import { PgGodError } from './error'
import * as url from 'url'

export type DbCredential = {
user: string
database: string
port: number
host: string
password: string
connectionString?: string
}

const defaultDbCred: DbCredential = {
Expand All @@ -31,12 +31,10 @@ export type NewDbConfig = {
*
* @example createDatabase({ databaseName: 'bank-development' })
*/
export async function createDatabase(newDbConfig: NewDbConfig, dbCredential: Partial<DbCredential>=defaultDbCred) {
const cred = { ...defaultDbCred, ...dbCredential }
const client = new Client(cred.connectionString ? { connectionString: cred.connectionString } : cred)
export async function createDatabase(newDbConfig: NewDbConfig, dbCredential:Partial<DbCredential>=defaultDbCred) {
const client = new Client({ ...defaultDbCred, ...dbCredential })
try {
client.connect()

await client.connect()
const existingDb = await client.query(`
SELECT datname
FROM pg_catalog.pg_database
Expand Down Expand Up @@ -68,12 +66,10 @@ export type DropDbConfig = {
*
* @example dropDatabase({ databaseName: 'bank-development' })
*/
export async function dropDatabase(dropDbConfig: DropDbConfig, dbCredential: DbCredential=defaultDbCred) {
const cred = { ...defaultDbCred, ...dbCredential }
const client = new Client(cred.connectionString ? { connectionString: cred.connectionString } : cred)
export async function dropDatabase(dropDbConfig: DropDbConfig, dbCredential: Partial<DbCredential>=defaultDbCred) {
const client = new Client({ ...defaultDbCred, ...dbCredential })
try {
client.connect()

await client.connect()
const existingDb = await client.query(`
SELECT datname
FROM pg_catalog.pg_database
Expand All @@ -83,7 +79,7 @@ export async function dropDatabase(dropDbConfig: DropDbConfig, dbCredential: DbC
if (existingDb.rowCount === 0 && dropDbConfig.errorIfNonExist) throw PgGodError.dbDoesNotExist()
if (existingDb.rowCount === 0 && !dropDbConfig.errorIfNonExist) return

if (dropDbConfig.dropConnections !== false) await dropDbConnections(client, dropDbConfig.databaseName)
if (dropDbConfig.dropConnections !== false) await dropDbOtherUserConnections(client, dropDbConfig.databaseName)

await client.query(`DROP DATABASE "${dropDbConfig.databaseName}";`)
} catch (error) {
Expand All @@ -93,7 +89,7 @@ export async function dropDatabase(dropDbConfig: DropDbConfig, dbCredential: DbC
}
}

async function dropDbConnections(client: Client, dbName: string) {
async function dropDbOtherUserConnections(client: Client, dbName: string) {
return client.query(`
SELECT
pg_terminate_backend(pg_stat_activity.pid)
Expand All @@ -104,3 +100,28 @@ async function dropDbConnections(client: Client, dbName: string) {
AND pid <> pg_backend_pid();
`)
}

export function parseDbUrl(dbUrl: string | undefined) {
if (!dbUrl) return {}
const urlQuery = url.parse(dbUrl)
return {
scheme: urlQuery.protocol?.substr(0, urlQuery.protocol?.length - 1),
userName: urlQuery.auth?.substr(0, urlQuery.auth?.indexOf(':')),
password: urlQuery.auth?.substr(urlQuery.auth?.indexOf(':') + 1, urlQuery.auth?.length),
host: urlQuery.hostname,
port: urlQuery.port,
databaseName: urlQuery.path?.slice(1),
}
}

/**
* Shallow merge objects without overriding fields with `undefined`.
* TODO: return better types
*/
export function merge(target: object, ...sources: object[]) {
return Object.assign({}, target, ...sources.map(x =>
Object.entries(x)
.filter(([key, value]) => value !== undefined)
.reduce((obj, [key, value]) => (obj[key] = value, obj), {} as Record<string, undefined>)
))
}

0 comments on commit bced4ea

Please sign in to comment.