USING GRAPHQL AND RELAY

FOR HYBRID

 RUBY ON RAILS APPLICATIONS

  • Bachelor and Master on Computer Science (UFBA)
     
  • Ruby On Rails since 2008
     
  • http://ca.ios.ba
  • Meedan Software Engineer since 2011      
     
  • San Francisco - California
     
  • http://meedan.com

Check

Social media fact-checking

2012 - today

github.com/meedan/checkdesk

   github.com/meedan/check-api

   github.com/meedan/check-web

   github.com/meedan/lapis

github.com/meedan/generator-keefer

Backend

Frontend

Ruby On Rails template

Yeoman generator

graphql

react.js

relay

ruby on rails

graphql

react.js

relay

ruby on rails

BACKEND

FRONTEND

GraphQL

"GraphQL is a query language for your API, and a server-side runtime for executing queries by using a type system you define for your data"

 

 

REST

Media

Comment

Tag

1

*

User

*

*

*

1

GET /api/v1/medias/1
GET /api/v1/medias/1/comments
GET /api/v1/medias/1/tags
GET /api/v1/medias/1/comments/1
GET /api/v1/users/1?fields=avatar,name
GET /api/v1/users/2?fields=avatar,name
GET /api/v1/users/3?fields=avatar,name
...

Reusable endpoints

GET /api/v1/medias/1?include=comments&count=5

GET /api/v1/medias/1?include=comments,tags
    &comments_count=5&tags_count=5

GET /api/v1/medias/1?fields=comments(text,date)
    &tags(tag)


...


GET /api/v1/media_and_comments/1
GET /api/v1/media_comments_and_tags/1
GET /api/v1/media_comments_tags_and_users/1
GET /api/v1/medias/1?include=comments&count=5

GET /api/v1/medias/1?include=comments,tags
    &comments_count=5&tags_count=5

GET /api/v1/medias/1?fields=comments(text,date)
    &tags(tag)


...


GET /api/v1/media_and_comments/1
GET /api/v1/media_comments_and_tags/1
GET /api/v1/media_comments_tags_and_users/1

Too many requests!

GraphQL

One endpoint to rule them all

POST /graphql

POST /api/graphql?query=
{
  media(id: 1) {
    title
    embed
    tags(first: 3) {
      tag
    }
    comments(first: 5) {
      created_at
      text
      user {
        name,
        avatar
      }
    }
  }
}
POST /api/graphql?query=
{
  media(id: 1) {
    title
    embed
    tags(first: 3) {
      tag
    }
    comments(first: 5) {
      created_at
      text
      user {
        name,
        avatar
      }
    }
  }
}

Media

Comment

Tag

1

*

User

*

*

*

1

~

POST /api/graphql?query=
{
  media(id: 1) {
    title
    embed
    tags(first: 3) {
      tag
    }
    comments(first: 5) {
      created_at
      text
      user {
        name,
        avatar
      }
    }
  }
}
{
  "media": {
    "title": "Avangers Hulk Smash",
    "embed": "<iframe src=\"...\"></iframe>",
    "tags": [
      { "tag": "avengers" },
      { "tag": "operation" }
    ],
    "comments": [
      {
        "text": "This is true",
        "created_at": "2016-09-18 15:04:39",
        "user": {
          "name": "Ironman",
          "avatar": "http://[...].png"
        }
      },
      ...
    ]
  }
}

Type System

POST /api/graphql?query=
{
  media(id: 1) {
    title
    embed
    tags(first: 3) {
      tag
    }
    comments(first: 5) {
      created_at
      text
      user {
        name,
        avatar
      }
    }
  }
}

Media

Comment

Tag

1

*

User

*

*

*

1

type QueryType {
  media(id: Int): Media
  comment(id: Int): Comment
  user(id: Int): User
  me: User
}
type Media {
  title: String
  embed: String
  comments: [Comment]
  tags:[Tag]
}
type Comment {
  created_at: String
  text: String
  user: User
}
type User {
  name: String
  avatar: String
}

GraphQL

Ruby On Rails

Auto-generated documentation

Code generation

Static validation

UI integration (GraphiQL)

Creating a new Rails API with GraphQL support

$ git clone https://github.com/meedan/lapis.git
$ rails new <name> -m <path to lapis_template.rb>
  • No view (headless API)
  • REST & GraphQL
  • Swagger UI & GraphiQL
  • Docker
  • Client gem generation
  • Webhooks & tokens

Adding GraphQL support to existing application

gem 'graphql'
gem 'graphql-relay'

Gemfile

