├── .babelrc ├── .gitignore ├── README.md ├── babelRelayPlugin.js ├── data └── quotes ├── index.js ├── js ├── app.js ├── quote.js ├── search-form.js └── thumbs-up-mutation.js ├── package.json ├── public └── index.html ├── schema └── main.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "passPerPreset": true, 3 | "presets": [ 4 | {"plugins": ["./babelRelayPlugin"]}, 5 | "react", 6 | "es2015", 7 | "stage-0" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/bundle.js 3 | public/bundle.js.map 4 | cache/schema.json 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Learning GraphQL and Relay 2 | 3 | A Packt Book: https://www.packtpub.com/web-development/learning-graphql-and-relay 4 | 5 | For questions: [slack.jscomplete.com](http://slack.jscomplete.com/) 6 | -------------------------------------------------------------------------------- /babelRelayPlugin.js: -------------------------------------------------------------------------------- 1 | const babelRelayPlugin = require('babel-relay-plugin'); 2 | const schema = require('./cache/schema.json'); 3 | 4 | module.exports = babelRelayPlugin(schema.data); 5 | -------------------------------------------------------------------------------- /data/quotes: -------------------------------------------------------------------------------- 1 | The best preparation for tomorrow is doing your best today. 2 | Life is 10 percent what happens to you and 90 percent how you react to it. 3 | If opportunity doesn't knock, build a door. 4 | Try to be a rainbow in someone's cloud. -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { MongoClient } = require('mongodb'); 4 | const assert = require('assert'); 5 | const { graphql } = require('graphql'); 6 | const { introspectionQuery } = require('graphql/utilities'); 7 | const graphqlHTTP = require('express-graphql'); 8 | const express = require('express'); 9 | 10 | const app = express(); 11 | app.use(express.static('public')); 12 | 13 | const mySchema = require('./schema/main'); 14 | const MONGO_URL = 'mongodb://localhost:27017/test'; 15 | 16 | MongoClient.connect(MONGO_URL, (err, db) => { 17 | assert.equal(null, err); 18 | console.log('Connected to MongoDB server'); 19 | 20 | app.use('/graphql', graphqlHTTP({ 21 | schema: mySchema, 22 | context: { db }, 23 | graphiql: true 24 | })); 25 | 26 | graphql(mySchema, introspectionQuery) 27 | .then(result => { 28 | fs.writeFileSync( 29 | path.join(__dirname, 'cache/schema.json'), 30 | JSON.stringify(result, null, 2) 31 | ); 32 | console.log('Generated cached schema.json file'); 33 | }) 34 | .catch(console.error); 35 | 36 | app.listen(3000, () => 37 | console.log('Running Express.js on port 3000') 38 | ); 39 | }); 40 | -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Relay from 'react-relay'; 4 | import { debounce } from 'lodash'; 5 | 6 | import SearchForm from './search-form'; 7 | import Quote from './quote'; 8 | 9 | class QuotesLibrary extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.search = debounce(this.search.bind(this), 300); 13 | } 14 | search(searchTerm) { 15 | this.props.relay.setVariables({searchTerm}); 16 | } 17 | render() { 18 | return ( 19 |
20 | 21 |
22 | {this.props.library.quotesConnection.edges.map(edge => 23 | 24 | )} 25 |
26 |
27 | ) 28 | } 29 | } 30 | 31 | QuotesLibrary = Relay.createContainer(QuotesLibrary, { 32 | initialVariables: { 33 | searchTerm: '' 34 | }, 35 | fragments: { 36 | library: () => Relay.QL ` 37 | fragment on QuotesLibrary { 38 | quotesConnection(first: 100, searchTerm: $searchTerm) { 39 | edges { 40 | node { 41 | id 42 | ${Quote.getFragment('quote')} 43 | } 44 | } 45 | } 46 | } 47 | ` 48 | } 49 | }); 50 | 51 | class AppRoute extends Relay.Route { 52 | static routeName = 'App'; 53 | static queries = { 54 | library: (Component) => Relay.QL ` 55 | query QuotesLibrary { 56 | quotesLibrary { 57 | ${Component.getFragment('library')} 58 | } 59 | } 60 | ` 61 | } 62 | } 63 | 64 | ReactDOM.render( 65 | , 69 | document.getElementById('react') 70 | ); 71 | -------------------------------------------------------------------------------- /js/quote.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | 4 | import ThumbsUpMutation from './thumbs-up-mutation'; 5 | 6 | class Quote extends React.Component { 7 | showLikes = () => { 8 | this.props.relay.setVariables({showLikes: true}); 9 | }; 10 | 11 | thumbsUpClick = () => { 12 | Relay.Store.commitUpdate( 13 | new ThumbsUpMutation({ 14 | quote: this.props.quote 15 | }) 16 | ) 17 | }; 18 | 19 | displayLikes() { 20 | if (!this.props.relay.variables.showLikes) { 21 | return null; 22 | } 23 | return ( 24 |
25 | {this.props.quote.likesCount}   26 | 28 |
29 | ); 30 | } 31 | 32 | render() { 33 | return ( 34 |
35 |

{this.props.quote.text}

36 |
{this.props.quote.author}
37 | {this.displayLikes()} 38 |
39 | ); 40 | } 41 | } 42 | 43 | Quote = Relay.createContainer(Quote, { 44 | initialVariables: { 45 | showLikes: false 46 | }, 47 | fragments: { 48 | quote: () => Relay.QL ` 49 | fragment OneQuote on Quote { 50 | ${ThumbsUpMutation.getFragment('quote')} 51 | text 52 | author 53 | likesCount @include(if: $showLikes) 54 | } 55 | ` 56 | } 57 | }); 58 | 59 | export default Quote; 60 | -------------------------------------------------------------------------------- /js/search-form.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class SearchForm extends React.Component { 4 | static propTypes = { 5 | searchAction: React.PropTypes.func.isRequired 6 | }; 7 | 8 | handleChange = event => { 9 | this.props.searchAction(event.target.value); 10 | }; 11 | 12 | render() { 13 | return ( 14 |
15 | 18 |
19 | ) 20 | } 21 | } 22 | 23 | export default SearchForm; 24 | -------------------------------------------------------------------------------- /js/thumbs-up-mutation.js: -------------------------------------------------------------------------------- 1 | import Relay from 'react-relay'; 2 | 3 | class ThumbsUpMutation extends Relay.Mutation { 4 | 5 | static fragments = { 6 | quote: () => Relay.QL ` 7 | fragment on Quote { 8 | id 9 | likesCount 10 | } 11 | ` 12 | }; 13 | 14 | getMutation() { 15 | return Relay.QL ` 16 | mutation { 17 | thumbsUp 18 | } 19 | `; 20 | } 21 | 22 | getVariables() { 23 | return { 24 | quoteId: this.props.quote.id 25 | }; 26 | } 27 | 28 | getFatQuery() { 29 | return Relay.QL ` 30 | fragment on ThumbsUpMutationPayload { 31 | quote { 32 | likesCount 33 | } 34 | } 35 | `; 36 | } 37 | 38 | getConfigs() { 39 | return [ 40 | { 41 | type: 'FIELDS_CHANGE', 42 | fieldIDs: { 43 | quote: this.props.quote.id 44 | } 45 | } 46 | ]; 47 | } 48 | 49 | getOptimisticResponse() { 50 | return { 51 | quote: { 52 | id: this.props.quote.id, 53 | likesCount: this.props.quote.likesCount + 1 54 | } 55 | }; 56 | } 57 | 58 | } 59 | 60 | export default ThumbsUpMutation; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-project", 3 | "version": "1.0.0", 4 | "description": "Learning GraphQL and Relay", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/edgecoders/learning-graphql-and-relay.git" 12 | }, 13 | "author": "Samer Buna", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/edgecoders/learning-graphql-and-relay/issues" 17 | }, 18 | "homepage": "https://github.com/edgecoders/learning-graphql-and-relay#readme", 19 | "dependencies": { 20 | "babel-loader": "^6.2.4", 21 | "babel-preset-es2015": "^6.13.2", 22 | "babel-preset-react": "^6.11.1", 23 | "babel-preset-stage-0": "^6.5.0", 24 | "babel-relay-plugin": "^0.9.2", 25 | "express": "^4.14.0", 26 | "express-graphql": "^0.5.3", 27 | "graphql": "^0.6.2", 28 | "graphql-relay": "^0.4.2", 29 | "lodash": "^4.14.2", 30 | "mongodb": "^2.2.5", 31 | "react": "^15.3.0", 32 | "react-dom": "^15.3.0", 33 | "react-relay": "^0.9.2", 34 | "webpack": "^1.13.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Quotes 5 | 7 | 8 | 9 |
10 | Loading... 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /schema/main.js: -------------------------------------------------------------------------------- 1 | const { 2 | GraphQLSchema, 3 | GraphQLObjectType, 4 | GraphQLString, 5 | GraphQLInt, 6 | GraphQLList, 7 | GraphQLBoolean, 8 | GraphQLEnumType 9 | } = require('graphql'); 10 | 11 | const { 12 | mutationWithClientMutationId, 13 | globalIdField, 14 | fromGlobalId, 15 | nodeDefinitions, 16 | connectionDefinitions, 17 | connectionArgs, 18 | connectionFromArray, 19 | connectionFromPromisedArray 20 | } = require('graphql-relay'); 21 | 22 | const { ObjectID } = require('mongodb'); 23 | 24 | const globalIdFetcher = (globalId, { db }) => { 25 | const { type, id } = fromGlobalId(globalId); 26 | switch (type) { 27 | case 'QuotesLibrary': 28 | // We only have one quote library 29 | return quotesLibrary; 30 | case 'Quote': 31 | return db.collection('quotes').findOne(ObjectID(id)); 32 | default: 33 | return null; 34 | } 35 | }; 36 | 37 | const globalTypeResolver = obj => obj.type || QuoteType; 38 | 39 | const { nodeInterface, nodeField } = nodeDefinitions( 40 | globalIdFetcher, 41 | globalTypeResolver 42 | ); 43 | 44 | const QuoteType = new GraphQLObjectType({ 45 | name: 'Quote', 46 | interfaces: [nodeInterface], 47 | fields: { 48 | id: globalIdField('Quote', obj => obj._id), 49 | text: { type: GraphQLString }, 50 | author: { type: GraphQLString }, 51 | likesCount: { 52 | type: GraphQLInt, 53 | resolve: obj => obj.likesCount || 0 54 | } 55 | } 56 | }); 57 | 58 | const { connectionType: QuotesConnectionType } = 59 | connectionDefinitions({ 60 | name: 'Quote', 61 | nodeType: QuoteType 62 | }); 63 | 64 | let connectionArgsWithSearch = connectionArgs; 65 | connectionArgsWithSearch.searchTerm = { type: GraphQLString }; 66 | 67 | const QuotesLibraryType = new GraphQLObjectType({ 68 | name: 'QuotesLibrary', 69 | interfaces: [nodeInterface], 70 | fields: { 71 | id: globalIdField('QuotesLibrary'), 72 | quotesConnection: { 73 | type: QuotesConnectionType, 74 | description: 'A list of the quotes in the database', 75 | args: connectionArgsWithSearch, 76 | resolve: (_, args, { db }) => { 77 | let findParams = {}; 78 | if (args.searchTerm) { 79 | findParams.text = new RegExp(args.searchTerm, 'i'); 80 | } 81 | return connectionFromPromisedArray( 82 | db.collection('quotes').find(findParams).toArray(), 83 | args 84 | ); 85 | } 86 | } 87 | } 88 | }); 89 | 90 | const quotesLibrary = { type: QuotesLibraryType }; 91 | 92 | const queryType = new GraphQLObjectType({ 93 | name: 'RootQuery', 94 | fields: { 95 | node: nodeField, 96 | quotesLibrary: { 97 | type: QuotesLibraryType, 98 | description: 'The Quotes Library', 99 | resolve: () => quotesLibrary 100 | } 101 | } 102 | }); 103 | 104 | const thumbsUpMutation = mutationWithClientMutationId({ 105 | name: 'ThumbsUpMutation', 106 | inputFields: { 107 | quoteId: { type: GraphQLString } 108 | }, 109 | outputFields: { 110 | quote: { 111 | type: QuoteType, 112 | resolve: obj => obj 113 | } 114 | }, 115 | mutateAndGetPayload: (params, { db }) => { 116 | const { id } = fromGlobalId(params.quoteId); 117 | return Promise.resolve( 118 | db.collection('quotes').updateOne( 119 | { _id: ObjectID(id) }, 120 | { $inc: { likesCount: 1 } } 121 | ) 122 | ).then(result => 123 | db.collection('quotes').findOne(ObjectID(id))); 124 | } 125 | }); 126 | 127 | const mutationType = new GraphQLObjectType({ 128 | name: 'RootMutation', 129 | fields: { 130 | thumbsUp: thumbsUpMutation 131 | } 132 | }); 133 | 134 | const mySchema = new GraphQLSchema({ 135 | query: queryType, 136 | mutation: mutationType 137 | }); 138 | 139 | module.exports = mySchema; 140 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './js/app.js', 5 | output: { 6 | path: path.join(__dirname, 'public'), 7 | filename: 'bundle.js' 8 | }, 9 | module: { 10 | loaders: [ 11 | { 12 | test: /\.js$/, 13 | exclude: /node_modules/, 14 | loader: 'babel-loader' 15 | } 16 | ] 17 | } 18 | }; 19 | --------------------------------------------------------------------------------