UNPKG

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
/** * 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