draig-car
Version:
Database REST API interactive generator CLI and REPL OpenAPI3 based JS generator with interactive ORM/ODM REPL
231 lines (218 loc) • 6.24 kB
JavaScript
const fs = require('fs')
const util = require('util')
const chalk = require('chalk')
const mustache = require('mustache')
const u = require('../util')
let workSchema = {}
function getParsedTemplate(tpl) {
const parsed = mustache.parse(tpl)
const r = e =>
e[0] === 'name' || e[0] === '&'
? e[1]
: e[0] === '#'
? { [e[1]]: e[4].reduce((i, e) => i.concat(r(e)), []) }
: e[0] === '^'
? { ['not' + e[1]]: e[4].reduce((i, e) => i.concat(r(e)), []) }
: []
return parsed.reduce((i, e) => i.concat(r(e)), [])
}
const completers = api => ({
yesno: ['yes', 'no'],
path: Object.keys(api.paths),
tag: api.tags.map(e => e.name),
schema: Object.keys(api.components.schemas),
bodySchema: Object.keys(api.components.schemas),
schemaPart: Object.keys(api.components.schemas),
columnType: ['integer', 'string', 'number', 'boolean'],
refType: Object.keys(api.components.schemas),
required:
'properties' in workSchema
? Object.keys(workSchema.properties)
: 'allOf' in workSchema && workSchema.allOf.find(o => 'properties' in o)
? Object.keys(workSchema.allOf.find(o => 'properties' in o).properties)
: [],
format: [
'int32',
'int64',
'float',
'double',
'byte',
'binary',
'date',
'date-time',
'password',
'email',
'uuid'
]
})
function completer(repl, bar) {
return (line, callback) => {
let comp = util.isArray(bar)
? bar
: completers(repl.context.api)[bar] || []
const hits = comp.filter(c => c.startsWith(line))
const ret = [hits && hits.length ? hits : comp, line || '']
callback(null, ret)
}
}
async function askVarObjects(repl, varName, vlist) {
let values = {}
for (let v of vlist) await getVars(repl, values, v)
return values
}
function question(repl, completions, msg, defvalue) {
return new Promise((resolve, reject) => {
let _comp = repl.completer
repl.completer = completer(repl, completions)
if(defvalue) repl.history.unshift(defvalue)
repl.question(msg, r => {
repl.completer = _comp
if (r) repl.history.splice(0, 1)
resolve(r)
})
})
}
async function askVarList(repl, varName, vlist) {
let valuesList = []
while (true) {
let values = {}
for (let v of vlist) await getVars(repl, values, v)
if (Object.keys(values).length) valuesList.push(values)
else break
}
return valuesList
}
async function getVars(repl, values, v) {
let varName = util.isObject(v) ? Object.keys(v)[0] : v
if (values[varName]) return
if (varName.endsWith('List')) {
values[varName] = await askVarList(repl, varName, v[varName])
} else if (varName.startsWith('has')) {
if (
'yes' ===
(await question(
repl,
'yesno',
chalk`Has this {green ${varName.replace('has', '')}}: `
))
)
values[varName] = await askVarObjects(repl, varName, v[varName])
} else if (varName.startsWith('not')) {
let value = values[varName.replace('not', '')]
if (value === undefined)
for (let e of v[varName]) await getVars(repl, values, e)
} else {
let resp,
opt = util.isObject(v) ? 'optional' : 'required'
resp = await question(
repl,
varName,
chalk` {green ?} Value for {green ${varName}} (${opt}): `
)
if (resp) values[varName] = resp
}
}
function allLocalSchemas(schemas, view) {
let local = /#\/components\/schemas\//
if (view)
for (let [k, v] of Object.entries(view)) {
if ('object' === typeof v) allLocalSchemas(schemas, v)
else if (k === '$ref' && !schemas.includes(v.replace(local, '')))
schemas.push(v.replace(local, ''))
}
return schemas
}
async function askVars(repl, name, tpl) {
let values = {}
console.log(chalk`Please enter values for the {green ${name}} template:\n`)
for (let v of getParsedTemplate(tpl)) await getVars(repl, values, v)
return values
}
module.exports = {
question,
askVars,
apiGet: function(obj, path) {
return path.length
? obj[path[0]] && module.exports.apiGet(obj[path[0]], path.slice(1))
: obj
},
apiSet: function(obj, path, value) {
if(path.length > 1) {
if(obj[path[0]])
return module.exports.apiSet(obj[path.shift()], path, value)
} else {
obj[path[0]] = value
return true
}
return false
},
apiValue: function(ctx, args) {
let cmdline = util.isArray(args) ? args : args.split(' ')
let path =
cmdline[0] === ''
? ctx.apiPath
: cmdline[0] === 'api'
? cmdline.slice(1)
: ctx.apiPath.concat(cmdline)
let value = ctx.api
for (let p of path) {
if (typeof value[p] === 'undefined') return [undefined]
value = value[p]
}
return [value, path.reduce((i, e) => i + "['" + e + "']", ''), path]
},
fillTemplate: async function(repl, tplDir, tplName, wsch) {
if (wsch) workSchema = wsch
// read template
let tpl = u.template(repl.context.argv, tplDir, tplName)
if (!tpl) {
console.error(
chalk`Template {yellow ${tplDir}/${tplName}} not found or unreadable`
)
return null
}
// ask for vars of the template
let values = await askVars(repl, tplName, tpl)
// make mustache substitution
let yamlSubst = mustache.render(tpl, values)
// ask for confirmation showing prettyfied yaml substitution
u.printYaml(yamlSubst)
let confirmed = await question(repl, 'yesno', 'Is the above data OK? ')
if (!confirmed || confirmed === 'no') {
console.log(chalk`{yellow Aborted} - No API modification performed`)
return null
}
// return view from yaml
return u.fromYaml(yamlSubst)
},
addView: function(obj, view) {
let key = Object.keys(view)[0]
return key ? { ...obj, [key]: { ...obj[key], ...view[key] } } : obj
},
validOperationId: function(paths, operationId) {
let op = Object.values(paths).find(p =>
Object.values(p).find(o => o.operationId === operationId)
)
if (op)
console.error(
chalk`{yellow ERROR} - operationId {yellow ${operationId}} already exists`
)
return typeof op === 'undefined'
},
validSchemas: function(api, view) {
let schemas = allLocalSchemas([], view)
let missing = schemas.filter(
s => !api.components.schemas.hasOwnProperty(s)
)
if (missing.length) {
console.error(
chalk`{yellow ERROR} - Found references to missing or non-local schemas:`,
missing
)
console.error(
'Generation will CRASH if the problem persist !!!'
)
}
return missing.length === 0
}
}