├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── app ├── index.js └── templates │ ├── .babelrc │ ├── .eslintrc │ ├── .travis.yml │ ├── _README.md │ ├── _package.json │ ├── gitignore │ ├── public │ └── .placeholder │ ├── src │ ├── _passport.js │ ├── _server.js │ ├── config │ │ ├── _main.json │ │ └── _passport.json │ ├── lib │ │ ├── items.js │ │ └── users.js │ ├── models │ │ ├── .placeholder │ │ └── User.js │ └── schema │ │ ├── _index.js │ │ ├── items.js │ │ └── users.js │ └── tools │ ├── build.js │ ├── bundle.js │ ├── clean.js │ ├── config.js │ ├── copy.js │ ├── lib │ ├── copy.js │ └── watch.js │ ├── serve.js │ └── start.js ├── package.json └── test └── app.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "6.0" 5 | - "5.11" 6 | - "4.4.3" 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## [0.8.0] - 2016-05-10 6 | ### Added 7 | - NodeJs v6.0 support 8 | 9 | ### Updated 10 | - Dependencies 11 | - Tests 12 | 13 | ### Removed 14 | - iojs support 15 | - support for NodeJs versions < 4.4.3 16 | 17 | ## [0.7.0] - 2015-12-13 18 | ### Added 19 | - Hot loading support 20 | - Persist session on server restart when mongoose is enabled 21 | 22 | ### Improved 23 | - Removed items from the webpack config that are not relevant for the server 24 | - Added more documentation to the README of the generated project 25 | - Testcases 26 | - Sensitive configuration now has a separate folder that is ignored by git 27 | - Generator now defaults to using mongoose and local auth 28 | 29 | ## [0.6.0] - 2015-12-12 30 | ### Added 31 | - Changelog 32 | - User profile skeleton 33 | - Signup via mutation 34 | - Use graphql-custom-types where appropriate 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # generator-es6-graphql 2 | [![npm version](https://badge.fury.io/js/generator-es6-graphql.svg)](https://badge.fury.io/js/generator-es6-graphql) [![Build Status](https://secure.travis-ci.org/stylesuxx/generator-es6-graphql.png?branch=master)](https://travis-ci.org/stylesuxx/generator-es6-graphql) [![Dependency Status](https://david-dm.org/stylesuxx/generator-es6-graphql.svg)](https://david-dm.org/stylesuxx/generator-es6-graphql) 3 | 4 | > This [yeoman](http://yeoman.io/) generator enables you to quickly create an [ES6 (ES2105)](http://es6-features.org/) [graphql](https://facebook.github.io/graphql/) enabled server with a lot of optional additions. 5 | 6 | ## Goal 7 | The goal of this yeoman generator is to enable you to quickly set up an ES6 (ES2105) enabled GraphQL server without dependencies you do not need. 8 | 9 | This means that the choice of for example *database* and *testing framework* is up to you, although some choices are available during setup to make your life easier: 10 | * *mongoose* as database 11 | * *passport* for authentication 12 | * *local authentication* with basic user model skeleton 13 | * *persistent sessions* if mongoose and authentication are enabled 14 | 15 | ## Installation 16 | Install yo and the generator globally by running: 17 | 18 | sudo npm install -g yo generator-es6-graphql 19 | 20 | Create a directory for your new project and run the generator: 21 | 22 | mkdir yourProject 23 | cd yourProject 24 | yo es6-graphql 25 | 26 | To start your newly generated GraphQL server on port **1234** run: 27 | 28 | npm start 29 | 30 | ## First steps 31 | If you chose to enable *GraphiQL* you can now browse to your [GraphQL endpoint](http://localhost:1234/graphql) and play around with the *Documentation Explorer* or invoke your own queries: 32 | 33 | ``` 34 | { 35 | getItem(id: 1){ 36 | id, 37 | name 38 | } 39 | } 40 | ``` 41 | 42 | ### Authentication 43 | If you chose to enable authentication and selected at least one OAuth strategy, do not forget to add your API credentials for the chosen services to *src/passportConfig.js*. 44 | 45 | #### Local Authentication 46 | If you chose to enable a database and local authentication, you will get a basic *User* model and routes for authentication. 47 | 48 | ##### User model 49 | The *User* model is very basic and only has the following fields: 50 | * \_id 51 | * username 52 | * password 53 | 54 | The Model also provides functionality to check if passwords match and to store passwords encrypted via *bcrypt*. 55 | 56 | ##### /login (POST) 57 | Parameters: 58 | * username 59 | * password 60 | 61 | Returns: 62 | * *200* if user could be logged 63 | * *400* if parameters are missing 64 | * *401* if credentials do not match 65 | 66 | ##### /logout (GET) 67 | Logs out a user and destroys his session. 68 | 69 | #### Usage 70 | Open *GraphiQL* and run: 71 | ``` 72 | { 73 | users {_id, username, mail}, 74 | self {_id, username, mail} 75 | } 76 | ``` 77 | 78 | You will see that *users* is an empty array and *self is null*, this is because there are no users signed up yet and you are not logged in. 79 | 80 | Sign up a user via mutation: 81 | ``` 82 | mutation { 83 | signup(username: "username", password: "password") { 84 | _id, 85 | username 86 | } 87 | } 88 | ``` 89 | 90 | Now login via the route mentioned above and query the *user* and *self* again. The users array still has one user, but now self should contain information about the currently logged in user. 91 | 92 | In a similar way you can now set your E-Mail address: 93 | ``` 94 | mutation { 95 | updateEmail(mail: "you@domain.com") { 96 | _id, 97 | username, 98 | mail 99 | } 100 | } 101 | ``` 102 | 103 | If you open the logout route mentioned above and run the commands again, self is null again. 104 | 105 | ## Development 106 | This generator is *work in progress*, feel free to submit issues if you have a problem or PR's if you want to contribute. 107 | 108 | Tests are in place and may be run by invoking: 109 | 110 | npm run test 111 | 112 | ### Versioning 113 | The version of the npm package reflects the current master branch. If you want to check out bleeding edge functionality check out the dev branch. 114 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var generator = require('yeoman-generator'); 3 | var chalk = require('chalk'); 4 | var crypto = require('crypto'); 5 | 6 | module.exports = generator.Base.extend({ 7 | initializing: function() { 8 | this.auth = []; 9 | this.authLocal = false; 10 | }, 11 | 12 | prompting: { 13 | appname: function() { 14 | var done = this.async(); 15 | this.prompt({ 16 | type: 'input', 17 | name: 'name', 18 | message: 'Your project name', 19 | default: this.appname 20 | }).then((answers) => { 21 | this.appname = answers.name; 22 | 23 | done(); 24 | }); 25 | }, 26 | 27 | graphqlroute: function() { 28 | var done = this.async(); 29 | this.prompt({ 30 | type: 'input', 31 | name: 'graphqlroute', 32 | message: 'Route to the GraphQL endpoint', 33 | default: '/graphql' 34 | }).then((answers) => { 35 | this.graphqlroute = answers.graphqlroute; 36 | if(this.graphqlroute[0] != '/') { 37 | this.graphqlroute = '/' + this.graphqlroute; 38 | } 39 | 40 | done(); 41 | }); 42 | }, 43 | 44 | graphiql: function() { 45 | var done = this.async(); 46 | this.prompt({ 47 | type: 'confirm', 48 | name: 'graphiql', 49 | message: 'Enable GraphiQL', 50 | default: true 51 | }).then((answers) => { 52 | this.graphiql = answers.graphiql; 53 | 54 | done(); 55 | }); 56 | }, 57 | 58 | database: function() { 59 | var done = this.async(); 60 | this.prompt({ 61 | type: 'list', 62 | name: 'database', 63 | message: 'Choose database', 64 | choices: [ 65 | {name: 'Mongoose', value: 'mongoose'}, 66 | {name: 'None', value: 'none'} 67 | ], 68 | default: 0 69 | }).then((answers) => { 70 | this.database = answers.database; 71 | 72 | if(this.database != 'none') { 73 | this.prompt({ 74 | type: 'input', 75 | name: 'name', 76 | message: 'Database name', 77 | default: this.appname 78 | }).then((answers) => { 79 | this.databaseName = answers.name; 80 | 81 | done(); 82 | }); 83 | } 84 | else { 85 | done(); 86 | } 87 | }); 88 | }, 89 | 90 | passport: function() { 91 | var done = this.async(); 92 | this.prompt({ 93 | type: 'confirm', 94 | name: 'authentication', 95 | message: 'Enable passport for authentication', 96 | default: true 97 | }).then((answers) => { 98 | this.authentication = answers.authentication; 99 | this.secret = crypto.randomBytes(16).toString('hex') 100 | 101 | done(); 102 | }); 103 | }, 104 | 105 | local: function() { 106 | var done = this.async(); 107 | if(this.authentication && this.database !== 'none') { 108 | this.prompt({ 109 | type: 'confirm', 110 | name: 'authLocal', 111 | message: 'Enable local authentication strategy', 112 | default: true 113 | }).then((answers) => { 114 | this.authLocal = answers.authLocal; 115 | 116 | done(); 117 | }); 118 | } 119 | else { 120 | done(); 121 | } 122 | }, 123 | 124 | strategies: function() { 125 | var done = this.async(); 126 | if(this.authentication) { 127 | this.authFull = []; 128 | 129 | var choices = [ 130 | { name: 'Facebook', value: 'passport-facebook', slug: 'facebook' }, 131 | { name: 'Github', value: 'passport-github', slug: 'github' }, 132 | { name: 'Google', value: 'passport-google-oauth', slug: 'google' }, 133 | //{name: 'Twitter', value: 'passport-twitter'} 134 | ]; 135 | 136 | this.prompt({ 137 | type: 'checkbox', 138 | name: 'auth', 139 | message: 'Choose OAuth strategies', 140 | choices: choices, 141 | default: [] 142 | }).then((answers) => { 143 | this.auth = answers.auth; 144 | 145 | choices.map((item) => { 146 | if(this.auth.indexOf(item.value) > -1) { 147 | this.authFull.push({ 148 | npm: item.value, 149 | name: item.name, 150 | slug: item.slug 151 | }); 152 | } 153 | }); 154 | 155 | done(); 156 | }); 157 | } 158 | else { 159 | done(); 160 | } 161 | }, 162 | }, 163 | 164 | writing: { 165 | yorc: function() { 166 | this.config.save(); 167 | }, 168 | 169 | projectfiles: function() { 170 | this.copy('.babelrc', '.babelrc'); 171 | this.copy('.eslintrc', '.eslintrc'); 172 | this.copy('.travis.yml', '.travis.yml'); 173 | this.template('_package.json', 'package.json'); 174 | this.template('_README.md', 'README.md'); 175 | }, 176 | 177 | gitfiles: function() { 178 | this.copy('gitignore', '.gitignore'); 179 | }, 180 | 181 | app: function() { 182 | this.template('src/_server.js', 'src/server.js'); 183 | this.template('src/config/_main.json', 'src/config/main.json'); 184 | this.copy('public/.placeholder', 'public/.placeholder'); 185 | }, 186 | 187 | tools: function() { 188 | this.copy('tools/lib/copy.js', 'tools/lib/copy.js'); 189 | this.copy('tools/lib/watch.js', 'tools/lib/watch.js'); 190 | this.copy('tools/build.js', 'tools/build.js'); 191 | this.copy('tools/bundle.js', 'tools/bundle.js'); 192 | this.copy('tools/clean.js', 'tools/clean.js'); 193 | this.copy('tools/config.js', 'tools/config.js'); 194 | this.copy('tools/copy.js', 'tools/copy.js'); 195 | this.copy('tools/serve.js', 'tools/serve.js'); 196 | this.copy('tools/start.js', 'tools/start.js'); 197 | }, 198 | 199 | lib: function() { 200 | this.copy('src/lib/items.js', 'src/lib/items.js'); 201 | if(this.authLocal) { 202 | this.copy('src/lib/users.js', 'src/lib/users.js'); 203 | } 204 | }, 205 | 206 | schema: function() { 207 | this.copy('src/schema/items.js', 'src/schema/items.js'); 208 | this.template('src/schema/_index.js', 'src/schema/index.js'); 209 | if(this.authLocal) { 210 | this.copy('src/schema/users.js', 'src/schema/users.js'); 211 | } 212 | }, 213 | 214 | models: function() { 215 | if(this.database === 'mongoose') { 216 | this.copy('src/models/.placeholder', 'src/models/.placeholder'); 217 | } 218 | 219 | if(this.authLocal) { 220 | this.copy('src/models/User.js', 'src/models/User.js'); 221 | } 222 | }, 223 | 224 | authentication: function() { 225 | if(this.authentication) { 226 | this.template('src/_passport.js', 'src/passport.js'); 227 | 228 | if(this.auth.length > 0) { 229 | this.template('src/config/_passport.json', 'src/config/passport.json'); 230 | } 231 | } 232 | } 233 | }, 234 | 235 | install: function() { 236 | this.npmInstall([ 237 | 'babel', 238 | 'express', 239 | 'express-graphql', 240 | 'graphql', 241 | 'source-map-support', 242 | 'webpack' 243 | ], {'save': true}); 244 | 245 | this.npmInstall([ 246 | 'babel-core', 247 | 'babel-polyfill', 248 | 'babel-cli', 249 | 'babel-eslint', 250 | 'babel-loader', 251 | 'babel-preset-es2015', 252 | 'babel-preset-stage-1', 253 | 'del', 254 | 'eslint', 255 | 'gaze', 256 | 'json-loader', 257 | 'lodash.merge', 258 | 'mkdirp', 259 | 'ncp', 260 | 'path', 261 | 'replace', 262 | ], {'saveDev': true}); 263 | 264 | if(this.database === 'mongoose') { 265 | this.npmInstall(['mongoose', 'connect-mongo'], {'save': true}); 266 | } 267 | 268 | if(this.authentication) { 269 | this.npmInstall(['passport', 'express-session'], {'save': true}); 270 | this.npmInstall(this.auth, {'save': true}); 271 | } 272 | 273 | if(this.authLocal) { 274 | this.npmInstall([ 275 | 'passport-local', 276 | 'body-parser', 277 | 'bcrypt', 278 | 'graphql-custom-types' 279 | ], {'save': true}); 280 | } 281 | }, 282 | 283 | end: { 284 | finished: function() { 285 | this.log(chalk.bold.green('\nGenerator setup finished.')); 286 | if(this.auth.length > 0) { 287 | this.log(chalk.bold.white('Do not forget to add your API credentials to src/config/passport.json')); 288 | } 289 | this.log('If you see no errors above, run the server:'); 290 | this.log(chalk.bold.white('npm start')); 291 | } 292 | } 293 | }); 294 | -------------------------------------------------------------------------------- /app/templates/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-1"] 3 | } 4 | -------------------------------------------------------------------------------- /app/templates/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | ], 5 | "env": { 6 | "browser": true, 7 | "node": true, 8 | "es6": true, 9 | }, 10 | "globals": { 11 | "__DEV__": true, 12 | "__SERVER__": true 13 | }, 14 | "ecmaFeatures": { 15 | }, 16 | "rules": { 17 | // Strict mode 18 | "strict": [2, "never"], 19 | 20 | // Code style 21 | "indent": [2, 2], 22 | "quotes": [2, "single"], 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/templates/.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 'iojs' 5 | - "4.1" 6 | - "4.0" 7 | - '0.12' 8 | - '0.10' -------------------------------------------------------------------------------- /app/templates/_README.md: -------------------------------------------------------------------------------- 1 | # <%= appname %> 2 | Your app description comes here. 3 | 4 | ## Usage 5 | 6 | ### Start the Development Server 7 | ```Bash 8 | npm start 9 | ``` 10 | 11 | When starting the server there are a couple of parameters you can pass: 12 | * verbose - Enable verbose output 13 | * release - Enable release mode, which will generally disable debugging features 14 | 15 | Hot reloading is in place, so you do not need to restart the server on file change. 16 | 17 | #### Environment Variables 18 | The following environment variables are supported: 19 | * PORT - defaults to 1234 if not set 20 | 21 | ### Deployment 22 | If you want to deploy your application, simply copy the build folder and invoke 23 | ```Bash 24 | node server.js 25 | ``` 26 | -------------------------------------------------------------------------------- /app/templates/_package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= appname %>", 3 | "version": "0.0.0", 4 | "description": "Add your description here.", 5 | "author": "Your Name ", 6 | "main": "src/server.js", 7 | "scripts": { 8 | "lint": "eslint src tools", 9 | "build": "babel-node --eval \"require('./tools/build').default().catch(err => console.error(err.stack))\"", 10 | "clean": "babel-node --eval \"require('./tools/clean').default().catch(err => console.error(err.stack))\"", 11 | "copy": "babel-node --eval \"require('./tools/copy').default().catch(err => console.error(err.stack))\"", 12 | "bundle": "babel-node --eval \"require('./tools/bundle').default().catch(err => console.error(err.stack))\"", 13 | "start": "babel-node --eval \"require('./tools/start').default().catch(err => console.error(err.stack))\"", 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "license": "ISC" 17 | } 18 | -------------------------------------------------------------------------------- /app/templates/gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | src/config/ 4 | -------------------------------------------------------------------------------- /app/templates/public/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stylesuxx/generator-es6-graphql/ef0d2a5413b833d667925710eb814b67e120d264/app/templates/public/.placeholder -------------------------------------------------------------------------------- /app/templates/src/_passport.js: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | <% if (authFull.length > 0) { %>/*import config from './passportConfig'*/<% } %><% authFull.forEach(function(auth){ %> 3 | import passport<%- auth.name %> from '<%- auth.npm %>';<% }); %><%if (auth.length > 0) { %> 4 | <% } %><% if (authLocal) { %>import passportLocal from 'passport-local'; 5 | import Users from './lib/users';<% } %> 6 | <% if (auth.indexOf('passport-github') > -1) { %> 7 | const config = require('./config/passport.json'); 8 | const GithubStrategy = passportGithub.Strategy;<% } %><% if (auth.indexOf('passport-google-oauth') > -1) { %> 9 | const GoogleStrategy = passportGoogle.OAuth2Strategy;<% } %><% if (auth.indexOf('passport-facebook') > -1) { %> 10 | const FacebookStrategy = passportFacebook.Strategy;<% } %><% if (authLocal) { %> 11 | const LocalStrategy = passportLocal.Strategy; 12 | const users = new Users();<% } %><%if (auth.length > 0 || authLocal) { %> 13 | <% } %> 14 | <% authFull.forEach(function(auth){ %>passport.use(new <%- auth.name %>Strategy( 15 | { 16 | clientID: config.<%- auth.slug %>.id, 17 | clientSecret: config.<%- auth.slug %>.secret, 18 | callbackURL: config.<%- auth.slug %>.cb 19 | }, 20 | function(accessToken, refreshToken, profile, done) { 21 | return done(null, profile); 22 | } 23 | )); 24 | 25 | <% }); if(authLocal) { %>passport.use(new LocalStrategy( 26 | function(username, password, done) { 27 | users.login(username, password, done); 28 | } 29 | )); 30 | 31 | <% } %>passport.serializeUser(function(user, done) { 32 | done(null, user); 33 | }); 34 | 35 | passport.deserializeUser(function(obj, done) { 36 | done(null, obj); 37 | }); 38 | 39 | export default passport; 40 | -------------------------------------------------------------------------------- /app/templates/src/_server.js: -------------------------------------------------------------------------------- 1 | import { Schema } from './schema'; 2 | import graphqlHTTP from 'express-graphql'; 3 | import express from 'express'; 4 | import 'babel-polyfill'; 5 | import path from 'path';<% if (database === 'mongoose') { %> 6 | import mongoose from 'mongoose';<% } %><% if (authentication) { %> 7 | import passport from './passport'; 8 | import session from 'express-session';<% } if (authLocal) { %> 9 | import bodyParser from 'body-parser'; 10 | import User from './models/User'; 11 | import mongoStore from 'connect-mongo';<% } %> 12 | 13 | <% if (authLocal) { %>const MongoStore = mongoStore(session);<% } %> 14 | const config = require('./config/main.json'); 15 | const port = (!global.process.env.PORT) ? 1234 : global.process.env.PORT; 16 | const server = global.server = express();<% if (database === 'mongoose') { %> 17 | 18 | mongoose.connect(config.mongoDB);<% } %> 19 | 20 | server.set('port', port); 21 | server.use(express.static(path.join(__dirname, 'public')));<% if (authLocal) { %> 22 | server.use(bodyParser.urlencoded({ extended: true })); 23 | server.use(bodyParser.json());<% } %><% if (authentication) { %> 24 | server.use(passport.initialize()); 25 | server.use(passport.session()); 26 | server.use(session({ 27 | secret: config.sessionSecret, 28 | resave: true, 29 | saveUninitialized: true<% if (authLocal) { %>, 30 | store: new MongoStore({ mongooseConnection: mongoose.connection })<% } %> 31 | }));<% if (auth.indexOf('passport-github') > -1) { %> 32 | 33 | server.get('/auth/github', passport.authenticate('github')); 34 | server.get('/auth/github/callback', passport.authenticate('github', { 35 | successRedirect: '/', 36 | failureRedirect: '/login' 37 | }));<% }; %><% if (auth.indexOf('passport-google-oauth') > -1) { %> 38 | 39 | server.get('/auth/google', passport.authenticate('google', {scope: 'https://www.google.com/m8/feeds'})); 40 | server.get('/auth/google/callback', passport.authenticate('google', { 41 | successRedirect: '/', 42 | failureRedirect: '/login' 43 | }));<% }; %><% if (auth.indexOf('passport-facebook') > -1) { %> 44 | 45 | server.get('/auth/facebook', passport.authenticate('facebook')); 46 | server.get('/auth/facebook/callback', passport.authenticate('facebook', { 47 | successRedirect: '/', 48 | failureRedirect: '/login' 49 | }));<% }; %><% } %> 50 | <% if(authLocal) { %> 51 | server.post('/login', passport.authenticate('local'), function(req, res) { 52 | res.sendStatus(200); 53 | }); 54 | 55 | server.get('/logout', function(req, res) { 56 | req.logout(); 57 | req.session.destroy(); 58 | res.sendStatus(200); 59 | }); 60 | <% } %> 61 | server.use('<%= graphqlroute %>', graphqlHTTP(request => ({ 62 | schema: Schema, 63 | rootValue: { session: request.session }, 64 | graphiql: <%= graphiql %> 65 | }))); 66 | 67 | server.listen(server.get('port'), () => { 68 | console.log('The server is running at http://localhost:' + server.get('port')); 69 | }); 70 | -------------------------------------------------------------------------------- /app/templates/src/config/_main.json: -------------------------------------------------------------------------------- 1 | { 2 | "sessionSecret": "<%= secret %>"<% if(database === 'mongoose') { %>, 3 | "mongoDB": "mongodb://localhost/<%= databaseName %>"<% } %> 4 | } 5 | -------------------------------------------------------------------------------- /app/templates/src/config/_passport.json: -------------------------------------------------------------------------------- 1 | { 2 | <% authFull.forEach(function(auth, index){ %> "<%- auth.slug %>": { 3 | "id": "<%- auth.slug %>-id", 4 | "secret": "<%- auth.slug %>-secret", 5 | "cb": "http://localhost:1234/auth/<%- auth.slug %>/callback" 6 | }<% if(index !== authFull.length - 1) { %>,<% } %> 7 | <% }); %>} 8 | -------------------------------------------------------------------------------- /app/templates/src/lib/items.js: -------------------------------------------------------------------------------- 1 | const items = [ 2 | { 3 | id: 1, 4 | name: 'item 1' 5 | }, 6 | { 7 | id: 2, 8 | name: 'item 2' 9 | }, 10 | ]; 11 | 12 | class Items { 13 | getAll() { 14 | return items; 15 | } 16 | 17 | getById(id) { 18 | return items[id - 1]; 19 | } 20 | } 21 | 22 | export default Items; 23 | -------------------------------------------------------------------------------- /app/templates/src/lib/users.js: -------------------------------------------------------------------------------- 1 | import User from '../models/User'; 2 | 3 | class Users { 4 | getList() { 5 | const getUsers = User.find({}).exec(); 6 | return getUsers; 7 | } 8 | 9 | signup(username, password) { 10 | var user = new User({ 11 | username: username, 12 | password: password 13 | }); 14 | 15 | return user.save(); 16 | } 17 | 18 | login(username, password, done) { 19 | var findUser = User.findOne({ username: username }).exec(); 20 | var user = {}; 21 | findUser.then((data) => { 22 | user = data; 23 | return user.validPassword(password); 24 | }).then(()=> { 25 | return done(null, user); 26 | }, (err)=> { 27 | return done(null, false, { message: 'Username and password do not match.' }); 28 | }); 29 | } 30 | 31 | updateMail(id, mail) { 32 | const getUser = User.findOne({_id: id}).exec(); 33 | return getUser.then((user) => { 34 | user.mail = mail; 35 | return user.save(); 36 | }); 37 | } 38 | } 39 | 40 | export default Users; 41 | -------------------------------------------------------------------------------- /app/templates/src/models/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stylesuxx/generator-es6-graphql/ef0d2a5413b833d667925710eb814b67e120d264/app/templates/src/models/.placeholder -------------------------------------------------------------------------------- /app/templates/src/models/User.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import bcrypt from 'bcrypt'; 3 | 4 | const Schema = mongoose.Schema; 5 | const UserSchema = new Schema({ 6 | username: { type: String, required: true, index: { unique: true } }, 7 | password: { type: String, required: true }, 8 | createdAt: { type: Date }, 9 | updatedAt: { type: Date }, 10 | mail: { type: String } 11 | }); 12 | 13 | UserSchema 14 | .virtual('createdAt') 15 | .get(function () { 16 | return this._id.getTimestamp(); 17 | }); 18 | 19 | UserSchema.set('toJSON', { virtuals: true, getters: true }); 20 | UserSchema.set('toObject', { virtuals: true, getters: true }); 21 | 22 | UserSchema.pre('save', function(next) { 23 | var user = this; 24 | const now = new Date(); 25 | 26 | user.updatedAt = now; 27 | 28 | if(!user.isModified('password')) return next(); 29 | bcrypt.genSalt(function(err, salt) { 30 | if (err) return next(err); 31 | 32 | bcrypt.hash(user.password, salt, function(err, hash) { 33 | if (err) return next(err); 34 | 35 | user.password = hash; 36 | next(); 37 | }); 38 | }); 39 | }); 40 | 41 | UserSchema.methods.validPassword = function(password) { 42 | return new Promise((resolve, reject) => { 43 | bcrypt.compare(password, this.password, function(err, match) { 44 | if (err) reject(err); 45 | (match) ? resolve() : reject('Passwords do not match'); 46 | }); 47 | }); 48 | }; 49 | 50 | module.exports = mongoose.model('User', UserSchema); 51 | -------------------------------------------------------------------------------- /app/templates/src/schema/_index.js: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLSchema } from 'graphql'; 2 | import { getItemField } from './items';<% if (authLocal) { %> 3 | import { 4 | selfField, 5 | userListField, 6 | userSignupField, 7 | userUpdateMailField 8 | } from './users';<% } %> 9 | 10 | const queryType = new GraphQLObjectType({ 11 | name: 'Query', 12 | description: 'Root for query operations.', 13 | fields: () => ({ 14 | getItem: getItemField<% if (authLocal) { %>, 15 | self: selfField, 16 | users: userListField<% } %> 17 | }) 18 | }); 19 | <% if (authLocal) { %> 20 | const mutationType = new GraphQLObjectType({ 21 | name: 'Mutation', 22 | description: 'Root for mutation operations.', 23 | fields: () => ({ 24 | signup: userSignupField, 25 | updateEmail: userUpdateMailField 26 | }) 27 | });<% } %> 28 | 29 | export const Schema = new GraphQLSchema({ 30 | query: queryType<% if (authLocal) { %>, 31 | mutation: mutationType<% } %> 32 | }); 33 | -------------------------------------------------------------------------------- /app/templates/src/schema/items.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInterfaceType, 3 | GraphQLObjectType, 4 | GraphQLSchema, 5 | GraphQLString, 6 | GraphQLInt, 7 | GraphQLFloat, 8 | GraphQLList, 9 | GraphQLNonNull 10 | } from 'graphql'; 11 | import Items from '../lib/items'; 12 | 13 | const items = new Items(); 14 | 15 | const itemType = new GraphQLObjectType({ 16 | name: 'Item', 17 | description: 'An item.', 18 | fields: () => ({ 19 | id: { 20 | description: 'The items id.', 21 | type: GraphQLInt 22 | }, 23 | name: { 24 | description: 'The items name.', 25 | type: GraphQLString 26 | } 27 | }) 28 | }); 29 | 30 | const getById = { 31 | description: 'Get an item by id.', 32 | type: itemType, 33 | args: { 34 | id: { 35 | description: 'ID of the item to retreive.', 36 | type: new GraphQLNonNull(GraphQLInt) 37 | } 38 | }, 39 | resolve: (root, {id}) => items.getById(id) 40 | }; 41 | 42 | export const getItemField = getById; 43 | -------------------------------------------------------------------------------- /app/templates/src/schema/users.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLString, 4 | GraphQLList, 5 | GraphQLID 6 | } from 'graphql'; 7 | import { 8 | GraphQLEmail, 9 | GraphQLPassword, 10 | GraphQLDateTime 11 | } from 'graphql-custom-types'; 12 | import { GraphQLError } from 'graphql/error'; 13 | import Users from '../lib/users'; 14 | 15 | const users = new Users(); 16 | 17 | const userType = new GraphQLObjectType({ 18 | name: 'User', 19 | description: 'Representation of public user data.', 20 | fields: () => ({ 21 | _id: { 22 | description: 'Unique user id.', 23 | type: GraphQLID 24 | }, 25 | username: { 26 | description: 'Unique username.', 27 | type: GraphQLString 28 | }, 29 | createdAt: { 30 | description: 'Time of user creation.', 31 | type: GraphQLDateTime 32 | }, 33 | updatedAt: { 34 | description: 'Time of last user update.', 35 | type: GraphQLDateTime 36 | }, 37 | mail: { 38 | description: 'Optional E-Mail address.', 39 | type: GraphQLEmail 40 | } 41 | }) 42 | }); 43 | 44 | const _self = { 45 | description: 'Information about the currently logged in user.', 46 | type: userType, 47 | resolve(parentValue, _, { rootValue: { session } }) { 48 | if(session.passport) { 49 | return session.passport.user; 50 | } 51 | throw new GraphQLError('Query error: Not logged in'); 52 | } 53 | }; 54 | 55 | const _list = { 56 | description: 'Information about all users.', 57 | type: new GraphQLList(userType), 58 | resolve(parentValue, _, { rootValue: { session } }) { 59 | return users.getList(); 60 | } 61 | }; 62 | 63 | const _updateMail = { 64 | description: 'Update mail address of the currently logged in user.', 65 | type: userType, 66 | args: { 67 | mail: { 68 | description: 'Non empty, valid E-Mail address.', 69 | type: GraphQLEmail 70 | } 71 | }, 72 | resolve(parentValue, _, { rootValue: { session } }) { 73 | if(session.passport) { 74 | return users.updateMail(session.passport.user._id, _.mail); 75 | } 76 | throw new GraphQLError('Query error: Not logged in'); 77 | } 78 | } 79 | 80 | const _signup = { 81 | description: 'Register a new user account. Returns newly created user or null if username is taken.', 82 | type: userType, 83 | args: { 84 | username: { 85 | description: 'Username for new account.', 86 | type: GraphQLString 87 | }, 88 | password: { 89 | description: 'Password for new account.', 90 | type: new GraphQLPassword(6) 91 | } 92 | }, 93 | resolve(parentValue, _, { rootValue: { data } }) { 94 | return users.signup(_.username, _.password); 95 | } 96 | } 97 | 98 | export const selfField = _self; 99 | export const userListField = _list; 100 | export const userUpdateMailField = _updateMail; 101 | export const userSignupField = _signup; 102 | -------------------------------------------------------------------------------- /app/templates/tools/build.js: -------------------------------------------------------------------------------- 1 | export default async () => { 2 | await require('./clean').default(); 3 | await require('./bundle').default(); 4 | await require('./copy').default(); 5 | }; 6 | -------------------------------------------------------------------------------- /app/templates/tools/bundle.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import config from './config'; 3 | 4 | export default async () => new Promise((resolve, reject) => { 5 | const bundler = webpack(config); 6 | let bundlerRunCount = 0; 7 | 8 | function bundle(err, stats) { 9 | if (err) { 10 | return reject(err); 11 | } 12 | 13 | console.log(stats.toString(config[0].stats)); 14 | 15 | if (++bundlerRunCount === (global.watch ? config.length : 1)) { 16 | return resolve(); 17 | } 18 | } 19 | 20 | if (global.WATCH) { 21 | bundler.watch(200, bundle); 22 | } else { 23 | bundler.run(bundle); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /app/templates/tools/clean.js: -------------------------------------------------------------------------------- 1 | import del from 'del'; 2 | import mkdirp from 'mkdirp'; 3 | 4 | export default async () => { 5 | await del(['build/*'], { dot: true }); 6 | await mkdirp('build/public'); 7 | }; 8 | -------------------------------------------------------------------------------- /app/templates/tools/config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack, { DefinePlugin, BannerPlugin } from 'webpack'; 3 | import merge from 'lodash.merge'; 4 | 5 | const DEBUG = !process.argv.includes('release'); 6 | const WATCH = global.WATCH || false; 7 | const VERBOSE = process.argv.includes('verbose'); 8 | const GLOBALS = { 9 | 'process.env.NODE_ENV': DEBUG ? '"development"' : '"production"', 10 | '__DEV__': DEBUG 11 | }; 12 | 13 | const config = { 14 | output: { 15 | publicPath: '/', 16 | sourcePrefix: ' ' 17 | }, 18 | 19 | cache: DEBUG, 20 | debug: DEBUG, 21 | 22 | stats: { 23 | colors: true, 24 | reasons: DEBUG, 25 | hash: VERBOSE, 26 | version: VERBOSE, 27 | timings: true, 28 | chunks: VERBOSE, 29 | chunkModules: VERBOSE, 30 | cached: VERBOSE, 31 | cachedAssets: VERBOSE 32 | }, 33 | 34 | plugins: [ 35 | new webpack.optimize.OccurenceOrderPlugin() 36 | ], 37 | 38 | module: { 39 | loaders: [{ 40 | test: /\.js$/, 41 | include: [ 42 | path.resolve(__dirname, '../src') 43 | ], 44 | loaders: ['babel-loader'] 45 | }, { 46 | test: /\.json$/, 47 | loader: 'json-loader' 48 | }, { 49 | test: /\.txt$/, 50 | loader: 'raw-loader' 51 | }, { 52 | test: /\.(png|jpg|jpeg|gif|svg|woff|woff2)$/, 53 | loader: 'url-loader?limit=10000' 54 | }, { 55 | test: /\.(eot|tft|wav|mp3)$/, 56 | loader: 'file-loader' 57 | }] 58 | } 59 | }; 60 | 61 | const serverConfig = merge({}, config, { 62 | entry: './src/server.js', 63 | output: { 64 | path: './build', 65 | filename: 'server.js', 66 | libraryTarget: 'commonjs2' 67 | }, 68 | target: 'node', 69 | externals: [ 70 | function (context, request, cb) { 71 | var isExternal = 72 | request.match(/^[a-z][a-z\/\.\-0-9]*$/i) && 73 | !request.match(/^react-routing/) && 74 | !context.match(/[\\/]react-routing/); 75 | cb(null, Boolean(isExternal)); 76 | } 77 | ], 78 | node: { 79 | console: false, 80 | global: false, 81 | process: false, 82 | Buffer: false, 83 | __filename: false, 84 | __dirname: false 85 | }, 86 | devtool: DEBUG ? 'source-map' : 'cheap-module-source-map', 87 | plugins: [ 88 | ...config.plugins, 89 | new DefinePlugin(merge({}, GLOBALS, {'__SERVER__': true})), 90 | new BannerPlugin('require("source-map-support").install();', 91 | { raw: true, entryOnly: false }) 92 | ] 93 | }); 94 | 95 | export default [serverConfig]; 96 | -------------------------------------------------------------------------------- /app/templates/tools/copy.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import replace from 'replace'; 3 | import copy from './lib/copy'; 4 | 5 | export default async () => { 6 | await Promise.all([ 7 | copy('package.json', 'build/package.json'), 8 | copy('public', 'build/public') 9 | ]); 10 | 11 | replace({ 12 | regex: '"start".*', 13 | replacement: '"start": "node server.js",', 14 | paths: [path.join(__dirname, '../build/package.json')], 15 | recursive: false, 16 | silent: false 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /app/templates/tools/lib/copy.js: -------------------------------------------------------------------------------- 1 | import ncp from 'ncp'; 2 | 3 | export default (source, dest) => new Promise((resolve, reject) => { 4 | ncp(source, dest, err => err ? reject(err) : resolve()); 5 | }); -------------------------------------------------------------------------------- /app/templates/tools/lib/watch.js: -------------------------------------------------------------------------------- 1 | import gaze from 'gaze'; 2 | 3 | export default (pattern) => new Promise((resolve, reject) => { 4 | gaze(pattern, (err, watcher) => err ? reject(err) : resolve(watcher)); 5 | }); 6 | -------------------------------------------------------------------------------- /app/templates/tools/serve.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import cp from 'child_process'; 3 | import watch from './lib/watch'; 4 | 5 | export default (env) => new Promise((resolve, reject) => { 6 | function start() { 7 | const server = cp.spawn( 8 | 'node', 9 | [path.join(__dirname, '../build/server.js')], 10 | { 11 | env: Object.assign({ NODE_ENV: 'development' }, process.env), 12 | silent: false 13 | } 14 | ); 15 | 16 | server.stdout.on('data', data => { 17 | var time = new Date().toTimeString(); 18 | time = time.replace(/.*(\d{2}:\d{2}:\d{2}).*/, '$1'); 19 | process.stdout.write(`[${time}] `); 20 | process.stdout.write(data); 21 | if (data.toString('utf8').includes('The server is running at')) { 22 | resolve(); 23 | } 24 | }); 25 | 26 | server.stderr.on('data', data => process.stderr.write(data)); 27 | server.on('error', err => reject(err)); 28 | server.on('exit', () => server.kill('SIGTERM')); 29 | 30 | return server; 31 | } 32 | var server = start(); 33 | 34 | if (global.WATCH) { 35 | watch('build/server.js').then(watcher => { 36 | watcher.on('changed', () => { 37 | server.kill('SIGTERM'); 38 | server = start(); 39 | }); 40 | }); 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /app/templates/tools/start.js: -------------------------------------------------------------------------------- 1 | export default async () => { 2 | global.WATCH = true; 3 | 4 | await require('./build').default(); 5 | await require('./serve').default('develop'); 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generator-es6-graphql", 3 | "repository": "stylesuxx/generator-es6-graphql", 4 | "version": "0.8.0", 5 | "author": "Chris Landa ", 6 | "description": "This generator enables you to quickly create an ES6 (ES2105) GraphQL enabled server with a lot of optional additions.", 7 | "files": [ 8 | "app" 9 | ], 10 | "scripts": { 11 | "test": "mocha" 12 | }, 13 | "keywords": [ 14 | "yeoman-generator", 15 | "es6", 16 | "es2015", 17 | "graphQL", 18 | "graphiQL", 19 | "express", 20 | "express-graphql", 21 | "webpack", 22 | "passport" 23 | ], 24 | "license": "MIT", 25 | "dependencies": { 26 | "chalk": "^1.1.1", 27 | "crypto": "0.0.3", 28 | "yeoman-assert": "^2.2.1", 29 | "yeoman-generator": "^0.23.3", 30 | "yeoman-test": "^1.4.0" 31 | }, 32 | "devDependencies": { 33 | "babel-eslint": "^6.0.4", 34 | "eslint": "^2.9.0", 35 | "mocha": "^2.3.3", 36 | "mockery": "^1.4.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var assert = require('yeoman-assert'); 5 | var helpers = require('yeoman-test'); 6 | var mockery = require('mockery'); 7 | 8 | var runAll = function(it) { 9 | it('fills package.json with correct information', function () { 10 | assert.fileContent('package.json', /"name": "temp"/); 11 | }); 12 | 13 | it('fills src/config/main.json with correct information', function () { 14 | assert.fileContent('src/config/main.json', /"sessionSecret": ".*"/); 15 | }); 16 | } 17 | 18 | var defaultFiles = function(it) { 19 | it('creates default files', function () { 20 | var expected = [ 21 | 'package.json', 22 | '.babelrc', 23 | '.eslintrc', 24 | '.travis.yml', 25 | 'README.md', 26 | '.gitignore', 27 | 'src/server.js', 28 | 'public/.placeholder', 29 | 'tools/lib/copy.js', 30 | 'tools/build.js', 31 | 'tools/bundle.js', 32 | 'tools/clean.js', 33 | 'tools/config.js', 34 | 'tools/copy.js', 35 | 'tools/serve.js', 36 | 'tools/start.js', 37 | 'src/config/main.json', 38 | 'src/lib/items.js', 39 | 'src/schema/items.js', 40 | 'src/schema/index.js', 41 | 'src/passport.js' 42 | ]; 43 | 44 | assert.file(expected); 45 | }); 46 | } 47 | 48 | describe('generator:app', function () { 49 | before(function () { 50 | mockery.enable({warnOnUnregistered: false}); 51 | }); 52 | 53 | after(function () { 54 | mockery.disable(); 55 | }); 56 | 57 | describe('defaults', function () { 58 | before(function (done) { 59 | helpers.run(path.join(__dirname, '../app')) 60 | .withPrompts({ 61 | name: 'temp', 62 | database: 'mongoose', 63 | authLocal: true 64 | }) 65 | .on('end', function() { 66 | done(); 67 | }); 68 | }); 69 | 70 | defaultFiles(it); 71 | runAll(it); 72 | 73 | it('creates passport related files', function () { 74 | var expected = [ 75 | 'src/passport.js' 76 | ]; 77 | 78 | assert.file(expected); 79 | }); 80 | }); 81 | 82 | describe('no database', function () { 83 | before(function (done) { 84 | helpers.run(path.join(__dirname, '../app')) 85 | .withPrompts({ 86 | name: 'temp', 87 | database: 'none' 88 | }) 89 | .on('end', done); 90 | }); 91 | 92 | defaultFiles(it); 93 | runAll(it); 94 | }); 95 | 96 | describe('oAuth', function () { 97 | before(function (done) { 98 | helpers.run(path.join(__dirname, '../app')) 99 | .withPrompts({ 100 | name: 'temp', 101 | auth: ['passport-facebook', 'passport-github', 'passport-google-oauth'] 102 | }) 103 | .on('end', done); 104 | }); 105 | 106 | defaultFiles(it); 107 | runAll(it); 108 | 109 | it('creates passport related files', function () { 110 | var expected = [ 111 | 'src/passport.js', 112 | 'src/config/passport.json' 113 | ]; 114 | 115 | assert.file(expected); 116 | }); 117 | 118 | it('fills src/config/passport.json with correct information', function () { 119 | assert.fileContent('src/config/passport.json', /"facebook":/); 120 | assert.fileContent('src/config/passport.json', /"github":/); 121 | assert.fileContent('src/config/passport.json', /"google":/); 122 | }); 123 | }); 124 | 125 | describe('oAuth & local', function () { 126 | before(function (done) { 127 | helpers.run(path.join(__dirname, '../app')) 128 | .withPrompts({ 129 | name: 'temp', 130 | database: 'mongoose', 131 | authLocal: true, 132 | auth: ['passport-facebook', 'passport-github', 'passport-google-oauth'] 133 | }) 134 | .on('end', done); 135 | }); 136 | 137 | defaultFiles(it); 138 | runAll(it); 139 | 140 | it('creates passport related files', function () { 141 | var expected = [ 142 | 'src/passport.js', 143 | 'src/config/passport.json' 144 | ]; 145 | 146 | assert.file(expected); 147 | }); 148 | 149 | it('fills src/config/passport.json with correct information', function () { 150 | assert.fileContent('src/config/passport.json', /"facebook":/); 151 | assert.fileContent('src/config/passport.json', /"github":/); 152 | assert.fileContent('src/config/passport.json', /"google":/); 153 | }); 154 | }); 155 | }); 156 | --------------------------------------------------------------------------------