├── .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 |
--------------------------------------------------------------------------------