├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── domain ├── managers │ ├── PostManager.js │ └── index.js ├── mocks │ ├── Author.js │ ├── Post.js │ ├── Query.js │ ├── String.js │ └── index.js ├── models │ ├── Author.js │ ├── Post.js │ ├── Query.js │ └── index.js └── schemas │ ├── Author.graphql │ ├── Post.graphql │ ├── Query.graphql │ ├── index.js │ └── schema.graphql ├── nodemon.json ├── package.json └── server ├── connectors ├── MongoDB.js └── index.js ├── fixtures.js ├── index.js └── models.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"], 3 | "plugins": [ 4 | ["babel-root-slash-import", { 5 | "rootPathSuffix": "./" 6 | }], 7 | "babel-plugin-inline-import", 8 | "transform-class-properties" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # database files 2 | data 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directory 30 | node_modules 31 | 32 | # Optional npm cache directory 33 | .npm 34 | 35 | # Optional REPL history 36 | .node_repl_history 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Quadric ApS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **DEPRECATION NOTE:** 2 | 3 | _This project has started a couple of years ago as we intended to create a way to allow developers to focus on building a structured domain based on well defined patterns instead of having the mess of loose uncoupled resolvers._ 4 | 5 | _Unfortunately we haven’t had the proper time to develop this project further, so it has never gained enough momentum. Since then, as well, lots has changed in the ecosystem. A lot of the goals of this project have already been achieved by other projects, such as https://prisma.io._ 6 | 7 | _Prisma is probably the way to go if you look for something mature and reliable. If you feel adventurous, though, you can take a look at our new experiment: https://github.com/zvictor/faugra._ 8 | 9 | --- 10 | 11 | # Perfect GraphQL Starter 12 | 13 | > "Have no fear of perfection, you’ll never reach it." - Salvador Dali 14 | 15 | 16 | 17 |

18 | 19 |

