about me

Tamara Temple

tamara@tamouse.org

@tamouse

tamouse

example repo

a bit of history

  • 10+ years doing Ruby on Rails
  • Started working for Reachlocal end of March, '17
  • Hired to be an "API-driven Rubyist" ...yeah
  • Sometime in July started rebuilding front end as a Standalone React client
  • GraphQL was chosen API
  • graphql-ruby was chosen Ruby server
  • Apollo was chosen GQL client

Learning React+Apollo+GraphQL

with Rails GraphQL APi

... all at once

or...

My experiences learning 3 deep technologies when I've been doing something else for about 10 years.

person being smashed between react, graphql, and apollo
	 above and rails below

agenda

  • what is graphql
  • server-side graphql
  • client-side graphql
  • shit i've learned
  • what's next

what is graphql?

  • a query language for your API
  • runtime for fulfilling those queries
  • provides a complete and understandable description of the data in your API
  • gives clients the power to ask for exactly what they need
http://graphql.org/

gql - graphql language

ask for just what you need

      
query {
  viewer {
    posts {
      title
      author {
        name
      }
      excerpt
      published_date
      comment_count
    }
  }
}
      
    

queries start from a root node in the tree

      
query {
  viewer {
    posts {
      title
      author {
        name
      }
      excerpt
      published_date
      comment_count
    }
  }
}
      
    

The viewer is the root of the query tree into your graph.

Using the current viewer (i.e. user) as the root of the query let's you determine who's looking at the data.

This is used in many cases to perform some form of authorization.

queries specify the things you want to see

      
query {
  viewer {
    posts {
      title
      author {
        name
      }
      excerpt
      published_date
      comment_count
    }
  }
}
      
    

This tells graphql to return the posts the viewer can see.

      
query {
  viewer {
    posts {
      title
      author {
        name
      }
      excerpt
      published_date
      comment_count
    }
  }
}
      
    

This gets the author for each post, which is a relation.

      
query {
  viewer {
    posts {
      title
      author {
        name
      }
      excerpt
      published_date
      comment_count
    }
  }
}
      
    

this indicates we only want the author's name.

as a rails / backend person

...naturally I decided to set up the back end first.

graphql-ruby

A gem for providing graphql processing in a Rails environment.
Just what I was looking for.
... mostly

Learning graphql-ruby

  • lots of new terminology
  • few examples to look at
  • difficult to translate javascript-based tutorials and docs into the graphql-ruby world

the practice app(s)

rather that trying to apply learning in the legacy app, I wrote some "toy" learning apps to explore things

Looking at my Github account , you can probably find several of them in various stages of disrepair.

Most recent one involves authentication using JWTs, and signup, login, and logout via GraphQL mutations. (PoC for a boss)

huge help from outside

Before I got to the project, they had some help from outside, calling on folks from Github who've been implementing a lot with GraphQL.

When I discovered this I started to understand a lot more.

lots of parts involved

The server side requires several things to be in place before you can really get going serving up data

... i.e., situation normal for any Rails app

you need a controller

you need a schema

you need queries and mutations

you need do define types

you need resolvers

and that's not everything you'll need

luckily for us,

graphql-ruby provides us
with some generators

  • graphql:install -- controller, schema, root query and root mutation
  • graphql:object -- create object types for how data looks
  • a whole bunch of other things
Graphql:
  graphql:enum
  graphql:function
  graphql:install
  graphql:interface
  graphql:loader
  graphql:mutation
  graphql:object
  graphql:union
    

the graphql controller

it does controllerish things

  • authorizes viewer (current user)
  • marshalls parameters
  • runs the query / mutation
  • processes the result
  • handles errors

the heart of the controller

this is the only action needed on the controller
def execute
  authenticate! # sets current_user
  result = GraphqlSchema.execute(
    params[:query],
    variables: params[:variables],
    context: {current_user: current_user},
    operation_name: params[:operation_name],
  )
  render json: result
end 
graphql_controller on github repo

the graphql schema

the mouth of the graphql system

  • defines the root query
  • defines the root mutation
  • defines the root subscriptions

what's all this then about roots?

The graphql system defines a graph of your data

  • you query a tree
  • the tree needs a starting point
  • the starting point is the root
  • the best root for a client-facing query is the viewer
  • allows easy authorization

... how's that for foreshadowing?

the schema is really pretty simple


GraphqlSchema = GraphQL::Schema.define do

  mutation(Types::MutationType)
  query(Types::QueryType)

end
    

the root query

the stomach of the graphql system

  • The root query is actually a data type
  • It defines the "shape" of the root
  • Fields below are defined by relationship (don't have to be ActiveRecord, but that's the easiest to show)

the root query can be simple, or not

