├── .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 | [](https://circleci.com/gh/AMoreaux/hapi-cli)
2 | [](https://www.npmjs.com/package/hapi-starter)
3 | [](https://coveralls.io/github/AMoreaux/hapi-cli?branch=master)
4 | [](https://snyk.io/test/github/amoreaux/hapi-cli)
5 |
6 |
7 | [](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 |
--------------------------------------------------------------------------------