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,034