Skip to content

Commit

Permalink
refactor: replace commander with built-in node parseArgs
Browse files Browse the repository at this point in the history
  • Loading branch information
aldis-ameriks committed Aug 12, 2023
1 parent 0468491 commit 4bfc1b3
Show file tree
Hide file tree
Showing 15 changed files with 5,507 additions and 5,553 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Options:
-o, --output <output> file output path (default: "stdout")
-e, --exclude <exclude> excluded tables and enums as comma separated string e.g. knex_migrations,knex_migrations_lock (default: [])
--type use type definitions instead of interfaces in generated output (default: false)
--noSemi, --no-semicolons omit semicolons in generated types
--noSemi, --no-semicolons omit semicolons in generated types (default: false)
--ssl use ssl (default: false)
--optionals use optionals "?" instead of null (default: false)
--comments generate table and column comments (default: false)
Expand Down
157 changes: 109 additions & 48 deletions cjs/src/index.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,85 @@
#! /usr/bin/env node

const fs = require('node:fs')
const { Command } = require('commander')
const { parseArgs } = require('node:util')
const typescript = require('./typescript.js')
const postgres = require('./postgres.js')

const packageJson = require('../../package.json')

const program = new Command()
program
.name('pg-typegen')
.version(`v${packageJson.version}`)
.arguments('<connection>')
.option('-f, --suffix <suffix>', 'suffix to append to generated table type, e.g. item -> ItemEntity', 'Entity')
.option('-s, --schema <schema>', 'schema', 'public')
.option('-h, --header <header>', 'header content', '')
.option('-o, --output <output>', 'file output path', 'stdout')
.option('-e, --exclude <exclude>', 'excluded tables and enums as comma separated string e.g. knex_migrations,knex_migrations_lock', parseArray, [])
.option('--type', 'use type definitions instead of interfaces in generated output', false)
.option('--noSemi, --no-semicolons', 'omit semicolons in generated types', false)
.option('--ssl', 'use ssl', false)
.option('--optionals', 'use optionals "?" instead of null', false)
.option('--comments', 'generate table and column comments', false)
.option('--pascal-enums', 'transform enum keys to pascal case', false)
.option('--bigint', 'use bigint for int8 types instead of strings', false)
.option('--date-as-string', 'use string for date types instead of javascript Date object', false)
.option('--insert-types', 'generate separate insert types with optional fields for columns allowing NULL value or having default values', false)
.option('--table-names', 'generate string literal type with all table names', false)

program.on('--help', () => {
console.log('')
console.log('Example:')
console.log(' $ pg-typegen -o ./entities.ts postgres://username:password@localhost:5432/database')
})

