├── .gitignore ├── README.md ├── babelRelayPlugin.js ├── data └── schema.js ├── js ├── app.js ├── components │ ├── Link.js │ └── Main.js └── mutations │ └── CreateLinkMutation.js ├── package.json ├── public ├── favicon.ico └── index.html ├── server.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bundle.js* 3 | data/schema.json 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## React GraphQL Relay Example 2 | 3 | A Relay.js example project backed by a GraphQL server on Express.js with data stored in MongoDB 4 | 5 | Contributions are welcome! 6 | 7 | ## Installing 8 | 9 | ``` 10 | git clone https://github.com/RGRjs/react-graphql-relay-example.git rgrjs 11 | cd rgrjs 12 | npm install 13 | export MONGO_URL=.... 14 | npm start 15 | npm run build 16 | ``` 17 | -------------------------------------------------------------------------------- /babelRelayPlugin.js: -------------------------------------------------------------------------------- 1 | var getBabelRelayPlugin = require('babel-relay-plugin'); 2 | 3 | var schemaData = require('./data/schema.json').data; 4 | 5 | module.exports = getBabelRelayPlugin(schemaData); 6 | -------------------------------------------------------------------------------- /data/schema.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLSchema, 3 | GraphQLObjectType, 4 | GraphQLList, 5 | GraphQLInt, 6 | GraphQLString, 7 | GraphQLNonNull, 8 | GraphQLID 9 | } from 'graphql'; 10 | 11 | import { 12 | globalIdField, 13 | fromGlobalId, 14 | nodeDefinitions, 15 | connectionDefinitions, 16 | connectionArgs, 17 | connectionFromPromisedArray, 18 | mutationWithClientMutationId 19 | } from "graphql-relay"; 20 | 21 | let Schema = (db) => { 22 | class Store {} 23 | let store = new Store(); 24 | 25 | let nodeDefs = nodeDefinitions( 26 | (globalId) => { 27 | let {type} = fromGlobalId(globalId); 28 | if (type === 'Store') { 29 | return store 30 | } 31 | return null; 32 | }, 33 | (obj) => { 34 | if (obj instanceof Store) { 35 | return storeType; 36 | } 37 | return null; 38 | } 39 | ); 40 | 41 | let storeType = new GraphQLObjectType({ 42 | name: 'Store', 43 | fields: () => ({ 44 | id: globalIdField("Store"), 45 | linkConnection: { 46 | type: linkConnection.connectionType, 47 | args: { 48 | ...connectionArgs, 49 | query: { type: GraphQLString } 50 | }, 51 | resolve: (_, args) => { 52 | let findParams = {}; 53 | if (args.query) { 54 | findParams.title = new RegExp(args.query, 'i'); 55 | } 56 | if (!args.limit || args.limit > 200) { 57 | args.limit = 100 58 | } 59 | return connectionFromPromisedArray( 60 | db.collection("links") 61 | .find(findParams) 62 | .sort({createdAt: -1}) 63 | .limit(args.first).toArray(), 64 | args 65 | ); 66 | } 67 | } 68 | }), 69 | interfaces: [nodeDefs.nodeInterface] 70 | }); 71 | 72 | let linkType = new GraphQLObjectType({ 73 | name: 'Link', 74 | fields: () => ({ 75 | id: { 76 | type: new GraphQLNonNull(GraphQLID), 77 | resolve: (obj) => obj._id 78 | }, 79 | title: { type: GraphQLString }, 80 | url: { type: GraphQLString }, 81 | createdAt: { 82 | type: GraphQLString, 83 | resolve: (obj) => new Date(obj.createdAt).toISOString() 84 | } 85 | }) 86 | }); 87 | 88 | let linkConnection = connectionDefinitions({ 89 | name: 'Link', 90 | nodeType: linkType 91 | }); 92 | 93 | let createLinkMutation = mutationWithClientMutationId({ 94 | name: 'CreateLink', 95 | 96 | inputFields: { 97 | title: { type: new GraphQLNonNull(GraphQLString) }, 98 | url: { type: new GraphQLNonNull(GraphQLString) }, 99 | }, 100 | 101 | outputFields: { 102 | linkEdge: { 103 | type: linkConnection.edgeType, 104 | resolve: (obj) => ({ node: obj.ops[0], cursor: obj.insertedId }) 105 | }, 106 | store: { 107 | type: storeType, 108 | resolve: () => store 109 | } 110 | }, 111 | 112 | mutateAndGetPayload: ({title, url}) => { 113 | return db.collection("links").insertOne({ 114 | title, 115 | url, 116 | createdAt: Date.now() 117 | }); 118 | } 119 | }); 120 | 121 | let schema = new GraphQLSchema({ 122 | query: new GraphQLObjectType({ 123 | name: 'Query', 124 | fields: () => ({ 125 | node: nodeDefs.nodeField, 126 | store: { 127 | type: storeType, 128 | resolve: () => store 129 | } 130 | }) 131 | }), 132 | 133 | mutation: new GraphQLObjectType({ 134 | name: 'Mutation', 135 | fields: () => ({ 136 | createLink: createLinkMutation 137 | }) 138 | }) 139 | }); 140 | 141 | return schema 142 | }; 143 | 144 | export default Schema; 145 | -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import Relay from "react-relay"; 4 | 5 | import Main from "./components/Main"; 6 | 7 | class HomeRoute extends Relay.Route { 8 | static routeName = 'Home'; 9 | static queries = { 10 | store: (Component) => Relay.QL` 11 | query MainQuery { 12 | store { ${Component.getFragment('store') } } 13 | } 14 | ` 15 | }; 16 | } 17 | 18 | ReactDOM.render( 19 | , 23 | document.getElementById('react') 24 | ); 25 | -------------------------------------------------------------------------------- /js/components/Link.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Relay from "react-relay"; 3 | import moment from "moment"; 4 | 5 | class Link extends React.Component { 6 | dateStyle = () => ({ 7 | color: '#888', 8 | fontSize: '0.7em', 9 | marginRight: '0.5em' 10 | }); 11 | 12 | urlStyle = () => ({ 13 | color: '#062', 14 | fontSize: '0.85em' 15 | }); 16 | 17 | dateLabel = () => { 18 | let {link, relay} = this.props; 19 | if (relay.hasOptimisticUpdate(link)) { 20 | return 'Saving...'; 21 | } 22 | return moment(link.createdAt).format('L') 23 | }; 24 | 25 | url = () => { 26 | return this.props.link.url.replace(/^https?:\/\/|\/$/ig,''); 27 | }; 28 | 29 | render() { 30 | let {link} = this.props; 31 | return ( 32 |
  • 33 |
    34 | {link.title} 35 |
    36 | 37 | {this.dateLabel()} 38 | 39 | 40 | {this.url()} 41 | 42 |
    43 |
    44 |
  • 45 | ); 46 | } 47 | } 48 | 49 | Link = Relay.createContainer(Link, { 50 | fragments: { 51 | link: () => Relay.QL` 52 | fragment on Link { 53 | url, 54 | title, 55 | createdAt, 56 | } 57 | ` 58 | } 59 | }); 60 | 61 | export default Link; 62 | -------------------------------------------------------------------------------- /js/components/Main.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Relay from "react-relay"; 3 | import {debounce} from "lodash"; 4 | 5 | import Link from "./Link"; 6 | import CreateLinkMutation from "../mutations/CreateLinkMutation"; 7 | 8 | class Main extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.setVariables = debounce(this.props.relay.setVariables, 300); 12 | } 13 | 14 | search = (e) => { 15 | this.setVariables({ query: e.target.value }); 16 | }; 17 | 18 | setLimit = (e) => { 19 | this.setVariables({ limit: Number(e.target.value) }); 20 | }; 21 | 22 | handleSubmit = (e) => { 23 | e.preventDefault(); 24 | let onSuccess = () => { 25 | $('#modal').closeModal(); 26 | }; 27 | let onFailure = (transaction) => { 28 | var error = transaction.getError() || new Error('Mutation failed.'); 29 | console.error(error); 30 | }; 31 | Relay.Store.commitUpdate( 32 | new CreateLinkMutation({ 33 | title: this.refs.newTitle.value, 34 | url: this.refs.newUrl.value, 35 | store: this.props.store 36 | }), 37 | {onFailure, onSuccess} 38 | ); 39 | this.refs.newTitle.value = ""; 40 | this.refs.newUrl.value = ""; 41 | }; 42 | 43 | componentDidMount() { 44 | $('.modal-trigger').leanModal(); 45 | } 46 | 47 | render() { 48 | let content = this.props.store.linkConnection.edges.map(edge => { 49 | return ; 50 | }); 51 | return ( 52 |
    53 |
    54 | 55 | 56 |
    57 | 58 |
    59 | Add New Resource 60 |
    61 | 62 | 65 | 66 |
    67 |
    68 |
    69 | @jsComplete 70 |
    71 |
    72 |
    73 |
    74 | 79 |
    80 |
    81 |
    82 | 83 | 104 | 105 |
    106 | ); 107 | } 108 | } 109 | 110 | // Declare the data requirement for this component 111 | Main = Relay.createContainer(Main, { 112 | initialVariables: { 113 | limit: 100, 114 | query: '' 115 | }, 116 | fragments: { 117 | store: () => Relay.QL` 118 | fragment on Store { 119 | id, 120 | linkConnection(first: $limit, query: $query) { 121 | edges { 122 | node { 123 | id, 124 | ${Link.getFragment('link')} 125 | } 126 | } 127 | } 128 | } 129 | ` 130 | } 131 | }); 132 | 133 | export default Main; 134 | -------------------------------------------------------------------------------- /js/mutations/CreateLinkMutation.js: -------------------------------------------------------------------------------- 1 | import Relay from "react-relay"; 2 | 3 | class CreateLinkMutation extends Relay.Mutation { 4 | getMutation() { 5 | return Relay.QL` 6 | mutation { createLink } 7 | `; 8 | } 9 | 10 | getVariables() { 11 | return { 12 | title: this.props.title, 13 | url: this.props.url 14 | } 15 | } 16 | 17 | getFatQuery() { 18 | return Relay.QL` 19 | fragment on CreateLinkPayload { 20 | linkEdge, 21 | store { linkConnection } 22 | } 23 | `; 24 | } 25 | 26 | getConfigs() { 27 | return [{ 28 | type: 'RANGE_ADD', 29 | parentName: 'store', 30 | parentID: this.props.store.id, 31 | connectionName: 'linkConnection', 32 | edgeName: 'linkEdge', 33 | rangeBehaviors: { 34 | '': 'prepend', 35 | }, 36 | }] 37 | } 38 | 39 | getOptimisticResponse() { 40 | return { 41 | linkEdge: { 42 | node: { 43 | title: this.props.title, 44 | url: this.props.url, 45 | } 46 | } 47 | } 48 | } 49 | } 50 | 51 | export default CreateLinkMutation; 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rgrjs", 3 | "version": "1.0.0", 4 | "description": "A collection of educational resources about React, GraphQL, and Relay", 5 | "scripts": { 6 | "start": "./node_modules/.bin/babel-node --presets react,es2015,stage-0 server.js", 7 | "build": "webpack" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/rgrjs/react-graphql-relay-example.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/rgrjs/react-graphql-relay-example/issues" 17 | }, 18 | "homepage": "https://github.com/rgrjs/react-graphql-relay-example#readme", 19 | "dependencies": { 20 | "babel": "^6.5.2", 21 | "babel-cli": "^6.5.1", 22 | "babel-core": "^6.5.2", 23 | "babel-loader": "^6.2.3", 24 | "babel-preset-es2015": "^6.5.0", 25 | "babel-preset-react": "^6.5.0", 26 | "babel-preset-stage-0": "^6.5.0", 27 | "babel-relay-plugin": "^0.7.1", 28 | "events": "^1.1.0", 29 | "express": "^4.13.4", 30 | "express-graphql": "^0.4.9", 31 | "flux": "^2.1.1", 32 | "graphql": "^0.4.18", 33 | "graphql-relay": "^0.3.6", 34 | "kerberos": "0.0.18", 35 | "lodash": "^4.5.0", 36 | "moment": "^2.11.2", 37 | "mongodb": "^2.1.7", 38 | "react": "^0.14.7", 39 | "react-dom": "^0.14.7", 40 | "react-relay": "^0.7.1", 41 | "webpack": "^1.12.13" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RGRjs/react-graphql-relay-example/4eff361c58048f023081de4ac60ec3c1544bd2e6/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Resources for React.js, GraphQL, and Relay 11 | 12 | 13 |
    14 |

    15 | React, GraphQL, and Relay 16 |

    17 |
    18 | Loading... 19 |
    20 | 25 | Fork me on GitHub 26 |
    27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import express from 'express'; 3 | import Schema from './data/schema'; 4 | import GraphQLHTTP from 'express-graphql'; 5 | import {graphql} from 'graphql'; 6 | import {introspectionQuery} from 'graphql/utilities'; 7 | 8 | import {MongoClient} from 'mongodb'; 9 | 10 | let app = express(); 11 | app.use(express.static('public')); 12 | 13 | (async () => { 14 | try { 15 | let db = await MongoClient.connect(process.env.MONGO_URL); 16 | let schema = Schema(db); 17 | 18 | app.use('/graphql', GraphQLHTTP({ 19 | schema, 20 | graphiql: true 21 | })); 22 | 23 | let server = app.listen(process.env.PORT || 3000, () => { 24 | console.log(`Listening on port ${server.address().port}`); 25 | }); 26 | 27 | // Generate schema.json 28 | let json = await graphql(schema, introspectionQuery); 29 | fs.writeFile('./data/schema.json', JSON.stringify(json, null, 2), err => { 30 | if (err) throw err; 31 | 32 | console.log("JSON schema created"); 33 | }); 34 | } catch(e) { 35 | console.log(e); 36 | } 37 | })(); 38 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: "./js/app.js", 5 | output: { 6 | path: path.resolve(__dirname, "public"), 7 | filename: "bundle.js" 8 | }, 9 | module: { 10 | loaders: [ 11 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader', 12 | query: { 13 | presets: ['react', 'es2015', 'stage-0'], 14 | plugins: [path.resolve(__dirname, 'babelRelayPlugin')] 15 | } 16 | } 17 | ] 18 | } 19 | }; 20 | --------------------------------------------------------------------------------