class GraphqlController < BaseApiController
  def create
    vars = params[:variables] || {}
    query = GraphQL::Query.new(RelayOnRailsSchema, params[:query], variables: vars)
    render json: query.result
  end
end

app/controllers/graphql_controller.rb (add route too)

RelayOnRailsSchema = GraphQL::Schema.new(query: QueryType, mutation: MutationType)

config/initializers/relay.rb

type Media {
  title: String
  embed: String
  user: User
  comments: [Comment]
}
# "Type" to differentiate from models
MediaType = GraphQL::ObjectType.define do
  name 'Media'
  description 'Media type'
  field :title, types.String
  field :embed, types.String
  field :user do
    type UserType
    resolve -> (media, _args, _ctx) {
      media.user
    }
  end
  connection :comments, ->{ CommentType.connection_type }
  do
    argument :user_id, types.Int

    resolve -> (media, args, ctx) {
      media.comments(args[:user_id], ctx[:current_user])
    }
  end
end

Defining a GraphQL type

app/graph/types/media_type.rb

# "Type" to differentiate from models
# MediaType = GraphQL::ObjectType.define do
#   name 'Media'
#   description 'Media type'
    field :title, types.String
    field :embed, types.String
#   field :user do
#     type UserType
#     resolve -> (media, _args, _ctx) {
#       media.user
#     }
#   end
#   connection :comments, ->{ CommentType.connection_type }
#   do
#     argument :user_id, types.Int
#
#     resolve -> (media, args, ctx) {
#       media.comments(args[:user_id], ctx[:current_user])
#     }
#   end
# end

Defining a GraphQL type

By default, fields resolve to model instance methods

# "Type" to differentiate from models
# MediaType = GraphQL::ObjectType.define do
#   name 'Media'
#   description 'Media type'
#   field :title, types.String
#   field :embed, types.String
   field :user do
     type UserType
     resolve -> (media, _args, _ctx) {
       media.user
     }
   end
#   connection :comments, ->{ CommentType.connection_type }
#   do
#     argument :user_id, types.Int
#
#     resolve -> (media, args, ctx) {
#       media.comments(args[:user_id], ctx[:current_user])
#     }
#   end
# end

Defining a GraphQL type

It's possible to have a "resolve" method with custom behavior and that references other custom GraphQL types

# "Type" para diferenciar dos models
# MediaType = GraphQL::ObjectType.define do
#   name 'Media'
#   description 'Media type'
#   field :title, types.String
#   field :embed, types.String
#   field :user do
#     type UserType
#     resolve -> (media, _args, _ctx) {
#       media.user
#     }
#   end
 connection :comments, ->{ CommentType.connection_type } 
 do
   argument :user_id, types.Int

   resolve -> (media, args, ctx) {
     media.comments(args[:user_id], ctx[:current_user])
   }
 end
# end

Defining a GraphQL type

Relationships reference connections of other GraphQL types, can receive arguments and have access to the context

Mutations

mutation { 
  createMedia(
    input: {
      url: "http://youtu.be/7a_insd29fk"
      clientMutationId: "1"
    }
   )
   {
     media {
       id
     }
   }
}

Mutations make changes on your server side.

CRUD:

Queries: Read

Mutations:

  • Create
  • Update
  • Delete