Types::QueryType = GraphQL::ObjectType.define do
  name "Query"
  field :viewer, Types::ViewerType, "Viewer of data, may be an anonymous user or registered user" do
    resolve ->(_object, _args, context) do
      if context[:current_user].present?
        context[:current_user]
      else
        NullUser.new
      end
    end
  end
end

graphql-ruby provides a DSL

  • corresponds to GraphQL terminology
  • enables descriptive schema exploration
  • specify types
  • specify resolvers (how data is populated)

I hear you ...

"What's with that NullUser??"

more foreshadowing

let's look at the viewer type

What is the Viewer?

  • The Viewer is either a logged in User
  • or an anonymous user
  • aka "NullUser"

Why?

authorization

  • All published posts can be viewed by any visitor
  • Only a post's author can modify a post
  • (really: create, update, publish, or delete)

The Viewer type provides the information we need to make this work

Types::ViewerType = GraphQL::ObjectType.define do
  name "Viewer"
  field :id, !types.ID, hash_key: :uuid
  field :name, !types.String
  field :my_posts, types[Types::PostType] do
    resolve ->(obj, _, _) { obj.posts }
  end
  field :public_posts, types[Types::PostType] do
    resolve ->(_,_,_){ Post.published }
  end
  field :all_authors, types[Types::UserType] do
    resolve ->(_,_,_){ User.all }
  end
end

On a big system with lots of data
we're not going to want to return
literally everything like that.

Pagination is provided for in the library.

But not in my example

... yet

Other types are probably less interesting.

the root mutation

the liver of the graphql system

  • how you change data with graphql
  • very similar structure
  • specify changing parts
  • specify what you want back

the root mutation

Types::MutationType = GraphQL::ObjectType.define do
  name "Mutation"

  field :createUser, Types::AuthenticateType do
    description "Creates a new user"
    argument :credentials, Types::AuthProviderCredentialsType
    resolve Mutations::CreateUser.new
  end

  field :loginUser, Types::AuthenticateType do
    description "Logs in a user"
    argument :credentials, Types::AuthProviderCredentialsType
    resolve Mutations::LoginUser.new
  end
end

fields are the different mutation operations

My project currently has two mutations

  • create a new user
  • log in a user

more mutations will come

The C.U.D. parts can be handled via an "upsert" sort of mutation.

I am concerned about how this might explode for very complicated systems (lots of models, lots of interrelationships).

let's look at the create user mutation

  field :createUser, Types::AuthenticateType do
    description "Creates a new user"
    argument :credentials, Types::AuthProviderCredentialsType
    resolve Mutations::CreateUser.new
  end

the field specifies a type that is returned

field :createUser, Types::AuthenticateType do
Types::AuthenticateType = GraphQL::ObjectType.define do
  name "Authenticate"
  field :token, types.String
  field :user, Types::UserType
en

the field has an argument, :credentials

argument :credentials, Types::AuthProviderCredentialsType
Types::AuthProviderCredentialsType = GraphQL::InputObjectType
  .define do

  name "AuthProviderCredentials"
  argument :name, types.String
  argument :email, !types.String
  argument :password, !types.String
end

the field references a resolver

resolve Mutations::CreateUser.new
  • The resolver is a PORO
  • It's doing the work you might see in an controller action
  • The resolver is passed 3 parameters.
    • the object
    • the arguments
    • the context

Mutations::CreateUser

class Mutations::CreateUser
  def call(_obj, args, _ctx)
    creds = args[:credentials]
    user = User.create!(name: creds[:name],email: creds[:email],
      password: creds[:password],
      password_confirmation: creds[:password])
    OpenStruct.new({
        token: AuthToken.new.token(user),
        user: user
      })
  rescue ActiveRecord::RecordInvalid => e
    raise GraphQL::ExecutionError.new("Invalid input")
  end
end

Mutations::CreateUser

class Mutations::CreateUser
  def call(_obj, args, _ctx)
    creds = args[:credentials]
    user = User.create!(name: creds[:name],email: creds[:email],
      password: creds[:password],
      password_confirmation: creds[:password])
    OpenStruct.new({
        token: AuthToken.new.token(user),
        user: user
      })
  rescue ActiveRecord::RecordInvalid => e
    raise GraphQL::ExecutionError.new("Invalid input")
  end
end

Mutations::CreateUser

class Mutations::CreateUser
  def call(_obj, args, _ctx)
    creds = args[:credentials]
    user = User.create!(
      name: creds[:name],email: creds[:email],
      password: creds[:password],
      password_confirmation: creds[:password])
    OpenStruct.new({
        token: AuthToken.new.token(user),
        user: user
      })
  rescue ActiveRecord::RecordInvalid => e
    raise GraphQL::ExecutionError.new("Invalid input")
  end
end

Mutations::CreateUser

