UNPKG

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
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') } }