function parseArray (value) {
return value.split(',')
const options = {
version: { type: 'boolean', short: 'V' },
help: { type: 'boolean' },
suffix: { type: 'string', short: 'f' },
schema: { type: 'string', short: 's' },
header: { type: 'string', short: 'h' },
output: { type: 'string', short: 'o' },
exclude: { type: 'string', short: 'e' },
type: { type: 'boolean' },
ssl: { type: 'boolean' },
optionals: { type: 'boolean' },
comments: { type: 'boolean' },
bigint: { type: 'boolean' },
noSemi: { type: 'boolean' },
'no-semicolons': { type: 'boolean' },
'pascal-enums': { type: 'boolean' },
'date-as-string': { type: 'boolean' },
'insert-types': { type: 'boolean' },
'table-names': { type: 'boolean' }
}

async function generateSchema (opts) {
let argv = process.argv
if (process.argv.length === 2) {
// Starting from commander v8, arguments are mandatory.
// We're adding placeholder argument to be able to parse and get the default args.
argv = [...process.argv, '']
}
const defaultOpts = program.parse(argv).opts()
opts = { ...defaultOpts, ...opts }
const defaultOptions = {
suffix: 'Entity',
schema: 'public',
header: '',
output: 'stdout',
exclude: [],
type: false,
semicolons: true,
ssl: false,
optionals: false,
comments: false,
pascalEnums: false,
bigint: false,
dateAsString: false,
insertTypes: false,
tableNames: false,
help: false,
version: false
}

if (!opts.connection) {
const help = program.helpInformation()
const help = `Usage: pg-typegen [options] <connection>
Options:
-V, --version output the version number
-f, --suffix <suffix> suffix to append to generated table type, e.g. item -> ItemEntity (default: "Entity")
-s, --schema <schema> schema (default: "public")
-h, --header <header> header content (default: "")
-o, --output <output> file output path (default: "stdout")
-e, --exclude <exclude> excluded tables and enums as comma separated string e.g. knex_migrations,knex_migrations_lock (default: [])
--type use type definitions instead of interfaces in generated output (default: false)
--noSemi, --no-semicolons omit semicolons in generated types (default: false)
--ssl use ssl (default: false)
--optionals use optionals "?" instead of null (default: false)
--comments generate table and column comments (default: false)
--pascal-enums transform enum keys to pascal case (default: false)
--bigint use bigint for int8 types instead of strings (default: false)
--date-as-string use string for date types instead of javascript Date object (default: false)
--insert-types generate separate insert types with optional fields for columns allowing NULL value or having default values (default: false)
--table-names generate string literal type with all table names (default: false)
--help display help for command
Example:
$ pg-typegen -o ./entities.ts postgres://username:password@localhost:5432/database`

async function generateSchema (opts) {
if (!opts || !opts.connection) {
console.log(help)
return help
return
}

opts = { ...defaultOptions, ...opts }

const schema = await postgres(opts)

if (opts.onSchema) {
Expand Down Expand Up @@ -81,16 +107,51 @@ if (require.main === module) {
(async () => {
if (process.argv.length === 2) {
// Calling script without any arguments, so we're showing help and exiting.
program.help()
console.log(help)
return
}

const command = program.parse(process.argv)
const opts = command.opts()
const args = command.args
const parsedArgs = parseArgs({ options, allowPositionals: true, args: process.argv })
const opts = parsedArgs.values
opts.connection = parsedArgs.positionals[2]

const parsedOpts = {
suffix: opts.suffix,
schema: opts.schema,
header: opts.header,
output: opts.output,
exclude: opts.exclude ? opts.exclude.split(',').map(e => e.trim()).filter(Boolean) : [],
type: opts.type,
semicolons: !(opts.noSemi === true || opts['no-semicolons'] === true),
ssl: opts.ssl,
optionals: opts.optionals,
comments: opts.comments,
pascalEnums: opts['pascal-enums'],
bigint: opts.bigint,
dateAsString: opts['date-as-string'],
insertTypes: opts['insert-types'],
tableNames: opts['table-names'],
help: opts.help,
version: opts.version,
connection: opts.connection
}

const optsWithDefaults = {
...defaultOptions,
...Object.fromEntries(Object.entries(parsedOpts).filter(([, value]) => value !== undefined))
}

opts.connection = args[0]
if (optsWithDefaults.help) {
console.log(help)
return
}

if (optsWithDefaults.version) {
console.log(`v${packageJson.version}`)
return
}

const result = await generateSchema(opts)
const result = await generateSchema(optsWithDefaults)
console.log(result)
})()
}
Expand Down
34 changes: 17 additions & 17 deletions cjs/test/cli.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
const childProcess = require('node:child_process')
const fs = require('node:fs')
const path = require('node:path')
const { test, only } = require('tap')
const t = require('tap')
const { getTestPostgresConnectionString } = require('./helpers/setup-postgres.js')

const connection = getTestPostgresConnectionString()
const ssl = process.env.DATABASE_SSL_ENABLED === 'true'

test('help', t => {
t.test('help', t => {
t.plan(1)

const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..', 'src', 'index.js'), '--help'], {
Expand All @@ -27,7 +27,7 @@ test('help', t => {
})
})

test('version', t => {
t.test('version', t => {
t.plan(1)

t.cleanSnapshot = s => s.replace(/v[0-9.]+/g, 'v{v}')
Expand All @@ -49,7 +49,7 @@ test('version', t => {
})
})

test('missing connection string', t => {
t.test('missing connection string', t => {
t.plan(1)

const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..', 'src', 'index.js')], {
Expand All @@ -69,7 +69,7 @@ test('missing connection string', t => {
})
})

test('generates types stdout', t => {
t.test('generates types stdout', t => {
t.plan(1)

const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..', 'src', 'index.js'), connection, ssl ? '--ssl' : ''], {
Expand All @@ -89,7 +89,7 @@ test('generates types stdout', t => {
})
})

only('generates types to file', t => {
t.test('generates types to file', t => {
t.plan(1)

const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..', 'src', 'index.js'), '-o', './entities.ts', connection, ssl ? '--ssl' : ''], {
Expand All @@ -107,7 +107,7 @@ only('generates types to file', t => {
})
})

test('reports success to stdout', t => {
t.test('reports success to stdout', t => {
t.plan(1)

const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..', 'src', 'index.js'), '-o', './entities.ts', connection, ssl ? '--ssl' : ''], {
Expand All @@ -127,7 +127,7 @@ test('reports success to stdout', t => {
})
})

test('generates types with exclusion', t => {
t.test('generates types with exclusion', t => {
t.plan(1)

const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..', 'src', 'index.js'), connection, ssl ? '--ssl' : '', '-e', 'types,snake_test'], {
Expand All @@ -147,7 +147,7 @@ test('generates types with exclusion', t => {
})
})

test('generates types with header', t => {
t.test('generates types with header', t => {
t.plan(1)

const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..', 'src', 'index.js'), connection, ssl ? '--ssl' : '', '-h', '/* eslint-disable */'], {
Expand All @@ -167,7 +167,7 @@ test('generates types with header', t => {
})
})

test('generates types with pascal case enums', t => {
t.test('generates types with pascal case enums', t => {
t.plan(1)

const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..', 'src', 'index.js'), connection, ssl ? '--ssl' : '', '--pascal-enums'], {
Expand All @@ -187,7 +187,7 @@ test('generates types with pascal case enums', t => {
})
})

test('generates types with noSemi option', t => {
t.test('generates types with noSemi option', t => {
t.plan(1)

const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..', 'src', 'index.js'), connection, ssl ? '--ssl' : '', '--noSemi'], {
Expand All @@ -207,7 +207,7 @@ test('generates types with noSemi option', t => {
})
})

test('generates types with no-semicolons option', t => {
t.test('generates types with no-semicolons option', t => {
t.plan(1)

const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..', 'src', 'index.js'), connection, ssl ? '--ssl' : '', '--no-semicolons'], {
Expand All @@ -227,7 +227,7 @@ test('generates types with no-semicolons option', t => {
})
})

test('generates types with bigint option', t => {
t.test('generates types with bigint option', t => {
t.plan(1)

const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..', 'src', 'index.js'), connection, ssl ? '--ssl' : '', '--bigint'], {
Expand All @@ -247,7 +247,7 @@ test('generates types with bigint option', t => {
})
})

test('generates types with types option', t => {
t.test('generates types with types option', t => {
t.plan(1)

const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..', 'src', 'index.js'), connection, ssl ? '--ssl' : '', '--type'], {
Expand All @@ -267,7 +267,7 @@ test('generates types with types option', t => {
})
})

test('generates types with insert types', t => {
t.test('generates types with insert types', t => {
t.plan(1)

const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..', 'src', 'index.js'), connection, ssl ? '--ssl' : '', '--insert-types'], {
Expand All @@ -287,7 +287,7 @@ test('generates types with insert types', t => {
})
})

test('generates table names', t => {
t.test('generates table names', t => {
t.plan(1)

const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..', 'src', 'index.js'), connection, ssl ? '--ssl' : '', '--table-names'], {
Expand All @@ -307,7 +307,7 @@ test('generates table names', t => {
})
})

test('sends error to stderr', t => {
t.test('sends error to stderr', t => {
t.plan(1)
const invalidConnection = 'postgres://postgres:postgres@0.0.0.0:1/test_db'
const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..', 'src', 'index.js'), '-o', './entities.ts', invalidConnection, ssl ? '--ssl' : ''], {
Expand Down
Loading

0 comments on commit 4bfc1b3

Please sign in to comment.