USING GRAPHQL AND RELAY
FOR HYBRID
RUBY ON RAILS APPLICATIONS
Rubyfuza, Cape Town, South Africa
February 02, 2018
- Brazilian
- 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"
}
},
...
]
}
}
GraphQL
Ruby On Rails
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" 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)
React.js
Another JavaScript framework?
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;
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
// 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>
);
}
}
React Native
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
https://github.com/necolas/react-native-web
Relay
Relay
GraphQL
React.js
React.js + Relay
- React component
- Relay container
- Relay container sends GraphQL data to React component through props
- 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
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
github.com/meedan/react-native-share-menu
Use case: Same codebase, different platforms
(Android, iOS, Chrome extension, Firefox extension)
github.com/meedan/check-mark
Thanks! :)
slides.com/caiosba/rubyfuza18
http://ca.ios.ba
@caiosba
Using GraphQL and Relay for hybrid Ruby on Rails applications
By Caio Sacramento