jzavisek / workshop-backend-graphql

Workshop about GraphQL with ApolloStack in reactiveconf.com/workshops

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Plan

  1. Every functionality has own branch
  2. Every step has readme.md
  3. If you get lost you can checkout next step git checkout step1 or take a look into readme

Optional

  1. Install nodemon npm install nodemon -g for live reloading
  2. Install schema generator npm install apollo-codegen -g for schema generating

Step 0

  1. git clone https://github.com/polacekpavel/graphqlworkshop
  2. cd graphqlworkshop
  3. npm install
  4. Start a client npm start -> http://localhost:3000
  5. Start a server nodemon src/server or node src/server -> http://localhost:8000

#Step 1

Basic schema definition

    # comment    
    type NameOfType {
        # Nullable string
        title: String
        # Non-Nullable string
        title: String!
        # Nullable integer
        count: Int
        # Non-Nullable integer
        count: Int!
        # Boolean
        completed: Boolean
        # Array of strings
        comments: [String]
        # Custom type
        avatar: Image
        # Array
        avatar: [Image]
    }

Basic resolvers

Query: {
        users(root, args, context) {
            return [{
                id: 1,
                firstName: 'myFirstName',
                lastName: 'myLastName',
                githubUsername: 'fake'
            }]
        }
    }
.
.
.

Completed type schema for our application

    # Entry point for our application
    type Query {
        # List of all users
        users: [User]
        # Get user detail
        user(githubUsername: String!): User
    }
    
    # Entry point for modifications
    type Mutation {
        # Create new user
        createUser(firstName: String, lastName: String, githubUsername: String!): User
    }
    
    type User {
        id: Int!,
        firstName: String
        lastName: String
        github: Github
    }
    
    type Github {
        username: String
        location: String
        avatarSrc: String
        events: [Event]
    }
    
    type Event {
        eventType: String,
        weather: Weather
    }
    
    type Weather {
        condition: String
    }

Resolvers for our application

{
    Query: {
        users(root, args, context) {
            return [{
                firstName: 'First name 1',
                lastName: 'Last name'
            },{
                firstName: 'First name 2',
                lastName: 'Last name'
            }]
        },
        user(root, args, context) {
            return {
                firstName: 'First name 1',
                lastName: args.githubUsername
            }
        }
    },
    Mutation: {
        createUser(root, args, context) {
            return {
                firstName: args.firstName,
                lastName: args.lastName
            }
        }
    },
    User: {
        github(root, args, context) {
            return {
                username: root.firstName,
                location: 'Prague',
                avatarSrc: 'https://avatars0.githubusercontent.com/u/273551?v=3&s=140'
            }
        }
    },
    Github: {
        events(root, args, context) {
            return [{
                eventType: 'Fork',
            },{
                eventType: 'Watch',
            }]

        }
    },
    Event: {
        weather(root, args, context) {
            return {
                condition: 'fog'
            }
        }
    }
}

Wire it up with Express

const makeExecutableSchema = require('graphql-tools').makeExecutableSchema;
const apolloExpress = require('apollo-server').apolloExpress;
const graphiqlExpress = require('apollo-server').graphiqlExpress;

const schema = require('./schema').schema;
const resolvers = require('./resolvers').resolvers;
app.use('*', cors())
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

const executableSchema = makeExecutableSchema({
    typeDefs: schema,
    resolvers: resolvers
});
app.use('/graphql', apolloExpress((req) => {
    return {
        schema:executableSchema
    }
}));

app.use('/graphiql', graphiqlExpress({
    endpointURL: '/graphql'
}));

Test your queries in graphiql http://localhost:8000/graphiql #Step 2

Postgres connection postgres://oakbmqixdijogm:WaIgtJyBSg9KBHa7sasNzNwBc1@ec2-54-228-192-254.eu-west-1.compute.amazonaws.com:5432/db7uuofu104gv6