# mutation { 
     createMedia(
#     input: {
        url: "http://youtu.be/7a_insd29fk"
#       clientMutationId: "1"
#     }
#    )
     {
       media {
         id
       }
     }
# }

Mutation name

Input parameters

Desired output

CreateMedia = GraphQL::Relay::Mutation.define do
  input_field :url, !types.String

  return_field :media, MediaType

  resolve -> (inputs, ctx) {
    media = Media.new
    media.url = inputs[:url]
    media.save!
  }
end

On Rails, a mutation must define the input fields, return fields and a resolution method (what the mutation actually does)

Metaprogramming FTW

https://github.com/meedan/check-api/blob/develop/

app/graph/mutations/graphql_crud_operations.rb

module MediaMutations
  Create, Update, Destroy = 
    GraphqlCrudOperations.define_crud_operations(
      'media',
      { url: '!str' },          # Fields available for "create"
      { url: 'str', id: '!id' } # Fields available for "update"
    )
end

React.js

  • Developed by Facebook
  • Library for the view layer

Another JavaScript framework?

Controllers

Directives

Templates

Global Event Listeners

Models

Only Components

RULE #1:

EVERYTHING IN REACT IS A COMPONENT

var MediaController = new Controller({
  addComment: function(comment) {
    // REST request
  },

  deleteComment: function(commentId) {
    // REST request
  },

  addTag: function(tag) {
    // REST request
  }
});
var MediaController = new Controller({
  addComment: function(comment) {
    // REST request
  },

  deleteComment: function(commentId) {
    // REST request
  },

  addTag: function(tag) {
    // REST request
  }
});
<div class="media">
  <h2>{{media.title}}</h2>
  <ul class="tags">
    <li ng-repeat="tag in tags">{{tag.tag}}</li>
  <ul>
  <ul class="comments">
    <li ng-repeat="comment in comments">
      <div ng-include="partials/comment.html">
    </li>
  <ul>
</div>
var MediaController = new Controller({
  addComment: function(comment) {
    // REST request
  },

  deleteComment: function(commentId) {
    // REST request
  },

  addTag: function(tag) {
    // REST request
  }
});
<div class="media">
  <h2>{{media.title}}</h2>
  <ul class="tags">
    <li ng-repeat="tag in tags">{{tag.tag}}</li>
  <ul>
  <ul class="comments">
    <li ng-repeat="comment in comments">
      <div ng-include="partials/comment.html">
    </li>
  <ul>
</div>
this.$el.find('#delete-comment')
.on('click', function() {
  // call deleteComment
});

MediaComponent

CommentsComponent

TagsComponent

TagComponent

CommentComponent

CommentComponent

CommentComponent

Concerns Separation


Components Separation

x

Self-contained components:

  • Testable
  • Composable
  • Reusable
  • Maintainable
import React, { Component, PropTypes } from 'react';
import Relay from 'react-relay';
import DeleteTagMutation from './DeleteTagMutation';

class Tag extends Component {
  delete() {
    const id = this.props.tag.id;
    Relay.Store.commitUpdate(
      new DeleteTagMutation({
        id: id
      })
    );
  }

  render() {
    const tag = this.props.tag;

    return (
      <div className="tag">
        <span>{tag.tag}</span>
        <span onClick={this.delete.bind(this)}>
          x
        </span>
      </div>
    );
  }
}

export default Tag;

JSX

  • Markup language similar to HTML
  • Declarative UI description
  • Combines the ease of templates with the power of JavaScript 
  • Pre-processor translates JSX to plain JavaScript

Good practices of performance

  • Avoid costly DOM operations
  • Minimize DOM access
  • Update elements offline before inserting on DOM
  • Avoid layout adjustments on JavaScript

 

The developer should be responsible for that?

RULE #2: React rebuilds everything on every change

Sounds costly? But it's fast!

Virtual DOM

  • Creates a light description of the interface
  • Calculates the difference between the current state and the previous one
  • Computes the minimum number of changes to be applied
  • Runs all operations in batch

RULE #3: Single data source

// import React, { Component, PropTypes } from 'react';
// import Relay from 'react-relay';
// import DeleteTagMutation from './DeleteTagMutation';
//
// class Tag extends Component {
//   handleDelete() {
       const id = this.props.tag.id;
       this.setState({ deleted: true });
//     Relay.Store.commitUpdate(
//       new DeleteTagMutation({
//         id: id
//       })
//     );
//   }
// 
//   render() {
      const tag = this.props.tag;

      if (this.state.deleted) {
        return (<span>Deleted</span>);
      }
//
//    componentWillMount() {
//      this.sort();
//    }
//
//    return (
//      <div className="tag">
//        <span>{tag.tag}</span>
//        <span onClick={this.delete.bind(this)}>
//        x
//        </span>
//      </div>
//    );
//  }
// }
//
// export default Tag;

props

Immutable

 

state

Mutable

class Tags extends Component {
  render() {
    return (
      <ul className="tags-list">
        {props.tags.map(function(tag) {
          return (
            <Tag tag={tag}>
          );
        })}
      </ul>
    );
  }
}
// import React, { Component, PropTypes } from 'react';
// import Relay from 'react-relay';
// import DeleteTagMutation from './DeleteTagMutation';
//
// class Tag extends Component {
     componentWillMount() {
       this.parent.sort();
     }
 
//   render() {
//     const tag = this.props.tag;
//
//     if (this.state.deleted) {
//       return (<span>Deleted</span>);
//     }
//
//     return (
//       <div className="tag">
//         <span>{tag.tag}</span>
//         <span onClick={this.delete.bind(this)}>
//         x
//         </span>
//       </div>
//     );
//   }
// }
//
// export default Tag;

Life cycle

Callbacks that are called in sequence when building or updating a component

React Native

  • Library that converts JSX to:
    • iOS Cocoa
    • Android UI
  • Application that have performance similar to native applications
  • Extensible
  • Reusable components
  • Portable to Android and iOS
class Tags extends Component {
  render() {
    return (
      <ul className="tags-list">
        {props.tags.map(function(tag) {
          return (
            <Tag tag={tag}>
          );
        })}
      </ul>
    );
  }
}

React component

React.js

DOM

React Native

Android

iOS

React Native Modules

  • Some implemented by default
  • Others implemented by the community
  • Rich documentation if you want to create your own

github.com/meedan/react-native-share-menu

One code, different platforms

  • Yeoman Generator
  • Generates a skeleton of a React / React Native app
  • Can receive a URL as parameter
  • Theming using SASS
  • Documentation
  • Platform abstraction
  • Docker
  • GraphQL / Relay
$ git clone 'https://github.com
  /meedan/generator-keefer.git'
$ cd generator-keefer
$ cp config.yml.example config.yml
$ npm install -g yo
$ npm link
$ yo keefer
$ PLATFORM=web npm run build

Web Application

$ PLATFORM=chrome npm run build

Chrome Extension

$ PLATFORM=android npm run build

Android App

Relay

Relay

GraphQL

React.js

React.js + Relay

  • React component
  • Relay container
  • Relay container sends GraphQL data to React component through props
class MediaComponent extends Component {
  render() {
    const media = this.props.media;

    return (
      <div>
        <article>
          <MediaDetail media={media} />
          <Tags tags={media.tags} />
          <h3>Verification Timeline</h3>
          <Comments comments={media.comments} />
        </article>
      </div>
    );
  }
}
class MediaComponent extends Component {
  render() {
    const media = this.props.media;

    return (
      <div>
        <article>
          <MediaDetail media={media} />
          <h2>{media.title}</h2>
          <Tags tags={media.tags} />
          <h3>Verification Timeline</h3>
          <Comments comments={media.comments} />
        </article>
      </div>
    );
  }
}
const MediaContainer = Relay.createContainer(
  MediaComponent, {
  fragments: {
    media: () => Relay.QL`
      fragment on Media {
        title
        embed
        user { ...userFragment }
        tags(first: 3) { edges { node {
          tag, id
        } } }
        comments(first: 5) { edges { node {
          ...commentFragment
        } } }
      }
    `
  }
});

class Media extends Component {
  render() {
    var route = new MediaRoute({ id: 1 });
    return (
      <Relay.RootContainer 
       Component={MediaContainer}
       route={route}
      />
    );
  }
}
class MediaRoute extends Relay.Route {
  static queries = {
    media: () => Relay.QL`query Media { 
                   media(id: $id)
                 }`, 
  };
  static paramDefinitions = {
    id: { required: true }
  };
  static routeName = 'MediaRoute';
};
  • Only new data is requested
  • Queries are validated locally first
  • Queries are made to local cache first
    • Relay Store

Mutations

class CreateTagMutation extends Relay.Mutation {
  getMutation() {
    return Relay.QL`mutation createTag {
      createTag
    }`;
  }

  getFatQuery() {
    return Relay.QL`fragment on CreateTagPayload
    { tagEdge, media { tags } }`;
  }

  getVariables() {
    const media = this.props.media;
    return { tag: media.tag.tag, media_id: media.id };
  }

  getConfigs() {
    return [{
      type: 'RANGE_ADD',
      parentName: 'media',
      parentID: this.props.media.id,
      connectionName: 'tags',
      edgeName: 'tagEdge',
      rangeBehaviors: {
        '': 'append'
      }
    }];
  }
}
// class CreateTagMutation extends Relay.Mutation {
     getMutation() {
       return Relay.QL`mutation createTag {
         createTag
       }`;
     }
// 
//   getFatQuery() {
//     return Relay.QL`fragment on CreateTagPayload
//     { tagEdge, media { tags } }`;
//   }
// 
//   getVariables() {
//     const media = this.props.media;
//     return { tag: media.tag.tag, media_id: media.id };
//   }
// 
//   getConfigs() {
//     return [{
//       type: 'RANGE_ADD',
//       parentName: 'media',
//       parentID: this.props.media.id,
//       connectionName: 'tags',
//       edgeName: 'tagEdge',
//       rangeBehaviors: {
//         '': 'append'
//       }
//     }];
//   }
// }

Mutation to be called

Expected return

Input parameters

How to update local cache

// class CreateTagMutation extends Relay.Mutation {
//   getMutation() {
//     return Relay.QL`mutation createTag {
//       createTag
//     }`;
//   }
// 
     getFatQuery() {
       return Relay.QL`fragment on CreateTagPayload
       { tagEdge, media { tags } }`;
     }
// 
//   getVariables() {
//     const media = this.props.media;
//     return { tag: media.tag.tag, media_id: media.id };
//   }
// 
//   getConfigs() {
//     return [{
//       type: 'RANGE_ADD',
//       parentName: 'media',
//       parentID: this.props.media.id,
//       connectionName: 'tags',
//       edgeName: 'tagEdge',
//       rangeBehaviors: {
//         '': 'append'
//       }
//     }];
//   }
// }

Mutation to be called

Expected return

Input parameters

How to update local cache

// class CreateTagMutation extends Relay.Mutation {
//   getMutation() {
//     return Relay.QL`mutation createTag {
//       createTag
//     }`;
//   }
// 
//   getFatQuery() {
//     return Relay.QL`fragment on CreateTagPayload
//     { tagEdge, media { tags } }`;
//   }
// 
     getVariables() {
       const media = this.props.media;
       return { tag: media.tag.tag, media_id: media.id };
     }
// 
//   getConfigs() {
//     return [{
//       type: 'RANGE_ADD',
//       parentName: 'media',
//       parentID: this.props.media.id,
//       connectionName: 'tags',
//       edgeName: 'tagEdge',
//       rangeBehaviors: {
//         '': 'append'
//       }
//     }];
//   }
// }

Mutation to be called

Expected return

Input parameters

How to update local cache

// class CreateTagMutation extends Relay.Mutation {
//   getMutation() {
//     return Relay.QL`mutation createTag {
//       createTag
//     }`;
//   }
// 
//   getFatQuery() {
//     return Relay.QL`fragment on CreateTagPayload
//     { tagEdge, media { tags } }`;
//   }
// 
//   getVariables() {
//     const media = this.props.media;
//     return { tag: media.tag.tag, media_id: media.id };
//   }
// 
     getConfigs() {
       return [{
         type: 'RANGE_ADD',
         parentName: 'media',
         parentID: this.props.media.id,
         connectionName: 'tags',
         edgeName: 'tagEdge',
         rangeBehaviors: {
           '': 'append'
         }
       }];
     }
   }

