Speech of Valentyn Ostakh, Ruby Developer at Ruby Garage, at Ruby Meditation 27, Dnipro, 19.05.2019
Slideshare -
Next conference - http://www.rubymeditation.com/
This talk explores basic concepts of GraphQL.
The main goal is to show how GraphQL works and of what parts it consists of.
From the Ruby side we will look at how to create a GraphQL schema.
In addition, we will consider what pitfalls can be encountered at the start of work with GraphQL.
Announcements and conference materials https://www.fb.me/RubyMeditation
News https://twitter.com/RubyMeditation
Photos https://www.instagram.com/RubyMeditation
The stream of Ruby conferences (not just ours) https://t.me/RubyMeditation
5. What is GraphQL?
• A query language for API
• A type system of defined data
• A platform of both backend + frontend applications
6. More About GraphQL
• GraphQL was developed internally by Facebook in 2012 before being
publicly released in 2015
• The idea of GraphQL became from the frontend team
• GraphQL services can be written in any language
• GraphQL is transport-agnostic
• GraphQL represents data in graphs
8. GraphQL SDL
GraphQL schema is at the center of any GraphQL server
implementation.
GraphQL schema describes the functionality available to the
clients who connect to it
13. GraphQL Object Type
type Character {
firstName: String!
lastName: String
friends: [Character!]
}
module Types
class CharacterType < BaseObject
field :first_name, String, null: false
field :last_name, String, null: true
field :friends, [Types::CharacterType], null: true
end
end
15. GraphQL Fields
type Character {
firstName: String!
lastName: String
friends: [Character!]
}
module Types
class CharacterType < BaseObject
field :first_name, String, null: false
field :last_name, String, null: true
field :friends, [Types::CharacterType], null: true
end
end
21. GraphQL Fields: Arguments
type Character {
firstName: String!
lastName(reverse: Boolean): String
friends(last: Int): [Character!]
}
module Types
class CharacterType < BaseObject
field :first_name, String, null: false
field :last_name, String, null: true do
argument :reverse, Boolean, required: false
end
field :friends, [Types::CharacterType], null: true do
argument :last, Integer, required: false
end
end
end
22. GraphQL Fields: Arguments
type Character {
firstName: String!
lastName(reverse: Boolean): String
friends(last: Int): [Character!]
}
module Types
class CharacterType < BaseObject
field :first_name, String, null: false
field :last_name, String, null: true do
argument :reverse, Boolean, required: false
end
field :friends, [Types::CharacterType], null: true do
argument :last, Integer, required: false
end
end
end
23. GraphQL Fields: Resolvers
module Types
class CharacterType < BaseObject
field :first_name, String, null: false
field :last_name, String, null: true
field :full_name, String, null: false
end
end
24. GraphQL Fields: Resolvers
module Types
class CharacterType < BaseObject
field :first_name, String, null: false
field :last_name, String, null: true
field :full_name, String, null: false
def full_name
[object.full_name, object.last_name].join(' ')
end
end
end
25. GraphQL Fields: Resolvers
module Types
class CharacterType < BaseObject
field :first_name, String, null: false
field :last_name, String, null: true
field :full_name, String, null: false, resolver: Resolvers::FullName
end
end
26. GraphQL Fields: Resolvers
module Resolvers
class Character < Resolvers::Base
type Character, null: false
argument :id, ID, required: true
def resolve(id:)
Character.find(id)
end
end
end
28. GraphQL Scalars
A GraphQL object type has a name and fields
but at some point those fields have to
become some concrete data.
That's where the scalar types come in: they
represent the leaves of the query.
29. GraphQL Scalars: Built-in Types
• Int: A signed 32‐bit integer
• Float: A signed double-precision floating-point value
• String: A UTF‐8 character sequence
• Boolean: true or false
• ID: The ID scalar type represents a unique identifier, often used to
refetch an object or as the key for a cache. The ID type is
serialized in the same way as a String; however, defining it as
an ID signifies that it is not intended to be human‐readable
30. GraphQL Scalars
In most GraphQL service implementations,
there is also a way to specify custom scalar
types.
31. GraphQL Scalars: GraphQL-Ruby Scalar Types
• Int: like a JSON or Ruby integer
• Float: like a JSON or Ruby floating point decimal
• String: like a JSON or Ruby string
• Boolean: like a JSON or Ruby boolean (true or false)
• ID: which a specialized String for representing unique object
identifiers
• ISO8601DateTime: an ISO 8601-encoded datetime
32. GraphQL Scalars: Custom Scalar
class Types::Url < Types::BaseScalar
description "A valid URL, transported as a string"
def self.coerce_input(input_value, context)
url = URI.parse(input_value)
if url.is_a?(URI::HTTP) || url.is_a?(URI::HTTPS)
url
else
raise GraphQL::CoercionError, "#{input_value.inspect} is not a valid URL"
end
end
def self.coerce_result(ruby_value, context)
ruby_value.to_s
end
end
33. GraphQL Scalars: Custom Scalar
class Types::Url < Types::BaseScalar
description "A valid URL, transported as a string"
def self.coerce_input(input_value, context)
url = URI.parse(input_value)
if url.is_a?(URI::HTTP) || url.is_a?(URI::HTTPS)
url
else
raise GraphQL::CoercionError, "#{input_value.inspect} is not a valid URL"
end
end
def self.coerce_result(ruby_value, context)
ruby_value.to_s
end
end
37. GraphQL Enums
enum RoleEnum {
FATHER
MOTHER
SON
DAUGHTER
DOG
}
class Types::RoleEnum < Types::BaseEnum
value ‘FATHER'
value ‘MOTHER'
value ‘SON'
value ‘DAUGHTER'
value ‘PET'
end
38. GraphQL Enums
enum RoleEnum {
FATHER
MOTHER
SON
DAUGHTER
DOG
}
class Types::RoleEnum < Types::BaseEnum
value 'FATHER', value: 1
value 'MOTHER', value: 2
value 'SON', value: 3
value 'DAUGHTER', value: 4
value 'PET', value: 5
end
39. GraphQL Enums
enum RoleEnum {
FATHER
MOTHER
SON
DAUGHTER
DOG
}
class Types::RoleEnum < Types::BaseEnum
value 'FATHER', value: :father
value 'MOTHER', value: :mom
value 'SON', value: :son
value 'DAUGHTER', value: :daughter
value 'PET', value: :animal
end
42. GraphQL Lists
type Character {
firstName: String!
lastName: String
friends: [Character!]
}
module Types
class CharacterType < BaseObject
field :first_name, String, null: false
field :last_name, String, null: true
field :friends, [Types::CharacterType], null: true
end
end
44. GraphQL Non-Null
In the GraphQL type system all types are nullable by default.
This means that a type like Int can take any integer (1, 2, etc.)
or null which represents the absence of any value.
However, the GraphQL type system allows you to make any type non-
null which means that the type will never produce a null value.
When using a non-null type there will always be a value.
45. GraphQL Non-Null
type Character {
firstName: String!
lastName: String
friends: [Character!]!
}
type Character {
firstName: String!
lastName: String
friends: [Character]!
}
type Character {
firstName: String!
lastName: String
friends: [Character!]
}
type Character {
firstName: String!
lastName: String
friends: [Character]
}
46. GraphQL Non-Null
module Types
class CharacterType < BaseObject
field :first_name, String, null: false
field :last_name, String, null: true
field :friends, [Types::CharacterType, null: true], null: true
field :friends, [Types::CharacterType, null: true], null: false
field :friends, [Types::CharacterType, null: false], null: true
field :friends, [Types::CharacterType, null: false], null: false
end
end
48. GraphQL Unions
{
search(in: "Adventure Time") {
__typename
... on Character {
firstName
}
... on land {
name
}
... on Building {
type
}
}
}
{
"data": {
"search": [
{
"__typename": "Character",
"firstName": "Finn"
},
{
"__typename": "Land",
"name": "Land of Ooo"
},
{
"__typename": "Building",
"type": "Fort"
}
]
}
}
49. GraphQL Unions
union Result = Character | Land | Building
type Character {
firstName: String!
}
type Building {
type: String!
}
type Land {
name: String!
}
type Query {
search: [Result]
}
50. GraphQL Unions
union Result = Character | Land | Building
type Character {
firstName: String!
}
type Building {
type: String!
}
type Land {
name: String!
}
type Query {
search: [Result]
}
51. GraphQL Unions
class Types::ResultType < Types::BaseUnion
possible_types Types::CharacterType, Types::LandType, Types::BuildingType
# Optional: if this method is defined, it will override `Schema.resolve_type`
def self.resolve_type(object, context)
if object.is_a?(Character)
Types::CharacterType
elsif object.is_a?(Land)
Types::LandType
else
Types::BuildingType
end
end
end
53. GraphQL Interfaces
interface Node {
id: ID!
}
interface Person {
firstName: String!
lastName: String
}
type Land implements Node {
id: ID!
name: String!
}
type Character implements Node & Person {
id: ID!
firstName: String!
lastName: String
}
54. GraphQL Interfaces
module Types::Interfaces::Node
include Types::BaseInterface
field :id, ID, null: false
end
module Types::Interfaces::Person
include Types::BaseInterface
field :first_name, String, null: false
field :last_name, String, null: true
end
module Types
class CharacterType < BaseObject
implements Types::Interfaces::Node
implements Types::Interfaces::Person
end
end
61. GraphQL Directives
• @deprecated(reason: String) - marks fields as deprecated with messages
• @skip(if: Boolean!) - GraphQL execution skips the field if true by not calling the resolver
• @include(id: Boolean!) - Calls resolver for annotated field if true
GraphQL provides several built-in directives:
62. GraphQL Directives
query {
character {
firstName
lastName
friends @skip(if: $isAlone){
firstName
lastName
}
}
}
type Character {
firstName: String!
lastName: String
surname: String @deprecated(reason: "Use lastName. Will be removed...")
friends: [Character!]
}
63. GraphQL Directives: Custom Directive
class Directives::Rest < GraphQL::Schema::Directive
description "..."
locations(
GraphQL::Schema::Directive::FIELD,
GraphQL::Schema::Directive::FRAGMENT_SPREAD,
GraphQL::Schema::Directive::INLINE_FRAGMENT
)
argument :url, String, required: true, description: "..."
def self.include?(obj, args, ctx)
# implementation
end
def self.resolve(obj, args, ctx)
# implementation
end
end
71. GraphQL Schema
class Types::QueryType < GraphQL::Schema::Object
field :character, Types::CharacterType, resolver: Resolvers::Character
end
# Similarly:
class Types::MutationType < GraphQL::Schema::Object
# ...
end
# and
class Types::SubscriptionType < GraphQL::Schema::Object
# ...
end
type Query {
character(id: ID!): CharacterType
}
76. When executing a GraphQL query one of the very first things the server needs to do
is transform the query (currently a string) into something it understands.
Transport
79. GraphQL-Ruby Phases of Execution
• Tokenize: splits the string into a stream of tokens
• Parse: builds an abstract syntax tree (AST) out of the stream of tokens
• Validate: validates the incoming AST as a valid query for the schema
• Rewrite: builds a tree of nodes which express the query in a simpler way than the
AST
• Analyze: if there are any query analyzers, they are run
• Execute: the query is traversed, resolve functions are called and the response is built
• Respond: the response is returned as a Hash
80.
81. Tokenize & Parsing
GraphQL has its own grammar.
We need this rules to split up the query.
GraphQL Ruby uses a tool called Ragel to generate its lexer.
How to understand that we have valid data?
82. How to verify incoming data?
• Breaks up a stream of characters (in our case a GraphQL query) into
tokens
• Turns sequences of tokens into a more abstract representation of the
language
85. Lexer & Parser
Javascript implementation of GraphQL has hand-written lexer and parser.
GraphQL Ruby uses a tool called Ragel to generate its lexer.
GraphQL Ruby uses a tool called Racc to generate its parser.
86. GraphQL-Ruby Phases of Execution
Tokenizing and Parsing are fundamental steps in the execution process.
Validating, Rewriting, Analyzing, Executing are just details of the implementation.
89. Authentication
• One endpoint
• Handle guests and registered users
• Fetch schema definition
• Solutions:
1. auth processes with GraphQL
2. auth by tokens (JWT, OAuth) (check tokens)
90. File uploading
• Vanilla GraphQL doesn’t support throwing raw files into your mutations
• Solutions:
1. Base64 Encoding
2. Separate Upload Requests
3. GraphQL Multipart Requests(most recommended)
91. N+1
• The server executes multiple unnecessary round trips to data stores for
nested data
• Problem in how GraphQL works, not Ruby
• Solutions:
1. preload data
2. gem ‘graphql-batch’
3. gem ‘batch-loader’
92. Caching
• Transport-agnostic
• Uses POST with HTTP
• Don’t have url with identifier
• Solution:
1. Server side caching(Memcache, Redis, etc.)
93. Performance
• Big queries can bring your server down to its knees
• Solutions:
1. Set query timeout
2. Check query depth
3. Check query complexity