class Mutations::CreateUser
  def call(_obj, args, _ctx)
    creds = args[:credentials]
    user = User.create!(
      name: creds[:name],email: creds[:email],
      password: creds[:password],
      password_confirmation: creds[:password])
    OpenStruct.new({
        token: AuthToken.new.token(user),
        user: user
      })
  rescue ActiveRecord::RecordInvalid => e
    raise GraphQL::ExecutionError.new("Invalid input")
  end
end

GraphQL is a "discoverable" API

This means you can figure out what to call by querying it

GraphiQL tool

  • a graphql "browser"
  • comes packaged with graphql-ruby
  • VERY useful for debugging
  • also a standalone electron app

In case you didn't get that, it's graph-i-ql
"Graphical"

client-side

have not gotten as far with the sample

at work have written lots of stuff

having so much fun!

no, really

Learning React

  • taking classes for the past 3-4 years
  • little learning samples
  • some project exposure
  • BUT THERE'S NOTHING LIKE TOTAL IMMERSION

initial obstacles

  • front end dev left
  • ancient legacy app (> 10 yo)
  • 21,000 lines of non-vendor js
  • jquery legacy js spread across the app
  • react rails components in ./app/assets
  • webpack 1 rails components in ./webpack
  • set up to add yet another way of integrating react
  • was not passing tests
  • no JS unit tests

other constraints

  • had to be incremental (no starting from scratch)
  • one rails view only to start
  • UX same as original modulo incremental improvements
  • must be delivered behind a guard
  • no super long branch diversions or skipping CI
  • no breaking existing stuff

xtra speshl funtiem stuph

  • no one else on project knew any of this stuff either
  • another team member given the same sort of assignment for the ReactNative mobile app

architecture (well, sorta)

  • webpack 2 and associated effluvia
  • react 15+
  • react router 4
  • apollo client
  • jest, eslint

initial approach

  • step back to zero
  • find out what was failing by incrementally adding stuff
  • create application layout for the app
  • write tests, get CI Green
  • figure out how to deliver bundles to production
  • document the heck out of it all
  • took maybe about 3 weeks

own goals established

  • web client will exclusively use graphql api
  • all clients, web, mobile, public api offerings, will be on same footing with the backend
  • api will be publicly offered (might be a fee, not my call)
  • web and mobile will share components eventually and opportunistically
  • web client will not use any external JS

How to organize this stuff

  • client in it's own directory
  • source files in ./client/src
  • generic reusable components in ./client/src/components/
  • pages, views, plus associated stores, under ./client/concerns/

organization continued

  • non-react utilities and stuff under ./client/utils
  • tests alongside the file under test (e.g. Foo.jsx and Foo.test.jsx in same directory)
  • external scss alongside file using it

directory structure

.client/
  src/
    components/
    concerns/
      jobs/
        detail/
          index.jsx
          index.test.jsx
          SummaryListing/
            index.jsx
            index.test.jsx
            SummaryActionBar/
              ...
     utils/
       ...

characteristics of work so far

  • lots of presentational components
  • several obvious sharable components
  • some high order component wrappers
  • not much graphql needed yet

back to the learning repo

  • React client made with create-react-app client
  • Add several libraries
  • Structure is a bit different than work
  • Jest tests missing :'(
  • Still trying to figure things out

internal architecture

  • Client in ./client/
  • Source in ./client/src/
  • Tests (cough) alongside components
  • Routed components as subdirs
  • Utils directory as well
  • Have not gotten sharable components yet

setting up the apollo client

define a client

  • Define a client to pass to the Apollo Provider
  • Declares the network interface to talk to the graphql API
  • Can be used to inject Authorization headers

create the network interface

import {createNetworkInterface} from 'react-apollo'

const networkInterface = createNetworkInterface({
  uri: "/graphql",
})

network interface middleware

networkInterface.use([{
  applyMiddleware(req, next) {
    let token = window.sessionStorage
      .getItem('token');
    if (token) {
      if (!req.options.headers) {
        req.options.headers = {};
      }
      req.options.headers.authorization =
        `Bearer ${token}`
    }
    next();
  }
}]);

create the client

const client = new ApolloClient({
  networkInterface,
})

wrapping the apollo provider

Much like you wrap Redux Store providers, you wrap the Apollo Provider around the application

Apollo Provider can take the place of a lot of your redux store needs

it uses Redux under the hood

Even better in v. 2

wrap the app

<ApolloProvider client={client}>
  <BrowserRouter>
    <Layout>
      <Route exact path="/" component={WhoAmI}/>
      <Route exact path="/login" component={LogIn}/>
      <Route exact path="/signup" component={SignUp}/>
      <Route exact path="/logout" component={LogOut}/>
      <Route exact path="/posts" component={PostsIndex}/>
    </Layout>
  </BrowserRouter>
