draig-car
Version:
Database REST API interactive generator CLI and REPL OpenAPI3 based JS generator with interactive ORM/ODM REPL
445 lines (429 loc) • 14.1 kB
JavaScript
const fs = require('fs')
const path = require('path')
const figl = require('figlet')
const chalk = require('chalk')
const yaml = require('js-yaml')
const mustache = require('mustache')
const clih = require('cli-highlight')
const colorize = require('json-colorizer')
const clearModule = require('clear-module')
const cmdexists = require('command-exists').sync
const { spawn, spawnSync } = require('child_process')
const { generateSeed } = require('./commands/util/genseed')
let codegen, packager, gswitch, lastquery, lasttransform, spin_seq = 0
function spinner(file, source, data) {
const spinchar = [ '|', '/', '-', '\\' ]
const now = new Date().toISOString()
spin_seq = spin_seq == 3 ? 0 : spin_seq + 1
process.stdout.write('\x1B[?25l')
process.stdout.write(spinchar[spin_seq] + '\b')
fs.writeSync(file, `${now} ${source} ${data}`)
}
function spawnProc(cmdarr, logfile, cwd) {
return new Promise((resolve, reject) => {
const logFile = fs.openSync(logfile, 'a')
const subprocess = spawn('sh', [ '-c', cmdarr.join(' ') ], { cwd })
process.stdout.write(chalk`{green ${cmdarr.join(' ')}}... `)
subprocess.on('error', err => reject(err))
subprocess.on('close', data => {
process.stdout.write('\x1B[?25h')
process.stdout.write(
chalk`\r{green ${cmdarr.join(' ')}} DONE (rc = {yellow ${data}})\n`
)
fs.closeSync(logFile)
resolve(undefined)
})
subprocess.stderr.on('data', d => spinner(logFile, 'ERR', d))
subprocess.stdout.on('data', d => spinner(logFile, 'OUT', d))
})
}
function loadConfig() {
let config = path.resolve(process.cwd(), 'draig.yaml')
if (!fs.existsSync(config)) {
console.error('Project configuration file "draig.yaml" does not exists!')
process.exit(1)
}
return yaml.safeLoad(fs.readFileSync(config))
}
function findEmpty(schemas) {
let emptySchemas = Object.keys(schemas).filter(
o =>
(schemas[o].properties && !Object.keys(schemas[o].properties).length) ||
(schemas[o].allOf &&
schemas[o].allOf.some(
e => e.properties && !Object.keys(e.properties).length
))
)
return emptySchemas
}
function runPackager(ctx, cmd) {
const cmdline = packager === 'npm' && cmd !== 'install' ? 'run ' + cmd : cmd
return spawnProc([ packager, cmdline ], 'packager.log', ctx.argv.projectDir)
}
function saveAPIpid(ctx, pid) {
fs.writeFileSync(path.resolve(ctx.argv.projectDir, 'api.pid'),
pid.toString())
}
function readAPIpid(ctx) {
let pidFile = path.resolve(ctx.argv.projectDir, 'api.pid')
return fs.existsSync(pidFile) ? fs.readFileSync(pidFile) : null
}
function removeAPIpid(ctx) {
let pidFile = path.resolve(ctx.argv.projectDir, 'api.pid')
if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile)
}
async function genSeed(knex, projPath, seedgen, schemas) {
const { allTabs, allRels } = require(path.join(projPath, 'models.js'))
console.log(
chalk`Generating seed-database.js for tables {green ${allTabs.join(
', '
)}}${allRels.length ? ' and relations ': ''}{green ${allRels.join(', ')}}`
)
// Clone seedGen arrays - it will be modified
const seedcfg = {
__localeData: 'es',
...allTabs.reduce((i, n) => ({ ...i, [n]: { __rows: 5 } }), {}),
...allRels.reduce(
(i, n) => ({ ...i, [n]: { relation: true, __rows: 5 } }),
{}
),
...seedgen
}
return await generateSeed(knex, seedcfg, schemas)
}
module.exports = {
runPackager,
loadConfig,
genSeed,
saveYaml: (filename, jsText) => {
fs.writeFileSync(filename, yaml.safeDump(jsText, { flowLevel: -1 }))
},
fromYaml: yamlText => yaml.safeLoad(yamlText),
printYaml: yaml => {
console.log(
clih.highlight(yaml, {
language: 'yaml',
theme: clih.parse(
'{ "literal": ["whiteBright", "bold"], "string": "whiteBright", "comment": "greenBright", "attr": "cyanBright", "number": ["whiteBright", "bold"] }'
)
})
)
},
initProject: vars => {
function logVar(name, variable) {
if (variable) console.log(chalk` ${name}: {green ${variable}}`)
}
function applyTemplate(tmpl, dst, view) {
const data = fs.readFileSync(
path.resolve(__dirname, '..', 'tpl', 'init', tmpl),
{ encoding: 'utf8' }
)
const rendered = mustache.render(data, view)
fs.writeFileSync(dst, rendered, 'utf8')
console.log(
chalk`Configuration file {green ${dst}} written successfully`
)
}
console.log(chalk`\nInitializing project: {green ${vars.project}}\n`)
logVar('DB Client ', vars.dbClient)
logVar('DB name ', vars.dbName)
logVar('DB username ', vars.dbUserName)
logVar('DB password ', vars.dbUserPassword)
logVar('DB host ', vars.dbHost)
console.log('')
applyTemplate('draig.mustache', 'draig.yaml', vars)
if(fs.existsSync(`${vars.project}.yaml`))
console.log(chalk`Using existing API definition {green ${vars.project}.yaml}`)
else
applyTemplate('api.mustache', `${vars.project}.yaml`, vars)
console.info('\nNext steps:\n')
let i = 1
console.info(
chalk`{cyan ${i++}.} Check that configuration files are correct`
)
if (vars.dbClient !== 'sqlite3')
console.info(chalk`{cyan ${i++}.} Execute \`{green draig run dbreset}'`)
if (!vars.seedGen || !vars.seedGen.__useFaker)
console.info(
chalk`{cyan ${i++}.} Create or copy here your '{green seed-database.js}' file`
)
console.info(chalk`{cyan ${i++}.} Generate project`)
console.info(chalk`{cyan ${i++}.} Enjoy !\n`)
},
setPrompt: ctx => {
if (ctx.apiPath) {
ctx.repl.setPrompt(
chalk`{cyan ${ctx.api.info.title}:} ${ctx.apiPath.join(' ')}{bold >} `
)
}
else ctx.repl.setPrompt(chalk`{cyan no api}:{bold >} `)
},
showBanner: () => {
const p = require(path.resolve(__dirname, '..', 'package.json'))
const v = p.version.split('.')
console.log(
chalk`{green ${figl.textSync(p.name)}} ` +
chalk`v{redBright ${v[0]}}.{greenBright ${v[1]}}.{blueBright ${v[2]}}\n`
)
const t = p.description.split('\n')
const title = t[0]
.split(' ')
.map(w => `${chalk.green(w.substring(0, 1))}${w.substring(1)}`)
.join(' ')
console.log(title)
console.log(chalk.green(t[1] + '\n'))
},
testRequisites: argv => {
const javaExists = cmdexists('java')
const oagExists = cmdexists('openapi-generator')
const sgcExists = cmdexists('swagger-codegen')
const yarnExists = cmdexists('yarn')
if (!javaExists || (!oagExists && !sgcExists)) {
console.error(
chalk`\nYou need {green java} (a JDK) and {green openapi-generator}` +
chalk` or {green swagger-codegen} in your PATH\n`
)
process.exit(1)
}
if (sgcExists) {
codegen = 'swagger-codegen'
gswitch = '-l'
} else {
codegen = 'openapi-generator'
gswitch = '-g'
}
packager = yarnExists ? 'yarn' : 'npm'
console.log(chalk`> Packager: {green ${packager}}`)
if (!argv.silent)
console.log(chalk`> Codegen provider: {green ${codegen}}`)
},
stopAPI: ctx => {
let pid = readAPIpid(ctx)
if (pid) {
console.log('Killing process', parseInt(pid))
if(process.platform === 'win32') {
spawn('taskkill', ['/pid', pid, '/f', '/t'], {
stdout: 'ignore', stderr: 'ignore'
})
}
else process.kill(pid, 'SIGTERM')
removeAPIpid(ctx)
if (ctx.tail) {
ctx.tail.stop()
delete ctx.tail
}
} else console.log('No current API process currently running')
},
statusAPI: ctx => {
let pid = readAPIpid(ctx)
if (pid) console.log('The API server is running with pid', parseInt(pid))
else console.log('API server is NOT running')
},
startAPI: (ctx, sync) => {
let pid = readAPIpid(ctx)
let checks = 50 // 50 * 100ms = 5s
let replsrv = ctx.repl
if (pid) return console.log('API already running with pid: %d', pid)
const Tail = require('tail-file')
const spwn = sync ? spawnSync : spawn
const checkStartTail = logfile => {
if (checks === 0) {
// Give up
console.warn(chalk`\n{red APIServer}: unable to start tail after 5s`)
replsrv.displayPrompt()
return
}
if (!fs.existsSync(logfile)) {
setTimeout(checkStartTail, 100, logfile)
checks--
} else {
ctx.tail = new Tail(logfile)
ctx.tail.on('line', data => {
console.log(chalk`\n{green APIServer}: ` + colorize(data, {
pretty: true,
colors: {
BRACE: 'green.bold',
COMMA: 'green.bold',
COLON: 'green.bold',
STRING_KEY: 'blue',
STRING_LITERAL: 'white',
NUMBER_LITERAL: 'yellow',
NULL_LITERAL: 'red'
}
}))
replsrv.displayPrompt()
})
ctx.tail.on('error', err => {
console.log(chalk`\n{brown ERROR} - Tail failure: `, err)
replsrv.displayPrompt()
})
ctx.tail.start()
}
}
try {
const subprocess = spwn('sh', ['-c', packager + ' start'], {
detached: !sync,
stdio: sync ? 'inherit' : 'ignore',
cwd: ctx.argv.projectDir
})
if (!sync) {
console.log('API started with pid', subprocess.pid)
subprocess.unref()
saveAPIpid(ctx, subprocess.pid)
const logfile = path.resolve(ctx.argv.projectDir, 'combined.log')
checkStartTail(logfile)
}
} catch (e) {
console.error('Error starting API server: ' + e)
console.error('Generate and install the API server before starting it')
}
},
generateAPI: async ctx => {
if (!ctx.api) ctx.api = yaml.safeLoad(fs.readFileSync(ctx.argv.api))
let emptySchemas = findEmpty(ctx.api.components.schemas)
if (emptySchemas.length) {
console.error(
chalk`{yellow ERROR}: Can't generate project - found empty local schemas:`,
emptySchemas
)
return true // failure - no need to reset CTX
}
// Check before generation !
let isDBClientChanged = fs.existsSync(ctx.argv.projectDir)
? ctx.argv.dbclient !==
require(path.resolve(path.join(ctx.argv.projectDir, 'knexfile.js')))
.development.client
: false
// Global installation of draig-oag or @openapitools/openapi-generator required
let args = [
'generate',
'-i',
ctx.argv.api,
gswitch,
ctx.argv.generator,
'-o',
ctx.argv.projectDir,
'-c', 'draig.yaml'
]
if (ctx.argv.template) args = args.concat(['-t', argv.template])
console.log('Generating API... ')
if(await spawnProc([ codegen, ...args ], 'generator.log')) {
console.error(
chalk`Generation failed - Please, check {green generator.log} file`
)
return true
}
if (ctx.argv.noinstall) {
console.log('Not preparing project')
return true
}
let fname = '01_create_tables.js'
let nfname = fname + '.new'
let migrations = fs.existsSync('migrations')
? path.resolve('.', 'migrations')
: path.resolve('.', ctx.argv.projectDir, 'migrations')
// If old sch exists the schema is new
let isNew = !fs.existsSync(path.resolve(migrations, fname))
let isChanged = false
if (!isNew) {
let newsch = fs.readFileSync(path.resolve(migrations, nfname))
let oldsch = fs.readFileSync(path.resolve(migrations, fname))
// If old sch != new sch, then the schema has changed
isChanged = 0 !== Buffer.compare(newsch, oldsch) || isDBClientChanged
}
console.log('Preparing project - please wait...')
await runPackager(ctx, 'install')
console.log(
chalk`Schema / DB client is {green ${
isNew ? 'new' : isChanged ? 'changed' : 'unchanged'
}}`
)
if (isChanged && !isDBClientChanged)
await runPackager(ctx, 'migrate-down')
if(fs.existsSync(path.resolve(migrations, nfname)))
fs.renameSync(
path.resolve(migrations, nfname),
path.resolve(migrations, fname)
)
if (isNew || isChanged)
await runPackager(ctx, 'migrate-up')
let projPath = path.resolve(ctx.argv.projectDir)
if (!fs.existsSync(path.join(projPath, 'utils', 'orm.js'))) return true
const knex = require(path.join(projPath, 'utils', 'orm.js')).knex()
const cfg = loadConfig()
ctx.argv.seedGen = cfg.seedGen
if (!ctx.argv.seedGen.__useFaker) return true
if (await genSeed(knex, projPath, ctx.argv.seedGen,
ctx.api.components.schemas)) {
console.log(chalk`{green genseed} DONE`)
await runPackager(ctx, 'seed')
}
},
templateList: (argv, dir, type) => {
let list = []
if (type === 'all' || type === 'user')
try {
list = list.concat(
fs.readdirSync(
path.resolve(path.dirname(path.resolve(argv.api)), 'tpl', dir)
)
)
} catch (e) {}
if (type === 'all' || type === 'system')
list = list.concat(
fs.readdirSync(path.resolve(__dirname, '..', 'tpl', dir))
)
return list.map(e => e.split('.')[0])
},
template: (argv, dir, name) => {
// look first user templates (=apidir + tpl + dir as api definition file)
let userTpl = path.resolve(
path.dirname(path.resolve(argv.api)),
'tpl',
dir
)
let f = path.resolve(userTpl, name + '.mustache')
if (fs.existsSync(f)) return fs.readFileSync(f, 'utf8')
// then look in the draig templates class dir for template
f = path.resolve(__dirname, '..', 'tpl', dir, name + '.mustache')
if (fs.existsSync(f)) return fs.readFileSync(f, 'utf8')
return null
},
configProject: argv => {
// Set api and projectDir based on current configuration file
const cfg = loadConfig()
if (!argv.projectDir && !cfg.apiProject) {
console.error(
'You must specify a `project-dir\' in args or an `apiProject\' in "draig.yaml"!'
)
process.exit(1)
}
argv.api = argv.projectDir ? argv.projectDir + '.yaml' : cfg.apiProject
argv.projectDir =
argv.projectDir || cfg.apiProject.replace(path.extname(argv.api), '')
argv.generator = cfg.openapiGenerator || 'draig'
argv.dbclient = cfg.dbClient
argv.seedGen = cfg.seedGen
},
listCmds: ctx => {
let replsrv = ctx.repl
let commands = replsrv.commands
for (let c of Object.keys(commands))
if (commands[c].runnable)
console.log(chalk`{green %s}: %s`, c, commands[c].help)
},
exitOrContinue: rpl => {
if (rpl.context.argv._.includes('run')) process.exit(0)
else rpl.displayPrompt()
},
clean: ctx => {
if(fs.existsSync(ctx.argv.projectDir))
fs.rmdirSync(ctx.argv.projectDir, { recursive: true})
if(fs.existsSync('generator.log'))
fs.unlinkSync('generator.log')
if(fs.existsSync('packager.log'))
fs.unlinkSync('packager.log')
if(fs.existsSync('seed-database.js'))
fs.unlinkSync('seed-database.js')
}
}