├── .gitignore
├── .babelrc
├── babelRelayPlugin.js
├── README.md
├── data
└── quotes
├── public
└── index.html
├── webpack.config.js
├── js
├── search-form.js
├── thumbs-up-mutation.js
├── quote.js
└── app.js
├── package.json
├── index.js
└── schema
└── main.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | public/bundle.js
3 | public/bundle.js.map
4 | cache/schema.json
5 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "passPerPreset": true,
3 | "presets": [
4 | {"plugins": ["./babelRelayPlugin"]},
5 | "react",
6 | "es2015",
7 | "stage-0"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/babelRelayPlugin.js:
--------------------------------------------------------------------------------
1 | const babelRelayPlugin = require('babel-relay-plugin');
2 | const schema = require('./cache/schema.json');
3 |
4 | module.exports = babelRelayPlugin(schema.data);
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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Quotes
5 |
7 |
8 |
9 |
10 | Loading...
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
19 | )
20 | }
21 | }
22 |
23 | export default SearchForm;
24 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------