diff --git a/app/index.js b/app/index.js index 81c9e40b7..15aaf737f 100644 --- a/app/index.js +++ b/app/index.js @@ -145,15 +145,38 @@ var AngularFullstackGenerator = yeoman.generators.Base.extend({ this.log('\n# Server\n'); this.prompt([{ - type: 'confirm', - name: 'mongoose', - message: 'Would you like to use mongoDB with Mongoose for data modeling?' + type: 'checkbox', + name: 'odms', + message: 'What would you like to use for data modeling?', + choices: [ + { + value: 'mongoose', + name: 'Mongoose (MongoDB)', + checked: true + }, + { + value: 'sequelize', + name: 'Sequelize (MySQL, SQLite, MariaDB, PostgreSQL)', + checked: false + } + ] + }, { + type: 'list', + name: 'models', + message: 'What would you like to use for the default models?', + choices: [ 'Mongoose', 'Sequelize' ], + filter: function( val ) { + return val.toLowerCase(); + }, + when: function(answers) { + return answers.odms && answers.odms.length > 1; + } }, { type: 'confirm', name: 'auth', message: 'Would you scaffold out an authentication boilerplate?', when: function (answers) { - return answers.mongoose; + return answers.odms && answers.odms.length !== 0; } }, { type: 'checkbox', @@ -183,15 +206,29 @@ var AngularFullstackGenerator = yeoman.generators.Base.extend({ type: 'confirm', name: 'socketio', message: 'Would you like to use socket.io?', - // to-do: should not be dependent on mongoose + // to-do: should not be dependent on ODMs when: function (answers) { - return answers.mongoose; + return answers.odms && answers.odms.length !== 0; }, default: true }], function (answers) { if(answers.socketio) this.filters.socketio = true; - if(answers.mongoose) this.filters.mongoose = true; if(answers.auth) this.filters.auth = true; + if(answers.odms.length > 0) { + var models; + if(!answers.models) { + models = answers.odms[0]; + } else { + models = answers.models; + } + this.filters.models = true; + this.filters[models + 'Models'] = true; + answers.odms.forEach(function(odm) { + this.filters[odm] = true; + }.bind(this)); + } else { + this.filters.noModels = true; + } if(answers.oauth) { if(answers.oauth.length) this.filters.oauth = true; answers.oauth.forEach(function(oauthStrategy) { @@ -265,6 +302,10 @@ var AngularFullstackGenerator = yeoman.generators.Base.extend({ this.config.set('registerSocketsFile', 'server/config/socketio.js'); this.config.set('socketsNeedle', '// Insert sockets below'); + this.config.set('insertModels', true); + this.config.set('registerModelsFile', 'server/sqldb/index.js'); + this.config.set('modelsNeedle', '// Insert models below'); + this.config.set('filters', this.filters); this.config.forceSave(); }, diff --git a/app/templates/_package.json b/app/templates/_package.json index d33133187..1666ea8b0 100644 --- a/app/templates/_package.json +++ b/app/templates/_package.json @@ -16,7 +16,9 @@ "jade": "~1.2.0",<% } %><% if (filters.html) { %> "ejs": "~0.8.4",<% } %><% if (filters.mongoose) { %> "mongoose": "~3.8.8", - "mongoose-bird": "~0.0.1",<% } %><% if (filters.auth) { %> + "mongoose-bird": "~0.0.1",<% } %><% if (filters.sequelize) { %> + "sequelize": "^2.0.0-rc2", + "sqlite3": "~3.0.2",<% } %><% if (filters.auth) { %> "jsonwebtoken": "^0.3.0", "express-jwt": "^0.1.3", "passport": "~0.2.0", @@ -24,8 +26,8 @@ "passport-facebook": "latest",<% } %><% if (filters.twitterAuth) { %> "passport-twitter": "latest",<% } %><% if (filters.googleAuth) { %> "passport-google-oauth": "latest",<% } %> - "composable-middleware": "^0.3.0", - "connect-mongo": "^0.4.1"<% if (filters.socketio) { %>, + "composable-middleware": "^0.3.0"<% if (filters.mongoose) { %>, + "connect-mongo": "^0.4.1"<% } %><% if (filters.socketio) { %>, "socket.io": "^1.0.6", "socket.io-client": "^1.0.6", "socketio-jwt": "^2.0.2"<% } %> diff --git a/app/templates/client/app/account(auth)/signup/signup.controller(coffee).coffee b/app/templates/client/app/account(auth)/signup/signup.controller(coffee).coffee index 3d96f3bf5..d167b7e30 100644 --- a/app/templates/client/app/account(auth)/signup/signup.controller(coffee).coffee +++ b/app/templates/client/app/account(auth)/signup/signup.controller(coffee).coffee @@ -20,11 +20,17 @@ angular.module '<%= scriptAppName %>' .catch (err) -> err = err.data $scope.errors = {} - +<% if (filters.mongooseModels) { %> # Update validity of form fields that match the mongoose errors angular.forEach err.errors, (error, field) -> form[field].$setValidity 'mongoose', false - $scope.errors[field] = error.message + $scope.errors[field] = error.message<% } + if (filters.sequelizeModels) { %> + # Update validity of form fields that match the sequelize errors + if err.name + angular.forEach err.fields, (field) -> + form[field].$setValidity 'mongoose', false + $scope.errors[field] = err.message<% } %> <% if (filters.oauth) {%> $scope.loginOauth = (provider) -> $window.location.href = '/auth/' + provider<% } %> diff --git a/app/templates/client/app/account(auth)/signup/signup.controller(js).js b/app/templates/client/app/account(auth)/signup/signup.controller(js).js index 7e93a6949..346eb7ea7 100644 --- a/app/templates/client/app/account(auth)/signup/signup.controller(js).js +++ b/app/templates/client/app/account(auth)/signup/signup.controller(js).js @@ -21,12 +21,20 @@ angular.module('<%= scriptAppName %>') .catch(function(err) { err = err.data; $scope.errors = {}; - +<% if (filters.mongooseModels) { %> // Update validity of form fields that match the mongoose errors angular.forEach(err.errors, function(error, field) { form[field].$setValidity('mongoose', false); $scope.errors[field] = error.message; - }); + });<% } + if (filters.sequelizeModels) { %> + // Update validity of form fields that match the sequelize errors + if (err.name) { + angular.forEach(err.fields, function(field) { + form[field].$setValidity('mongoose', false); + $scope.errors[field] = err.message; + }); + }<% } %> }); } }; diff --git a/app/templates/client/app/main/main.controller(coffee).coffee b/app/templates/client/app/main/main.controller(coffee).coffee index 3dffae2f7..4b04a951b 100644 --- a/app/templates/client/app/main/main.controller(coffee).coffee +++ b/app/templates/client/app/main/main.controller(coffee).coffee @@ -7,7 +7,7 @@ angular.module '<%= scriptAppName %>' $http.get('/api/things').success (awesomeThings) -> $scope.awesomeThings = awesomeThings <% if (filters.socketio) { %>socket.syncUpdates 'thing', $scope.awesomeThings<% } %> -<% if (filters.mongoose) { %> +<% if (filters.models) { %> $scope.addThing = -> return if $scope.newThing is '' $http.post '/api/things', diff --git a/app/templates/client/app/main/main.controller(js).js b/app/templates/client/app/main/main.controller(js).js index 8164a90dc..345d9909d 100644 --- a/app/templates/client/app/main/main.controller(js).js +++ b/app/templates/client/app/main/main.controller(js).js @@ -8,7 +8,7 @@ angular.module('<%= scriptAppName %>') $scope.awesomeThings = awesomeThings;<% if (filters.socketio) { %> socket.syncUpdates('thing', $scope.awesomeThings);<% } %> }); -<% if (filters.mongoose) { %> +<% if (filters.models) { %> $scope.addThing = function() { if ($scope.newThing === '') { return; diff --git a/app/templates/e2e/account(auth)/login/login.spec(jasmine).js b/app/templates/e2e/account(auth)/login/login.spec(jasmine).js index d3f0d48ed..e690c2601 100644 --- a/app/templates/e2e/account(auth)/login/login.spec(jasmine).js +++ b/app/templates/e2e/account(auth)/login/login.spec(jasmine).js @@ -1,7 +1,8 @@ 'use strict'; -var config = protractor.getInstance().params; -var UserModel = require(config.serverConfig.root + '/server/api/user/user.model'); +var config = protractor.getInstance().params;<% if (filters.mongooseModels) { %> +var UserModel = require(config.serverConfig.root + '/server/api/user/user.model');<% } %><% if (filters.sequelizeModels) { %> +var UserModel = require(config.serverConfig.root + '/server/sqldb').User;<% } %> describe('Login View', function() { var page; @@ -18,9 +19,11 @@ describe('Login View', function() { }; beforeEach(function(done) { - UserModel.removeAsync() + <% if (filters.mongooseModels) { %>UserModel.removeAsync()<% } + if (filters.sequelizeModels) { %>UserModel.destroy()<% } %> .then(function() { - return UserModel.createAsync(testUser); + <% if (filters.mongooseModels) { %>return UserModel.createAsync(testUser);<% } + if (filters.sequelizeModels) { %>return UserModel.create(testUser);<% } %> }) .then(loadPage) .finally(done); diff --git a/app/templates/e2e/account(auth)/login/login.spec(mocha).js b/app/templates/e2e/account(auth)/login/login.spec(mocha).js index a2c986f32..3335cf3b4 100644 --- a/app/templates/e2e/account(auth)/login/login.spec(mocha).js +++ b/app/templates/e2e/account(auth)/login/login.spec(mocha).js @@ -1,7 +1,8 @@ 'use strict'; -var config = protractor.getInstance().params; -var UserModel = require(config.serverConfig.root + '/server/api/user/user.model'); +var config = protractor.getInstance().params;<% if (filters.mongooseModels) { %> +var UserModel = require(config.serverConfig.root + '/server/api/user/user.model');<% } %><% if (filters.sequelizeModels) { %> +var UserModel = require(config.serverConfig.root + '/server/sqldb').User;<% } %> describe('Login View', function() { var page; @@ -19,15 +20,18 @@ describe('Login View', function() { before(function() { return UserModel - .removeAsync() + <% if (filters.mongooseModels) { %>.removeAsync()<% } + if (filters.sequelizeModels) { %>.destroy()<% } %> .then(function() { - return UserModel.createAsync(testUser); + <% if (filters.mongooseModels) { %>return UserModel.createAsync(testUser);<% } + if (filters.sequelizeModels) { %>return UserModel.create(testUser);<% } %> }) .then(loadPage); }); after(function() { - return UserModel.removeAsync(); + <% if (filters.mongooseModels) { %>return UserModel.removeAsync();<% } + if (filters.sequelizeModels) { %>return UserModel.destroy();<% } %> }); it('should include login form with correct inputs and submit button', function() { diff --git a/app/templates/e2e/account(auth)/logout/logout.spec(jasmine).js b/app/templates/e2e/account(auth)/logout/logout.spec(jasmine).js index f496459d8..f3149865c 100644 --- a/app/templates/e2e/account(auth)/logout/logout.spec(jasmine).js +++ b/app/templates/e2e/account(auth)/logout/logout.spec(jasmine).js @@ -1,7 +1,8 @@ 'use strict'; -var config = protractor.getInstance().params; -var UserModel = require(config.serverConfig.root + '/server/api/user/user.model'); +var config = protractor.getInstance().params;<% if (filters.mongooseModels) { %> +var UserModel = require(config.serverConfig.root + '/server/api/user/user.model');<% } %><% if (filters.sequelizeModels) { %> +var UserModel = require(config.serverConfig.root + '/server/sqldb').User;<% } %> describe('Logout View', function() { var login = function(user) { @@ -16,9 +17,11 @@ describe('Logout View', function() { }; beforeEach(function(done) { - UserModel.removeAsync() + <% if (filters.mongooseModels) { %>UserModel.removeAsync()<% } + if (filters.sequelizeModels) { %>UserModel.destroy()<% } %> .then(function() { - return UserModel.createAsync(testUser); + <% if (filters.mongooseModels) { %>return UserModel.createAsync(testUser);<% } + if (filters.sequelizeModels) { %>return UserModel.create(testUser);<% } %> }) .then(function() { return login(testUser); diff --git a/app/templates/e2e/account(auth)/logout/logout.spec(mocha).js b/app/templates/e2e/account(auth)/logout/logout.spec(mocha).js index 3adba51f8..85d92cf75 100644 --- a/app/templates/e2e/account(auth)/logout/logout.spec(mocha).js +++ b/app/templates/e2e/account(auth)/logout/logout.spec(mocha).js @@ -1,7 +1,8 @@ 'use strict'; -var config = protractor.getInstance().params; -var UserModel = require(config.serverConfig.root + '/server/api/user/user.model'); +var config = protractor.getInstance().params;<% if (filters.mongooseModels) { %> +var UserModel = require(config.serverConfig.root + '/server/api/user/user.model');<% } %><% if (filters.sequelizeModels) { %> +var UserModel = require(config.serverConfig.root + '/server/sqldb').User;<% } %> describe('Logout View', function() { var login = function(user) { @@ -17,9 +18,11 @@ describe('Logout View', function() { beforeEach(function() { return UserModel - .removeAsync() + <% if (filters.mongooseModels) { %>.removeAsync()<% } + if (filters.sequelizeModels) { %>.destroy()<% } %> .then(function() { - return UserModel.createAsync(testUser); + <% if (filters.mongooseModels) { %>return UserModel.createAsync(testUser);<% } + if (filters.sequelizeModels) { %>return UserModel.create(testUser);<% } %> }) .then(function() { return login(testUser); @@ -27,7 +30,8 @@ describe('Logout View', function() { }); after(function() { - return UserModel.removeAsync(); + <% if (filters.mongooseModels) { %>return UserModel.removeAsync();<% } + if (filters.sequelizeModels) { %>return UserModel.destroy();<% } %> }) describe('with local auth', function() { diff --git a/app/templates/e2e/account(auth)/signup/signup.spec(jasmine).js b/app/templates/e2e/account(auth)/signup/signup.spec(jasmine).js index 856786e4a..abbfc98a4 100644 --- a/app/templates/e2e/account(auth)/signup/signup.spec(jasmine).js +++ b/app/templates/e2e/account(auth)/signup/signup.spec(jasmine).js @@ -1,7 +1,8 @@ 'use strict'; -var config = protractor.getInstance().params; -var UserModel = require(config.serverConfig.root + '/server/api/user/user.model'); +var config = protractor.getInstance().params;<% if (filters.mongooseModels) { %> +var UserModel = require(config.serverConfig.root + '/server/api/user/user.model');<% } %><% if (filters.sequelizeModels) { %> +var UserModel = require(config.serverConfig.root + '/server/sqldb').User;<% } %> describe('Signup View', function() { var page; @@ -35,7 +36,8 @@ describe('Signup View', function() { describe('with local auth', function() { it('should signup a new user, log them in, and redirecting to "/"', function(done) { - UserModel.remove(function() { + <% if (filters.mongooseModels) { %>UserModel.remove(function() {<% } + if (filters.sequelizeModels) { %>UserModel.destroy().then(function() {<% } %> page.signup(testUser); var navbar = require('../../components/navbar/navbar.po'); diff --git a/app/templates/e2e/account(auth)/signup/signup.spec(mocha).js b/app/templates/e2e/account(auth)/signup/signup.spec(mocha).js index 6a6b0a775..5351ea75d 100644 --- a/app/templates/e2e/account(auth)/signup/signup.spec(mocha).js +++ b/app/templates/e2e/account(auth)/signup/signup.spec(mocha).js @@ -1,7 +1,8 @@ 'use strict'; -var config = protractor.getInstance().params; -var UserModel = require(config.serverConfig.root + '/server/api/user/user.model'); +var config = protractor.getInstance().params;<% if (filters.mongooseModels) { %> +var UserModel = require(config.serverConfig.root + '/server/api/user/user.model');<% } %><% if (filters.sequelizeModels) { %> +var UserModel = require(config.serverConfig.root + '/server/sqldb').User;<% } %> describe('Signup View', function() { var page; @@ -22,7 +23,8 @@ describe('Signup View', function() { }); after(function() { - return UserModel.removeAsync(); + <% if (filters.mongooseModels) { %>return UserModel.removeAsync();<% } + if (filters.sequelizeModels) { %>return UserModel.destroy();<% } %> }); it('should include signup form with correct inputs and submit button', function() { @@ -39,7 +41,8 @@ describe('Signup View', function() { describe('with local auth', function() { it('should signup a new user, log them in, and redirecting to "/"', function(done) { - UserModel.remove(function() { + <% if (filters.mongooseModels) { %>UserModel.remove(function() {<% } + if (filters.sequelizeModels) { %>UserModel.destroy().then(function() {<% } %> page.signup(testUser); var navbar = require('../../components/navbar/navbar.po'); diff --git a/app/templates/protractor.conf.js b/app/templates/protractor.conf.js index f0061ff86..a5c42a390 100644 --- a/app/templates/protractor.conf.js +++ b/app/templates/protractor.conf.js @@ -75,10 +75,10 @@ var config = { ); <% } %> var serverConfig = config.params.serverConfig; - +<% if (filters.mongoose) { %> // Setup mongo for tests var mongoose = require('mongoose-bird')(); - mongoose.connect(serverConfig.mongo.uri, serverConfig.mongo.options); // Connect to database + mongoose.connect(serverConfig.mongo.uri, serverConfig.mongo.options); // Connect to database<% } %> } }; diff --git a/app/templates/server/api/thing/index.js b/app/templates/server/api/thing/index.js index ea697d38b..fde8f3968 100644 --- a/app/templates/server/api/thing/index.js +++ b/app/templates/server/api/thing/index.js @@ -5,7 +5,7 @@ var controller = require('./thing.controller'); var router = express.Router(); -router.get('/', controller.index);<% if (filters.mongoose) { %> +router.get('/', controller.index);<% if (filters.models) { %> router.get('/:id', controller.show); router.post('/', controller.create); router.put('/:id', controller.update); diff --git a/app/templates/server/api/thing/index.spec.js b/app/templates/server/api/thing/index.spec.js index 899b22d79..91f02f6df 100644 --- a/app/templates/server/api/thing/index.spec.js +++ b/app/templates/server/api/thing/index.spec.js @@ -3,7 +3,7 @@ var proxyquire = require('proxyquire').noPreserveCache(); var thingCtrlStub = { - index: 'thingCtrl.index'<% if (filters.mongoose) { %>, + index: 'thingCtrl.index'<% if (filters.models) { %>, show: 'thingCtrl.show', create: 'thingCtrl.create', update: 'thingCtrl.update', @@ -11,7 +11,7 @@ var thingCtrlStub = { }; var routerStub = { - get: sinon.spy()<% if (filters.mongoose) { %>, + get: sinon.spy()<% if (filters.models) { %>, put: sinon.spy(), patch: sinon.spy(), post: sinon.spy(), @@ -42,7 +42,7 @@ describe('Thing API Router:', function() { .should.have.been.calledOnce; }); - });<% if (filters.mongoose) { %> + });<% if (filters.models) { %> describe('GET /api/things/:id', function() { diff --git a/app/templates/server/api/thing/thing.controller.js b/app/templates/server/api/thing/thing.controller(models).js similarity index 52% rename from app/templates/server/api/thing/thing.controller.js rename to app/templates/server/api/thing/thing.controller(models).js index 710c7cd38..a14fbd4b3 100644 --- a/app/templates/server/api/thing/thing.controller.js +++ b/app/templates/server/api/thing/thing.controller(models).js @@ -7,10 +7,12 @@ * DELETE /things/:id -> destroy */ -'use strict';<% if (filters.mongoose) { %> +'use strict'; -var _ = require('lodash'); -var Thing = require('./thing.model'); +var _ = require('lodash');<% if (filters.mongooseModels) { %> +var Thing = require('./thing.model');<% } %><% if (filters.sequelizeModels) { %> +var sqldb = require('../../sqldb') +var Thing = sqldb.Thing;<% } %> function handleError(res, statusCode) { statusCode = statusCode || 500; @@ -40,9 +42,11 @@ function handleEntityNotFound(res) { function saveUpdates(updates) { return function(entity) { - var updated = _.merge(entity, updates); + <% if (filters.mongooseModels) { %>var updated = _.merge(entity, updates); return updated.saveAsync() - .spread(function(updated) { + .spread(function(updated) {<% } + if (filters.sequelizeModels) { %>return entity.updateAttributes(updates) + .then(function(updated) {<% } %> return updated; }); }; @@ -51,48 +55,31 @@ function saveUpdates(updates) { function removeEntity(res) { return function(entity) { if (entity) { - return entity.removeAsync() + <% if (filters.mongooseModels) { %>return entity.removeAsync()<% } + if (filters.sequelizeModels) { %>return entity.destroy()<% } %> .then(function() { return res.send(204); }); } }; -}<% } %> +} // Get list of things -exports.index = function(req, res) {<% if(!filters.mongoose) { %> - res.json([{ - name : 'Development Tools', - info : 'Integration with popular tools such as Bower, Grunt, Karma, Mocha, JSHint, ' + - 'Node Inspector, Livereload, Protractor, Jade, Stylus, Sass, CoffeeScript, and Less.' - }, { - name : 'Server and Client integration', - info : 'Built with a powerful and fun stack: MongoDB, Express, AngularJS, and Node.' - }, { - name : 'Smart Build System', - info : 'Build system ignores `spec` files, allowing you to keep tests alongside code. ' + - 'Automatic injection of scripts and styles into your index.html' - }, { - name : 'Modular Structure', - info : 'Best practice client and server structures allow for more code reusability and ' + - 'maximum scalability' - }, { - name : 'Optimized Build', - info : 'Build process packs up your templates as a single JavaScript payload, minifies ' + - 'your scripts/css/images, and rewrites asset names for caching.' - }, { - name : 'Deployment Ready', - info : 'Easily deploy your app to Heroku or Openshift with the heroku and openshift ' + - 'subgenerators' - }]);<% } if (filters.mongoose) { %> - Thing.findAsync() +exports.index = function(req, res) { + <% if (filters.mongooseModels) { %>Thing.findAsync()<% } + if (filters.sequelizeModels) { %>Thing.findAll()<% } %> .then(responseWithResult(res)) - .catch(handleError(res));<% } %> -};<% if (filters.mongoose) { %> + .catch(handleError(res)); +}; // Get a single thing exports.show = function(req, res) { - Thing.findByIdAsync(req.params.id) + <% if (filters.mongooseModels) { %>Thing.findByIdAsync(req.params.id)<% } + if (filters.sequelizeModels) { %>Thing.find({ + where: { + _id: req.params.id + } + })<% } %> .then(handleEntityNotFound(res)) .then(responseWithResult(res)) .catch(handleError(res)); @@ -100,7 +87,8 @@ exports.show = function(req, res) { // Creates a new thing in the DB. exports.create = function(req, res) { - Thing.createAsync(req.body) + <% if (filters.mongooseModels) { %>Thing.createAsync(req.body)<% } + if (filters.sequelizeModels) { %>Thing.create(req.body)<% } %> .then(responseWithResult(res, 201)) .catch(handleError(res)); }; @@ -110,7 +98,12 @@ exports.update = function(req, res) { if (req.body._id) { delete req.body._id; } - Thing.findByIdAsync(req.params.id) + <% if (filters.mongooseModels) { %>Thing.findByIdAsync(req.params.id)<% } + if (filters.sequelizeModels) { %>Thing.find({ + where: { + _id: req.params.id + } + })<% } %> .then(handleEntityNotFound(res)) .then(saveUpdates(req.body)) .then(responseWithResult(res)) @@ -119,8 +112,13 @@ exports.update = function(req, res) { // Deletes a thing from the DB. exports.destroy = function(req, res) { - Thing.findByIdAsync(req.params.id) + <% if (filters.mongooseModels) { %>Thing.findByIdAsync(req.params.id)<% } + if (filters.sequelizeModels) { %>Thing.find({ + where: { + _id: req.params.id + } + })<% } %> .then(handleEntityNotFound(res)) .then(removeEntity(res)) .catch(handleError(res)); -};<% } %> +}; diff --git a/app/templates/server/api/thing/thing.controller(noModels).js b/app/templates/server/api/thing/thing.controller(noModels).js new file mode 100644 index 000000000..02b2410c5 --- /dev/null +++ b/app/templates/server/api/thing/thing.controller(noModels).js @@ -0,0 +1,38 @@ +/** + * Using Rails-like standard naming convention for endpoints. + * GET /things -> index + * POST /things -> create + * GET /things/:id -> show + * PUT /things/:id -> update + * DELETE /things/:id -> destroy + */ + +'use strict'; + +// Get list of things +exports.index = function(req, res) { + res.json([{ + name : 'Development Tools', + info : 'Integration with popular tools such as Bower, Grunt, Karma, Mocha, JSHint, ' + + 'Node Inspector, Livereload, Protractor, Jade, Stylus, Sass, CoffeeScript, and Less.' + }, { + name : 'Server and Client integration', + info : 'Built with a powerful and fun stack: MongoDB, Express, AngularJS, and Node.' + }, { + name : 'Smart Build System', + info : 'Build system ignores `spec` files, allowing you to keep tests alongside code. ' + + 'Automatic injection of scripts and styles into your index.html' + }, { + name : 'Modular Structure', + info : 'Best practice client and server structures allow for more code reusability and ' + + 'maximum scalability' + }, { + name : 'Optimized Build', + info : 'Build process packs up your templates as a single JavaScript payload, minifies ' + + 'your scripts/css/images, and rewrites asset names for caching.' + }, { + name : 'Deployment Ready', + info : 'Easily deploy your app to Heroku or Openshift with the heroku and openshift ' + + 'subgenerators' + }]); +}; diff --git a/app/templates/server/api/thing/thing.integration.js b/app/templates/server/api/thing/thing.integration.js index de7ed7ff5..3eb5d05d8 100644 --- a/app/templates/server/api/thing/thing.integration.js +++ b/app/templates/server/api/thing/thing.integration.js @@ -1,7 +1,7 @@ 'use strict'; var app = require('../../app'); -var request = require('supertest');<% if (filters.mongoose) { %> +var request = require('supertest');<% if (filters.models) { %> var newThing;<% } %> @@ -28,7 +28,7 @@ describe('Thing API:', function() { things.should.be.instanceOf(Array); }); - });<% if (filters.mongoose) { %> + });<% if (filters.models) { %> describe('POST /api/things', function() { beforeEach(function(done) { @@ -130,7 +130,7 @@ describe('Thing API:', function() { }); }); - it('should respond with 404 when thing does not exsist', function(done) { + it('should respond with 404 when thing does not exist', function(done) { request(app) .delete('/api/things/' + newThing._id) .expect(404) diff --git a/app/templates/server/api/thing/thing.model(mongoose).js b/app/templates/server/api/thing/thing.model(mongooseModels).js similarity index 100% rename from app/templates/server/api/thing/thing.model(mongoose).js rename to app/templates/server/api/thing/thing.model(mongooseModels).js diff --git a/app/templates/server/api/thing/thing.model(sequelizeModels).js b/app/templates/server/api/thing/thing.model(sequelizeModels).js new file mode 100644 index 000000000..8e8072da4 --- /dev/null +++ b/app/templates/server/api/thing/thing.model(sequelizeModels).js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = function(sequelize, DataTypes) { + return sequelize.define('Thing', { + _id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true + }, + name: DataTypes.STRING, + info: DataTypes.STRING, + active: DataTypes.BOOLEAN + }); +}; diff --git a/app/templates/server/api/thing/thing.socket(socketio).js b/app/templates/server/api/thing/thing.socket(socketio).js index 2d0171cdc..d0d98a6ca 100644 --- a/app/templates/server/api/thing/thing.socket(socketio).js +++ b/app/templates/server/api/thing/thing.socket(socketio).js @@ -3,15 +3,24 @@ */ 'use strict'; +<% if (filters.mongooseModels) { %> +var thing = require('./thing.model');<% } %><% if (filters.sequelizeModels) { %> +var thing = require('../../sqldb').Thing;<% } %> -var thing = require('./thing.model'); - -exports.register = function(socket) { - thing.schema.post('save', function(doc) { +exports.register = function(socket) {<% if (filters.sequelizeModels) { %> + thing.hook('afterCreate', function(doc, fields, fn) { onSave(socket, doc); + fn(null); + });<% } %> + <% if (filters.mongooseModels) { %>thing.schema.post('save', function(doc) {<% } + if (filters.sequelizeModels) { %>thing.hook('afterUpdate', function(doc, fields, fn) {<% } %> + onSave(socket, doc);<% if (filters.sequelizeModels) { %> + fn(null);<% } %> }); - thing.schema.post('remove', function(doc) { - onRemove(socket, doc); + <% if (filters.mongooseModels) { %>thing.schema.post('remove', function(doc) {<% } + if (filters.sequelizeModels) { %>thing.hook('afterDestroy', function(doc, fields, fn) {<% } %> + onRemove(socket, doc);<% if (filters.sequelizeModels) { %> + fn(null);<% } %> }); }; diff --git a/app/templates/server/api/user(auth)/user.controller.js b/app/templates/server/api/user(auth)/user.controller.js index 793da0255..8b7f4e0b8 100644 --- a/app/templates/server/api/user(auth)/user.controller.js +++ b/app/templates/server/api/user(auth)/user.controller.js @@ -1,6 +1,9 @@ 'use strict'; - -var User = require('./user.model'); +<% if (filters.mongooseModels) { %> +var User = require('./user.model');<% } %><% if (filters.sequelizeModels) { %> +var _ = require('lodash'); +var sqldb = require('../../sqldb'); +var User = sqldb.User;<% } %> var passport = require('passport'); var config = require('../../config/environment'); var jwt = require('jsonwebtoken'); @@ -31,7 +34,16 @@ function respondWith(res, statusCode) { * restriction: 'admin' */ exports.index = function(req, res) { - User.findAsync({}, '-salt -hashedPassword') + <% if (filters.mongooseModels) { %>User.findAsync({}, '-salt -hashedPassword')<% } + if (filters.sequelizeModels) { %>User.findAll({ + attributes: [ + '_id', + 'name', + 'email', + 'role', + 'provider' + ] + })<% } %> .then(function(users) { res.json(200, users); }) @@ -42,11 +54,16 @@ exports.index = function(req, res) { * Creates a new user */ exports.create = function(req, res, next) { - var newUser = new User(req.body); + <% if (filters.mongooseModels) { %>var newUser = new User(req.body); newUser.provider = 'local'; newUser.role = 'user'; - newUser.saveAsync() - .spread(function(user) { + newUser.saveAsync()<% } + if (filters.sequelizeModels) { %>var newUser = User.build(req.body); + newUser.setDataValue('provider', 'local'); + newUser.setDataValue('role', 'user'); + newUser.save()<% } %> + <% if (filters.mongooseModels) { %>.spread(function(user) {<% } + if (filters.sequelizeModels) { %>.then(function(user) {<% } %> var token = jwt.sign({ _id: user._id }, config.secrets.session, { expiresInMinutes: 60 * 5 }); @@ -61,7 +78,12 @@ exports.create = function(req, res, next) { exports.show = function(req, res, next) { var userId = req.params.id; - User.findByIdAsync(userId) + <% if (filters.mongooseModels) { %>User.findByIdAsync(userId)<% } + if (filters.sequelizeModels) { %>User.find({ + where: { + _id: userId + } + })<% } %> .then(function(user) { if (!user) { return res.send(401); @@ -78,7 +100,8 @@ exports.show = function(req, res, next) { * restriction: 'admin' */ exports.destroy = function(req, res) { - User.findByIdAndRemoveAsync(req.params.id) + <% if (filters.mongooseModels) { %>User.findByIdAndRemoveAsync(req.params.id)<% } + if (filters.sequelizeModels) { %>User.destroy({ _id: req.params.id })<% } %> .then(respondWith(res, 204)) .catch(handleError(res)); }; @@ -91,12 +114,18 @@ exports.changePassword = function(req, res, next) { var oldPass = String(req.body.oldPassword); var newPass = String(req.body.newPassword); - User.findByIdAsync(userId) + <% if (filters.mongooseModels) { %>User.findByIdAsync(userId)<% } + if (filters.sequelizeModels) { %>User.find({ + where: { + _id: userId + } + })<% } %> .then(function(user) { if (user.authenticate(oldPass)) { user.password = newPass; - return user.saveAsync() - .spread(respondWith(res, 200)) + <% if (filters.mongooseModels) { %>return user.saveAsync()<% } + if (filters.sequelizeModels) { %>return user.save()<% } %> + .then(respondWith(res, 200)) .catch(validationError(res)); } else { return res.send(403); @@ -110,7 +139,19 @@ exports.changePassword = function(req, res, next) { exports.me = function(req, res, next) { var userId = req.user._id; - User.findOneAsync({ _id: userId }, '-salt -hashedPassword') + <% if (filters.mongooseModels) { %>User.findOneAsync({ _id: userId }, '-salt -hashedPassword')<% } + if (filters.sequelizeModels) { %>User.find({ + where: { + _id: userId + }, + attributes: [ + '_id', + 'name', + 'email', + 'role', + 'provider' + ] + })<% } %> .then(function(user) { // don't ever give out the password or salt if (!user) { return res.json(401); } res.json(user); diff --git a/app/templates/server/api/user(auth)/user.integration.js b/app/templates/server/api/user(auth)/user.integration.js index ee97d3051..35bcdb573 100644 --- a/app/templates/server/api/user(auth)/user.integration.js +++ b/app/templates/server/api/user(auth)/user.integration.js @@ -1,7 +1,8 @@ 'use strict'; -var app = require('../../app'); -var User = require('./user.model'); +var app = require('../../app');<% if (filters.mongooseModels) { %> +var User = require('./user.model');<% } %><% if (filters.sequelizeModels) { %> +var User = require('../../sqldb').User;<% } %> var request = require('supertest'); describe('User API:', function() { @@ -9,25 +10,33 @@ describe('User API:', function() { // Clear users before testing before(function(done) { - User.remove(function() { - user = new User({ + <% if (filters.mongooseModels) { %>User.remove(function() {<% } + if (filters.sequelizeModels) { %>User.destroy().then(function() {<% } %> + <% if (filters.mongooseModels) { %>user = new User({<% } + if (filters.sequelizeModels) { %>user = User.build({<% } %> name: 'Fake User', email: 'test@test.com', password: 'password' }); - user.save(function(err) { + <% if (filters.mongooseModels) { %>user.save(function(err) { if (err) { return done(err); } done(); - }); + });<% } + if (filters.sequelizeModels) { %>user.save().then(function() { + done(); + }, function(err) { + return done(err); + });<% } %> }); }); // Clear users after testing after(function() { - return User.remove().exec(); + <% if (filters.mongooseModels) { %>return User.remove().exec();<% } + if (filters.sequelizeModels) { %>return User.destroy();<% } %> }); describe('GET /api/users/me', function() { @@ -55,7 +64,7 @@ describe('User API:', function() { .expect(200) .expect('Content-Type', /json/) .end(function(err, res) { - res.body._id.should.equal(user._id.toString()); + res.body._id.toString().should.equal(user._id.toString()); done(); }); }); diff --git a/app/templates/server/api/user(auth)/user.model.js b/app/templates/server/api/user(auth)/user.model(mongooseModels).js similarity index 100% rename from app/templates/server/api/user(auth)/user.model.js rename to app/templates/server/api/user(auth)/user.model(mongooseModels).js diff --git a/app/templates/server/api/user(auth)/user.model(sequelizeModels).js b/app/templates/server/api/user(auth)/user.model(sequelizeModels).js new file mode 100644 index 000000000..af593157a --- /dev/null +++ b/app/templates/server/api/user(auth)/user.model(sequelizeModels).js @@ -0,0 +1,235 @@ +'use strict'; + +var crypto = require('crypto');<% if (filters.oauth) { %> +var authTypes = ['github', 'twitter', 'facebook', 'google'];<% } %> + +var validatePresenceOf = function(value) { + return value && value.length; +}; + +module.exports = function(sequelize, DataTypes) { + var User = sequelize.define('User', { + + _id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true + }, + name: DataTypes.STRING, + email: { + type: DataTypes.STRING, + unique: { + msg: 'The specified email address is already in use.' + }, + validate: { + isEmail: true + } + }, + role: { + type: DataTypes.STRING, + defaultValue: 'user' + }, + password: { + type: DataTypes.STRING, + validate: { + notEmpty: true + } + }, + provider: DataTypes.STRING, + salt: DataTypes.STRING<% if (filters.oauth) { %>,<% if (filters.facebookAuth) { %> + facebook: DataTypes.TEXT,<% } %><% if (filters.twitterAuth) { %> + twitter: DataTypes.TEXT,<% } %><% if (filters.googleAuth) { %> + google: DataTypes.TEXT,<% } %> + github: DataTypes.TEXT<% } %> + + }, { + + /** + * Virtual Getters + */ + getterMethods: { + // Public profile information + profile: function() { + return { + 'name': this.name, + 'role': this.role + }; + }, + + // Non-sensitive info we'll be putting in the token + token: function() { + return { + '_id': this._id, + 'role': this.role + }; + } + }, + + /** + * Pre-save hooks + */ + hooks: { + beforeBulkCreate: function(users, fields, fn) { + var totalUpdated = 0; + users.forEach(function(user) { + user.updatePassword(function(err) { + if (err) { + return fn(err); + } + totalUpdated += 1; + if (totalUpdated === users.length) { + return fn(); + } + }); + }); + }, + beforeCreate: function(user, fields, fn) { + user.updatePassword(fn); + }, + beforeUpdate: function(user, fields, fn) { + if (user.changed('password')) { + user.updatePassword(fn); + } + } + }, + + /** + * Instance Methods + */ + instanceMethods: { + /** + * Authenticate - check if the passwords are the same + * + * @param {String} password + * @param {Function} callback + * @return {Boolean} + * @api public + */ + authenticate: function(password, callback) { + if (!callback) { + return this.password === this.encryptPassword(password); + } + + var _this = this; + this.encryptPassword(password, function(err, pwdGen) { + if (err) { + callback(err); + } + + if (_this.password === pwdGen) { + callback(null, true); + } + else { + callback(null, false); + } + }); + }, + + /** + * Make salt + * + * @param {Number} byteSize Optional salt byte size, default to 16 + * @param {Function} callback + * @return {String} + * @api public + */ + makeSalt: function(byteSize, callback) { + var defaultByteSize = 16; + + if (typeof arguments[0] === 'function') { + callback = arguments[0]; + byteSize = defaultByteSize; + } + else if (typeof arguments[1] === 'function') { + callback = arguments[1]; + } + + if (!byteSize) { + byteSize = defaultByteSize; + } + + if (!callback) { + return crypto.randomBytes(byteSize).toString('base64'); + } + + return crypto.randomBytes(byteSize, function(err, salt) { + if (err) { + callback(err); + } + return callback(null, salt.toString('base64')); + }); + }, + + /** + * Encrypt password + * + * @param {String} password + * @param {Function} callback + * @return {String} + * @api public + */ + encryptPassword: function(password, callback) { + if (!password || !this.salt) { + if (!callback) { + return null; + } + return callback(null); + } + + var defaultIterations = 10000; + var defaultKeyLength = 64; + var salt = new Buffer(this.salt, 'base64'); + + if (!callback) { + return crypto.pbkdf2Sync(password, salt, defaultIterations, defaultKeyLength) + .toString('base64'); + } + + return crypto.pbkdf2(password, salt, defaultIterations, defaultKeyLength, + function(err, key) { + if (err) { + callback(err); + } + return callback(null, key.toString('base64')); + }); + }, + + /** + * Update password field + * + * @param {Function} fn + * @return {String} + * @api public + */ + updatePassword: function(fn) { + // Handle new/update passwords + if (this.password) { + if (!validatePresenceOf(this.password)<% if (filters.oauth) { %> && authTypes.indexOf(this.provider) === -1<% } %>) { + fn(new Error('Invalid password')); + } + + // Make salt with a callback + var _this = this; + this.makeSalt(function(saltErr, salt) { + if (saltErr) { + fn(saltErr); + } + _this.salt = salt; + _this.encryptPassword(_this.password, function(encryptErr, hashedPassword) { + if (encryptErr) { + fn(encryptErr); + } + _this.password = hashedPassword; + fn(null); + }); + }); + } else { + fn(null); + } + } + } + }); + + return User; +}; diff --git a/app/templates/server/api/user(auth)/user.model.spec.js b/app/templates/server/api/user(auth)/user.model.spec(mongooseModels).js similarity index 100% rename from app/templates/server/api/user(auth)/user.model.spec.js rename to app/templates/server/api/user(auth)/user.model.spec(mongooseModels).js diff --git a/app/templates/server/api/user(auth)/user.model.spec(sequelizeModels).js b/app/templates/server/api/user(auth)/user.model.spec(sequelizeModels).js new file mode 100644 index 000000000..88156151b --- /dev/null +++ b/app/templates/server/api/user(auth)/user.model.spec(sequelizeModels).js @@ -0,0 +1,52 @@ +'use strict'; + +var app = require('../../app'); +var User = require('../../sqldb').User; + +var userTemplate = { + provider: 'local', + name: 'Fake User', + email: 'test@test.com', + password: 'password' +}; + +var user = User.build(userTemplate); + +describe('User Model', function() { + before(function() { + // Sync and clear users before testing + User.sync().then(function() { + return User.destroy(); + }); + }); + + afterEach(function() { + return User.destroy(); + }); + + it('should begin with no users', function() { + return User.findAll() + .should.eventually.have.length(0); + }); + + it('should fail when saving a duplicate user', function() { + return user.save() + .then(function() { + var userDup = User.build(userTemplate); + return userDup.save(); + }).should.be.rejected; + }); + + it('should fail when saving without an email', function() { + user.email = ''; + return user.save().should.be.rejected; + }); + + it('should authenticate user if password is valid', function() { + user.authenticate('password').should.be.true; + }); + + it('should not authenticate user if password is invalid', function() { + user.authenticate('blah').should.not.be.true; + }); +}); diff --git a/app/templates/server/app.js b/app/templates/server/app.js index 632dd134f..7c668a881 100644 --- a/app/templates/server/app.js +++ b/app/templates/server/app.js @@ -8,16 +8,17 @@ process.env.NODE_ENV = process.env.NODE_ENV || 'development'; var express = require('express');<% if (filters.mongoose) { %> -var mongoose = require('mongoose-bird')();<% } %> +var mongoose = require('mongoose-bird')();<% } %><% if (filters.sequelize) { %> +var sqldb = require('./sqldb');<% } %> var config = require('./config/environment'); <% if (filters.mongoose) { %> -// Connect to database +// Connect to MongoDB mongoose.connect(config.mongo.uri, config.mongo.options); - -// Populate DB with sample data +<% } %><% if (filters.models) { %> +// Populate databases with sample data if (config.seedDB) { require('./config/seed'); } - -<% } %>// Setup server +<% } %> +// Setup server var app = express(); var server = require('http').createServer(app);<% if (filters.socketio) { %> var socketio = require('socket.io')(server, { @@ -29,9 +30,19 @@ require('./config/express')(app); require('./routes')(app); // Start server -server.listen(config.port, config.ip, function() { - console.log('Express server listening on %d, in %s mode', config.port, app.get('env')); -}); - +function startServer() { + server.listen(config.port, config.ip, function() { + console.log('Express server listening on %d, in %s mode', config.port, app.get('env')); + }); +} +<% if (filters.sequelize) { %> +sqldb.sequelize.sync() + .then(startServer) + .catch(function(err) { + console.log('Server failed to start due to error: %s', err); + }); +<% } else { %> +setImmediate(startServer); +<% } %> // Expose app exports = module.exports = app; diff --git a/app/templates/server/auth(auth)/auth.service.js b/app/templates/server/auth(auth)/auth.service.js index 43ea2851f..5b9a6d66f 100644 --- a/app/templates/server/auth(auth)/auth.service.js +++ b/app/templates/server/auth(auth)/auth.service.js @@ -1,12 +1,13 @@ 'use strict'; - -var mongoose = require('mongoose-bird')(); +<% if (filters.mongooseModels) { %> +var mongoose = require('mongoose-bird')();<% } %> var passport = require('passport'); var config = require('../config/environment'); var jwt = require('jsonwebtoken'); var expressJwt = require('express-jwt'); -var compose = require('composable-middleware'); -var User = require('../api/user/user.model'); +var compose = require('composable-middleware');<% if (filters.mongooseModels) { %> +var User = require('../api/user/user.model');<% } %><% if (filters.sequelizeModels) { %> +var User = require('../sqldb').User;<% } %> var validateJwt = expressJwt({ secret: config.secrets.session }); @@ -27,7 +28,12 @@ function isAuthenticated() { }) // Attach user to request .use(function(req, res, next) { - User.findByIdAsync(req.user._id) + <% if (filters.mongooseModels) { %>User.findByIdAsync(req.user._id)<% } + if (filters.sequelizeModels) { %>User.find({ + where: { + _id: req.user._id + } + })<% } %> .then(function(user) { if (!user) { return res.send(401); diff --git a/app/templates/server/auth(auth)/facebook(facebookAuth)/passport.js b/app/templates/server/auth(auth)/facebook(facebookAuth)/passport.js index b29d74c69..92911b22f 100644 --- a/app/templates/server/auth(auth)/facebook(facebookAuth)/passport.js +++ b/app/templates/server/auth(auth)/facebook(facebookAuth)/passport.js @@ -8,31 +8,35 @@ exports.setup = function(User, config) { callbackURL: config.facebook.callbackURL }, function(accessToken, refreshToken, profile, done) { - User.findOne({ + <% if (filters.mongooseModels) { %>User.findOneAsync({<% } + if (filters.sequelizeModels) { %>User.find({<% } %> 'facebook.id': profile.id - }, - function(err, user) { - if (err) { - return done(err); - } - if (!user) { - user = new User({ - name: profile.displayName, - email: profile.emails[0].value, - role: 'user', - username: profile.username, - provider: 'facebook', - facebook: profile._json - }); - user.save(function(err) { - if (err) { - done(err); - } - return done(err, user); - }); - } else { - return done(err, user); - } }) + .then(function(user) { + if (!user) { + <% if (filters.mongooseModels) { %>user = new User({<% } + if (filters.sequelizeModels) { %>user = User.build({<% } %> + name: profile.displayName, + email: profile.emails[0].value, + role: 'user', + username: profile.username, + provider: 'facebook', + facebook: profile._json + }); + <% if (filters.mongooseModels) { %>user.saveAsync()<% } + if (filters.sequelizeModels) { %>user.save()<% } %> + .then(function(user) { + return done(null, user); + }) + .catch(function(err) { + return done(err); + }); + } else { + return done(null, user); + } + }) + .catch(function(err) { + return done(err); + }); })); }; diff --git a/app/templates/server/auth(auth)/google(googleAuth)/passport.js b/app/templates/server/auth(auth)/google(googleAuth)/passport.js index 0e7a3602d..0c9462a8d 100644 --- a/app/templates/server/auth(auth)/google(googleAuth)/passport.js +++ b/app/templates/server/auth(auth)/google(googleAuth)/passport.js @@ -8,27 +8,35 @@ exports.setup = function(User, config) { callbackURL: config.google.callbackURL }, function(accessToken, refreshToken, profile, done) { - User.findOne({ + <% if (filters.mongooseModels) { %>User.findOneAsync({<% } + if (filters.sequelizeModels) { %>User.find({<% } %> 'google.id': profile.id - }, function(err, user) { - if (!user) { - user = new User({ - name: profile.displayName, - email: profile.emails[0].value, - role: 'user', - username: profile.username, - provider: 'google', - google: profile._json - }); - user.save(function(err) { - if (err) { - done(err); - } - return done(err, user); - }); - } else { - return done(err, user); - } - }); + }) + .then(function(user) { + if (!user) { + <% if (filters.mongooseModels) { %>user = new User({<% } + if (filters.sequelizeModels) { %>user = User.build({<% } %> + name: profile.displayName, + email: profile.emails[0].value, + role: 'user', + username: profile.username, + provider: 'google', + google: profile._json + }); + <% if (filters.mongooseModels) { %>user.saveAsync()<% } + if (filters.sequelizeModels) { %>user.save()<% } %> + .then(function(user) { + return done(null, user); + }) + .catch(function(err) { + return done(err); + }); + } else { + return done(null, user); + } + }) + .catch(function(err) { + return done(err); + }); })); }; diff --git a/app/templates/server/auth(auth)/index.js b/app/templates/server/auth(auth)/index.js index b930ad93f..75ddfdcb8 100644 --- a/app/templates/server/auth(auth)/index.js +++ b/app/templates/server/auth(auth)/index.js @@ -2,8 +2,9 @@ var express = require('express'); var passport = require('passport'); -var config = require('../config/environment'); -var User = require('../api/user/user.model'); +var config = require('../config/environment');<% if (filters.mongooseModels) { %> +var User = require('../api/user/user.model');<% } %><% if (filters.sequelizeModels) { %> +var User = require('../sqldb').User;<% } %> // Passport Configuration require('./local/passport').setup(User, config);<% if (filters.facebookAuth) { %> diff --git a/app/templates/server/auth(auth)/local/passport.js b/app/templates/server/auth(auth)/local/passport.js index f4898b73a..2bd3366f8 100644 --- a/app/templates/server/auth(auth)/local/passport.js +++ b/app/templates/server/auth(auth)/local/passport.js @@ -1,18 +1,16 @@ var passport = require('passport'); var LocalStrategy = require('passport-local').Strategy; -exports.setup = function(User, config) { - passport.use(new LocalStrategy({ - usernameField: 'email', - passwordField: 'password' // this is the virtual field on the model - }, function(email, password, done) { - User.findOne({ +function localAuthenticate(User, email, password, done) { + <% if (filters.mongooseModels) { %>User.findOneAsync({ + email: email.toLowerCase() + })<% } + if (filters.sequelizeModels) { %>User.find({ + where: { email: email.toLowerCase() - }, function(err, user) { - if (err) { - return done(err); - } - + } + })<% } %> + .then(function(user) { if (!user) { return done(null, false, { message: 'This email is not registered.' @@ -30,6 +28,17 @@ exports.setup = function(User, config) { return done(null, user); } }); + }) + .catch(function(err) { + return done(err); }); - })); +} + +exports.setup = function(User, config) { + passport.use(new LocalStrategy({ + usernameField: 'email', + passwordField: 'password' // this is the virtual field on the model + }, function(email, password, done) {<% if (filters.models) { %> + return localAuthenticate(User, email, password, done); +<% } %> })); }; diff --git a/app/templates/server/auth(auth)/twitter(twitterAuth)/passport.js b/app/templates/server/auth(auth)/twitter(twitterAuth)/passport.js index 7428e8afd..bf23bd3ba 100644 --- a/app/templates/server/auth(auth)/twitter(twitterAuth)/passport.js +++ b/app/templates/server/auth(auth)/twitter(twitterAuth)/passport.js @@ -8,29 +8,34 @@ exports.setup = function(User, config) { callbackURL: config.twitter.callbackURL }, function(token, tokenSecret, profile, done) { - User.findOne({ + <% if (filters.mongooseModels) { %>User.findOneAsync({<% } + if (filters.sequelizeModels) { %>User.find({<% } %> 'twitter.id_str': profile.id - }, function(err, user) { - if (err) { + }) + .then(function(user) { + if (!user) { + <% if (filters.mongooseModels) { %>user = new User({<% } + if (filters.sequelizeModels) { %>user = User.build({<% } %> + name: profile.displayName, + username: profile.username, + role: 'user', + provider: 'twitter', + twitter: profile._json + }); + <% if (filters.mongooseModels) { %>user.saveAsync()<% } + if (filters.sequelizeModels) { %>user.save()<% } %> + .then(function(user) { + return done(null, user); + }) + .catch(function(err) { + return done(err); + }); + } else { + return done(null, user); + } + }) + .catch(function(err) { return done(err); - } - if (!user) { - user = new User({ - name: profile.displayName, - username: profile.username, - role: 'user', - provider: 'twitter', - twitter: profile._json - }); - user.save(function(err) { - if (err) { - return done(err); - } - return done(err, user); - }); - } else { - return done(err, user); - } - }); + }); })); }; diff --git a/app/templates/server/config/environment/development.js b/app/templates/server/config/environment/development.js index fb33d6eab..20656595b 100644 --- a/app/templates/server/config/environment/development.js +++ b/app/templates/server/config/environment/development.js @@ -7,6 +7,16 @@ module.exports = { mongo: { uri: 'mongodb://localhost/<%= _.slugify(appname) %>-dev' }, + sequelize: { + uri: 'sqlite://', + options: { + logging: false, + storage: 'dev.sqlite', + define: { + timestamps: false + } + } + }, seedDB: true }; diff --git a/app/templates/server/config/environment/test.js b/app/templates/server/config/environment/test.js index 6ead6e1aa..021938424 100644 --- a/app/templates/server/config/environment/test.js +++ b/app/templates/server/config/environment/test.js @@ -6,5 +6,15 @@ module.exports = { // MongoDB connection options mongo: { uri: 'mongodb://localhost/<%= _.slugify(appname) %>-test' + }, + sequelize: { + uri: 'sqlite://', + options: { + logging: false, + storage: 'test.sqlite', + define: { + timestamps: false + } + } } }; diff --git a/app/templates/server/config/express.js b/app/templates/server/config/express.js index f44999c1e..9a2018263 100644 --- a/app/templates/server/config/express.js +++ b/app/templates/server/config/express.js @@ -15,9 +15,9 @@ var errorHandler = require('errorhandler'); var path = require('path'); var config = require('./environment');<% if (filters.auth) { %> var passport = require('passport');<% } %><% if (filters.twitterAuth) { %> -var session = require('express-session'); +var session = require('express-session');<% if (filters.mongoose) { %> var mongoStore = require('connect-mongo')(session); -var mongoose = require('mongoose-bird')();<% } %> +var mongoose = require('mongoose-bird')();<% } %><% } %> module.exports = function(app) { var env = app.get('env'); @@ -33,13 +33,13 @@ module.exports = function(app) { app.use(cookieParser());<% if (filters.auth) { %> app.use(passport.initialize());<% } %><% if (filters.twitterAuth) { %> - // Persist sessions with mongoStore + // Persist sessions with mongoStore / sequelizeStore // We need to enable sessions for passport twitter because its an oauth 1.0 strategy app.use(session({ secret: config.secrets.session, resave: true, - saveUninitialized: true, - store: new mongoStore({ mongoose_connection: mongoose.connection }) + saveUninitialized: true<% if (filters.mongoose) { %>, + store: new mongoStore({ mongoose_connection: mongoose.connection })<% } %> })); <% } %> app.set('appPath', path.join(config.root, 'client')); diff --git a/app/templates/server/config/seed(mongoose).js b/app/templates/server/config/seed(models).js similarity index 64% rename from app/templates/server/config/seed(mongoose).js rename to app/templates/server/config/seed(models).js index 1f6945d8d..635658010 100644 --- a/app/templates/server/config/seed(mongoose).js +++ b/app/templates/server/config/seed(models).js @@ -4,13 +4,22 @@ */ 'use strict'; - +<% if (filters.mongooseModels) { %> var Thing = require('../api/thing/thing.model'); <% if (filters.auth) { %>var User = require('../api/user/user.model');<% } %> - -Thing.find({}).removeAsync() +<% } %><% if (filters.sequelizeModels) { %> +var sqldb = require('../sqldb'); +var Thing = sqldb.Thing; +<% if (filters.auth) { %>var User = sqldb.User;<% } %> +<% } %> +<% if (filters.mongooseModels) { %>Thing.find({}).removeAsync()<% } + if (filters.sequelizeModels) { %>Thing.sync() + .then(function() { + return Thing.destroy(); + })<% } %> .then(function() { - Thing.create({ + <% if (filters.mongooseModels) { %>Thing.create({<% } + if (filters.sequelizeModels) { %>Thing.bulkCreate([{<% } %> name : 'Development Tools', info : 'Integration with popular tools such as Bower, Grunt, Karma, ' + 'Mocha, JSHint, Node Inspector, Livereload, Protractor, Jade, ' + @@ -37,12 +46,18 @@ Thing.find({}).removeAsync() name : 'Deployment Ready', info : 'Easily deploy your app to Heroku or Openshift with the heroku ' + 'and openshift subgenerators' - }); + <% if (filters.mongooseModels) { %>});<% } + if (filters.sequelizeModels) { %>}]);<% } %> }); <% if (filters.auth) { %> -User.find({}).removeAsync() +<% if (filters.mongooseModels) { %>User.find({}).removeAsync()<% } + if (filters.sequelizeModels) { %>User.sync() + .then(function() { + User.destroy(); + })<% } %> .then(function() { - User.create({ + <% if (filters.mongooseModels) { %>User.createAsync({<% } + if (filters.sequelizeModels) { %>User.bulkCreate([{<% } %> provider: 'local', name: 'Test User', email: 'test@test.com', @@ -53,8 +68,9 @@ User.find({}).removeAsync() name: 'Admin', email: 'admin@admin.com', password: 'admin' - }, function() { + <% if (filters.mongooseModels) { %>})<% } + if (filters.sequelizeModels) { %>}])<% } %> + .then(function() { console.log('finished populating users'); }); - }); -<% } %> \ No newline at end of file + });<% } %> diff --git a/app/templates/server/sqldb(sequelize)/index.js b/app/templates/server/sqldb(sequelize)/index.js new file mode 100644 index 000000000..231db9823 --- /dev/null +++ b/app/templates/server/sqldb(sequelize)/index.js @@ -0,0 +1,35 @@ +/** + * Sequelize initialization module + */ + +'use strict'; + +var path = require('path'); +var config = require('../config/environment'); + +var Sequelize = require('sequelize'); + +var db = { + Sequelize: Sequelize, + sequelize: new Sequelize(config.sequelize.uri, config.sequelize.options) +}; +<% if (filters.sequelizeModels) { %> +db.Thing = db.sequelize.import(path.join( + config.root, + 'server', + 'api', + 'thing', + 'thing.model' +)); +<% if (filters.auth) { %> +db.User = db.sequelize.import(path.join( + config.root, + 'server', + 'api', + 'user', + 'user.model' +)); +<% } %><% } %> +// Insert models below + +module.exports = db; diff --git a/endpoint/index.js b/endpoint/index.js index d67f78e51..f870c265f 100644 --- a/endpoint/index.js +++ b/endpoint/index.js @@ -25,11 +25,24 @@ Generator.prototype.askFor = function askFor() { name = name + 's'; } + var self = this; var prompts = [ { name: 'route', message: 'What will the url of your endpoint to be?', default: base + name + }, + { + type: 'list', + name: 'models', + message: 'What would you like to use for the endpoint\'s models?', + choices: [ 'Mongoose', 'Sequelize' ], + filter: function( val ) { + return val.toLowerCase(); + }, + when: function() { + return self.filters.mongoose && self.filters.sequelize; + } } ]; @@ -39,6 +52,16 @@ Generator.prototype.askFor = function askFor() { } this.route = props.route; + + if (props.models) { + delete this.filters.mongoose; + delete this.filters.mongooseModels; + delete this.filters.sequelize; + delete this.filters.sequelizeModels; + + this.filters[props.models] = true; + this.filters[props.models + 'Models'] = true; + } done(); }.bind(this)); }; @@ -67,6 +90,25 @@ Generator.prototype.registerEndpoint = function registerEndpoint() { ngUtil.rewriteFile(socketConfig); } } + + if (this.filters.sequelize) { + if (this.config.get('insertModels')) { + var modelConfig = { + file: this.config.get('registerModelsFile'), + needle: this.config.get('modelsNeedle'), + splicable: [ + "db." + this.classedName + " = db.sequelize.import(path.join(\n" + + " config.root,\n" + + " 'server',\n" + + " 'api',\n" + + " '" + this.name + "',\n" + + " '" + this.name + ".model'\n" + + "));" + ] + }; + ngUtil.rewriteFile(modelConfig); + } + } }; Generator.prototype.createFiles = function createFiles() { diff --git a/endpoint/templates/name.model(sequelize).js b/endpoint/templates/name.model(sequelize).js new file mode 100644 index 000000000..051c5daf2 --- /dev/null +++ b/endpoint/templates/name.model(sequelize).js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = function(sequelize, DataTypes) { + return sequelize.define('<%= classedName %>', { + _id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true + }, + name: DataTypes.STRING, + info: DataTypes.STRING, + active: DataTypes.BOOLEAN + }); +}; diff --git a/endpoint/templates/name.socket(socketio).js b/endpoint/templates/name.socket(socketio).js index 260c80720..e55dfcac0 100644 --- a/endpoint/templates/name.socket(socketio).js +++ b/endpoint/templates/name.socket(socketio).js @@ -3,15 +3,24 @@ */ 'use strict'; +<% if (filters.mongoose) { %> +var <%= classedName %> = require('./<%= name %>.model');<% } %><% if (filters.sequelize) { %> +var <%= classedName %> = require('../../sqldb').<%= classedName %>;<% } %> -var <%= classedName %> = require('./<%= name %>.model'); - -exports.register = function(socket) { - <%= classedName %>.schema.post('save', function(doc) { +exports.register = function(socket) {<% if (filters.sequelize) { %> + <%= classedName %>.hook('afterCreate', function(doc, fields, fn) { onSave(socket, doc); + fn(null); + });<% } %> + <% if (filters.mongoose) { %><%= classedName %>.schema.post('save', function(doc) {<% } + if (filters.sequelize) { %><%= classedName %>.hook('afterUpdate', function(doc, fields, fn) {<% } %> + onSave(socket, doc);<% if (filters.sequelize) { %> + fn(null);<% } %> }); - <%= classedName %>.schema.post('remove', function(doc) { - onRemove(socket, doc); + <% if (filters.mongoose) { %><%= classedName %>.schema.post('remove', function(doc) {<% } + if (filters.sequelize) { %><%= classedName %>.hook('afterDestroy', function(doc, fields, fn) {<% } %> + onRemove(socket, doc);<% if (filters.sequelize) { %> + fn(null);<% } %> }); }; diff --git a/test/test-file-creation.js b/test/test-file-creation.js index 6164b690b..0e227efef 100644 --- a/test/test-file-creation.js +++ b/test/test-file-creation.js @@ -18,7 +18,7 @@ describe('angular-fullstack generator', function () { chai: 'expect', bootstrap: true, uibootstrap: true, - mongoose: true, + odms: [ 'mongoose' ], auth: true, oauth: [], socketio: true @@ -153,7 +153,8 @@ describe('angular-fullstack generator', function () { var script = mapping.script[ops.script], markup = mapping.markup[ops.markup], - stylesheet = mapping.stylesheet[ops.stylesheet]; + stylesheet = mapping.stylesheet[ops.stylesheet], + models = ops.models ? ops.models : ops.odms[0]; /* Core Files */ files = files.concat([ @@ -224,14 +225,21 @@ describe('angular-fullstack generator', function () { ]); } - /* Mongoose */ - if (ops.mongoose) { + /* Models - Mongoose or Sequelize */ + if (models) { files = files.concat([ 'server/api/thing/thing.model.js', 'server/config/seed.js' ]); } + /* Sequelize */ + if (ops.odms.indexOf('sequelize') !== -1) { + files = files.concat([ + 'server/sqldb/index.js' + ]); + } + /* Authentication */ if (ops.auth) { files = files.concat([ @@ -475,7 +483,79 @@ describe('angular-fullstack generator', function () { stylesheet: 'less', router: 'uirouter', testing: 'jasmine', - mongoose: true, + odms: [ 'mongoose' ], + auth: true, + oauth: ['twitterAuth', 'facebookAuth', 'googleAuth'], + socketio: true, + bootstrap: true, + uibootstrap: true + }; + + beforeEach(function() { + helpers.mockPrompt(gen, testOptions); + }); + + it('should run client tests successfully', function(done) { + runTest('grunt test:client', this, done); + }); + + it('should pass jscs', function(done) { + runTest('grunt jscs', this, done); + }); + + it('should pass lint', function(done) { + runTest('grunt jshint', this, done); + }); + + it('should run server tests successfully', function(done) { + runTest('grunt test:server', this, done); + }); + + it('should pass jscs with generated endpoint', function(done) { + runTest('grunt jscs', this, done, 'foo'); + }); + + it('should pass lint with generated snake-case endpoint', function(done) { + runTest('grunt jshint', this, done, 'foo-bar'); + }); + + it('should run server tests successfully with generated snake-case endpoint', function(done) { + runTest('grunt test:server', this, done, 'foo-bar'); + }); + + it('should generate expected files', function (done) { + gen.run({}, function () { + helpers.assertFile(genFiles(testOptions)); + done(); + }); + }); + + it('should not generate unexpected files', function (done) { + gen.run({}, function () { + assertOnlyFiles(genFiles(testOptions), done); + }); + }); + + if(!process.env.SKIP_E2E) { + it('should run e2e tests successfully', function (done) { + runTest('grunt test:e2e', this, done, 240000); + }); + + //it('should run e2e tests successfully for production app', function (done) { + // runTest('grunt test:e2e:prod', this, done, 240000); + //}); + } + + }); + + describe('with sequelize models, auth', function() { + var testOptions = { + script: 'js', + markup: 'jade', + stylesheet: 'stylus', + router: 'uirouter', + testing: 'jasmine', + odms: [ 'sequelize' ], auth: true, oauth: ['twitterAuth', 'facebookAuth', 'googleAuth'], socketio: true, @@ -548,7 +628,7 @@ describe('angular-fullstack generator', function () { router: 'ngroute', testing: 'mocha', chai: 'should', - mongoose: false, + odms: [], auth: false, oauth: [], socketio: false, @@ -621,7 +701,7 @@ describe('angular-fullstack generator', function () { stylesheet: 'css', router: 'ngroute', testing: 'jasmine', - mongoose: false, + odms: [], auth: false, oauth: [], socketio: false,