GQLi - GraphQL Client for humans
GQLi is a DSL (Domain Specific Language) to consume GraphQL APIs.
DISCLAIMER:
This gem is in stable state and will be updated with more features. You can start using it for your respective projects, although there is no support available from Contentful.
This gem is not covered under the Contentful SLAs.
Users are free to create Github issues and collaborate.
Installation
Install it via the command line:
gem install gqli
Or add it to your Gemfile
:
gem 'gqli'
Usage
Creating a GraphQL Client
For the examples throughout this README, we'll be using the Contentful and Github GraphQL APIs. Therefore, here's the initialization code required for both of them:
require 'gqli'
# Creating a Contentful GraphQL Client
SPACE_ID = 'cfexampleapi'
CF_ACCESS_TOKEN = 'b4c0n73n7fu1'
CONTENTFUL_GQL = GQLi::Client.new(
"https://graphql.contentful.com/content/v1/spaces/#{SPACE_ID}",
headers: { "Authorization" => "Bearer #{CF_ACCESS_TOKEN}" }
)
# Creating a Github GraphQL Client
GITHUB_ACCESS_TOKEN = ENV['GITHUB_TOKEN']
GITHUB_GQL = GQLi::Client.new(
"https://api.github.com/graphql",
headers: { "Authorization" => "Bearer #{GITHUB_ACCESS_TOKEN}" }
)
Creating a Query
Queries are the way to request data from a GraphQL API. This gem provides a simple DSL to create your own queries.
The query operator is GQLi::DSL.query
.
# Query to fetch the usernames for the first 10 watchers of the first 10 repositories I belong to
WatchersQuery = GQLi::DSL.query {
viewer {
login
repositories(first: 10) {
edges {
node {
nameWithOwner
watchers(first: 10) {
edges {
node {
login
}
}
}
}
}
}
}
}
Divide and conquer - using Fragments
In order to reuse parts of queries, we can split chunks of our queries into Fragments.
The fragment operator is GQLi::DSL.fragment
.
To include fragments within other nodes, use the _
operator as shown below.
To do type matching, use the __on
operator as shown below.
# Base fragment that will be reused for all Cat queries.
CatBase = GQLi::DSL.fragment('CatBase', 'Cat') {
name
likes
lives
}
CatBestFriend = GQLi::DSL.fragment('CatBestFriend', 'Cat') {
bestFriend {
# Here, because `bestFriend` is polimorphic in our GraphQL API,
# we need to explicitly state for which Type we want to include our fragment.
# To do a type match, instead of GraphQLs `... on SomeType` we do `__on('SomeType')`.
__on('Cat') {
# To include a fragment, instead of GraphQLs `...`, we use `___`.
___ CatBase
}
}
}
# A fragment reusing multiple fragments
CatImportantFields = GQLi::DSL.fragment('CatImportantFields', 'Cat') {
___ CatBase
___ CatBestFriend
}
# A fragment used to define a query, alongside other regular fields.
CatQuery = GQLi::DSL.query {
catCollection(limit: 1) {
items {
___ CatImportantFields
image {
url
}
}
}
}
Executing the queries
To execute the queries, you need to pass a Query
object to the
client's #execute
method. This will return a
Response
object which contains the data
and the
query
executed.
For example:
response = CONTENTFUL_GQL.execute(CatQuery)
puts "Query sent:"
puts response.query.to_gql
puts
puts "Response received"
response.data.catCollection.items.each do |c|
puts "Name: #{c.name}"
puts "Likes: #{c.likes.join(", ")}"
puts "Lives #: #{c.lives}"
c.bestFriend.tap do |bf|
puts "Best Friend:"
puts "\tName: #{bf.name}"
puts "\tLikes: #{bf.likes.join(", ")}"
puts "\tLives #: #{bf.lives}"
end
end
The output is:
Query sent:
query {
catCollection(limit: 1) {
items {
name
likes
lives
bestFriend {
... on Cat {
name
likes
lives
}
}
image {
url
}
}
}
}
Response received
Name: Happy Cat
Likes: cheezburger
Lives #: 1
Best Friend:
Name: Nyan Cat
Likes: rainbows, fish
Lives #: 1337
Schema Introspection and Validation
By default this library will fetch and cache a copy of the GraphQL Schema for any API you create a client for.
This schema is used for query validation before running queries against the APIs. In case a query is invalid for the given schema, an exception will be raised.
To disable schema caching completely, when you initialize your client, send
validate_query: false
.
Queries executed using the #execute
method on the client will
be validated before executing the request if the option is set to
true
(which it is by default).
To avoid validating a query, you can use #execute!
instead.
To validate the query outside of the scope of an HTTP request, you can use
MY_CLIENT.schema.valid?(query)
.
Embedding the DSL in your classes
If you want to avoid the need for prepending all GQLi DSL's calls with
GQLi::DSL.
, then you can extend
and/or
include
the module within your own classes. When using
extend
, you will have access to the DSL at a class level. When
using include
you will have access to the DSL at an object
level.
class ContentfulClient
extend GQLi::DSL # Makes DSL available at a class level
include GQLi::DSL # Makes DSL available at an object level
SPACE_ID = 'cfexampleapi'
ACCESS_TOKEN = 'b4c0n73n7fu1'
CONTENTFUL_GQL = GQLi::Client.new(
"https://graphql.contentful.com/content/v1/spaces/#{SPACE_ID}",
headers: { "Authorization" => "Bearer #{ACCESS_TOKEN}" }
)
CatBase = fragment('CatBase', 'Cat') {
name
likes
lives
}
CatBestFriend = fragment('CatBestFriend', 'Cat') {
bestFriend {
__on('Cat') {
___ CatBase
}
}
}
CatImportantFields = fragment('CatImportantFields', 'Cat') {
___ CatBase
___ CatBestFriend
}
def cats(limit)
CONTENTFUL_GQL.execute(
query {
catCollection(limit: limit) {
items {
___ CatImportantFields
image {
url
}
}
}
}
)
end
end
response = ContentfulClient.new.cats(5)
Dealing with name collisions
By defining queries via a DSL, you may sometimes find that the fields you query for are also the names of built-in methods or reserved keywords of the language.
To avoid collisions you can use the __node
helper, for
example:
query = GQLi::DSL.query {
catCollection {
items {
sys {
__node('id')
}
}
}
}
The helper method __node
, can also receive arguments and have
children nodes as expected from any other node declaration, for example:
query = GQLi::DSL.query {
__node('catCollection', limit: 5) {
items {
name
}
}
}
Directives
In GraphQL, nodes can be selectively included or removed by the usage of
directives, there are 2 directives available for the querying specification
to do this: @include
and @skip
.
query = GQLi::DSL.query {
someNode(:@include => {if: object.includes_some_node?})
}
This will get transformed to:
query {
someNode @include(if: true) # or false
}
This behaviour is equivalent to using native Ruby to include/exclude a field from the query:
query = GQLi::DSL.query {
someNode if object.includes_some_node?
}
The difference is that by using the native implementation, in case of the condition not being met, the node will be completely not included in the outgoing query.
Yet to be implemented
-
Mutation queries
-
Subscription queries
Get involved
We appreciate any help on our repositories.
License
This repository is published under the MIT license.
Code of Conduct
We want to provide a safe, inclusive, welcoming, and harassment-free space and experience for all participants, regardless of gender identity and expression, sexual orientation, disability, physical appearance, socioeconomic status, body size, ethnicity, nationality, level of experience, age, religion (or lack thereof), or other identity markers.