├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── app.js ├── configs │ └── config.js ├── controllers │ └── controller.js ├── models │ └── model.js ├── package.json ├── policies │ └── policy.js ├── routes │ └── route.js ├── services │ └── service.js └── utils │ ├── db.js │ └── utils.js ├── index.js ├── lib ├── generate │ ├── generate.controller.js │ ├── generate.model.js │ ├── generate.policy.js │ ├── generate.route.js │ └── generate.service.js ├── new │ ├── genConfigFile.js │ ├── genFolders.js │ └── newProject.js ├── tests │ ├── cli.spec.js │ ├── generate.spec.js │ ├── mocks │ │ ├── files.mocks.js │ │ └── folders.mocks.js │ └── utils.js └── utils │ ├── addCustomDataHelper.js │ ├── askQuestion.js │ ├── capitalize.js │ ├── getProjectPath.js │ ├── getPropertiesFromModel.js │ ├── logger.js │ ├── newFile.js │ └── stringifyByFileType.js └── package.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:latest 11 | 12 | working_directory: ~/hapi-cli 13 | 14 | steps: 15 | - checkout 16 | 17 | # Download and cache dependencies 18 | - run: npm install 19 | - run: sudo npm install nyc mocha chai chai-json-schema chai-fs -g 20 | 21 | # run tests! 22 | - run: npm test 23 | 24 | 25 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/tests/* 2 | assets/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "rules": { 4 | "no-restricted-syntax": "off", 5 | "no-await-in-loop": "off", 6 | "no-param-reassign": "off" 7 | } 8 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | 41 | .idea 42 | /.nyc_output/ 43 | docker-compose.yml 44 | .DS_Store 45 | 46 | package-lock.json 47 | /.coveralls.yml 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Antoine Moreaux 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://img.shields.io/circleci/project/github/RedSparr0w/node-csgo-parser.svg)](https://circleci.com/gh/AMoreaux/hapi-cli) 2 | [![NPM Downloads](https://img.shields.io/npm/dm/hapi-starter.svg)](https://www.npmjs.com/package/hapi-starter) 3 | [![Coverage Status](https://coveralls.io/repos/github/AMoreaux/hapi-cli/badge.svg?branch=master)](https://coveralls.io/github/AMoreaux/hapi-cli?branch=master) 4 | [![Known Vulnerabilities](https://snyk.io/test/github/amoreaux/hapi-cli/badge.svg)](https://snyk.io/test/github/amoreaux/hapi-cli) 5 | Dependency Status 6 | devDependency Status 7 | [![License](http://img.shields.io/npm/l/@ljharb/eslint-config.svg)](http://img.shields.io/npm/l/@ljharb/eslint-config.svg) 8 | 9 | # Hapi-cli generator 10 | 11 | 12 | ## Files structure 13 | 14 | ``` 15 | project/ 16 | ├── app.js 17 | ├── config/ 18 | │ ├── default.json 19 | │ └── production.json 20 | ├── controllers/ 21 | │ └── user.controller.js 22 | ├── models/ 23 | │ └── user.model.js 24 | ├── policies/ 25 | │ ├── admin.policy.js 26 | │ └── default.policy.js 27 | ├── routes/ 28 | │ ├── base.route.js 29 | │ └── user.route.js 30 | └── services/ 31 | ├── utils/ 32 | │ ├── db.js 33 | │ └── utils.js 34 | └── hashPassword.service.js 35 | ``` 36 | ## Installation 37 | 38 | npm install -g hapi-starter 39 | 40 | ## Create project 41 | 42 | hapi-cli new [project-name] 43 | 44 | cd [project-name] 45 | 46 | npm start 47 | 48 | ## Create model 49 | 50 | hapi-cli generate model [name] [options] 51 | 52 | #### Options 53 | 54 | --properties | -p : List of properties of your entitie. (format: [name]:[type]) 55 | 56 | #### Examples 57 | 58 | hapi-cli generate model room --properties size:number,name:string 59 | 60 | ## Create controller 61 | 62 | hapi-cli generate controller [name] [options] 63 | 64 | #### Options 65 | 66 | --methods | -m : List of methods for your controller (default: create,remove,find,update ) 67 | 68 | #### Examples 69 | 70 | hapi-cli generate controller room 71 | hapi-cli generate controller room --methods create,remove 72 | 73 | ## Create Route 74 | 75 | hapi-cli generate route [name] [options] 76 | 77 | #### Options 78 | 79 | --verbs | -v : List of endpoints for your route (default: get,post,delete,put ) 80 | --controller | -c : Name of controller. (default: file's name ) 81 | 82 | #### Examples 83 | 84 | hapi-cli generate route room 85 | hapi-cli generate route room --verbs get,post 86 | 87 | ## Create API 88 | 89 | hapi-cli generate api [name] [options] 90 | 91 | #### Options 92 | 93 | --verbs | -v : List of endpoints for your route (default: get,post,delete,put ) 94 | --controller | -c : Name of controller. (default: file's name ) 95 | --methods | -m : List of methods for your controller (default: create,remove,find,update ) 96 | --properties | -p : List of properties of your entitie. (format: [name]:[type]) 97 | 98 | #### Example 99 | 100 | hapi-cli generate api owner 101 | 102 | 103 | ## project 104 | 105 | ### TODO 106 | 107 | If you'd like the cli to do something that it doesn't do or want to report a bug please use the github issue tracker on github 108 | 109 | ### fork / patches / pull requests 110 | 111 | You are very welcome to send patches or pull requests 112 | -------------------------------------------------------------------------------- /assets/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const appPackage = require(__dirname + '/package.json'); 3 | const Hapi = require('hapi'); 4 | const colors = require('colors/safe'); 5 | const Config = require('config'); 6 | const utils = require('./services/utils/utils.js'); 7 | const db = require('./services/utils/db.js') 8 | 9 | 10 | async function start() { 11 | 12 | try { 13 | db.connect(); 14 | 15 | utils.addModels(); 16 | 17 | const server = new Hapi.Server(Config.util.toObject(Config.get('server.connection'))) 18 | 19 | await utils.addPolicies(server) 20 | 21 | utils.addRoute(server); 22 | 23 | await server.start() 24 | console.log(colors.green('%s %s started on %s'), appPackage.name, appPackage.version, server.info.uri); 25 | 26 | module.exports = server; 27 | 28 | } catch (err) { 29 | console.log(err) 30 | process.exit(0) 31 | } 32 | } 33 | 34 | start() 35 | -------------------------------------------------------------------------------- /assets/configs/config.js: -------------------------------------------------------------------------------- 1 | module.exports = (type) => { 2 | return { 3 | 'server': { 4 | 'auth': { 5 | 'saltFactor': 10 6 | }, 7 | 'connection': { 8 | 'port': (type === 'production') ? 80 : 3001, 9 | 'routes': { 10 | 'cors': { 11 | 'origin': ['*'], 12 | 'headers': [ 13 | 'Access-Control-Allow-Origin', 14 | 'Access-Control-Allow-Headers', 15 | 'Origin', 16 | 'X-Requested-With', 17 | 'Content-Type', 18 | 'Authorization' 19 | ], 20 | 'credentials': true 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /assets/controllers/controller.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | create: `async (request, h) => { 4 | try{ 5 | const {{entity}} = await new {{entity.upperFirstChar}}(request.payload).save(); 6 | return h.response({'success': '{{entity}}_created', '{{entity}}': {{entity}}}).code(201); 7 | }catch (err){ 8 | throw Boom.badRequest(err); 9 | } 10 | }`, 11 | 12 | update: `async (request, h) => { 13 | try{ 14 | const result = await {{entity.upperFirstChar}}.findOneAndUpdate({_id: request.params.id}, request.payload, {new: true}).exec(); 15 | return result; 16 | }catch(e){ 17 | return Boom.badRequest(err); 18 | } 19 | }`, 20 | 21 | find: `async (request, h) => { 22 | try{ 23 | const query = await {{entity.upperFirstChar}}.find({ 24 | _id: request.params.id 25 | }).exec(); 26 | return query; 27 | }catch(e){ 28 | return Boom.badData(err); 29 | } 30 | }`, 31 | 32 | remove: `async (request, h) => { 33 | try{ 34 | await {{entity.upperFirstChar}}.findOneAndRemove({ 35 | _id: request.params.id 36 | }).exec(); 37 | return {'success': 'user_delete'}; 38 | }catch(e){ 39 | return Boom.badData(err); 40 | } 41 | }`, 42 | 43 | get: function (methodList) { 44 | 45 | const result = {}; 46 | 47 | methodList.forEach((method) => { 48 | result[method] = this[method]; 49 | }); 50 | 51 | return result; 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /assets/models/model.js: -------------------------------------------------------------------------------- 1 | const capitalize = require('../../lib/utils/capitalize') 2 | 3 | module.exports = { 4 | schema:{ 5 | createdAt:{ 6 | "type": "Date", 7 | "default": "Date.now()" 8 | }, 9 | updatedAt: { 10 | "type": "Date", 11 | "default": "Date.now()" 12 | }, 13 | }, 14 | methods: { 15 | comparePassword: `function (candidatePassword, cb) { 16 | Bcrypt.compare(candidatePassword, this.password, (err, isMatch) => { 17 | if (err) { 18 | return cb(err); 19 | } 20 | cb(null, isMatch); 21 | }); 22 | }`, 23 | }, 24 | hooks: { 25 | addDate : `function(){ 26 | this.update({},{ $set: { updatedAt: new Date() } }); 27 | }` 28 | }, 29 | 30 | 31 | getProperties:function (properties){ 32 | const result = {}; 33 | properties.forEach((property) => { 34 | result[property] = this.schema[property]; 35 | }); 36 | return result; 37 | }, 38 | 39 | getMethods:function (methods){ 40 | const result = {}; 41 | methods.forEach((method) => { 42 | result[method] = this.methods[method]; 43 | }); 44 | return result; 45 | }, 46 | 47 | getHooks: function (hooks, onSchema) { 48 | hooks.forEach((hook) => { 49 | const flowLocationAndName = hook.split('.'); 50 | if(!onSchema[flowLocationAndName[0]][flowLocationAndName[1]]) onSchema[flowLocationAndName[0]][flowLocationAndName[1]] = []; 51 | onSchema[flowLocationAndName[0]][flowLocationAndName[1]].push((this.hooks[flowLocationAndName[2]]) ? this.hooks[flowLocationAndName[2]] : capitalize(flowLocationAndName[2])); 52 | }); 53 | return onSchema 54 | } 55 | 56 | }; 57 | -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "description": "Please write a description", 4 | "main": "app.js", 5 | "scripts": { 6 | "start":"node app.js", 7 | "test": "lab -C -D -v -l --context-timeout 0 --timeout 10000" 8 | }, 9 | "author": "AMoreaux", 10 | "license": "MIT", 11 | "dependencies": { 12 | "config": "^1.30.0", 13 | "bcrypt": "^1.0.3", 14 | "boom": "^7.2.0", 15 | "colors": "^1.2.1", 16 | "hapi": "^17.2.2", 17 | "hapi-auth-jwt2": "^8.0.0", 18 | "joi": "^13.0.2", 19 | "jsonwebtoken": "^8.2.0", 20 | "mongoose": "4.13.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /assets/policies/policy.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | default: `async (decoded, request) => { 4 | jwt.verify(request.headers.authorization, Config.get('server.auth.secretKey'), (err, decoded) => { 5 | 6 | if (typeof decoded === 'undefined') { 7 | return { isValid: false }; 8 | } 9 | 10 | User.findOne({_id: decoded.iduser}).exec((err, currentUser) => { 11 | 12 | if (!currentUser || err) { 13 | return { isValid: false }; 14 | } 15 | 16 | request.currentUser = currentUser; 17 | return { isValid: true }; 18 | }); 19 | }); 20 | };`, 21 | 22 | admin: `async (decoded, request) => { 23 | jwt.verify(request.headers.authorization, Config.get('server.auth.secretKey'), (err, decoded) => { 24 | 25 | if (typeof decoded === 'undefined') { 26 | return { isValid: false }; 27 | } 28 | 29 | User.findOne({_id: decoded.iduser, admin: true}).exec((err, currentUser) => { 30 | 31 | if (!currentUser || err) { 32 | return Boom.badRequest('You must be admin user'); 33 | } 34 | 35 | request.currentUser = currentUser; 36 | return { isValid: true }; 37 | }); 38 | }); 39 | };`, 40 | }; 41 | -------------------------------------------------------------------------------- /assets/routes/route.js: -------------------------------------------------------------------------------- 1 | const hoek = require('hoek'); 2 | const getPropertiesFromModel = require('../../lib/utils/getPropertiesFromModel'); 3 | 4 | module.exports = { 5 | 404: { 6 | handler: `(request, h) => { 7 | return Boom.badRequest('route does not exist'); 8 | }`, 9 | uri: '/{p*}', 10 | options: {} 11 | }, 12 | POST: { 13 | handler: '{{entity.upperFirstChar}}Controller.create', 14 | uri: '', 15 | options: { 16 | validate: { 17 | payload: {}//getPropertiesFromModel, 18 | }, 19 | } 20 | }, 21 | GET: { 22 | handler: '{{entity.upperFirstChar}}Controller.find', 23 | uri: '/{id}', 24 | options: { 25 | validate: { 26 | params: { 27 | id: 'Joi.required()', 28 | }, 29 | }, 30 | }, 31 | }, 32 | DELETE: { 33 | handler: '{{entity.upperFirstChar}}Controller.remove', 34 | uri: '/{id}', 35 | options: { 36 | validate: { 37 | params: { 38 | id: 'Joi.required()', 39 | }, 40 | }, 41 | }, 42 | }, 43 | PUT: { 44 | handler: '{{entity.upperFirstChar}}Controller.update', 45 | uri: '/{id}', 46 | options: { 47 | validate: { 48 | params: { 49 | id: 'Joi.required()', 50 | }, 51 | }, 52 | }, 53 | }, 54 | get(name, projectPath, modelName) { 55 | const route = hoek.merge(this[name], this.defaultConfig(name)); 56 | this.customConfig(name, modelName, route, projectPath); 57 | return route; 58 | }, 59 | 60 | defaultConfig(name) { 61 | // if (!this[name].options.validate) return {}; 62 | 63 | return { 64 | options: { 65 | auth: false, 66 | validate: { 67 | options: { 68 | abortEarly: false, 69 | allowUnknown: true, 70 | }, 71 | }, 72 | }, 73 | pre: [], 74 | }; 75 | }, 76 | 77 | customConfig(name, modelName, route, projectPath) { 78 | if (!this[name].options.validate) return {}; 79 | 80 | for (const validate in this[name].options.validate) { 81 | if (typeof this[name].options.validate[validate] === 'function') { 82 | this[name].options.validate[validate] = this[name].options.validate[validate](projectPath, modelName); 83 | } 84 | } 85 | 86 | route.options.validate = hoek.merge(route.options.validate, this[name].options.validate); 87 | }, 88 | }; 89 | 90 | -------------------------------------------------------------------------------- /assets/services/service.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | hashPassword: `function (next) { 4 | let user = (this.op === 'update') ? this._update.$set : this; 5 | if (!user || !user.password || user.password.length === 60) { 6 | return next(); 7 | } 8 | Bcrypt.genSalt(Config.get('server.auth.saltFactor'), (err, salt) => { 9 | if (err) { 10 | return next(err); 11 | } 12 | Bcrypt.hash(user.password, salt, (err, hash) => { 13 | if (err) { 14 | return next(err); 15 | } 16 | user.password = hash; 17 | next(); 18 | }); 19 | }); 20 | };`, 21 | 22 | get:function (services){ 23 | const result = {}; 24 | if(!Array.isArray(services)){ 25 | return this[services] 26 | } 27 | services.forEach((service) => { 28 | result[service] = this[service]; 29 | }); 30 | return result; 31 | }, 32 | 33 | }; -------------------------------------------------------------------------------- /assets/utils/db.js: -------------------------------------------------------------------------------- 1 | const colors = require('colors/safe'); 2 | const Mongoose = require('mongoose'); 3 | const Config = require('config'); 4 | const err = console.error; 5 | const log = console.log; 6 | 7 | module.exports ={ 8 | connect: () => { 9 | Mongoose.connect(Config.get('mongodb.uri'),{useMongoClient: true}); 10 | 11 | Mongoose.connection.on('error', (e)=> { 12 | err('Mongoose can not open connection'); 13 | err(e); 14 | process.exit(); 15 | }); 16 | 17 | Mongoose.connection.on('connected', () => { 18 | log(colors.green('Connection DB ok', Config.get('mongodb.uri'))); 19 | }); 20 | 21 | Mongoose.connection.on('disconnected', () => { 22 | err(colors.red('Connection DB lost')); 23 | 24 | setTimeout(() => { 25 | Mongoose.connect(Config.get('mongodb.uri')); 26 | err('DB reconnection'); 27 | }, 15000); 28 | }); 29 | } 30 | }; -------------------------------------------------------------------------------- /assets/utils/utils.js: -------------------------------------------------------------------------------- 1 | const Path = require('path'); 2 | const Mongoose = require('mongoose'); 3 | Mongoose.Promise = global.Promise; 4 | const path = require('path'); 5 | const Config = require('config'); 6 | 7 | module.exports = { 8 | 9 | getFiles(path) { 10 | path = path[path.length - 1] !== '/' ? path + '/' : path; 11 | let files = []; 12 | try { 13 | files = require('fs').readdirSync(Path.resolve(__dirname, '../..', path)); 14 | } catch (e) { 15 | console.log(e); 16 | process.exit(); 17 | } 18 | return files.map((file) => { 19 | return Path.resolve(__dirname, '../..', path, file) 20 | }); 21 | }, 22 | 23 | addRoute(server) { 24 | this.getFiles('routes').forEach((routesFile) => { 25 | 26 | require(routesFile).forEach((route) => { 27 | server.route(route); 28 | }); 29 | }); 30 | }, 31 | 32 | async addPolicies(server) { 33 | await server.register(require('hapi-auth-jwt2')); 34 | 35 | this.getFiles('policies').forEach((policyFile) => { 36 | 37 | let policy = require(policyFile); 38 | let name = path.basename(policyFile, '.js'); 39 | let namePolicie = name.split('.')[0]; 40 | 41 | 42 | server.auth.strategy(namePolicie, 'jwt', { 43 | key: Config.get('server.auth.secretKey'), 44 | validate: policy, 45 | verifyOptions: {algorithms: ['HS256']} 46 | }); 47 | 48 | }); 49 | server.auth.default('default', 'jwt'); 50 | 51 | }, 52 | 53 | addModels() { 54 | global.Models = {}; 55 | 56 | this.getFiles('models').forEach((modelFile) => { 57 | 58 | let modelInterface = require(modelFile); 59 | let schema = Mongoose.Schema(modelInterface.schema, {versionKey: false}); 60 | let name = path.basename(modelFile, '.js'); 61 | 62 | if (modelInterface.statics) { 63 | for (let modelStatic in modelInterface.statics) { 64 | schema.statics[modelStatic] = modelInterface.statics[modelStatic]; 65 | } 66 | } 67 | 68 | if (modelInterface.methods) { 69 | for (let modelMethod in modelInterface.methods) { 70 | schema.methods[modelMethod] = modelInterface.methods[modelMethod]; 71 | } 72 | } 73 | 74 | if (modelInterface.onSchema) { 75 | for (let type in modelInterface.onSchema) { 76 | for (let func in modelInterface.onSchema[type]) { 77 | if (Array.isArray(modelInterface.onSchema[type][func])) { 78 | for (var i = 0; i < modelInterface.onSchema[type][func].length; i++) { 79 | schema[type](func, modelInterface.onSchema[type][func][i]); 80 | } 81 | } else { 82 | schema[type](func, modelInterface.onSchema[type][func]); 83 | } 84 | } 85 | } 86 | } 87 | 88 | let nameModel = name.split('.')[0]; 89 | nameModel = nameModel.charAt(0).toUpperCase() + nameModel.slice(1); 90 | Models[nameModel] = Mongoose.model(nameModel, schema); 91 | }); 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 4 | const program = require('commander'); 5 | const newProject = require('./lib/new/newProject'); 6 | const logger = require('./lib/utils/logger'); 7 | const {exec} = require('child_process'); 8 | 9 | const newModel = require('./lib/generate/generate.model'); 10 | const newController = require('./lib/generate/generate.controller'); 11 | const newRoute = require('./lib/generate/generate.route'); 12 | 13 | 14 | program 15 | .command('new ') 16 | .description('Create new project.') 17 | .option('-d, --debug [debug]', 'active mode debug') 18 | .action(async (name, options) => { 19 | if (!options.name) options.name = 'new-project'; 20 | 21 | await newProject 22 | .new(name) 23 | .catch(err => logger.error((options.debug) ? err.stack : err)); 24 | 25 | process.exit(); 26 | }); 27 | 28 | program 29 | .command('generate ') 30 | .description('generate new file') 31 | .option('-m, --methods [methods]', 'List methods for new controller like (create,remove,find,update)') 32 | .option('-p, --properties [properties]', 'For model list properties like (firstname:String,age:Number)') 33 | .option('-v, --verbs [verbs]', 'Set verbs for your route like (get,post)') 34 | .option('-c, --controller [controller]', 'Set the name of the controller which contain handlers') 35 | .option('-d, --debug [debug]', 'active mode debug') 36 | .action(async (type, name, options) => { 37 | switch (type) { 38 | case 'controller': 39 | await newController.new(name, options) 40 | .catch(error => logger.error((options.debug) ? error.stack : error)); 41 | break; 42 | case 'model': 43 | await newModel.new(name, options) 44 | .catch(error => logger.error((options.debug) ? error.stack : error)); 45 | break; 46 | case 'route': 47 | await newRoute.new(name, options) 48 | .catch(error => logger.error((options.debug) ? error.stack : error)); 49 | break; 50 | case 'api': 51 | try { 52 | await newController.new(name, options); 53 | await newModel.new(name, options); 54 | await newRoute.new(name, options); 55 | } catch (error) { 56 | logger.error((options.debug) ? error.stack : error); 57 | } 58 | break; 59 | default: 60 | break; 61 | } 62 | 63 | process.exit(); 64 | }); 65 | 66 | // program 67 | // .command('*', 'default', {isDefault: true}) 68 | // .action(() => { 69 | // 70 | // exec('hc new --help', (error, stdout, stderr) => { 71 | // if (error) { 72 | // logger.error(`exec error: ${error}`); 73 | // return; 74 | // } 75 | // logger.log(`stdout: ${stdout}`); 76 | // logger.log(`stderr: ${stderr}`); 77 | // }); 78 | // 79 | // exec('hc generate --help', (error, stdout, stderr) => { 80 | // if (error) { 81 | // logger.error(`exec error: ${error}`); 82 | // return; 83 | // } 84 | // logger.log(`stdout: ${stdout}`); 85 | // logger.log(`stderr: ${stderr}`); 86 | // }); 87 | // 88 | // process.exit(); 89 | // }); 90 | // 91 | 92 | program.parse(process.argv); 93 | -------------------------------------------------------------------------------- /lib/generate/generate.controller.js: -------------------------------------------------------------------------------- 1 | const newFile = require('../utils/newFile'); 2 | const controllerAssets = require('../../assets/controllers/controller'); 3 | const logger = require('../utils/logger'); 4 | const capitalize = require('../utils/capitalize'); 5 | const getProjectPath = require('../utils/getProjectPath'); 6 | 7 | const availableMethods = ['create', 'remove', 'find', 'update']; 8 | 9 | exports.new = async (name, options = {}, projectPath = getProjectPath()) => { 10 | function getMethods() { 11 | const methods = (!options.methods || options.methods === 'crud') ? availableMethods : options.methods.split(','); 12 | 13 | methods.forEach((method) => { 14 | if (availableMethods.indexOf(method) === -1) { 15 | logger.warn(`${method} is not a valid method. You can use "create", "remove", "find" or "update")`); 16 | } 17 | }); 18 | 19 | return controllerAssets.get(methods); 20 | } 21 | const controller = await getMethods(options); 22 | await newFile({ 23 | projectPath, 24 | filePath: 'controllers/', 25 | fileName: `${name}.controller`, 26 | fileType: 'js', 27 | fileContent: controller, 28 | nodeModule: true, 29 | entity: name, 30 | modules: ['mongoose', 'boom', { [name]: `Models.${capitalize(name)}` }], 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /lib/generate/generate.model.js: -------------------------------------------------------------------------------- 1 | const newFile = require('../utils/newFile'); 2 | const capitalize = require('../utils/capitalize'); 3 | const modelsAssets = require('../../assets/models/model.js'); 4 | const getProjectPath = require('../utils/getProjectPath'); 5 | 6 | const schemaTypes = ['String', 'Number', 'Buffer', 'Boolean', 'Date', 'Mixed', 'ObjectId', 'Array']; 7 | const logger = require('../utils/logger'); 8 | 9 | exports.new = async (name, options, projectPath = getProjectPath()) => { 10 | function getContent() { 11 | const model = { 12 | schema: modelsAssets.getProperties(['createdAt', 'updatedAt']), 13 | statics: {}, 14 | methods: {}, 15 | onSchema: { 16 | pre: {}, 17 | post: {}, 18 | }, 19 | }; 20 | 21 | if (options.properties) { 22 | if (!/([a-zA-Z]+:[a-zA-Z]+) */g.test(options.properties)) { 23 | throw new Error('error: wrong format of params (firstname:String,age:Number)'); 24 | } 25 | 26 | const properties = options.properties.split(','); 27 | 28 | properties.forEach((elm) => { 29 | const key = elm.split(':')[0]; 30 | const value = capitalize(elm.split(':')[1]); 31 | 32 | if (schemaTypes.indexOf(value) !== -1) { 33 | model.schema[key] = { type: value }; 34 | } else { 35 | logger.warn(`${value} for ${key} is unavailable schema type`); 36 | } 37 | }); 38 | } 39 | 40 | if (options.methods) { 41 | const methods = options.methods.split(','); 42 | model.methods = modelsAssets.getMethods(methods); 43 | } 44 | 45 | if (options.hooks) { 46 | const hooks = options.hooks.split(','); 47 | model.onSchema = modelsAssets.getHooks(hooks, model.onSchema); 48 | } 49 | 50 | return model; 51 | } 52 | 53 | const model = await getContent(options); 54 | 55 | await newFile({ 56 | projectPath, 57 | filePath: 'models/', 58 | fileName: `${name}.model`, 59 | fileType: 'js', 60 | fileContent: model, 61 | modules: ['mongoose', 'bcrypt', { Schema: 'Mongoose.Schema' }, { ObjectId: 'Schema.ObjectId' }, { hashPassword: 'require("../services/hashPassword.service")' }], 62 | nodeModule: true, 63 | }); 64 | }; 65 | 66 | -------------------------------------------------------------------------------- /lib/generate/generate.policy.js: -------------------------------------------------------------------------------- 1 | const newFile = require('../utils/newFile'); 2 | const policyAssets = require('../../assets/policies/policy'); 3 | const getProjectPath = require('../utils/getProjectPath'); 4 | 5 | exports.new = async (name, options = {}, projectPath = getProjectPath()) => { 6 | await newFile({ 7 | projectPath, 8 | filePath: 'policies/', 9 | fileName: `${name}.policy`, 10 | fileType: 'js', 11 | fileContent: policyAssets[name], 12 | nodeModule: true, 13 | entity: name, 14 | modules: ['mongoose', 'boom', { User: 'Models.User' }], 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/generate/generate.route.js: -------------------------------------------------------------------------------- 1 | const newFile = require('../utils/newFile'); 2 | const routeAssets = require('../../assets/routes/route.js'); 3 | const capitalize = require('../utils/capitalize'); 4 | const getProjectPath = require('../utils/getProjectPath'); 5 | 6 | const availableVerbs = ['GET', 'POST', 'DELETE', 'OPTIONS', 'PATCH', 'PUT', 'HEAD', 'LINK', 'UNLINK', 'PURGE', 'LOCK', 'UNLOCK', 'COPY', '*']; 7 | 8 | exports.new = async (name, options, projectPath = getProjectPath()) => { 9 | const modules = ['joi', 'boom']; 10 | if (options.controller !== 'none') { 11 | const controllerName = `${capitalize(options.controller || name)}Controller`; 12 | const property = `require('../controllers/${name.toLowerCase()}.controller')`; 13 | modules.push({ [controllerName]: property }); 14 | } 15 | 16 | function getProperties() { 17 | const verbs = (!options.verbs || options.verbs.toUpperCase() === 'CRUD') ? ['GET', 'POST', 'DELETE', 'PUT'] : options.verbs.split(','); 18 | 19 | return verbs.map((verb) => { 20 | verb = verb.toUpperCase(); 21 | if (availableVerbs.indexOf(verb) === -1) throw new Error(`VERB ${verb} is not available`); 22 | const routeParams = routeAssets.get(options.custom || verb, projectPath, name); 23 | return { 24 | method: `'${verb}'`, 25 | path: `'${((options.custom) ? routeParams.uri : `/${name}${routeParams.uri}`)}'`, 26 | handler: routeParams.handler, 27 | options: routeParams.options, 28 | }; 29 | }); 30 | } 31 | 32 | await newFile({ 33 | projectPath, 34 | filePath: 'routes/', 35 | fileName: `${name}.route`, 36 | entity: name, 37 | controller: options.controller, 38 | fileType: 'js', 39 | fileContent: getProperties(options), 40 | nodeModule: true, 41 | modules, 42 | }); 43 | }; 44 | 45 | -------------------------------------------------------------------------------- /lib/generate/generate.service.js: -------------------------------------------------------------------------------- 1 | const newFile = require('../utils/newFile'); 2 | const serviceAssets = require('../../assets/services/service'); 3 | const getProjectPath = require('../utils/getProjectPath'); 4 | 5 | exports.new = async (name, options = {}, projectPath = getProjectPath()) => { 6 | await newFile({ 7 | projectPath, 8 | filePath: 'services/', 9 | fileName: `${name}.service`, 10 | fileType: 'js', 11 | fileContent: serviceAssets.get(name), 12 | nodeModule: true, 13 | modules: options.modules.split(','), 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /lib/new/genConfigFile.js: -------------------------------------------------------------------------------- 1 | const newFile = require('../utils/newFile'); 2 | const askQuestions = require('../utils/askQuestion'); 3 | const configAssets = require('../../assets/configs/config'); 4 | 5 | module.exports = async (projectPath, projectName, type) => { 6 | const config = configAssets(type); 7 | let hasDatabase; 8 | 9 | config.server.connection.host = await askQuestions(`What is your ${type} server host (default: localhost) : `) || 'localhost'; 10 | config.server.auth.secretKey = await askQuestions(`What is your ${type} secret key (default: ChangeThisSecret) : `) || 'ChangeThisSecret'; 11 | 12 | if (type !== 'default') { 13 | hasDatabase = await askQuestions(`Do you want a database for the ${type} env ? (Y/n) : `); 14 | } 15 | 16 | if (type === 'default' || hasDatabase.toLowerCase() === 'y') { 17 | config.mongodb = {}; 18 | const host = await askQuestions(`What is your ${type} database host (default: localhost) : `) || 'localhost'; 19 | const name = await askQuestions(`What is your ${type} database name (default: ${projectName} ) : `) || projectName; 20 | const user = await askQuestions(`What is your ${type} database user (if none, no authentication) : `) || null; 21 | if (user) { 22 | const password = await askQuestions(`What is your ${type} database password (default: admin) : `) || 'admin'; 23 | config.mongodb.uri = `mongodb://${user}:${password}@${host}/${name}`; 24 | } else { 25 | config.mongodb.uri = `mongodb://${host}/${name}`; 26 | } 27 | await newFile({ 28 | projectPath, 29 | filePath: '/utils/', 30 | fileName: 'db', 31 | fileType: 'js', 32 | hasModel: true, 33 | outputFilePath: '/services/utils/', 34 | }); 35 | } 36 | 37 | await newFile({ 38 | projectPath, 39 | filePath: 'config/', 40 | fileName: type, 41 | fileType: 'json', 42 | fileContent: config, 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /lib/new/genFolders.js: -------------------------------------------------------------------------------- 1 | const Path = require('path'); 2 | const util = require('util'); 3 | const fs = require('fs'); 4 | const logger = require('../utils/logger'); 5 | 6 | const fsMkdirPromise = util.promisify(fs.mkdir); 7 | 8 | module.exports = async (params) => { 9 | if (fs.existsSync(params.projectPath)) throw new Error(`project ${params.projectName} already exists`); 10 | 11 | for (const folder of params.folders) { 12 | await fsMkdirPromise(Path.join(params.projectPath, folder)); 13 | } 14 | 15 | return logger.info('Folders created'); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/new/newProject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by antoinemoreaux on 08/06/2016. 3 | */ 4 | 5 | const { spawnSync } = require('child_process'); 6 | const genFolders = require('./genFolders'); 7 | const genConfigFile = require('./genConfigFile'); 8 | const newFile = require('../utils/newFile'); 9 | const generateModel = require('../generate/generate.model'); 10 | const generateRoute = require('../generate/generate.route'); 11 | const generateController = require('../generate/generate.controller'); 12 | const generatePolicy = require('../generate/generate.policy'); 13 | const generateService = require('../generate/generate.service'); 14 | const logger = require('../utils/logger'); 15 | const getProjectPath = require('../utils/getProjectPath'); 16 | 17 | exports.new = async (name) => { 18 | const projectPath = getProjectPath(name); 19 | 20 | await genFolders({ 21 | projectPath, 22 | projectName: name, 23 | folders: ['', 'controllers', 'models', 'routes', 'policies', 'config', 'services', 'services/utils'], 24 | }); 25 | 26 | await spawnSync('git', ['init'], { cwd: projectPath }); 27 | await newFile([{ 28 | projectPath, 29 | filePath: '/', 30 | fileName: 'app', 31 | fileType: 'js', 32 | hasModel: true, 33 | }, { 34 | projectPath, 35 | filePath: '/', 36 | fileName: 'package', 37 | fileType: 'json', 38 | customData: { 39 | method: 'merge', 40 | content: { 41 | name, scripts: { start: 'node app.js' }, 42 | }, 43 | }, 44 | hasModel: true, 45 | }, { 46 | projectPath, 47 | filePath: '/utils/', 48 | fileName: 'utils', 49 | fileType: 'js', 50 | hasModel: true, 51 | outputFilePath: '/services/utils/', 52 | }]); 53 | 54 | await generateController.new('user', { methods: 'crud' }, projectPath); 55 | await generateModel.new('user', { properties: 'firstname:string,age:number', methods: 'comparePassword', hooks: 'pre.save.hashPassword,pre.findOneAndUpdate.hashPassword,pre.findOneAndUpdate.addDate' }, projectPath); 56 | await generateRoute.new('user', {}, projectPath); 57 | await generateRoute.new('base', { 58 | verbs: '*', 59 | custom: '404', 60 | controller: 'none', 61 | }, projectPath); 62 | await generatePolicy.new('default', {}, projectPath); 63 | await generatePolicy.new('admin', {}, projectPath); 64 | await generateService.new('hashPassword', { modules: 'bcrypt,config' }, projectPath); 65 | await genConfigFile(projectPath, name, 'default'); 66 | await genConfigFile(projectPath, name, 'production'); 67 | 68 | if (process.env.NODE_ENV !== 'test') { 69 | await spawnSync('npm', ['install'], { stdio: 'inherit', cwd: projectPath }); 70 | } 71 | 72 | return logger.info(`Project ${name} created.`); 73 | }; 74 | -------------------------------------------------------------------------------- /lib/tests/cli.spec.js: -------------------------------------------------------------------------------- 1 | const utils = require('./utils'); 2 | const path = require('path'); 3 | const {spawn, exec} = require('child_process'); 4 | const {expect} = require('chai').use(require('chai-fs')).use(require('chai-json-schema')); 5 | const getProjectPath = require('../utils/getProjectPath'); 6 | const mocksFolders = require('./mocks/folders.mocks'); 7 | const mocksFiles = require('./mocks/files.mocks'); 8 | const projectPath = getProjectPath('test-cli'); 9 | 10 | describe('Commands line', () => { 11 | 12 | before(() => process.env.NODE_ENV = 'test'); 13 | 14 | afterEach(() => exec('rm -r ./test-cli')); 15 | 16 | it('New project', async () => { 17 | 18 | await utils.newProject(); 19 | 20 | let mock; 21 | expect(projectPath) 22 | .to.be.a.directory() 23 | .and.include.deep.contents(mocksFolders.folders.slice(1)); 24 | mock = mocksFiles.jsFile(); 25 | expect(path.join(projectPath, mock.filePath, mock.fileName + '.' + mock.fileType)) 26 | .to.be.a.file() 27 | .to.have.extname('.js'); 28 | mock = mocksFiles.jsonFile(); 29 | expect(path.join(projectPath, mock.filePath, mock.fileName + '.' + mock.fileType)) 30 | .to.be.a.file() 31 | .to.have.extname('.json'); 32 | mock = mocksFiles.controller(); 33 | expect(path.join(projectPath, 'controllers/', mock.name + '.controller.js')) 34 | .to.be.a.file() 35 | .to.have.extname('.js'); 36 | mock = mocksFiles.model(); 37 | expect(path.join(projectPath, 'models/', mock.name + '.model.js')) 38 | .to.be.a.file() 39 | .to.have.extname('.js'); 40 | mock = mocksFiles.route(); 41 | expect(path.join(projectPath, 'routes/', mock.name + '.route.js')) 42 | .to.be.a.file() 43 | .to.have.extname('.js'); 44 | expect(path.join(projectPath, 'config/', 'default.json')) 45 | .to.be.a.file() 46 | .to.have.extname('.json') 47 | .with.json.using.schema(utils.schemaConfig); 48 | expect(path.join(projectPath, 'config/', 'production.json')) 49 | .to.be.a.file() 50 | .to.have.extname('.json') 51 | .with.json.using.schema(utils.schemaConfig); 52 | 53 | 54 | }); 55 | 56 | it('New controller', async () => { 57 | 58 | await utils.newProject(); 59 | 60 | const child = spawn('node', ['../index', 'generate', 'controller', 'room'], {stdio: 'pipe', cwd: projectPath}); 61 | 62 | await new Promise((res) => { 63 | child.on('close', () => { 64 | expect(path.join(projectPath, 'controllers/', 'room' + '.controller.js')) 65 | .to.be.a.file() 66 | .to.have.extname('.js'); 67 | res() 68 | }) 69 | }) 70 | }); 71 | 72 | it('New controller with params', async () => { 73 | 74 | await utils.newProject(); 75 | 76 | const child = spawn('node', ['../index.js','generate', 'controller', 'room', '-m', 'create,remove'], {stdio: 'pipe', cwd: projectPath}); 77 | 78 | await new Promise((res) => { 79 | child.on('close', () => { 80 | expect(path.join(projectPath, 'controllers/', 'room' + '.controller.js')) 81 | .to.be.a.file() 82 | .to.have.extname('.js'); 83 | res() 84 | }) 85 | }) 86 | }); 87 | 88 | it('New model', async () => { 89 | 90 | await utils.newProject(); 91 | 92 | const child = spawn('node', ['../index', 'generate', 'model', 'room'], {stdio: 'pipe', cwd: projectPath}); 93 | 94 | await new Promise((res) => { 95 | child.on('close', () => { 96 | expect(path.join(projectPath, 'models/', 'room' + '.model.js')) 97 | .to.be.a.file() 98 | .to.have.extname('.js'); 99 | res() 100 | }) 101 | }) 102 | }); 103 | 104 | it('New model with params', async () => { 105 | 106 | await utils.newProject(); 107 | 108 | const child = spawn('node', ['../index', 'generate', 'model', 'room', '-p', 'size:number,name:string'], {stdio: 'pipe', cwd: projectPath}); 109 | 110 | await new Promise((res) => { 111 | child.on('close', (err) => { 112 | expect(path.join(projectPath, 'models/', 'room' + '.model.js')) 113 | .to.be.a.file() 114 | .to.have.extname('.js'); 115 | res() 116 | }) 117 | }) 118 | }); 119 | 120 | it('New route', async () => { 121 | 122 | await utils.newProject(); 123 | 124 | const child = spawn('node', ['../index', 'generate', 'route', 'room'], {stdio: 'pipe', cwd: projectPath}); 125 | 126 | await new Promise((res) => { 127 | child.on('close', () => { 128 | expect(path.join(projectPath, 'routes/', 'room' + '.route.js')) 129 | .to.be.a.file() 130 | .to.have.extname('.js'); 131 | res() 132 | }) 133 | }) 134 | }); 135 | 136 | it('New route with params', async () => { 137 | 138 | await utils.newProject(); 139 | 140 | const child = spawn('node', ['../index', 'generate', 'route', 'room', '-v', 'get,post', '-c', 'room'], {stdio: 'pipe', cwd: projectPath}); 141 | 142 | await new Promise((res) => { 143 | child.on('close', (err) => { 144 | expect(path.join(projectPath, 'routes/', 'room' + '.route.js')) 145 | .to.be.a.file() 146 | .to.have.extname('.js'); 147 | res() 148 | }) 149 | }) 150 | }); 151 | 152 | it('New API', async () => { 153 | 154 | await utils.newProject(); 155 | 156 | const child = spawn('node', ['../index', 'generate', 'api', 'room'], {stdio: 'pipe', cwd: projectPath}); 157 | 158 | await new Promise((res) => { 159 | child.on('close', () => { 160 | expect(path.join(projectPath, 'routes/', 'room' + '.route.js')) 161 | .to.be.a.file() 162 | .to.have.extname('.js'); 163 | expect(path.join(projectPath, 'controllers/', 'room' + '.controller.js')) 164 | .to.be.a.file() 165 | .to.have.extname('.js'); 166 | expect(path.join(projectPath, 'models/', 'room' + '.model.js')) 167 | .to.be.a.file() 168 | .to.have.extname('.js'); 169 | res() 170 | }) 171 | }) 172 | }); 173 | }); -------------------------------------------------------------------------------- /lib/tests/generate.spec.js: -------------------------------------------------------------------------------- 1 | const {exec} = require('child_process'); 2 | const path = require('path'); 3 | const {expect} = require('chai').use(require('chai-fs')); 4 | const generateController = require('../generate/generate.controller'); 5 | const generateModel = require('../generate/generate.model'); 6 | const generateRoute = require('../generate/generate.route'); 7 | const generatePolicy = require('../generate/generate.policy'); 8 | const generateService = require('../generate/generate.service'); 9 | const genFolders = require('../new/genFolders'); 10 | const newFile = require('../utils/newFile'); 11 | const getProjectPath = require('../utils/getProjectPath'); 12 | const mocksFolders = require('./mocks/folders.mocks'); 13 | const mocksFiles = require('./mocks/files.mocks'); 14 | 15 | describe('Create files and folders', () => { 16 | 17 | before(() => process.env.NODE_ENV = 'test'); 18 | 19 | after(() => exec('rm -r ./test')); 20 | 21 | it('get project path', () => { 22 | const projectPath = getProjectPath(); 23 | expect(projectPath).to.be.a.path(); 24 | expect(projectPath).to.be.a.directory(); 25 | }); 26 | 27 | it('generate folders', async () => { 28 | await genFolders(mocksFolders); 29 | expect(mocksFolders.projectPath).to.be.a.directory().with.deep.contents(mocksFolders.folders.slice(1)); 30 | }); 31 | 32 | it('generate js file', async () => { 33 | const mock = mocksFiles.jsFile(); 34 | await newFile(mock); 35 | expect(path.join(mock.projectPath, mock.filePath, mock.fileName + '.' + mock.fileType)).to.be.a.file().to.have.extname('.js') 36 | }); 37 | 38 | it('generate js file with outputFilePath', async () => { 39 | const mock = mocksFiles.jsFileWithOutputFilePath(); 40 | await newFile(mock); 41 | expect(path.join(mock.projectPath, mock.outputFilePath, mock.fileName + '.' + mock.fileType)).to.be.a.file().to.have.extname('.js') 42 | }); 43 | 44 | it('generate json file', async () => { 45 | const mock = mocksFiles.jsonFile(); 46 | await newFile(mock); 47 | expect(path.join(mock.projectPath, mock.filePath, mock.fileName + '.' + mock.fileType)).to.be.a.file().to.have.extname('.json') 48 | }); 49 | 50 | it('generate controller', async () => { 51 | const mock = mocksFiles.controller(); 52 | await generateController.new(mock.name, mock.options, mock.projectPath); 53 | expect(path.join(mock.projectPath, 'controllers/', mock.name + '.controller.js')).to.be.a.file().to.have.extname('.js') 54 | }); 55 | 56 | it('generate controller with specific methods', async () => { 57 | const mock = mocksFiles.controllerWithSpecificMethods(); 58 | await generateController.new(mock.name, mock.options, mock.projectPath); 59 | expect(path.join(mock.projectPath, 'controllers/', mock.name + '.controller.js')).to.be.a.file().to.have.extname('.js') 60 | }); 61 | 62 | 63 | it('generate service', async () => { 64 | const mock = mocksFiles.service(); 65 | await generateService.new(mock.name, mock.options, mock.projectPath); 66 | expect(path.join(mock.projectPath, 'services/', mock.name + '.service.js')).to.be.a.file().to.have.extname('.js') 67 | }); 68 | 69 | 70 | it('generate model', async () => { 71 | const mock = mocksFiles.model(); 72 | await generateModel.new(mock.name, mock.options, mock.projectPath); 73 | expect(path.join(mock.projectPath, 'models/', mock.name + '.model.js')).to.be.a.file().to.have.extname('.js') 74 | }); 75 | 76 | it('generate model with specific properties', async () => { 77 | const mock = mocksFiles.modelWithSpecificProperties(); 78 | await generateModel.new(mock.name, mock.options, mock.projectPath); 79 | expect(path.join(mock.projectPath, 'models/', mock.name + '.model.js')).to.be.a.file().to.have.extname('.js') 80 | }); 81 | 82 | it('generate route', async () => { 83 | const mock = mocksFiles.route(); 84 | await generateRoute.new(mock.name, mock.options, mock.projectPath); 85 | expect(path.join(mock.projectPath, 'routes/', mock.name + '.route.js')).to.be.a.file().to.have.extname('.js') 86 | }); 87 | 88 | it('generate route with specific options', async () => { 89 | const mock = mocksFiles.routelWithSpecificOptions(); 90 | await generateRoute.new(mock.name, mock.options, mock.projectPath); 91 | expect(path.join(mock.projectPath, 'routes/', mock.name + '.route.js')).to.be.a.file().to.have.extname('.js') 92 | }); 93 | 94 | it('generate policy default', async () => { 95 | const mock = mocksFiles.policyDefault(); 96 | await generatePolicy.new(mock.name, mock.options, mock.projectPath); 97 | expect(path.join(mock.projectPath, 'policies/', mock.name + '.policy.js')).to.be.a.file().to.have.extname('.js') 98 | }); 99 | 100 | it('generate policy admin', async () => { 101 | const mock = mocksFiles.policyAdmin(); 102 | await generatePolicy.new(mock.name, mock.options, mock.projectPath); 103 | expect(path.join(mock.projectPath, 'policies/', mock.name + '.policy.js')).to.be.a.file().to.have.extname('.js') 104 | }); 105 | }); 106 | 107 | -------------------------------------------------------------------------------- /lib/tests/mocks/files.mocks.js: -------------------------------------------------------------------------------- 1 | const getProjectPath = require('../../utils/getProjectPath'); 2 | 3 | module.exports = { 4 | jsFile: () => { 5 | return { 6 | projectPath: getProjectPath('test'), 7 | filePath: '/', 8 | fileName: 'app', 9 | fileType: 'js', 10 | hasModel: true, 11 | } 12 | }, 13 | jsonFile: () => { 14 | return { 15 | projectPath: getProjectPath('test'), 16 | filePath: '/', 17 | fileName: 'package', 18 | fileType: 'json', 19 | customData: { 20 | method: 'merge', 21 | content: { 22 | name: 'test', 23 | scripts: { 24 | start: 'node app.js' 25 | } 26 | } 27 | }, 28 | hasModel: true, 29 | } 30 | }, 31 | jsFileWithOutputFilePath: () => { 32 | return { 33 | projectPath: getProjectPath('test'), 34 | filePath: '/utils/', 35 | fileName: 'utils', 36 | fileType: 'js', 37 | hasModel: true, 38 | outputFilePath: '/services/utils/', 39 | } 40 | }, 41 | controller: () => { 42 | return { 43 | name: 'user', 44 | projectPath: getProjectPath('test'), 45 | options: { 46 | methods: 'crud' 47 | } 48 | } 49 | }, 50 | controllerWithSpecificMethods: () => { 51 | return { 52 | name: 'otherUser', 53 | projectPath: getProjectPath('test'), 54 | options: { 55 | methods: 'create,remove' 56 | } 57 | } 58 | }, 59 | model: () => { 60 | return { 61 | name: 'user', 62 | projectPath: getProjectPath('test'), 63 | options: {} 64 | } 65 | }, 66 | modelWithSpecificProperties: () => { 67 | return { 68 | name: 'otherUser', 69 | projectPath: getProjectPath('test'), 70 | options: { 71 | properties: 'firstname:string', 72 | methods: 'comparePassword', 73 | hooks: 'pre.save.hashPassword,pre.findOneAndUpdate.hashPassword,pre.findOneAndUpdate.addDate' 74 | } 75 | } 76 | }, 77 | route: () => { 78 | return { 79 | name: 'user', 80 | projectPath: getProjectPath('test'), 81 | options: { 82 | verb: 'GET,POST,PUT,DELETE', 83 | uri: '/user/{id}' 84 | } 85 | } 86 | }, 87 | routelWithSpecificOptions: () => { 88 | return { 89 | name: 'otherUser', 90 | projectPath: getProjectPath('test'), 91 | options: { 92 | verb: 'GET', 93 | uri: '/{p*}', 94 | custom: '404', 95 | controller: 'none' 96 | } 97 | } 98 | }, 99 | service: () => { 100 | return { 101 | name :'hashPassword', 102 | projectPath: getProjectPath('test'), 103 | options: { 104 | modules: 'bcrypt,config' 105 | } 106 | } 107 | }, 108 | policyDefault: () => { 109 | return { 110 | name: 'default', 111 | options: {}, 112 | projectPath: getProjectPath('test') 113 | } 114 | }, 115 | policyAdmin: () => { 116 | return { 117 | name: 'admin', 118 | options: {}, 119 | projectPath: getProjectPath('test') 120 | } 121 | }, 122 | config: () => { 123 | return { 124 | name: 'test', 125 | projectPath: getProjectPath('test'), 126 | type: 'default' 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /lib/tests/mocks/folders.mocks.js: -------------------------------------------------------------------------------- 1 | const getProjectPath = require('../../utils/getProjectPath'); 2 | 3 | module.exports = { 4 | projectPath: getProjectPath('test'), 5 | projectName: 'test', 6 | folders: ['', 'controllers', 'models', 'routes', 'policies', 'config', 'services', 'services/utils'] 7 | } -------------------------------------------------------------------------------- /lib/tests/utils.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process'); 2 | 3 | module.exports = { 4 | schemaConfig: { 5 | title: 'config file', 6 | type: 'object', 7 | required: ['server'], 8 | properties: { 9 | server: { 10 | auth: { 11 | saltFactor: 'number' 12 | }, 13 | }, 14 | connection: { 15 | port: 'number', 16 | routes: { 17 | cors: 'boolean' 18 | }, 19 | host: 'string' 20 | } 21 | } 22 | }, 23 | schemaConfigWithBDD: { 24 | title: 'config file', 25 | type: 'object', 26 | required: ['server', 'mongodb'], 27 | properties: { 28 | server: { 29 | auth: { 30 | saltFactor: 'number' 31 | }, 32 | }, 33 | connection: { 34 | port: 'number', 35 | routes: { 36 | cors: 'boolean' 37 | }, 38 | host: 'string' 39 | }, 40 | mongodb: { 41 | uri: "string", 42 | options: { 43 | user: "string", 44 | pass: "string" 45 | } 46 | } 47 | } 48 | }, 49 | newProject: () => { 50 | const child = spawn('node', ['index', 'new', 'test-cli'], {stdio: 'pipe'}); 51 | return new Promise((res) => { 52 | child.on('close', () => res()); 53 | child.stdout.on('data', (data) => { 54 | switch (true) { 55 | case /What is your default server host/.test(data.toString()): 56 | child.stdin.write('127.0.0.1\r'); 57 | break; 58 | case /What is your default secret key/.test(data.toString()): 59 | child.stdin.write('rergrgarekfij9809809cnjn0990809noNJinuinUINO\r'); 60 | break; 61 | case /What is your default database host/.test(data.toString()): 62 | child.stdin.write('102.222.222.222\r'); 63 | break; 64 | case /What is your default database name/.test(data.toString()): 65 | child.stdin.write('my-local-database\r'); 66 | break; 67 | case /What is your default database user/.test(data.toString()): 68 | child.stdin.write('\r'); 69 | break; 70 | case /What is your production secret key/.test(data.toString()): 71 | child.stdin.write('rergrgarekfij9809809cnjn0990809noNJinuinUINO\r'); 72 | break; 73 | case /Do you want a database for the production env/.test(data.toString()): 74 | child.stdin.write('n\r'); 75 | break; 76 | case /What is your production server host/.test(data.toString()): 77 | child.stdin.write('127.0.0.1\r'); 78 | break; 79 | } 80 | }) 81 | }) 82 | } 83 | }; -------------------------------------------------------------------------------- /lib/utils/addCustomDataHelper.js: -------------------------------------------------------------------------------- 1 | const hoek = require('hoek'); 2 | 3 | module.exports = { 4 | 5 | merge: (params) => { 6 | if (Buffer.isBuffer(params.fileContent)) { 7 | params.fileContent = JSON.parse(params.fileContent); 8 | } 9 | return hoek.merge(params.customData.content, params.fileContent); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /lib/utils/askQuestion.js: -------------------------------------------------------------------------------- 1 | const style = require('./logger').getTheme().question; 2 | const rl = require('readline').createInterface({ 3 | input: process.stdin, 4 | output: process.stdout, 5 | }); 6 | 7 | module.exports = question => new Promise((res) => { 8 | rl.question(style(question), (answer) => { 9 | res(answer); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /lib/utils/capitalize.js: -------------------------------------------------------------------------------- 1 | module.exports = string => string.charAt(0).toUpperCase() + string.slice(1); 2 | -------------------------------------------------------------------------------- /lib/utils/getProjectPath.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-confusing-arrow */ 2 | const path = require('path'); 3 | 4 | module.exports = name => (name) ? path.join(process.cwd(), name) : process.cwd(); 5 | -------------------------------------------------------------------------------- /lib/utils/getPropertiesFromModel.js: -------------------------------------------------------------------------------- 1 | const { promisify } = require('util'); 2 | const fs = require('fs'); 3 | const Path = require('path'); 4 | 5 | const fsReadFilePromise = promisify(fs.readFile); 6 | 7 | module.exports = async (projectPath, modelName) => { 8 | const path = Path.join(projectPath, 'models', `${modelName}.model.js`); 9 | const modelContentStingify = await fsReadFilePromise(path, 'utf8'); 10 | const result = {}; 11 | let property = ''; 12 | let key = ['']; 13 | let onKey = true; 14 | let level = 0; 15 | 16 | function rebuildJsonSchema(string) { 17 | const decomposeString = string.replace(/( |\n|\r)+/gm, '').split(''); 18 | decomposeString.pop(); 19 | decomposeString.shift(); 20 | 21 | console.log('>>>>>>>>>>', decomposeString[0], decomposeString[decomposeString.length - 1]); 22 | decomposeString.forEach((elm) => { 23 | if (elm === ':') { 24 | onKey = false; 25 | return; 26 | } 27 | if (elm === '{') { 28 | result[key[level]] = {}; 29 | key.push(''); 30 | level += 1; 31 | onKey = true; 32 | return; 33 | } 34 | 35 | if (elm === ',') { 36 | key[level] = ''; 37 | result[key[level]] = property; 38 | property = ''; 39 | onKey = true; 40 | return; 41 | } 42 | 43 | if (elm === '}') { 44 | level -= 1; 45 | onKey = true; 46 | return; 47 | } 48 | 49 | if (onKey) { 50 | key[level] += elm; 51 | } else { 52 | property += elm; 53 | } 54 | }); 55 | } 56 | 57 | 58 | rebuildJsonSchema(modelContentStingify.match(/{[^]+/gm)[0]); 59 | 60 | console.log('>>>>>>>>>>', result); 61 | // const modelContent = eval('(' + modelContentStingify.match(/{[^]+/gm)[0]+')') 62 | // 63 | // const result = []; 64 | // for(let key in modelContent.schema){ 65 | // if(key !== 'updatedAt' && key !== 'createdAt'){ 66 | // if(typeof modelContent.schema[key] === 'object'){ 67 | // result.push({[key]: 'Joi.optionnal()'modelContent.schema[key].type.name}); 68 | // continue 69 | // } 70 | // result.push({[key]: modelContent.schema[key].name}) 71 | // } 72 | // } 73 | // return result; 74 | }; 75 | -------------------------------------------------------------------------------- /lib/utils/logger.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const chalk = require('chalk'); 3 | 4 | const theme = { 5 | error: chalk.bold.red, 6 | info: chalk.bold.green, 7 | question: chalk.bold.blue, 8 | warn: chalk.bold.yellow, 9 | }; 10 | 11 | 12 | module.exports = { 13 | warn: message => console.log(theme.warn(message)), 14 | question: message => console.log(theme.question(message)), 15 | error: message => console.log(theme.error(`\u274C ${message}`)), 16 | info: (message) => { 17 | if (process.env.NODE_ENV !== 'test')console.log(theme.info(`\u2713 ${message}`)); 18 | }, 19 | getTheme: () => theme, 20 | }; 21 | -------------------------------------------------------------------------------- /lib/utils/newFile.js: -------------------------------------------------------------------------------- 1 | const Path = require('path'); 2 | const fs = require('fs'); 3 | const beautify = require('js-beautify').js_beautify; 4 | const { promisify } = require('util'); 5 | const addCustomDataHelper = require('./addCustomDataHelper'); 6 | const stringifyByFileType = require('./stringifyByFileType'); 7 | const logger = require('./logger'); 8 | const capitalize = require('./capitalize'); 9 | const endOfLine = require('os').EOL; 10 | 11 | const fsWriteFilePromise = promisify(fs.writeFile); 12 | const fsReadFilePromise = promisify(fs.readFile); 13 | const fsAccessFilePromise = promisify(fs.access); 14 | 15 | 16 | module.exports = async (files) => { 17 | async function isExist(params) { 18 | const path = Path.join(params.projectPath, params.outputFilePath, `${params.outPutFileName}.${params.outputFileType}`); 19 | 20 | try { 21 | await fsAccessFilePromise(path, fs.constants.F_OK); 22 | throw new Error(); 23 | } catch (e) { 24 | if (e.code !== 'ENOENT') { 25 | throw new Error(`file ${path} already exist`); 26 | } 27 | } 28 | } 29 | 30 | async function readFile(params) { 31 | if (!params.hasModel) return; 32 | 33 | const fileReferencePath = Path.join(__dirname, '../../assets', `${params.filePath}${params.fileName}.${params.fileType}`); 34 | 35 | try { 36 | params.fileContent = await fsReadFilePromise(fileReferencePath); 37 | } catch (err) { 38 | throw new Error(`error to generate ${params.outPutFileName}.${params.outputFileType} file : ${err}`); 39 | } 40 | } 41 | 42 | function addCustomData(params) { 43 | if (params.customData) { 44 | params.fileContent = addCustomDataHelper[params.customData.method](params); 45 | } 46 | } 47 | 48 | function isNodeModule(params) { 49 | let modules = ''; 50 | 51 | if (params.nodeModule) { 52 | if (params.modules) { 53 | for (const module of params.modules) { 54 | if (typeof module === 'object') { 55 | const key = Object.keys(module)[0]; 56 | modules += ` const ${capitalize(key)} = ${module[key]} ${endOfLine}`; 57 | } else { 58 | modules += ` const ${capitalize(module)} = require('${module}'); ${endOfLine}`; 59 | } 60 | } 61 | } 62 | 63 | params.fileContent = `${modules} ${endOfLine} module.exports = ${params.fileContent.replace(/"|\\n|\\r/gm, '')}`; 64 | } 65 | } 66 | 67 | function replaceEntity(params) { 68 | if (params.entity) { 69 | params.fileContent = params.fileContent.replace(new RegExp('{{entity}}', 'gm'), params.entity.toLowerCase()); 70 | params.fileContent = params.fileContent.replace(new RegExp('{{entity.upperFirstChar}}', 'gm'), capitalize(params.entity)); 71 | } 72 | } 73 | 74 | function addUseStrict(params) { 75 | if (params.fileType === 'js') { 76 | params.fileContent = `'use strict'; ${params.fileContent}`; 77 | } 78 | } 79 | 80 | async function writeFile(params) { 81 | try { 82 | params.fileContent = (params.outputFileType !== 'json') ? beautify(params.fileContent, { indent_size: 4 }) : params.fileContent; 83 | await fsWriteFilePromise(`${params.projectPath}/${params.outputFilePath}${params.outPutFileName}.${params.outputFileType}`, params.fileContent, 'utf8'); 84 | logger.info(`${params.outPutFileName}.${params.outputFileType} file created`); 85 | } catch (err) { 86 | throw new Error(err); 87 | } 88 | } 89 | 90 | async function generateFile(params) { 91 | params.outputFilePath = (params.outputFilePath) ? params.outputFilePath : params.filePath; 92 | params.outputFileType = (params.outputFileType) ? params.outputFileType : params.fileType; 93 | params.outPutFileName = (params.outputFileName) ? params.outputFileName : params.fileName; 94 | 95 | await isExist(params); 96 | await readFile(params); 97 | await addCustomData(params); 98 | await stringifyByFileType[params.outputFileType](params); 99 | await isNodeModule(params); 100 | await replaceEntity(params); 101 | await addUseStrict(params); 102 | await writeFile(params); 103 | } 104 | 105 | if (!Array.isArray(files)) { 106 | await generateFile(files); 107 | } else { 108 | for (const file of files) { 109 | await generateFile(file); 110 | } 111 | } 112 | }; 113 | -------------------------------------------------------------------------------- /lib/utils/stringifyByFileType.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | js: (params) => { 4 | if (Buffer.isBuffer(params.fileContent)) { 5 | params.fileContent = params.fileContent.toString().replace(/"/g, ''); 6 | } else { 7 | params.fileContent = JSON.stringify(params.fileContent, null, 4); 8 | } 9 | }, 10 | 11 | json: (params) => { 12 | if (Buffer.isBuffer(params.fileContent)) { 13 | params.fileContent = JSON.parse(params.fileContent); 14 | } 15 | 16 | params.fileContent = JSON.stringify(params.fileContent, null, 4); 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-starter", 3 | "version": "0.2.4", 4 | "description": "boilerplate API - MVC for hapijs V17 with mongodb, mongoose.", 5 | "main": "./index.js", 6 | "preferGlobal": true, 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/AMoreaux/hapi-cli" 10 | }, 11 | "scripts": { 12 | "test": "mocha lib/tests/**/*.spec.js", 13 | "coverage": "nyc npm test && nyc report --reporter=text-lcov | coveralls", 14 | "eslint": "eslint lib/**" 15 | }, 16 | "bin": { 17 | "hapi-cli": "./index.js", 18 | "hc": "./index.js" 19 | }, 20 | "author": "AMoreaux", 21 | "engines": { 22 | "node": ">=8.1.3" 23 | }, 24 | "license": "MIT", 25 | "dependencies": { 26 | "chalk": "^2.3.2", 27 | "commander": "^2.15.0", 28 | "hoek": "^5.0.3", 29 | "js-beautify": "^1.7.5" 30 | }, 31 | "devDependencies": { 32 | "chai": "^4.1.2", 33 | "chai-fs": "^2.0.0", 34 | "chai-json-schema": "^1.5.0", 35 | "coveralls": "^3.0.0", 36 | "eslint": "^4.8.0", 37 | "eslint-config-airbnb-base": "^12.1.0", 38 | "eslint-plugin-import": "^2.8.0", 39 | "eslint-plugin-node": "^6.0.1", 40 | "eslint-plugin-promise": "^3.6.0", 41 | "eslint-plugin-standard": "^3.0.1", 42 | "mocha": "^5.0.4", 43 | "mocha-lcov-reporter": "^1.3.0", 44 | "nyc": "^11.3.0" 45 | } 46 | } 47 | --------------------------------------------------------------------------------