const Sequalize = require('sequelize');
const db = new Sequelize('postgres://oakbmqixdijogm:WaIgtJyBSg9KBHa7sasNzNwBc1@ec2-54-228-192-254.eu-west-1.compute.amazonaws.com:5432/db7uuofu104gv6', {    
    dialectOptions: {
        "ssl": true
    },
    dialect: 'postgres',
    pool: {
        max: 1,
        min: 0,
        idle: 10000
    }
});


const UserModel = db.define('User', {
    githubUsername: { type: Sequelize.STRING, fieldName: 'github_username' },
    firstName: { type: Sequelize.STRING, fieldName: 'first_name' },
    lastName: { type: Sequelize.STRING, fieldName: 'last_name' },
}, {
    timestamps: false
});

const User = db.models.User

db.sync();

exports.user = User;

Resolvers.js

  Query: {
        users(root, args, context) {
            return user.findAll();
        },
        user(root, args, context) {
            return user.findOne({ where: { githubUsername: args.githubUsername }});
        }
    },
    Mutation: {
        createUser(root, args, context) {
            return user.create({
                firstName: args.firstName,
                lastName: args.lastName,
                githubUsername: args.githubUsername
            })
        }
    },

Test your queries and mutation in graphiql http://localhost:8000/graphiql

#Step 3

0.(optional) Update your schema apollo-codegen download-schema http://localhost:8000/graphql --output ./graphql.schema.json
1.Bootstrap Apollo client

import { ApolloProvider } from "react-apollo";
import ApolloClient, { createNetworkInterface } from "apollo-client";


const networkInterface = createNetworkInterface('http://localhost:8000/graphql');

const client = new ApolloClient({
    networkInterface
});

ReactDOM.render(
    <ApolloProvider client={client}>
        <App />
    </ApolloProvider>,
    document.getElementById('root')
);

2.Connect your react component with ApolloData (Users.js)

import { graphql } from "react-apollo";
import gql from "graphql-tag";

class Users extends Component {
 ...   
}


const UsersQuery = gql`
    query getAllUsers {
        users {
            id
            firstName,
            lastName,
            github {
                username
            }
        }
    }
`;
export default graphql(UsersQuery)(Users);

3.Connect your react component (UserDetail) with ApolloData and using variables (UserDetail.js)

import { graphql } from "react-apollo";
import gql from "graphql-tag";
class UserDetail extends Component {
 ...   
}
const UserDetailQuery = gql`
    query getUsersDetail($githubUsername: String!) {
        user(githubUsername: $githubUsername) {
            id,
            firstName,
            github {
                avatarSrc,
                events {
                    createdAt,
                    eventType,
                    weather {
                        condition
                    }
                },
                username,
                location
            },
            lastName
        }
    }
`
export default graphql(UserDetailQuery, {
    options: (props) => {
        return {
            variables: {
                githubUsername: props.user.github.username
            }
        }
    }
})(UserDetail);

Step 4

CreateUser.js

import { graphql } from "react-apollo";
import gql from "graphql-tag";
 <button className="btn btn-success"
                        onClick={() => {
                            this.props.mutate({
                                variables: {
                                    firstName: this.state.firstName,
                                    lastName: this.state.lastName,
                                    githubUsername: this.state.githubUsername
                                }
                            }).then(() => this.props.onCreate());

                        }}>
                    Save
                </button>
const CreateUserQuery = gql`
    mutation createUser ($firstName: String!, $lastName: String!, $githubUsername: String!) {
        createUser(firstName: $firstName, lastName: $lastName, githubUsername: $githubUsername) {
            firstName,
            id,
            github {
                username
            }
            lastName
        }
    }
`
export default graphql(CreateUserQuery)(CreateUser);

Refetch data

this.users.data.refetch();

Step 5

//Client index .js
networkInterface.use([{
    applyMiddleware(req, next) {
        if (!req.options.headers) {
            req.options.headers = {};
        }

        req.options.headers.authorization = 'xxx' //github api personal access token; 
        next();
    }
}]);
//Server App.js
const addSchemaLevelResolveFunction = require('graphql-tools').addSchemaLevelResolveFunction;
addSchemaLevelResolveFunction(executableSchema, (root, args, context, info) => {
    if (!context || !context.authorization) {
        throw new Error('non-auth');
    }
});
app.use('/graphql', apolloExpress((req) => {
    return {
        schema: executableSchema,
        context: {
            authorization: req.headers.authorization
        },
    }
}));