</ApolloProvider>

establishing identity: authentication

  • at first, anonymous user performs query
  • sees list of published posts
  • has login and signup links
  • login and signup components perform mutations
  • logged in users perform same query, but are getting more information

create a user

  • the "/signup" path
  • the SignUp.jsx component
  • class extending React.Component implementing the SignUp form
  • Apollo graphql HOC wraps the form component with the mutation, passing in a mutate function
  • SignUp handles the form field updates and submission, calling the mutate function

the mutation

const signUpMutation = gql`

mutation SignUpUser(
    $credentials: AuthProviderCredentials
  ) {

  createUser(credentials: $credentials)

    {token user {name email}}

}`

the constructor

        constructor(props) {
    super(props)
    this.state = {
      name: '',
      email: '',
      password: '',
      mutate: props.mutate,
    }
    this.handleChange = this.handleChange.bind(this)
    this.handleSubmit = this.handleSubmit.bind(this)
  }
    

handleChange

        handleChange(e) {
    const target = e.target
    const value = target.type === 'checkbox' ?
      target.checked :
      target.value
    const name = target.name

    this.setState({
      [name]: value,
    })
  }
    

handleSubmit

        handleSubmit(e) {
    e.preventDefault()
    const credentials = {
      'name': this.state.name,
      'email': this.state.password,
      'password': this.state.password
    }

    this.state.mutate({ variables: {credentials} })

    .then(response => {
      let token = response.data.createUser.token
      window.sessionStorage.setItem('token', token)
    })
  }
    

the render

render() {
  if (this.state.loggedIn) return <Redirect to="/"/>
  return (
    <div>
      <form onSubmit={this.handleSubmit}>>
        <div>
          <label>Name: <input type="text" name="name"
            value={this.state.name} onChange={this.handleChange}/></label>
        </div>
        <div>
          <label>Email: <input type="email" name="email"
            value={this.state.email} onChange={this.handleChange}/></label>
        </div>
        <div>
          <label>Password: <input type="text" name="password"
            value={this.state.password} onChange={this.handleChange}/></label>
        </div>
        <div> <input type="submit"/> </div>
      </form>
    </div>
  )
}

where is all comes together

      

const SignUpWithMutation =
  graphql(signUpMutation)(SignUp)


export default SignUpWithMutation
    

the caller

Back in main application, we're importing this component and invoking on on a matching route

      import SignUp from './SignUp'
    
      <Route exact path="/signup" component={SignUp}/>
    

way back on the server side

 
The mutation is calling the :createUser mutation

 
When that completes, the new user's name and email, and their JWT is returned

and back to the client

 
the mutate promise resolves, and the token is put into session storage
 

because of the middleware in the network interface, the "Authorization" header is set with the token

Posts

The posts component gives a list of public posts

It's another React Component class

A posts query is wrapped around the display component

the query

The query is rooted at the viewer, who is established via the authorization header on the server

const listPosts = gql`query Posts{

viewer {

  public_posts {
    id
    title
    excerpt
    publishedAt

}}}`

the component

GraphQL wrapped query components have a sort of canonical form

      const MyComponent = props => {

  const { data: {
    loading, error, data_name } } = props;

  if (loading) return <p>Loading...</p>;

  if (error) return <p>Error! {error}</p>;

  return (<div>
    { do something with the data returned }
    </div>);
}
    

dereferencing the props passing in from graphql

      const MyComponent = props => {

  const { data: {
    loading, error, data_name } } = props;

  if (loading) return <p>Loading...</p>;

  if (error) return <p>Error! {error}</p>;

  return (<div>
     { do something with the data returned }
      </div>);
}
    

data_name will correspond to the root of the query

In my example query, this would be viewer

      
const listPosts = gql`query Posts{

  viewer {

    public_posts {
      id title excerpt
      publishedAt
    }

}}`
      

    

structure of data returned

The data returned is a JSON object with the structure of

      {
  "data": {
    "viewer": {
       "public_posts" : [

         {"id": 1, "title": "A fine day",
          "excerpt": "It started off sunny ...",
          "publishedAt": "2017-09-21..."},

       ]
    }
  }
}
    

rendering the list

      

return (

  <div>
    <h1>Posts</h1>

    {public_posts.map((post, idx) => {
      return <PostSummary post={post} key={idx} />
    })}

  </div>

)
    

bringing it together

just like we saw in the SignUp mutation, graphql wraps the component with the query, and returns the high order component.

      const PostsIndexWithData =
  graphql(listPosts)(PostsIndex)

export default PostsIndexWithData
    

shit i learned

like, holy crap, there's a lot to learn

what's next?

???

thanks!

example repo


Tamara Temple

tamara@tamouse.org

@tamouse

tamouse