Coder Social home page Coder Social logo

graphql-client's Introduction

graphql-client Gem Version CI

GraphQL Client is a Ruby library for declaring, composing and executing GraphQL queries.

Usage

Installation

Add graphql-client to your Gemfile and then run bundle install.

# Gemfile
gem 'graphql-client'

Configuration

Sample configuration for a GraphQL Client to query from the SWAPI GraphQL Wrapper.

require "graphql/client"
require "graphql/client/http"

# Star Wars API example wrapper
module SWAPI
  # Configure GraphQL endpoint using the basic HTTP network adapter.
  HTTP = GraphQL::Client::HTTP.new("https://example.com/graphql") do
    def headers(context)
      # Optionally set any HTTP headers
      { "User-Agent": "My Client" }
    end
  end  

  # Fetch latest schema on init, this will make a network request
  Schema = GraphQL::Client.load_schema(HTTP)

  # However, it's smart to dump this to a JSON file and load from disk
  #
  # Run it from a script or rake task
  #   GraphQL::Client.dump_schema(SWAPI::HTTP, "path/to/schema.json")
  #
  # Schema = GraphQL::Client.load_schema("path/to/schema.json")

  Client = GraphQL::Client.new(schema: Schema, execute: HTTP)
end

Defining Queries

If you haven't already, familiarize yourself with the GraphQL query syntax. Queries are declared with the same syntax inside of a <<-'GRAPHQL' heredoc. There isn't any special query builder Ruby DSL.

This client library encourages all GraphQL queries to be declared statically and assigned to a Ruby constant.

HeroNameQuery = SWAPI::Client.parse <<-'GRAPHQL'
  query {
    hero {
      name
    }
  }
GRAPHQL

Queries can reference variables that are passed in at query execution time.

HeroFromEpisodeQuery = SWAPI::Client.parse <<-'GRAPHQL'
  query($episode: Episode) {
    hero(episode: $episode) {
      name
    }
  }
GRAPHQL

Fragments are declared similarly.

HumanFragment = SWAPI::Client.parse <<-'GRAPHQL'
  fragment on Human {
    name
    homePlanet
  }
GRAPHQL

To include a fragment in a query, reference the fragment by constant.

HeroNameQuery = SWAPI::Client.parse <<-'GRAPHQL'
  {
    luke: human(id: "1000") {
      ...HumanFragment
    }
    leia: human(id: "1003") {
      ...HumanFragment
    }
  }
GRAPHQL

This works for namespaced constants.

module Hero
  Query = SWAPI::Client.parse <<-'GRAPHQL'
    {
      luke: human(id: "1000") {
        ...Human::Fragment
      }
      leia: human(id: "1003") {
        ...Human::Fragment
      }
    }
  GRAPHQL
end

:: is invalid in regular GraphQL syntax, but #parse makes an initial pass on the query string and resolves all the fragment spreads with constantize.

Executing queries

Pass the reference of a parsed query definition to GraphQL::Client#query. Data is returned back in a wrapped GraphQL::Client::Schema::ObjectType struct that provides Ruby-ish accessors.

result = SWAPI::Client.query(Hero::Query)

# The raw data is Hash of JSON values
# result["data"]["luke"]["homePlanet"]

# The wrapped result allows to you access data with Ruby methods
result.data.luke.home_planet

GraphQL::Client#query also accepts variables and context parameters that can be leveraged by the underlying network executor.

result = SWAPI::Client.query(Hero::HeroFromEpisodeQuery, variables: {episode: "JEDI"}, context: {user_id: current_user_id})

Rails ERB integration

If you're using Ruby on Rails ERB templates, theres a ERB extension that allows static queries to be defined in the template itself.

In standard Ruby you can simply assign queries and fragments to constants and they'll be available throughout the app. However, the contents of an ERB template is compiled into a Ruby method, and methods can't assign constants. So a new ERB tag was extended to declare static sections that include a GraphQL query.

<%# app/views/humans/human.html.erb %>
<%graphql
  fragment HumanFragment on Human {
    name
    homePlanet
  }
%>

<p><%= human.name %> lives on <%= human.home_planet %>.</p>

These <%graphql sections are simply ignored at runtime but make their definitions available through constants. The module namespacing is derived from the .erb's path plus the definition name.

>> "views/humans/human".camelize
=> "Views::Humans::Human"
>> Views::Humans::Human::HumanFragment
=> #<GraphQL::Client::FragmentDefinition>

Examples

