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/1Too 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
endapp/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
endDefining 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
# endDefining 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
# endDefining 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
# endDefining 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!
  }
endOn 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"
    )
endReact.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 buildWeb Application
$ PLATFORM=chrome npm run buildChrome Extension


$ PLATFORM=android npm run buildAndroid 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,297
 
   
   
  