'use strict'
// @ts-check
const { version } = require('../package.json')
const Util = require('util')
const { promisify } = Util
const _ = require('lodash')
const ReplaceInFiles = require('replace-in-files')
const Fs = require('fs')
const Path = require('path')
const Inflector = require('inflected')
const { Command } = require('commander')
const Process = require('process')
const Colors = require('colors/safe')
const SequelizeAuto = require('sequelize-auto')
const Config = require('config')
/**
*
*
* @class Scaffolding
*/
class Scaffolding {
/**
*Creates an instance of Scaffolding.
* @param {IScaffoldingConfig} [options={}]
* @memberof Scaffolding
*/
constructor(options = {}) {
this._commander = new Command()
this._sequelizeAuto = undefined
this._logicalFieldTemplate = `\t\tlogical: {
\t\t\ttype: new DataTypes.VIRTUAL(DataTypes.INTEGER(1), ['logical']),
\t\t\tallowNull: true,
\t\t\tfield: 'logical',
\t\t\tget() {
\t\t\t\treturn this.getDataValue('logical')
\t\t\t},
\t\t\tset(val) {}
\t\t}`
this._attributes = ` id: {
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true,
field: 'id'
},
status: {
type: DataTypes.ENUM('active', 'inactive', 'pending'),
values: ['active', 'inactive', 'pending'],
allowNull: false,
defaultValue: 'pending',
field: 'status',
validate: {
isIn: [
['active', 'inactive', 'pending']
]
}
},
createdId: {
type: DataTypes.INTEGER(8).UNSIGNED,
allowNull: true,
field: 'created_id'
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: sequelize.literal('CURRENT_TIMESTAMP'),
field: 'created_at'
},
updatedId: {
type: DataTypes.INTEGER(8).UNSIGNED,
allowNull: true,
field: 'updated_id'
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: sequelize.literal('CURRENT_TIMESTAMP'),
field: 'updated_at'
},
deletedId: {
type: DataTypes.INTEGER(8).UNSIGNED,
allowNull: true,
field: 'deleted_id'
},
deletedAt: {
type: DataTypes.DATE,
allowNull: true,
field: 'deleted_at'
},
${this._logicalFieldTemplate}`
this._references = []
this._files = []
this._args = {}
/** @type {IScaffoldingConfig} */
this._options = this._deepExtend(
{
path: {
controllers: './controllers/',
routes: './routes/',
models: './models/',
schemas: './schemas/'
}
},
options
)
this._init()
}
/**
* @private
* @memberof Scaffolding
*/
_init() {
this._commander
.command('generate <name>')
.version(process.env.npm_package_version || version, '-v, --version', 'SHOW CURRENT VERSION')
.alias('g')
.description('[SCAFFOLDING CLI]: SCAFFOLDING [controller|route]')
.option('-m, --model', 'SCAFFOLDING modle')
.option('-c, --controller', 'SCAFFOLDING controller')
.option('-r, --route', 'SCAFFOLDING route')
.option('-s, --schema', 'SCAFFOLDING schema')
.option('-f, --force', 'CREATE FORCIBLY IF FILE ALREADY EXISTS')
.option('-a, --run-all', 'CREATE [controller|route|modle|schema]')
.option('-t, --type <items>', 'SCAFFOLDING TYPE', value => (value || []).split(','), [])
.helpOption('-h, --help', 'READ MORE INFORMATION')
.on('--help', this._cmdHelp.bind(this))
.action(this._cmdAction.bind(this))
this._commander.version(process.env.npm_package_version || version, '-v, --version', 'SHOW CURRENT VERSION')
}
/**
*
*
* @param {string[]} [payload=Process.argv]
* @return {Promise<Object>}
* @memberof Scaffolding
*/
async run(payload = Process.argv) {
return await Promise.resolve(this._commander.parse(payload))
}
/**
*
* @private
* @param {string} name
* @param {Object} options
* @memberof Scaffolding
*/
async _cmdAction(name, options) {
try {
if (_.isEmpty(name)) {
throw new TypeError('Please specify a parameter: [name]')
}
let {
type = [],
runAll = false,
force = false,
controller = false,
route = false,
model = false,
schema = false
} = options
controller = controller || !_.isUndefined(type.find(t => t === 'controller')) || runAll
route = route || !_.isUndefined(type.find(t => t === 'route')) || runAll
model = model || !_.isUndefined(type.find(t => t === 'model')) || runAll
schema = schema || !_.isUndefined(type.find(t => t === 'schema')) || runAll
this._args = _.assign(
{},
{
type,
name,
controller,
route,
model,
schema,
runAll,
force
}
)
if (controller) {
await this._createController(name)
}
if (route) {
this._createRoute(name)
}
if (model) {
await this._createModel(name)
}
if (schema) {
await this._createSchema(name)
}
if (!_.size(this._files)) {
throw new TypeError('Please specify at least 1 type: [controller|route|modle|schema]')
}
await this._replaceFiles()
} catch (error) {
this._log(error, 'error')
this._showHelp(false, 'red')
}
}
/**
*
* @memberof Scaffolding
*/
async _replaceFiles() {
try {
const name = _.get(this._args, 'name')
const base = {
files: this._files,
optionsForFiles: {
ignore: ['**/node_modules/**']
},
allowEmptyPaths: false,
saveOldFile: false,
encoding: 'utf8',
onlyFindPathsWithoutReplace: false,
returnPaths: true,
returnCountOfMatchesByPaths: true
}
await ReplaceInFiles(
this._deepExtend(base, {
from: new RegExp(/\{\{CLAZZ\}\}/, 'g'),
to: this._toModelizeAndPluralize(name)
})
)
await ReplaceInFiles(
this._deepExtend(base, {
from: new RegExp(/\{\{PLURAL\}\}/, 'g'),
to: this._toResource(name)
})
)
await ReplaceInFiles(
this._deepExtend(base, {
from: new RegExp(/\{\{SINGULAR\}\}/, 'g'),
to: this._toUnderscore(name)
})
)
await ReplaceInFiles(
this._deepExtend(base, {
from: new RegExp(/\{\{FIELDS\}\}/, 'g'),
to: this._attributes
})
)
await ReplaceInFiles(
this._deepExtend(base, {
from: new RegExp(/\{\{RELATIONS\}\}/, 'g'),
to: this._getModelReferencesAsString()
})
)
await ReplaceInFiles(
this._deepExtend(base, {
from: new RegExp(/\{\{TABLE\}\}/, 'g'),
to: this._toTableize(name)
})
)
await ReplaceInFiles(
this._deepExtend(base, {
from: new RegExp(/\{\{MODEL\}\}/, 'g'),
to: this._toModelClassify(name)
})
)
this._log(`SCAFFOLDING DONE... ${JSON.stringify(this._files)}`, 'log')
} catch (error) {
this._log(error, 'error')
throw error
}
}
/**
* @private
* @description create controller for `express`
* @param {string} [name='']
* @returns {string}
* @memberof Scaffolding
*/
async _createController(name = '') {
const src = this._getTemplatePath('controllers')
const dist = this._getControllerPath(name)
return this._copyTemplate(src, dist)
}
/**
* @private
* @description create route definition for `express`
* @param {string} [name='']
* @returns {string}
* @memberof Scaffolding
*/
async _createRoute(name = '') {
const src = this._getTemplatePath('routes')
const dist = this._getRoutePath(name)
return this._copyTemplate(src, dist)
}
/**
* @private
* @description create model class for `Sequelize` with typescript using `SequelizeAuto`
* @param {string} [name='']
* @returns {string}
* @memberof Scaffolding
*/
async _createModel(name = '') {
try {
const src = this._getTemplatePath('models')
const dist = this._getModelPath(name)
const options = {
encoding: 'utf-8'
}
await this._runSequelizeAuto(name)
if (this._fileExists(dist)) {
this._setModelAttributes(name, Fs.readFileSync(dist, options))
this._setModelReferences()
Fs.unlinkSync(dist)
}
return this._copyTemplate(src, dist)
} catch (error) {
this._log(error, 'error')
}
}
/**
* @private
* @description create empty schema for `MongoDB`
* @param {string} [name='']
* @returns {string}
* @memberof Scaffolding
*/
async _createSchema(name = '') {
const src = this._getTemplatePath('schemas')
const dist = this._getSchemaPath(name)
return this._copyTemplate(src, dist)
}
/**
* @private
* @description copy from template to be SCAFFOLDING
* @param {string} src
* @param {string} dist
* @returns {string}
* @memberof Scaffolding
*/
_copyTemplate(src, dist) {
try {
const force = _.get(this._args, 'force')
if (!this._fileExists(src)) {
throw new TypeError(`Specified template does not exists!: ${src}`)
}
if (this._fileExists(dist)) {
if (!force) {
throw new TypeError(`Specified file already exists!: ${dist}`)
}
Fs.unlinkSync(dist)
}
Fs.copyFileSync(src, dist)
this._log(`Generated file: ${dist}`, 'log')
this._files.push(dist)
return dist
} catch (error) {
this._log(error, 'error')
throw error
}
}
/**
*
* @private
* @param {string} [name='']
* @returns {Promise<any>}
* @memberof Scaffolding
*/
async _runSequelizeAuto(name = '') {
try {
const options = _.cloneDeep(Config.util.toObject(Config.db[Process.env.NODE_ENV || 'development']))
const {
user,
username,
password,
host,
database,
port,
dialect,
dialectOptions,
pool,
timezone,
define,
benchmark
} = options
const tables = [this._toTableize(name)]
const camelCase = true
const indentation = 1
this._sequelizeAuto = new SequelizeAuto(database, username, password, {
user,
username,
password,
database,
host,
port,
dialect,
dialectOptions,
pool,
timezone,
define,
benchmark,
tables,
camelCase,
indentation
})
await promisify(this._sequelizeAuto.run).bind(this._sequelizeAuto)()
return await this._sleep(1000)
} catch (error) {
this._log(error, 'error')
throw error
}
}
/**
*
* @private
* @param {string} [name='']
* @param {string} [str='']
* @returns {void}
* @memberof Scaffolding
*/
_setModelAttributes(name = '', str = '') {
const table = this._toTableize(name)
const clazz = this._toModelClassify(name)
this._attributes = str
// eslint-disable-next-line no-useless-escape
.replace(new RegExp(/\/\*\sjshint\sindent\:\s1\s\*\//, 'g'), '')
// eslint-disable-next-line no-useless-escape
.replace(new RegExp(/module\.exports\s\=\sfunction\(sequelize,\sDataTypes\)\s\{/, 'g'), '')
.replace(
new RegExp(
/\t{1,10}logical:\s\{\n\t{1,10}type:\sDataTypes\.INTEGER\(1\),\n\t{1,10}allowNull:\strue,\n\t{1,10}field:\s'logical'\n\t{1,10}\}/,
'g'
),
this._logicalFieldTemplate
)
.replace(new RegExp('\\s\\sreturn\\ssequelize\\.define\\(\'' + clazz + '\',\\s\\{', 'g'), '')
.replace(new RegExp('\\treturn\\ssequelize\\.define\\(\'' + clazz + '\',\\s\\{', 'g'), '')
.replace(new RegExp('\\},\\s\\{\\n\\t\\ttableName:\\s\'' + table + '\'\\n\t\\}\\);\\n\\};', 'g'), '')
.replace(new RegExp(/\n{2,3}/, 'g'), '')
.replace(new RegExp(/\n\t\n/, 'g'), '')
.replace(new RegExp(/\t{3}/, 'g'), ' ')
.replace(new RegExp(/\t{2}/, 'g'), ' ')
}
/**
*
* @private
* @memberof Scaffolding
*/
_setModelReferences() {
const regexp = new RegExp(/references:\s\{\n\s{1,16}model:\s'(\w{1,128})',/, 'g')
while (true) {
const result = regexp.exec(this._attributes)
const model = RegExp.$1
if (_.isEmpty(result)) {
break
}
if (!_.isEmpty(model)) {
this._references.push(model)
continue
}
}
}
/**
*
* @private
* @returns {string}
* @memberof Scaffolding
*/
_getModelReferencesAsString() {
const tab = ' '
const br = '\n'
if (_.isEmpty(this._references)) {
return '\t\t// TODO: implement your relations'
}
return this._references.map(ref => `${tab}this.belongsTo(models.${this._toModelClassify(ref)})`).join(br)
}
/**
*
* @private
* @param {string} [filename='']
* @returns {boolean}
* @memberof Scaffolding
*/
_fileExists(filename = '') {
return Fs.existsSync(filename)
}
/**
*
* @private
* @param {boolean} [withExit=false]
* @param {TScaffoldingLogColor} [color='green']
* @returns {void}
* @memberof Scaffolding
*/
_showHelp(withExit = false, color = 'green') {
const handler = txt => Colors[color].bind(Colors)(txt)
if (withExit) {
return this._commander.help(handler)
}
return this._commander.outputHelp(handler)
}
/**
* @private
* @description show command help
* @returns {void}
* @memberof Scaffolding
*/
_cmdHelp() {
this._log('', 'log')
this._log(' Generate Template via CLI:', 'debug')
this._log(' $ node cli generate <name> -c -m', 'log')
this._log(' $ node cli g <name>', 'log')
this._log('', 'log')
this._log(' Examples:', 'debug')
this._log(' # Create a new controller', 'debug')
this._log(' $ node cli generate your_class_name --controller ', 'log')
this._log(' $ node cli g your_class_name -c ', 'log')
this._log('', 'log')
this._log(' # Create a new route', 'debug')
this._log(' $ node cli generate your_class_name --route ', 'log')
this._log(' $ node cli g your_class_name -r ', 'log')
this._log('', 'log')
this._log(' # Create a new model ', 'debug')
this._log(' # [!!!IMPORTANT!!!]: you must create a table on corresponded database before running', 'error')
this._log(' $ node cli generate your_class_name --model ', 'log')
this._log(' $ node cli g your_class_name -m ', 'log')
this._log('', 'log')
this._log(' # Create a new schema for mongoDB', 'debug')
this._log(' $ node cli generate your_class_name --schema ', 'log')
this._log(' $ node cli g your_class_name -s ', 'log')
this._log('', 'log')
this._log(' # Create all-in-one(controller/route/model/schema)', 'debug')
this._log(' $ node cli generate your_class_name --run-all ', 'log')
this._log(' $ node cli g your_class_name -a ', 'log')
this._log('', 'log')
this._log(' # Or you can specify with the type parameter as a list (comma separated)', 'debug')
this._log(' $ node cli generate your_class_name --type "controller,route,model"', 'log')
this._log(' $ node cli g your_class_name -t "controller,route,model"', 'log')
this._log('', 'log')
this._log(' # Run with force option ', 'debug')
this._log(' # [!!!IMPORTANT!!!]: running with this options will overwrite the exists file!', 'error')
this._log(' $ node cli generate your_class_name --force --controller ', 'log')
this._log(' $ node cli g your_class_name -f -c ', 'log')
this._log('', 'log')
}
/**
*
* @private
* @returns {string}
* @memberof Scaffolding
*/
_getControllerDir() {
return Path.resolve(Process.cwd(), this._options.path.controllers)
}
/**
*
* @private
* @returns {string}
* @memberof Scaffolding
*/
_getRouteDir() {
return Path.resolve(Process.cwd(), this._options.path.routes)
}
/**
*
* @private
* @returns {string}
* @memberof Scaffolding
*/
_getModelDir() {
return Path.resolve(Process.cwd(), this._options.path.models)
}
/**
*
* @private
* @returns {string}
* @memberof Scaffolding
*/
_getSchemaDir() {
return Path.resolve(Process.cwd(), this._options.path.schemas)
}
/**
* @private
* @param {string} [type='']
* @returns {string}
* @memberof Scaffolding
*/
_getTemplatePath(type = '') {
const filename = `${type}.tpl`
const dir = Path.resolve(__dirname, '../templates/')
return Path.join(dir, filename)
}
/**
*
* @private
* @param {string} [str='']
* @returns {string}
* @memberof Scaffolding
*/
_getRoutePath(str = '') {
return Path.join(this._getRouteDir(), `${this._getSingularFileName(str)}.js`)
}
/**
*
* @private
* @param {string} [str='']
* @returns {string}
* @memberof Scaffolding
*/
_getControllerPath(str = '') {
return Path.join(this._getControllerDir(), `${this._getSingularFileName(str)}.js`)
}
/**
*
* @private
* @param {string} [str='']
* @returns {string}
* @memberof Scaffolding
*/
_getModelPath(str = '') {
return Path.join(this._getModelDir(), `${this._getPluralFileName(str)}.js`)
}
/**
*
* @private
* @param {string} [str='']
* @returns {string}
* @memberof Scaffolding
*/
_getSchemaPath(str = '') {
return Path.join(this._getSchemaDir(), `${this._getSingularFileName(str)}.js`)
}
/**
*
* @private
* @param {string} [str='']
* @returns {string}
* @memberof Scaffolding
*/
_getSingularFileName(str = '') {
return _.toLower(Inflector.singularize(Inflector.underscore(str)))
}
/**
*
* @private
* @param {string} [str='']
* @returns {string}
* @memberof Scaffolding
*/
_getPluralFileName(str = '') {
return _.toLower(Inflector.pluralize(Inflector.underscore(str)))
}
/**
*
* @private
* @param {string} [str='']
* @returns {string}
* @memberof Scaffolding
*/
_toSingular(str = '') {
return Inflector.singularize(Inflector.camelize(Inflector.underscore(str), false))
}
/**
*
* @private
* @param {string} [str='']
* @returns {string}
* @memberof Scaffolding
*/
_toPluralize(str = '') {
return Inflector.pluralize(Inflector.camelize(Inflector.underscore(str), false))
}
/**
*
* @private
* @param {string} [str='']
* @returns {string}
* @memberof Scaffolding
*/
_toClassify(str = '') {
return Inflector.classify(Inflector.camelize(Inflector.underscore(str)))
}
/**
*
* @private
* @param {string} [str='']
* @returns {string}
* @memberof Scaffolding
*/
_toModelizeAndPluralize(str = '') {
return Inflector.pluralize(Inflector.classify(Inflector.camelize(Inflector.underscore(str), true)))
}
/**
*
* @private
* @param {string} [str='']
* @returns {string}
* @memberof Scaffolding
*/
_toModelize(str = '') {
return Inflector.singularize(Inflector.camelize(Inflector.underscore(str), false))
}
/**
*
* @private
* @param {string} [str='']
* @returns {string}
* @memberof Scaffolding
*/
_toModelClassify(str = '') {
return Inflector.pluralize(Inflector.camelize(Inflector.underscore(str), false))
}
/**
*
* @private
* @param {string} [str='']
* @returns {string}
* @memberof Scaffolding
*/
_toTableize(str = '') {
return Inflector.tableize(Inflector.classify(Inflector.camelize(Inflector.underscore(str))))
}
/**
*
* @private
* @param {string} [str='']
* @returns {string}
* @memberof Scaffolding
*/
_toResource(str = '') {
return Inflector.underscore(Inflector.pluralize(Inflector.camelize(Inflector.underscore(str), false)))
}
/**
*
* @private
* @param {string} [str='']
* @returns {string}
* @memberof Scaffolding
*/
_toUnderscore(str = '') {
return Inflector.underscore(Inflector.singularize(Inflector.camelize(Inflector.underscore(str), false)))
}
/**
*
* @private
* @param {Object} src
* @param {Object} extend
* @returns {Object}
* @memberof Scaffolding
*/
_deepExtend(src, extend) {
return _.assign(_.cloneDeep(src), extend)
}
/**
*
* @private
* @param {*} val
* @param {TScaffoldingLogLevel} [level='error']
* @returns {void}
* @memberof Scaffolding
*/
_log(val, level = 'error') {
/** @type {TScaffoldingLogColor} */
let color = 'white'
switch (level) {
case 'error':
color = 'red'
break
case 'warn':
color = 'yellow'
break
case 'info':
color = 'cyan'
break
case 'log':
color = 'green'
break
case 'debug':
color = 'white'
break
default:
color = 'gray'
break
}
const logger = _.bind(_.get(console, level), console)
const args = _.bind(_.get(Colors, color))(val)
return logger(args)
}
/**
*
* @private
* @static
* @param {number} [ms=1000]
* @returns {Promise<void>}
* @memberof Scaffolding
*/
async _sleep(ms = 100) {
return new Promise(resolve => setTimeout(resolve, ms))
}
}
module.exports = new Scaffolding()
Source