github/github-graphql-rails-example is an example application using this library to implement views on the GitHub GraphQL API.

Installation

Add graphql-client to your app's Gemfile:

gem 'graphql-client'

See Also

graphql-client's People

Contributors

aroben avatar arthurschreiber avatar byroot avatar chrisbloom7 avatar daemonsy avatar dewski avatar edouard-chin avatar elvinefendi avatar gjtorikian avatar hogelog avatar imwiss avatar jhawthorn avatar josh avatar jsoref avatar jturkel avatar kevinsawicki avatar lsanwick avatar meganemura avatar mikrobi avatar nakajima avatar rmosolgo avatar samsinite avatar seejohnrun avatar srgoldman avatar tenderlove avatar tjoyal avatar tma avatar wuarmin avatar yuki24 avatar zombre98 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

graphql-client's Issues

graphql-client .parse function creates invalid GraphQL syntax

I have the following GraphQL code:

#Creates auth token needed to access the API
mutation GenerateKey($email: String!, $password: String!) {
  authenticate(email: $email, password: $password) {
    success
    errors {messages, field}
    result {key}
  }
}

#Send out email
mutation ForgotPassword($email: String!) {
  forgotPassword(email: $email) {
    success
    errors {messages, field}
  }
}

#Updates password, Key is not the auth token it comes from the reset email
mutation ResetPassword($email: String!, $key: String!, $password: String!) {
  resetPassword(email: $email, key: $key, password: $password) {
    success
    errors {messages, field}
  }
}

I defined GraphQL client as client and I parse the code above:
mutations = client.parse(graphql_code), which returns a Module with three constants: GenerateKey,ForgotPassword,ResetPassword - GraphQL::Client::OperationDefinition objects.
I couldn't execute this mutation with client.query(mutations.const_get(:GenerateKey), variables: { email: '[email protected]', password: 'my_pass'}), as I received the following error:

#<GraphQL::Client::Errors @messages={"data"=>["Syntax Error: Expected '$', found Name 'email'."]} @details={"data"=>[{"message"=>"Syntax Error: Expected '$', found Name 'email'.", "locations"=>[{"line"=>2, "column"=>16}], "normalizedPath"=>["data"]}]}>

So I took a look at executed query using mutations.const_get(:GenerateKey).document.to_query_string and received:

mutation #<Module:0x0000000107b5ef70>__GenerateKey($email: String!, $password: String!) {
  authenticate(email: $email, password: $password) {
    success
    errors {
      messages
      field
    }
    result {
      key
    }
  }
}

#<Module:0x0000000107b5ef70>__GenerateKey mutation name is not a valid GraphQL syntax. Is there a way to workaround it?

There's a circular dependency between Schema::ObjectType and Schema::PossibleTypes

Hey @rmosolgo ๐Ÿ‘‹ ,

GraphQL::Client::Schema::ObjectType requires "graphql/client/schema/possible_types"

require "graphql/client/schema/possible_types"

and GraphQL::Client::Schema::PossibleTypes requires "graphql/client/schema/object_type"

require "graphql/client/schema/object_type"

I think we need to fix this. Otherwise warnings like this will appear

/usr/local/bundle/gems/zeitwerk-2.6.13/lib/zeitwerk/kernel.rb:34: warning: /usr/local/bundle/gems/zeitwerk-2.6.13/lib/zeitwerk/kernel.rb:34: warning: loading in progress, circular require considered harmful - /usr/local/bundle/gems/graphql-client-0.21.0/lib/graphql/client/schema/object_type.rb

Thanks
best regards

Can't use `#to_param` as a field any more

Below is a piece of code that used to work fine until v0.16.0 and stopped since v0.17.0. Tested with Ruby 3.2.2.

We use similar code to make the objects returned from GraphQL queries work with Rails' route helpers and for that we need to add #to_param to our type definitions.

In a Rails project #to_param is defined on the base Object class and by default returns self.to_s (source).

The objects returned from queries inherit from GraphQL::Client::Schema::ObjectClass which inherits from Object. So when we call #to_param on an object returned from the query it gets dispatched as Object#to_param and doesn't end up in GraphQL::Client::Schema::ObjectClass#method_missing. Thus instead of IDs of records we get strings like "#<#<Module:0x00000001062529f0>::User:0x0000000106316d50>".

It looks that in v0.16.0 all the typed fields were defined as methods on the query objects and since v0.17.0 the library moved to method_missing dispatch. That's why it's no longer possible to use fields that correspond to instance methods of Object, like #to_param.