20 | 21 | ## Why 22 | 23 | This project aims to be a place for the community to **spread good practices** and the use of related technologies. 24 | 25 | 26 | It is inspired by the tutorial [How to build a GraphQL server](https://medium.com/apollo-stack/tutorial-building-a-graphql-server-cddaa023c035#.wy5h1htxs) and its [repository](https://github.com/apollostack/apollo-starter-kit). 27 | 28 | _There will never be an agreement on a perfect boilerplate project for any technology we are aware of and it would not be different for a GraphQL-based project. But it doesn't mean we should not try to get as close as we can get from it. So please don't mind our pretentious project name, it's just a catchy one._ 29 | 30 | ## Install 31 | 32 | As simple as that: 33 | ```sh 34 | git clone https://github.com/Quadric/perfect-graphql-starter 35 | cd perfect-graphql-starter 36 | npm install 37 | npm start 38 | ``` 39 | 40 | ## Getting started 41 | * open [http://localhost:8080/graphiql/](http://localhost:8080/graphiql/) 42 | 43 | * Paste this on the left side of the page: 44 | 45 | ([Run](http://localhost:8080/graphiql/?query=%7B%0A%20%20getAuthor\(_id%3A%202\)%20%7B%0A%20%20%20%20lastName%0A%20%20%20%20posts%20%7B%0A%20%20%20%20%20%20text%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D)) 46 | ```graphql 47 | { 48 | getAuthor(_id: 2) { 49 | lastName 50 | posts { 51 | text 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | * Hit the play button (cmd-return), then you should get this on the right side: 58 | 59 | ```json 60 | { 61 | "data": { 62 | "getAuthor": { 63 | "lastName": "Lombardi", 64 | "posts": [ 65 | { 66 | "text": "Perfection is not attainable, but if we chase perfection we can catch excellence.", 67 | } 68 | ] 69 | } 70 | } 71 | } 72 | ``` 73 | 74 | # Examples 75 | There is more you can try! Go back to the [interactive tool](http://localhost:8080/graphiql/) and paste any of the following snippets there and check the result: 76 | 77 | ([Run](http://localhost:8080/graphiql/?query=%7B%0A%20%20getAuthor\(_id%3A%202\)%20%7B%20%20%23%20Almost%20the%20same%20as%0A%20%20%20%20firstName%20%20%20%20%20%20%20%20%20%20%23%20before%2C%20but%20with%20extra%0A%20%20%20%20lastName%20%20%20%20%20%20%20%20%20%20%20%23%20fields.%0A%20%20%20%20posts%20%7B%0A%20%20%20%20%20%20title%0A%20%20%20%20%20%20text%0A%20%20%20%20%20%20views%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D)) 78 | ```graphql 79 | { 80 | getAuthor(_id: 2) { # Almost the same as 81 | firstName # before, but with extra 82 | lastName # fields. 83 | posts { 84 | title 85 | text 86 | views 87 | } 88 | } 89 | } 90 | ``` 91 | 92 | ([Run](http://localhost:8080/graphiql/?query=%7B%0A%20%20getPostsByTitle\(titleContains%3A%20%22fear%22\)%20%7B%0A%20%20%20%20title%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Try%20adding%20the%20%27author%27%0A%20%20%20%20text%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20field%20anywhere%20inside%0A%20%20%20%20views%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20this%20block%20%3B\)%0A%20%20%7D%0A%7D&variables=)) 93 | ```graphql 94 | { 95 | getPostsByTitle(titleContains: "fear") { 96 | title # Try adding the 'author' 97 | text # field anywhere inside 98 | views # this block ;) 99 | } 100 | } 101 | ``` 102 | 103 | ([Run](http://localhost:8080/graphiql/?query=%7B%0A%20%20getPostsByAuthor\(authorId%3A1\)%20%7B%20%20%23%20This%20author%20has%20a%20private%0A%20%20%20%20title%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20post.%20You%20should%20get%20an%0A%20%20%20%20text%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Authorization%20error.%0A%20%20%20%20views%0A%20%20%7D%0A%7D&variables=)) 104 | ```graphql 105 | { 106 | getPostsByAuthor(authorId:1) { # This author has a private 107 | title # post. You should get an 108 | text # Authorization error. 109 | views 110 | } 111 | } 112 | ``` 113 | -------------------------------------------------------------------------------- /domain/managers/PostManager.js: -------------------------------------------------------------------------------- 1 | import { Manager } from 'graph-object'; 2 | 3 | export default class PostManager extends Manager { 4 | findByAuthor(_id) { 5 | return this.find({ 'author._id': _id }); 6 | } 7 | 8 | findByTitle(title) { 9 | const selector = { title }; 10 | 11 | if (title && title.contains) { 12 | selector.title = new RegExp(title.contains, 'i'); 13 | } 14 | 15 | return this.find(selector); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /domain/managers/index.js: -------------------------------------------------------------------------------- 1 | import PostManager from './PostManager'; 2 | 3 | export { 4 | PostManager, 5 | }; 6 | -------------------------------------------------------------------------------- /domain/mocks/Author.js: -------------------------------------------------------------------------------- 1 | import casual from 'casual'; 2 | 3 | export default () => ({ 4 | firstName: casual.first_name, 5 | lastName: casual.last_name, 6 | }); 7 | -------------------------------------------------------------------------------- /domain/mocks/Post.js: -------------------------------------------------------------------------------- 1 | import casual from 'casual'; 2 | 3 | export default () => ({ 4 | title: casual.title, 5 | text: casual.sentences(3), 6 | }); 7 | -------------------------------------------------------------------------------- /domain/mocks/Query.js: -------------------------------------------------------------------------------- 1 | export default () => ({ 2 | author: (root, args) => ({ 3 | firstName: args.firstName, 4 | lastName: args.lastName, 5 | }), 6 | }); 7 | -------------------------------------------------------------------------------- /domain/mocks/String.js: -------------------------------------------------------------------------------- 1 | export default () => 'Lorem Ipsum'; 2 | -------------------------------------------------------------------------------- /domain/mocks/index.js: -------------------------------------------------------------------------------- 1 | import Author from './Author'; 2 | import Post from './Post'; 3 | import Query from './Query'; 4 | import String from './String'; 5 | 6 | export { 7 | Author, 8 | Post, 9 | Query, 10 | String, 11 | }; 12 | -------------------------------------------------------------------------------- /domain/models/Author.js: -------------------------------------------------------------------------------- 1 | import { Model } from 'graph-object'; 2 | import Post from './Post'; 3 | 4 | export default class Author extends Model { 5 | posts() { 6 | return this._posts || (this._posts = Post.objects.find({ 'author._id': this._raw._id })); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /domain/models/Post.js: -------------------------------------------------------------------------------- 1 | import { Model, allow } from 'graph-object'; 2 | import { PostManager } from '/domain/managers'; 3 | import Author from './Author'; 4 | 5 | export default class Post extends Model { 6 | static managers = { 7 | objects: PostManager, 8 | }; 9 | 10 | get author() { 11 | return this._author || (this._author = Author.objects.getById(this._raw.author._id)); 12 | } 13 | } 14 | 15 | allow(Post, { 16 | read(context) { 17 | return this.author._id === context.userId || !this.private; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /domain/models/Query.js: -------------------------------------------------------------------------------- 1 | import Author from './Author'; 2 | import Post from './Post'; 3 | 4 | export default class Query { 5 | getAuthor({ _id }, context) { 6 | return Author.objects.getById(_id); 7 | } 8 | 9 | getPostsByTitle({ titleContains }, context) { 10 | return Post.objects.findByTitle({ contains: titleContains }); 11 | } 12 | 13 | getPostsByAuthor({ authorId }, context) { 14 | return Post.objects.findByAuthor(authorId); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /domain/models/index.js: -------------------------------------------------------------------------------- 1 | import Author from './Author'; 2 | import Post from './Post'; 3 | import Query from './Query'; 4 | 5 | export { 6 | Author, 7 | Post, 8 | Query, 9 | }; 10 | -------------------------------------------------------------------------------- /domain/schemas/Author.graphql: -------------------------------------------------------------------------------- 1 | type Author { 2 | _id: Int! 3 | firstName: String 4 | lastName: String 5 | posts: [Post] 6 | } 7 | -------------------------------------------------------------------------------- /domain/schemas/Post.graphql: -------------------------------------------------------------------------------- 1 | type Post { 2 | _id: Int! 3 | private: Boolean 4 | title: String 5 | text: String 6 | views: Int 7 | author: Author 8 | } 9 | -------------------------------------------------------------------------------- /domain/schemas/Query.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | getAuthor(_id: Int!): Author 3 | getPostsByAuthor(authorId: Int!): [Post] 4 | getPostsByTitle(titleContains: String!): [Post] 5 | } 6 | -------------------------------------------------------------------------------- /domain/schemas/index.js: -------------------------------------------------------------------------------- 1 | import { addMockFunctionsToSchema, buildSchemaFromTypeDefinitions } from 'graphql-tools'; 2 | import * as mocks from '/domain/mocks'; 3 | import Author from './Author.graphql'; 4 | import Post from './Post.graphql'; 5 | import Query from './Query.graphql'; 6 | import schema from './schema.graphql'; 7 | 8 | export const raw = [ 9 | Author, 10 | Post, 11 | Query, 12 | schema, 13 | ]; 14 | 15 | const executable = buildSchemaFromTypeDefinitions(raw); 16 | 17 | export const mocked = addMockFunctionsToSchema({ 18 | mocks, 19 | preserveResolvers: false, 20 | schema: buildSchemaFromTypeDefinitions(raw), 21 | }); 22 | 23 | export default executable; 24 | -------------------------------------------------------------------------------- /domain/schemas/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | } 4 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": false, 3 | "ignore": ["node_modules"], 4 | "env": { 5 | "NODE_ENV": "development", 6 | "BABEL_DISABLE_CACHE": 1, 7 | "MONGO_URL": "mongodb://localhost:27017/perfect-graphql-starter" 8 | }, 9 | "execMap": { 10 | "js": "babel-node" 11 | }, 12 | "ext": ".js,.json,.graphql", 13 | "watch": "./", 14 | "events": { 15 | "start": "mkdir -p data && mongod --dbpath ./data > /dev/null" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "perfect-graphql-starter", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "'Perfect' set of code to write a GraphQL server with Apollo", 6 | "engine": "node >= 5.11.1", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/Quadric/perfect-graphql-starter.git" 10 | }, 11 | "keywords": [ 12 | "Node.js", 13 | "Javascript", 14 | "GraphQL", 15 | "Express", 16 | "Apollo", 17 | "Meteor" 18 | ], 19 | "author": "Victor Duarte ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/Quadric/perfect-graphql-starter/issues" 23 | }, 24 | "homepage": "https://github.com/Quadric/perfect-graphql-starter#readme", 25 | "scripts": { 26 | "start": "nodemon server/index.js", 27 | "debug": "nodemon server/index.js --exec babel-node-debug", 28 | "test": "echo \"Error: no test specified\" && exit 1", 29 | "lint": "eslint .", 30 | "lint:fix": "eslint . --fix" 31 | }, 32 | "dependencies": { 33 | "apollo-server": "^0.2.6", 34 | "body-parser": "^1.15.2", 35 | "casual": "^1.5.3", 36 | "express": "4.14.0", 37 | "graph-object": "^1.0.0", 38 | "graphql": "^0.7.0", 39 | "graphql-tools": "^0.6.5", 40 | "lodash": "^4.15.0", 41 | "mongodb": "^3.6.1" 42 | }, 43 | "devDependencies": { 44 | "babel-cli": "6.14.0", 45 | "babel-core": "^6.14.0", 46 | "babel-eslint": "^6.1.2", 47 | "babel-loader": "6.2.5", 48 | "babel-node-debug": "^2.0.0", 49 | "babel-plugin-inline-import": "^2.0.1", 50 | "babel-plugin-transform-class-properties": "^6.11.5", 51 | "babel-polyfill": "6.13.0", 52 | "babel-preset-es2015": "6.14.0", 53 | "babel-preset-react": "^6.11.1", 54 | "babel-preset-stage-0": "6.5.0", 55 | "babel-root-slash-import": "^1.1.0", 56 | "eslint": "^3.4.0", 57 | "eslint-config-airbnb": "^10.0.1", 58 | "eslint-plugin-import": "^1.14.0", 59 | "eslint-plugin-react": "^6.2.0", 60 | "nodemon": "^1.10.2" 61 | }, 62 | "eslintConfig": { 63 | "parser": "babel-eslint", 64 | "extends": [ 65 | "airbnb/base", 66 | "plugin:import/errors" 67 | ], 68 | "rules": { 69 | "no-use-before-define": 0, 70 | "arrow-body-style": 0, 71 | "dot-notation": 0, 72 | "no-console": 0 73 | }, 74 | "env": { 75 | "mocha": true 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /server/connectors/MongoDB.js: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb'; 2 | 3 | export default class MongoDB { 4 | static connection; 5 | 6 | constructor() { 7 | if (MongoDB.connection === undefined) { 8 | MongoDB.connection = null; 9 | 10 | Promise.all([ 11 | MongoDB.connect(), 12 | ]) 13 | .then(() => { console.log('[MongoDb] connected.'); }) 14 | .catch((err) => { 15 | console.error(err.stack || err); 16 | process.exit(1); 17 | }); 18 | } 19 | } 20 | 21 | static get isConnected() { 22 | return !!MongoDB.connection; 23 | } 24 | 25 | static async connect() { 26 | try { 27 | if (!process.env.MONGO_URL) { 28 | throw new Error(`Environment variable MONGO_URL is missing.`); 29 | } 30 | 31 | MongoDB.connectionPromise = MongoClient.connect(process.env.MONGO_URL); 32 | MongoDB.connection = await MongoDB.connectionPromise; 33 | 34 | return Promise.resolve(new MongoDB()); 35 | } catch (err) { 36 | console.error('Problems with the connection to the database.'); 37 | return Promise.reject(err); 38 | } 39 | } 40 | 41 | collection(name) { 42 | if (!MongoDB.isConnected) { 43 | throw new Error( 44 | `collection '${name}' could not be accessed because MongoDB was not connected.` 45 | ); 46 | } 47 | 48 | return MongoDB.connection.collection(name); 49 | } 50 | 51 | static close() { 52 | return MongoDB.connection && MongoDB.connection.close(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /server/connectors/index.js: -------------------------------------------------------------------------------- 1 | import MongoDB from './MongoDB'; 2 | 3 | export default { 4 | MongoDB, 5 | }; 6 | -------------------------------------------------------------------------------- /server/fixtures.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | run(connection) { 3 | connection.then(connector => { 4 | connector.collection('authors').deleteMany({}).then(() => { 5 | connector.collection('authors').insertMany([ 6 | { _id: 1, firstName: 'Salvador', lastName: 'Dali' }, 7 | { _id: 2, firstName: 'Vince', lastName: 'Lombardi' }, 8 | ]); 9 | }); 10 | 11 | connector.collection('posts').deleteMany({}).then(() => { 12 | connector.collection('posts').insertMany([ 13 | { _id: 1, author: { _id: 1 }, title: 'Perfection fear', text: 'Have no fear of perfection, you’ll never reach it.', views: 20 }, 14 | { _id: 2, author: { _id: 2 }, title: 'Catch excellence', text: 'Perfection is not attainable, but if we chase perfection we can catch excellence.', views: 61 }, 15 | { _id: 3, author: { _id: 1 }, private: true, title: 'The Secret Life of Salvador Dalí', text: 'this is a private post!', views: 1 }, 16 | ]); 17 | }); 18 | }).catch(err => { 19 | console.error(err.stack || err); 20 | }); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import bodyParser from 'body-parser'; 3 | import { apolloExpress, graphiqlExpress } from 'apollo-server'; 4 | import { addResolveFunctionsToSchema } from 'graphql-tools'; 5 | import { generateResolvers } from 'graph-object'; 6 | import schema from '/domain/schemas'; 7 | import connectors from './connectors'; 8 | import * as models from './models'; 9 | 10 | // Initial fixtures 11 | require('./fixtures').run(connectors.MongoDB.connect()); 12 | 13 | const PORT = 8080; 14 | const app = express(); 15 | 16 | const resolvers = generateResolvers(schema, models); 17 | addResolveFunctionsToSchema(schema, resolvers); 18 | 19 | app.use('/graphql', bodyParser.json(), apolloExpress({ 20 | schema, 21 | resolvers, 22 | context: {}, 23 | formatError(error) { 24 | console.error(error.stack); 25 | return error; 26 | }, 27 | })); 28 | 29 | app.use( 30 | '/graphiql', 31 | graphiqlExpress({ 32 | endpointURL: '/graphql', 33 | }) 34 | ); 35 | 36 | app.listen(PORT, () => console.log( 37 | `GraphQL Server is now running on http://localhost:${PORT}/graphiql` 38 | )); 39 | 40 | process.on('exit', () => { 41 | console.log('Shutting down!'); 42 | 43 | for (const connector of Object.values(connectors)) { 44 | connector.close(); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /server/models.js: -------------------------------------------------------------------------------- 1 | import { injectConnectors } from 'graph-object'; 2 | import * as models from '/domain/models'; 3 | import connectors from './connectors'; 4 | 5 | const collections = { 6 | Author: 'authors', 7 | Post: 'posts', 8 | }; 9 | 10 | for (const name of Object.keys(models)) { 11 | const model = models[name]; 12 | 13 | if (!collections[name]) { 14 | continue; 15 | } 16 | 17 | injectConnectors(model, { 18 | connector: new connectors.MongoDB(), 19 | collection: collections[name], 20 | }); 21 | } 22 | 23 | module.exports = models; 24 | --------------------------------------------------------------------------------