├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── app ├── USAGE ├── generator.js ├── index.js └── templates │ ├── .buildignore │ ├── .editorconfig │ ├── .gitattributes │ ├── .jscsrc │ ├── .travis.yml │ ├── Gruntfile.js │ ├── README.md │ ├── _.gitignore │ ├── _package.json │ ├── mocha.conf.js │ └── server │ ├── .jshintrc │ ├── .jshintrc-spec │ ├── api │ └── user(auth) │ │ ├── index.js │ │ ├── index.spec.js │ │ ├── user.controller.js │ │ ├── user.events.js │ │ ├── user.integration.js │ │ ├── user.model(mongooseModels).js │ │ ├── user.model(sequelizeModels).js │ │ ├── user.model.spec(mongooseModels).js │ │ └── user.model.spec(sequelizeModels).js │ ├── app.js │ ├── auth(auth) │ ├── auth.service.js │ ├── facebook(facebookAuth) │ │ ├── index.js │ │ └── passport.js │ ├── google(googleAuth) │ │ ├── index.js │ │ └── passport.js │ ├── index.js │ ├── local │ │ ├── index.js │ │ └── passport.js │ └── twitter(twitterAuth) │ │ ├── index.js │ │ └── passport.js │ ├── components │ └── errors │ │ └── index.js │ ├── config │ ├── _local.env.js │ ├── _local.env.sample.js │ ├── environment │ │ ├── development.js │ │ ├── index.js │ │ ├── production.js │ │ ├── shared.js │ │ └── test.js │ ├── express.js │ ├── seed(models).js │ └── socketio(socketio).js │ ├── index.js │ ├── routes.js │ └── sqldb(sequelize) │ └── index.js ├── contributing.md ├── endpoint ├── generator.js ├── index.js └── templates │ ├── basename.controller.js │ ├── basename.events(models).js │ ├── basename.integration.js │ ├── basename.model(mongooseModels).js │ ├── basename.model(sequelizeModels).js │ ├── basename.socket(socketio).js │ ├── index.js │ └── index.spec.js ├── generator-base.js ├── heroku ├── USAGE ├── index.js └── templates │ └── Procfile ├── openshift ├── USAGE ├── index.js └── templates │ └── hot_deploy ├── package.json ├── provider └── index.js ├── readme.md ├── route └── index.js ├── scripts └── sauce_connect_setup.sh ├── service └── index.js ├── task-utils ├── changelog-templates │ └── commit.hbs └── grunt.js ├── test ├── fixtures │ └── .yo-rc.json └── test-file-creation.js └── util.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | 3 | # Denote all files that are truly binary and should not be modified. 4 | *.png binary 5 | *.gif binary 6 | *.jpg binary 7 | *.jpeg binary 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": false, 5 | "curly": false, 6 | "eqeqeq": true, 7 | "eqnull": true, 8 | "immed": true, 9 | "latedef": true, 10 | "newcap": true, 11 | "noarg": true, 12 | "undef": true, 13 | "strict": false, 14 | "trailing": true, 15 | "smarttabs": true 16 | } 17 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | expressjs-api-deps 2 | test 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '0.12' 5 | - '4.0.0' 6 | env: 7 | global: 8 | - SAUCE_USERNAME=fullstack_ci 9 | - SAUCE_ACCESS_KEY=1a527ca6-4aa5-4618-86ce-0278bf158cbf 10 | matrix: 11 | fast_finish: true 12 | allow_failures: 13 | - node_js: 4.0.0 14 | before_install: 15 | - ./scripts/sauce_connect_setup.sh 16 | - gem update --system 17 | - gem install sass --version "=3.3.7" 18 | - npm install -g grunt-cli 19 | services: mongodb 20 | cache: 21 | directories: 22 | - node_modules 23 | - test/fixtures/node_modules 24 | notifications: 25 | webhooks: 26 | urls: 27 | - secure: "DhPNqHXuUIeIGE9Ek3+63qhco+4MozXqMZL6dAKoq1MHQ2RAPO6SYIkUYZqDnuWYlwWao2EnTYcDREivIV/m/RnkP9bKlpX/n/RNJe+X4bwFaCU55fVKgkAFn3takSBC5SVoeTWHdWu3WhhqSdioWjT7mlE1wtt/RanSMb5Id8M=" 28 | on_success: change # options: [always|never|change] default: always 29 | on_failure: always # options: [always|never|change] default: always 30 | on_start: false # default: false 31 | git: 32 | submodules: false 33 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var shell = require('shelljs'); 4 | var child_process = require('child_process'); 5 | var Q = require('q'); 6 | var helpers = require('yeoman-generator').test; 7 | var fs = require('fs'); 8 | var path = require('path'); 9 | 10 | module.exports = function (grunt) { 11 | var gruntUtils = require('./task-utils/grunt')(grunt); 12 | var gitCmd = gruntUtils.gitCmd; 13 | var gitCmdAsync = gruntUtils.gitCmdAsync; 14 | 15 | // Load grunt tasks automatically, when needed 16 | require('jit-grunt')(grunt, { 17 | buildcontrol: 'grunt-build-control' 18 | }); 19 | 20 | grunt.initConfig({ 21 | config: { 22 | demo: 'demo' 23 | }, 24 | pkg: grunt.file.readJSON('package.json'), 25 | conventionalChangelog: { 26 | options: { 27 | changelogOpts: { 28 | // conventional-changelog options go here 29 | preset: 'angular' 30 | }, 31 | writerOpts: { 32 | // conventional-changelog-writer options go here 33 | finalizeContext: gruntUtils.conventionalChangelog.finalizeContext, 34 | commitPartial: gruntUtils.conventionalChangelog.commitPartial 35 | } 36 | }, 37 | release: { 38 | src: 'CHANGELOG.md' 39 | } 40 | }, 41 | release: { 42 | options: { 43 | bump: false, // remove after 3.0.0 release 44 | commitMessage: '<%= version %>', 45 | tagName: '<%= version %>', 46 | file: 'package.json', 47 | beforeBump: ['updateSubmodules'], 48 | afterBump: ['updateFixtures:deps'], 49 | beforeRelease: ['stage'], 50 | push: false, 51 | pushTags: false, 52 | npm: false 53 | } 54 | }, 55 | stage: { 56 | options: { 57 | files: ['CHANGELOG.md', 'expressjs-api-deps'] 58 | } 59 | }, 60 | buildcontrol: { 61 | options: { 62 | dir: 'demo', 63 | commit: true, 64 | push: true, 65 | connectCommits: false, 66 | message: 'Built using ExpressJS Api v<%= pkg.version %> from commit %sourceCommit%' 67 | }, 68 | release: { 69 | options: { 70 | remote: 'origin', 71 | branch: 'master' 72 | } 73 | } 74 | }, 75 | jshint: { 76 | options: { 77 | curly: false, 78 | node: true 79 | }, 80 | all: ['Gruntfile.js', '*/index.js'] 81 | }, 82 | env: { 83 | fast: { 84 | SKIP_E2E: true 85 | } 86 | }, 87 | mochaTest: { 88 | test: { 89 | src: [ 90 | 'test/*.js' 91 | ], 92 | options: { 93 | reporter: 'spec', 94 | timeout: 120000 95 | } 96 | } 97 | }, 98 | clean: { 99 | demo: { 100 | files: [{ 101 | dot: true, 102 | src: [ 103 | '<%= config.demo %>/*', 104 | '!<%= config.demo %>/readme.md', 105 | '!<%= config.demo %>/node_modules', 106 | '!<%= config.demo %>/.git', 107 | '!<%= config.demo %>/dist' 108 | ] 109 | }] 110 | } 111 | }, 112 | david: { 113 | gen: { 114 | options: {} 115 | }, 116 | app: { 117 | options: { 118 | package: 'test/fixtures/package.json' 119 | } 120 | } 121 | } 122 | }); 123 | 124 | grunt.registerTask('stage', 'git add files before running the release task', function () { 125 | var files = grunt.config('stage.options').files; 126 | gitCmd(['add'].concat(files), {}, this.async()); 127 | }); 128 | 129 | grunt.registerTask('updateSubmodules', function() { 130 | grunt.config.requires('updateSubmodules.options.modules'); 131 | var modules = grunt.config.get('updateSubmodules').options.modules; 132 | 133 | Q() 134 | .then(gitCmdAsync(['submodule', 'update', '--init', '--recursive'])) 135 | .then(function() { 136 | var thens = []; 137 | for (var i = 0, modulesLength = modules.length; i < modulesLength; i++) { 138 | var opts = {cwd: modules[i]}; 139 | thens.push(gitCmdAsync(['checkout', 'master'], opts)); 140 | thens.push(gitCmdAsync(['fetch'], opts)); 141 | thens.push(gitCmdAsync(['pull'], opts)); 142 | } 143 | return thens.reduce(Q.when, Q()); 144 | }) 145 | .catch(grunt.fail.fatal.bind(grunt.fail)) 146 | .finally(this.async()); 147 | }); 148 | 149 | grunt.registerTask('commitNgFullstackDeps', function() { 150 | grunt.config.requires( 151 | 'commitNgFullstackDeps.options.files', 152 | 'commitNgFullstackDeps.options.cwd' 153 | ); 154 | var ops = grunt.config.get('commitNgFullstackDeps').options; 155 | var version = require('./package.json').version || 'NO VERSION SET'; 156 | if (Array.isArray(ops.files) && ops.files.length > 0) { 157 | gitCmd(['commit', '-m', version].concat(ops.files), { 158 | cwd: path.resolve(__dirname, ops.cwd) 159 | }, this.async()); 160 | } else { 161 | grunt.log.writeln('No files were commited'); 162 | } 163 | }); 164 | 165 | grunt.registerTask('generateDemo', 'generate demo', function () { 166 | var done = this.async(); 167 | 168 | shell.mkdir(grunt.config('config').demo); 169 | shell.cd(grunt.config('config').demo); 170 | 171 | Q() 172 | .then(generateDemo) 173 | .then(function() { 174 | shell.cd('../'); 175 | }) 176 | .catch(function(msg){ 177 | grunt.fail.warn(msg || 'failed to generate demo') 178 | }) 179 | .finally(done); 180 | 181 | function generateDemo() { 182 | var deferred = Q.defer(); 183 | var options = { 184 | script: 'js', 185 | markup: 'html', 186 | stylesheet: 'sass', 187 | router: 'uirouter', 188 | bootstrap: true, 189 | uibootstrap: true, 190 | mongoose: true, 191 | testing: 'jasmine', 192 | auth: true, 193 | oauth: ['googleAuth', 'twitterAuth'], 194 | socketio: true 195 | }; 196 | 197 | var deps = [ 198 | '../app', 199 | [ 200 | helpers.createDummyGenerator(), 201 | 'ng-component:app' 202 | ] 203 | ]; 204 | 205 | var gen = helpers.createGenerator('expressjs-api:app', deps); 206 | 207 | helpers.mockPrompt(gen, options); 208 | gen.run({}, function () { 209 | deferred.resolve(); 210 | }); 211 | 212 | return deferred.promise; 213 | } 214 | }); 215 | 216 | grunt.registerTask('releaseDemoBuild', 'builds and releases demo', function () { 217 | var done = this.async(); 218 | 219 | shell.cd(grunt.config('config').demo); 220 | 221 | Q() 222 | .then(gruntBuild) 223 | .then(gruntRelease) 224 | .then(function() { 225 | shell.cd('../'); 226 | }) 227 | .catch(function(msg){ 228 | grunt.fail.warn(msg || 'failed to release demo') 229 | }) 230 | .finally(done); 231 | 232 | function run(cmd) { 233 | var deferred = Q.defer(); 234 | var generator = shell.exec(cmd, {async:true}); 235 | generator.stdout.on('data', function (data) { 236 | grunt.verbose.writeln(data); 237 | }); 238 | generator.on('exit', function (code) { 239 | deferred.resolve(); 240 | }); 241 | 242 | return deferred.promise; 243 | } 244 | 245 | function gruntBuild() { 246 | return run('grunt'); 247 | } 248 | 249 | function gruntRelease() { 250 | return run('grunt buildcontrol:heroku'); 251 | } 252 | }); 253 | 254 | grunt.registerTask('updateFixtures', 'updates package', function(target) { 255 | var genVer = require('./package.json').version; 256 | var dest = __dirname + ((target === 'deps') ? '/expressjs-api-deps/' : '/test/fixtures/'); 257 | var appName = (target === 'deps') ? 'expressjs-api-deps' : 'tempApp'; 258 | 259 | var processJson = function(s, d) { 260 | // read file, strip all ejs conditionals, and parse as json 261 | var json = JSON.parse(fs.readFileSync(path.resolve(s), 'utf8').replace(/<%(.*)%>/g, '')); 262 | // set properties 263 | json.name = appName, json.version = genVer; 264 | if (target === 'deps') { json.private = false; } 265 | // stringify json and write it to the destination 266 | fs.writeFileSync(path.resolve(d), JSON.stringify(json, null, 2)); 267 | }; 268 | 269 | processJson('app/templates/_package.json', dest + 'package.json'); 270 | }); 271 | 272 | grunt.registerTask('installFixtures', 'install package fixtures', function() { 273 | var done = this.async(); 274 | 275 | shell.cd('test/fixtures'); 276 | grunt.log.ok('installing npm dependencies for generated app'); 277 | child_process.exec('npm install --quiet', {cwd: '../fixtures'}, function (error, stdout, stderr) { 278 | shell.cd('../../'); 279 | done(); 280 | }) 281 | }); 282 | }); 283 | 284 | grunt.registerTask('test', function(target, option) { 285 | if (target === 'fast') { 286 | grunt.task.run([ 287 | 'env:fast' 288 | ]); 289 | } 290 | 291 | return grunt.task.run([ 292 | 'updateFixtures', 293 | 'installFixtures', 294 | 'mochaTest' 295 | ]) 296 | }); 297 | 298 | grunt.registerTask('deps', function(target) { 299 | if (!target || target === 'app') grunt.task.run(['updateFixtures']); 300 | grunt.task.run(['david:' + (target || '')]); 301 | }); 302 | 303 | grunt.registerTask('demo', [ 304 | 'clean:demo', 305 | 'generateDemo' 306 | ]); 307 | 308 | grunt.registerTask('releaseDemo', [ 309 | 'demo', 310 | 'releaseDemoBuild', 311 | 'buildcontrol:release' 312 | ]); 313 | 314 | //grunt.registerTask('default', ['bump', 'changelog', 'stage', 'release']); 315 | }; 316 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Robbie Buchanan 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 | -------------------------------------------------------------------------------- /app/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Creates a ExpressJS API Project 3 | 4 | Example: 5 | yo expressjs-api 6 | 7 | Sub Generators: 8 | 9 | Server Side: 10 | expressjs-api:endpoint 11 | 12 | Deployment: 13 | expressjs-api:openshift 14 | expressjs-api:heroku 15 | -------------------------------------------------------------------------------- /app/generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import chalk from 'chalk'; 6 | import {Base} from 'yeoman-generator'; 7 | import {genBase} from '../generator-base'; 8 | 9 | export default class Generator extends Base { 10 | 11 | constructor(...args) { 12 | super(...args); 13 | 14 | this.argument('name', { type: String, required: false }); 15 | 16 | this.option('skip-install', { 17 | desc: 'Do not install dependencies', 18 | type: Boolean, 19 | defaults: false 20 | }); 21 | 22 | this.option('app-suffix', { 23 | desc: 'Allow a custom suffix to be added to the module name', 24 | type: String, 25 | defaults: 'App' 26 | }); 27 | } 28 | 29 | get initializing() { 30 | return { 31 | 32 | init: function () { 33 | this.config.set('generatorVersion', this.rootGeneratorVersion()); 34 | this.filters = {}; 35 | 36 | // init shared generator properies and methods 37 | genBase(this); 38 | }, 39 | 40 | info: function () { 41 | this.log(chalk.red(` 42 | ################################################################ 43 | # NOTE: You are using a pre-release version of 44 | # generator-expressjs-api. For a more stable version, run 45 | # \`npm install -g generator-expressjs-api@^1.0.0\` 46 | ################################################################`)); 47 | this.log('You\'re using the ExpressJS Api Generator, version ' + this.rootGeneratorVersion()); 48 | this.log(this.yoWelcome); 49 | this.log('Out of the box I create an Express server for creating WebServices (Api\'s).\n'); 50 | }, 51 | 52 | checkForConfig: function() { 53 | var cb = this.async(); 54 | var existingFilters = this.config.get('filters'); 55 | 56 | if(existingFilters) { 57 | this.prompt([{ 58 | type: 'confirm', 59 | name: 'skipConfig', 60 | message: 'Existing .yo-rc configuration found, would you like to use it?', 61 | default: true, 62 | }], function (answers) { 63 | this.skipConfig = answers.skipConfig; 64 | 65 | if (this.skipConfig) { 66 | this.filters = existingFilters; 67 | } else { 68 | this.filters = {}; 69 | this.forceConfig = true; 70 | this.config.set('filters', this.filters); 71 | this.config.forceSave(); 72 | } 73 | 74 | cb(); 75 | }.bind(this)); 76 | } else { 77 | cb(); 78 | } 79 | } 80 | 81 | }; 82 | } 83 | 84 | get prompting() { 85 | return { 86 | serverPrompts: function() { 87 | if(this.skipConfig) return; 88 | var cb = this.async(); 89 | var self = this; 90 | this.log('\n# Server\n'); 91 | 92 | this.prompt([{ 93 | type: 'checkbox', 94 | name: 'odms', 95 | message: 'What would you like to use for data modeling?', 96 | choices: [ 97 | { 98 | value: 'mongoose', 99 | name: 'Mongoose (MongoDB)', 100 | checked: true 101 | }, 102 | { 103 | value: 'sequelize', 104 | name: 'Sequelize (MySQL, SQLite, MariaDB, PostgreSQL)', 105 | checked: false 106 | } 107 | ] 108 | }, { 109 | type: 'list', 110 | name: 'models', 111 | message: 'What would you like to use for the default models?', 112 | choices: [ 'Mongoose', 'Sequelize' ], 113 | filter: function( val ) { 114 | return val.toLowerCase(); 115 | }, 116 | when: function(answers) { 117 | return answers.odms && answers.odms.length > 1; 118 | } 119 | }, { 120 | type: 'confirm', 121 | name: 'pluralization', 122 | message: 'Enable Pluralized table names (User becomes Users)', 123 | when: function(answers){ 124 | return answers.odms && answers.odms.length >= 1 && answers.odms.indexOf('sequelize') !=1; 125 | } 126 | }, { 127 | type: 'confirm', 128 | name: 'timestamps', 129 | message: 'Enable timestamps (created_at, updated_at)?', 130 | when: function (answers){ 131 | return answers.odms && answers.odms.length >= 1 && answers.odms.indexOf('sequelize') != -1; 132 | } 133 | }, { 134 | type: 'confirm', 135 | name: 'paranoid', 136 | message: 'Enable "paranoid" deletes (deleted_at) within SQL?', 137 | when: function (answers){ 138 | return answers.odms && answers.odms.length >= 1 && answers.odms.indexOf('sequelize') != -1; 139 | } 140 | },{ 141 | type: 'list', 142 | name: 'primaryKey', 143 | message: 'What type of RDBMS Primary Key should be used?', 144 | choices: [ 145 | { 146 | value:'serial', 147 | name: 'Auto-Incrementing (Serial)' 148 | },{ 149 | value:'uuid', 150 | name: 'Universal Unique Identifier (UUIDv4)' 151 | }], 152 | filter: function ( val ) { 153 | return val.toLowerCase(); 154 | }, 155 | when: function (answers){ 156 | return answers.odms && answers.odms.length >= 0 && answers.odms.indexOf('sequelize') != -1; 157 | } 158 | }, { 159 | type: 'confirm', 160 | name: 'auth', 161 | message: 'Would you scaffold out an authentication boilerplate?', 162 | when: function (answers) { 163 | return answers.odms && answers.odms.length !== 0; 164 | } 165 | }, { 166 | type: 'checkbox', 167 | name: 'oauth', 168 | message: 'Would you like to include additional oAuth strategies?', 169 | when: function (answers) { 170 | return answers.auth; 171 | }, 172 | choices: [ 173 | { 174 | value: 'googleAuth', 175 | name: 'Google', 176 | checked: false 177 | }, 178 | { 179 | value: 'facebookAuth', 180 | name: 'Facebook', 181 | checked: false 182 | }, 183 | { 184 | value: 'twitterAuth', 185 | name: 'Twitter', 186 | checked: false 187 | } 188 | ] 189 | }, { 190 | type: 'confirm', 191 | name: 'socketio', 192 | message: 'Would you like to use socket.io?', 193 | // to-do: should not be dependent on ODMs 194 | when: function (answers) { 195 | return answers.odms && answers.odms.length !== 0; 196 | }, 197 | default: true 198 | }], function (answers) { 199 | if(answers.socketio) this.filters.socketio = true; 200 | if(answers.auth) this.filters.auth = true; 201 | if(answers.odms && answers.odms.length > 0) { 202 | var models; 203 | if(!answers.models) { 204 | models = answers.odms[0]; 205 | } else { 206 | models = answers.models; 207 | } 208 | this.filters.models = true; 209 | this.filters[models + 'Models'] = {}; 210 | this.filters[models + 'Models'].serial = answers.primaryKey === 'serial' ? true : false; 211 | this.filters[models + 'Models'].uuid = answers.primaryKey === 'uuid' ? true : false; 212 | delete answers.primaryKey; 213 | this.filters[models + 'Models'].paranoid = answers.paranoid || false; 214 | delete answers.paranoid; 215 | this.filters[models + 'Models'].timestamps = answers.timestamps || false; 216 | delete answers.timestamps; 217 | this.filters[models + 'Models'].pluralization = answers.pluralization || false; 218 | delete answers.pluralization; 219 | answers.odms.forEach(function(odm) { 220 | this.filters[odm] = true; 221 | }.bind(this)); 222 | } else { 223 | this.filters.noModels = true; 224 | } 225 | if(answers.oauth) { 226 | if(answers.oauth.length) this.filters.oauth = true; 227 | answers.oauth.forEach(function(oauthStrategy) { 228 | this.filters[oauthStrategy] = true; 229 | }.bind(this)); 230 | } 231 | 232 | cb(); 233 | }.bind(this)); 234 | }, 235 | 236 | projectPrompts: function() { 237 | if(this.skipConfig) return; 238 | var cb = this.async(); 239 | var self = this; 240 | 241 | this.log('\n# Project\n'); 242 | 243 | this.prompt([{ 244 | type: 'list', 245 | name: 'testing', 246 | message: 'What would you like to write tests with?', 247 | choices: [ 'Jasmine', 'Mocha + Chai + Sinon'], 248 | filter: function( val ) { 249 | var filterMap = { 250 | 'Jasmine': 'jasmine', 251 | 'Mocha + Chai + Sinon': 'mocha' 252 | }; 253 | 254 | return filterMap[val]; 255 | } 256 | }, { 257 | type: 'list', 258 | name: 'chai', 259 | message: 'What would you like to write Chai assertions with?', 260 | choices: ['Expect', 'Should'], 261 | filter: function( val ) { 262 | return val.toLowerCase(); 263 | }, 264 | when: function( answers ) { 265 | return answers.testing === 'mocha'; 266 | } 267 | }], function (answers) { 268 | /** 269 | * Default to grunt until gulp support is implemented 270 | */ 271 | this.filters.grunt = true; 272 | 273 | this.filters[answers.testing] = true; 274 | if (answers.testing === 'mocha') { 275 | this.filters.jasmine = false; 276 | this.filters.should = false; 277 | this.filters.expect = false; 278 | this.filters[answers.chai] = true; 279 | } 280 | if (answers.testing === 'jasmine') { 281 | this.filters.mocha = false; 282 | this.filters.should = false; 283 | this.filters.expect = false; 284 | } 285 | 286 | cb(); 287 | }.bind(this)); 288 | } 289 | 290 | }; 291 | } 292 | 293 | get configuring() { 294 | return { 295 | 296 | saveSettings: function() { 297 | if(this.skipConfig) return; 298 | this.config.set('endpointDirectory', 'server/api/'); 299 | this.config.set('insertRoutes', true); 300 | this.config.set('registerRoutesFile', 'server/routes.js'); 301 | this.config.set('routesNeedle', '// Insert routes below'); 302 | 303 | this.config.set('routesBase', '/api/'); 304 | this.config.set('pluralizeRoutes', true); 305 | 306 | this.config.set('insertSockets', true); 307 | this.config.set('registerSocketsFile', 'server/config/socketio.js'); 308 | this.config.set('socketsNeedle', '// Insert sockets below'); 309 | 310 | this.config.set('insertModels', true); 311 | this.config.set('registerModelsFile', 'server/sqldb/index.js'); 312 | this.config.set('modelsNeedle', '// Insert models below'); 313 | 314 | this.config.set('filters', this.filters); 315 | this.config.forceSave(); 316 | } 317 | }; 318 | } 319 | 320 | get default() { 321 | return {}; 322 | } 323 | 324 | get writing() { 325 | return { 326 | 327 | generateProject: function() { 328 | this.sourceRoot(path.join(__dirname, './templates')); 329 | this.processDirectory('.', '.'); 330 | }, 331 | 332 | generateEndpoint: function() { 333 | var models; 334 | if (this.filters.mongooseModels) { 335 | models = 'mongoose'; 336 | } else if (this.filters.sequelizeModels) { 337 | models = 'sequelize'; 338 | } 339 | var options = { 340 | route: '/api/things', 341 | models: models 342 | } 343 | options[models+'Models'] = this.filters[models+'Models'] || {}; 344 | this.composeWith('expressjs-api:endpoint', { 345 | options: options, 346 | args: ['thing'] 347 | }); 348 | } 349 | 350 | }; 351 | } 352 | 353 | get install() { 354 | return { 355 | 356 | installDeps: function() { 357 | this.installDependencies({ 358 | skipInstall: this.options['skip-install'] 359 | }); 360 | } 361 | 362 | }; 363 | } 364 | 365 | get end() { 366 | return {}; 367 | } 368 | 369 | } 370 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Register the Babel require hook 4 | require('babel-core/register')({ 5 | only: /generator-expressjs-api\/(?!node_modules)/ 6 | }); 7 | 8 | // Export the generator 9 | exports = module.exports = require('./generator'); 10 | -------------------------------------------------------------------------------- /app/templates/.buildignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ioneyed/generator-expressjs-api/188e0f87179c92171cfe5d1c61bc42790f541a42/app/templates/.buildignore -------------------------------------------------------------------------------- /app/templates/.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 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /app/templates/.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | 3 | # Denote all files that are truly binary and should not be modified. 4 | *.png binary 5 | *.gif binary 6 | *.jpg binary 7 | *.jpeg binary 8 | -------------------------------------------------------------------------------- /app/templates/.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "maximumLineLength": { 4 | "value": 100, 5 | "allowComments": true, 6 | "allowRegex": true 7 | }, 8 | "disallowMixedSpacesAndTabs": true, 9 | "disallowMultipleLineStrings": true, 10 | "disallowNewlineBeforeBlockStatements": true, 11 | "disallowSpaceAfterObjectKeys": true, 12 | "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], 13 | "disallowSpaceBeforeBinaryOperators": [","], 14 | "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], 15 | "disallowSpacesInAnonymousFunctionExpression": { 16 | "beforeOpeningRoundBrace": true 17 | }, 18 | "disallowSpacesInFunctionDeclaration": { 19 | "beforeOpeningRoundBrace": true 20 | }, 21 | "disallowSpacesInNamedFunctionExpression": { 22 | "beforeOpeningRoundBrace": true 23 | }, 24 | "disallowSpacesInsideArrayBrackets": true, 25 | "disallowSpacesInsideParentheses": true, 26 | "disallowTrailingComma": true, 27 | "disallowTrailingWhitespace": true, 28 | "requireCommaBeforeLineBreak": true, 29 | "requireLineFeedAtFileEnd": true, 30 | "requireSpaceAfterBinaryOperators": ["?", ":", "+", "-", "/", "*", "%", "==", "===", "!=", "!==", ">", ">=", "<", "<=", "&&", "||"], 31 | "requireSpaceBeforeBinaryOperators": ["?", ":", "+", "-", "/", "*", "%", "==", "===", "!=", "!==", ">", ">=", "<", "<=", "&&", "||"], 32 | "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], 33 | "requireSpaceBeforeBlockStatements": true, 34 | "requireSpacesInConditionalExpression": { 35 | "afterTest": true, 36 | "beforeConsequent": true, 37 | "afterConsequent": true, 38 | "beforeAlternate": true 39 | }, 40 | "requireSpacesInFunction": { 41 | "beforeOpeningCurlyBrace": true 42 | }, 43 | "validateLineBreaks": "LF", 44 | "validateParameterSeparator": ", " 45 | } 46 | -------------------------------------------------------------------------------- /app/templates/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.12' 4 | - '4.0.0' 5 | matrix: 6 | fast_finish: true 7 | allow_failures: 8 | - node_js: 4.0.0 9 | before_script: 10 | - npm install -g grunt-cli<% if (filters.sass) { %> 11 | - gem install sass<% } %> 12 | services: mongodb 13 | -------------------------------------------------------------------------------- /app/templates/Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on <%= (new Date).toISOString().split('T')[0] %> using <%= rootGeneratorName() %> <%= rootGeneratorVersion() %> 2 | 'use strict'; 3 | 4 | module.exports = function (grunt) { 5 | var localConfig; 6 | try { 7 | localConfig = require('./server/config/local.env'); 8 | } catch(e) { 9 | localConfig = {}; 10 | } 11 | 12 | // Load grunt tasks automatically, when needed 13 | require('jit-grunt')(grunt, { 14 | express: 'grunt-express-server', 15 | buildcontrol: 'grunt-build-control', 16 | istanbul_check_coverage: 'grunt-mocha-istanbul', 17 | }); 18 | 19 | // Time how long tasks take. Can help when optimizing build times 20 | require('time-grunt')(grunt); 21 | 22 | // Define the configuration for all the tasks 23 | grunt.initConfig({ 24 | 25 | // Project settings 26 | pkg: grunt.file.readJSON('package.json'), 27 | yeoman: { 28 | // configurable paths 29 | server: 'server', 30 | dist: 'dist' 31 | }, 32 | express: { 33 | options: { 34 | port: process.env.PORT || 9000 35 | }, 36 | dev: { 37 | options: { 38 | script: '<%%= yeoman.server %>', 39 | debug: true 40 | } 41 | }, 42 | prod: { 43 | options: { 44 | script: '<%%= yeoman.dist %>/<%%= yeoman.server %>' 45 | } 46 | } 47 | }, 48 | watch: { 49 | mochaTest: { 50 | files: ['<%%= yeoman.server %>/**/*.{spec,integration}.js'], 51 | tasks: ['env:test', 'mochaTest'] 52 | }, 53 | gruntfile: { 54 | files: ['Gruntfile.js'] 55 | }, 56 | livereload: { 57 | files: [ 58 | '{.tmp,<%%= yeoman.client %>}/{app,components}/**/!(*.spec|*.mock).js', 59 | ], 60 | options: { 61 | livereload: true 62 | } 63 | }, 64 | express: { 65 | files: ['<%%= yeoman.server %>/**/*.{js,json}'], 66 | tasks: ['express:dev', 'wait'], 67 | options: { 68 | livereload: true, 69 | spawn: false //Without this option specified express won't be reloaded 70 | } 71 | } 72 | }, 73 | 74 | // Make sure code styles are up to par and there are no obvious mistakes 75 | jshint: { 76 | options: { 77 | jshintrc: '<%%= yeoman.client %>/.jshintrc', 78 | reporter: require('jshint-stylish') 79 | }, 80 | server: { 81 | options: { 82 | jshintrc: '<%%= yeoman.server %>/.jshintrc' 83 | }, 84 | src: ['<%%= yeoman.server %>/**/!(*.spec|*.integration).js'] 85 | }, 86 | serverTest: { 87 | options: { 88 | jshintrc: '<%%= yeoman.server %>/.jshintrc-spec' 89 | }, 90 | src: ['<%%= yeoman.server %>/**/*.{spec,integration}.js'] 91 | } 92 | }, 93 | 94 | jscs: { 95 | options: { 96 | config: ".jscsrc" 97 | }, 98 | main: { 99 | files: { 100 | src: [ 101 | '<%%= yeoman.server %>/**/*.js' 102 | ] 103 | } 104 | } 105 | }, 106 | 107 | // Empties folders to start fresh 108 | clean: { 109 | dist: { 110 | files: [{ 111 | dot: true, 112 | src: [ 113 | '.tmp', 114 | '<%%= yeoman.dist %>/!(.git*|.openshift|Procfile)**' 115 | ] 116 | }] 117 | }, 118 | server: '.tmp' 119 | }, 120 | 121 | // Debugging with node inspector 122 | 'node-inspector': { 123 | custom: { 124 | options: { 125 | 'web-host': 'localhost' 126 | } 127 | } 128 | }, 129 | 130 | // Use nodemon to run server in debug mode with an initial breakpoint 131 | nodemon: { 132 | debug: { 133 | script: '<%%= yeoman.server %>', 134 | options: { 135 | nodeArgs: ['--debug-brk'], 136 | env: { 137 | PORT: process.env.PORT || 9000 138 | }, 139 | callback: function (nodemon) { 140 | nodemon.on('log', function (event) { 141 | console.log(event.colour); 142 | }); 143 | 144 | // opens browser on initial server start 145 | nodemon.on('config:update', function () { 146 | setTimeout(function () { 147 | require('open')('http://localhost:8080/debug?port=5858'); 148 | }, 500); 149 | }); 150 | } 151 | } 152 | } 153 | }, 154 | 155 | buildcontrol: { 156 | options: { 157 | dir: '<%%= yeoman.dist %>', 158 | commit: true, 159 | push: true, 160 | connectCommits: false, 161 | message: 'Built %sourceName% from commit %sourceCommit% on branch %sourceBranch%' 162 | }, 163 | heroku: { 164 | options: { 165 | remote: 'heroku', 166 | branch: 'master' 167 | } 168 | }, 169 | openshift: { 170 | options: { 171 | remote: 'openshift', 172 | branch: 'master' 173 | } 174 | } 175 | }, 176 | // Test settings 177 | karma: { 178 | unit: { 179 | configFile: 'karma.conf.js', 180 | singleRun: true 181 | } 182 | }, 183 | 184 | mochaTest: { 185 | options: { 186 | reporter: 'spec', 187 | require: 'mocha.conf.js', 188 | timeout: 5000 // set default mocha spec timeout 189 | }, 190 | unit: { 191 | src: ['<%%= yeoman.server %>/**/*.spec.js'] 192 | }, 193 | integration: { 194 | src: ['<%%= yeoman.server %>/**/*.integration.js'] 195 | } 196 | }, 197 | 198 | mocha_istanbul: { 199 | unit: { 200 | options: { 201 | excludes: ['**/*.{spec,mock,integration}.js'], 202 | reporter: 'spec', 203 | require: ['mocha.conf.js'], 204 | mask: '**/*.spec.js', 205 | coverageFolder: 'coverage/server/unit' 206 | }, 207 | src: '<%%= yeoman.server %>' 208 | }, 209 | integration: { 210 | options: { 211 | excludes: ['**/*.{spec,mock,integration}.js'], 212 | reporter: 'spec', 213 | require: ['mocha.conf.js'], 214 | mask: '**/*.integration.js', 215 | coverageFolder: 'coverage/server/integration' 216 | }, 217 | src: '<%%= yeoman.server %>' 218 | } 219 | }, 220 | 221 | istanbul_check_coverage: { 222 | default: { 223 | options: { 224 | coverageFolder: 'coverage/**', 225 | check: { 226 | lines: 80, 227 | statements: 80, 228 | branches: 80, 229 | functions: 80 230 | } 231 | } 232 | } 233 | }, 234 | 235 | env: { 236 | test: { 237 | NODE_ENV: 'test' 238 | }, 239 | prod: { 240 | NODE_ENV: 'production' 241 | }, 242 | all: localConfig 243 | }, 244 | // Compiles ES6 to JavaScript using Babel 245 | babel: { 246 | options: { 247 | sourceMap: true 248 | }, 249 | server: { 250 | options: { 251 | optional: ['runtime'] 252 | }, 253 | files: [{ 254 | expand: true, 255 | cwd: '<%%= yeoman.server %>', 256 | src: ['**/*.{js,json}'], 257 | dest: '<%%= yeoman.dist %>/<%%= yeoman.server %>' 258 | }] 259 | } 260 | }, 261 | }); 262 | 263 | // Used for delaying livereload until after server has restarted 264 | grunt.registerTask('wait', function () { 265 | grunt.log.ok('Waiting for server reload...'); 266 | 267 | var done = this.async(); 268 | 269 | setTimeout(function () { 270 | grunt.log.writeln('Done waiting!'); 271 | done(); 272 | }, 1500); 273 | }); 274 | 275 | grunt.registerTask('express-keepalive', 'Keep grunt running', function() { 276 | this.async(); 277 | }); 278 | 279 | grunt.registerTask('serve', function (target) { 280 | if (target === 'dist') { 281 | return grunt.task.run(['build', 'env:all', 'env:prod', 'express:prod', 'wait', 'express-keepalive']); 282 | } 283 | 284 | if (target === 'debug') { 285 | return grunt.task.run([ 286 | 'clean:server', 287 | 'env:all', 288 | ]); 289 | } 290 | 291 | grunt.task.run([ 292 | 'clean:server', 293 | 'env:all', 294 | 'express:dev', 295 | 'wait', 296 | 'watch' 297 | ]); 298 | }); 299 | 300 | grunt.registerTask('server', function () { 301 | grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.'); 302 | grunt.task.run(['serve']); 303 | }); 304 | 305 | grunt.registerTask('test', function(target, option) { 306 | if (target === 'server') { 307 | return grunt.task.run([ 308 | 'env:all', 309 | 'env:test', 310 | 'mochaTest:unit', 311 | 'mochaTest:integration' 312 | ]); 313 | } 314 | 315 | else if (target === 'e2e') { 316 | 317 | if (option === 'prod') { 318 | return grunt.task.run([ 319 | 'build', 320 | 'env:all', 321 | 'env:prod', 322 | 'express:prod', 323 | 'protractor' 324 | ]); 325 | } 326 | 327 | else { 328 | return grunt.task.run([ 329 | 'clean:server', 330 | 'env:all', 331 | 'env:test', 332 | 'concurrent:pre', 333 | 'concurrent:test', 334 | 'express:dev', 335 | 'protractor' 336 | ]); 337 | } 338 | } 339 | 340 | else if (target === 'coverage') { 341 | 342 | if (option === 'unit') { 343 | return grunt.task.run([ 344 | 'env:all', 345 | 'env:test', 346 | 'mocha_istanbul:unit' 347 | ]); 348 | } 349 | 350 | else if (option === 'integration') { 351 | return grunt.task.run([ 352 | 'env:all', 353 | 'env:test', 354 | 'mocha_istanbul:integration' 355 | ]); 356 | } 357 | 358 | else if (option === 'check') { 359 | return grunt.task.run([ 360 | 'istanbul_check_coverage' 361 | ]); 362 | } 363 | 364 | else { 365 | return grunt.task.run([ 366 | 'env:all', 367 | 'env:test', 368 | 'mocha_istanbul', 369 | 'istanbul_check_coverage' 370 | ]); 371 | } 372 | 373 | } 374 | 375 | else grunt.task.run([ 376 | 'test:server', 377 | ]); 378 | }); 379 | 380 | grunt.registerTask('build', [ 381 | 'clean:dist', 382 | 'concurrent:pre', 383 | 'concurrent:dist', 384 | 'injector', 385 | 'wiredep:client', 386 | 'concat', 387 | 'copy:dist', 388 | 'babel:server', 389 | ]); 390 | 391 | grunt.registerTask('default', [ 392 | 'newer:jshint', 393 | 'test', 394 | 'build' 395 | ]); 396 | }; 397 | -------------------------------------------------------------------------------- /app/templates/README.md: -------------------------------------------------------------------------------- 1 | # <%= lodash.slugify(lodash.humanize(appname)) %> 2 | 3 | This project was generated with the [Express API Generator](https://github.com/ioneyed/generator-expressjs-api) version <%= rootGeneratorVersion() %>. 4 | 5 | ## Getting Started 6 | 7 | ### Prerequisites 8 | 9 | - [Git](https://git-scm.com/) 10 | - [Node.js and NPM](nodejs.org) >= v0.12.0 11 | - [Ruby](https://www.ruby-lang.org) and then `gem install sass`<% if(filters.grunt) { %> 12 | - [Grunt](http://gruntjs.com/) (`npm install --global grunt-cli`)<% } if(filters.gulp) { %> 13 | - [Gulp](http://gulpjs.com/) (`npm install --global gulp`)<% } if(filters.mongoose) { %> 14 | - [MongoDB](https://www.mongodb.org/) - Keep a running daemon with `mongod`<% } if(filters.sequelize) { %> 15 | - [SQLite](https://www.sqlite.org/quickstart.html)<% } %> 16 | 17 | ### Developing<% var i = 1; %> 18 | 19 | <%= i++ %>. Run `npm install` to install server dependencies. 20 | 21 | <% if(filters.mongoose) { %><%= i++ %>. Run `mongod` in a separate shell to keep an instance of the MongoDB Daemon running<% } %> 22 | 23 | <%= i++ %>. Run <% if(filters.grunt) { %>`grunt serve`<% } if(filters.grunt && filters.gulp) { %> or <% } if(filters.gulp) { %>`gulp serve`<% } %> to start the development server. It should automatically open the client in your browser when ready. 24 | 25 | ## Build & development 26 | 27 | Run `grunt build` for building and `grunt serve` for preview. 28 | 29 | ## Testing 30 | 31 | Running `npm test` will run the unit tests with karma. 32 | -------------------------------------------------------------------------------- /app/templates/_.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public 3 | .tmp 4 | .idea 5 | dist 6 | /server/config/local.env.js 7 | npm-debug.log 8 | -------------------------------------------------------------------------------- /app/templates/_package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= lodash.slugify(lodash.humanize(appname)) %>", 3 | "version": "0.0.0", 4 | "main": "server/app.js", 5 | "dependencies": { 6 | "express": "^4.13.3", 7 | "morgan": "~1.6.1", 8 | "body-parser": "^1.13.3", 9 | "method-override": "^2.3.5", 10 | "cookie-parser": "^1.3.5", 11 | "express-session": "^1.11.3", 12 | "errorhandler": "^1.4.2", 13 | "compression": "^1.5.2", 14 | "composable-middleware": "^0.3.0", 15 | "lodash": "^3.10.1", 16 | "lusca": "^1.3.0", 17 | "babel-runtime": "^5.8.20", 18 | "ejs": "^2.3.3",<% if (filters.mongoose) { %> 19 | "mongoose": "^4.1.2", 20 | "bluebird": "^2.9.34", 21 | "connect-mongo": "^0.8.1",<% } %><% if (filters.sequelize) { %> 22 | "sequelize": "^3.5.1", 23 | "sqlite3": "~3.1.0", 24 | "express-sequelize-session": "0.4.0",<% } %><% if (filters.auth) { %> 25 | "jsonwebtoken": "^5.0.0", 26 | "express-jwt": "^3.0.0", 27 | "passport": "~0.3.0", 28 | "passport-local": "^1.0.0",<% } %><% if (filters.facebookAuth) { %> 29 | "passport-facebook": "^2.0.0",<% } %><% if (filters.twitterAuth) { %> 30 | "passport-twitter": "^1.0.3",<% } %><% if (filters.googleAuth) { %> 31 | "passport-google-oauth": "~0.2.0",<% } %><% if (filters.socketio) { %> 32 | "socket.io": "^1.3.5", 33 | "socket.io-client": "^1.3.5", 34 | "socketio-jwt": "^4.2.0",<% } %> 35 | "serve-favicon": "^2.3.0" 36 | }, 37 | "devDependencies": { 38 | "autoprefixer": "^6.0.0", 39 | "babel-core": "^5.6.4", 40 | "grunt": "~0.4.5", 41 | "grunt-wiredep": "^2.0.0", 42 | "grunt-concurrent": "^2.0.1", 43 | "grunt-contrib-clean": "^0.6.0", 44 | "grunt-contrib-concat": "^0.5.1", 45 | "grunt-contrib-copy": "^0.8.0", 46 | "grunt-contrib-jshint": "~0.11.2", 47 | "grunt-contrib-uglify": "^0.9.1", 48 | "grunt-contrib-watch": "~0.6.1", 49 | <% if(filters.babel) { %>"karma-babel-preprocessor": "^5.2.1", <% } %> 50 | "grunt-babel": "~5.0.0", 51 | "grunt-google-cdn": "~0.4.0", 52 | "grunt-jscs": "^2.1.0", 53 | "grunt-newer": "^1.1.1", 54 | "grunt-filerev": "^2.3.1", 55 | "grunt-usemin": "^3.0.0", 56 | "grunt-env": "~0.4.1", 57 | "grunt-node-inspector": "^0.4.1", 58 | "grunt-nodemon": "^0.4.0", 59 | "grunt-dom-munger": "^3.4.0", 60 | "grunt-protractor-runner": "^2.0.0", 61 | "grunt-injector": "^0.6.0", 62 | "grunt-karma": "~0.12.0", 63 | "grunt-build-control": "^0.6.0", 64 | "jit-grunt": "^0.9.1", 65 | "time-grunt": "^1.2.1", 66 | "grunt-express-server": "^0.5.1", 67 | "grunt-open": "~0.2.3", 68 | "open": "~0.0.4", 69 | "jshint-stylish": "~2.0.1", 70 | "connect-livereload": "^0.5.3", 71 | "mocha": "^2.2.5", 72 | "grunt-mocha-test": "~0.12.7", 73 | "grunt-mocha-istanbul": "^3.0.1", 74 | "istanbul": "^0.3.17", 75 | "chai": "^3.2.0", 76 | "sinon": "^1.16.1", 77 | "chai-as-promised": "^5.1.0", 78 | "chai-things": "^0.2.0", 79 | "sinon-chai": "^2.8.0",<% if (filters.mocha) { %> 80 | "karma-mocha": "^0.2.0", 81 | "karma-chai-plugins": "^0.6.0",<% } if (filters.jasmine) { %> 82 | "jasmine-core": "^2.3.4", 83 | "karma-jasmine": "~0.3.0", 84 | "jasmine-spec-reporter": "^2.4.0",<% } %> 85 | "karma-ng-scenario": "~0.1.0", 86 | "karma-firefox-launcher": "~0.1.6", 87 | "karma-script-launcher": "~0.1.0", 88 | "karma-html2js-preprocessor": "~0.1.0", 89 | "karma-chrome-launcher": "~0.2.0", 90 | "requirejs": "~2.1.11", 91 | "karma-requirejs": "~0.2.2", 92 | "phantomjs": "^1.9.18", 93 | "karma-phantomjs-launcher": "~0.2.0", 94 | "karma": "~0.13.3", 95 | "karma-spec-reporter": "~0.0.20", 96 | "proxyquire": "^1.0.1", 97 | "supertest": "^1.1.0" 98 | }, 99 | "engines": { 100 | "node": ">=0.12.0" 101 | }, 102 | "scripts": { 103 | "start": "node server", 104 | "test": "grunt test", 105 | "update-webdriver": "node node_modules/grunt-protractor-runner/node_modules/protractor/bin/webdriver-manager update" 106 | }, 107 | "private": true 108 | } 109 | -------------------------------------------------------------------------------- /app/templates/mocha.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Register the Babel require hook 4 | require('babel-core/register'); 5 | 6 | var chai = require('chai'); 7 | 8 | // Load Chai assertions 9 | global.expect = chai.expect; 10 | global.assert = chai.assert; 11 | chai.should(); 12 | 13 | // Load Sinon 14 | global.sinon = require('sinon'); 15 | 16 | // Initialize Chai plugins 17 | chai.use(require('sinon-chai')); 18 | chai.use(require('chai-as-promised')); 19 | chai.use(require('chai-things')) 20 | -------------------------------------------------------------------------------- /app/templates/server/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "expr": true, 3 | "node": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "eqeqeq": true, 7 | "immed": true, 8 | "latedef": "nofunc", 9 | "newcap": true, 10 | "noarg": true, 11 | "undef": true, 12 | "smarttabs": true, 13 | "asi": true, 14 | "debug": true 15 | } 16 | -------------------------------------------------------------------------------- /app/templates/server/.jshintrc-spec: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ".jshintrc", 3 | "globals": {<% if (filters.jasmine) { %> 4 | "jasmine": true,<% } %> 5 | "describe": true, 6 | "it": true, 7 | "before": true, 8 | "beforeEach": true, 9 | "after": true, 10 | "afterEach": true, 11 | "expect": true, 12 | "assert": true, 13 | "sinon": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/templates/server/api/user(auth)/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import express from 'express'; 4 | import controller from './user.controller'; 5 | import auth from '../../auth/auth.service'; 6 | 7 | var router = express.Router(); 8 | 9 | router.get('/', auth.hasRole('admin'), controller.index); 10 | router.delete('/:id', auth.hasRole('admin'), controller.destroy); 11 | router.get('/me', auth.isAuthenticated(), controller.me); 12 | router.put('/:id/password', auth.isAuthenticated(), controller.changePassword); 13 | router.get('/:id', auth.isAuthenticated(), controller.show); 14 | router.post('/', controller.create); 15 | 16 | module.exports = router; 17 | -------------------------------------------------------------------------------- /app/templates/server/api/user(auth)/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var proxyquire = require('proxyquire').noPreserveCache(); 4 | 5 | var userCtrlStub = { 6 | index: 'userCtrl.index', 7 | destroy: 'userCtrl.destroy', 8 | me: 'userCtrl.me', 9 | changePassword: 'userCtrl.changePassword', 10 | show: 'userCtrl.show', 11 | create: 'userCtrl.create' 12 | }; 13 | 14 | var authServiceStub = { 15 | isAuthenticated: function() { 16 | return 'authService.isAuthenticated'; 17 | }, 18 | hasRole: function(role) { 19 | return 'authService.hasRole.' + role; 20 | } 21 | }; 22 | 23 | var routerStub = { 24 | get: sinon.spy(), 25 | put: sinon.spy(), 26 | post: sinon.spy(), 27 | delete: sinon.spy() 28 | }; 29 | 30 | // require the index with our stubbed out modules 31 | var userIndex = proxyquire('./index', { 32 | 'express': { 33 | Router: function() { 34 | return routerStub; 35 | } 36 | }, 37 | './user.controller': userCtrlStub, 38 | '../../auth/auth.service': authServiceStub 39 | }); 40 | 41 | describe('User API Router:', function() { 42 | 43 | it('should return an express router instance', function() { 44 | <%= expect() %>userIndex<%= to() %>.equal(routerStub); 45 | }); 46 | 47 | describe('GET /api/users', function() { 48 | 49 | it('should verify admin role and route to user.controller.index', function() { 50 | <%= expect() %>routerStub.get 51 | .withArgs('/', 'authService.hasRole.admin', 'userCtrl.index') 52 | <%= to() %>.have.been.calledOnce; 53 | }); 54 | 55 | }); 56 | 57 | describe('DELETE /api/users/:id', function() { 58 | 59 | it('should verify admin role and route to user.controller.destroy', function() { 60 | <%= expect() %>routerStub.delete 61 | .withArgs('/:id', 'authService.hasRole.admin', 'userCtrl.destroy') 62 | <%= to() %>.have.been.calledOnce; 63 | }); 64 | 65 | }); 66 | 67 | describe('GET /api/users/me', function() { 68 | 69 | it('should be authenticated and route to user.controller.me', function() { 70 | <%= expect() %>routerStub.get 71 | .withArgs('/me', 'authService.isAuthenticated', 'userCtrl.me') 72 | <%= to() %>.have.been.calledOnce; 73 | }); 74 | 75 | }); 76 | 77 | describe('PUT /api/users/:id/password', function() { 78 | 79 | it('should be authenticated and route to user.controller.changePassword', function() { 80 | <%= expect() %>routerStub.put 81 | .withArgs('/:id/password', 'authService.isAuthenticated', 'userCtrl.changePassword') 82 | <%= to() %>.have.been.calledOnce; 83 | }); 84 | 85 | }); 86 | 87 | describe('GET /api/users/:id', function() { 88 | 89 | it('should be authenticated and route to user.controller.show', function() { 90 | <%= expect() %>routerStub.get 91 | .withArgs('/:id', 'authService.isAuthenticated', 'userCtrl.show') 92 | <%= to() %>.have.been.calledOnce; 93 | }); 94 | 95 | }); 96 | 97 | describe('POST /api/users', function() { 98 | 99 | it('should route to user.controller.create', function() { 100 | <%= expect() %>routerStub.post 101 | .withArgs('/', 'userCtrl.create') 102 | <%= to() %>.have.been.calledOnce; 103 | }); 104 | 105 | }); 106 | 107 | }); 108 | -------------------------------------------------------------------------------- /app/templates/server/api/user(auth)/user.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | <% if (filters.mongooseModels) { %> 3 | import User from './user.model';<% } %><% if (filters.sequelizeModels) { %> 4 | import {User} from '../../sqldb';<% } %> 5 | import passport from 'passport'; 6 | import config from '../../config/environment'; 7 | import jwt from 'jsonwebtoken'; 8 | 9 | function validationError(res, statusCode) { 10 | statusCode = statusCode || 422; 11 | return function(err) { 12 | res.status(statusCode).json(err); 13 | } 14 | } 15 | 16 | function handleError(res, statusCode) { 17 | statusCode = statusCode || 500; 18 | return function(err) { 19 | res.status(statusCode).send(err); 20 | }; 21 | } 22 | 23 | function respondWith(res, statusCode) { 24 | statusCode = statusCode || 200; 25 | return function() { 26 | res.status(statusCode).end(); 27 | }; 28 | } 29 | 30 | /** 31 | * Get list of users 32 | * restriction: 'admin' 33 | */ 34 | exports.index = function(req, res) { 35 | <% if (filters.mongooseModels) { %>User.findAsync({}, '-salt -hashedPassword')<% } 36 | if (filters.sequelizeModels) { %>User.findAll({ 37 | attributes: [ 38 | 'id', 39 | 'name', 40 | 'email', 41 | 'role', 42 | 'provider' 43 | ] 44 | })<% } %> 45 | .then(function(users) { 46 | res.status(200).json(users); 47 | }) 48 | .catch(handleError(res)); 49 | }; 50 | 51 | /** 52 | * Creates a new user 53 | */ 54 | exports.create = function(req, res, next) { 55 | <% if (filters.mongooseModels) { %> 56 | var newUser = new User(req.body); 57 | newUser.provider = 'local'; 58 | newUser.role = 'user'; 59 | newUser.saveAsync()<% } 60 | if (filters.sequelizeModels) { %> 61 | var newUser = User.build(req.body); 62 | newUser.setDataValue('provider', 'local'); 63 | newUser.setDataValue('role', 'user'); 64 | newUser.save()<% } %> 65 | <% if (filters.mongooseModels) { %>.spread(function(user) {<% } 66 | if (filters.sequelizeModels) { %>.then(function(user) {<% } %> 67 | <% if (filters.mongooseModels) { %> 68 | var token = jwt.sign({ _id: user._id }, config.secrets.session, { 69 | expiresIn: 60 * 60 * 5 70 | }); 71 | <% } if (filters.sequelizeModels) { %> 72 | var token = jwt.sign({ id: user.id }, config.secrets.session, { 73 | expiresIn: 60 * 60 * 5 74 | }); 75 | <% } %> 76 | res.json({ token: token }); 77 | }) 78 | .catch(validationError(res)); 79 | }; 80 | 81 | /** 82 | * Get a single user 83 | */ 84 | exports.show = function(req, res, next) { 85 | var userId = req.params.id; 86 | 87 | <% if (filters.mongooseModels) { %>User.findByIdAsync(userId)<% } 88 | if (filters.sequelizeModels) { %>User.find({ 89 | where: { 90 | id: userId 91 | } 92 | })<% } %> 93 | .then(function(user) { 94 | if (!user) { 95 | return res.status(404).end(); 96 | } 97 | res.json(user.profile); 98 | }) 99 | .catch(function(err) { 100 | return next(err); 101 | }); 102 | }; 103 | 104 | /** 105 | * Deletes a user 106 | * restriction: 'admin' 107 | */ 108 | exports.destroy = function(req, res) { 109 | <% if (filters.mongooseModels) { %>User.findByIdAndRemoveAsync(req.params.id)<% } 110 | if (filters.sequelizeModels) { %>User.destroy({ id: req.params.id })<% } %> 111 | .then(function() { 112 | res.status(204).end(); 113 | }) 114 | .catch(handleError(res)); 115 | }; 116 | 117 | /** 118 | * Change a users password 119 | */ 120 | exports.changePassword = function(req, res, next) { 121 | <% if (filters.mongooseModels) { %> var userId = req.user._id; <% } %> 122 | <% if (filters.sequelizeModels){ %> var userId = req.user.id; <% } %> 123 | var oldPass = String(req.body.oldPassword); 124 | var newPass = String(req.body.newPassword); 125 | 126 | <% if (filters.mongooseModels) { %>User.findByIdAsync(userId)<% } 127 | if (filters.sequelizeModels) { %>User.find({ 128 | where: { 129 | id: userId 130 | } 131 | })<% } %> 132 | .then(function(user) { 133 | if (user.authenticate(oldPass)) { 134 | user.password = newPass; 135 | <% if (filters.mongooseModels) { %>return user.saveAsync()<% } 136 | if (filters.sequelizeModels) { %>return user.save()<% } %> 137 | .then(function() { 138 | res.status(204).end(); 139 | }) 140 | .catch(validationError(res)); 141 | } else { 142 | return res.status(403).end(); 143 | } 144 | }); 145 | }; 146 | 147 | /** 148 | * Get my info 149 | */ 150 | exports.me = function(req, res, next) { 151 | <% if (filters.mongooseModels) { %>var userId = req.user._id;<% } 152 | if(filters.sequelizeModels) { %> var userId = req.user.id; <% } %> 153 | 154 | <% if (filters.mongooseModels) { %>User.findOneAsync({ _id: userId }, '-salt -hashedPassword')<% } 155 | if (filters.sequelizeModels) { %>User.find({ 156 | where: { 157 | id: userId 158 | }, 159 | attributes: [ 160 | 'id', 161 | 'name', 162 | 'email', 163 | 'role', 164 | 'provider' 165 | ] 166 | })<% } %> 167 | .then(function(user) { // don't ever give out the password or salt 168 | if (!user) { 169 | return res.status(401).end(); 170 | } 171 | res.json(user); 172 | }) 173 | .catch(function(err) { 174 | return next(err); 175 | }); 176 | }; 177 | 178 | /** 179 | * Authentication callback 180 | */ 181 | exports.authCallback = function(req, res, next) { 182 | res.redirect('/'); 183 | }; 184 | -------------------------------------------------------------------------------- /app/templates/server/api/user(auth)/user.events.js: -------------------------------------------------------------------------------- 1 | /** 2 | * User model events 3 | */ 4 | 5 | 'use strict'; 6 | 7 | import {EventEmitter} from 'events';<% if (filters.mongooseModels) { %> 8 | import User from './user.model';<% } if (filters.sequelizeModels) { %> 9 | import {User} from '../../sqldb';<% } %> 10 | var UserEvents = new EventEmitter(); 11 | 12 | // Set max event listeners (0 == unlimited) 13 | UserEvents.setMaxListeners(0); 14 | 15 | // Model events<% if (filters.mongooseModels) { %> 16 | var events = { 17 | 'save': 'save', 18 | 'remove': 'remove' 19 | };<% } if (filters.sequelizeModels) { %> 20 | var events = { 21 | 'afterCreate': 'save', 22 | 'afterUpdate': 'save', 23 | 'afterDestroy': 'remove' 24 | };<% } %> 25 | 26 | // Register the event emitter to the model events 27 | for (var e in events) { 28 | var event = events[e];<% if (filters.mongooseModels) { %> 29 | User.schema.post(e, emitEvent(event));<% } if (filters.sequelizeModels) { %> 30 | User.hook(e, emitEvent(event));<% } %> 31 | } 32 | 33 | function emitEvent(event) { 34 | return function(doc<% if (filters.sequelizeModels) { %>, options, done<% } %>) { 35 | <% if(filters.mongooseModels) { %>UserEvents.emit(event + ':' + doc._id, doc);<% } 36 | if(filters.sequelizeModels) { %> UserEvents.emit(event + ':' + doc.id, doc); <% } %> 37 | UserEvents.emit(event, doc);<% if (filters.sequelizeModels) { %> 38 | done(null);<% } %> 39 | } 40 | } 41 | 42 | module.exports = UserEvents; 43 | -------------------------------------------------------------------------------- /app/templates/server/api/user(auth)/user.integration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import app from '../..';<% if (filters.mongooseModels) { %> 4 | import User from './user.model';<% } %><% if (filters.sequelizeModels) { %> 5 | import {User} from '../../sqldb';<% } %> 6 | import request from 'supertest'; 7 | 8 | describe('User API:', function() { 9 | var user; 10 | 11 | // Clear users before testing 12 | before(function() { 13 | return <% if (filters.mongooseModels) { %>User.removeAsync().then(function() {<% } 14 | if (filters.sequelizeModels) { %>User.destroy({ where: {} }).then(function() {<% } %> 15 | <% if (filters.mongooseModels) { %>user = new User({<% } 16 | if (filters.sequelizeModels) { %>user = User.build({<% } %> 17 | name: 'Fake User', 18 | email: 'test@example.com', 19 | password: 'password' 20 | }); 21 | 22 | return <% if (filters.mongooseModels) { %>user.saveAsync();<% } 23 | if (filters.sequelizeModels) { %>user.save();<% } %> 24 | }); 25 | }); 26 | 27 | // Clear users after testing 28 | after(function() { 29 | <% if (filters.mongooseModels) { %>return User.removeAsync();<% } 30 | if (filters.sequelizeModels) { %>return User.destroy({ where: {} });<% } %> 31 | }); 32 | 33 | describe('GET /api/users/me', function() { 34 | var token; 35 | 36 | before(function(done) { 37 | request(app) 38 | .post('/auth/local') 39 | .send({ 40 | email: 'test@example.com', 41 | password: 'password' 42 | }) 43 | .expect(200) 44 | .expect('Content-Type', /json/) 45 | .end(function(err, res) { 46 | token = res.body.token; 47 | done(); 48 | }); 49 | }); 50 | 51 | it('should respond with a user profile when authenticated', function(done) { 52 | request(app) 53 | .get('/api/users/me') 54 | .set('authorization', 'Bearer ' + token) 55 | .expect(200) 56 | .expect('Content-Type', /json/) 57 | .end(function(err, res) { 58 | <%= expect() %> <% if (filters.mongooseModels) { %>res.body._id.toString() <% } if (filters.sequelizeModels) { %> res.body.id.toString() <% } %> <%= to() %>.equal(<% if(filters.mongooseModels) { %> user._id.toString() <% } if (filters.sequelizeModels) { %> user.id.toString() <% } %>); 59 | done(); 60 | }); 61 | }); 62 | 63 | it('should respond with a 401 when not authenticated', function(done) { 64 | request(app) 65 | .get('/api/users/me') 66 | .expect(401) 67 | .end(done); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /app/templates/server/api/user(auth)/user.model(mongooseModels).js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import crypto from 'crypto'; 4 | var mongoose = require('bluebird').promisifyAll(require('mongoose')); 5 | var Schema = mongoose.Schema;<% if (filters.oauth) { %> 6 | var authTypes = ['github', 'twitter', 'facebook', 'google'];<% } %> 7 | 8 | var UserSchema = new Schema({ 9 | name: String, 10 | email: { 11 | type: String, 12 | lowercase: true 13 | }, 14 | role: { 15 | type: String, 16 | default: 'user' 17 | }, 18 | password: String, 19 | provider: String, 20 | salt: String<% if (filters.oauth) { %>,<% if (filters.facebookAuth) { %> 21 | facebook: {},<% } %><% if (filters.twitterAuth) { %> 22 | twitter: {},<% } %><% if (filters.googleAuth) { %> 23 | google: {},<% } %> 24 | github: {}<% } %> 25 | }); 26 | 27 | /** 28 | * Virtuals 29 | */ 30 | 31 | // Public profile information 32 | UserSchema 33 | .virtual('profile') 34 | .get(function() { 35 | return { 36 | 'name': this.name, 37 | 'role': this.role 38 | }; 39 | }); 40 | 41 | // Non-sensitive info we'll be putting in the token 42 | UserSchema 43 | .virtual('token') 44 | .get(function() { 45 | return { 46 | <% if (filters.mongooseModels) { %>'_id': this._id, <% } %> 47 | 'role': this.role 48 | }; 49 | }); 50 | 51 | /** 52 | * Validations 53 | */ 54 | 55 | // Validate empty email 56 | UserSchema 57 | .path('email') 58 | .validate(function(email) {<% if (filters.oauth) { %> 59 | if (authTypes.indexOf(this.provider) !== -1) { 60 | return true; 61 | }<% } %> 62 | return email.length; 63 | }, 'Email cannot be blank'); 64 | 65 | // Validate empty password 66 | UserSchema 67 | .path('password') 68 | .validate(function(password) {<% if (filters.oauth) { %> 69 | if (authTypes.indexOf(this.provider) !== -1) { 70 | return true; 71 | }<% } %> 72 | return password.length; 73 | }, 'Password cannot be blank'); 74 | 75 | // Validate email is not taken 76 | UserSchema 77 | .path('email') 78 | .validate(function(value, respond) { 79 | var self = this; 80 | return this.constructor.findOneAsync({ email: value }) 81 | .then(function(user) { 82 | if (user) { 83 | if (self.id === user.id) { 84 | return respond(true); 85 | } 86 | return respond(false); 87 | } 88 | return respond(true); 89 | }) 90 | .catch(function(err) { 91 | throw err; 92 | }); 93 | }, 'The specified email address is already in use.'); 94 | 95 | var validatePresenceOf = function(value) { 96 | return value && value.length; 97 | }; 98 | 99 | /** 100 | * Pre-save hook 101 | */ 102 | UserSchema 103 | .pre('save', function(next) { 104 | // Handle new/update passwords 105 | if (this.isModified('password')) { 106 | if (!validatePresenceOf(this.password)<% if (filters.oauth) { %> && authTypes.indexOf(this.provider) === -1<% } %>) { 107 | next(new Error('Invalid password')); 108 | } 109 | 110 | // Make salt with a callback 111 | var _this = this; 112 | this.makeSalt(function(saltErr, salt) { 113 | if (saltErr) { 114 | next(saltErr); 115 | } 116 | _this.salt = salt; 117 | _this.encryptPassword(_this.password, function(encryptErr, hashedPassword) { 118 | if (encryptErr) { 119 | next(encryptErr); 120 | } 121 | _this.password = hashedPassword; 122 | next(); 123 | }); 124 | }); 125 | } else { 126 | next(); 127 | } 128 | }); 129 | 130 | /** 131 | * Methods 132 | */ 133 | UserSchema.methods = { 134 | /** 135 | * Authenticate - check if the passwords are the same 136 | * 137 | * @param {String} password 138 | * @param {Function} callback 139 | * @return {Boolean} 140 | * @api public 141 | */ 142 | authenticate: function(password, callback) { 143 | if (!callback) { 144 | return this.password === this.encryptPassword(password); 145 | } 146 | 147 | var _this = this; 148 | this.encryptPassword(password, function(err, pwdGen) { 149 | if (err) { 150 | callback(err); 151 | } 152 | 153 | if (_this.password === pwdGen) { 154 | callback(null, true); 155 | } 156 | else { 157 | callback(null, false); 158 | } 159 | }); 160 | }, 161 | 162 | /** 163 | * Make salt 164 | * 165 | * @param {Number} byteSize Optional salt byte size, default to 16 166 | * @param {Function} callback 167 | * @return {String} 168 | * @api public 169 | */ 170 | makeSalt: function(byteSize, callback) { 171 | var defaultByteSize = 16; 172 | 173 | if (typeof arguments[0] === 'function') { 174 | callback = arguments[0]; 175 | byteSize = defaultByteSize; 176 | } 177 | else if (typeof arguments[1] === 'function') { 178 | callback = arguments[1]; 179 | } 180 | 181 | if (!byteSize) { 182 | byteSize = defaultByteSize; 183 | } 184 | 185 | if (!callback) { 186 | return crypto.randomBytes(byteSize).toString('base64'); 187 | } 188 | 189 | return crypto.randomBytes(byteSize, function(err, salt) { 190 | if (err) { 191 | callback(err); 192 | } 193 | return callback(null, salt.toString('base64')); 194 | }); 195 | }, 196 | 197 | /** 198 | * Encrypt password 199 | * 200 | * @param {String} password 201 | * @param {Function} callback 202 | * @return {String} 203 | * @api public 204 | */ 205 | encryptPassword: function(password, callback) { 206 | if (!password || !this.salt) { 207 | return null; 208 | } 209 | 210 | var defaultIterations = 10000; 211 | var defaultKeyLength = 64; 212 | var salt = new Buffer(this.salt, 'base64'); 213 | 214 | if (!callback) { 215 | return crypto.pbkdf2Sync(password, salt, defaultIterations, defaultKeyLength) 216 | .toString('base64'); 217 | } 218 | 219 | return crypto.pbkdf2(password, salt, defaultIterations, defaultKeyLength, function(err, key) { 220 | if (err) { 221 | callback(err); 222 | } 223 | return callback(null, key.toString('base64')); 224 | }); 225 | } 226 | }; 227 | 228 | module.exports = mongoose.model('User', UserSchema); 229 | -------------------------------------------------------------------------------- /app/templates/server/api/user(auth)/user.model(sequelizeModels).js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import crypto from 'crypto';<% if (filters.oauth) { %> 4 | var authTypes = ['github', 'twitter', 'facebook', 'google'];<% } %> 5 | 6 | var validatePresenceOf = function(value) { 7 | return value && value.length; 8 | }; 9 | 10 | module.exports = function(sequelize, DataTypes) { 11 | var User = sequelize.define('User', { 12 | 13 | id: { 14 | type: <% if(filters.sequelizeModels.serial) { %> DataTypes.INTEGER <% } if (filters.sequelizeModels.uuid) { %> DataTypes.UUID <% } %>, 15 | allowNull: false, 16 | primaryKey: true, 17 | <% if(filters.sequelizeModels.serial) { %> autoIncrement: true <% } %><% if(filters.sequelizeModels.uuid) { %> defaultValue: DataTypes.UUIDV4 <% } %> 18 | }, 19 | name: DataTypes.STRING, 20 | email: { 21 | type: DataTypes.STRING, 22 | unique: { 23 | msg: 'The specified email address is already in use.' 24 | }, 25 | validate: { 26 | isEmail: true 27 | } 28 | }, 29 | role: { 30 | type: DataTypes.STRING, 31 | defaultValue: 'user' 32 | }, 33 | password: { 34 | type: DataTypes.STRING, 35 | validate: { 36 | notEmpty: true 37 | } 38 | }, 39 | provider: DataTypes.STRING, 40 | salt: DataTypes.STRING<% if (filters.oauth) { %>,<% if (filters.facebookAuth) { %> 41 | facebook: DataTypes.TEXT,<% } %><% if (filters.twitterAuth) { %> 42 | twitter: DataTypes.TEXT,<% } %><% if (filters.googleAuth) { %> 43 | google: DataTypes.TEXT,<% } %> 44 | github: DataTypes.TEXT<% } %> 45 | 46 | }, {<% if (filters.sequelizeModels.paranoid) { %> 47 | timestamps: true, 48 | paranoid: true,<% } %><% if (filters.sequelizeModels) { %> 49 | underscored: true, 50 | freezeTableName:true, 51 | tableName:'user<% if (filters.sequelizeModels.pluralization) { %>s<% } %>',<% } %> 52 | /** 53 | * Virtual Getters 54 | */ 55 | getterMethods: { 56 | // Public profile information 57 | profile: function() { 58 | return { 59 | 'name': this.name, 60 | 'role': this.role 61 | }; 62 | }, 63 | 64 | // Non-sensitive info we'll be putting in the token 65 | token: function() { 66 | return { 67 | 'id': this.id, 68 | 'role': this.role 69 | }; 70 | } 71 | }, 72 | 73 | /** 74 | * Pre-save hooks 75 | */ 76 | hooks: { 77 | beforeBulkCreate: function(users, fields, fn) { 78 | var totalUpdated = 0; 79 | users.forEach(function(user) { 80 | user.updatePassword(function(err) { 81 | if (err) { 82 | return fn(err); 83 | } 84 | totalUpdated += 1; 85 | if (totalUpdated === users.length) { 86 | return fn(); 87 | } 88 | }); 89 | }); 90 | }, 91 | beforeCreate: function(user, fields, fn) { 92 | user.updatePassword(fn); 93 | }, 94 | beforeUpdate: function(user, fields, fn) { 95 | if (user.changed('password')) { 96 | return user.updatePassword(fn); 97 | } 98 | fn(); 99 | } 100 | }, 101 | 102 | /** 103 | * Instance Methods 104 | */ 105 | instanceMethods: { 106 | /** 107 | * Authenticate - check if the passwords are the same 108 | * 109 | * @param {String} password 110 | * @param {Function} callback 111 | * @return {Boolean} 112 | * @api public 113 | */ 114 | authenticate: function(password, callback) { 115 | if (!callback) { 116 | return this.password === this.encryptPassword(password); 117 | } 118 | 119 | var _this = this; 120 | this.encryptPassword(password, function(err, pwdGen) { 121 | if (err) { 122 | callback(err); 123 | } 124 | 125 | if (_this.password === pwdGen) { 126 | callback(null, true); 127 | } 128 | else { 129 | callback(null, false); 130 | } 131 | }); 132 | }, 133 | 134 | /** 135 | * Make salt 136 | * 137 | * @param {Number} byteSize Optional salt byte size, default to 16 138 | * @param {Function} callback 139 | * @return {String} 140 | * @api public 141 | */ 142 | makeSalt: function(byteSize, callback) { 143 | var defaultByteSize = 16; 144 | 145 | if (typeof arguments[0] === 'function') { 146 | callback = arguments[0]; 147 | byteSize = defaultByteSize; 148 | } 149 | else if (typeof arguments[1] === 'function') { 150 | callback = arguments[1]; 151 | } 152 | 153 | if (!byteSize) { 154 | byteSize = defaultByteSize; 155 | } 156 | 157 | if (!callback) { 158 | return crypto.randomBytes(byteSize).toString('base64'); 159 | } 160 | 161 | return crypto.randomBytes(byteSize, function(err, salt) { 162 | if (err) { 163 | callback(err); 164 | } 165 | return callback(null, salt.toString('base64')); 166 | }); 167 | }, 168 | 169 | /** 170 | * Encrypt password 171 | * 172 | * @param {String} password 173 | * @param {Function} callback 174 | * @return {String} 175 | * @api public 176 | */ 177 | encryptPassword: function(password, callback) { 178 | if (!password || !this.salt) { 179 | if (!callback) { 180 | return null; 181 | } 182 | return callback(null); 183 | } 184 | 185 | var defaultIterations = 10000; 186 | var defaultKeyLength = 64; 187 | var salt = new Buffer(this.salt, 'base64'); 188 | 189 | if (!callback) { 190 | return crypto.pbkdf2Sync(password, salt, defaultIterations, defaultKeyLength) 191 | .toString('base64'); 192 | } 193 | 194 | return crypto.pbkdf2(password, salt, defaultIterations, defaultKeyLength, 195 | function(err, key) { 196 | if (err) { 197 | callback(err); 198 | } 199 | return callback(null, key.toString('base64')); 200 | }); 201 | }, 202 | 203 | /** 204 | * Update password field 205 | * 206 | * @param {Function} fn 207 | * @return {String} 208 | * @api public 209 | */ 210 | updatePassword: function(fn) { 211 | // Handle new/update passwords 212 | if (this.password) { 213 | if (!validatePresenceOf(this.password)<% if (filters.oauth) { %> && authTypes.indexOf(this.provider) === -1<% } %>) { 214 | fn(new Error('Invalid password')); 215 | } 216 | 217 | // Make salt with a callback 218 | var _this = this; 219 | this.makeSalt(function(saltErr, salt) { 220 | if (saltErr) { 221 | fn(saltErr); 222 | } 223 | _this.salt = salt; 224 | _this.encryptPassword(_this.password, function(encryptErr, hashedPassword) { 225 | if (encryptErr) { 226 | fn(encryptErr); 227 | } 228 | _this.password = hashedPassword; 229 | fn(null); 230 | }); 231 | }); 232 | } else { 233 | fn(null); 234 | } 235 | } 236 | } 237 | }); 238 | 239 | return User; 240 | }; 241 | -------------------------------------------------------------------------------- /app/templates/server/api/user(auth)/user.model.spec(mongooseModels).js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import app from '../..'; 4 | import User from './user.model'; 5 | var user; 6 | var genUser = function() { 7 | user = new User({ 8 | provider: 'local', 9 | name: 'Fake User', 10 | email: 'test@example.com', 11 | password: 'password' 12 | }); 13 | return user; 14 | }; 15 | 16 | describe('User Model', function() { 17 | before(function() { 18 | // Clear users before testing 19 | return User.removeAsync(); 20 | }); 21 | 22 | beforeEach(function() { 23 | genUser(); 24 | }); 25 | 26 | afterEach(function() { 27 | return User.removeAsync(); 28 | }); 29 | 30 | it('should begin with no users', function() { 31 | return <%= expect() %>User.findAsync({})<%= to() %> 32 | .eventually.have.length(0); 33 | }); 34 | 35 | it('should fail when saving a duplicate user', function() { 36 | return <%= expect() %>user.saveAsync() 37 | .then(function() { 38 | var userDup = genUser(); 39 | return userDup.saveAsync(); 40 | })<%= to() %>.be.rejected; 41 | }); 42 | 43 | describe('#email', function() { 44 | it('should fail when saving without an email', function() { 45 | user.email = ''; 46 | return <%= expect() %>user.saveAsync()<%= to() %>.be.rejected; 47 | }); 48 | }); 49 | 50 | describe('#password', function() { 51 | beforeEach(function() { 52 | return user.saveAsync(); 53 | }); 54 | 55 | it('should authenticate user if valid', function() { 56 | <%= expect() %>user.authenticate('password')<%= to() %>.be.true; 57 | }); 58 | 59 | it('should not authenticate user if invalid', function() { 60 | <%= expect() %>user.authenticate('blah')<%= to() %>.not.be.true; 61 | }); 62 | 63 | it('should remain the same hash unless the password is updated', function() { 64 | user.name = 'Test User'; 65 | return <%= expect() %>user.saveAsync() 66 | .spread(function(u) { 67 | return u.authenticate('password'); 68 | })<%= to() %>.eventually.be.true; 69 | }); 70 | }); 71 | 72 | }); 73 | -------------------------------------------------------------------------------- /app/templates/server/api/user(auth)/user.model.spec(sequelizeModels).js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import app from '../..'; 4 | import {User} from '../../sqldb'; 5 | var user; 6 | var genUser = function() { 7 | user = User.build({ 8 | provider: 'local', 9 | name: 'Fake User', 10 | email: 'test@example.com', 11 | password: 'password' 12 | }); 13 | return user; 14 | }; 15 | 16 | describe('User Model', function() { 17 | before(function() { 18 | // Sync and clear users before testing 19 | return User.sync().then(function() { 20 | return User.destroy({ where: {} }); 21 | }); 22 | }); 23 | 24 | beforeEach(function() { 25 | genUser(); 26 | }); 27 | 28 | afterEach(function() { 29 | return User.destroy({ where: {} }); 30 | }); 31 | 32 | it('should begin with no users', function() { 33 | return <%= expect() %>User.findAll()<%= to() %> 34 | .eventually.have.length(0); 35 | }); 36 | 37 | it('should fail when saving a duplicate user', function() { 38 | return <%= expect() %>user.save() 39 | .then(function() { 40 | var userDup = genUser(); 41 | return userDup.save(); 42 | })<%= to() %>.be.rejected; 43 | }); 44 | 45 | describe('#email', function() { 46 | it('should fail when saving without an email', function() { 47 | user.email = ''; 48 | return <%= expect() %>user.save()<%= to() %>.be.rejected; 49 | }); 50 | }); 51 | 52 | describe('#password', function() { 53 | beforeEach(function() { 54 | return user.save(); 55 | }); 56 | 57 | it('should authenticate user if valid', function() { 58 | <%= expect() %>user.authenticate('password')<%= to() %>.be.true; 59 | }); 60 | 61 | it('should not authenticate user if invalid', function() { 62 | <%= expect() %>user.authenticate('blah')<%= to() %>.not.be.true; 63 | }); 64 | 65 | it('should remain the same hash unless the password is updated', function() { 66 | user.name = 'Test User'; 67 | return <%= expect() %>user.save() 68 | .then(function(u) { 69 | return u.authenticate('password'); 70 | })<%= to() %>.eventually.be.true; 71 | }); 72 | }); 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /app/templates/server/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Main application file 3 | */ 4 | 5 | 'use strict'; 6 | 7 | import express from 'express';<% if (filters.mongoose) { %> 8 | import mongoose from 'mongoose';<% } %><% if (filters.sequelize) { %> 9 | import sqldb from './sqldb';<% } %> 10 | import config from './config/environment'; 11 | import http from 'http'; 12 | <% if (filters.mongoose) { %> 13 | // Connect to MongoDB 14 | mongoose.connect(config.mongo.uri, config.mongo.options); 15 | mongoose.connection.on('error', function(err) { 16 | console.error('MongoDB connection error: ' + err); 17 | process.exit(-1); 18 | }); 19 | <% } %><% if (filters.models) { %> 20 | // Populate databases with sample data 21 | if (config.seedDB) { require('./config/seed'); } 22 | <% } %> 23 | // Setup server 24 | var app = express(); 25 | var server = http.createServer(app);<% if (filters.socketio) { %> 26 | var socketio = require('socket.io')(server, { 27 | serveClient: config.env !== 'production', 28 | path: '/socket.io-client' 29 | }); 30 | require('./config/socketio')(socketio);<% } %> 31 | require('./config/express')(app); 32 | require('./routes')(app); 33 | 34 | // Start server 35 | function startServer() { 36 | server.listen(config.port, config.ip, function() { 37 | console.log('Express server listening on %d, in %s mode', config.port, app.get('env')); 38 | }); 39 | } 40 | <% if (filters.sequelize) { %> 41 | sqldb.sequelize.sync() 42 | .then(startServer) 43 | .catch(function(err) { 44 | console.log('Server failed to start due to error: %s', err); 45 | }); 46 | <% } else { %> 47 | setImmediate(startServer); 48 | <% } %> 49 | // Expose app 50 | exports = module.exports = app; 51 | -------------------------------------------------------------------------------- /app/templates/server/auth(auth)/auth.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import passport from 'passport'; 4 | import config from '../config/environment'; 5 | import jwt from 'jsonwebtoken'; 6 | import expressJwt from 'express-jwt'; 7 | import compose from 'composable-middleware';<% if (filters.mongooseModels) { %> 8 | import User from '../api/user/user.model';<% } %><% if (filters.sequelizeModels) { %> 9 | import {User} from'../sqldb';<% } %> 10 | var validateJwt = expressJwt({ 11 | secret: config.secrets.session 12 | }); 13 | 14 | /** 15 | * Attaches the user object to the request if authenticated 16 | * Otherwise returns 403 17 | */ 18 | function isAuthenticated() { 19 | return compose() 20 | // Validate jwt 21 | .use(function(req, res, next) { 22 | // allow access_token to be passed through query parameter as well 23 | if (req.query && req.query.hasOwnProperty('access_token')) { 24 | req.headers.authorization = 'Bearer ' + req.query.access_token; 25 | } 26 | validateJwt(req, res, next); 27 | }) 28 | // Attach user to request 29 | .use(function(req, res, next) { 30 | <% if (filters.mongooseModels) { %>User.findByIdAsync(req.user._id)<% } 31 | if (filters.sequelizeModels) { %>User.find({ 32 | where: { 33 | id: req.user.id 34 | } 35 | })<% } %> 36 | .then(function(user) { 37 | if (!user) { 38 | return res.status(401).end(); 39 | } 40 | req.user = user; 41 | next(); 42 | }) 43 | .catch(function(err) { 44 | return next(err); 45 | }); 46 | }); 47 | } 48 | 49 | /** 50 | * Checks if the user role meets the minimum requirements of the route 51 | */ 52 | function hasRole(roleRequired) { 53 | if (!roleRequired) { 54 | throw new Error('Required role needs to be set'); 55 | } 56 | 57 | return compose() 58 | .use(isAuthenticated()) 59 | .use(function meetsRequirements(req, res, next) { 60 | if (config.userRoles.indexOf(req.user.role) >= 61 | config.userRoles.indexOf(roleRequired)) { 62 | next(); 63 | } 64 | else { 65 | res.status(403).send('Forbidden'); 66 | } 67 | }); 68 | } 69 | 70 | /** 71 | * Returns a jwt token signed by the app secret 72 | */ 73 | function signToken(id, role) { 74 | <% if(filters.mongooseModels) { %> 75 | return jwt.sign({ _id: id, role: role }, config.secrets.session, { 76 | expiresIn: 60 * 60 * 5 77 | }); 78 | <% } if (filters.sequelizeModels) { %> 79 | return jwt.sign({ id: id, role: role }, config.secrets.session, { 80 | expiresIn: 60 * 60 * 5 81 | }); <% }%> 82 | } 83 | 84 | /** 85 | * Set token cookie directly for oAuth strategies 86 | */ 87 | function setTokenCookie(req, res) { 88 | if (!req.user) { 89 | return res.status(404).send('Something went wrong, please try again.'); 90 | } 91 | <% if (filters.mongooseModels) { %>var token = signToken(req.user._id, req.user.role); <% } %> 92 | <% if (filters.sequelizeModels) { %>var token = signToken(req.user.id, req.user.role); <% } %> 93 | res.cookie('token', token); 94 | res.redirect('/'); 95 | } 96 | 97 | exports.isAuthenticated = isAuthenticated; 98 | exports.hasRole = hasRole; 99 | exports.signToken = signToken; 100 | exports.setTokenCookie = setTokenCookie; 101 | -------------------------------------------------------------------------------- /app/templates/server/auth(auth)/facebook(facebookAuth)/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import express from 'express'; 4 | import passport from 'passport'; 5 | import auth from '../auth.service'; 6 | 7 | var router = express.Router(); 8 | 9 | router 10 | .get('/', passport.authenticate('facebook', { 11 | scope: ['email', 'user_about_me'], 12 | failureRedirect: '/signup', 13 | session: false 14 | })) 15 | 16 | .get('/callback', passport.authenticate('facebook', { 17 | failureRedirect: '/signup', 18 | session: false 19 | }), auth.setTokenCookie); 20 | 21 | module.exports = router; 22 | -------------------------------------------------------------------------------- /app/templates/server/auth(auth)/facebook(facebookAuth)/passport.js: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import {Strategy as FacebookStrategy} from 'passport-facebook'; 3 | 4 | exports.setup = function(User, config) { 5 | passport.use(new FacebookStrategy({ 6 | clientID: config.facebook.clientID, 7 | clientSecret: config.facebook.clientSecret, 8 | callbackURL: config.facebook.callbackURL, 9 | profileFields: [ 10 | 'displayName', 11 | 'emails' 12 | ] 13 | }, 14 | function(accessToken, refreshToken, profile, done) { 15 | <% if (filters.mongooseModels) { %>User.findOneAsync({<% } 16 | if (filters.sequelizeModels) { %>User.find({<% } %> 17 | 'facebook.id': profile.id 18 | }) 19 | .then(function(user) { 20 | if (!user) { 21 | <% if (filters.mongooseModels) { %>user = new User({<% } 22 | if (filters.sequelizeModels) { %>user = User.build({<% } %> 23 | name: profile.displayName, 24 | email: profile.emails[0].value, 25 | role: 'user', 26 | provider: 'facebook', 27 | facebook: profile._json 28 | }); 29 | <% if (filters.mongooseModels) { %>user.saveAsync()<% } 30 | if (filters.sequelizeModels) { %>user.save()<% } %> 31 | .then(function(user) { 32 | return done(null, user); 33 | }) 34 | .catch(function(err) { 35 | return done(err); 36 | }); 37 | } else { 38 | return done(null, user); 39 | } 40 | }) 41 | .catch(function(err) { 42 | return done(err); 43 | }); 44 | })); 45 | }; 46 | -------------------------------------------------------------------------------- /app/templates/server/auth(auth)/google(googleAuth)/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import express from 'express'; 4 | import passport from 'passport'; 5 | import auth from '../auth.service'; 6 | 7 | var router = express.Router(); 8 | 9 | router 10 | .get('/', passport.authenticate('google', { 11 | failureRedirect: '/signup', 12 | scope: [ 13 | 'profile', 14 | 'email' 15 | ], 16 | session: false 17 | })) 18 | 19 | .get('/callback', passport.authenticate('google', { 20 | failureRedirect: '/signup', 21 | session: false 22 | }), auth.setTokenCookie); 23 | 24 | module.exports = router; 25 | -------------------------------------------------------------------------------- /app/templates/server/auth(auth)/google(googleAuth)/passport.js: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import {OAuth2Strategy as GoogleStrategy} from 'passport-google-oauth'; 3 | 4 | exports.setup = function(User, config) { 5 | passport.use(new GoogleStrategy({ 6 | clientID: config.google.clientID, 7 | clientSecret: config.google.clientSecret, 8 | callbackURL: config.google.callbackURL 9 | }, 10 | function(accessToken, refreshToken, profile, done) { 11 | <% if (filters.mongooseModels) { %>User.findOneAsync({<% } 12 | if (filters.sequelizeModels) { %>User.find({<% } %> 13 | 'google.id': profile.id 14 | }) 15 | .then(function(user) { 16 | if (!user) { 17 | <% if (filters.mongooseModels) { %>user = new User({<% } 18 | if (filters.sequelizeModels) { %>user = User.build({<% } %> 19 | name: profile.displayName, 20 | email: profile.emails[0].value, 21 | role: 'user', 22 | username: profile.emails[0].value.split('@')[0], 23 | provider: 'google', 24 | google: profile._json 25 | }); 26 | <% if (filters.mongooseModels) { %>user.saveAsync()<% } 27 | if (filters.sequelizeModels) { %>user.save()<% } %> 28 | .then(function(user) { 29 | return done(null, user); 30 | }) 31 | .catch(function(err) { 32 | return done(err); 33 | }); 34 | } else { 35 | return done(null, user); 36 | } 37 | }) 38 | .catch(function(err) { 39 | return done(err); 40 | }); 41 | })); 42 | }; 43 | -------------------------------------------------------------------------------- /app/templates/server/auth(auth)/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import express from 'express'; 4 | import passport from 'passport'; 5 | import config from '../config/environment';<% if (filters.mongooseModels) { %> 6 | import User from '../api/user/user.model';<% } %><% if (filters.sequelizeModels) { %> 7 | import {User} from '../sqldb';<% } %> 8 | 9 | // Passport Configuration 10 | require('./local/passport').setup(User, config);<% if (filters.facebookAuth) { %> 11 | require('./facebook/passport').setup(User, config);<% } %><% if (filters.googleAuth) { %> 12 | require('./google/passport').setup(User, config);<% } %><% if (filters.twitterAuth) { %> 13 | require('./twitter/passport').setup(User, config);<% } %> 14 | 15 | var router = express.Router(); 16 | 17 | router.use('/local', require('./local'));<% if (filters.facebookAuth) { %> 18 | router.use('/facebook', require('./facebook'));<% } %><% if (filters.twitterAuth) { %> 19 | router.use('/twitter', require('./twitter'));<% } %><% if (filters.googleAuth) { %> 20 | router.use('/google', require('./google'));<% } %> 21 | 22 | module.exports = router; 23 | -------------------------------------------------------------------------------- /app/templates/server/auth(auth)/local/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import express from 'express'; 4 | import passport from 'passport'; 5 | import auth from '../auth.service'; 6 | 7 | var router = express.Router(); 8 | 9 | router.post('/', function(req, res, next) { 10 | passport.authenticate('local', function(err, user, info) { 11 | var error = err || info; 12 | if (error) { 13 | return res.status(401).json(error); 14 | } 15 | if (!user) { 16 | return res.status(404).json({message: 'Something went wrong, please try again.'}); 17 | } 18 | 19 | <% if (filters.mongooseModels) { %>var token = auth.signToken(user._id, user.role);<% } %> 20 | <% if (filters.sequelizeModels) { %>var token = auth.signToken(user.id, user.role);<% } %> 21 | res.json({ token: token }); 22 | })(req, res, next) 23 | }); 24 | 25 | module.exports = router; 26 | -------------------------------------------------------------------------------- /app/templates/server/auth(auth)/local/passport.js: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import {Strategy as LocalStrategy} from 'passport-local'; 3 | 4 | function localAuthenticate(User, email, password, done) { 5 | <% if (filters.mongooseModels) { %>User.findOneAsync({ 6 | email: email.toLowerCase() 7 | })<% } 8 | if (filters.sequelizeModels) { %>User.find({ 9 | where: { 10 | email: email.toLowerCase() 11 | } 12 | })<% } %> 13 | .then(function(user) { 14 | if (!user) { 15 | return done(null, false, { 16 | message: 'This email is not registered.' 17 | }); 18 | } 19 | user.authenticate(password, function(authError, authenticated) { 20 | if (authError) { 21 | return done(authError); 22 | } 23 | if (!authenticated) { 24 | return done(null, false, { 25 | message: 'This password is not correct.' 26 | }); 27 | } else { 28 | return done(null, user); 29 | } 30 | }); 31 | }) 32 | .catch(function(err) { 33 | return done(err); 34 | }); 35 | } 36 | 37 | exports.setup = function(User, config) { 38 | passport.use(new LocalStrategy({ 39 | usernameField: 'email', 40 | passwordField: 'password' // this is the virtual field on the model 41 | }, function(email, password, done) {<% if (filters.models) { %> 42 | return localAuthenticate(User, email, password, done); 43 | <% } %> })); 44 | }; 45 | -------------------------------------------------------------------------------- /app/templates/server/auth(auth)/twitter(twitterAuth)/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import express from 'express'; 4 | import passport from 'passport'; 5 | import auth from '../auth.service'; 6 | 7 | var router = express.Router(); 8 | 9 | router 10 | .get('/', passport.authenticate('twitter', { 11 | failureRedirect: '/signup', 12 | session: false 13 | })) 14 | 15 | .get('/callback', passport.authenticate('twitter', { 16 | failureRedirect: '/signup', 17 | session: false 18 | }), auth.setTokenCookie); 19 | 20 | module.exports = router; 21 | -------------------------------------------------------------------------------- /app/templates/server/auth(auth)/twitter(twitterAuth)/passport.js: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import {Strategy as TwitterStrategy} from 'passport-twitter'; 3 | 4 | exports.setup = function(User, config) { 5 | passport.use(new TwitterStrategy({ 6 | consumerKey: config.twitter.clientID, 7 | consumerSecret: config.twitter.clientSecret, 8 | callbackURL: config.twitter.callbackURL 9 | }, 10 | function(token, tokenSecret, profile, done) { 11 | <% if (filters.mongooseModels) { %>User.findOneAsync({<% } 12 | if (filters.sequelizeModels) { %>User.find({<% } %> 13 | 'twitter.id_str': profile.id 14 | }) 15 | .then(function(user) { 16 | if (!user) { 17 | <% if (filters.mongooseModels) { %>user = new User({<% } 18 | if (filters.sequelizeModels) { %>user = User.build({<% } %> 19 | name: profile.displayName, 20 | username: profile.username, 21 | role: 'user', 22 | provider: 'twitter', 23 | twitter: profile._json 24 | }); 25 | <% if (filters.mongooseModels) { %>user.saveAsync()<% } 26 | if (filters.sequelizeModels) { %>user.save()<% } %> 27 | .then(function(user) { 28 | return done(null, user); 29 | }) 30 | .catch(function(err) { 31 | return done(err); 32 | }); 33 | } else { 34 | return done(null, user); 35 | } 36 | }) 37 | .catch(function(err) { 38 | return done(err); 39 | }); 40 | })); 41 | }; 42 | -------------------------------------------------------------------------------- /app/templates/server/components/errors/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Error responses 3 | */ 4 | 5 | 'use strict'; 6 | 7 | module.exports[404] = function pageNotFound(req, res) { 8 | var viewFilePath = '404'; 9 | var statusCode = 404; 10 | var result = { 11 | status: statusCode 12 | }; 13 | 14 | res.status(result.status); 15 | res.render(viewFilePath, {}, function(err, html) { 16 | if (err) { 17 | return res.status(result.status).json(result); 18 | } 19 | 20 | res.send(html); 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /app/templates/server/config/_local.env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Use local.env.js for environment variables that grunt will set when the server starts locally. 4 | // Use for your api keys, secrets, etc. This file should not be tracked by git. 5 | // 6 | // You will need to set these on the server you deploy to. 7 | 8 | module.exports = { 9 | DOMAIN: 'http://localhost:9000', 10 | SESSION_SECRET: '<%= lodash.slugify(appname) + "-secret" %>',<% if (filters.facebookAuth) { %> 11 | 12 | FACEBOOK_ID: 'app-id', 13 | FACEBOOK_SECRET: 'secret',<% } if (filters.twitterAuth) { %> 14 | 15 | TWITTER_ID: 'app-id', 16 | TWITTER_SECRET: 'secret',<% } if (filters.googleAuth) { %> 17 | 18 | GOOGLE_ID: 'app-id', 19 | GOOGLE_SECRET: 'secret', 20 | <% } %> 21 | // Control debug level for modules using visionmedia/debug 22 | DEBUG: '' 23 | }; 24 | -------------------------------------------------------------------------------- /app/templates/server/config/_local.env.sample.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Use local.env.js for environment variables that grunt will set when the server starts locally. 4 | // Use for your api keys, secrets, etc. This file should not be tracked by git. 5 | // 6 | // You will need to set these on the server you deploy to. 7 | 8 | module.exports = { 9 | DOMAIN: 'http://localhost:9000', 10 | SESSION_SECRET: '<%= lodash.slugify(appname) + "-secret" %>',<% if (filters.facebookAuth) { %> 11 | 12 | FACEBOOK_ID: 'app-id', 13 | FACEBOOK_SECRET: 'secret',<% } if (filters.twitterAuth) { %> 14 | 15 | TWITTER_ID: 'app-id', 16 | TWITTER_SECRET: 'secret',<% } if (filters.googleAuth) { %> 17 | 18 | GOOGLE_ID: 'app-id', 19 | GOOGLE_SECRET: 'secret',<% } %> 20 | 21 | // Control debug level for modules using visionmedia/debug 22 | DEBUG: '' 23 | }; 24 | -------------------------------------------------------------------------------- /app/templates/server/config/environment/development.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Development specific configuration 4 | // ================================== 5 | module.exports = {<% if (filters.mongoose) { %> 6 | 7 | // MongoDB connection options 8 | mongo: { 9 | uri: 'mongodb://localhost/<%= lodash.slugify(appname) %>-dev' 10 | },<% } if (filters.sequelize) { %> 11 | 12 | // Sequelize connecton opions 13 | sequelize: { 14 | uri: 'sqlite://', 15 | options: { 16 | logging: false, 17 | storage: 'dev.sqlite', 18 | define: { 19 | timestamps: false 20 | } 21 | } 22 | },<% } %> 23 | 24 | // Seed database on startup 25 | seedDB: true 26 | 27 | }; 28 | -------------------------------------------------------------------------------- /app/templates/server/config/environment/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var _ = require('lodash'); 5 | 6 | function requiredProcessEnv(name) { 7 | if (!process.env[name]) { 8 | throw new Error('You must set the ' + name + ' environment variable'); 9 | } 10 | return process.env[name]; 11 | } 12 | 13 | // All configurations will extend these options 14 | // ============================================ 15 | var all = { 16 | env: process.env.NODE_ENV, 17 | 18 | // Root path of server 19 | root: path.normalize(__dirname + '/../../..'), 20 | 21 | // Server port 22 | port: process.env.PORT || 9000, 23 | 24 | // Server IP 25 | ip: process.env.IP || '0.0.0.0', 26 | 27 | // Should we populate the DB with sample data? 28 | seedDB: false, 29 | 30 | // Secret for session, you will want to change this and make it an environment variable 31 | secrets: { 32 | session: '<%= lodash.slugify(lodash.humanize(appname)) + '-secret' %>' 33 | }, 34 | 35 | // MongoDB connection options 36 | mongo: { 37 | options: { 38 | db: { 39 | safe: true 40 | } 41 | } 42 | }<% if (filters.facebookAuth) { %>, 43 | 44 | facebook: { 45 | clientID: process.env.FACEBOOK_ID || 'id', 46 | clientSecret: process.env.FACEBOOK_SECRET || 'secret', 47 | callbackURL: (process.env.DOMAIN || '') + '/auth/facebook/callback' 48 | }<% } %><% if (filters.twitterAuth) { %>, 49 | 50 | twitter: { 51 | clientID: process.env.TWITTER_ID || 'id', 52 | clientSecret: process.env.TWITTER_SECRET || 'secret', 53 | callbackURL: (process.env.DOMAIN || '') + '/auth/twitter/callback' 54 | }<% } %><% if (filters.googleAuth) { %>, 55 | 56 | google: { 57 | clientID: process.env.GOOGLE_ID || 'id', 58 | clientSecret: process.env.GOOGLE_SECRET || 'secret', 59 | callbackURL: (process.env.DOMAIN || '') + '/auth/google/callback' 60 | }<% } %> 61 | }; 62 | 63 | // Export the config object based on the NODE_ENV 64 | // ============================================== 65 | module.exports = _.merge( 66 | all, 67 | require('./shared'), 68 | require('./' + process.env.NODE_ENV + '.js') || {}); 69 | -------------------------------------------------------------------------------- /app/templates/server/config/environment/production.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Production specific configuration 4 | // ================================= 5 | module.exports = { 6 | // Server IP 7 | ip: process.env.OPENSHIFT_NODEJS_IP || 8 | process.env.IP || 9 | undefined, 10 | 11 | // Server port 12 | port: process.env.OPENSHIFT_NODEJS_PORT || 13 | process.env.PORT || 14 | 8080<% if (filters.mongoose) { %>, 15 | 16 | // MongoDB connection options 17 | mongo: { 18 | uri: process.env.MONGOLAB_URI || 19 | process.env.MONGOHQ_URL || 20 | process.env.OPENSHIFT_MONGODB_DB_URL + 21 | process.env.OPENSHIFT_APP_NAME || 22 | 'mongodb://localhost/<%= lodash.slugify(appname) %>' 23 | }<% } if (filters.sequelize) { %>, 24 | 25 | sequelize: { 26 | uri: process.env.SEQUELIZE_URI || 27 | 'sqlite://', 28 | options: { 29 | logging: false, 30 | storage: 'dist.sqlite', 31 | define: { 32 | timestamps: false 33 | } 34 | } 35 | }<% } %> 36 | }; 37 | -------------------------------------------------------------------------------- /app/templates/server/config/environment/shared.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports = module.exports = { 4 | // List of user roles 5 | userRoles: ['guest', 'user', 'admin'] 6 | }; 7 | -------------------------------------------------------------------------------- /app/templates/server/config/environment/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Test specific configuration 4 | // =========================== 5 | module.exports = { 6 | // MongoDB connection options 7 | mongo: { 8 | uri: 'mongodb://localhost/<%= lodash.slugify(appname) %>-test' 9 | }, 10 | sequelize: { 11 | uri: 'sqlite://', 12 | options: { 13 | logging: false, 14 | storage: 'test.sqlite', 15 | define: { 16 | timestamps: false 17 | } 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /app/templates/server/config/express.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Express configuration 3 | */ 4 | 5 | 'use strict'; 6 | 7 | import express from 'express'; 8 | import favicon from 'serve-favicon'; 9 | import morgan from 'morgan'; 10 | import compression from 'compression'; 11 | import bodyParser from 'body-parser'; 12 | import methodOverride from 'method-override'; 13 | import cookieParser from 'cookie-parser'; 14 | import errorHandler from 'errorhandler'; 15 | import path from 'path'; 16 | import lusca from 'lusca'; 17 | import config from './environment';<% if (filters.auth) { %> 18 | import passport from 'passport';<% } %> 19 | import session from 'express-session';<% if (filters.mongoose) { %> 20 | import connectMongo from 'connect-mongo'; 21 | import mongoose from 'mongoose'; 22 | var mongoStore = connectMongo(session);<% } else if(filters.sequelize) { %> 23 | import sqldb from '../sqldb'; 24 | import expressSequelizeSession from 'express-sequelize-session'; 25 | var Store = expressSequelizeSession(session.Store);<% } %> 26 | 27 | module.exports = function(app) { 28 | var env = app.get('env'); 29 | 30 | app.engine('html', require('ejs').renderFile); 31 | app.set('view engine', 'html'); 32 | app.use(compression()); 33 | app.use(bodyParser.urlencoded({ extended: false })); 34 | app.use(bodyParser.json()); 35 | app.use(methodOverride()); 36 | app.use(cookieParser());<% if (filters.auth) { %> 37 | app.use(passport.initialize());<% } %> 38 | 39 | // Persist sessions with mongoStore / sequelizeStore 40 | // We need to enable sessions for passport-twitter because it's an 41 | // oauth 1.0 strategy, and Lusca depends on sessions 42 | app.use(session({ 43 | secret: config.secrets.session, 44 | saveUninitialized: true, 45 | resave: false<% if (filters.mongoose) { %>, 46 | store: new mongoStore({ 47 | mongooseConnection: mongoose.connection, 48 | db: '<%= lodash.slugify(lodash.humanize(appname)) %>' 49 | })<% } else if(filters.sequelize) { %>, 50 | store: new Store(sqldb.sequelize)<% } %> 51 | })); 52 | 53 | /** 54 | * Lusca - express server security 55 | * https://github.com/krakenjs/lusca 56 | */ 57 | if ('test' !== env) { 58 | app.use(lusca({ 59 | csrf: { 60 | angular: true 61 | }, 62 | xframe: 'SAMEORIGIN', 63 | hsts: { 64 | maxAge: 31536000, //1 year, in seconds 65 | includeSubDomains: true, 66 | preload: true 67 | }, 68 | xssProtection: true 69 | })); 70 | } 71 | 72 | app.set('appPath', path.join(config.root, 'client')); 73 | 74 | if ('production' === env) { 75 | app.use(favicon(path.join(config.root, 'client', 'favicon.ico'))); 76 | app.use(express.static(app.get('appPath'))); 77 | app.use(morgan('dev')); 78 | } 79 | 80 | if ('development' === env) { 81 | app.use(require('connect-livereload')()); 82 | } 83 | 84 | if ('development' === env || 'test' === env) { 85 | app.use(express.static(path.join(config.root, '.tmp'))); 86 | app.use(express.static(app.get('appPath'))); 87 | app.use(morgan('dev')); 88 | app.use(errorHandler()); // Error handler - has to be last 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /app/templates/server/config/seed(models).js: -------------------------------------------------------------------------------- 1 | /** 2 | * Populate DB with sample data on server start 3 | * to disable, edit config/environment/index.js, and set `seedDB: false` 4 | */ 5 | 6 | 'use strict';<% if (filters.mongooseModels) { %> 7 | import Thing from '../api/thing/thing.model';<% if (filters.auth) { %> 8 | import User from '../api/user/user.model';<% } %><% } %><% if (filters.sequelizeModels) { %> 9 | import sqldb from '../sqldb'; 10 | var Thing = sqldb.Thing;<% if (filters.auth) { %> 11 | var User = sqldb.User;<% } %><% } %> 12 | 13 | <% if (filters.mongooseModels) { %>Thing.find({}).removeAsync()<% } 14 | if (filters.sequelizeModels) { %>Thing.sync() 15 | .then(function() { 16 | return Thing.destroy({ where: {} }); 17 | })<% } %> 18 | .then(function() { 19 | <% if (filters.mongooseModels) { %>Thing.create({<% } 20 | if (filters.sequelizeModels) { %>Thing.bulkCreate([{<% } %> 21 | name: 'Development Tools', 22 | info: 'Integration with popular tools such as Grunt, Babel, Karma, ' + 23 | 'Mocha, JSHint, Node Inspector, Livereload, Protractor, Jade, ' + 24 | 'Stylus, Sass, and Less.' 25 | }, { 26 | name: 'Server and Client integration', 27 | info: 'Built with a powerful and fun stack: MongoDB, Express, ' + 28 | 'AngularJS, and Node.' 29 | }, { 30 | name: 'Smart Build System', 31 | info: 'Build system ignores `spec` files, allowing you to keep ' + 32 | 'tests alongside code. Automatic injection of scripts and ' + 33 | 'styles into your index.html' 34 | }, { 35 | name: 'Modular Structure', 36 | info: 'Best practice client and server structures allow for more ' + 37 | 'code reusability and maximum scalability' 38 | }, { 39 | name: 'Optimized Build', 40 | info: 'Build process packs up your templates as a single JavaScript ' + 41 | 'payload, minifies your scripts/css/images, and rewrites asset ' + 42 | 'names for caching.' 43 | }, { 44 | name: 'Deployment Ready', 45 | info: 'Easily deploy your app to Heroku or Openshift with the heroku ' + 46 | 'and openshift subgenerators' 47 | <% if (filters.mongooseModels) { %>});<% } 48 | if (filters.sequelizeModels) { %>}]);<% } %> 49 | }); 50 | <% if (filters.auth) { %> 51 | <% if (filters.mongooseModels) { %>User.find({}).removeAsync()<% } 52 | if (filters.sequelizeModels) { %>User.sync() 53 | .then(function() { 54 | return User.destroy({ where: {} }); 55 | })<% } %> 56 | .then(function() { 57 | <% if (filters.mongooseModels) { %>User.createAsync({<% } 58 | if (filters.sequelizeModels) { %>User.bulkCreate([{<% } %> 59 | provider: 'local', 60 | name: 'Test User', 61 | email: 'test@example.com', 62 | password: 'test' 63 | }, { 64 | provider: 'local', 65 | role: 'admin', 66 | name: 'Admin', 67 | email: 'admin@example.com', 68 | password: 'admin' 69 | <% if (filters.mongooseModels) { %>})<% } 70 | if (filters.sequelizeModels) { %>}])<% } %> 71 | .then(function() { 72 | console.log('finished populating users'); 73 | }); 74 | });<% } %> 75 | -------------------------------------------------------------------------------- /app/templates/server/config/socketio(socketio).js: -------------------------------------------------------------------------------- 1 | /** 2 | * Socket.io configuration 3 | */ 4 | 5 | 'use strict'; 6 | 7 | import config from './environment'; 8 | 9 | // When the user disconnects.. perform this 10 | function onDisconnect(socket) { 11 | } 12 | 13 | // When the user connects.. perform this 14 | function onConnect(socket) { 15 | // When the client emits 'info', this listens and executes 16 | socket.on('info', function(data) { 17 | socket.log(JSON.stringify(data, null, 2)); 18 | }); 19 | 20 | // Insert sockets below 21 | 22 | } 23 | 24 | module.exports = function(socketio) { 25 | // socket.io (v1.x.x) is powered by debug. 26 | // In order to see all the debug output, set DEBUG (in server/config/local.env.js) to including the desired scope. 27 | // 28 | // ex: DEBUG: "http*,socket.io:socket" 29 | 30 | // We can authenticate socket.io users and access their token through socket.decoded_token 31 | // 32 | // 1. You will need to send the token in `client/components/socket/socket.service.js` 33 | // 34 | // 2. Require authentication here: 35 | // socketio.use(require('socketio-jwt').authorize({ 36 | // secret: config.secrets.session, 37 | // handshake: true 38 | // })); 39 | 40 | socketio.on('connection', function(socket) { 41 | socket.address = socket.request.connection.remoteAddress + 42 | ':' + socket.request.connection.remotePort; 43 | 44 | socket.connectedAt = new Date(); 45 | 46 | socket.log = function(...data) { 47 | console.log(`SocketIO ${socket.nsp.name} [${socket.address}]`, ...data); 48 | }; 49 | 50 | // Call onDisconnect. 51 | socket.on('disconnect', function() { 52 | onDisconnect(socket); 53 | socket.log('DISCONNECTED'); 54 | }); 55 | 56 | // Call onConnect. 57 | onConnect(socket); 58 | socket.log('CONNECTED'); 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /app/templates/server/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Set default node environment to development 4 | var env = process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 5 | 6 | if (env === 'development' || env === 'test') { 7 | // Register the Babel require hook 8 | require('babel-core/register'); 9 | } 10 | 11 | // Export the application 12 | exports = module.exports = require('./app'); 13 | -------------------------------------------------------------------------------- /app/templates/server/routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Main application routes 3 | */ 4 | 5 | 'use strict'; 6 | 7 | import errors from './components/errors'; 8 | import path from 'path'; 9 | 10 | module.exports = function(app) { 11 | 12 | // Insert routes below 13 | <% if (filters.auth) { %> 14 | app.use('/api/users', require('./api/user')); 15 | app.use('/auth', require('./auth')); 16 | <% } %> 17 | app.use('/api/things', require('./api/thing')); 18 | // All undefined asset or api routes should return a 404 19 | app.route('/:url(api|auth)/*') 20 | .get(errors[404]); 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /app/templates/server/sqldb(sequelize)/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sequelize initialization module 3 | */ 4 | 5 | 'use strict'; 6 | 7 | import path from 'path'; 8 | import config from '../config/environment'; 9 | import Sequelize from 'sequelize'; 10 | 11 | var db = { 12 | Sequelize: Sequelize, 13 | sequelize: new Sequelize(config.sequelize.uri, config.sequelize.options) 14 | }; 15 | 16 | // Insert models below 17 | <% if (filters.sequelizeModels && filters.auth) { %>db.User = db.sequelize.import('../api/user/user.model');<% } %> 18 | db.Thing = db.sequelize.import('../api/thing/thing.model'); 19 | 20 | module.exports = db; 21 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /endpoint/generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import path from 'path'; 4 | import {NamedBase} from 'yeoman-generator'; 5 | import {genNamedBase} from '../generator-base'; 6 | 7 | export default class Generator extends NamedBase { 8 | 9 | constructor(...args) { 10 | super(...args); 11 | 12 | this.option('route', { 13 | desc: 'URL for the endpoint', 14 | type: String 15 | }); 16 | 17 | this.option('models', { 18 | desc: 'Specify which model(s) to use', 19 | type: String 20 | }); 21 | 22 | this.option('endpointDirectory', { 23 | desc: 'Parent directory for enpoints', 24 | type: String 25 | }); 26 | } 27 | 28 | initializing() { 29 | // init shared generator properies and methods 30 | genNamedBase(this); 31 | } 32 | 33 | prompting() { 34 | var done = this.async(); 35 | var promptCb = function (props) { 36 | if(props.route.charAt(0) !== '/') { 37 | props.route = '/' + props.route; 38 | } 39 | 40 | this.route = props.route; 41 | 42 | if (props.models) { 43 | delete this.filters.mongoose; 44 | delete this.filters.mongooseModels; 45 | delete this.filters.sequelize; 46 | delete this.filters.sequelizeModels; 47 | 48 | this.filters[props.models] = true; 49 | this.filters[props.models + 'Models'] = props[props.models+'Models'] || {}; 50 | } 51 | done(); 52 | }.bind(this); 53 | 54 | if (this.options.route) { 55 | if (this.filters.mongoose && this.filters.sequelize) { 56 | if (this.options.models) { 57 | return promptCb(this.options); 58 | } 59 | } else { 60 | if (this.filters.mongooseModels) { this.options.models = 'mongoose'; } 61 | else if (this.filters.sequelizeModels) { this.options.models = 'sequelize'; } 62 | else { delete this.options.models; } 63 | return promptCb(this.options); 64 | } 65 | } 66 | 67 | var name = this.name; 68 | 69 | var base = this.config.get('routesBase') || '/api/'; 70 | if(base.charAt(base.length-1) !== '/') { 71 | base = base + '/'; 72 | } 73 | 74 | // pluralization defaults to true for backwards compat 75 | if (this.config.get('pluralizeRoutes') !== false) { 76 | name = name + 's'; 77 | } 78 | 79 | var self = this; 80 | var prompts = [ 81 | { 82 | name: 'route', 83 | message: 'What will the url of your endpoint be?', 84 | default: base + name 85 | }, 86 | { 87 | type: 'list', 88 | name: 'models', 89 | message: 'What would you like to use for the endpoint\'s models?', 90 | choices: [ 'Mongoose', 'Sequelize' ], 91 | default: self.filters.sequelizeModels ? 1 : 0, 92 | filter: function( val ) { 93 | return val.toLowerCase(); 94 | }, 95 | when: function() { 96 | return self.filters.mongoose && self.filters.sequelize; 97 | } 98 | } 99 | ]; 100 | 101 | this.prompt(prompts, promptCb); 102 | } 103 | 104 | configuring() { 105 | this.routeDest = path.join(this.options.endpointDirectory || 106 | this.config.get('endpointDirectory') || 'server/api/', this.name); 107 | } 108 | 109 | writing() { 110 | this.sourceRoot(path.join(__dirname, './templates')); 111 | this.processDirectory('.', this.routeDest); 112 | } 113 | 114 | end() { 115 | if(this.config.get('insertRoutes')) { 116 | var routesFile = this.config.get('registerRoutesFile'); 117 | var reqPath = this.relativeRequire(this.routeDest, routesFile); 118 | var routeConfig = { 119 | file: routesFile, 120 | needle: this.config.get('routesNeedle'), 121 | splicable: [ 122 | "app.use(\'" + this.route +"\', require(\'" + reqPath + "\'));" 123 | ] 124 | }; 125 | this.rewriteFile(routeConfig); 126 | } 127 | 128 | if (this.filters.socketio && this.config.get('insertSockets')) { 129 | var socketsFile = this.config.get('registerSocketsFile'); 130 | var reqPath = this.relativeRequire(this.routeDest + '/' + this.basename + 131 | '.socket', socketsFile); 132 | var socketConfig = { 133 | file: socketsFile, 134 | needle: this.config.get('socketsNeedle'), 135 | splicable: [ 136 | "require(\'" + reqPath + "\').register(socket);" 137 | ] 138 | }; 139 | this.rewriteFile(socketConfig); 140 | } 141 | 142 | if (this.filters.sequelize && this.config.get('insertModels')) { 143 | var modelsFile = this.config.get('registerModelsFile'); 144 | var reqPath = this.relativeRequire(this.routeDest + '/' + this.basename + 145 | '.model', modelsFile); 146 | var modelConfig = { 147 | file: modelsFile, 148 | needle: this.config.get('modelsNeedle'), 149 | splicable: [ 150 | "db." + this.classedName + " = db.sequelize.import(\'" + reqPath +"\');" 151 | ] 152 | }; 153 | this.rewriteFile(modelConfig); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /endpoint/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Register the Babel require hook 4 | require('babel-core/register')({ 5 | only: /generator-expressjs-api\/(?!node_modules)/ 6 | }); 7 | 8 | // Export the generator 9 | exports = module.exports = require('./generator'); 10 | -------------------------------------------------------------------------------- /endpoint/templates/basename.controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Using Rails-like standard naming convention for endpoints. 3 | * GET <%= route %> -> index<% if (filters.models) { %> 4 | * POST <%= route %> -> create 5 | * GET <%= route %>/:id -> show 6 | * PUT <%= route %>/:id -> update 7 | * DELETE <%= route %>/:id -> destroy<% } %> 8 | */ 9 | 10 | 'use strict';<% if (filters.models) { %> 11 | 12 | var _ = require('lodash');<% if (filters.mongooseModels) { %> 13 | var <%= classedName %> = require('./<%= basename %>.model');<% } if (filters.sequelizeModels) { %> 14 | var sqldb = require('<%= relativeRequire(config.get('registerModelsFile')) %>'); 15 | var <%= classedName %> = sqldb.<%= classedName %>;<% } %> 16 | 17 | function handleError(res, statusCode) { 18 | statusCode = statusCode || 500; 19 | return function(err) { 20 | res.status(statusCode).send(err); 21 | }; 22 | } 23 | 24 | function responseWithResult(res, statusCode) { 25 | statusCode = statusCode || 200; 26 | return function(entity) { 27 | if (entity) { 28 | res.status(statusCode).json(entity); 29 | } 30 | }; 31 | } 32 | 33 | function handleEntityNotFound(res) { 34 | return function(entity) { 35 | if (!entity) { 36 | res.status(404).end(); 37 | return null; 38 | } 39 | return entity; 40 | }; 41 | } 42 | 43 | function saveUpdates(updates) { 44 | return function(entity) { 45 | <% if (filters.mongooseModels) { %>var updated = _.merge(entity, updates); 46 | return updated.saveAsync() 47 | .spread(function(updated) {<% } 48 | if (filters.sequelizeModels) { %>return entity.updateAttributes(updates) 49 | .then(function(updated) {<% } %> 50 | return updated; 51 | }); 52 | }; 53 | } 54 | 55 | function removeEntity(res) { 56 | return function(entity) { 57 | if (entity) { 58 | <% if (filters.mongooseModels) { %>return entity.removeAsync()<% } 59 | if (filters.sequelizeModels) { %>return entity.destroy()<% } %> 60 | .then(function() { 61 | res.status(204).end(); 62 | }); 63 | } 64 | }; 65 | }<% } %> 66 | 67 | // Gets a list of <%= classedName %>s 68 | exports.index = function(req, res) {<% if (!filters.models) { %> 69 | res.json([]);<% } else { %> 70 | <% if (filters.mongooseModels) { %><%= classedName %>.findAsync()<% } 71 | if (filters.sequelizeModels) { %><%= classedName %>.findAll()<% } %> 72 | .then(responseWithResult(res)) 73 | .catch(handleError(res));<% } %> 74 | };<% if (filters.models) { %> 75 | 76 | // Gets a single <%= classedName %> from the DB 77 | exports.show = function(req, res) { 78 | <% if (filters.mongooseModels) { %><%= classedName %>.findByIdAsync(req.params.id)<% } 79 | if (filters.sequelizeModels) { %><%= classedName %>.find({ 80 | where: { 81 | id: req.params.id 82 | } 83 | })<% } %> 84 | .then(handleEntityNotFound(res)) 85 | .then(responseWithResult(res)) 86 | .catch(handleError(res)); 87 | }; 88 | 89 | // Creates a new <%= classedName %> in the DB 90 | exports.create = function(req, res) { 91 | <% if (filters.mongooseModels) { %><%= classedName %>.createAsync(req.body)<% } 92 | if (filters.sequelizeModels) { %><%= classedName %>.create(req.body)<% } %> 93 | .then(responseWithResult(res, 201)) 94 | .catch(handleError(res)); 95 | }; 96 | 97 | // Updates an existing <%= classedName %> in the DB 98 | exports.update = function(req, res) { 99 | <% if (filters.mongooseModels) { %> 100 | if (req.body._id) { 101 | delete req.body._id; 102 | } 103 | <% } if (filters.sequelizeModels) { %> 104 | if(req.body.id){ 105 | delete req.body.id; 106 | }<% } %> 107 | <% if (filters.mongooseModels) { %><%= classedName %>.findByIdAsync(req.params.id)<% } 108 | if (filters.sequelizeModels) { %><%= classedName %>.find({ 109 | where: { 110 | id: req.params.id 111 | } 112 | })<% } %> 113 | .then(handleEntityNotFound(res)) 114 | .then(saveUpdates(req.body)) 115 | .then(responseWithResult(res)) 116 | .catch(handleError(res)); 117 | }; 118 | 119 | // Deletes a <%= classedName %> from the DB 120 | exports.destroy = function(req, res) { 121 | <% if (filters.mongooseModels) { %><%= classedName %>.findByIdAsync(req.params.id)<% } 122 | if (filters.sequelizeModels) { %><%= classedName %>.find({ 123 | where: { 124 | id: req.params.id 125 | } 126 | })<% } %> 127 | .then(handleEntityNotFound(res)) 128 | .then(removeEntity(res)) 129 | .catch(handleError(res)); 130 | };<% } %> 131 | -------------------------------------------------------------------------------- /endpoint/templates/basename.events(models).js: -------------------------------------------------------------------------------- 1 | /** 2 | * <%= classedName %> model events 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var EventEmitter = require('events').EventEmitter;<% if (filters.mongooseModels) { %> 8 | var <%= classedName %> = require('./<%= basename %>.model');<% } if (filters.sequelizeModels) { %> 9 | var <%= classedName %> = require('<%= relativeRequire(config.get('registerModelsFile')) %>').<%= classedName %>;<% } %> 10 | var <%= classedName %>Events = new EventEmitter(); 11 | 12 | // Set max event listeners (0 == unlimited) 13 | <%= classedName %>Events.setMaxListeners(0); 14 | 15 | // Model events<% if (filters.mongooseModels) { %> 16 | var events = { 17 | 'save': 'save', 18 | 'remove': 'remove' 19 | };<% } if (filters.sequelizeModels) { %> 20 | var events = { 21 | 'afterCreate': 'save', 22 | 'afterUpdate': 'save', 23 | 'afterDestroy': 'remove' 24 | };<% } %> 25 | 26 | // Register the event emitter to the model events 27 | for (var e in events) { 28 | var event = events[e];<% if (filters.mongooseModels) { %> 29 | <%= classedName %>.schema.post(e, emitEvent(event));<% } if (filters.sequelizeModels) { %> 30 | <%= classedName %>.hook(e, emitEvent(event));<% } %> 31 | } 32 | 33 | function emitEvent(event) { 34 | return function(doc<% if (filters.sequelizeModels) { %>, options, done<% } %>) { 35 | <% if(filters.mongooseModels) { %><%= classedName %>Events.emit(event + ':' + doc._id, doc);<% } %> 36 | <% if(filters.sequelizeModels) { %><%= classedName %>Events.emit(event + ':' + doc.id, doc);<% } %> 37 | <%= classedName %>Events.emit(event, doc);<% if (filters.sequelizeModels) { %> 38 | done(null);<% } %> 39 | } 40 | } 41 | 42 | module.exports = <%= classedName %>Events; 43 | -------------------------------------------------------------------------------- /endpoint/templates/basename.integration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = require('<%= relativeRequire('server') %>'); 4 | var request = require('supertest');<% if(filters.models) { %> 5 | 6 | var new<%= classedName %>;<% } %> 7 | 8 | describe('<%= classedName %> API:', function() { 9 | 10 | describe('GET <%= route %>', function() { 11 | var <%= cameledName %>s; 12 | 13 | beforeEach(function(done) { 14 | request(app) 15 | .get('<%= route %>') 16 | .expect(200) 17 | .expect('Content-Type', /json/) 18 | .end(function(err, res) { 19 | if (err) { 20 | return done(err); 21 | } 22 | <%= cameledName %>s = res.body; 23 | done(); 24 | }); 25 | }); 26 | 27 | it('should respond with JSON array', function() { 28 | <%= expect() %><%= cameledName %>s<%= to() %>.be.instanceOf(Array); 29 | }); 30 | 31 | });<% if(filters.models) { %> 32 | 33 | describe('POST <%= route %>', function() { 34 | beforeEach(function(done) { 35 | request(app) 36 | .post('<%= route %>') 37 | .send({ 38 | name: 'New <%= classedName %>', 39 | info: 'This is the brand new <%= cameledName %>!!!' 40 | }) 41 | .expect(201) 42 | .expect('Content-Type', /json/) 43 | .end(function(err, res) { 44 | if (err) { 45 | return done(err); 46 | } 47 | new<%= classedName %> = res.body; 48 | done(); 49 | }); 50 | }); 51 | 52 | it('should respond with the newly created <%= cameledName %>', function() { 53 | <%= expect() %>new<%= classedName %>.name<%= to() %>.equal('New <%= classedName %>'); 54 | <%= expect() %>new<%= classedName %>.info<%= to() %>.equal('This is the brand new <%= cameledName %>!!!'); 55 | }); 56 | 57 | }); 58 | 59 | describe('GET <%= route %>/:id', function() { 60 | var <%= cameledName %>; 61 | 62 | beforeEach(function(done) { 63 | request(app) 64 | <% if (filters.mongooseModels) { %>.get('<%= route %>/' + new<%= classedName %>._id)<% } %> 65 | <% if (filters.sequelizeModels) { %>.get('<%= route %>/' + new<%= classedName %>.id)<% } %> 66 | .expect(200) 67 | .expect('Content-Type', /json/) 68 | .end(function(err, res) { 69 | if (err) { 70 | return done(err); 71 | } 72 | <%= cameledName %> = res.body; 73 | done(); 74 | }); 75 | }); 76 | 77 | afterEach(function() { 78 | <%= cameledName %> = {}; 79 | }); 80 | 81 | it('should respond with the requested <%= cameledName %>', function() { 82 | <%= expect() %><%= cameledName %>.name<%= to() %>.equal('New <%= classedName %>'); 83 | <%= expect() %><%= cameledName %>.info<%= to() %>.equal('This is the brand new <%= cameledName %>!!!'); 84 | }); 85 | 86 | }); 87 | 88 | describe('PUT <%= route %>/:id', function() { 89 | var updated<%= classedName %> 90 | 91 | beforeEach(function(done) { 92 | request(app) 93 | <% if (filters.mongooseModels) { %>.put('<%= route %>/' + new<%= classedName %>._id) <% } %> 94 | <% if (filters.sequelizeModels) { %>.put('<%= route %>/' + new<%= classedName %>.id)<% } %> 95 | .send({ 96 | name: 'Updated <%= classedName %>', 97 | info: 'This is the updated <%= cameledName %>!!!' 98 | }) 99 | .expect(200) 100 | .expect('Content-Type', /json/) 101 | .end(function(err, res) { 102 | if (err) { 103 | return done(err); 104 | } 105 | updated<%= classedName %> = res.body; 106 | done(); 107 | }); 108 | }); 109 | 110 | afterEach(function() { 111 | updated<%= classedName %> = {}; 112 | }); 113 | 114 | it('should respond with the updated <%= cameledName %>', function() { 115 | <%= expect() %>updated<%= classedName %>.name<%= to() %>.equal('Updated <%= classedName %>'); 116 | <%= expect() %>updated<%= classedName %>.info<%= to() %>.equal('This is the updated <%= cameledName %>!!!'); 117 | }); 118 | 119 | }); 120 | 121 | describe('DELETE <%= route %>/:id', function() { 122 | 123 | it('should respond with 204 on successful removal', function(done) { 124 | request(app) 125 | <% if (filters.mongooseModels) { %>.delete('<%= route %>/' + new<%= classedName %>._id)<% } %> 126 | <% if (filters.sequelizeModels) { %>.delete('<%= route %>/' + new<%= classedName %>.id)<% } %> 127 | .expect(204) 128 | .end(function(err, res) { 129 | if (err) { 130 | return done(err); 131 | } 132 | done(); 133 | }); 134 | }); 135 | 136 | it('should respond with 404 when <%= cameledName %> does not exist', function(done) { 137 | request(app) 138 | <% if (filters.mongooseModels) { %>.delete('<%= route %>/' + new<%= classedName %>._id)<% } %> 139 | <% if (filters.sequelizeModels) { %>.delete('<%= route %>/' + new<%= classedName %>.id)<% } %> 140 | .expect(404) 141 | .end(function(err, res) { 142 | if (err) { 143 | return done(err); 144 | } 145 | done(); 146 | }); 147 | }); 148 | 149 | });<% } %> 150 | 151 | }); 152 | -------------------------------------------------------------------------------- /endpoint/templates/basename.model(mongooseModels).js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mongoose = require('bluebird').promisifyAll(require('mongoose')); 4 | var Schema = mongoose.Schema; 5 | 6 | var <%= classedName %>Schema = new Schema({ 7 | name: String, 8 | info: String, 9 | active: Boolean 10 | }); 11 | 12 | module.exports = mongoose.model('<%= classedName %>', <%= classedName %>Schema); 13 | -------------------------------------------------------------------------------- /endpoint/templates/basename.model(sequelizeModels).js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(sequelize, DataTypes) { 4 | return sequelize.define('<%= classedName %>', { 5 | id: { 6 | type: <% if (filters.sequelizeModels.serial) { %> DataTypes.INTEGER <% } if (filters.sequelizeModels.uuid) { %> DataTypes.UUID <% } %>, 7 | allowNull: false, 8 | primaryKey: true,<% if (filters.sequelizeModels.serial) { %> 9 | autoIncrement: true <% } %><% if (filters.sequelizeModels.uuid) { %> 10 | defaultValue: DataTypes.UUIDV4 <% } %> 11 | }, 12 | name: DataTypes.STRING, 13 | info: DataTypes.STRING, 14 | active: DataTypes.BOOLEAN 15 | },{ 16 | <% if (filters.sequelizeModels.timestamps) { %>timestamps: true, <% } %><% if (filters.sequelizeModels.paranoid) { %> 17 | paranoid: true,<% } %><% if (filters.sequelizeModels) { %> 18 | underscored: true, 19 | freezeTableName:true, 20 | tableName:'<%= classedName.toLowerCase() %><% if (filters.sequelizeModels.pluralization) { %>s<% } %>' 21 | <% } %> 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /endpoint/templates/basename.socket(socketio).js: -------------------------------------------------------------------------------- 1 | /** 2 | * Broadcast updates to client when the model changes 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var <%= classedName %>Events = require('./<%= basename %>.events'); 8 | 9 | // Model events to emit 10 | var events = ['save', 'remove']; 11 | 12 | exports.register = function(socket) { 13 | // Bind model events to socket events 14 | for (var i = 0, eventsLength = events.length; i < eventsLength; i++) { 15 | var event = events[i]; 16 | var listener = createListener('<%= cameledName %>:' + event, socket); 17 | 18 | <%= classedName %>Events.on(event, listener); 19 | socket.on('disconnect', removeListener(event, listener)); 20 | } 21 | }; 22 | 23 | 24 | function createListener(event, socket) { 25 | return function(doc) { 26 | socket.emit(event, doc); 27 | }; 28 | } 29 | 30 | function removeListener(event, listener) { 31 | return function() { 32 | <%= classedName %>Events.removeListener(event, listener); 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /endpoint/templates/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var controller = require('./<%= basename %>.controller'); 5 | 6 | var router = express.Router(); 7 | 8 | router.get('/', controller.index);<% if (filters.models) { %> 9 | router.get('/:id', controller.show); 10 | router.post('/', controller.create); 11 | router.put('/:id', controller.update); 12 | router.patch('/:id', controller.update); 13 | router.delete('/:id', controller.destroy);<% } %> 14 | 15 | module.exports = router; 16 | -------------------------------------------------------------------------------- /endpoint/templates/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var proxyquire = require('proxyquire').noPreserveCache(); 4 | 5 | var <%= cameledName %>CtrlStub = { 6 | index: '<%= cameledName %>Ctrl.index'<% if(filters.models) { %>, 7 | show: '<%= cameledName %>Ctrl.show', 8 | create: '<%= cameledName %>Ctrl.create', 9 | update: '<%= cameledName %>Ctrl.update', 10 | destroy: '<%= cameledName %>Ctrl.destroy'<% } %> 11 | }; 12 | 13 | var routerStub = { 14 | get: sinon.spy()<% if(filters.models) { %>, 15 | put: sinon.spy(), 16 | patch: sinon.spy(), 17 | post: sinon.spy(), 18 | delete: sinon.spy()<% } %> 19 | }; 20 | 21 | // require the index with our stubbed out modules 22 | var <%= cameledName %>Index = proxyquire('./index.js', { 23 | 'express': { 24 | Router: function() { 25 | return routerStub; 26 | } 27 | }, 28 | './<%= basename %>.controller': <%= cameledName %>CtrlStub 29 | }); 30 | 31 | describe('<%= classedName %> API Router:', function() { 32 | 33 | it('should return an express router instance', function() { 34 | <%= expect() %><%= cameledName %>Index<%= to() %>.equal(routerStub); 35 | }); 36 | 37 | describe('GET <%= route %>', function() { 38 | 39 | it('should route to <%= cameledName %>.controller.index', function() { 40 | <%= expect() %>routerStub.get 41 | .withArgs('/', '<%= cameledName %>Ctrl.index') 42 | <%= to() %>.have.been.calledOnce; 43 | }); 44 | 45 | });<% if(filters.models) { %> 46 | 47 | describe('GET <%= route %>/:id', function() { 48 | 49 | it('should route to <%= cameledName %>.controller.show', function() { 50 | <%= expect() %>routerStub.get 51 | .withArgs('/:id', '<%= cameledName %>Ctrl.show') 52 | <%= to() %>.have.been.calledOnce; 53 | }); 54 | 55 | }); 56 | 57 | describe('POST <%= route %>', function() { 58 | 59 | it('should route to <%= cameledName %>.controller.create', function() { 60 | <%= expect() %>routerStub.post 61 | .withArgs('/', '<%= cameledName %>Ctrl.create') 62 | <%= to() %>.have.been.calledOnce; 63 | }); 64 | 65 | }); 66 | 67 | describe('PUT <%= route %>/:id', function() { 68 | 69 | it('should route to <%= cameledName %>.controller.update', function() { 70 | <%= expect() %>routerStub.put 71 | .withArgs('/:id', '<%= cameledName %>Ctrl.update') 72 | <%= to() %>.have.been.calledOnce; 73 | }); 74 | 75 | }); 76 | 77 | describe('PATCH <%= route %>/:id', function() { 78 | 79 | it('should route to <%= cameledName %>.controller.update', function() { 80 | <%= expect() %>routerStub.patch 81 | .withArgs('/:id', '<%= cameledName %>Ctrl.update') 82 | <%= to() %>.have.been.calledOnce; 83 | }); 84 | 85 | }); 86 | 87 | describe('DELETE <%= route %>/:id', function() { 88 | 89 | it('should route to <%= cameledName %>.controller.destroy', function() { 90 | <%= expect() %>routerStub.delete 91 | .withArgs('/:id', '<%= cameledName %>Ctrl.destroy') 92 | <%= to() %>.have.been.calledOnce; 93 | }); 94 | 95 | });<% } %> 96 | 97 | }); 98 | -------------------------------------------------------------------------------- /generator-base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import util from 'util'; 4 | import path from 'path'; 5 | import lodash from 'lodash'; 6 | import s from 'underscore.string'; 7 | import yoWelcome from 'yeoman-welcome'; 8 | import * as genUtils from './util'; 9 | 10 | // extend lodash with underscore.string 11 | lodash.mixin(s.exports()); 12 | 13 | export function genBase(self) { 14 | self = self || this; 15 | 16 | self.lodash = lodash; 17 | self.yoWelcome = yoWelcome; 18 | 19 | self.appname = lodash.camelize(lodash.slugify( 20 | lodash.humanize(self.determineAppname()) 21 | )); 22 | self.scriptAppName = self.appname + genUtils.appSuffix(self); 23 | 24 | self.filters = self.filters || self.config.get('filters'); 25 | 26 | // dynamic assertion statements 27 | self.expect = function() { 28 | return self.filters.expect ? 'expect(' : ''; 29 | }; 30 | self.to = function() { 31 | return self.filters.expect ? ').to' : '.should'; 32 | }; 33 | 34 | // dynamic relative require path 35 | self.relativeRequire = genUtils.relativeRequire.bind(self); 36 | // process template directory 37 | self.processDirectory = genUtils.processDirectory.bind(self); 38 | // rewrite a file in place 39 | self.rewriteFile = genUtils.rewriteFile; 40 | } 41 | 42 | export function genNamedBase(self) { 43 | self = self || this; 44 | 45 | // extend genBase 46 | genBase(self); 47 | 48 | var name = self.name.replace(/\//g, '-'); 49 | 50 | self.cameledName = lodash.camelize(name); 51 | self.classedName = lodash.classify(name); 52 | 53 | self.basename = path.basename(self.name); 54 | self.dirname = (self.name.indexOf('/') >= 0) ? path.dirname(self.name) : self.name; 55 | } 56 | -------------------------------------------------------------------------------- /heroku/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Initalizes a heroku app and generates a `dist` folder which is ready to push to heroku. 3 | 4 | Example: 5 | yo expressjs-api:heroku 6 | 7 | This will create: 8 | a dist folder and initialize a heroku app 9 | -------------------------------------------------------------------------------- /heroku/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var util = require('util'); 3 | var yeoman = require('yeoman-generator'); 4 | var exec = require('child_process').exec; 5 | var chalk = require('chalk'); 6 | var path = require('path'); 7 | var s = require('underscore.string'); 8 | 9 | var Generator = module.exports = function Generator() { 10 | yeoman.generators.Base.apply(this, arguments); 11 | this.sourceRoot(path.join(__dirname, './templates')); 12 | 13 | try { 14 | this.appname = require(path.join(process.cwd(), 'package.json')).name; 15 | } catch (e) { 16 | this.appname = path.basename(process.cwd()); 17 | } 18 | this.appname = s.slugify(this.appname); 19 | this.filters = this.config.get('filters') || {}; 20 | }; 21 | 22 | util.inherits(Generator, yeoman.generators.NamedBase); 23 | 24 | Generator.prototype.askForName = function askForName() { 25 | var done = this.async(); 26 | 27 | var prompts = [{ 28 | name: 'deployedName', 29 | message: 'Name to deploy as (Leave blank for a random name):' 30 | }]; 31 | 32 | this.prompt(prompts, function (props) { 33 | this.deployedName = s.slugify(props.deployedName); 34 | done(); 35 | }.bind(this)); 36 | }; 37 | 38 | Generator.prototype.askForRegion = function askForRegion() { 39 | var done = this.async(); 40 | 41 | var prompts = [{ 42 | type: "list", 43 | name: 'region', 44 | message: 'On which region do you want to deploy ?', 45 | choices: [ "US", "EU"], 46 | default: 0 47 | }]; 48 | 49 | this.prompt(prompts, function (props) { 50 | this.region = props.region.toLowerCase(); 51 | done(); 52 | }.bind(this)); 53 | }; 54 | 55 | Generator.prototype.checkInstallation = function checkInstallation() { 56 | if(this.abort) return; 57 | var done = this.async(); 58 | 59 | exec('heroku --version', function (err) { 60 | if (err) { 61 | this.log.error('You don\'t have the Heroku Toolbelt installed. ' + 62 | 'Grab it from https://toolbelt.heroku.com/'); 63 | this.abort = true; 64 | } 65 | done(); 66 | }.bind(this)); 67 | }; 68 | 69 | Generator.prototype.gitInit = function gitInit() { 70 | if(this.abort) return; 71 | var done = this.async(); 72 | 73 | this.log(chalk.bold('\nInitializing deployment repo')); 74 | this.mkdir('dist'); 75 | var child = exec('git init', { cwd: 'dist' }, function (err, stdout, stderr) { 76 | done(); 77 | }.bind(this)); 78 | child.stdout.on('data', function(data) { 79 | console.log(data.toString()); 80 | }); 81 | }; 82 | 83 | Generator.prototype.herokuCreate = function herokuCreate() { 84 | if(this.abort) return; 85 | var done = this.async(); 86 | var regionParams = (this.region !== 'us') ? ' --region ' + this.region : ''; 87 | 88 | this.log(chalk.bold('Creating heroku app and setting node environment')); 89 | var child = exec('heroku apps:create ' + this.deployedName + regionParams + ' && heroku config:set NODE_ENV=production', { cwd: 'dist' }, function (err, stdout, stderr) { 90 | if (err) { 91 | this.abort = true; 92 | this.log.error(err); 93 | } else { 94 | this.log('stdout: ' + stdout); 95 | } 96 | done(); 97 | }.bind(this)); 98 | 99 | child.stdout.on('data', function(data) { 100 | var output = data.toString(); 101 | this.log(output); 102 | }.bind(this)); 103 | }; 104 | 105 | Generator.prototype.copyProcfile = function copyProcfile() { 106 | if(this.abort) return; 107 | var done = this.async(); 108 | this.log(chalk.bold('Creating Procfile')); 109 | this.copy('Procfile', 'dist/Procfile'); 110 | this.conflicter.resolve(function (err) { 111 | done(); 112 | }); 113 | }; 114 | 115 | Generator.prototype.gruntBuild = function gruntBuild() { 116 | if(this.abort) return; 117 | var done = this.async(); 118 | 119 | this.log(chalk.bold('\nBuilding dist folder, please wait...')); 120 | var child = exec('grunt build', function (err, stdout) { 121 | done(); 122 | }.bind(this)); 123 | child.stdout.on('data', function(data) { 124 | this.log(data.toString()); 125 | }.bind(this)); 126 | }; 127 | 128 | Generator.prototype.gitCommit = function gitInit() { 129 | if(this.abort) return; 130 | var done = this.async(); 131 | 132 | this.log(chalk.bold('Adding files for initial commit')); 133 | var child = exec('git add -A && git commit -m "Initial commit"', { cwd: 'dist' }, function (err, stdout, stderr) { 134 | if (stdout.search('nothing to commit') >= 0) { 135 | this.log('Re-pushing the existing "dist" build...'); 136 | } else if (err) { 137 | this.log.error(err); 138 | } else { 139 | this.log(chalk.green('Done, without errors.')); 140 | } 141 | done(); 142 | }.bind(this)); 143 | 144 | child.stdout.on('data', function(data) { 145 | this.log(data.toString()); 146 | }.bind(this)); 147 | }; 148 | 149 | Generator.prototype.gitForcePush = function gitForcePush() { 150 | if(this.abort) return; 151 | var done = this.async(); 152 | 153 | this.log(chalk.bold("\nUploading your initial application code.\n This may take "+chalk.cyan('several minutes')+" depending on your connection speed...")); 154 | 155 | var child = exec('git push -f heroku master', { cwd: 'dist' }, function (err, stdout, stderr) { 156 | if (err) { 157 | this.log.error(err); 158 | } else { 159 | var hasWarning = false; 160 | 161 | if(this.filters.mongoose) { 162 | this.log(chalk.yellow('\nBecause you\'re using mongoose, you must add mongoDB to your heroku app.\n\t' + 'from `/dist`: ' + chalk.bold('heroku addons:create mongolab') + '\n')); 163 | hasWarning = true; 164 | } 165 | 166 | if(this.filters.facebookAuth) { 167 | this.log(chalk.yellow('You will need to set environment variables for facebook auth. From `/dist`:\n\t' + 168 | chalk.bold('heroku config:set FACEBOOK_ID=appId\n\t') + 169 | chalk.bold('heroku config:set FACEBOOK_SECRET=secret\n'))); 170 | hasWarning = true; 171 | } 172 | if(this.filters.googleAuth) { 173 | this.log(chalk.yellow('You will need to set environment variables for google auth. From `/dist`:\n\t' + 174 | chalk.bold('heroku config:set GOOGLE_ID=appId\n\t') + 175 | chalk.bold('heroku config:set GOOGLE_SECRET=secret\n'))); 176 | hasWarning = true; 177 | } 178 | if(this.filters.twitterAuth) { 179 | this.log(chalk.yellow('You will need to set environment variables for twitter auth. From `/dist`:\n\t' + 180 | chalk.bold('heroku config:set TWITTER_ID=appId\n\t') + 181 | chalk.bold('heroku config:set TWITTER_SECRET=secret\n'))); 182 | hasWarning = true; 183 | } 184 | 185 | this.log(chalk.green('\nYour app should now be live. To view it run\n\t' + chalk.bold('cd dist && heroku open'))); 186 | if(hasWarning) { 187 | this.log(chalk.green('\nYou may need to address the issues mentioned above and restart the server for the app to work correctly.')); 188 | } 189 | 190 | this.log(chalk.yellow('After app modification run\n\t' + chalk.bold('grunt build') + 191 | '\nThen deploy with\n\t' + chalk.bold('grunt buildcontrol:heroku'))); 192 | } 193 | done(); 194 | }.bind(this)); 195 | 196 | child.stdout.on('data', function(data) { 197 | this.log(data.toString()); 198 | }.bind(this)); 199 | }; 200 | -------------------------------------------------------------------------------- /heroku/templates/Procfile: -------------------------------------------------------------------------------- 1 | web: node server/app.js 2 | -------------------------------------------------------------------------------- /openshift/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Initalizes an openshift app and generates a `dist` folder and pushes it to openshift. 3 | 4 | Example: 5 | yo expressjs-api:openshift 6 | 7 | This will create: 8 | a dist folder and initialize an openshift app 9 | -------------------------------------------------------------------------------- /openshift/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var util = require('util'); 3 | var yeoman = require('yeoman-generator'); 4 | var childProcess = require('child_process'); 5 | var chalk = require('chalk'); 6 | var path = require('path'); 7 | var s = require('underscore.string'); 8 | var exec = childProcess.exec; 9 | var spawn = childProcess.spawn; 10 | 11 | var Generator = module.exports = function Generator() { 12 | yeoman.generators.Base.apply(this, arguments); 13 | this.sourceRoot(path.join(__dirname, './templates')); 14 | 15 | try { 16 | this.appname = require(path.join(process.cwd(), 'package.json')).name; 17 | } catch (e) { 18 | this.appname = path.basename(process.cwd()); 19 | } 20 | this.appname = s.slugify(this.appname).split('-').join(''); 21 | this.filters = this.config.get('filters') || {}; 22 | }; 23 | 24 | util.inherits(Generator, yeoman.generators.NamedBase); 25 | 26 | Generator.prototype.askForName = function askForName() { 27 | var done = this.async(); 28 | 29 | var prompts = [{ 30 | name: 'deployedName', 31 | message: 'Name to deploy as:', 32 | default: this.appname 33 | }]; 34 | 35 | this.prompt(prompts, function (props) { 36 | this.deployedName = s.slugify(props.deployedName).split('-').join(''); 37 | done(); 38 | }.bind(this)); 39 | }; 40 | 41 | Generator.prototype.checkInstallation = function checkInstallation() { 42 | if(this.abort) return; 43 | var done = this.async(); 44 | 45 | exec('rhc --version', function (err) { 46 | if (err) { 47 | this.log.error('OpenShift\'s rhc command line interface is not available. ' + 48 | 'You can install it via RubyGems with: gem install rhc'); 49 | this.abort = true; 50 | } 51 | done(); 52 | }.bind(this)); 53 | }; 54 | 55 | Generator.prototype.gitInit = function gitInit() { 56 | if(this.abort) return; 57 | var done = this.async(); 58 | 59 | this.log(chalk.bold('Initializing deployment repo')); 60 | this.mkdir('dist'); 61 | exec('git init', { cwd: 'dist' }, function (err, stdout, stderr) { 62 | this.log(stdout); 63 | done(); 64 | }.bind(this)); 65 | }; 66 | 67 | Generator.prototype.gitRemoteCheck = function gitRemoteCheck() { 68 | this.openshift_remote_exists = false; 69 | if(this.abort || typeof this.dist_repo_url !== 'undefined') return; 70 | var done = this.async(); 71 | 72 | this.log(chalk.bold("\nChecking for an existing git remote named '"+'openshift'+"'...")); 73 | exec('git remote -v', { cwd: 'dist' }, function (err, stdout, stderr) { 74 | var lines = stdout.split('\n'); 75 | var dist_repo = ''; 76 | if (err && stderr.search('DL is deprecated') === -1) { 77 | this.log.error(err); 78 | } else { 79 | var repo_url_finder = new RegExp('openshift'+"[ ]*"); 80 | lines.forEach(function(line) { 81 | if(line.search(repo_url_finder) === 0 && dist_repo === '') { 82 | var dist_repo_detailed = line.slice(line.match(repo_url_finder)[0].length); 83 | dist_repo = dist_repo_detailed.slice(0, dist_repo_detailed.length - 7); 84 | }}); 85 | if (dist_repo !== ''){ 86 | console.log("Found an existing git remote for this app: "+dist_repo); 87 | this.dist_repo_url = dist_repo; 88 | this.openshift_remote_exists = true; 89 | } else { 90 | console.log('No existing remote found.'); 91 | } 92 | } 93 | done(); 94 | }.bind(this)); 95 | }; 96 | 97 | Generator.prototype.rhcAppShow = function rhcAppShow() { 98 | if(this.abort || typeof this.dist_repo_url !== 'undefined') return; 99 | var done = this.async(); 100 | 101 | this.log(chalk.bold("\nChecking for an existing OpenShift hosting environment...")); 102 | var child = exec('rhc app show '+this.deployedName+' --noprompt', { cwd: 'dist' }, function (err, stdout, stderr) { 103 | var lines = stdout.split('\n'); 104 | var dist_repo = ''; 105 | // Unauthenticated 106 | if (stdout.search('Not authenticated') >= 0 || stdout.search('Invalid characters found in login') >= 0) { 107 | this.log.error('Error: Not authenticated. Run "rhc setup" to login to your OpenShift account and try again.'); 108 | this.abort = true; 109 | } 110 | // No remote found 111 | else if (stdout.search('not found.') >= 0) { 112 | console.log('No existing app found.'); 113 | } 114 | // Error 115 | else if (err && stderr.search('DL is deprecated') === -1) { 116 | this.log.error(err); 117 | } 118 | // Remote found 119 | else { 120 | this.log(stdout); 121 | var repo_url_finder = / *Git URL: */; 122 | lines.forEach(function(line) { 123 | if(line.search(repo_url_finder) === 0) { 124 | dist_repo = line.slice(line.match(repo_url_finder)[0].length); 125 | console.log("Found an existing git remote for this app: "+dist_repo); 126 | }}); 127 | if (dist_repo !== '') this.dist_repo_url = dist_repo; 128 | } 129 | done(); 130 | }.bind(this)); 131 | }; 132 | 133 | Generator.prototype.rhcAppCreate = function rhcAppCreate() { 134 | if(this.abort || typeof this.dist_repo_url !== 'undefined') return; 135 | var done = this.async(); 136 | 137 | this.log(chalk.bold("\nCreating your OpenShift hosting environment, this may take a couple minutes...")); 138 | var child = exec('rhc app create '+this.deployedName+' nodejs-0.10 mongodb-2.4 -s --noprompt --no-git NODE_ENV=production', { cwd: 'dist' }, function (err, stdout, stderr) { 139 | var lines = stdout.split('\n'); 140 | this.log(stdout); 141 | if (stdout.search('Not authenticated') >= 0 || stdout.search('Invalid characters found in login') >= 0) { 142 | this.log.error('Error: Not authenticated. Run "rhc setup" to login to your OpenShift account and try again.'); 143 | this.abort = true; 144 | } else if (err && stderr.search('DL is deprecated') === -1) { 145 | this.log.error(err); 146 | } else { 147 | var dist_repo = ''; 148 | var repo_url_finder = / *Git remote: */; 149 | lines.forEach(function(line) { 150 | if(line.search(repo_url_finder) === 0) { 151 | dist_repo = line.slice(line.match(repo_url_finder)[0].length); 152 | }}); 153 | 154 | if (dist_repo !== '') this.dist_repo_url = dist_repo; 155 | if(this.dist_repo_url !== undefined) { 156 | this.log("New remote git repo at: "+this.dist_repo_url); 157 | } 158 | } 159 | done(); 160 | }.bind(this)); 161 | 162 | child.stdout.on('data', function(data) { 163 | this.log(data.toString()); 164 | }.bind(this)); 165 | }; 166 | 167 | Generator.prototype.gitRemoteAdd = function gitRemoteAdd() { 168 | if(this.abort || typeof this.dist_repo_url === 'undefined' || this.openshift_remote_exists) return; 169 | var done = this.async(); 170 | this.log(chalk.bold("\nAdding remote repo url: "+this.dist_repo_url)); 171 | 172 | var child = exec('git remote add '+'openshift'+' '+this.dist_repo_url, { cwd: 'dist' }, function (err, stdout, stderr) { 173 | if (err) { 174 | this.log.error(err); 175 | } else { 176 | this.openshift_remote_exists = true; 177 | } 178 | done(); 179 | }.bind(this)); 180 | 181 | child.stdout.on('data', function(data) { 182 | this.log(data.toString()); 183 | }.bind(this)); 184 | }; 185 | 186 | Generator.prototype.enableOpenShiftHotDeploy = function enableOpenshiftHotDeploy() { 187 | if(this.abort || !this.openshift_remote_exists ) return; 188 | var done = this.async(); 189 | this.log(chalk.bold("\nEnabling HotDeploy for OpenShift")); 190 | this.copy('hot_deploy', 'dist/.openshift/markers/hot_deploy'); 191 | this.conflicter.resolve(function (err) { 192 | done(); 193 | }); 194 | }; 195 | 196 | Generator.prototype.gruntBuild = function gruntBuild() { 197 | if(this.abort || !this.openshift_remote_exists ) return; 198 | var done = this.async(); 199 | 200 | this.log(chalk.bold('\nBuilding dist folder, please wait...')); 201 | var child = exec('grunt build', function (err, stdout) { 202 | if (err) { 203 | this.log.error(err); 204 | } 205 | done(); 206 | }.bind(this)); 207 | 208 | child.stdout.on('data', function(data) { 209 | this.log(data.toString()); 210 | }.bind(this)); 211 | }; 212 | 213 | Generator.prototype.gitCommit = function gitInit() { 214 | if(this.abort || !this.openshift_remote_exists ) return; 215 | var done = this.async(); 216 | 217 | this.log(chalk.bold('\nAdding files for initial commit')); 218 | var child = exec('git add -A && git commit -m "Initial commit"', { cwd: 'dist' }, function (err, stdout, stderr) { 219 | if (stdout.search('nothing to commit') >= 0) { 220 | this.log('Re-pushing the existing "dist" build...'); 221 | } else if (err) { 222 | this.log.error(err); 223 | } else { 224 | this.log(chalk.green('Done, without errors.')); 225 | } 226 | done(); 227 | }.bind(this)); 228 | 229 | child.stdout.on('data', function(data) { 230 | this.log(data.toString()); 231 | }.bind(this)); 232 | }; 233 | 234 | Generator.prototype.gitForcePush = function gitForcePush() { 235 | if (this.abort || !this.openshift_remote_exists) return; 236 | var done = this.async(); 237 | this.log(chalk.bold("\nUploading your initial application code.\n This may take " + chalk.cyan('several minutes') + " depending on your connection speed...")); 238 | 239 | var push = spawn('git', ['push', '-f', 'openshift', 'master'], {cwd: 'dist'}); 240 | var error = null; 241 | 242 | push.stderr.on('data', function (data) { 243 | var output = data.toString(); 244 | this.log.error(output); 245 | }.bind(this)); 246 | 247 | push.stdout.on('data', function (data) { 248 | var output = data.toString(); 249 | this.log.stdin(output); 250 | }.bind(this)); 251 | 252 | push.on('exit', function (code) { 253 | if (code !== 0) { 254 | this.abort = true; 255 | return done(); 256 | } 257 | done(); 258 | }.bind(this)); 259 | }; 260 | 261 | Generator.prototype.restartApp = function restartApp() { 262 | if(this.abort || !this.openshift_remote_exists ) return; 263 | this.log(chalk.bold("\nRestarting your openshift app.\n")); 264 | 265 | var child = exec('rhc app restart -a ' + this.deployedName, function(err, stdout, stderr) { 266 | 267 | var host_url = ''; 268 | var hasWarning = false; 269 | var before_hostname = this.dist_repo_url.indexOf('@') + 1; 270 | var after_hostname = this.dist_repo_url.length - ( 'openshift'.length + 12 ); 271 | host_url = 'http://' + this.dist_repo_url.slice(before_hostname, after_hostname) + 'com'; 272 | 273 | if(this.filters.facebookAuth) { 274 | this.log(chalk.yellow('You will need to set environment variables for facebook auth:\n\t' + 275 | chalk.bold('rhc set-env FACEBOOK_ID=id -a ' + this.deployedName + '\n\t') + 276 | chalk.bold('rhc set-env FACEBOOK_SECRET=secret -a ' + this.deployedName + '\n'))); 277 | hasWarning = true; 278 | } 279 | if(this.filters.googleAuth) { 280 | this.log(chalk.yellow('You will need to set environment variables for google auth:\n\t' + 281 | chalk.bold('rhc set-env GOOGLE_ID=id -a ' + this.deployedName + '\n\t') + 282 | chalk.bold('rhc set-env GOOGLE_SECRET=secret -a ' + this.deployedName + '\n'))); 283 | hasWarning = true; 284 | } 285 | if(this.filters.twitterAuth) { 286 | this.log(chalk.yellow('You will need to set environment variables for twitter auth:\n\t' + 287 | chalk.bold('rhc set-env TWITTER_ID=id -a ' + this.deployedName + '\n\t') + 288 | chalk.bold('rhc set-env TWITTER_SECRET=secret -a ' + this.deployedName + '\n'))); 289 | hasWarning = true; 290 | } 291 | 292 | this.log(chalk.green('\nYour app should now be live at \n\t' + chalk.bold(host_url))); 293 | if(hasWarning) { 294 | this.log(chalk.green('\nYou may need to address the issues mentioned above and restart the server for the app to work correctly \n\t' + 295 | 'rhc app-restart -a ' + this.deployedName)); 296 | } 297 | this.log(chalk.yellow('After app modification run\n\t' + chalk.bold('grunt build') + 298 | '\nThen deploy with\n\t' + chalk.bold('grunt buildcontrol:openshift'))); 299 | }.bind(this)); 300 | }; 301 | -------------------------------------------------------------------------------- /openshift/templates/hot_deploy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ioneyed/generator-expressjs-api/188e0f87179c92171cfe5d1c61bc42790f541a42/openshift/templates/hot_deploy -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generator-expressjs-api", 3 | "version": "0.0.1", 4 | "description": "Yeoman generator for creating Web APIs using MongoDB/Sequelize (MySQL, Postgres, etc), Express, and Node", 5 | "keywords": [ 6 | "yeoman-generator", 7 | "mongodb", 8 | "postgres", 9 | "express", 10 | "scaffold", 11 | "framework", 12 | "component", 13 | "app", 14 | "api" 15 | ], 16 | "homepage": "https://github.com/ioneyed/generator-expressjs-api", 17 | "bugs": "https://github.com/ioneyed/generator-expressjs-api/issues", 18 | "author": "Robert Buchanan (http://ioneyed.com/)", 19 | "contributors": [ 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "git://github.com/ioneyed/generator-expressjs-api.git" 24 | }, 25 | "scripts": { 26 | "test": "grunt test" 27 | }, 28 | "dependencies": { 29 | "babel-core": "^5.8.23", 30 | "chalk": "^1.1.0", 31 | "glob": "^5.0.14", 32 | "lodash": "^3.10.1", 33 | "underscore.string": "^3.1.1", 34 | "yeoman-generator": "~0.20.3", 35 | "yeoman-welcome": "^1.0.1" 36 | }, 37 | "devDependencies": { 38 | "chai": "^3.2.0", 39 | "grunt": "~0.4.1", 40 | "grunt-build-control": "^0.6.0", 41 | "grunt-contrib-clean": "^0.6.0", 42 | "grunt-contrib-jshint": "^0.11.2", 43 | "grunt-conventional-changelog": "^4.1.0", 44 | "grunt-david": "~0.5.0", 45 | "grunt-env": "^0.4.1", 46 | "grunt-mocha-test": "^0.12.7", 47 | "grunt-release": "^0.13.0", 48 | "jit-grunt": "^0.9.1", 49 | "mocha": "^2.2.5", 50 | "q": "^1.0.1", 51 | "recursive-readdir": "^1.2.0", 52 | "shelljs": "^0.5.3", 53 | "yeoman-assert": "^2.0.0" 54 | }, 55 | "engines": { 56 | "node": ">=0.12.0", 57 | "npm": ">=1.2.10" 58 | }, 59 | "license": "BSD-2-Clause" 60 | } 61 | -------------------------------------------------------------------------------- /provider/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var yeoman = require('yeoman-generator'); 3 | 4 | var Generator = yeoman.generators.Base.extend({ 5 | compose: function() { 6 | this.composeWith('ng-component:provider', {arguments: this.arguments}, { local: require.resolve('generator-ng-component/provider') }); 7 | } 8 | }); 9 | 10 | module.exports = Generator; 11 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ExpressJS API generator 2 | > Yeoman generator for creating API's using MongoDB/Sequelize (Mysql, Postgres, sqlite), Express, and Node - lets you quickly set up a project following best practices. 3 | 4 | #### Generated project: 5 | 6 | ## Usage 7 | 8 | Install `yo`, `grunt-cli`, and `generator-expressjs-api`: 9 | ``` 10 | npm install -g yo grunt-cli generator-expressjs-api 11 | ``` 12 | 13 | Make a new directory, and `cd` into it: 14 | ``` 15 | mkdir my-new-project && cd $_ 16 | ``` 17 | 18 | Run `yo expressjs-api`, optionally passing an app name: 19 | ``` 20 | yo expressjs-api [app-name] 21 | ``` 22 | 23 | Run `grunt` for building, `grunt serve` for preview, and `grunt serve:dist` for a preview of the built app. 24 | 25 | ## Prerequisites 26 | 27 | * MongoDB - Download and Install [MongoDB](http://www.mongodb.org/downloads) - If you plan on scaffolding your project with mongoose, you'll need mongoDB to be installed and have the `mongod` process running. 28 | 29 | ## Supported Configurations 30 | 31 | **General** 32 | 33 | * Build Systems: `Grunt`, `Gulp` (Coming Soon) 34 | * Testing: 35 | * `Jasmine` 36 | * `Mocha + Chai + Sinon` 37 | * Chai assertions: 38 | * `Expect` 39 | * `Should` 40 | 41 | **Server** 42 | 43 | * Scripts: `Babel` 44 | * Database: 45 | * `None`, 46 | * `MongoDB`, `SQL` 47 | * Sequelize (SQL) Table Options: `Timestamps`, `Paranoid`, `Pluralized Names` 48 | * Authentication boilerplate: `Yes`, `No` 49 | * oAuth integrations: `Facebook` `Twitter` `Google` 50 | * Socket.io integration: `Yes`, `No` 51 | 52 | ## Generators 53 | 54 | Available generators: 55 | 56 | * App 57 | - [expressjs-api](#app) (aka [expressjs-api:app](#app)) 58 | * Server Side 59 | - [expressjs-api:endpoint](#endpoint) 60 | * Deployment 61 | - [expressjs-api:openshift](#openshift) 62 | - [expressjs-api:heroku](#heroku) 63 | 64 | ### App 65 | Sets up a new ExpressJS API Boilerplate with best practices 66 | 67 | Usage: 68 | ```bash 69 | Usage: 70 | yo expressjs-api:app [options] [] 71 | 72 | Options: 73 | -h, --help # Print the generator's options and usage 74 | --skip-cache # Do not remember prompt answers Default: false 75 | --skip-install # Do not install dependencies Default: false 76 | --app-suffix # Allow a custom suffix to be added to the module name Default: App 77 | 78 | Arguments: 79 | name Type: String Required: false 80 | ``` 81 | 82 | Example: 83 | ```bash 84 | yo expressjs-api 85 | ``` 86 | 87 | ### Endpoint 88 | Generates a new API endpoint. 89 | 90 | Usage: 91 | ```bash 92 | Usage: 93 | yo expressjs-api:endpoint [options] 94 | 95 | Options: 96 | -h, --help # Print the generator's options and usage 97 | --skip-cache # Do not remember prompt answers Default: false 98 | --route # URL for the endpoint 99 | --models # Specify which model(s) to use 100 | --endpointDirectory # Parent directory for enpoints 101 | 102 | Arguments: 103 | name Type: String Required: true 104 | ``` 105 | 106 | Example: 107 | ```bash 108 | yo expressjs-api:endpoint message 109 | [?] What will the url of your endpoint be? /api/messages 110 | ``` 111 | 112 | Produces: 113 | 114 | server/api/message/index.js 115 | server/api/message/index.spec.js 116 | server/api/message/message.controller.js 117 | server/api/message/message.integration.js 118 | server/api/message/message.model.js (optional) 119 | server/api/message/message.events.js (optional) 120 | server/api/message/message.socket.js (optional) 121 | 122 | ###Openshift 123 | 124 | Deploying to OpenShift can be done in just a few steps: 125 | 126 | yo expressjs-api:openshift 127 | 128 | A live application URL will be available in the output. 129 | 130 | > **oAuth** 131 | > 132 | > If you're using any oAuth strategies, you must set environment variables for your selected oAuth. For example, if we're using Facebook oAuth we would do this : 133 | > 134 | > rhc set-env FACEBOOK_ID=id -a my-openshift-app 135 | > rhc set-env FACEBOOK_SECRET=secret -a my-openshift-app 136 | > 137 | > You will also need to set `DOMAIN` environment variable: 138 | > 139 | > rhc set-env DOMAIN=.rhcloud.com 140 | > 141 | > # or (if you're using it): 142 | > 143 | > rhc set-env DOMAIN= 144 | > 145 | > After you've set the required environment variables, restart the server: 146 | > 147 | > rhc app-restart -a my-openshift-app 148 | 149 | To make your deployment process easier consider using [grunt-build-control](https://github.com/robwierzbowski/grunt-build-control). 150 | 151 | **Pushing Updates** 152 | 153 | grunt 154 | 155 | Commit and push the resulting build, located in your dist folder: 156 | 157 | grunt buildcontrol:openshift 158 | 159 | ### Heroku 160 | 161 | Deploying to heroku only takes a few steps. 162 | 163 | yo expressjs-api:heroku 164 | 165 | To work with your new heroku app using the command line, you will need to run any `heroku` commands from the `dist` folder. 166 | 167 | 168 | If you're using mongoDB you will need to add a database to your app: 169 | 170 | heroku addons:create mongolab 171 | 172 | Your app should now be live. To view it run `heroku open`. 173 | 174 | > 175 | > If you're using any oAuth strategies, you must set environment variables for your selected oAuth. For example, if we're using **Facebook** oAuth we would do this : 176 | > 177 | > heroku config:set FACEBOOK_ID=id 178 | > heroku config:set FACEBOOK_SECRET=secret 179 | > 180 | > You will also need to set `DOMAIN` environment variable: 181 | > 182 | > heroku config:set DOMAIN=.herokuapp.com 183 | > 184 | > # or (if you're using it): 185 | > 186 | > heroku config:set DOMAIN= 187 | > 188 | 189 | To make your deployment process easier consider using [grunt-build-control](https://github.com/robwierzbowski/grunt-build-control). 190 | 191 | #### Pushing Updates 192 | 193 | grunt 194 | 195 | Commit and push the resulting build, located in your dist folder: 196 | 197 | grunt buildcontrol:heroku 198 | 199 | ## Configuration 200 | Yeoman generated projects can be further tweaked according to your needs by modifying project files appropriately. 201 | 202 | A `.yo-rc` file is generated for helping you copy configuration across projects, and to allow you to keep track of your settings. You can change this as you see fit. 203 | 204 | ## Testing 205 | 206 | Running `grunt test` will run the client and server unit tests with karma and mocha. 207 | 208 | Use `grunt test:server` to only run server tests. 209 | 210 | **Protractor tests** 211 | 212 | To setup protractor e2e tests, you must first run 213 | 214 | `npm run update-webdriver` 215 | 216 | Use `grunt test:e2e` to have protractor go through tests located in the `e2e` folder. 217 | 218 | **Code Coverage** 219 | 220 | Use `grunt test:coverage` to run mocha-istanbul and generate code coverage reports. 221 | 222 | `coverage/server` will be populated with `e2e` and `unit` folders containing the `lcov` reports. 223 | 224 | The coverage taget has 3 available options: 225 | - `test:coverage:unit` generate server unit test coverage 226 | - `test:coverage:e2e` generate server e2e test coverage 227 | - `test:coverage:check` combine the coverage reports and check against predefined thresholds 228 | 229 | * *when no option is given `test:coverage` runs all options in the above order* 230 | 231 | **Debugging** 232 | 233 | Use `grunt serve:debug` for a more debugging-friendly environment. 234 | 235 | ## Environment Variables 236 | 237 | Keeping your app secrets and other sensitive information in source control isn't a good idea. To have grunt launch your app with specific environment variables, add them to the git ignored environment config file: `server/config/local.env.js`. 238 | 239 | ## Project Structure 240 | 241 | Overview 242 | 243 | └── server 244 | ├── api - Our apps server api 245 | ├── auth - For handling authentication with different auth strategies 246 | ├── components - Our reusable or app-wide components 247 | ├── config - Where we do the bulk of our apps configuration 248 | │ └── local.env.js - Keep our environment variables out of source control 249 | │   └── environment - Configuration specific to the node environment 250 | 251 | An example server component in `server/api` 252 | 253 | thing 254 | ├── index.js - Routes 255 | ├── thing.controller.js - Controller for our `thing` endpoint 256 | ├── thing.model.js - Database model 257 | ├── thing.socket.js - Register socket events 258 | └── thing.spec.js - Test 259 | 260 | ## Contribute 261 | 262 | See the [contributing docs](https://github.com/ioneyed/generator-expressjs-api/blob/master/contributing.md) 263 | 264 | This project has 2 main branches: `master` and `canary`. The `master` branch is where the current stable code lives and should be used for production setups. The `canary` branch is the main development branch, this is where PRs should be submitted to (backport fixes may be applied to `master`). 265 | 266 | By separating the current stable code from the cutting-edge development we hope to provide a stable and efficient workflow for users and developers alike. 267 | 268 | When submitting an issue, please follow the [guidelines](https://github.com/yeoman/yeoman/blob/master/contributing.md#issue-submission). Especially important is to make sure Yeoman is up-to-date, and providing the command or commands that cause the issue. 269 | 270 | When submitting a bugfix, try to write a test that exposes the bug and fails before applying your fix. Submit the test alongside the fix. 271 | 272 | When submitting a new feature, add tests that cover the feature. 273 | 274 | See the `travis.yml` for configuration required to run tests. 275 | 276 | ## License 277 | 278 | [BSD license](http://opensource.org/licenses/bsd-license.php) 279 | -------------------------------------------------------------------------------- /route/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var yeoman = require('yeoman-generator'); 3 | 4 | var Generator = yeoman.generators.Base.extend({ 5 | compose: function() { 6 | this.composeWith('ng-component:route', {arguments: this.arguments}, { local: require.resolve('generator-ng-component/route') }); 7 | } 8 | }); 9 | 10 | module.exports = Generator; 11 | -------------------------------------------------------------------------------- /scripts/sauce_connect_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Setup and start Sauce Connect for your TravisCI build 4 | # This script requires your .travis.yml to include the following two private env variables: 5 | # SAUCE_USERNAME 6 | # SAUCE_ACCESS_KEY 7 | # Follow the steps at https://saucelabs.com/opensource/travis to set that up. 8 | # https://gist.githubusercontent.com/santiycr/4683e262467c0a84d857/raw/1254ace59257e341ab200cbc8946e626756f079f/sauce-connect.sh 9 | 10 | if [ -z "${SAUCE_USERNAME}" ] || [ -z "${SAUCE_ACCESS_KEY}" ]; then 11 | echo "This script can't run without your Sauce credentials" 12 | echo "Please set SAUCE_USERNAME and SAUCE_ACCESS_KEY env variables" 13 | echo "export SAUCE_USERNAME=ur-username" 14 | echo "export SAUCE_ACCESS_KEY=ur-access-key" 15 | exit 1 16 | fi 17 | 18 | SAUCE_TMP_DIR="$(mktemp -d -t sc.XXXX)" 19 | echo "Using temp dir $SAUCE_TMP_DIR" 20 | pushd $SAUCE_TMP_DIR 21 | 22 | SAUCE_CONNECT_PLATFORM=$(uname | sed -e 's/Darwin/osx/' -e 's/Linux/linux/') 23 | case "${SAUCE_CONNECT_PLATFORM}" in 24 | linux) 25 | SC_DISTRIBUTION_FMT=tar.gz;; 26 | *) 27 | SC_DISTRIBUTION_FMT=zip;; 28 | esac 29 | SC_DISTRIBUTION=sc-latest-${SAUCE_CONNECT_PLATFORM}.${SC_DISTRIBUTION_FMT} 30 | SC_READYFILE=sauce-connect-ready-$RANDOM 31 | SC_LOGFILE=$HOME/sauce-connect.log 32 | if [ ! -z "${TRAVIS_JOB_NUMBER}" ]; then 33 | SC_TUNNEL_ID="-i ${TRAVIS_JOB_NUMBER}" 34 | fi 35 | echo "Downloading Sauce Connect" 36 | wget https://saucelabs.com/downloads/${SC_DISTRIBUTION} 37 | SC_DIR=$(tar -ztf ${SC_DISTRIBUTION} | head -n1) 38 | 39 | echo "Extracting Sauce Connect" 40 | case "${SC_DISTRIBUTION_FMT}" in 41 | tar.gz) 42 | tar zxf $SC_DISTRIBUTION;; 43 | zip) 44 | unzip $SC_DISTRIBUTION;; 45 | esac 46 | 47 | echo "Starting Sauce Connect" 48 | ${SC_DIR}/bin/sc \ 49 | ${SC_TUNNEL_ID} \ 50 | -f ${SC_READYFILE} \ 51 | -l ${SC_LOGFILE} & 52 | 53 | echo "Waiting for Sauce Connect readyfile" 54 | while [ ! -f ${SC_READYFILE} ]; do 55 | sleep .5 56 | done 57 | 58 | unset SAUCE_CONNECT_PLATFORM SAUCE_TMP_DIR SC_DIR SC_DISTRIBUTION SC_READYFILE SC_LOGFILE SC_TUNNEL_ID 59 | 60 | popd 61 | -------------------------------------------------------------------------------- /service/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var yeoman = require('yeoman-generator'); 3 | 4 | var Generator = yeoman.generators.Base.extend({ 5 | compose: function() { 6 | this.composeWith('ng-component:service', {arguments: this.arguments}, { local: require.resolve('generator-ng-component/service') }); 7 | } 8 | }); 9 | 10 | module.exports = Generator; 11 | -------------------------------------------------------------------------------- /task-utils/changelog-templates/commit.hbs: -------------------------------------------------------------------------------- 1 | {{#if subScope}} {{/if}}*{{#if scope}}{{#unless subScope}} **{{index}}{{scope}}:**{{/unless}}{{/if}} {{#unless leadScope}}{{#if subject}}{{subject}}{{else}}{{header}}{{/if}}{{/unless}}{{#if leadScope}}{{#if subject}} 2 | * {{subject}}{{else}}{{header}}{{/if}}{{/if}} 3 | 4 | {{~!-- commit hash --}} {{#if @root.linkReferences}}([{{hash}}]({{@root.host}}/{{#if @root.owner}}{{@root.owner}}/{{/if}}{{@root.repository}}/{{@root.commit}}/{{hash}})){{else}}{{hash~}}{{/if}} 5 | 6 | {{~!-- commit references --}}{{#if references}}, closes{{~#each references}} {{#if @root.linkReferences}}[{{#if this.owner}}{{this.owner}}/{{/if}}{{this.repository}}#{{this.issue}}]({{@root.host}}/{{#if this.repository}}{{#if this.owner}}{{this.owner}}/{{/if}}{{this.repository}}{{else}}{{#if @root.owner}}{{@root.owner}}/{{/if}}{{@root.repository}}{{/if}}/{{@root.issue}}/{{this.issue}}){{else}}{{this.repository}}#{{this.issue}}{{/if}}{{/each}}{{/if}} 7 | -------------------------------------------------------------------------------- /task-utils/grunt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var Q = require('q'); 6 | 7 | exports = module.exports = function(grunt) { 8 | var self; 9 | return self = { 10 | gitCmd: function(args, opts, done) { 11 | grunt.util.spawn({ 12 | cmd: process.platform === 'win32' ? 'git.cmd' : 'git', 13 | args: args, 14 | opts: opts || {} 15 | }, done); 16 | }, 17 | 18 | gitCmdAsync: function(args, opts) { 19 | return function() { 20 | var deferred = Q.defer(); 21 | self.gitCmd(args, opts, function(err) { 22 | if (err) { return deferred.reject(err); } 23 | deferred.resolve(); 24 | }); 25 | return deferred.promise; 26 | }; 27 | }, 28 | 29 | conventionalChangelog: { 30 | finalizeContext: function(context, writerOpts, commits, keyCommit) { 31 | var gitSemverTags = context.gitSemverTags; 32 | var commitGroups = context.commitGroups; 33 | 34 | if ((!context.currentTag || !context.previousTag) && keyCommit) { 35 | var match = /tag:\s*(.+?)[,\)]/gi.exec(keyCommit.gitTags); 36 | var currentTag = context.currentTag = context.currentTag || match ? match[1] : null; 37 | var index = gitSemverTags.indexOf(currentTag); 38 | var previousTag = context.previousTag = gitSemverTags[index + 1]; 39 | 40 | if (!previousTag) { 41 | if (options.append) { 42 | context.previousTag = context.previousTag || commits[0] ? commits[0].hash : null; 43 | } else { 44 | context.previousTag = context.previousTag || commits[commits.length - 1] ? commits[commits.length - 1].hash : null; 45 | } 46 | } 47 | } else { 48 | context.previousTag = context.previousTag || gitSemverTags[0]; 49 | context.currentTag = context.currentTag || 'v' + context.version; 50 | } 51 | 52 | if (typeof context.linkCompare !== 'boolean' && context.previousTag && context.currentTag) { 53 | context.linkCompare = true; 54 | } 55 | 56 | if (Array.isArray(commitGroups)) { 57 | for (var i = 0, commitGroupsLength = commitGroups.length; i < commitGroupsLength; i++) { 58 | var commits = commitGroups[i].commits; 59 | if (Array.isArray(commits)) { 60 | for (var n = 1, commitsLength = commits.length; n < commitsLength; n++) { 61 | var commit = commits[n], prevCommit = commits[n - 1]; 62 | if (commit.scope && commit.scope === prevCommit.scope) { 63 | commit.subScope = true; 64 | if (prevCommit.scope && !prevCommit.subScope) { 65 | prevCommit.leadScope = true; 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | return context; 73 | }, 74 | commitPartial: fs.readFileSync(path.resolve(__dirname, 'changelog-templates', 'commit.hbs')).toString() 75 | } 76 | }; 77 | }; 78 | -------------------------------------------------------------------------------- /test/fixtures/.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-expressjs-api": { 3 | "endpointDirectory": "server/api/", 4 | "insertRoutes": true, 5 | "registerRoutesFile": "server/routes.js", 6 | "routesNeedle": "// Insert routes below", 7 | "routesBase": "/api/", 8 | "pluralizeRoutes": true, 9 | "insertSockets": true, 10 | "registerSocketsFile": "server/config/socketio.js", 11 | "socketsNeedle": "// Insert sockets below", 12 | "insertModels": true, 13 | "registerModelsFile": "server/sqldb/index.js", 14 | "modelsNeedle": "// Insert models below", 15 | "filters": { 16 | "babel": true, 17 | "html": true, 18 | "less": true, 19 | "uirouter": true, 20 | "bootstrap": false, 21 | "uibootstrap": false, 22 | "socketio": true, 23 | "auth": true, 24 | "models": true, 25 | "mongooseModels": true, 26 | "mongoose": true, 27 | "oauth": true, 28 | "googleAuth": true, 29 | "grunt": true, 30 | "mocha": true, 31 | "jasmine": false, 32 | "should": true, 33 | "expect": false 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/test-file-creation.js: -------------------------------------------------------------------------------- 1 | /*global describe, beforeEach, it */ 2 | 'use strict'; 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var exec = require('child_process').exec; 6 | var helpers = require('yeoman-generator').test; 7 | var assert = require('yeoman-assert'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var recursiveReadDir = require('recursive-readdir'); 11 | 12 | describe('expressjs-api generator', function () { 13 | var gen, defaultOptions = { 14 | testing: 'mocha', 15 | chai: 'expect', 16 | odms: [ 'mongoose' ], 17 | auth: true, 18 | oauth: [], 19 | socketio: true 20 | }, dependenciesInstalled = false; 21 | 22 | function copySync(s, d) { fs.writeFileSync(d, fs.readFileSync(s)); } 23 | 24 | function generatorTest(generatorType, name, mockPrompt, callback) { 25 | gen.run(function () { 26 | var afGenerator; 27 | var deps = [path.join('../..', generatorType)]; 28 | afGenerator = helpers.createGenerator('expressjs-api:' + generatorType, deps, [name], { 29 | skipInstall: true 30 | }); 31 | 32 | helpers.mockPrompt(afGenerator, mockPrompt); 33 | afGenerator.run(function () { 34 | callback(); 35 | }); 36 | }); 37 | } 38 | 39 | /** 40 | * Assert that only an array of files exist at a given path 41 | * 42 | * @param {Array} expectedFiles - array of files 43 | * @param {Function} done - callback(error{Error}) 44 | * @param {String} topLevelPath - top level path to assert files at (optional) 45 | * @param {Array} skip - array of paths to skip/ignore (optional) 46 | * 47 | */ 48 | function assertOnlyFiles(expectedFiles, done, topLevelPath, skip) { 49 | topLevelPath = topLevelPath || './'; 50 | skip = skip || ['node_modules']; 51 | 52 | recursiveReadDir(topLevelPath, skip, function(err, actualFiles) { 53 | if (err) { return done(err); } 54 | var files = actualFiles.concat(); 55 | 56 | expectedFiles.forEach(function(file, i) { 57 | var index = files.indexOf(path.normalize(file)); 58 | if (index >= 0) { 59 | files.splice(index, 1); 60 | } 61 | }); 62 | 63 | if (files.length !== 0) { 64 | err = new Error('unexpected files found'); 65 | err.expected = expectedFiles.join('\n'); 66 | err.actual = files.join('\n'); 67 | return done(err); 68 | } 69 | 70 | done(); 71 | }); 72 | } 73 | 74 | /** 75 | * Exec a command and run test assertion(s) based on command type 76 | * 77 | * @param {String} cmd - the command to exec 78 | * @param {Object} self - context of the test 79 | * @param {Function} cb - callback() 80 | * @param {String} endpoint - endpoint to generate before exec (optional) 81 | * @param {Number} timeout - timeout for the exec and test (optional) 82 | * 83 | */ 84 | function runTest(cmd, self, cb) { 85 | var args = Array.prototype.slice.call(arguments), 86 | endpoint = (args[3] && typeof args[3] === 'string') ? args.splice(3, 1)[0] : null, 87 | timeout = (args[3] && typeof args[3] === 'number') ? args.splice(3, 1)[0] : null; 88 | 89 | self.timeout(timeout || 60000); 90 | 91 | var execFn = function() { 92 | var cmdCode; 93 | var cp = exec(cmd, function(error, stdout, stderr) { 94 | if(cmdCode !== 0) { 95 | console.error(stdout); 96 | throw new Error('Error running command: ' + cmd); 97 | } 98 | cb(); 99 | }); 100 | cp.on('exit', function (code) { 101 | cmdCode = code; 102 | }); 103 | }; 104 | 105 | if (endpoint) { 106 | generatorTest('endpoint', endpoint, {}, execFn); 107 | } else { 108 | gen.run(execFn); 109 | } 110 | } 111 | 112 | /** 113 | * Generate an array of files to expect from a set of options 114 | * 115 | * @param {Object} ops - generator options 116 | * @return {Array} - array of files 117 | * 118 | */ 119 | function genFiles(ops) { 120 | var mapping = { 121 | stylesheet: { 122 | sass: 'scss', 123 | stylus: 'styl', 124 | less: 'less', 125 | css: 'css' 126 | }, 127 | markup: { 128 | jade: 'jade', 129 | html: 'html' 130 | }, 131 | script: { 132 | js: 'js' 133 | } 134 | }, 135 | files = []; 136 | 137 | /** 138 | * Generate an array of OAuth files based on type 139 | * 140 | * @param {String} type - type of oauth 141 | * @return {Array} - array of files 142 | * 143 | */ 144 | var oauthFiles = function(type) { 145 | return [ 146 | 'server/auth/' + type + '/index.js', 147 | 'server/auth/' + type + '/passport.js', 148 | ]; 149 | }; 150 | 151 | 152 | var script = mapping.script[ops.script], 153 | markup = mapping.markup[ops.markup], 154 | stylesheet = mapping.stylesheet[ops.stylesheet], 155 | models = ops.models ? ops.models : ops.odms[0]; 156 | 157 | /* Core Files */ 158 | files = files.concat( 159 | 'server/.jshintrc', 160 | 'server/.jshintrc-spec', 161 | 'server/app.js', 162 | 'server/index.js', 163 | 'server/routes.js', 164 | 'server/api/thing/index.js', 165 | 'server/api/thing/index.spec.js', 166 | 'server/api/thing/thing.controller.js', 167 | 'server/api/thing/thing.integration.js', 168 | 'server/components/errors/index.js', 169 | 'server/config/local.env.js', 170 | 'server/config/local.env.sample.js', 171 | 'server/config/express.js', 172 | 'server/config/environment/index.js', 173 | 'server/config/environment/development.js', 174 | 'server/config/environment/production.js', 175 | 'server/config/environment/test.js', 176 | 'server/config/environment/shared.js', 177 | '.buildignore', 178 | '.editorconfig', 179 | '.gitattributes', 180 | '.gitignore', 181 | '.travis.yml', 182 | '.jscsrc', 183 | '.yo-rc.json', 184 | 'Gruntfile.js', 185 | 'package.json', 186 | 'mocha.conf.js', 187 | 'README.md' 188 | ]); 189 | 190 | /* Models - Mongoose or Sequelize */ 191 | if (models) { 192 | files = files.concat([ 193 | 'server/api/thing/thing.model.js', 194 | 'server/api/thing/thing.events.js', 195 | 'server/config/seed.js' 196 | ]); 197 | } 198 | 199 | /* Sequelize */ 200 | if (ops.odms.indexOf('sequelize') !== -1) { 201 | files = files.concat([ 202 | 'server/sqldb/index.js' 203 | ]); 204 | } 205 | 206 | /* Authentication */ 207 | if (ops.auth) { 208 | files = files.concat([ 209 | 'server/api/user/index.js', 210 | 'server/api/user/index.spec.js', 211 | 'server/api/user/user.controller.js', 212 | 'server/api/user/user.integration.js', 213 | 'server/api/user/user.model.js', 214 | 'server/api/user/user.model.spec.js', 215 | 'server/api/user/user.events.js', 216 | 'server/auth/index.js', 217 | 'server/auth/auth.service.js', 218 | 'server/auth/local/index.js', 219 | 'server/auth/local/passport.js', 220 | ]); 221 | } 222 | 223 | if (ops.oauth && ops.oauth.length) { 224 | /* OAuth (see oauthFiles function above) */ 225 | ops.oauth.forEach(function(type, i) { 226 | files = files.concat(oauthFiles(type.replace('Auth', ''))); 227 | }); 228 | } 229 | 230 | /* Socket.IO */ 231 | if (ops.socketio) { 232 | files = files.concat([ 233 | 'server/api/thing/thing.socket.js', 234 | 'server/config/socketio.js' 235 | ]); 236 | } 237 | 238 | return files; 239 | } 240 | 241 | 242 | /** 243 | * Generator tests 244 | */ 245 | 246 | beforeEach(function (done) { 247 | this.timeout(10000); 248 | var deps = [ 249 | '../../endpoint', 250 | ]; 251 | 252 | helpers.testDirectory(path.join(__dirname, 'temp'), function (err) { 253 | if (err) { 254 | return done(err); 255 | } 256 | 257 | gen = helpers.createGenerator('expressjs-api:app', deps, [], { 258 | skipInstall: true 259 | }); 260 | done(); 261 | }.bind(this)); 262 | }); 263 | 264 | describe('making sure test fixtures are present', function() { 265 | 266 | it('should have package.json in fixtures', function() { 267 | assert.file([ 268 | path.join(__dirname, 'fixtures', 'package.json') 269 | ]); 270 | }); 271 | 272 | it('should have all npm packages in fixtures/node_modules', function() { 273 | var packageJson = require('./fixtures/package.json'); 274 | var deps = Object.keys(packageJson.dependencies); 275 | deps = deps.concat(Object.keys(packageJson.devDependencies)); 276 | deps = deps.map(function(dep) { 277 | return path.join(__dirname, 'fixtures', 'node_modules', dep); 278 | }); 279 | assert.file(deps); 280 | }); 281 | 282 | }); 283 | 284 | describe('running app', function() { 285 | 286 | beforeEach(function() { 287 | this.timeout(20000); 288 | fs.symlinkSync(__dirname + '/fixtures/node_modules', __dirname + '/temp/node_modules'); 289 | }); 290 | 291 | describe('with default options', function() { 292 | beforeEach(function() { 293 | helpers.mockPrompt(gen, defaultOptions); 294 | }); 295 | 296 | it('should pass jscs', function(done) { 297 | runTest('grunt jscs', this, done); 298 | }); 299 | 300 | it('should pass lint', function(done) { 301 | runTest('grunt jshint', this, done); 302 | }); 303 | 304 | it('should run server tests successfully', function(done) { 305 | runTest('grunt test:server', this, done); 306 | }); 307 | 308 | it('should pass jscs with generated endpoint', function(done) { 309 | runTest('grunt jscs', this, done, 'foo'); 310 | }); 311 | 312 | it('should pass lint with generated endpoint', function(done) { 313 | runTest('grunt jshint', this, done, 'foo'); 314 | }); 315 | 316 | it('should run server tests successfully with generated endpoint', function(done) { 317 | runTest('grunt test:server', this, done, 'foo'); 318 | }); 319 | 320 | it('should pass lint with generated capitalized endpoint', function(done) { 321 | runTest('grunt jshint', this, done, 'Foo'); 322 | }); 323 | 324 | it('should run server tests successfully with generated capitalized endpoint', function(done) { 325 | runTest('grunt test:server', this, done, 'Foo'); 326 | }); 327 | 328 | it('should pass lint with generated path name endpoint', function(done) { 329 | runTest('grunt jshint', this, done, 'foo/bar'); 330 | }); 331 | 332 | it('should run server tests successfully with generated path name endpoint', function(done) { 333 | runTest('grunt test:server', this, done, 'foo/bar'); 334 | }); 335 | 336 | it('should generate expected files with path name endpoint', function(done) { 337 | runTest('(exit 0)', this, function() { 338 | assert.file([ 339 | 'server/api/foo/bar/index.js', 340 | 'server/api/foo/bar/index.spec.js', 341 | 'server/api/foo/bar/bar.controller.js', 342 | 'server/api/foo/bar/bar.events.js', 343 | 'server/api/foo/bar/bar.integration.js', 344 | 'server/api/foo/bar/bar.model.js', 345 | 'server/api/foo/bar/bar.socket.js' 346 | ]); 347 | done(); 348 | }, 'foo/bar'); 349 | }); 350 | 351 | it('should use existing config if available', function(done) { 352 | this.timeout(60000); 353 | copySync(__dirname + '/fixtures/.yo-rc.json', __dirname + '/temp/.yo-rc.json'); 354 | var gen = helpers.createGenerator('expressjs-api:app', [ 355 | '../../endpoint', 356 | ], [], { 357 | skipInstall: true 358 | }); 359 | helpers.mockPrompt(gen, { 360 | skipConfig: true 361 | }); 362 | gen.run(function () { 363 | assert.file([ 364 | 'server/auth/google/passport.js' 365 | ]); 366 | done(); 367 | }); 368 | }); 369 | 370 | it('should generate expected files', function (done) { 371 | gen.run(function () { 372 | assert.file(genFiles(defaultOptions)); 373 | done(); 374 | }); 375 | }); 376 | 377 | it('should not generate unexpected files', function (done) { 378 | gen.run(function () { 379 | assertOnlyFiles(genFiles(defaultOptions), done); 380 | }); 381 | }); 382 | }); 383 | 384 | describe('with other preprocessors and oauth', function() { 385 | var testOptions = { 386 | testing: 'jasmine', 387 | odms: [ 'mongoose' ], 388 | auth: true, 389 | oauth: ['twitterAuth', 'facebookAuth', 'googleAuth'], 390 | socketio: true, 391 | }; 392 | 393 | beforeEach(function() { 394 | helpers.mockPrompt(gen, testOptions); 395 | }); 396 | 397 | it('should pass jscs', function(done) { 398 | runTest('grunt jscs', this, done); 399 | }); 400 | 401 | it('should pass lint', function(done) { 402 | runTest('grunt jshint', this, done); 403 | }); 404 | 405 | it('should run server tests successfully', function(done) { 406 | runTest('grunt test:server', this, done); 407 | }); 408 | 409 | it('should pass jscs with generated endpoint', function(done) { 410 | runTest('grunt jscs', this, done, 'foo'); 411 | }); 412 | 413 | it('should pass lint with generated snake-case endpoint', function(done) { 414 | runTest('grunt jshint', this, done, 'foo-bar'); 415 | }); 416 | 417 | it('should run server tests successfully with generated snake-case endpoint', function(done) { 418 | runTest('grunt test:server', this, done, 'foo-bar'); 419 | }); 420 | 421 | it('should generate expected files', function (done) { 422 | gen.run(function () { 423 | assert.file(genFiles(testOptions)); 424 | done(); 425 | }); 426 | }); 427 | 428 | it('should not generate unexpected files', function (done) { 429 | gen.run(function () { 430 | assertOnlyFiles(genFiles(testOptions), done); 431 | }); 432 | }); 433 | 434 | }); 435 | 436 | describe('with sequelize models, auth', function() { 437 | var testOptions = { 438 | testing: 'jasmine', 439 | odms: [ 'sequelize' ], 440 | auth: true, 441 | oauth: ['twitterAuth', 'facebookAuth', 'googleAuth'], 442 | socketio: true, 443 | }; 444 | 445 | beforeEach(function() { 446 | helpers.mockPrompt(gen, testOptions); 447 | }); 448 | 449 | it('should pass jscs', function(done) { 450 | runTest('grunt jscs', this, done); 451 | }); 452 | 453 | it('should pass lint', function(done) { 454 | runTest('grunt jshint', this, done); 455 | }); 456 | 457 | it('should run server tests successfully', function(done) { 458 | runTest('grunt test:server', this, done); 459 | }); 460 | 461 | it('should pass jscs with generated endpoint', function(done) { 462 | runTest('grunt jscs', this, done, 'foo'); 463 | }); 464 | 465 | it('should pass lint with generated snake-case endpoint', function(done) { 466 | runTest('grunt jshint', this, done, 'foo-bar'); 467 | }); 468 | 469 | it('should run server tests successfully with generated snake-case endpoint', function(done) { 470 | runTest('grunt test:server', this, done, 'foo-bar'); 471 | }); 472 | 473 | it('should generate expected files', function (done) { 474 | gen.run(function () { 475 | assert.file(genFiles(testOptions)); 476 | done(); 477 | }); 478 | }); 479 | 480 | it('should not generate unexpected files', function (done) { 481 | gen.run(function () { 482 | assertOnlyFiles(genFiles(testOptions), done); 483 | }); 484 | }); 485 | 486 | }); 487 | 488 | describe('with other preprocessors and no server options', function() { 489 | var testOptions = { 490 | testing: 'mocha', 491 | chai: 'should', 492 | odms: [], 493 | auth: false, 494 | oauth: [], 495 | socketio: false, 496 | }; 497 | 498 | beforeEach(function(done) { 499 | helpers.mockPrompt(gen, testOptions); 500 | done(); 501 | }); 502 | 503 | it('should pass jscs', function(done) { 504 | runTest('grunt jscs', this, done); 505 | }); 506 | 507 | it('should pass lint', function(done) { 508 | runTest('grunt jshint', this, done); 509 | }); 510 | 511 | it('should run server tests successfully', function(done) { 512 | runTest('grunt test:server', this, done); 513 | }); 514 | 515 | it('should pass jscs with generated endpoint', function(done) { 516 | runTest('grunt jscs', this, done, 'foo'); 517 | }); 518 | 519 | it('should pass lint with generated endpoint', function(done) { 520 | runTest('grunt jshint', this, done, 'foo'); 521 | }); 522 | 523 | it('should run server tests successfully with generated endpoint', function(done) { 524 | runTest('grunt test:server', this, done, 'foo'); 525 | }); 526 | 527 | it('should generate expected files', function (done) { 528 | gen.run(function () { 529 | assert.file(genFiles(testOptions)); 530 | done(); 531 | }); 532 | }); 533 | 534 | it('should not generate unexpected files', function (done) { 535 | gen.run(function () { 536 | assertOnlyFiles(genFiles(testOptions), done); 537 | }); 538 | }); 539 | }); 540 | }); 541 | }); 542 | -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | import glob from 'glob'; 6 | 7 | function expandFiles(pattern, options) { 8 | options = options || {}; 9 | var cwd = options.cwd || process.cwd(); 10 | return glob.sync(pattern, options).filter(function (filepath) { 11 | return fs.statSync(path.join(cwd, filepath)).isFile(); 12 | }); 13 | } 14 | 15 | export function rewriteFile(args) { 16 | args.path = args.path || process.cwd(); 17 | var fullPath = path.join(args.path, args.file); 18 | 19 | args.haystack = fs.readFileSync(fullPath, 'utf8'); 20 | var body = rewrite(args); 21 | 22 | fs.writeFileSync(fullPath, body); 23 | } 24 | 25 | function escapeRegExp(str) { 26 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); 27 | } 28 | 29 | export function rewrite(args) { 30 | // check if splicable is already in the body text 31 | var re = new RegExp(args.splicable.map(function(line) { 32 | return '\s*' + escapeRegExp(line); 33 | }).join('\n')); 34 | 35 | if (re.test(args.haystack)) { 36 | return args.haystack; 37 | } 38 | 39 | var lines = args.haystack.split('\n'); 40 | 41 | var otherwiseLineIndex = -1; 42 | lines.forEach(function (line, i) { 43 | if (line.indexOf(args.needle) !== -1) { 44 | otherwiseLineIndex = i; 45 | } 46 | }); 47 | if(otherwiseLineIndex === -1) return lines.join('\n'); 48 | 49 | var spaces = 0; 50 | while (lines[otherwiseLineIndex].charAt(spaces) === ' ') { 51 | spaces += 1; 52 | } 53 | 54 | var spaceStr = ''; 55 | while ((spaces -= 1) >= 0) { 56 | spaceStr += ' '; 57 | } 58 | 59 | lines.splice(otherwiseLineIndex + 1, 0, args.splicable.map(function(line) { 60 | return spaceStr + line; 61 | }).join('\n')); 62 | 63 | return lines.join('\n'); 64 | } 65 | 66 | export function appSuffix(self) { 67 | var suffix = self.options['app-suffix']; 68 | return (typeof suffix === 'string') ? self.lodash.classify(suffix) : ''; 69 | } 70 | 71 | export function relativeRequire(to, fr) { 72 | fr = this.destinationPath(fr || this.filePath); 73 | to = this.destinationPath(to); 74 | return path.relative(path.dirname(fr), to) 75 | .replace(/\\/g, '/') // convert win32 separator to posix 76 | .replace(/^(?!\.\.)(.*)/, './$1') // prefix non parent path with ./ 77 | .replace(/[\/\\]index\.js$/, ''); // strip index.js suffix from path 78 | } 79 | 80 | function filterFile(template) { 81 | // Find matches for parans 82 | var filterMatches = template.match(/\(([^)]+)\)/g); 83 | var filters = []; 84 | if(filterMatches) { 85 | filterMatches.forEach(function(filter) { 86 | filters.push(filter.replace('(', '').replace(')', '')); 87 | template = template.replace(filter, ''); 88 | }); 89 | } 90 | 91 | return { name: template, filters: filters }; 92 | } 93 | 94 | function templateIsUsable(self, filteredFile) { 95 | var filters = self.filters || self.config.get('filters'); 96 | var enabledFilters = []; 97 | for(var key in filters) { 98 | if(filters[key]) enabledFilters.push(key); 99 | } 100 | var matchedFilters = self.lodash.intersection(filteredFile.filters, enabledFilters); 101 | // check that all filters on file are matched 102 | if(filteredFile.filters.length && matchedFilters.length !== filteredFile.filters.length) { 103 | return false; 104 | } 105 | return true; 106 | } 107 | 108 | export function processDirectory(source, destination) { 109 | var self = this; 110 | var root = path.isAbsolute(source) ? source : path.join(self.sourceRoot(), source); 111 | var files = expandFiles('**', { dot: true, cwd: root }); 112 | var dest, src; 113 | 114 | files.forEach(function(f) { 115 | var filteredFile = filterFile(f); 116 | if(self.basename) { 117 | filteredFile.name = filteredFile.name.replace('basename', self.basename); 118 | } 119 | if(self.name) { 120 | filteredFile.name = filteredFile.name.replace('name', self.name); 121 | } 122 | var name = filteredFile.name; 123 | var copy = false, stripped; 124 | 125 | src = path.join(root, f); 126 | dest = path.join(destination, name); 127 | 128 | if(path.basename(dest).indexOf('_') === 0) { 129 | stripped = path.basename(dest).replace(/^_/, ''); 130 | dest = path.join(path.dirname(dest), stripped); 131 | } 132 | 133 | if(path.basename(dest).indexOf('!') === 0) { 134 | stripped = path.basename(dest).replace(/^!/, ''); 135 | dest = path.join(path.dirname(dest), stripped); 136 | copy = true; 137 | } 138 | 139 | if(templateIsUsable(self, filteredFile)) { 140 | if(copy) { 141 | self.fs.copy(src, dest); 142 | } else { 143 | self.filePath = dest; 144 | self.fs.copyTpl(src, dest, self); 145 | delete self.filePath; 146 | } 147 | } 148 | }); 149 | } 150 | --------------------------------------------------------------------------------