Mutation to be called

Expected return

Input parameters

How to update local cache

// import React, { Component, PropTypes } from 'react';
// import Relay from 'react-relay';
// import DeleteTagMutation from './DeleteTagMutation';
// 
// class Tag extends Component {
//   handleDelete() {
//     const id = this.props.tag.id;
       Relay.Store.commitUpdate(
         new DeleteTagMutation({
           id: id
         })
       );
//   }
// 
//   render() {
//     const tag = this.props.tag;
// 
//     return (
//       <div className="tag">
//         <span>{tag.tag}</span>
//         <span onClick={this.delete.bind(this)}>
//           x
//         </span>
//       </div>
//     );
//   }
// }
// 
// export default Tag;

Calling
a
mutation

Thanks! :)

slides.com/caiosba/rubyconfpt16

ca.ios.ba

Using GraphQL and Relay for hybrid Ruby on Rails applications

By Caio Sacramento

Using GraphQL and Relay for hybrid Ruby on Rails applications

Today, users have many ways to access an application. It can be an Android app, an iOS app, a web app or even a browser extension. Writing different source codes for different platforms, and maintain and test them, is a challenge that becomes highly costly and unfeasible in many cases. For a long time, people dreamed about a technology that allowed a developer to write only one code that could be compiled into different platforms. Cordova / Ionic seemed to be a way to, at least, allow hybrid applications between web, iOS and Android; but, since it was not based on native code, the applications based on those technologies suffered with performance issues. Regarding performance, not only the frontend has issues with that, but also the backend. Using polling for auto-refresh and no data caching on the client side can lead to poor performance on the server side too. Recently, Facebook open sourced React.js and React Native, JavaScript-based technologies but that brought a new paradigm to hybrid applications by compiling to native code and by taking care of the application state, which led to better performance. Besides that, Facebook also open sourced GraphQL (a declarative language to request data and that returns only what the client asked for) and Relay (another JavaScript-based technology that makes GraphQL consuming easy and that automatically handles pagination, caching and subscriptions). In this talk, the author will show how to add a GraphQL layer to an existing Ruby On Rails application, that is able to expose part of the data model through this interface, and how to build a client side application based on React. React Native and Relay that consumes this information and that works in different platforms (iOS, Android, Google Chrome, web) but that shares the same source code among them. Regarding the examples, two frameworks will be used. Keefer (open source Node.js generator that makes it easy to create multi-platform applications based on those technologies) and Lapis (open source Rails template to create web APIs integrated with Swagger and GraphQL), both maintained by the author. Source: http://rubyconf.pt/schedule/

  • 1,018