#!/usr/bin/env ruby
require "bundler/inline"
gemfile do
  source "https://rubygems.org"

  gem "graphql", "1.13.2"
  # gem "graphql-client", "0.16.0" # <--- this works as expected
  gem "graphql-client", "0.21.0"
  gem "activesupport", "7.1.3.2"
end
require "graphql"
require "graphql/client"
require "active_support/core_ext/object/to_query"

module MyData
  User = Data.define(:id)
end

module MyTypes
  class User < GraphQL::Schema::Object
    field :id, ID, null: false
    field :to_param, String, null: false, resolver_method: :resolve_to_param

    def resolve_to_param
      "user-#{object.id}"
    end
  end

  class Query < GraphQL::Schema::Object
    field :current_user, User, null: false

    def current_user
      MyData::User.new(id: 1)
    end
  end
end

class MySchema < GraphQL::Schema
  query(MyTypes::Query)
end

client = GraphQL::Client.new(schema: MySchema, execute: MySchema)

TEST_QUERY = client.parse(<<~GRAPHQL)
  query {
    currentUser {
      toParam
    }
  }
GRAPHQL

current_user = client.query(TEST_QUERY).data.current_user

puts "current_user is #{current_user.inspect}"
puts "Expecting #to_param to return \"user-1\", got: #{current_user.to_param.inspect}"

TODO for graphql-client 1.0

This library has been around a long time, and it's a hard dependency for a lot of projects. I'd like to release a 1.0.0 version, and before that, I have at least a few things in mind:

  • Performance audit, especially for view helpers and runtime code
  • Find a way to support subscriptions
  • Return response metadata in HTTP client results

If anyone has other suggestions for 1.0, please share them in a comment below.

Automatic parsing of `ISO8601Date` and other custom types

We use graphql-ruby and graphql-client to talk to each other. The graphql-ruby library defines a few extra types beyond the standard GraphQL types, e.g. ISO8601Date, ISO8601DateTime, etc, which we make use of.

In the client we've found that the values of these fields are not getting automatically cast back to Date / Time objects, like they do for other types of object.

I was wondering what might be the best way to get automatic casting for these types. At the moment the only way I can see it might work is by monkey-patching the GraphQL::Schema::BUILT_IN_TYPES hash, which is a bit ๐Ÿคข .

module GraphQL
  class Schema
    BUILT_IN_TYPES['ISO8601Date'] = GraphQL::Types::ISO8601Date
    BUILT_IN_TYPES['ISO8601DateTime'] = GraphQL::Types::ISO8601DateTime
  end
end

Is there a better way of managing this? I can imagine that other GraphQL schemas might have other sorts of custom types, but then how should they be re-interpreted by the client?

Incidentally the tests don't tickle this situation, as the schema is defined using the DSL. To get the datetime/date parsing tests to fail, change test/test_query_result.rb to dump/reload the schema:

ReloadedSchema = GraphQL::Client.load_schema(Schema.execute(GraphQL::Introspection.query(include_deprecated_args: true, include_schema_description: true, include_specified_by_url: true, include_is_repeatable: true)))

def setup
  @client = GraphQL::Client.new(schema: ReloadedSchema, execute: Schema, enforce_collocated_callers: true)
end

Then the test fails:

bundle exec rake test TEST=test/test_query_result.rb
Run options: --seed 9819

# Running:

F.........F.......F...................

Finished in 0.077330s, 491.4005 runs/s, 1551.7910 assertions/s.

  1) Failure:
TestQueryResult#test_interface_within_union_values [test/test_query_result.rb:592]:
Expected: 1970-01-01 00:00:01 UTC
  Actual: "1970-01-01T01:00:01+01:00"

  2) Failure:
TestQueryResult#test_date_scalar_casting [test/test_query_result.rb:628]:
--- expected
+++ actual
@@ -1 +1,3 @@
-#<Date: 1970-01-01 ((2440588j,0s,0n),+0s,2299161j)>
+# encoding: US-ASCII
+#    valid: true
+"1970-01-01"


  3) Failure:
TestQueryResult#test_datetime_scalar_casting [test/test_query_result.rb:610]:
Expected: 1970-01-01 01:00:00 +0100
  Actual: "1970-01-01T01:00:00+01:00"

38 runs, 120 assertions, 3 failures, 0 errors, 0 skips
rake aborted!

Adding the "monkey patch" above then re-fixes the tests as the casting happens automatically.

This all feels a bit gross! Is there a better way?

Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.