├── .babelrc ├── .eslintrc ├── .gitignore ├── README.md ├── bin ├── start.js └── sync.js ├── package.json ├── pm2.json └── src ├── config.js ├── graphql ├── mutations │ ├── createUser.js │ ├── followUser.js │ ├── index.js │ └── unfollowUser.js ├── nodeDefinitions.js ├── queries │ └── index.js ├── schema.js └── types │ ├── globalIdType.js │ └── userType.js ├── helpers ├── connection.js └── invariant.js ├── index.js ├── setup.js └── tables ├── env.js ├── userFollowTable.js └── userTable.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["add-module-exports"], 3 | "presets": ["es2015", "stage-0"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb/base", 4 | "env": { 5 | "node": true, 6 | "mocha": true, 7 | }, 8 | "rules": { 9 | "id-length": 0, 10 | "func-names": 0, 11 | "no-use-before-define": 0, 12 | "no-unused-expressions": 0, 13 | "space-before-function-paren": 0, 14 | "brace-style": 0, 15 | "max-len": 0, 16 | "newline-per-chained-call": 0, 17 | "no-underscore-dangle": 0, 18 | "arrow-body-style": 0, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | rethinkdb_data 3 | .*.swp 4 | *.log 5 | .idea 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL-Relay-Rethinkdb-Example 2 | 3 | Example Implementation of [GraphQL](http://facebook.github.io/graphql/) & [Relay](https://facebook.github.io/relay/) server. 4 | 5 | Used libraries: 6 | 7 | - [graphql-js](https://github.com/graphql/graphql-js) 8 | - [graphql-relay-js](https://github.com/graphql/graphql-relay-js) 9 | - [express](https://github.com/expressjs/express) 10 | - [express-graphql](https://github.com/graphql/express-graphql) 11 | - [rethinkdb](https://github.com/rethinkdb/rethinkdb) 12 | - [rethinkdb-pool](https://github.com/hden/rethinkdb-pool) 13 | - [nothinkdb](https://github.com/ediket/nothinkdb) 14 | - [nothinkdb-graphql](https://github.com/ediket/nothinkdb-graphql) 15 | 16 | # Setup 17 | 18 | ``` 19 | git clone https://github.com/ediket/graphql-relay-rethinkdb-example 20 | cd graphql-relay-rethinkdb-example 21 | npm install 22 | ``` 23 | 24 | # Usage 25 | 26 | 1. Run rethinkdb server 27 | 28 | ``` 29 | rethinkdb 30 | ``` 31 | 32 | 1. Sync rethinkdb schema (create db, table, index) 33 | 34 | ``` 35 | npm run sync 36 | ``` 37 | 38 | 1. Run server 39 | 40 | ``` 41 | npm run start && npm run logs 42 | ``` 43 | 44 | 1. Query & Mutation with [http://0.0.0.0:7777/graphql](http://0.0.0.0:7777/graphql) 45 | 46 | 1. Delete server 47 | 48 | ``` 49 | npm run delete # or npm run stop 50 | ``` 51 | 52 | # Commands 53 | 54 | Run server 55 | 56 | ``` 57 | npm run start 58 | ``` 59 | 60 | Stop server 61 | 62 | ``` 63 | npm run stop 64 | ``` 65 | 66 | Delete server 67 | 68 | ``` 69 | npm run stop 70 | ``` 71 | 72 | View server log 73 | 74 | ``` 75 | npm run logs 76 | ``` 77 | 78 | # Quries 79 | 80 | ```graphql 81 | query UserConnection { 82 | users(first: 2) { 83 | edges { 84 | node { 85 | id 86 | name 87 | } 88 | } 89 | } 90 | } 91 | ``` 92 | 93 | # Mutations 94 | 95 | ```graphql 96 | mutation CreateUser { 97 | createUser(input: { 98 | clientMutationId: "1", 99 | name: "SOME_USER_NAME" 100 | }) { 101 | user { 102 | id 103 | name 104 | } 105 | } 106 | } 107 | ``` 108 | 109 | ```graphql 110 | mutation FollowUser { 111 | followUser(input: { 112 | clientMutationId: "1", 113 | followerId: "FOLLOWER_ID", 114 | followeeId: "FOLLOWEE_ID" 115 | }) { 116 | follower { 117 | followers(first: 10) { 118 | edges { 119 | node { 120 | id 121 | name 122 | } 123 | } 124 | } 125 | } 126 | followee { 127 | following(first: 10) { 128 | edges { 129 | node { 130 | id 131 | name 132 | } 133 | } 134 | } 135 | } 136 | } 137 | } 138 | ``` 139 | 140 | ```graphql 141 | mutation UnfollowUser { 142 | unfollowUser(input: { 143 | clientMutationId: "1", 144 | followerId: "FOLLOWER_ID", 145 | followeeId: "FOLLOWEE_ID" 146 | }) { 147 | follower { 148 | followers(first: 10) { 149 | edges { 150 | node { 151 | id 152 | name 153 | } 154 | } 155 | } 156 | } 157 | followee { 158 | following(first: 10) { 159 | edges { 160 | node { 161 | id 162 | name 163 | } 164 | } 165 | } 166 | } 167 | } 168 | } 169 | ``` 170 | -------------------------------------------------------------------------------- /bin/start.js: -------------------------------------------------------------------------------- 1 | /* eslint no-var: 0, vars-on-top: 0, no-console: 0 */ 2 | require('babel-polyfill'); 3 | require('babel-register')(); 4 | 5 | var server = require('../'); 6 | var PORT = 7777; 7 | var HOST = '0.0.0.0'; 8 | 9 | server.listen(PORT, HOST, () => { 10 | console.log(`GraphQL API Server Start http://${HOST}:${PORT}`); 11 | }); 12 | -------------------------------------------------------------------------------- /bin/sync.js: -------------------------------------------------------------------------------- 1 | import r from 'rethinkdb'; 2 | import { RETHINKDB } from '../src/config'; 3 | import { getConnection } from '../src/helpers/connection'; 4 | import env from '../src/tables/env'; 5 | import '../src/tables/userTable'; 6 | import '../src/tables/userFollowTable'; 7 | 8 | async function syncDB() { 9 | const connection = await getConnection({ db: null }); 10 | r.branch( 11 | r.dbList().contains(RETHINKDB.DB).not(), 12 | r.dbCreate(RETHINKDB.DB), 13 | null 14 | ).run(connection); 15 | await connection.close(); 16 | } 17 | 18 | async function syncTables() { 19 | const connection = await getConnection(); 20 | await env.sync(connection); 21 | await connection.close(); 22 | } 23 | 24 | syncDB() 25 | .then(() => syncTables()) 26 | .catch(e => { 27 | console.log(e.stack); 28 | }) 29 | .then(() => process.exit()); 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-relay-rethinkdb-example", 3 | "version": "0.0.0", 4 | "description": "Example implementation of GraphQL & Relay server", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/ediket/graphql-relay-rethinkdb-example.git" 8 | }, 9 | "keywords": [ 10 | "graphql", 11 | "relay", 12 | "nothinkdb", 13 | "nothinkdb-graphql" 14 | ], 15 | "author": "ironhee ", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/ediket/graphql-relay-rethinkdb-example/issues" 19 | }, 20 | "homepage": "https://github.com/ediket/graphql-relay-rethinkdb-example#readme", 21 | "main": "src", 22 | "scripts": { 23 | "start": "pm2 start pm2.json", 24 | "stop": "pm2 stop pm2.json", 25 | "restart": "pm2 restart pm2.json", 26 | "delete": "pm2 delete pm2.json", 27 | "logs": "pm2 logs graphql-relay-rethinkdb-example", 28 | "sync": "babel-node bin/sync" 29 | }, 30 | "devDependencies": { 31 | "babel-cli": "^6.9.0", 32 | "babel-eslint": "^6.0.4", 33 | "babel-plugin-add-module-exports": "^0.2.1", 34 | "babel-polyfill": "^6.9.0", 35 | "babel-preset-es2015": "^6.9.0", 36 | "babel-preset-stage-0": "^6.5.0", 37 | "babel-register": "^6.9.0", 38 | "chai": "^3.4.1", 39 | "eslint": "^2.10.2", 40 | "eslint-config-airbnb": "^9.0.1", 41 | "eslint-plugin-import": "^1.8.0", 42 | "pm2": "^1.1.3", 43 | "rimraf": "^2.5.1" 44 | }, 45 | "dependencies": { 46 | "express": "^4.13.4", 47 | "express-graphql": "^0.5.1", 48 | "graphql": "^0.4.17", 49 | "graphql-relay": "^0.3.6", 50 | "joi": "^8.0.1", 51 | "lodash": "^4.4.0", 52 | "nothinkdb": "^0.5.29", 53 | "nothinkdb-graphql": "^0.1.24", 54 | "rethinkdb": "2.2.3", 55 | "rethinkdb-pool": "^1.1.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pm2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-relay-rethinkdb-example", 3 | "script": "./bin/start.js", 4 | "watch": ["lib", "bin"], 5 | "ignore_watch": [ 6 | ".git", 7 | "node_modules", 8 | "rethinkdb_data" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const CONFIG = { 2 | PORT: 5555, 3 | HOST: '0.0.0.0', 4 | RETHINKDB: { 5 | HOST: '0.0.0.0', 6 | PORT: 28015, 7 | DB: 'example', 8 | TIMEOUT: 10, 9 | AUTHKEY: null, 10 | }, 11 | }; 12 | 13 | export default CONFIG; 14 | -------------------------------------------------------------------------------- /src/graphql/mutations/createUser.js: -------------------------------------------------------------------------------- 1 | import { GraphQLNonNull, GraphQLString } from 'graphql'; 2 | import { mutationWithClientMutationId } from 'graphql-relay'; 3 | import userTable from '../../tables/userTable'; 4 | import userType from '../types/userType'; 5 | import { pool } from '../../helpers/connection'; 6 | 7 | export default mutationWithClientMutationId({ 8 | name: 'CreateUser', 9 | outputFields: { 10 | user: { type: userType }, 11 | }, 12 | inputFields: { 13 | name: { type: new GraphQLNonNull(GraphQLString) }, 14 | }, 15 | async mutateAndGetPayload(args) { 16 | const { name } = args; 17 | 18 | const user = userTable.create({ name }); 19 | await pool.run(userTable.insert(user)); 20 | 21 | return { user }; 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /src/graphql/mutations/followUser.js: -------------------------------------------------------------------------------- 1 | import { GraphQLNonNull } from 'graphql'; 2 | import { mutationWithClientMutationId } from 'graphql-relay'; 3 | import userTable from '../../tables/userTable'; 4 | import userType from '../types/userType'; 5 | import globalIdType from '../types/globalIdType'; 6 | import { pool } from '../../helpers/connection'; 7 | 8 | export default mutationWithClientMutationId({ 9 | name: 'FollowUser', 10 | outputFields: { 11 | follower: { type: userType }, 12 | followee: { type: userType }, 13 | }, 14 | inputFields: { 15 | followerId: { type: new GraphQLNonNull(globalIdType) }, 16 | followeeId: { type: new GraphQLNonNull(globalIdType) }, 17 | }, 18 | async mutateAndGetPayload(args) { 19 | const { followeeId, followerId } = args; 20 | 21 | await pool.run(userTable.createRelation('following', followeeId, followerId)); 22 | 23 | return { 24 | follower: await pool.run(userTable.get(followerId)), 25 | followee: await pool.run(userTable.get(followeeId)), 26 | }; 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /src/graphql/mutations/index.js: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from 'graphql'; 2 | import createUser from './createUser'; 3 | import followUser from './followUser'; 4 | import unfollowUser from './unfollowUser'; 5 | 6 | export default new GraphQLObjectType({ 7 | name: 'Mutation', 8 | fields: () => ({ 9 | createUser, 10 | followUser, 11 | unfollowUser, 12 | }), 13 | }); 14 | -------------------------------------------------------------------------------- /src/graphql/mutations/unfollowUser.js: -------------------------------------------------------------------------------- 1 | import { GraphQLNonNull } from 'graphql'; 2 | import { mutationWithClientMutationId } from 'graphql-relay'; 3 | import userTable from '../../tables/userTable'; 4 | import userType from '../types/userType'; 5 | import globalIdType from '../types/globalIdType'; 6 | import { pool } from '../../helpers/connection'; 7 | 8 | export default mutationWithClientMutationId({ 9 | name: 'UnfollowUser', 10 | outputFields: { 11 | follower: { type: userType }, 12 | followee: { type: userType }, 13 | }, 14 | inputFields: { 15 | followerId: { type: new GraphQLNonNull(globalIdType) }, 16 | followeeId: { type: new GraphQLNonNull(globalIdType) }, 17 | }, 18 | async mutateAndGetPayload(args) { 19 | const { followeeId, followerId } = args; 20 | 21 | await pool.run(userTable.removeRelation('following', followeeId, followerId)); 22 | 23 | return { 24 | follower: await pool.run(userTable.get(followerId)), 25 | followee: await pool.run(userTable.get(followeeId)), 26 | }; 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /src/graphql/nodeDefinitions.js: -------------------------------------------------------------------------------- 1 | import { 2 | nodeDefinitions, 3 | } from 'nothinkdb-graphql'; 4 | import { pool } from '../helpers/connection'; 5 | 6 | export const { 7 | nodeField, 8 | nodeInterface, 9 | registerType, 10 | } = nodeDefinitions({ 11 | runQuery: query => pool.run(query), 12 | }); 13 | -------------------------------------------------------------------------------- /src/graphql/queries/index.js: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from 'graphql'; 2 | import { nodeField } from '../nodeDefinitions'; 3 | import { userConnectionField } from '../types/userType'; 4 | 5 | const queryType = new GraphQLObjectType({ 6 | name: 'Query', 7 | fields: () => ({ 8 | node: nodeField, 9 | users: userConnectionField, 10 | }), 11 | }); 12 | 13 | export default queryType; 14 | -------------------------------------------------------------------------------- /src/graphql/schema.js: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql'; 2 | import query from './queries'; 3 | import mutation from './mutations'; 4 | 5 | const schema = new GraphQLSchema({ 6 | query, 7 | mutation, 8 | }); 9 | 10 | export default schema; 11 | -------------------------------------------------------------------------------- /src/graphql/types/globalIdType.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLScalarType, 3 | } from 'graphql'; 4 | import { Kind } from 'graphql/language'; 5 | import { fromGlobalId } from 'graphql-relay'; 6 | import _ from 'lodash'; 7 | import invariant from '../../helpers/invariant'; 8 | 9 | function coreceType(value) { 10 | const { id } = fromGlobalId(value); 11 | invariant(id, `Query error: invalid globalId type: ${[value]}`); 12 | return id; 13 | } 14 | 15 | // refer: https://github.com/graphql/graphql-js/blob/master/src/type/scalars.js 16 | export default new GraphQLScalarType({ 17 | name: 'GlobalIdType', 18 | serialize: () => null, // GlobalIdType is only InputType 19 | parseValue: value => { 20 | if (_.isString(value)) { 21 | return coreceType(value); 22 | } 23 | return null; 24 | }, 25 | parseLiteral: ast => { 26 | if (ast.kind === Kind.STRING) { 27 | return coreceType(ast.value); 28 | } 29 | return null; 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /src/graphql/types/userType.js: -------------------------------------------------------------------------------- 1 | import { 2 | getGraphQLFieldsFromTable, 3 | fieldFromConnectionType, 4 | } from 'nothinkdb-graphql'; 5 | import { 6 | GraphQLObjectType, 7 | } from 'graphql'; 8 | import { 9 | connectionDefinitions, 10 | } from 'graphql-relay'; 11 | import r from 'rethinkdb'; 12 | import { pool } from '../../helpers/connection'; 13 | import userTable from '../../tables/userTable'; 14 | import { 15 | nodeInterface, 16 | registerType, 17 | } from '../nodeDefinitions'; 18 | 19 | export const userFields = getGraphQLFieldsFromTable(userTable); 20 | 21 | const userType = new GraphQLObjectType({ 22 | name: 'Foo', 23 | interfaces: [nodeInterface], 24 | fields: () => ({ 25 | ...userFields, 26 | following: fieldFromConnectionType({ 27 | connectionType: userConnectionType, 28 | table: userTable, 29 | graphQLType: userType, 30 | getQuery: user => userTable 31 | .queryRelated(user.id, 'following') 32 | .orderBy(r.desc('createdAt')), 33 | runQuery: query => pool.run(query), 34 | }), 35 | followers: fieldFromConnectionType({ 36 | connectionType: userConnectionType, 37 | table: userTable, 38 | graphQLType: userType, 39 | getQuery: user => userTable 40 | .queryRelated(user.id, 'followers') 41 | .orderBy(r.desc('createdAt')), 42 | runQuery: query => pool.run(query), 43 | }), 44 | }), 45 | }); 46 | 47 | registerType({ 48 | table: userTable, 49 | type: userType, 50 | }); 51 | 52 | export const { 53 | connectionType: userConnectionType, 54 | edgeType: userEdgeType, 55 | } = connectionDefinitions({ nodeType: userType }); 56 | 57 | export const userConnectionField = fieldFromConnectionType({ 58 | connectionType: userConnectionType, 59 | table: userTable, 60 | graphQLType: userType, 61 | getQuery: () => userTable.query().orderBy({ index: r.desc('createdAt') }), 62 | runQuery: query => pool.run(query), 63 | }); 64 | 65 | export default userType; 66 | -------------------------------------------------------------------------------- /src/helpers/connection.js: -------------------------------------------------------------------------------- 1 | import r from 'rethinkdb'; 2 | import Pool from 'rethinkdb-pool'; 3 | import { promisifyAll } from 'bluebird'; 4 | import { RETHINKDB } from '../config'; 5 | 6 | export function getConnection(options = {}) { 7 | return r.connect({ 8 | db: RETHINKDB.DB, 9 | host: RETHINKDB.HOST, 10 | port: RETHINKDB.PORT, 11 | authKey: RETHINKDB.AUTHKEY, 12 | timeout: RETHINKDB.TIMEOUT, 13 | ...options, 14 | }); 15 | } 16 | 17 | export const pool = promisifyAll( 18 | new Pool(r, { 19 | db: RETHINKDB.DB, 20 | host: RETHINKDB.HOST, 21 | port: RETHINKDB.PORT, 22 | authKey: RETHINKDB.AUTHKEY, 23 | timeout: RETHINKDB.TIMEOUT, 24 | }) 25 | ); 26 | -------------------------------------------------------------------------------- /src/helpers/invariant.js: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql'; 2 | 3 | export default function invariant(value, message) { 4 | if (!value) { 5 | throw new GraphQLError(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import graphqlHTTP from 'express-graphql'; 3 | import schema from './graphql/schema'; 4 | 5 | const server = express(); 6 | 7 | server.use('/graphql', 8 | graphqlHTTP({ 9 | schema, 10 | pretty: true, 11 | graphiql: true, 12 | }) 13 | ); 14 | 15 | export default server; 16 | -------------------------------------------------------------------------------- /src/setup.js: -------------------------------------------------------------------------------- 1 | import env from './tables/env'; 2 | 3 | env.init(); 4 | -------------------------------------------------------------------------------- /src/tables/env.js: -------------------------------------------------------------------------------- 1 | import { Environment, Table, schema } from 'nothinkdb'; 2 | 3 | class BaseTable extends Table { 4 | constructor(options) { 5 | super({ 6 | ...options, 7 | schema: () => ({ 8 | id: schema.id, 9 | updatedAt: schema.updatedAt, 10 | createdAt: schema.createdAt, 11 | ...options.schema(), 12 | }), 13 | }); 14 | } 15 | } 16 | 17 | const env = new Environment({ 18 | Table: BaseTable, 19 | }); 20 | 21 | export default env; 22 | -------------------------------------------------------------------------------- /src/tables/userFollowTable.js: -------------------------------------------------------------------------------- 1 | import r from 'rethinkdb'; 2 | import env from './env'; 3 | import userTable from './userTable'; 4 | 5 | const followingTable = env.createTable({ 6 | tableName: 'UserFollow', 7 | schema: () => ({ 8 | followerId: userTable.getForeignKey({ isManyToMany: true }), 9 | followeeId: userTable.getForeignKey({ isManyToMany: true }), 10 | }), 11 | index: { 12 | following: [r.row('followerId'), r.row('followeeId')], 13 | followers: [r.row('followeeId'), r.row('followerId')], 14 | }, 15 | }); 16 | 17 | export default followingTable; 18 | -------------------------------------------------------------------------------- /src/tables/userTable.js: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { belongsToMany } from 'nothinkdb'; 3 | import env from './env'; 4 | import userFollowTable from './userFollowTable'; 5 | 6 | const userTable = env.createTable({ 7 | tableName: 'Foo', 8 | schema: () => ({ 9 | name: Joi.string(), 10 | }), 11 | relations: () => ({ 12 | following: belongsToMany([ 13 | userTable.linkedBy(userFollowTable, 'followerId'), 14 | userFollowTable.linkTo(userTable, 'followeeId'), 15 | ], { index: 'following' }), 16 | followers: belongsToMany([ 17 | userTable.linkedBy(userFollowTable, 'followeeId'), 18 | userFollowTable.linkTo(userTable, 'followerId'), 19 | ], { index: 'followers' }), 20 | }), 21 | }); 22 | 23 | export default userTable; 24 | --------------------------------------------------------------------------------