├── .gitignore ├── LICENSE ├── README.md ├── actions ├── generate-action.js ├── generate-files.js ├── generate-model.js ├── new-project.js └── show-help.js ├── index.js ├── lib ├── colors.js └── nov.js ├── november-logo.png ├── package.json └── template-files ├── action.js ├── blueprint-project ├── .editorconfig ├── .env ├── .gitignore ├── .sequelizerc ├── README.md ├── app │ ├── actions │ │ └── .index.js │ ├── controllers │ │ └── .index.js │ ├── middleware │ │ ├── access-controls.js │ │ ├── index.js │ │ └── sequelize.js │ ├── models │ │ └── index.js │ └── router.js ├── config │ └── config.json ├── lib │ ├── give-error.js │ ├── give-json.js │ ├── handy.js │ └── render.js ├── package.json ├── public │ ├── images │ │ └── november │ │ │ ├── built-with-november.png │ │ │ └── built-with-november@2x.png │ └── index.html └── server.js ├── controller-files ├── .index.js ├── add.js ├── list.js ├── load.js ├── remove.js └── update.js ├── model.js ├── router-action.js └── router-model.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tristan Edwards 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![November logo](https://raw.githubusercontent.com/t4t5/november-cli/master/november-logo.png) 2 | 3 | November helps you generate a simple Node.js API tailored for [Ember.js](http://emberjs.com) apps, with the help of [Express](http://expressjs.com) and [Sequelize](http://docs.sequelizejs.com/en/latest). 4 | 5 | 6 | Installation 7 | ------------ 8 | ```bash 9 | $ npm install -g november-cli 10 | ``` 11 | 12 | 13 | Get started 14 | ----------- 15 | ```bash 16 | $ november new my-app 17 | ``` 18 | This will create a new project with the following structure: 19 | ``` 20 | ├── app 21 | │ ├── actions 22 | │ ├── controllers 23 | │ ├── middleware 24 | │ ├── models 25 | │ ├── router.js 26 | │ 27 | ├── config 28 | │ ├── config.json 29 | │ 30 | ├── lib 31 | ├── migrations 32 | ├── node_modules 33 | ├── public 34 | ├── server.js 35 | ├── test 36 | ``` 37 | 38 | By default, MySQL is used as a database, but you can use any relational database [supported by Sequelize](http://docs.sequelizejs.com/en/latest) by changing the values in `config/config.json`. 39 | 40 | To run the app, run `npm start` (or just `nodemon` if you have it installed) in your app’s directory and visit `localhost:9000`. 41 | 42 | 43 | In your Ember.js app 44 | -------------------- 45 | 46 | Make sure you change the host in your Ember.js adapter file so that it can communicate with November: 47 | ```bash 48 | # In your ember project folder 49 | $ ember generate adapter 50 | ``` 51 | ```javascript 52 | // app/adapters/application.js 53 | import DS from "ember-data"; 54 | 55 | export default DS.RESTAdapter.reopen({ 56 | host: 'http://localhost:9000' 57 | }); 58 | ``` 59 | 60 | 61 | Models 62 | ------ 63 | ```bash 64 | $ november generate model user 65 | ``` 66 | 67 | This generates: 68 | - A **model file** (`app/models/user.js`) for the user, which will determine the structure of the database table 69 | - **Routes** in `app/router.js` for creating, reading, updating and deleting users (based on the conventions of Ember Data). Feel free to remove the actions you don't need. 70 | - **Controller files**, which hook up the routes to database actions: 71 | - `app/controllers/user/add.js` 72 | - `app/controllers/user/list.js` 73 | - `app/controllers/user/load.js` 74 | - `app/controllers/user/update.js` 75 | - `app/controllers/user/remove.js` 76 | 77 | With the app and your local database running in the background, visit `localhost:9000/users`, and you should see: 78 | ```json 79 | { 80 | "users": [] 81 | } 82 | ``` 83 | The table `users` has automatically been created in your database. 84 | 85 | 86 | Actions 87 | ------- 88 | Actions are for API endpoints which are not specifically tied to any model. 89 | ```bash 90 | $ november generate action login 91 | ``` 92 | 93 | This generates: 94 | - An **action file** (`app/actions/login.js`) 95 | - A **route** in `app/router.js` (`POST` by default) 96 | 97 | 98 | Render() 99 | ------- 100 | The `render()`-method in your controllers is used for rendering both your *models* and your *error messages*. It takes a single argument. 101 | 102 | 103 | Rendering models 104 | ------------- 105 | If you pass a valid sequelize model to `render()`, it will generate that model according to the [JSON API](http://jsonapi.org) conventions used by Ember Data. 106 | 107 | The most basic usage: 108 | ``` 109 | render(); 110 | ``` 111 | ...which can also be typed like this: 112 | ``` 113 | render({ 114 | model: 115 | }); 116 | ``` 117 | returns: 118 | ```javascript 119 | { 120 | "user": { 121 | <...> 122 | } 123 | } 124 | ``` 125 | 126 | If your sequelize model includes [associated models](http://docs.sequelizejs.com/en/latest/api/associations), they are sideloaded by default: 127 | ```javascript 128 | { 129 | "user": { 130 | <...> 131 | "tweets": [1, 5] 132 | }, 133 | "tweets": [ 134 | <...> 135 | ] 136 | } 137 | ``` 138 | 139 | However, you can also specify if you want some (or all) associations to be embedded instead. 140 | 141 | Here we specify that we want the tweets-association to be embedded. If we wanted *all* associations to be embedded, we would set `embedded: true` 142 | ```javascript 143 | render({ 144 | model: , 145 | embedded: ['tweets'] 146 | }); 147 | ``` 148 | ... which returns: 149 | ```javascript 150 | { 151 | "user": { 152 | <...> 153 | "tweets": [ 154 | { 155 | id: 1, 156 | <...> 157 | }, 158 | { 159 | id: 5, 160 | <...> 161 | } 162 | ] 163 | } 164 | } 165 | ``` 166 | 167 | Rendering errors 168 | --------------- 169 | 170 | Controllers generated by November rely heavily on promises. If they catch an error, they call `render(error)`. 171 | 172 | Let's say we accidentally search for a field (`name`) which doesn't exist in our database table: 173 | 174 | ```javascript 175 | // app/controllers/user/list.js 176 | req.models.user 177 | .findAll({ 178 | where: { 179 | name: "Bob" 180 | } 181 | }) 182 | .then(function(users) { 183 | // Not gonna happen 184 | }) 185 | .catch(function(err) { 186 | render(err); 187 | }); 188 | ``` 189 | 190 | An error will be catched and `render(err)` will return this JSON to the client: 191 | ```json 192 | { 193 | "error": { 194 | "code": 500, 195 | "message": "Could not load users" 196 | } 197 | } 198 | ``` 199 | ... while still showing a more descriptive error to the developer in the console so that you can locate the problem: 200 | 201 | ![A console error](http://tristanedwards.me/u/november/console-error.png) 202 | 203 | You can also render your own exceptions to the user, by throwing a **string** with the error message or an **array** where the first element is the error code and the second is the error message: 204 | 205 | ```javascript 206 | // app/controllers/user/update.js 207 | req.models.user.find({ 208 | where: { 209 | username: req.params.username 210 | } 211 | }) 212 | .then(function(user) { 213 | if (user.id !== req.user) { 214 | throw [403, "You are not allowed to edit this user!"] 215 | } 216 | return user.save(); 217 | }) 218 | .then(function(user) { 219 | // Not gonna happen 220 | }) 221 | .catch(function(err) { 222 | render(err); 223 | }); 224 | ``` 225 | 226 | ...what the client will see: 227 | ```json 228 | { 229 | "error": { 230 | "code": 403, 231 | "message": "You are not allowed to edit this user!" 232 | } 233 | } 234 | ``` 235 | 236 | Todos 237 | ----- 238 | TDD is not really my thing, but it would be nice to get some automatic Mocha tests when you generate new models. :) 239 | 240 | Contact 241 | ------- 242 | 243 | If you have any questions, feel free to [ping me on Twitter](https://twitter.com/t4t5) or just open an issue! 244 | 245 | -------------------------------------------------------------------------------- /actions/generate-action.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'); 2 | var inflect = require('inflect'); 3 | var nov = require('../lib/nov'); 4 | var colors = require('../lib/colors'); 5 | var fs = require('fs'); 6 | 7 | Promise.promisifyAll(fs); 8 | 9 | /* 10 | * Update router.js and add the action file 11 | */ 12 | module.exports = function(actionName) { 13 | var finalResolver = Promise.pending(); 14 | 15 | if (!actionName) { 16 | return nov.logErr("You need to specify a name for your action!"); 17 | } 18 | 19 | // Check if the action already exists 20 | try { 21 | stats = fs.lstatSync(nov.novemberDir() + 'app/actions/' + actionName + '.js'); 22 | finalResolver.reject("There's already an action with the name " + actionName + "!"); 23 | } 24 | catch (e) { 25 | var actionFileName = inflect.parameterize(actionName); 26 | var routerFileEnding = '\n\n};' 27 | var routerContents; 28 | 29 | // Get current contents of router.js file and remove last part 30 | fs.readFileAsync(nov.novemberDir() + 'app/router.js', 'utf8') 31 | .then(function(fileContents) { 32 | routerContents = fileContents.substr(0, fileContents.lastIndexOf('}')); 33 | return fs.readFileAsync(nov.templateDir('router-action.js'), 'utf8'); 34 | }) 35 | // Inject the code for a new route and save the new router.js file 36 | .then(function(actionCode) { 37 | actionCode = nov.fillTemplatePlaceholders(actionCode, actionName); 38 | routerContents = routerContents + actionCode + routerFileEnding; 39 | return fs.writeFileAsync(nov.novemberDir() + 'app/router.js', routerContents, 'utf8'); 40 | }) 41 | // Add action file 42 | .then(function() { 43 | var templateFile = 'template-files/action.js'; 44 | var targetPath = 'app/actions/' + inflect.dasherize(actionName) + '.js'; 45 | 46 | return nov.generateFile(templateFile, targetPath, actionName); 47 | }) 48 | .then(function() { 49 | finalResolver.resolve(); 50 | }) 51 | .catch(function(err) { 52 | finalResolver.reject(err); 53 | }); 54 | } 55 | 56 | return finalResolver.promise; 57 | 58 | }; 59 | -------------------------------------------------------------------------------- /actions/generate-files.js: -------------------------------------------------------------------------------- 1 | var nov = require('../lib/nov'); 2 | var colors = require('../lib/colors'); 3 | var generateModel = require('./generate-model'); 4 | var generateAction = require('./generate-action'); 5 | 6 | module.exports = function(userArgs) { 7 | var fileType = userArgs[1]; 8 | 9 | if (!nov.novemberDir()) { 10 | return nov.logErr("You have to be inside a November project in order to use this command."); 11 | } 12 | 13 | if (!fileType) { 14 | return nov.logErr("You need to specify what you want to generate"); 15 | } 16 | 17 | if (!userArgs[2]) { 18 | return nov.logErr("You need to specify the name of what you want to generate!"); 19 | } 20 | 21 | 22 | switch (fileType) { 23 | 24 | case "model": 25 | var modelName = userArgs[2]; 26 | 27 | generateModel(modelName).then(function() { 28 | nov.logSuccess(modelName + " model created"); 29 | }).catch(nov.logErr); 30 | 31 | break; 32 | 33 | case "action": 34 | var actionName = userArgs[2]; 35 | 36 | generateAction(actionName).then(function() { 37 | nov.logSuccess(actionName + " action created"); 38 | }).catch(nov.logErr); 39 | 40 | break; 41 | 42 | default: 43 | nov.logErr('Unknown type. Did you mean `november generate model ' + userArgs[2] + '`?'); 44 | 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /actions/generate-model.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'); 2 | var async = require('async'); 3 | var inflect = require('inflect'); 4 | var nov = require('../lib/nov'); 5 | var colors = require('../lib/colors'); 6 | var fs = require('fs'); 7 | var mkdirp = require('mkdirp'); 8 | 9 | Promise.promisifyAll(fs); 10 | Promise.promisifyAll(mkdirp); 11 | 12 | /* 13 | * Update router.js, add the CRUD-files and finally the model file 14 | */ 15 | module.exports = function(modelName) { 16 | 17 | var finalResolver = Promise.pending(); 18 | 19 | if (!modelName) { 20 | return nov.logErr("You need to specify a name for your model"); 21 | } 22 | 23 | // Check if the model already exists 24 | try { 25 | stats = fs.lstatSync(nov.novemberDir() + 'app/models/' + modelName + '.js'); 26 | finalResolver.reject("There's already a model with the name " + modelName + "!"); 27 | } 28 | catch (e) { 29 | var routerContents; 30 | var routerFileEnding = "\n\n};"; 31 | var modelFolderName = inflect.parameterize(inflect.singularize(modelName)); 32 | 33 | // Get current contents of router.js file and remove last part 34 | fs.readFileAsync(nov.novemberDir() + 'app/router.js', 'utf8') 35 | .then(function(fileContents) { 36 | routerContents = fileContents.substr(0, fileContents.lastIndexOf('}')); 37 | return fs.readFileAsync(nov.templateDir('router-model.js'), 'utf8'); 38 | }) 39 | // Inject the code for a new route and save the new router.js file 40 | .then(function(routeCode) { 41 | routeCode = nov.fillTemplatePlaceholders(routeCode, modelName); 42 | routerContents = routerContents + routeCode + routerFileEnding; 43 | return fs.writeFileAsync(nov.novemberDir() + 'app/router.js', routerContents); 44 | }) 45 | // Create the model's folder inside "controllers" 46 | .then(function() { 47 | return mkdirp.mkdirpAsync(nov.novemberDir() + 'app/controllers/' + modelFolderName) 48 | }) 49 | // Add all the CRUD-actions for the model 50 | .then(function() { 51 | var resolver = Promise.pending(); 52 | 53 | var files = ['.index', 'load', 'list', 'add', 'update', 'remove']; 54 | 55 | async.each(files, function(file, callback) { 56 | var templateMethod = 'template-files/controller-files/' + file + '.js'; 57 | var targetMethod = 'app/controllers/' + modelFolderName + '/' + file + '.js'; 58 | 59 | nov.generateFile(templateMethod, targetMethod, modelName) 60 | .then(function() { 61 | callback(); 62 | }) 63 | .catch(function(err) { 64 | return callback(err); 65 | }); 66 | 67 | }, function(err) { 68 | if (err) return resolver.reject(err); 69 | resolver.resolve(); 70 | }); 71 | 72 | return resolver.promise; 73 | }) 74 | // Generate the file inside "models" 75 | .then(function() { 76 | var templateFile = 'template-files/model.js'; 77 | var targetPath = 'app/models/' + modelFolderName + '.js'; 78 | 79 | return nov.generateFile(templateFile, targetPath, modelName); 80 | }) 81 | .then(function() { 82 | finalResolver.resolve(); 83 | }) 84 | .catch(function(err) { 85 | finalResolver.reject(err); 86 | }); 87 | } 88 | 89 | return finalResolver.promise; 90 | 91 | }; 92 | -------------------------------------------------------------------------------- /actions/new-project.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'); 2 | var path = require('path'); 3 | var exec = require('promised-exec'); 4 | var nov = require('../lib/nov'); 5 | var colors = require('../lib/colors'); 6 | var ncp = require('ncp'); 7 | var fs = require('fs'); 8 | var Spinner = require('cli-spinner').Spinner; 9 | 10 | Promise.promisifyAll(fs); 11 | Promise.promisifyAll(ncp); 12 | 13 | /* 14 | * Copy the blueprint folder, and run "npm install" inside it 15 | */ 16 | 17 | module.exports = function(userArgs) { 18 | var projectName = userArgs[1]; 19 | 20 | if (!projectName) { 21 | return nov.logErr("You need to specify a name for your project"); 22 | } 23 | 24 | if (nov.novemberDir()) { 25 | userArgs.shift(); 26 | return nov.logErr("You're trying to create a new November project inside an existing one! Did you mean to use `november generate " + userArgs.join(' ') + "?"); 27 | } 28 | 29 | // Check if the folder already exists 30 | try { 31 | stats = fs.lstatSync(projectName); 32 | return nov.logErr("There's already a project with the name " + projectName + " in this directory!"); 33 | } 34 | catch (e) { 35 | var spinner = new Spinner('%s Building November project...'); 36 | 37 | // Copy the contents of the blueprint folder to the user's app folder 38 | ncp.ncpAsync(path.resolve(__dirname, '../template-files/blueprint-project'), projectName) 39 | .then(function() { 40 | return fs.readFileAsync(projectName + '/package.json', 'utf8'); 41 | }) 42 | // Set the name of the app in package.json 43 | .then(function(packageJsonContents) { 44 | packageJsonContents = nov.fillTemplatePlaceholders(packageJsonContents, projectName); 45 | return fs.writeFileAsync(projectName + '/package.json', packageJsonContents, 'utf8'); 46 | }) 47 | .then(function() { 48 | return fs.readFileAsync(projectName + '/public/index.html', 'utf8'); 49 | }) 50 | // Set the name of the app in index.html 51 | .then(function(htmlContents) { 52 | htmlContents = nov.fillTemplatePlaceholders(htmlContents, projectName); 53 | return fs.writeFileAsync(projectName + '/public/index.html', htmlContents, 'utf8'); 54 | }) 55 | .then(function() { 56 | return fs.readFileAsync(projectName + '/README.md', 'utf8'); 57 | }) 58 | // Set the name of the app in README.md 59 | .then(function(readmeContents) { 60 | readmeContents = nov.fillTemplatePlaceholders(readmeContents, projectName); 61 | return fs.writeFileAsync(projectName + '/README.md', readmeContents, 'utf8'); 62 | }) 63 | // Install NPM dependencies 64 | .then(function() { 65 | spinner.start(); 66 | process.chdir(projectName); // Go into the created app's directory 67 | return exec('npm install') 68 | }) 69 | .then(function() { 70 | spinner.stop(true); 71 | nov.logSuccess("Created " + projectName + " project! Now run `cd " + projectName + "` to get started!"); 72 | }) 73 | .catch(function(err) { 74 | console.log(err); 75 | nov.logErr(err); 76 | }); 77 | } 78 | 79 | }; 80 | -------------------------------------------------------------------------------- /actions/show-help.js: -------------------------------------------------------------------------------- 1 | // var Promise = require('bluebird'); 2 | // var path = require('path'); 3 | // var exec = require('promised-exec'); 4 | // var nov = require('../lib/nov'); 5 | // var colors = require('../lib/colors'); 6 | // var ncp = require('ncp'); 7 | // var fs = require('fs'); 8 | // var Spinner = require('cli-spinner').Spinner; 9 | 10 | // Promise.promisifyAll(fs); 11 | // Promise.promisifyAll(ncp); 12 | 13 | /* 14 | * Copy the blueprint folder, and run "npm install" inside it 15 | */ 16 | 17 | module.exports = function(userArgs) { 18 | 19 | console.log("\nAvailable commands in November CLI:\n".help); 20 | 21 | console.log("november new " + "".yellow); 22 | console.log(" Creates a new November project from scratch.\n".grey); 23 | 24 | console.log("november generate model " + "".yellow); 25 | console.log(" Creates routes, controllers and a model.js file for your new model.\n".grey); 26 | 27 | console.log("november generate action " + "".yellow); 28 | console.log(" Creates a route and a controller for your new action.\n".grey); 29 | 30 | console.log("november help"); 31 | console.log(" Bring up this info box. :)\n".grey); 32 | 33 | console.log(''); 34 | 35 | // var projectName = userArgs[1]; 36 | 37 | // if (!projectName) { 38 | // return nov.logErr("You need to specify a name for your project"); 39 | // } 40 | 41 | // if (nov.novemberDir()) { 42 | // userArgs.shift(); 43 | // return nov.logErr("You're trying to create a new November project inside an existing one! Did you mean to use `november generate " + userArgs.join(' ') + "?"); 44 | // } 45 | 46 | // // Check if the folder already exists 47 | // try { 48 | // stats = fs.lstatSync(projectName); 49 | // return nov.logErr("There's already a project with the name " + projectName + " in this directory!"); 50 | // } 51 | // catch (e) { 52 | // var spinner = new Spinner('%s Building November project...'); 53 | 54 | // // Copy the contents of the blueprint folder to the user's app folder 55 | // ncp.ncpAsync(path.resolve(__dirname, '../template-files/blueprint-project'), projectName) 56 | // .then(function() { 57 | // return fs.readFileAsync(projectName + '/package.json', 'utf8'); 58 | // }) 59 | // // Set the name of the app in package.json 60 | // .then(function(packageJsonContents) { 61 | // packageJsonContents = nov.fillTemplatePlaceholders(packageJsonContents, projectName); 62 | // return fs.writeFileAsync(projectName + '/package.json', packageJsonContents, 'utf8'); 63 | // }) 64 | // .then(function() { 65 | // return fs.readFileAsync(projectName + '/public/index.html', 'utf8'); 66 | // }) 67 | // // Set the name of the app in index.html 68 | // .then(function(htmlContents) { 69 | // htmlContents = nov.fillTemplatePlaceholders(htmlContents, projectName); 70 | // return fs.writeFileAsync(projectName + '/public/index.html', htmlContents, 'utf8'); 71 | // }) 72 | // .then(function() { 73 | // return fs.readFileAsync(projectName + '/README.md', 'utf8'); 74 | // }) 75 | // // Set the name of the app in README.md 76 | // .then(function(readmeContents) { 77 | // readmeContents = nov.fillTemplatePlaceholders(readmeContents, projectName); 78 | // return fs.writeFileAsync(projectName + '/README.md', readmeContents, 'utf8'); 79 | // }) 80 | // // Install NPM dependencies 81 | // .then(function() { 82 | // spinner.start(); 83 | // process.chdir(projectName); // Go into the created app's directory 84 | // return exec('npm install') 85 | // }) 86 | // .then(function() { 87 | // spinner.stop(true); 88 | // nov.logSuccess("Created " + projectName + " project! Now run `cd " + projectName + "` to get started!"); 89 | // }) 90 | // .catch(function(err) { 91 | // console.log(err); 92 | // nov.logErr(err); 93 | // }); 94 | // } 95 | 96 | }; 97 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | // November commands 6 | var newProject = require('./actions/new-project'); 7 | var generateFiles = require('./actions/generate-files'); 8 | var showHelp = require('./actions/show-help'); 9 | 10 | 11 | // Everything starts here... 12 | readFromCommand(process.argv.splice(2)); 13 | 14 | 15 | function readFromCommand(userArgs) { 16 | var action = userArgs[0]; 17 | 18 | switch (action) { 19 | 20 | case "new": 21 | case "n": 22 | newProject(userArgs); 23 | break; 24 | 25 | case "generate": 26 | case "g": 27 | generateFiles(userArgs); 28 | break; 29 | 30 | case "help": 31 | case "h": 32 | showHelp(); 33 | break; 34 | 35 | default: 36 | console.log("Invalid command".error); 37 | showHelp(); 38 | //console.log("Here are some things you can do:".help); 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /lib/colors.js: -------------------------------------------------------------------------------- 1 | var colors = require('colors'); 2 | 3 | colors.setTheme({ 4 | silly: 'rainbow', 5 | input: 'grey', 6 | verbose: 'cyan', 7 | prompt: 'grey', 8 | info: 'green', 9 | data: 'grey', 10 | help: 'cyan', 11 | warn: 'yellow', 12 | debug: 'blue', 13 | error: 'red' 14 | }); 15 | 16 | exports.colors = colors; -------------------------------------------------------------------------------- /lib/nov.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'); 2 | var path = require('path'); 3 | var inflect = require('inflect'); 4 | var colors = require('./colors'); 5 | var fs = require('fs'); 6 | var Finder = require('fs-finder'); 7 | 8 | Promise.promisifyAll(fs); 9 | 10 | /* 11 | * Some nice colorized logging 12 | */ 13 | module.exports.logErr = function(str) { 14 | if (typeof(str) !== "string") { 15 | str = str.toString(); 16 | } 17 | 18 | if (arguments[1] === "sys") { 19 | str = str.substring(str.lastIndexOf(':') + 2); 20 | str = "Error: " + str.replace(/(\r\n|\n|\r)/gm,""); 21 | } 22 | 23 | console.log(str.error); 24 | }; 25 | module.exports.logInfo = function(str) { 26 | console.log(str.data); 27 | }; 28 | module.exports.logSuccess = function(str) { 29 | console.log(str.info); 30 | }; 31 | 32 | 33 | /* 34 | * Check if the user is inside a November app directory (or a sub-directory) by > 35 | * > seeing if there's a package.json-file (this function could be better) 36 | */ 37 | var novemberDir = function() { 38 | var files = Finder.in('.').findFiles('package.json'); 39 | 40 | var novemberDir; 41 | if (files.length) { 42 | novemberDir = files[0]; 43 | novemberDir = novemberDir.replace(/package.json/, ''); 44 | } 45 | 46 | return novemberDir; 47 | }; 48 | exports.novemberDir = novemberDir; 49 | 50 | 51 | /* 52 | * Generate a new file based on a template file 53 | */ 54 | module.exports.generateFile = function(sourceFile, targetFile, modelName) { 55 | var resolver = Promise.pending(); 56 | 57 | fs.readFileAsync(path.resolve(__dirname, '../' + sourceFile), 'utf8') 58 | .then(function(data) { 59 | if (modelName) { 60 | data = fillTemplatePlaceholders(data, modelName); 61 | } 62 | return fs.writeFileAsync(novemberDir() + targetFile, data, 'utf8'); 63 | }) 64 | .then(function() { 65 | console.log(('Generated ' + targetFile).data); 66 | resolver.resolve(); 67 | }) 68 | .catch(function(err) { 69 | resolver.reject(err); 70 | }); 71 | 72 | return resolver.promise; 73 | }; 74 | 75 | 76 | /* 77 | * Replace {{x}} in the template files with the model/action name 78 | */ 79 | var fillTemplatePlaceholders = function(str, modelName) { 80 | str = str.replace(/{{x}}/g, modelName); 81 | str = str.replace(/{{x-singular}}/g, inflect.decapitalize(inflect.camelize(inflect.singularize(modelName)))); 82 | str = str.replace(/{{x-plural}}/g, inflect.decapitalize(inflect.camelize(inflect.pluralize(modelName)))); 83 | 84 | str = str.replace(/{{x-camelcase}}/g, inflect.decapitalize(inflect.camelize(modelName.replace(/-/g, '_')))); 85 | str = str.replace(/{{x-dashed}}/g, inflect.dasherize(modelName)); 86 | str = str.replace(/{{x-underscore}}/g, inflect.underscore(modelName)); 87 | str = str.replace(/{{x-human}}/g, inflect.humanize(modelName).replace(/-/g, ' ')); 88 | 89 | str = str.replace(/{{x-plural-camelcase}}/g, inflect.decapitalize(inflect.camelize(inflect.pluralize(modelName.replace(/-/g, '_'))))); 90 | str = str.replace(/{{x-singular-camelcase}}/g, inflect.decapitalize(inflect.camelize(inflect.singularize(modelName.replace(/-/g, '_'))))); 91 | str = str.replace(/{{x-singular-capitalize}}/g, inflect.camelize(inflect.singularize(modelName.replace(/-/g, '_')))); 92 | str = str.replace(/{{x-singular-underscore}}/g, inflect.underscore(inflect.singularize(modelName))); 93 | str = str.replace(/{{x-table}}/g, inflect.underscore(inflect.pluralize(modelName))); 94 | 95 | return str; 96 | }; 97 | exports.fillTemplatePlaceholders = fillTemplatePlaceholders; 98 | 99 | 100 | /* 101 | * Quick access to the template-files directory 102 | */ 103 | module.exports.templateDir = function(dir) { 104 | return path.resolve(__dirname, '../template-files/' + dir); 105 | }; 106 | -------------------------------------------------------------------------------- /november-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t4t5/november-cli/f3badfad47ec0afde4229267ed3f383553fb2054/november-logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "november-cli", 3 | "version": "0.2.1", 4 | "description": "Generate a Node.js API for your Ember.js app ", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "preferGlobal": true, 10 | "bin": { 11 | "november": "index.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/t4t5/november-cli" 16 | }, 17 | "keywords": [ 18 | "ember", 19 | "emberjs", 20 | "node", 21 | "nodejs", 22 | "sequelize", 23 | "backend", 24 | "orm" 25 | ], 26 | "author": "Tristan Edwards (http://tristanedwards.me)", 27 | "license": "MIT", 28 | "dependencies": { 29 | "async": "^0.9.0", 30 | "bluebird": "^2.9.24", 31 | "cli-spinner": "^0.2.1", 32 | "colors": "^1.0.3", 33 | "fs-finder": "^1.8.0", 34 | "inflect": "^0.3.0", 35 | "mkdirp": "^0.5.0", 36 | "prettyjson": "^1.1.0", 37 | "promised-exec": "^1.0.1", 38 | "ncp": "^2.0.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /template-files/action.js: -------------------------------------------------------------------------------- 1 | module.exports = function(req, res, render) { 2 | 3 | /* 4 | * Do something here 5 | */ 6 | 7 | }; -------------------------------------------------------------------------------- /template-files/blueprint-project/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.js] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.hbs] 21 | insert_final_newline = false 22 | indent_style = space 23 | indent_size = 2 24 | 25 | [*.css] 26 | indent_style = space 27 | indent_size = 2 28 | 29 | [*.html] 30 | indent_style = space 31 | indent_size = 2 32 | 33 | [*.{diff,md}] 34 | trim_trailing_whitespace = false 35 | -------------------------------------------------------------------------------- /template-files/blueprint-project/.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL=mysql://root:root@localhost:3306/database_production 2 | NODE_ENV=development -------------------------------------------------------------------------------- /template-files/blueprint-project/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # misc 7 | .env 8 | .DS_Store 9 | npm-debug.log 10 | -------------------------------------------------------------------------------- /template-files/blueprint-project/.sequelizerc: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | 'models-path': path.resolve('app', 'models') 5 | } -------------------------------------------------------------------------------- /template-files/blueprint-project/README.md: -------------------------------------------------------------------------------- 1 | # {{x-human}} 2 | 3 | An API built with November. 4 | 5 | Prerequisites 6 | ------------- 7 | You will need: 8 | - [Node.js](https://nodejs.org) with [NPM](https://www.npmjs.com) 9 | - [November CLI](https://github.com/t4t5/november-cli). 10 | - A local database supported by [Sequelize](http://docs.sequelizejs.com/en/latest) 11 | 12 | 13 | Install and run 14 | --------------- 15 | - `git clone ` this repository 16 | - go to the app’s directory 17 | - `npm install` 18 | - `npm start` (or `nodemon` for live-reload) 19 | Visit the API at http://localhost:9000 20 | 21 | Generators 22 | ---------- 23 | - `november g model ` will generate new models with CRUD actions 24 | - `november g action ` will generate new actions -------------------------------------------------------------------------------- /template-files/blueprint-project/app/actions/.index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var inflect = require('inflect'); 6 | var basename = path.basename(module.filename); 7 | var controls = {}; 8 | 9 | fs 10 | .readdirSync(__dirname) 11 | .filter(function(file) { 12 | return (file.indexOf('.') !== 0) && (file !== basename); 13 | }) 14 | .forEach(function(file) { 15 | var baseName = path.basename(file, '.js'); 16 | var underscoreName = inflect.underscore(baseName); 17 | controls[underscoreName] = require('./' + file); 18 | }); 19 | 20 | module.exports = controls; -------------------------------------------------------------------------------- /template-files/blueprint-project/app/controllers/.index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var inflect = require('inflect'); 6 | var basename = path.basename(module.filename); 7 | var models = {}; 8 | 9 | fs 10 | .readdirSync(__dirname) 11 | .filter(function(file) { 12 | return (file.indexOf('.') !== 0) && (file !== basename); 13 | }) 14 | .forEach(function(file) { 15 | var underscore = inflect.singularize(inflect.underscore(file)); 16 | models[underscore] = require('./' + file + '/.index.js'); 17 | }); 18 | 19 | module.exports = models; -------------------------------------------------------------------------------- /template-files/blueprint-project/app/middleware/access-controls.js: -------------------------------------------------------------------------------- 1 | var env = process.env.NODE_ENV || 'development'; 2 | var config = require(__dirname + '/../../config/config.json')[env]; 3 | 4 | module.exports = function(req, res, next) { 5 | res.setHeader('Access-Control-Allow-Origin', config.allow_origin); 6 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); 7 | res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, content-type, Authorization'); 8 | next(); 9 | }; -------------------------------------------------------------------------------- /template-files/blueprint-project/app/middleware/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var bodyParser = require('body-parser'); 3 | 4 | var accessControls = require('./access-controls'); 5 | var loadDatabase = require('./sequelize'); 6 | 7 | 8 | module.exports = function(app) { 9 | 10 | // Allow JSON and URL-encoded bodies 11 | app.use(bodyParser.json({ limit: '10mb' })); 12 | app.use(bodyParser.urlencoded({ extended: false })); 13 | 14 | // Allow static assets 15 | app.use(express.static('public')); 16 | 17 | // Access control 18 | app.use(accessControls); 19 | 20 | // Load the models and their relations 21 | app.use(loadDatabase); 22 | 23 | }; 24 | -------------------------------------------------------------------------------- /template-files/blueprint-project/app/middleware/sequelize.js: -------------------------------------------------------------------------------- 1 | module.exports = function(req, res, next) { 2 | var db = require('../models'); 3 | 4 | db.sequelize.sync() 5 | .then(function(err) { 6 | req.models = db; 7 | next(); 8 | }, function (err) { 9 | console.log('An error occurred while creating the table:', err); 10 | next(err); 11 | }); 12 | }; -------------------------------------------------------------------------------- /template-files/blueprint-project/app/models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var Sequelize = require('sequelize'); 6 | var inflect = require('inflect'); 7 | var ssaclAttributeRoles = require('ssacl-attribute-roles'); 8 | var basename = path.basename(module.filename); 9 | var env = process.env.NODE_ENV || 'development'; 10 | var config = require(__dirname + '/../../config/config.json')[env]; 11 | var db = {}; 12 | var sequelize; 13 | 14 | // Create database connection 15 | if (env === 'production') { 16 | sequelize = new Sequelize(process.env.DATABASE_URL); 17 | } else { 18 | sequelize = new Sequelize(config.database, config.username, config.password, config); 19 | } 20 | 21 | // Attribute whitelisting/blacklisting 22 | ssaclAttributeRoles(sequelize); 23 | 24 | // Import models 25 | fs 26 | .readdirSync(__dirname) 27 | .filter(function(file) { 28 | return (file.indexOf('.') !== 0) && (file !== basename); 29 | }) 30 | .forEach(function(file) { 31 | var model = sequelize['import'](path.join(__dirname, file)); 32 | var underscoreModel = inflect.singularize(inflect.underscore(model.name)); 33 | db[underscoreModel] = model; 34 | }); 35 | 36 | Object.keys(db).forEach(function(modelName) { 37 | if ('associate' in db[modelName]) { 38 | db[modelName].associate(db); 39 | } 40 | }); 41 | 42 | db.sequelize = sequelize; 43 | db.Sequelize = Sequelize; 44 | 45 | module.exports = db; -------------------------------------------------------------------------------- /template-files/blueprint-project/app/router.js: -------------------------------------------------------------------------------- 1 | var controllers = require('./controllers/.index'); 2 | var actions = require('./actions/.index'); 3 | 4 | module.exports = function(app) { 5 | 6 | }; -------------------------------------------------------------------------------- /template-files/blueprint-project/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "username": "root", 4 | "password": null, 5 | "database": "database_development", 6 | "host": "127.0.0.1", 7 | "dialect": "mysql", 8 | "logging": false, 9 | "allow_origin": "http://localhost:4200" 10 | }, 11 | "test": { 12 | "username": "root", 13 | "password": null, 14 | "database": "database_test", 15 | "host": "127.0.0.1", 16 | "dialect": "mysql", 17 | "allow_origin": "http://localhost:4200" 18 | }, 19 | "production": { 20 | "use_env_variable": "DATABASE_URL", 21 | "allow_origin": "http://example.com" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /template-files/blueprint-project/lib/give-error.js: -------------------------------------------------------------------------------- 1 | var prettyjson = require('prettyjson'); 2 | var colors = require('colors/safe'); 3 | var inflect = require('inflect'); 4 | var handy = require('./handy'); 5 | 6 | /* 7 | * Take a Sequelize error (or one thrown by the user) and > 8 | * > show a detailed error for the dev while showing a human-friendly 9 | * > error for the client user 10 | */ 11 | module.exports = function(err, req, res, next) { 12 | var userErr = generateUserError(req); 13 | 14 | if (typeof err === "string") { 15 | userErr[1] = err; 16 | } 17 | 18 | if (err.constructor && err.constructor === Array 19 | && err.length > 1 20 | && (typeof err[0] === "number") && (typeof err[1] === "string")) { 21 | 22 | userErr[0] = err[0]; 23 | userErr[1] = err[1]; 24 | } 25 | 26 | errorForDev(req, err, userErr); 27 | errorForUser(res, userErr); 28 | }; 29 | 30 | // Error that the client user sees in the browser (JSON) 31 | function errorForUser(res, err) { 32 | var errorObj = { 33 | code: err[0], 34 | title: err[1] 35 | }; 36 | 37 | res.status(err[0]).json({ error: errorObj }); 38 | return true; 39 | } 40 | 41 | // Error that the developer sees in the terminal 42 | function errorForDev(req, err, userErr) { 43 | var errorObj = { 44 | code: err.code || userErr[0], 45 | api_message: userErr[1], 46 | error: err.message 47 | }; 48 | 49 | if (getFormatedStack(err.stack)) { 50 | errorObj.stack = getFormatedStack(err.stack); 51 | } 52 | 53 | errorObj.at = (new Date).toUTCString(); 54 | 55 | logErr(req.method + ' ' + req.url); 56 | logErr(errorObj); 57 | } 58 | 59 | // Nice colors for the developer 60 | function logErr(err) { 61 | if (typeof err === "object") { 62 | console.log(prettyjson.render(err, { 63 | keysColor: 'yellow', 64 | dashColor: 'magenta', 65 | numberColor: 'cyan', 66 | stringColor: 'white' 67 | })); 68 | } else { 69 | console.log(colors.red(err)); 70 | } 71 | } 72 | 73 | // Show relevant information from the stack trace 74 | function getFormatedStack(stack) { 75 | if (!stack) return ''; 76 | var stack = stack.match(/\n\s{4}at\s(.*)\s\((.*\/)?(.*)\:([\d]+\:[\d]+)\)\n/); 77 | stack[2] = stack[2]&&stack[2].length?stack[2].replace(/^.*\/(.*\/)$/, " $1"):""; 78 | stack = "in " + stack[1] + ", " + stack[2] + stack[3] + " " + stack[4]; 79 | return stack; 80 | } 81 | 82 | /* 83 | * If no custom error was set, we can generate a generic one using the req-object. 84 | * The client user will always get a friendly notice that something went wrong. 85 | */ 86 | function generateUserError(req) { 87 | if (req) { 88 | var hasId = handy.urlContainsId(req); 89 | var modelName = handy.getModelName(req); 90 | var modelSingular = inflect.decapitalize(inflect.humanize(inflect.singularize(modelName))); 91 | var modelPlural = inflect.decapitalize(inflect.humanize(inflect.pluralize(modelName))); 92 | 93 | var isLoad = (hasId && req.method === "GET"); 94 | var isUpdate = (hasId && req.method === "PUT"); 95 | var isDelete = (hasId && req.method === "DELETE"); 96 | var isList = (!hasId && req.method === "GET"); 97 | var isAdd = (!hasId && req.method === "POST"); 98 | 99 | var modelId; 100 | if (hasId) { 101 | modelId = handy.getModelId(req); 102 | } 103 | 104 | if (isLoad) { 105 | return [500, "Could not load the " + modelSingular + " with id " + modelId]; 106 | } else if (isUpdate) { 107 | return [500, "Could not update the " + modelSingular + " with id " + modelId]; 108 | } else if (isDelete) { 109 | return [500, "Could not delete the " + modelSingular + " with id " + modelId]; 110 | } else if (isList) { 111 | return [500, "Could not load " + modelPlural]; 112 | } else if (isAdd) { 113 | return [500, "Could not create a new " + modelSingular]; 114 | } 115 | } 116 | 117 | function getUrlVars(url) { 118 | var vars = {}; 119 | var parts = url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) { 120 | vars[key] = value; 121 | }); 122 | return vars; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /template-files/blueprint-project/lib/give-json.js: -------------------------------------------------------------------------------- 1 | var prettyjson = require('prettyjson'); 2 | var colors = require('colors/safe'); 3 | var inflect = require('inflect'); 4 | var humps = require('humps'); 5 | var handy = require('./handy'); 6 | 7 | /* 8 | * Take a Sequelize model (with all of its eventual associations) > 9 | * > and convert it to simple JSON according to the JSON API spec > 10 | * > used by Ember Data. 11 | */ 12 | module.exports = function(req, res, obj, opts) { 13 | var JSONKey = determineJSONKeyName(req); 14 | 15 | // Convert Sequelize model to simple JSON 16 | var objData = unwrapProperties(obj); 17 | objData = replaceForeignKeys(objData); 18 | objData = humps.camelizeKeys(objData); 19 | 20 | // Set the key name for that object 21 | var json = {}; 22 | json[JSONKey] = objData; 23 | 24 | // Determine whether to sideload associations or not 25 | if (!opts || opts && opts.embedded !== true) { 26 | json = sideload(json, JSONKey, opts); 27 | } 28 | 29 | console.log(colors.green(req.method + ' ' + req.url)); // console output (for dev) 30 | res.json(json); // http output (for user) 31 | } 32 | 33 | 34 | /* 35 | * Convert embedded data to sideloaded data 36 | * Go through each element, keep their ids intact and push the values to a new root key 37 | * Example: { user: {...} } => becomes { user: 1 } with a sideloaded user 38 | */ 39 | function sideload(json, JSONKey, opts) { 40 | 41 | if (json[JSONKey].constructor === Array) { 42 | json[JSONKey].forEach(function(element) { 43 | element = convertToSideload(element); 44 | }); 45 | } else { 46 | json[JSONKey] = convertToSideload(json[JSONKey]); 47 | } 48 | 49 | function convertToSideload(obj) { 50 | for (i in obj) { 51 | var value = obj[i]; 52 | var isArray = (value && typeof value[0] === "object"); 53 | var isSingleObj = (typeof value === "object" && value && value.id); 54 | 55 | // For both 56 | if (isArray || isSingleObj) { 57 | var sideKey = inflect.pluralize(i); 58 | 59 | // Check if user doesnt want certain objects embedded 60 | if (opts && opts.embedded && opts.embedded.indexOf(sideKey) !== -1) { 61 | continue; 62 | } 63 | 64 | if (!json[sideKey]) { 65 | json[sideKey] = []; 66 | } 67 | } 68 | 69 | // Specific cases 70 | if (isArray) { 71 | json[sideKey].push(value[0]); 72 | 73 | var onlyIds = []; 74 | value.forEach(function(valueObj) { 75 | onlyIds.push(valueObj.id); 76 | }); 77 | 78 | obj[i] = onlyIds; 79 | 80 | } else if (isSingleObj) { 81 | json[sideKey].push(value); 82 | obj[i] = value.id; 83 | } 84 | } 85 | 86 | return obj; 87 | } 88 | 89 | return json; 90 | } 91 | 92 | 93 | /* 94 | * If one of the keys is a foreign key (ends with _id) > 95 | * > then replace it with the name of the field (nicer to work with in Ember) 96 | * Example: user_id => user 97 | */ 98 | function replaceForeignKeys(objData) { 99 | 100 | if (objData.constructor === Array) { 101 | for (i in objData) { 102 | var objEl = objData[i]; 103 | objData[i] = removeIdPart(objEl); 104 | } 105 | } else { 106 | objData = removeIdPart(objData); 107 | } 108 | 109 | function removeIdPart(objEl) { 110 | for (key in objEl) { 111 | var lastThree = key.substr(key.length - 3); 112 | var keyWithoutLastThree = key.substr(0, key.length - 3); 113 | 114 | if (lastThree === "_id") { 115 | if (!objEl[keyWithoutLastThree]) { 116 | objEl[keyWithoutLastThree] = objEl[key]; 117 | } 118 | delete objEl[key]; 119 | } 120 | } 121 | return objEl; 122 | } 123 | 124 | return objData; 125 | } 126 | 127 | 128 | /* 129 | * Takes a Sequelize object and returns a > 130 | * > simple JavaScript object with the model values 131 | */ 132 | function unwrapProperties(obj) { 133 | 134 | var objData; 135 | 136 | // Get standard properties 137 | if (obj.constructor === Array) { 138 | objData = []; 139 | if (obj.length) { 140 | obj.forEach(function(objEl) { 141 | objData.push(objEl.get()); 142 | }); 143 | } 144 | } else { 145 | objData = obj.get(); 146 | } 147 | 148 | // Get pseudo (computed) properties 149 | if (obj.constructor === Array) { 150 | for (i in obj) { 151 | var objEl = obj[i]; 152 | objData[i] = getPseudoProperties(objData[i], objEl); 153 | } 154 | } else { 155 | objData = getPseudoProperties(objData, obj); 156 | } 157 | 158 | function getPseudoProperties(objData, objEl) { 159 | if (objEl.__options && objEl.__options.getterMethods) { 160 | for (getter in objEl.__options.getterMethods) { 161 | objData[getter] = objEl[getter]; 162 | } 163 | } 164 | 165 | return objData; 166 | } 167 | 168 | function isForeignKey(value) { 169 | return ( 170 | value && 171 | typeof value === "object" && 172 | (value[0] && value[0]['dataValues'] || value.dataValues) 173 | ); 174 | } 175 | 176 | if (objData.constructor && objData.constructor === Array) { 177 | objData.forEach(function(dataRow) { 178 | dataRow = simplifyEmbedded(dataRow); 179 | }); 180 | } else { 181 | objData = simplifyEmbedded(objData); 182 | } 183 | 184 | // We call the function recursively so that the associations are also unwrapped 185 | function simplifyEmbedded(objData) { 186 | for (i in objData) { 187 | if (isForeignKey(objData[i])) { 188 | objData[i] = unwrapProperties(objData[i]); 189 | } 190 | } 191 | 192 | return objData; 193 | } 194 | 195 | return objData; 196 | } 197 | 198 | /* 199 | * The root key of the returned JSON should > 200 | * > contain the model name (plural or singular depending on the request) 201 | */ 202 | function determineJSONKeyName(req) { 203 | var modelName = handy.getModelName(req); 204 | 205 | if (handy.urlContainsId(req)) { // Singular 206 | modelName = inflect.singularize(modelName); 207 | } else { 208 | modelName = inflect.pluralize(modelName); 209 | } 210 | 211 | return inflect.decapitalize(inflect.camelize(modelName)); 212 | } -------------------------------------------------------------------------------- /template-files/blueprint-project/lib/handy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * These functions are used in giveError and giveJSON 3 | */ 4 | 5 | function getModelName(req) { 6 | var modelURL = getBaseURL(req); 7 | 8 | if (urlContainsId(req) && modelURL.indexOf('/') !== -1) { 9 | modelURL = modelURL.substr(0, modelURL.lastIndexOf('/')); 10 | } 11 | 12 | return (modelURL.substr(modelURL.lastIndexOf('/') + 1)); 13 | } 14 | 15 | function getModelId(req) { 16 | var modelId; 17 | var baseURL = getBaseURL(req); 18 | 19 | if (baseURL.indexOf('/') !== -1) { 20 | modelId = baseURL.substr(baseURL.lastIndexOf('/') + 1); 21 | } 22 | 23 | return (!isNaN(modelId)) ? modelId : null; 24 | } 25 | 26 | function getBaseURL(req) { 27 | var baseURL = null; 28 | 29 | if (req.url) { 30 | if (req.url.indexOf("?") !== -1){ 31 | baseURL = req.url.substr(0, req.url.indexOf("?")); 32 | } else { 33 | baseURL = req.url; 34 | } 35 | } 36 | 37 | return baseURL; 38 | } 39 | 40 | function urlContainsId(req) { 41 | var modelId; 42 | var baseURL = getBaseURL(req); 43 | 44 | if ((baseURL.match(/\//g) || []).length > 1) { 45 | modelId = baseURL.substr(baseURL.lastIndexOf('/') + 1); 46 | } 47 | 48 | return !! modelId; 49 | } 50 | 51 | module.exports.getModelName = getModelName; 52 | module.exports.getModelId = getModelId; 53 | module.exports.getBaseURL = getBaseURL; 54 | module.exports.urlContainsId = urlContainsId; 55 | -------------------------------------------------------------------------------- /template-files/blueprint-project/lib/render.js: -------------------------------------------------------------------------------- 1 | var giveError = require('./give-error'); 2 | var giveJSON = require('./give-json'); 3 | 4 | module.exports = function(obj, req, res, next) { 5 | 6 | var isValidModel = ( 7 | obj.constructor && obj.constructor === Array && obj.length === 0 8 | || obj.model 9 | || (obj[0] && obj[0].dataValues) 10 | || obj.dataValues 11 | ); 12 | 13 | 14 | if (isValidModel) { 15 | 16 | var model, opts; 17 | if (obj.model) { 18 | model = obj.model; 19 | opts = obj; 20 | delete opts.model; 21 | } else { 22 | model = obj; 23 | opts = null; 24 | } 25 | return giveJSON(req, res, model, opts); 26 | } else { 27 | return giveError(obj, req, res); 28 | } 29 | 30 | }; -------------------------------------------------------------------------------- /template-files/blueprint-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{x-dashed}}", 3 | "version": "0.0.1", 4 | "description": "An API built with November", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "repository": { 11 | "type": "git", 12 | "url": "" 13 | }, 14 | "license": "ISC", 15 | "dependencies": { 16 | "body-parser": "^1.12.4", 17 | "express": "^4.12.3", 18 | "humps": "^0.5.2", 19 | "inflect": "^0.3.0", 20 | "mysql": "^2.6.2", 21 | "pg": "^4.4.0", 22 | "pg-hstore": "^2.3.2", 23 | "sequelize": "^2.1.3", 24 | "colors": "^1.1.0", 25 | "prettyjson": "^1.1.1", 26 | "ssacl-attribute-roles": "0.0.5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /template-files/blueprint-project/public/images/november/built-with-november.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t4t5/november-cli/f3badfad47ec0afde4229267ed3f383553fb2054/template-files/blueprint-project/public/images/november/built-with-november.png -------------------------------------------------------------------------------- /template-files/blueprint-project/public/images/november/built-with-november@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t4t5/november-cli/f3badfad47ec0afde4229267ed3f383553fb2054/template-files/blueprint-project/public/images/november/built-with-november@2x.png -------------------------------------------------------------------------------- /template-files/blueprint-project/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{x-human}} 6 | 7 | 8 | 47 | 48 | 49 | 50 | 51 |

{{x-human}}

52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /template-files/blueprint-project/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | 4 | var middleware = require('./app/middleware'); 5 | var router = require('./app/router'); 6 | var novrender = require('./lib/render'); 7 | 8 | // Set up all the routes and requirements for HTTP requests 9 | middleware(app); 10 | router(app); 11 | 12 | // Render either the model or the error from the routes 13 | app.use(novrender); 14 | 15 | // Listen to port 9000 by default 16 | app.listen(process.env.PORT || 9000, function() { 17 | console.log("November is running on port 9000"); 18 | }); 19 | -------------------------------------------------------------------------------- /template-files/controller-files/.index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var basename = path.basename(module.filename); 6 | var controls = {}; 7 | 8 | fs 9 | .readdirSync(__dirname) 10 | .filter(function(file) { 11 | return (file.indexOf('.') !== 0) && (file !== basename); 12 | }) 13 | .forEach(function(file) { 14 | var baseName = path.basename(file, '.js'); 15 | controls[baseName] = require('./' + file); 16 | }); 17 | 18 | module.exports = controls; -------------------------------------------------------------------------------- /template-files/controller-files/add.js: -------------------------------------------------------------------------------- 1 | module.exports = function(req, res, render) { 2 | 3 | req.models.{{x-singular-underscore}} 4 | .create(req.body.{{x-singular-camelcase}}) 5 | .then(function({{x-singular-camelcase}}) { 6 | render({{x-singular-camelcase}}); 7 | }) 8 | .catch(function(err) { 9 | render(err); 10 | }); 11 | 12 | }; -------------------------------------------------------------------------------- /template-files/controller-files/list.js: -------------------------------------------------------------------------------- 1 | module.exports = function(req, res, render) { 2 | 3 | req.models.{{x-singular-underscore}} 4 | .findAll() 5 | .then(function({{x-plural-camelcase}}) { 6 | render({{x-plural-camelcase}}); 7 | }) 8 | .catch(function(err) { 9 | render(err); 10 | }); 11 | 12 | }; -------------------------------------------------------------------------------- /template-files/controller-files/load.js: -------------------------------------------------------------------------------- 1 | module.exports = function(req, res, render) { 2 | 3 | req.models.{{x-singular-underscore}}.find({ 4 | where: { 5 | id: req.params.{{x-singular-underscore}}_id 6 | } 7 | }) 8 | .then(function({{x-singular-camelcase}}) { 9 | render({ 10 | model: {{x-singular-camelcase}} 11 | }); 12 | }) 13 | .catch(function(err) { 14 | render(err); 15 | }); 16 | 17 | }; -------------------------------------------------------------------------------- /template-files/controller-files/remove.js: -------------------------------------------------------------------------------- 1 | module.exports = function(req, res, render) { 2 | 3 | req.models.{{x-singular-underscore}}.find({ 4 | where: { 5 | id: req.params.{{x-singular-underscore}}_id 6 | } 7 | }) 8 | .then(function({{x-singular-camelcase}}) { 9 | return {{x-singular-camelcase}}.destroy(); 10 | }) 11 | .then(function() { 12 | res.json({}); 13 | }) 14 | .catch(function(err) { 15 | render(err); 16 | }); 17 | 18 | }; -------------------------------------------------------------------------------- /template-files/controller-files/update.js: -------------------------------------------------------------------------------- 1 | module.exports = function(req, res, render) { 2 | 3 | req.models.{{x-singular-underscore}}.find({ 4 | where: { 5 | id: req.params.{{x-singular-underscore}}_id 6 | } 7 | }) 8 | .then(function({{x-singular-camelcase}}) { 9 | 10 | /* 11 | * Set new values like this: 12 | * {{x-singular}}.some_field_name = req.body.{{x-singular}}.someFieldName; 13 | */ 14 | 15 | return {{x-singular-camelcase}}.save(); 16 | }) 17 | .then(function({{x-singular-camelcase}}) { 18 | render({{x-singular-camelcase}}); 19 | }) 20 | .catch(function(err) { 21 | render(err); 22 | }); 23 | 24 | }; -------------------------------------------------------------------------------- /template-files/model.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ssaclAttributeRoles = require('ssacl-attribute-roles'); 4 | 5 | module.exports = function(sequelize, DataTypes) { 6 | var {{x-singular-capitalize}} = sequelize.define('{{x-table}}', { 7 | /* 8 | * Set the table fields that you want for your model 9 | * Example: 10 | * username: DataTypes.STRING 11 | * 12 | * The "id", "created_at" and "updated_at" fields are automatically added > 13 | * > unless anything else is specified 14 | */ 15 | }, { 16 | getterMethods: { 17 | /* 18 | * Set pseudo properties 19 | * These will not be stored in the table, but rendered in the JSON response 20 | * Example: 21 | * username_uppercased: function() { return this.getDataValue('username').toUpperCase(); } 22 | */ 23 | }, 24 | 25 | classMethods: { 26 | associate: function(models) { 27 | /* 28 | * Define relationships with other models 29 | * Example: 30 | * models.user.hasOne(models.project); 31 | */ 32 | } 33 | }, 34 | 35 | underscored: true, 36 | underscoredAll: true 37 | 38 | /* Find more configurations at: 39 | * http://docs.sequelizejs.com/en/latest/docs/models-definition/#configuration 40 | */ 41 | }); 42 | 43 | ssaclAttributeRoles({{x-singular-capitalize}}); 44 | 45 | return {{x-singular-capitalize}}; 46 | }; 47 | -------------------------------------------------------------------------------- /template-files/router-action.js: -------------------------------------------------------------------------------- 1 | // {{x-human}} 2 | app.post('/{{x-camelcase}}', actions.{{x-underscore}}); -------------------------------------------------------------------------------- /template-files/router-model.js: -------------------------------------------------------------------------------- 1 | // {{x-human}} 2 | app.route('/{{x-plural-camelcase}}') 3 | .get(controllers.{{x-singular-underscore}}.list) 4 | .post(controllers.{{x-singular-underscore}}.add); 5 | app.route('/{{x-plural-camelcase}}/:{{x-singular-underscore}}_id') 6 | .get(controllers.{{x-singular-underscore}}.load) 7 | .put(controllers.{{x-singular-underscore}}.update) 8 | .delete(controllers.{{x-singular-underscore}}.remove); --------------------------------------------------------------------------------