#Step 6

//client batching
const client = new ApolloClient({
    networkInterface,
    shouldBatch: true
});

API endpoints Google places api https://maps.googleapis.com/maps/api/geocode/json?address=${location}

Weather api - time machine https://api.darksky.net/forecast/${apiKey}/${lat},${long},${time}?exclude=currently,flags

Github events api https://api.github.com/users/${username}/events

Github user detail api https://api.github.com/users/${githubUsername}

//Server batching and caching
const locationLoader = new DataLoader((ids) => {
    return fetch(`https://maps.googleapis.com/maps/api/geocode/json?address=${ids}`)
        .then((res) => res.json())
        .then((res) => {
            const lat = res.results[0].geometry.location.lat;
            const long = res.results[0].geometry.location.lng;

            return [{
                lat,
                long
            }];
        })
}, {
    batch: false
});

Competed resolvers with funcionality

const user = require('./db').user;
const DataLoader = require('dataloader');
const fetch = require('node-fetch');
const locationLoader = new DataLoader((ids) => {
    return fetch(`https://maps.googleapis.com/maps/api/geocode/json?address=${ids}`)
        .then((res) => res.json())
        .then((res) => {
            const lat = res.results[0].geometry.location.lat;
            const long = res.results[0].geometry.location.lng;

            return [{
                lat,
                long
            }];
        })
}, {
    batch: false
});

exports.resolvers = {
    Query: {
        users(root, args, context) {
            return user.findAll();
        },
        user(root, args, context) {
            return user.findOne({ where: { githubUsername: args.githubUsername } });
        }
    },
    Mutation: {
        createUser(root, args, context) {
            return user.create({
                firstName: args.firstName,
                lastName: args.lastName,
                githubUsername: args.githubUsername
            })
        }
    },
    User: {
        github(root, args, context) {
            return fetch(`https://api.github.com/users/${root.githubUsername}`)
                .then((res) => res.json())
                .then((res) => {
                    return {
                        username: root.githubUsername,
                        avatarSrc: res.avatar_url,
                        location: res.location
                    }
                });
        }
    },
    Github: {
        events(root, args, context) {
            return fetch(`https://api.github.com/users/${root.username}/events`)
                .then((res) => res.json())
                .then((res) => {
                    return res.map((event) => {
                        return {
                            eventType: event.type,
                            createdAt: event.created_at,
                            location: root.location
                        }
                    });
                })
        }
    },
    Event: {
        weather(root, args, context) {
            const apiKey = '458461701954a3df9a801e38d6033d17';
            return locationLoader.load(root.location).then((res) => {
                const time = Math.round(new Date(root.createdAt).getTime() / 1000);
                return fetch(`https://api.darksky.net/forecast/${apiKey}/${res.lat},${res.long},${time}?exclude=currently,flags`)
                    .then((res) => res.json())
                    .then((res) => {
                        return {
                            condition: res.daily.data[0].icon
                        }
                    });
            });
        }
    }
}

#Step 7

import update from 'react-addons-update';
...

this.props.mutate({
                                variables: {
                                    ...
                                },
                                optimisticResponse: {
                                    createUser: {
                                        id: 100,
                                        firstName: this.state.firstName,
                                        lastName: this.state.lastName,
                                      github: {
                                          username: this.state.githubUsername
                                      }
                                    }
                                },
                                updateQueries: {
                                    getAllUsers: (prev, { mutationResult }) => {
                                        const newUser = mutationResult.data.createUser;
                                        return update(prev, {
                                            users: {
                                                $unshift: [newUser]
                                            }
                                        });
                                   

About

Workshop about GraphQL with ApolloStack in reactiveconf.com/workshops


Languages

Language:JavaScript 66.9%Language:CSS 21.4%Language:HTML 11.7%