draig-car
Version:
Database REST API interactive generator CLI and REPL OpenAPI3 based JS generator with interactive ORM/ODM REPL
327 lines (305 loc) • 9.84 kB
JavaScript
/**
* TODO:
* 1.- La columnas que admiten nulos se deben crear con datos y nulos
* 2.- Columnas de relación en las tablas, asignación de identificadores
* 3.- Lanzar excepción sino se encuentra una correlación de datos Faker
* 4.- Traducción de los nombres de columnas en las distintas BBDD
* 4.- MetadataLocale (traducción de nombres de columnas a ingles)
* 5.- Añadir helper a Faker
*/
const fs = require('fs')
const util = require('util')
const faker = require('faker')
const chalk = require('chalk')
const modulesFaker = Object.keys(faker)
const regexpId = /(.*)\_id/i
const randint = n => Math.floor(Math.random() * n) + 1
const isView = (schemas, tableName) => {
const sname = Object.keys(schemas).find(s =>
schemas[s]['x-draig-tableName'] === tableName && schemas[s]['x-draig-sch-raw'])
return sname !== undefined
}
const getEnumList = (schemas, tableName, col) => {
let enumList = []
// Which schema matches this table?
const sname = Object.keys(schemas).find(
s => schemas[s]['x-draig-tableName'] === tableName)
// Look in the class properties for the enum col
if(sname && schemas[sname] && schemas[sname].properties
&& schemas[sname].properties[col])
enumList = schemas[sname].properties[col].enum ||
(schemas[sname].properties[col].type === 'boolean' && [true, false])
else { // If not found, look in derived classes
const regex = /#\/components\/schemas\/(.*)/
const matched = schemas[sname].allOf.map(d => d['$ref'].match(regex))
for(const d of matched) {
const derived = d[1]
if(schemas[derived] && schemas[derived].properties
&& schemas[derived].properties[col])
enumList = schemas[derived].properties[col].enum ||
(schemas[derived].properties[col].type === 'boolean' && [true, false])
}
}
return enumList
}
const validateData = (tables, data) => {
for (let [table, tValue] of Object.entries(tables)) {
let error = []
data[table].forEach(d => {
for (let [name, value] of Object.entries(d)) {
if (tValue.__info[name] && tValue.__info[name].nullable === false
&& (value === null || value === 'unefined')) {
const e = `The column "${name}" does not allow null`
if(!error.includes(e)) error.push(e)
}
let maxL = tValue.__info[name] ? tValue.__info[name].maxLength : undefined
if (maxL && value && value.toString().length > maxL) {
const e = `The size of column "${name}" cannot have length > ${maxL}`
if(!error.includes(e)) error.push(e)
}
}
})
if (error && error.length !== 0) {
throw error
}
}
}
/**
* Returns an object with the data to insert in the relation nameRel
* @param {string} nameRel - Relation name. Format: table1_table2_....
* @param {number} nRows - number of __rows to insert
* @param {object} infoCols table column __information
*/
async function genRelationData(nameRel, nRows, infoCols) {
const cross = (n, m) => [].concat(...n.map(x => m.map(y => [].concat(x, y))))
const makearr = n => [...Array(n).keys()].map(e => e + 1)
const cartesian = (n, m) => cross(makearr(n), makearr(m))
const namesCol = Object.keys(infoCols)
let nmIds = cartesian(nRows, nRows)
let result = []
for (i = 0; i < nRows; i++) {
let ids = nmIds.splice(randint(nmIds.length) - 1, 1).flat()
result.push({ [namesCol[0]]: ids[0], [namesCol[1]]: ids[1] })
}
return result
}
/**
* Generate assignment structure of attribute table with function to get
* the faker data.
* @param {string} tableName table name
* @param {integer} nRows number of __rows to generate
* @param {object} infoCols table column __information
*/
async function genDataFaker(tableName, values, schemas) {
const nRows = values.__rows
const infoCols = values.__info
const fnDefault = (col, inf) => {
const elist = getEnumList(schemas, tableName, col)
if(elist && elist.length > 0)
return () => faker.random.arrayElement(elist)
switch (inf.type.toUpperCase()) {
case 'DATETIME':
case 'TIMESTAMP':
case 'TIMESTAMP WITHOUT TIME ZONE':
case 'TIMESTAMP WITH TIME ZONE':
return () => {
return faker.date.past().toISOString()
}
case 'BOOLEAN':
return () => {
return faker.datatype.boolean()
}
case 'NUMBER':
case 'INTEGER':
case 'INT':
case 'BIGINT':
return () => {
return faker.datatype.number(
inf.maxLength ? { min: 1, max: inf.maxLength } : undefined
)
}
case 'TINYINT':
return () => {
return faker.datatype.number({ min: 0, max: 1 })
}
case 'DECIMAL':
return () => {
return faker.datatype.number(
inf.maxLength
? { min: 1, max: inf.maxLength, precision: 0.01 }
: { precission: 0.01 }
)
}
case 'TEXT':
case 'LONGTEXT':
case 'VARCHAR2':
case 'VARCHAR':
case 'CHARACTER VARYING':
return () => {
let str = faker.random.word()
if (inf.maxLength) {
return str.substring(0, inf.maxLength)
}
return str
}
default:
console.log(
`Unknown TYPE for ${tableName}.${col}: ${inf.type.toUpperCase()}`
)
if (inf.defaultValue) {
return () => {
return inf.defaultValue
}
} else {
return undefined
}
}
}
const fnData = (col, inf) => {
let fixedVals = []
if(col in values) {
if(typeof(values[col]) === 'string') {
return () => {
const val = faker.fake(values[col])
switch(inf.type.toLowerCase()) {
// Json type is for arrays
case 'json':
case 'longtext':
return JSON.stringify(faker.datatype.array(
faker.datatype.number({min:1, max:3})).fill(val))
case 'int':
case 'integer':
case 'number':
return Number(val)
case 'timestamp':
case 'timestamp(6) with local time zone':
return new Date(Date.parse(val))
default:
//console.warn(`Warning: unknown type ${inf.type} for col ${col}`)
return val
}
}
} else { // Fixed bag (array) of values sequentially sampled
if(fixedVals.length === 0) fixedVals = [ ...values[col] ]
return () => fixedVals.pop()
}
}
if(regexpId.test(col)) {
// 5 are the default number of rows generated
return () => randint(5)
}
for (let g of modulesFaker) {
let res = Object.keys(faker[g]).find(o =>
o.toLowerCase().indexOf(col.toLowerCase()) != -1 &&
typeof faker[g][o] === 'function'
)
if (res !== undefined) {
let fn = faker[g][res]
Object.defineProperty(fn, 'name', { value:res, configurable:true })
//console.warn(chalk`Using faker ${fn.name} found for {green ${tableName}.${col}}`)
return fn
}
}
console.warn(
chalk`Faker fn not found for {green ${tableName}.${col}} - using defaults`
)
return fnDefault(col, inf)
}
return Object.keys(infoCols).reduce((i, e) =>
e !== 'id' ? { ...i, [e]: fnData(e, infoCols[e]) } : i, {})
}
/**
* Generate the faker data set.
*
* @param {knex} knex SQL builder for get column __info
* @param {object} config data
* @param {object} allTables all tables on which to generate faker
* {
* key: name of relation
* value: {
* __rows: number of __rows generate,
* relation: true if the relation table, otherwise false
* },
* }
*/
async function generateSeed(knex, config, schemas) {
// Filter configuration keys from list of table __rows config
const allTables = Object.keys(config).reduce((i, n) =>
(!n.startsWith('__') && !isView(schemas, n) ? { ...i, [n]: config[n] } : i), {})
// Configure locale
faker.locale = config['__localeData'] || faker.locale
// Add __info data to columns
for (let [key, value] of Object.entries(allTables)) {
let __info = await knex(key).columnInfo()
if(!Object.keys(__info).length) {
console.error(chalk`Table or view does not exists: {green ${key}} `
+ `- Did you create the tables?`)
return false
}
allTables[key] = { ...value, __info }
}
const tables = Object.keys(allTables).filter(t => !allTables[t].relation)
const relations = Object.keys(allTables).filter(t => allTables[t].relation)
// Set and reset (at the end of the func) generation conditions
const initialDateToString = Date.prototype.toString
const initialTZ = process.env.TZ
// For this generation only
Date.prototype.toString = Date.prototype.toISOString
process.env.TZ = 'UTC'
// Add faker fns for every column
let fakerTables = {}
for (let t of tables) {
let values = allTables[t]
fakerTables = {
...fakerTables,
[t]: await genDataFaker(t, values, schemas)
}
}
// Generate data for tables
let genData = {}
// Generate data tables
for (let [key, value] of Object.entries(fakerTables)) {
let data = []
let table = allTables[key]
for (let i = 0; i < (table.__rows || 5); i++) {
let d = {}
for (let [n, fn] of Object.entries(value)) {
const opt = {
'datetime': { min:2724302168, max:2150196367003 },
'randint': table.__rows
}
let res = typeof fn === 'function' ? fn(opt[fn.name]) : fn
let tMaxLength = table.__info[n].maxLength || undefined
while(tMaxLength && res && (res.length > tMaxLength))
res = res.substring(0, tMaxLength)
// Date must be coerced to string
if(toString.call(res) === '[object Date]')
res = res.toISOString()
d = { ...d, [n]: res }
}
data.push(d)
}
genData = { ...genData, [key]: data }
}
// Generate data relations
for (let r of relations) {
let e = allTables[r]
genData = {
...genData,
[r]: await genRelationData(r, e.__rows, e.__info)
}
}
// Validate data
validateData(allTables, genData)
// Generate file
let data =
'const db = ' +
util.inspect(genData, { depth: 3 }) +
'\nmodule.exports = db\n'
fs.writeFileSync('seed-database.js', data, 'utf-8')
// Reset initial configuration
Date.prototype.toString = initialDateToString
process.env.TZ = initialTZ
return true
}
module.exports.generateSeed = generateSeed