graphql-client-0.18.0/0000755000004100000410000000000014240205174014541 5ustar www-datawww-datagraphql-client-0.18.0/graphql-client.gemspec0000644000004100000410000000743014240205174021024 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: graphql-client 0.18.0 ruby lib Gem::Specification.new do |s| s.name = "graphql-client".freeze s.version = "0.18.0" s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.require_paths = ["lib".freeze] s.authors = ["GitHub".freeze] s.date = "2022-05-02" s.description = "A Ruby library for declaring, composing and executing GraphQL queries".freeze s.email = "engineering@github.com".freeze s.files = ["LICENSE".freeze, "README.md".freeze, "lib/graphql/client.rb".freeze, "lib/graphql/client/collocated_enforcement.rb".freeze, "lib/graphql/client/definition.rb".freeze, "lib/graphql/client/definition_variables.rb".freeze, "lib/graphql/client/document_types.rb".freeze, "lib/graphql/client/erb.rb".freeze, "lib/graphql/client/error.rb".freeze, "lib/graphql/client/errors.rb".freeze, "lib/graphql/client/erubi_enhancer.rb".freeze, "lib/graphql/client/erubis.rb".freeze, "lib/graphql/client/erubis_enhancer.rb".freeze, "lib/graphql/client/fragment_definition.rb".freeze, "lib/graphql/client/hash_with_indifferent_access.rb".freeze, "lib/graphql/client/http.rb".freeze, "lib/graphql/client/list.rb".freeze, "lib/graphql/client/log_subscriber.rb".freeze, "lib/graphql/client/operation_definition.rb".freeze, "lib/graphql/client/query_typename.rb".freeze, "lib/graphql/client/railtie.rb".freeze, "lib/graphql/client/response.rb".freeze, "lib/graphql/client/schema.rb".freeze, "lib/graphql/client/schema/base_type.rb".freeze, "lib/graphql/client/schema/enum_type.rb".freeze, "lib/graphql/client/schema/include_directive.rb".freeze, "lib/graphql/client/schema/interface_type.rb".freeze, "lib/graphql/client/schema/list_type.rb".freeze, "lib/graphql/client/schema/non_null_type.rb".freeze, "lib/graphql/client/schema/object_type.rb".freeze, "lib/graphql/client/schema/possible_types.rb".freeze, "lib/graphql/client/schema/scalar_type.rb".freeze, "lib/graphql/client/schema/skip_directive.rb".freeze, "lib/graphql/client/schema/union_type.rb".freeze, "lib/graphql/client/view_module.rb".freeze, "lib/rubocop/cop/graphql/heredoc.rb".freeze, "lib/rubocop/cop/graphql/overfetch.rb".freeze] s.homepage = "https://github.com/github/graphql-client".freeze s.licenses = ["MIT".freeze] s.required_ruby_version = Gem::Requirement.new(">= 2.1.0".freeze) s.rubygems_version = "3.2.5".freeze s.summary = "GraphQL Client".freeze if s.respond_to? :specification_version then s.specification_version = 4 end if s.respond_to? :add_runtime_dependency then s.add_development_dependency(%q.freeze, [">= 3.2.22"]) s.add_runtime_dependency(%q.freeze, [">= 3.0"]) s.add_development_dependency(%q.freeze, ["~> 1.6"]) s.add_development_dependency(%q.freeze, ["~> 2.7"]) s.add_runtime_dependency(%q.freeze, [">= 0"]) s.add_development_dependency(%q.freeze, ["~> 5.9"]) s.add_development_dependency(%q.freeze, ["~> 11.2"]) s.add_development_dependency(%q.freeze, ["~> 0.55"]) s.add_development_dependency(%q.freeze, ["~> 0.10", "<= 0.16.0"]) else s.add_dependency(%q.freeze, [">= 3.2.22"]) s.add_dependency(%q.freeze, [">= 3.0"]) s.add_dependency(%q.freeze, ["~> 1.6"]) s.add_dependency(%q.freeze, ["~> 2.7"]) s.add_dependency(%q.freeze, [">= 0"]) s.add_dependency(%q.freeze, ["~> 5.9"]) s.add_dependency(%q.freeze, ["~> 11.2"]) s.add_dependency(%q.freeze, ["~> 0.55"]) s.add_dependency(%q.freeze, ["~> 0.10", "<= 0.16.0"]) end end graphql-client-0.18.0/README.md0000644000004100000410000001242514240205174016024 0ustar www-datawww-data# graphql-client [![Gem Version](https://badge.fury.io/rb/graphql-client.svg)](https://badge.fury.io/rb/graphql-client) [![CI](https://github.com/github/graphql-client/workflows/CI/badge.svg)](https://github.com/github/graphql-client/actions?query=workflow) 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`. ```ruby # Gemfile gem 'graphql-client' ``` ### Configuration Sample configuration for a GraphQL Client to query from the [SWAPI GraphQL Wrapper](https://github.com/graphql/swapi-graphql). ```ruby 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](http://graphql.org/docs/queries/). 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. ```ruby HeroNameQuery = SWAPI::Client.parse <<-'GRAPHQL' query { hero { name } } GRAPHQL ``` Queries can reference variables that are passed in at query execution time. ```ruby HeroFromEpisodeQuery = SWAPI::Client.parse <<-'GRAPHQL' query($episode: Episode) { hero(episode: $episode) { name } } GRAPHQL ``` Fragments are declared similarly. ```ruby HumanFragment = SWAPI::Client.parse <<-'GRAPHQL' fragment on Human { name homePlanet } GRAPHQL ``` To include a fragment in a query, reference the fragment by constant. ```ruby HeroNameQuery = SWAPI::Client.parse <<-'GRAPHQL' { luke: human(id: "1000") { ...HumanFragment } leia: human(id: "1003") { ...HumanFragment } } GRAPHQL ``` This works for namespaced constants. ```ruby 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`](http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-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. ```ruby 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. ```ruby 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. ```erb <%# app/views/humans/human.html.erb %> <%graphql fragment HumanFragment on Human { name homePlanet } %>

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

``` 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 => # ``` ## Examples [github/github-graphql-rails-example](https://github.com/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: ```ruby gem 'graphql-client' ``` ## See Also * [graphql-ruby](https://github.com/rmosolgo/graphql-ruby) gem which implements 80% of what this library provides. ❤️ [@rmosolgo](https://github.com/rmosolgo) * [Facebook's GraphQL homepage](http://graphql.org/) * [Facebook's Relay homepage](https://facebook.github.io/relay/) graphql-client-0.18.0/LICENSE0000644000004100000410000000204014240205174015542 0ustar www-datawww-dataCopyright (c) 2016 GitHub, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. graphql-client-0.18.0/lib/0000755000004100000410000000000014240205174015307 5ustar www-datawww-datagraphql-client-0.18.0/lib/rubocop/0000755000004100000410000000000014240205174016760 5ustar www-datawww-datagraphql-client-0.18.0/lib/rubocop/cop/0000755000004100000410000000000014240205174017541 5ustar www-datawww-datagraphql-client-0.18.0/lib/rubocop/cop/graphql/0000755000004100000410000000000014240205174021177 5ustar www-datawww-datagraphql-client-0.18.0/lib/rubocop/cop/graphql/heredoc.rb0000644000004100000410000000202314240205174023132 0ustar www-datawww-data# frozen_string_literal: true require "rubocop" module RuboCop module Cop module GraphQL # Public: Cop for enforcing non-interpolated GRAPHQL heredocs. class Heredoc < Cop def on_dstr(node) check_str(node) end def on_str(node) check_str(node) end def check_str(node) return unless node.location.is_a?(Parser::Source::Map::Heredoc) return unless node.location.expression.source =~ /^<<(-|~)?GRAPHQL/ node.each_child_node(:begin) do |begin_node| add_offense(begin_node, location: :expression, message: "Do not interpolate variables into GraphQL queries, " \ "used variables instead.") end add_offense(node, location: :expression, message: "GraphQL heredocs should be quoted. <<-'GRAPHQL'") end def autocorrect(node) ->(corrector) do corrector.replace(node.location.expression, "<<-'GRAPHQL'") end end end end end end graphql-client-0.18.0/lib/rubocop/cop/graphql/overfetch.rb0000644000004100000410000000465614240205174023524 0ustar www-datawww-data# frozen_string_literal: true require "active_support/inflector" require "graphql" require "graphql/client/view_module" require "rubocop" module RuboCop module Cop module GraphQL # Public: Rubocop for catching overfetched fields in ERB templates. class Overfetch < Cop if defined?(RangeHelp) # rubocop 0.53 moved the #source_range method into this module include RangeHelp end def_node_search :send_methods, "({send csend block_pass} ...)" def investigate(processed_source) erb = File.read(processed_source.buffer.name) query, = ::GraphQL::Client::ViewModule.extract_graphql_section(erb) return unless query aliases = {} fields = {} ranges = {} # TODO: Use GraphQL client parser document = ::GraphQL.parse(query.gsub(/::/, "__")) visitor = ::GraphQL::Language::Visitor.new(document) visitor[::GraphQL::Language::Nodes::Field] << ->(node, _parent) do name = node.alias || node.name fields[name] ||= 0 field_aliases(name).each { |n| (aliases[n] ||= []) << name } ranges[name] ||= source_range(processed_source.buffer, node.line, 0) end visitor.visit send_methods(processed_source.ast).each do |node| method_names = method_names_for(*node) method_names.each do |method_name| aliases.fetch(method_name, []).each do |field_name| fields[field_name] += 1 end end end fields.each do |field, count| next if count > 0 add_offense(nil, location: ranges[field], message: "GraphQL field '#{field}' query but was not used in template.") end end def field_aliases(name) names = Set.new names << name names << "#{name}?" names << underscore_name = ActiveSupport::Inflector.underscore(name) names << "#{underscore_name}?" names end def method_names_for(*node) receiver, method_name, *_args = node method_names = [] method_names << method_name if method_name # add field accesses like `nodes.map(&:field)` method_names.concat(receiver.children) if receiver && receiver.sym_type? method_names.map!(&:to_s) end end end end end graphql-client-0.18.0/lib/graphql/0000755000004100000410000000000014240205174016745 5ustar www-datawww-datagraphql-client-0.18.0/lib/graphql/client/0000755000004100000410000000000014240205174020223 5ustar www-datawww-datagraphql-client-0.18.0/lib/graphql/client/list.rb0000644000004100000410000000070414240205174021524 0ustar www-datawww-data# frozen_string_literal: true require "graphql/client/errors" module GraphQL class Client # Public: Array wrapper for value returned from GraphQL List. class List < Array def initialize(values, errors = Errors.new) super(values) @errors = errors freeze end # Public: Return errors associated with list of data. # # Returns Errors collection. attr_reader :errors end end end graphql-client-0.18.0/lib/graphql/client/erb.rb0000644000004100000410000000250614240205174021323 0ustar www-datawww-data# frozen_string_literal: true require "action_view" module GraphQL class Client begin require "action_view/template/handlers/erb/erubi" rescue LoadError require "graphql/client/erubis_enhancer" # Public: Extended Erubis implementation that supports GraphQL static # query sections. # # <%graphql # query GetVersion { # version # } # %> # <%= data.version %> # # Configure ActionView's default ERB implementation to use this class. # # ActionView::Template::Handlers::ERB.erb_implementation = GraphQL::Client::Erubis # class ERB < ActionView::Template::Handlers::Erubis include ErubisEnhancer end else require "graphql/client/erubi_enhancer" # Public: Extended Erubis implementation that supports GraphQL static # query sections. # # <%graphql # query GetVerison { # version # } # %> # <%= data.version %> # # Configure ActionView's default ERB implementation to use this class. # # ActionView::Template::Handlers::ERB.erb_implementation = GraphQL::Client::Erubi # class ERB < ActionView::Template::Handlers::ERB::Erubi include ErubiEnhancer end end end end graphql-client-0.18.0/lib/graphql/client/log_subscriber.rb0000644000004100000410000000217514240205174023561 0ustar www-datawww-data# frozen_string_literal: true require "active_support/log_subscriber" module GraphQL class Client # Public: Logger for "*.graphql" notification events. # # Logs GraphQL queries to Rails logger. # # UsersController::ShowQuery QUERY (123ms) # UsersController::UpdateMutation MUTATION (456ms) # # Enable GraphQL Client query logging. # # require "graphql/client/log_subscriber" # GraphQL::Client::LogSubscriber.attach_to :graphql # class LogSubscriber < ActiveSupport::LogSubscriber def query(event) logger.info do name = event.payload[:operation_name].gsub("__", "::") type = event.payload[:operation_type].upcase color("#{name} #{type} (#{event.duration.round(1)}ms)", nil, true) end logger.debug do event.payload[:document].to_query_string end end def error(event) logger.error do name = event.payload[:operation_name].gsub("__", "::") message = event.payload[:message] color("#{name} ERROR: #{message}", nil, true) end end end end end graphql-client-0.18.0/lib/graphql/client/schema/0000755000004100000410000000000014240205174021463 5ustar www-datawww-datagraphql-client-0.18.0/lib/graphql/client/schema/possible_types.rb0000644000004100000410000000276714240205174025070 0ustar www-datawww-data# frozen_string_literal: true require "graphql/client/error" require "graphql/client/schema/base_type" require "graphql/client/schema/object_type" module GraphQL class Client module Schema class PossibleTypes include BaseType def initialize(type, types) @type = type unless types.is_a?(Enumerable) raise TypeError, "expected types to be Enumerable, but was #{types.class}" end @possible_types = {} types.each do |klass| unless klass.is_a?(ObjectType) raise TypeError, "expected type to be #{ObjectType}, but was #{type.class}" end @possible_types[klass.type.graphql_name] = klass end end attr_reader :possible_types # Internal: Cast JSON value to wrapped value. # # value - JSON value # errors - Errors instance # # Returns BaseType instance. def cast(value, errors) case value when Hash typename = value["__typename"] if type = possible_types[typename] type.cast(value, errors) else raise InvariantError, "expected value to be one of (#{possible_types.keys.join(", ")}), but was #{typename.inspect}" end when NilClass nil else raise InvariantError, "expected value to be a Hash, but was #{value.class}" end end end end end end graphql-client-0.18.0/lib/graphql/client/schema/non_null_type.rb0000644000004100000410000000237014240205174024677 0ustar www-datawww-data# frozen_string_literal: true require "graphql/client/error" require "graphql/client/schema/base_type" module GraphQL class Client module Schema class NonNullType include BaseType # Internal: Construct non-nullable wrapper from other BaseType. # # of_klass - BaseType instance def initialize(of_klass) unless of_klass.is_a?(BaseType) raise TypeError, "expected #{of_klass.inspect} to be a #{BaseType}" end @of_klass = of_klass end # Internal: Get wrapped klass. # # Returns BaseType instance. attr_reader :of_klass # Internal: Cast JSON value to wrapped value. # # value - JSON value # errors - Errors instance # # Returns BaseType instance. def cast(value, errors) case value when NilClass raise InvariantError, "expected value to be non-nullable, but was nil" else of_klass.cast(value, errors) end end # Internal: Get non-nullable wrapper of this type class. # # Returns NonNullType instance. def to_non_null_type self end end end end end graphql-client-0.18.0/lib/graphql/client/schema/skip_directive.rb0000644000004100000410000000175014240205174025017 0ustar www-datawww-data# frozen_string_literal: true require "graphql/client/schema/base_type" module GraphQL class Client module Schema class SkipDirective include BaseType # Internal: Construct list wrapper from other BaseType. # # of_klass - BaseType instance def initialize(of_klass) unless of_klass.is_a?(BaseType) raise TypeError, "expected #{of_klass.inspect} to be a #{BaseType}" end @of_klass = of_klass end # Internal: Get wrapped klass. # # Returns BaseType instance. attr_reader :of_klass # Internal: Cast JSON value to wrapped value. # # values - JSON value # errors - Errors instance # # Returns List instance or nil. def cast(value, errors) case value when NilClass nil else of_klass.cast(value, errors) end end end end end end graphql-client-0.18.0/lib/graphql/client/schema/union_type.rb0000644000004100000410000000150114240205174024176 0ustar www-datawww-data# frozen_string_literal: true require "graphql/client/schema/possible_types" module GraphQL class Client module Schema class UnionType < Module include BaseType def initialize(type) unless type.kind.union? raise "expected type to be a Union, but was #{type.class}" end @type = type end def new(types) PossibleTypes.new(type, types) end def define_class(definition, ast_nodes) possible_type_names = definition.client.possible_types(type).map(&:graphql_name) possible_types = possible_type_names.map { |concrete_type_name| schema_module.get_class(concrete_type_name).define_class(definition, ast_nodes) } new(possible_types) end end end end end graphql-client-0.18.0/lib/graphql/client/schema/enum_type.rb0000644000004100000410000000476414240205174024030 0ustar www-datawww-data# frozen_string_literal: true require "graphql/client/error" require "graphql/client/schema/base_type" module GraphQL class Client module Schema class EnumType < Module include BaseType class EnumValue < String def initialize(obj, enum_value, enum) super(obj) @enum_value = enum_value @enum = enum end def respond_to_missing?(method_name, include_private = false) if method_name[-1] == "?" && @enum.include?(method_name[0..-2]) true else super end end def method_missing(method_name, *args) if method_name[-1] == "?" queried_value = method_name[0..-2] if @enum.include?(queried_value) raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0)" unless args.empty? return @enum_value == queried_value end end super end end # Internal: Construct enum wrapper from another GraphQL::EnumType. # # type - GraphQL::EnumType instance def initialize(type) unless type.kind.enum? raise "expected type to be an Enum, but was #{type.class}" end @type = type @values = {} all_values = type.values.keys comparison_set = all_values.map { |s| -s.downcase }.to_set all_values.each do |value| str = EnumValue.new(-value, -value.downcase, comparison_set).freeze const_set(value, str) if value =~ /^[A-Z]/ @values[str.to_s] = str end @values.freeze end def define_class(definition, ast_nodes) self end def [](value) @values[value] end # Internal: Cast JSON value to the enumeration's corresponding constant string instance # with the convenience predicate methods. # # values - JSON value # errors - Errors instance # # Returns String or nil. def cast(value, _errors = nil) case value when String raise Error, "unexpected enum value #{value}" unless @values.key?(value) @values[value] when NilClass value else raise InvariantError, "expected value to be a String, but was #{value.class}" end end end end end end graphql-client-0.18.0/lib/graphql/client/schema/object_type.rb0000644000004100000410000002422514240205174024324 0ustar www-datawww-data# frozen_string_literal: true require "active_support/inflector" require "graphql/client/error" require "graphql/client/errors" require "graphql/client/schema/base_type" require "graphql/client/schema/possible_types" module GraphQL class Client module Schema module ObjectType def self.new(type, fields = {}) Class.new(ObjectClass) do extend BaseType extend ObjectType define_singleton_method(:type) { type } define_singleton_method(:fields) { fields } const_set(:READERS, {}) const_set(:PREDICATES, {}) end end class WithDefinition include BaseType include ObjectType EMPTY_SET = Set.new.freeze attr_reader :klass, :defined_fields, :definition def type @klass.type end def fields @klass.fields end def spreads if defined?(@spreads) @spreads else EMPTY_SET end end def initialize(klass, defined_fields, definition, spreads) @klass = klass @defined_fields = defined_fields.map do |k, v| [-k.to_s, v] end.to_h @definition = definition @spreads = spreads unless spreads.empty? @defined_fields.keys.each do |attr| name = ActiveSupport::Inflector.underscore(attr) @klass::READERS[:"#{name}"] ||= attr @klass::PREDICATES[:"#{name}?"] ||= attr end end def new(data = {}, errors = Errors.new) @klass.new(data, errors, self) end end def define_class(definition, ast_nodes) # First, gather all the ast nodes representing a certain selection, by name. # We gather AST nodes into arrays so that multiple selections can be grouped, for example: # # { # f1 { a b } # f1 { b c } # } # # should be treated like `f1 { a b c }` field_nodes = {} ast_nodes.each do |ast_node| ast_node.selections.each do |selected_ast_node| gather_selections(field_nodes, definition, selected_ast_node) end end # After gathering all the nodes by name, prepare to create methods and classes for them. field_classes = {} field_nodes.each do |result_name, field_ast_nodes| # `result_name` might be an alias, so make sure to get the proper name field_name = field_ast_nodes.first.name field_definition = definition.client.schema.get_field(type.graphql_name, field_name) field_return_type = field_definition.type field_classes[result_name.to_sym] = schema_module.define_class(definition, field_ast_nodes, field_return_type) end spreads = definition.indexes[:spreads][ast_nodes.first] WithDefinition.new(self, field_classes, definition, spreads) end def define_field(name, type) name = name.to_s method_name = ActiveSupport::Inflector.underscore(name) define_method(method_name) do @casted_data.fetch(name) do @casted_data[name] = type.cast(@data[name], @errors.filter_by_path(name)) end end define_method("#{method_name}?") do @data[name] ? true : false end end def cast(value, errors) case value when Hash new(value, errors) when NilClass nil else raise InvariantError, "expected value to be a Hash, but was #{value.class}" end end private # Given an AST selection on this object, gather it into `fields` if it applies. # If it's a fragment, continue recursively checking the selections on the fragment. def gather_selections(fields, definition, selected_ast_node) case selected_ast_node when GraphQL::Language::Nodes::InlineFragment continue_selection = if selected_ast_node.type.nil? true else type_condition = definition.client.get_type(selected_ast_node.type.name) applicable_types = definition.client.possible_types(type_condition) # continue if this object type is one of the types matching the fragment condition applicable_types.include?(type) end if continue_selection selected_ast_node.selections.each do |next_selected_ast_node| gather_selections(fields, definition, next_selected_ast_node) end end when GraphQL::Language::Nodes::FragmentSpread fragment_definition = definition.document.definitions.find do |defn| defn.is_a?(GraphQL::Language::Nodes::FragmentDefinition) && defn.name == selected_ast_node.name end type_condition = definition.client.get_type(fragment_definition.type.name) applicable_types = definition.client.possible_types(type_condition) # continue if this object type is one of the types matching the fragment condition continue_selection = applicable_types.include?(type) if continue_selection fragment_definition.selections.each do |next_selected_ast_node| gather_selections(fields, definition, next_selected_ast_node) end end when GraphQL::Language::Nodes::Field operation_definition_for_field = definition.indexes[:definitions][selected_ast_node] # Ignore fields defined in other documents. if definition.source_document.definitions.include?(operation_definition_for_field) field_method_name = selected_ast_node.alias || selected_ast_node.name ast_nodes = fields[field_method_name] ||= [] ast_nodes << selected_ast_node end else raise "Unexpected selection node: #{selected_ast_node}" end end end class ObjectClass def initialize(data = {}, errors = Errors.new, definer = nil) @data = data @casted_data = {} @errors = errors # If we are not provided a definition, we can use this empty default definer ||= ObjectType::WithDefinition.new(self.class, {}, nil, []) @definer = definer @enforce_collocated_callers = source_definition && source_definition.client.enforce_collocated_callers end # Public: Returns the raw response data # # Returns Hash def to_h @data end def _definer @definer end def _spreads @definer.spreads end def source_definition @definer.definition end def respond_to_missing?(name, priv) if (attr = self.class::READERS[name]) || (attr = self.class::PREDICATES[name]) @definer.defined_fields.key?(attr) || super else super end end # Public: Return errors associated with data. # # It's possible to define "errors" as a field. Ideally this shouldn't # happen, but if it does we should prefer the field rather than the # builtin error type. # # Returns Errors collection. def errors if type = @definer.defined_fields["errors"] read_attribute("errors", type) else @errors end end def method_missing(name, *args) if (attr = self.class::READERS[name]) && (type = @definer.defined_fields[attr]) if @enforce_collocated_callers verify_collocated_path do read_attribute(attr, type) end else read_attribute(attr, type) end elsif (attr = self.class::PREDICATES[name]) && @definer.defined_fields[attr] has_attribute?(attr) else begin super rescue NoMethodError => e type = self.class.type if ActiveSupport::Inflector.underscore(e.name.to_s) != e.name.to_s raise e end all_fields = type.respond_to?(:all_fields) ? type.all_fields : type.fields.values field = all_fields.find do |f| f.name == e.name.to_s || ActiveSupport::Inflector.underscore(f.name) == e.name.to_s end unless field raise UnimplementedFieldError, "undefined field `#{e.name}' on #{type.graphql_name} type. https://git.io/v1y3m" end if @data.key?(field.name) raise ImplicitlyFetchedFieldError, "implicitly fetched field `#{field.name}' on #{type} type. https://git.io/v1yGL" else raise UnfetchedFieldError, "unfetched field `#{field.name}' on #{type} type. https://git.io/v1y3U" end end end end def inspect parent = self.class until parent.superclass == ObjectClass parent = parent.superclass end ivars = @data.map { |key, value| if value.is_a?(Hash) || value.is_a?(Array) "#{key}=..." else "#{key}=#{value.inspect}" end } buf = "#<#{parent.name}".dup buf << " " << ivars.join(" ") if ivars.any? buf << ">" buf end private def verify_collocated_path location = caller_locations(2, 1)[0] CollocatedEnforcement.verify_collocated_path(location, source_definition.source_location[0]) do yield end end def read_attribute(attr, type) @casted_data.fetch(attr) do @casted_data[attr] = type.cast(@data[attr], @errors.filter_by_path(attr)) end end def has_attribute?(attr) !!@data[attr] end end end end end graphql-client-0.18.0/lib/graphql/client/schema/scalar_type.rb0000644000004100000410000000210014240205174024307 0ustar www-datawww-data# frozen_string_literal: true require "graphql/client/schema/base_type" module GraphQL class Client module Schema class ScalarType include BaseType # Internal: Construct type wrapper from another GraphQL::BaseType. # # type - GraphQL::BaseType instance def initialize(type) unless type.kind.scalar? raise "expected type to be a Scalar, but was #{type.class}" end @type = type end def define_class(definition, ast_nodes) self end # Internal: Cast raw JSON value to Ruby scalar object. # # value - JSON value # errors - Errors instance # # Returns casted Object. def cast(value, _errors = nil) case value when NilClass nil else if type.respond_to?(:coerce_isolated_input) type.coerce_isolated_input(value) else type.coerce_input(value) end end end end end end end graphql-client-0.18.0/lib/graphql/client/schema/interface_type.rb0000644000004100000410000000151614240205174025014 0ustar www-datawww-data# frozen_string_literal: true require "graphql/client/schema/possible_types" module GraphQL class Client module Schema class InterfaceType < Module include BaseType def initialize(type) unless type.kind.interface? raise "expected type to be an Interface, but was #{type.class}" end @type = type end def new(types) PossibleTypes.new(type, types) end def define_class(definition, ast_nodes) possible_type_names = definition.client.possible_types(type).map(&:graphql_name) possible_types = possible_type_names.map { |concrete_type_name| schema_module.get_class(concrete_type_name).define_class(definition, ast_nodes) } new(possible_types) end end end end end graphql-client-0.18.0/lib/graphql/client/schema/base_type.rb0000644000004100000410000000177014240205174023770 0ustar www-datawww-data# frozen_string_literal: true module GraphQL class Client module Schema module BaseType # Public: Get associated GraphQL::BaseType with for this class. attr_reader :type # Internal: Get owner schema Module container. attr_accessor :schema_module # Internal: Cast JSON value to wrapped value. # # value - JSON value # errors - Errors instance # # Returns BaseType instance. def cast(value, errors) raise NotImplementedError, "subclasses must implement #cast(value, errors)" end # Internal: Get non-nullable wrapper of this type class. # # Returns NonNullType instance. def to_non_null_type @null_type ||= NonNullType.new(self) end # Internal: Get list wrapper of this type class. # # Returns ListType instance. def to_list_type @list_type ||= ListType.new(self) end end end end end graphql-client-0.18.0/lib/graphql/client/schema/include_directive.rb0000644000004100000410000000175314240205174025477 0ustar www-datawww-data# frozen_string_literal: true require "graphql/client/schema/base_type" module GraphQL class Client module Schema class IncludeDirective include BaseType # Internal: Construct list wrapper from other BaseType. # # of_klass - BaseType instance def initialize(of_klass) unless of_klass.is_a?(BaseType) raise TypeError, "expected #{of_klass.inspect} to be a #{BaseType}" end @of_klass = of_klass end # Internal: Get wrapped klass. # # Returns BaseType instance. attr_reader :of_klass # Internal: Cast JSON value to wrapped value. # # values - JSON value # errors - Errors instance # # Returns List instance or nil. def cast(value, errors) case value when NilClass nil else of_klass.cast(value, errors) end end end end end end graphql-client-0.18.0/lib/graphql/client/schema/list_type.rb0000644000004100000410000000262114240205174024025 0ustar www-datawww-data# frozen_string_literal: true require "graphql/client/error" require "graphql/client/list" require "graphql/client/schema/base_type" module GraphQL class Client module Schema class ListType include BaseType # Internal: Construct list wrapper from other BaseType. # # of_klass - BaseType instance def initialize(of_klass) unless of_klass.is_a?(BaseType) raise TypeError, "expected #{of_klass.inspect} to be a #{BaseType}" end @of_klass = of_klass end # Internal: Get wrapped klass. # # Returns BaseType instance. attr_reader :of_klass # Internal: Cast JSON value to wrapped value. # # values - JSON value # errors - Errors instance # # Returns List instance or nil. def cast(values, errors) case values when Array List.new(values.each_with_index.map { |e, idx| of_klass.cast(e, errors.filter_by_path(idx)) }, errors) when NilClass nil else raise InvariantError, "expected value to be a list, but was #{values.class}" end end # Internal: Get list wrapper of this type class. # # Returns ListType instance. def to_list_type self end end end end end graphql-client-0.18.0/lib/graphql/client/document_types.rb0000644000004100000410000000350014240205174023610 0ustar www-datawww-data# frozen_string_literal: true require "graphql" module GraphQL class Client # Internal: Use schema to detect definition and field types. module DocumentTypes # Internal: Detect all types used in a given document # # schema - A GraphQL::Schema # document - A GraphQL::Language::Nodes::Document to scan # # Returns a Hash[Language::Nodes::Node] to GraphQL::Type objects. def self.analyze_types(schema, document) unless schema.is_a?(GraphQL::Schema) || (schema.is_a?(Class) && schema < GraphQL::Schema) raise TypeError, "expected schema to be a GraphQL::Schema, but was #{schema.class}" end unless document.is_a?(GraphQL::Language::Nodes::Document) raise TypeError, "expected schema to be a GraphQL::Language::Nodes::Document, but was #{document.class}" end visitor = GraphQL::Language::Visitor.new(document) type_stack = GraphQL::StaticValidation::TypeStack.new(schema, visitor) fields = {} visitor[GraphQL::Language::Nodes::OperationDefinition] << ->(node, _parent) do fields[node] = type_stack.object_types.last end visitor[GraphQL::Language::Nodes::FragmentDefinition] << ->(node, _parent) do fields[node] = type_stack.object_types.last end visitor[GraphQL::Language::Nodes::InlineFragment] << ->(node, _parent) do fields[node] = type_stack.object_types.last end visitor[GraphQL::Language::Nodes::Field] << ->(node, _parent) do fields[node] = type_stack.field_definitions.last.type end visitor.visit fields rescue StandardError => err if err.is_a?(TypeError) raise end # FIXME: TypeStack my crash on invalid documents fields end end end end graphql-client-0.18.0/lib/graphql/client/view_module.rb0000644000004100000410000001077714240205174023103 0ustar www-datawww-data# frozen_string_literal: true require "active_support/dependencies" require "active_support/inflector" require "graphql/client/erubis_enhancer" module GraphQL class Client # Allows a magic namespace to map to app/views/**/*.erb files to retrieve # statically defined GraphQL definitions. # # # app/views/users/show.html.erb # <%grapql # fragment UserFragment on User { } # %> # # # Loads graphql section from app/views/users/show.html.erb # Views::Users::Show::UserFragment # module ViewModule attr_accessor :client # Public: Extract GraphQL section from ERB template. # # src - String ERB text # # Returns String GraphQL query and line number or nil or no section was # defined. def self.extract_graphql_section(src) query_string = src.scan(/<%graphql([^%]+)%>/).flatten.first return nil unless query_string [query_string, Regexp.last_match.pre_match.count("\n") + 1] end # Public: Eager load module and all subdependencies. # # Use in production when cache_classes is true. # # Traverses all app/views/**/*.erb and loads all static constants defined in # ERB files. # # Examples # # Views.eager_load! # # Returns nothing. def eager_load! return unless File.directory?(load_path) Dir.entries(load_path).sort.each do |entry| next if entry == "." || entry == ".." name = entry.sub(/(\.\w+)+$/, "").camelize.to_sym if ViewModule.valid_constant_name?(name) mod = const_defined?(name, false) ? const_get(name) : load_and_set_module(name) mod.eager_load! if mod end end nil end # Internal: Check if name is a valid Ruby constant identifier. # # name - String or Symbol constant name # # Examples # # valid_constant_name?("Foo") #=> true # valid_constant_name?("404") #=> false # # Returns true if name is a valid constant, otherwise false if name would # result in a "NameError: wrong constant name". def self.valid_constant_name?(name) name.to_s =~ /^[A-Z][a-zA-Z0-9_]*$/ end # Public: Directory to retrieve nested GraphQL definitions from. # # Returns absolute String path under app/views. attr_accessor :load_path alias_method :path=, :load_path= alias_method :path, :load_path # Public: if this module was defined by a view # # Returns absolute String path under app/views. attr_accessor :source_path # Internal: Initialize new module for constant name and load ERB statics. # # name - String or Symbol constant name. # # Examples # # Views::Users.load_module(:Profile) # Views::Users::Profile.load_module(:Show) # # Returns new Module implementing Loadable concern. def load_module(name) pathname = ActiveSupport::Inflector.underscore(name.to_s) path = Dir[File.join(load_path, "{#{pathname},_#{pathname}}{.*}")].sort.map { |fn| File.expand_path(fn) }.first return if !path || File.extname(path) != ".erb" contents = File.read(path) query, lineno = ViewModule.extract_graphql_section(contents) return unless query mod = client.parse(query, path, lineno) mod.extend(ViewModule) mod.load_path = File.join(load_path, pathname) mod.source_path = path mod.client = client mod end def placeholder_module(name) dirname = File.join(load_path, ActiveSupport::Inflector.underscore(name.to_s)) return nil unless Dir.exist?(dirname) Module.new.tap do |mod| mod.extend(ViewModule) mod.load_path = dirname mod.client = client end end def load_and_set_module(name) placeholder = placeholder_module(name) const_set(name, placeholder) if placeholder mod = load_module(name) return placeholder unless mod remove_const(name) if placeholder const_set(name, mod) mod.unloadable if mod.respond_to?(:unloadable) mod end # Public: Implement constant missing hook to autoload View ERB statics. # # name - String or Symbol constant name # # Returns module or raises NameError if missing. def const_missing(name) load_and_set_module(name) || super end end end end graphql-client-0.18.0/lib/graphql/client/railtie.rb0000644000004100000410000000272714240205174022211 0ustar www-datawww-data# frozen_string_literal: true require "graphql" require "graphql/client" require "rails/railtie" module GraphQL class Client # Optional Rails configuration for GraphQL::Client. # # Simply require this file to activate in the application. # # # config/application.rb # require "graphql/client/railtie" # class Railtie < Rails::Railtie config.graphql = ActiveSupport::OrderedOptions.new config.graphql.client = GraphQL::Client.new initializer "graphql.configure_log_subscriber" do |_app| require "graphql/client/log_subscriber" GraphQL::Client::LogSubscriber.attach_to :graphql end initializer "graphql.configure_erb_implementation" do |_app| require "graphql/client/erb" ActionView::Template::Handlers::ERB.erb_implementation = GraphQL::Client::ERB end initializer "graphql.configure_views_namespace" do |app| require "graphql/client/view_module" path = app.paths["app/views"].first # TODO: Accessing config.graphql.client during the initialization # process seems error prone. The application may reassign # config.graphql.client after this block is executed. client = config.graphql.client config.watchable_dirs[path] = [:erb] Object.const_set(:Views, Module.new do extend GraphQL::Client::ViewModule self.path = path self.client = client end) end end end end graphql-client-0.18.0/lib/graphql/client/collocated_enforcement.rb0000644000004100000410000000405114240205174025246 0ustar www-datawww-data# frozen_string_literal: true require "graphql/client/error" module GraphQL class Client # Collocation will not be enforced if a stack trace includes any of these gems. WHITELISTED_GEM_NAMES = %w{pry byebug} # Raised when method is called from outside the expected file scope. class NonCollocatedCallerError < Error; end # Enforcements collocated object access best practices. module CollocatedEnforcement extend self # Public: Ignore collocated caller enforcement for the scope of the block. def allow_noncollocated_callers Thread.current[:query_result_caller_location_ignore] = true yield ensure Thread.current[:query_result_caller_location_ignore] = nil end def verify_collocated_path(location, path, method = "method") return yield if Thread.current[:query_result_caller_location_ignore] if (location.path != path) && !(WHITELISTED_GEM_NAMES.any? { |g| location.path.include?("gems/#{g}") }) error = NonCollocatedCallerError.new("#{method} was called outside of '#{path}' https://git.io/v1syX") error.set_backtrace(caller(2)) raise error end begin Thread.current[:query_result_caller_location_ignore] = true yield ensure Thread.current[:query_result_caller_location_ignore] = nil end end # Internal: Decorate method with collocated caller enforcement. # # mod - Target Module/Class # methods - Array of Symbol method names # path - String filename to assert calling from # # Returns nothing. def enforce_collocated_callers(mod, methods, path) mod.prepend(Module.new do methods.each do |method| define_method(method) do |*args, &block| location = caller_locations(1, 1)[0] CollocatedEnforcement.verify_collocated_path(location, path, method) do super(*args, &block) end end end end) end end end end graphql-client-0.18.0/lib/graphql/client/schema.rb0000644000004100000410000000740014240205174022011 0ustar www-datawww-data# frozen_string_literal: true require "graphql" require "graphql/client/schema/enum_type" require "graphql/client/schema/include_directive" require "graphql/client/schema/interface_type" require "graphql/client/schema/list_type" require "graphql/client/schema/non_null_type" require "graphql/client/schema/object_type" require "graphql/client/schema/scalar_type" require "graphql/client/schema/skip_directive" require "graphql/client/schema/union_type" module GraphQL class Client module Schema module ClassMethods def define_class(definition, ast_nodes, type) type_class = case type.kind.name when "NON_NULL" define_class(definition, ast_nodes, type.of_type).to_non_null_type when "LIST" define_class(definition, ast_nodes, type.of_type).to_list_type else get_class(type.graphql_name).define_class(definition, ast_nodes) end ast_nodes.each do |ast_node| ast_node.directives.each do |directive| if directive = self.directives[directive.name.to_sym] type_class = directive.new(type_class) end end end type_class end def get_class(type_name) const_get(normalize_type_name(type_name)) end def set_class(type_name, klass) class_name = normalize_type_name(type_name) if constants.include?(class_name.to_sym) raise ArgumentError, "Can't define #{class_name} to represent type #{type_name} " \ "because it's already defined" end const_set(class_name, klass) end DIRECTIVES = { include: IncludeDirective, skip: SkipDirective }.freeze def directives DIRECTIVES end private def normalize_type_name(type_name) /\A[A-Z]/.match?(type_name) ? type_name : type_name.camelize end end def self.generate(schema) mod = Module.new mod.extend ClassMethods cache = {} schema.types.each do |name, type| next if name.start_with?("__") if klass = class_for(schema, type, cache) klass.schema_module = mod mod.set_class(name, klass) end end mod end def self.class_for(schema, type, cache) return cache[type] if cache[type] case type.kind.name when "INPUT_OBJECT" nil when "SCALAR" cache[type] = ScalarType.new(type) when "ENUM" cache[type] = EnumType.new(type) when "LIST" cache[type] = class_for(schema, type.of_type, cache).to_list_type when "NON_NULL" cache[type] = class_for(schema, type.of_type, cache).to_non_null_type when "UNION" klass = cache[type] = UnionType.new(type) type.possible_types.each do |possible_type| possible_klass = class_for(schema, possible_type, cache) possible_klass.send :include, klass end klass when "INTERFACE" cache[type] = InterfaceType.new(type) when "OBJECT" klass = cache[type] = ObjectType.new(type) type.interfaces.each do |interface| klass.send :include, class_for(schema, interface, cache) end # Legacy objects have `.all_fields` all_fields = type.respond_to?(:all_fields) ? type.all_fields : type.fields.values all_fields.each do |field| klass.fields[field.name.to_sym] = class_for(schema, field.type, cache) end klass else raise TypeError, "unexpected #{type.class} (#{type.inspect})" end end end end end graphql-client-0.18.0/lib/graphql/client/hash_with_indifferent_access.rb0000644000004100000410000000262314240205174026427 0ustar www-datawww-data# frozen_string_literal: true require "active_support/inflector" require "forwardable" module GraphQL class Client # Public: Implements a read only hash where keys can be accessed by # strings, symbols, snake or camel case. # # Also see ActiveSupport::HashWithIndifferentAccess. class HashWithIndifferentAccess extend Forwardable include Enumerable def initialize(hash = {}) @hash = hash @aliases = {} hash.each_key do |key| if key.is_a?(String) key_alias = ActiveSupport::Inflector.underscore(key) @aliases[key_alias] = key if key != key_alias end end freeze end def_delegators :@hash, :each, :empty?, :inspect, :keys, :length, :size, :to_h, :to_hash, :values def [](key) @hash[convert_value(key)] end def fetch(key, *args, &block) @hash.fetch(convert_value(key), *args, &block) end def key?(key) @hash.key?(convert_value(key)) end alias include? key? alias has_key? key? alias member? key? def each_key(&block) @hash.each_key { |key| yield convert_value(key) } end private def convert_value(key) case key when String, Symbol key = key.to_s @aliases.fetch(key, key) else key end end end end end graphql-client-0.18.0/lib/graphql/client/erubi_enhancer.rb0000644000004100000410000000100014240205174023510 0ustar www-datawww-data# frozen_string_literal: true module GraphQL class Client # Public: Erubi enhancer that adds support for GraphQL static query sections. # # <%graphql # query GetVersion { # version # } # %> # <%= data.version %> # module ErubiEnhancer # Internal: Extend Erubi handler to simply ignore <%graphql sections. def initialize(input, *args) input = input.gsub(/<%graphql/, "<%#") super(input, *args) end end end end graphql-client-0.18.0/lib/graphql/client/http.rb0000644000004100000410000000572414240205174021537 0ustar www-datawww-data# frozen_string_literal: true require "json" require "net/http" require "uri" module GraphQL class Client # Public: Basic HTTP network adapter. # # GraphQL::Client.new( # execute: GraphQL::Client::HTTP.new("http://graphql-swapi.parseapp.com/") # ) # # Assumes GraphQL endpoint follows the express-graphql endpoint conventions. # https://github.com/graphql/express-graphql#http-usage # # Production applications should consider implementing there own network # adapter. This class exists for trivial stock usage and allows for minimal # request header configuration. class HTTP # Public: Create HTTP adapter instance for a single GraphQL endpoint. # # GraphQL::Client::HTTP.new("http://graphql-swapi.parseapp.com/") do # def headers(context) # { "User-Agent": "My Client" } # end # end # # uri - String endpoint URI # block - Optional block to configure class def initialize(uri, &block) @uri = URI.parse(uri) singleton_class.class_eval(&block) if block_given? end # Public: Parsed endpoint URI # # Returns URI. attr_reader :uri # Public: Extension point for subclasses to set custom request headers. # # Returns Hash of String header names and values. def headers(_context) {} end # Public: Make an HTTP request for GraphQL query. # # Implements Client's "execute" adapter interface. # # document - The Query GraphQL::Language::Nodes::Document # operation_name - The String operation definition name # variables - Hash of query variables # context - An arbitrary Hash of values which you can access # # Returns { "data" => ... , "errors" => ... } Hash. def execute(document:, operation_name: nil, variables: {}, context: {}) request = Net::HTTP::Post.new(uri.request_uri) request.basic_auth(uri.user, uri.password) if uri.user || uri.password request["Accept"] = "application/json" request["Content-Type"] = "application/json" headers(context).each { |name, value| request[name] = value } body = {} body["query"] = document.to_query_string body["variables"] = variables if variables.any? body["operationName"] = operation_name if operation_name request.body = JSON.generate(body) response = connection.request(request) case response when Net::HTTPOK, Net::HTTPBadRequest JSON.parse(response.body) else { "errors" => [{ "message" => "#{response.code} #{response.message}" }] } end end # Public: Extension point for subclasses to customize the Net:HTTP client # # Returns a Net::HTTP object def connection Net::HTTP.new(uri.host, uri.port).tap do |client| client.use_ssl = uri.scheme == "https" end end end end end graphql-client-0.18.0/lib/graphql/client/errors.rb0000644000004100000410000001331214240205174022064 0ustar www-datawww-data# frozen_string_literal: true require "graphql/client/hash_with_indifferent_access" module GraphQL class Client # Public: Collection of errors associated with GraphQL object type. # # Inspired by ActiveModel::Errors. class Errors include Enumerable # Internal: Normalize GraphQL Error "path" ensuring the path exists. # # Records "normalizedPath" value to error object. # # data - Hash of response data # errors - Array of error Hashes # # Returns nothing. def self.normalize_error_paths(data = nil, errors = []) errors.each do |error| path = ["data"] current = data error.fetch("path", []).each do |key| break unless current path << key current = current[key] end error["normalizedPath"] = path end errors end # Internal: Initialize from collection of errors. # # errors - Array of GraphQL Hash error objects # path - Array of String|Integer fields to data # all - Boolean flag if all nested errors should be available def initialize(errors = [], path = [], all = false) @ast_path = path @all = all @raw_errors = errors end # Public: Return collection of all nested errors. # # data.errors[:node] # data.errors.all[:node] # # Returns Errors collection. def all if @all self else self.class.new(@raw_errors, @ast_path, true) end end # Internal: Return collection of errors for a given subfield. # # data.errors.filter_by_path("node") # # Returns Errors collection. def filter_by_path(field) self.class.new(@raw_errors, @ast_path + [field], @all) end # Public: Access Hash of error messages. # # data.errors.messages["node"] # data.errors.messages[:node] # # Returns HashWithIndifferentAccess. def messages return @messages if defined? @messages messages = {} details.each do |field, errors| messages[field] ||= [] errors.each do |error| messages[field] << error.fetch("message") end end @messages = HashWithIndifferentAccess.new(messages) end # Public: Access Hash of error objects. # # data.errors.details["node"] # data.errors.details[:node] # # Returns HashWithIndifferentAccess. def details return @details if defined? @details details = {} @raw_errors.each do |error| path = error.fetch("normalizedPath", []) matched_path = @all ? path[0, @ast_path.length] : path[0...-1] next unless @ast_path == matched_path field = path[@ast_path.length] next unless field details[field] ||= [] details[field] << error end @details = HashWithIndifferentAccess.new(details) end # Public: When passed a symbol or a name of a field, returns an array of # errors for the method. # # data.errors[:node] # => ["couldn't find node by id"] # data.errors['node'] # => ["couldn't find node by id"] # # Returns Array of errors. def [](key) messages.fetch(key, []) end # Public: Iterates through each error key, value pair in the error # messages hash. Yields the field and the error for that attribute. If the # field has more than one error message, yields once for each error # message. def each return enum_for(:each) unless block_given? messages.each_key do |field| messages[field].each { |error| yield field, error } end end # Public: Check if there are any errors on a given field. # # data.errors.messages # => {"node"=>["couldn't find node by id", "unauthorized"]} # data.errors.include?("node") # => true # data.errors.include?("version") # => false # # Returns true if the error messages include an error for the given field, # otherwise false. def include?(field) self[field].any? end alias has_key? include? alias key? include? # Public: Count the number of errors on object. # # data.errors.messages # => {"node"=>["couldn't find node by id", "unauthorized"]} # data.errors.size # => 2 # # Returns the number of error messages. def size values.flatten.size end alias count size # Public: Check if there are no errors on object. # # data.errors.messages # => {"node"=>["couldn't find node by id"]} # data.errors.empty? # => false # # Returns true if no errors are found, otherwise false. def empty? size.zero? end alias blank? empty? # Public: Returns all message keys. # # data.errors.messages # => {"node"=>["couldn't find node by id"]} # data.errors.values # => ["node"] # # Returns Array of String field names. def keys messages.keys end # Public: Returns all message values. # # data.errors.messages # => {"node"=>["couldn't find node by id"]} # data.errors.values # => [["couldn't find node by id"]] # # Returns Array of Array String messages. def values messages.values end # Public: Display console friendly representation of errors collection. # # Returns String. def inspect "#<#{self.class} @messages=#{messages.inspect} @details=#{details.inspect}>" end end end end graphql-client-0.18.0/lib/graphql/client/error.rb0000644000004100000410000000062514240205174021704 0ustar www-datawww-data# frozen_string_literal: true module GraphQL class Client # Public: Abstract base class for all errors raised by GraphQL::Client. class Error < StandardError end class InvariantError < Error end class ImplicitlyFetchedFieldError < NoMethodError end class UnfetchedFieldError < NoMethodError end class UnimplementedFieldError < NoMethodError end end end graphql-client-0.18.0/lib/graphql/client/erubis_enhancer.rb0000644000004100000410000000100214240205174023675 0ustar www-datawww-data# frozen_string_literal: true module GraphQL class Client # Public: Erubis enhancer that adds support for GraphQL static query sections. # # <%graphql # query GetVersion { # version # } # %> # <%= data.version %> # module ErubisEnhancer # Internal: Extend Erubis handler to simply ignore <%graphql sections. def convert_input(src, input) input = input.gsub(/<%graphql/, "<%#") super(src, input) end end end end graphql-client-0.18.0/lib/graphql/client/definition.rb0000644000004100000410000001637514240205174022714 0ustar www-datawww-data# frozen_string_literal: true require "graphql" require "graphql/client/collocated_enforcement" require "graphql/client/schema/object_type" require "graphql/client/schema/possible_types" require "set" module GraphQL class Client # Definitions are constructed by Client.parse and wrap a parsed AST of the # query string as well as hold references to any external query definition # dependencies. # # Definitions MUST be assigned to a constant. class Definition < Module def self.for(ast_node:, **kargs) case ast_node when Language::Nodes::OperationDefinition OperationDefinition.new(ast_node: ast_node, **kargs) when Language::Nodes::FragmentDefinition FragmentDefinition.new(ast_node: ast_node, **kargs) else raise TypeError, "expected node to be a definition type, but was #{ast_node.class}" end end def initialize(client:, document:, source_document:, ast_node:, source_location:) @client = client @document = document @source_document = source_document @definition_node = ast_node @source_location = source_location definition_type = case ast_node when GraphQL::Language::Nodes::OperationDefinition case ast_node.operation_type when "mutation" @client.schema.mutation when "subscription" @client.schema.subscription when "query", nil @client.schema.query else raise "Unexpected operation_type: #{ast_node.operation_type}" end when GraphQL::Language::Nodes::FragmentDefinition @client.get_type(ast_node.type.name) else raise "Unexpected ast_node: #{ast_node}" end @schema_class = client.types.define_class(self, [ast_node], definition_type) # Clear cache only needed during initialization @indexes = nil end # Internal: Get associated owner GraphQL::Client instance. attr_reader :client # Internal root schema class for definition. Returns # GraphQL::Client::Schema::ObjectType or # GraphQL::Client::Schema::PossibleTypes. attr_reader :schema_class # Internal: Get underlying operation or fragment definition AST node for # definition. # # Returns OperationDefinition or FragmentDefinition object. attr_reader :definition_node # Internal: Get original document that created this definition, without # any additional dependencies. # # Returns GraphQL::Language::Nodes::Document. attr_reader :source_document # Public: Global name of definition in client document. # # Returns a GraphQL safe name of the Ruby constant String. # # "Users::UserQuery" #=> "Users__UserQuery" # # Returns String. def definition_name return @definition_name if defined?(@definition_name) if name @definition_name = name.gsub("::", "__").freeze else "#{self.class.name}_#{object_id}".gsub("::", "__").freeze end end # Public: Get document with only the definitions needed to perform this # operation. # # Returns GraphQL::Language::Nodes::Document with one OperationDefinition # and any FragmentDefinition dependencies. attr_reader :document # Public: Returns the Ruby source filename and line number containing this # definition was not defined in Ruby. # # Returns Array pair of [String, Fixnum]. attr_reader :source_location def new(obj, errors = Errors.new) case schema_class when GraphQL::Client::Schema::PossibleTypes case obj when NilClass obj else cast_object(obj) end when GraphQL::Client::Schema::ObjectType::WithDefinition case obj when schema_class.klass if obj._definer == schema_class obj else cast_object(obj) end when nil nil when Hash schema_class.new(obj, errors) else cast_object(obj) end when GraphQL::Client::Schema::ObjectType case obj when nil, schema_class obj when Hash schema_class.new(obj, errors) else cast_object(obj) end else raise TypeError, "unexpected #{schema_class}" end end # Internal: Nodes AST indexes. def indexes @indexes ||= begin visitor = GraphQL::Language::Visitor.new(document) definitions = index_node_definitions(visitor) spreads = index_spreads(visitor) visitor.visit { definitions: definitions, spreads: spreads } end end private def cast_object(obj) if obj.class.is_a?(GraphQL::Client::Schema::ObjectType) unless obj._spreads.include?(definition_node.name) raise TypeError, "#{definition_node.name} is not included in #{obj.source_definition.name}" end schema_class.cast(obj.to_h, obj.errors) else raise TypeError, "unexpected #{obj.class}" end end EMPTY_SET = Set.new.freeze def index_spreads(visitor) spreads = {} on_node = ->(node, _parent) do node_spreads = flatten_spreads(node).map(&:name) spreads[node] = node_spreads.empty? ? EMPTY_SET : Set.new(node_spreads).freeze end visitor[GraphQL::Language::Nodes::Field] << on_node visitor[GraphQL::Language::Nodes::FragmentDefinition] << on_node visitor[GraphQL::Language::Nodes::OperationDefinition] << on_node spreads end def flatten_spreads(node) spreads = [] node.selections.each do |selection| case selection when Language::Nodes::FragmentSpread spreads << selection when Language::Nodes::InlineFragment spreads.concat(flatten_spreads(selection)) else # Do nothing, not a spread end end spreads end def index_node_definitions(visitor) current_definition = nil enter_definition = ->(node, _parent) { current_definition = node } leave_definition = ->(node, _parent) { current_definition = nil } visitor[GraphQL::Language::Nodes::FragmentDefinition].enter << enter_definition visitor[GraphQL::Language::Nodes::FragmentDefinition].leave << leave_definition visitor[GraphQL::Language::Nodes::OperationDefinition].enter << enter_definition visitor[GraphQL::Language::Nodes::OperationDefinition].leave << leave_definition definitions = {} on_node = ->(node, _parent) { definitions[node] = current_definition } visitor[GraphQL::Language::Nodes::Field] << on_node visitor[GraphQL::Language::Nodes::FragmentDefinition] << on_node visitor[GraphQL::Language::Nodes::InlineFragment] << on_node visitor[GraphQL::Language::Nodes::OperationDefinition] << on_node definitions end end end end graphql-client-0.18.0/lib/graphql/client/erubis.rb0000644000004100000410000000020614240205174022037 0ustar www-datawww-data# frozen_string_literal: true require "graphql/client/erb" module GraphQL class Client Erubis = GraphQL::Client::ERB end end graphql-client-0.18.0/lib/graphql/client/fragment_definition.rb0000644000004100000410000000056714240205174024573 0ustar www-datawww-data# frozen_string_literal: true require "graphql/client/definition" module GraphQL class Client # Specific fragment definition subtype. class FragmentDefinition < Definition def new(obj, *args) if obj.is_a?(Hash) raise TypeError, "constructing fragment wrapper from Hash is deprecated" end super end end end end graphql-client-0.18.0/lib/graphql/client/response.rb0000644000004100000410000000224214240205174022406 0ustar www-datawww-data# frozen_string_literal: true require "graphql/client/errors" module GraphQL class Client # Public: Abstract base class for GraphQL responses. # # https://facebook.github.io/graphql/#sec-Response-Format class Response # Public: Original JSON response hash returned from server. # # Returns Hash. attr_reader :original_hash alias_method :to_h, :original_hash # Public: Wrapped ObjectType of data returned from the server. # # https://facebook.github.io/graphql/#sec-Data # # Returns instance of ObjectType subclass. attr_reader :data # Public: Get partial failures from response. # # https://facebook.github.io/graphql/#sec-Errors # # Returns Errors collection object with zero or more errors. attr_reader :errors # Public: Hash of server specific extension metadata. attr_reader :extensions # Internal: Initialize base class. def initialize(hash, data: nil, errors: Errors.new, extensions: {}) @original_hash = hash @data = data @errors = errors @extensions = extensions end end end end graphql-client-0.18.0/lib/graphql/client/operation_definition.rb0000644000004100000410000000052014240205174024755 0ustar www-datawww-data# frozen_string_literal: true require "graphql/client/definition" module GraphQL class Client # Specific operation definition subtype for queries, mutations or # subscriptions. class OperationDefinition < Definition # Public: Alias for definition name. alias operation_name definition_name end end end graphql-client-0.18.0/lib/graphql/client/query_typename.rb0000644000004100000410000000763314240205174023630 0ustar www-datawww-data# frozen_string_literal: true require "graphql" require "graphql/client/document_types" module GraphQL class Client # Internal: Insert __typename field selections into query. module QueryTypename # Internal: Insert __typename field selections into query. # # Skips known types when schema is provided. # # document - GraphQL::Language::Nodes::Document to modify # schema - Optional Map of GraphQL::Language::Nodes::Node to GraphQL::Type # # Returns the document with `__typename` added to it if GraphQL::Language::Nodes::AbstractNode.method_defined?(:merge) # GraphQL 1.9 introduces a new visitor class # and doesn't expose writer methods for node attributes. # So, use the node mutation API instead. class InsertTypenameVisitor < GraphQL::Language::Visitor def initialize(document, types:) @types = types super(document) end def add_typename(node, parent) type = @types[node] type = type && type.unwrap if (node.selections.any? && (type.nil? || type.kind.interface? || type.kind.union?)) || (node.selections.none? && (type && type.kind.object?)) names = QueryTypename.node_flatten_selections(node.selections).map { |s| s.respond_to?(:name) ? s.name : nil } names = Set.new(names.compact) if names.include?("__typename") yield(node, parent) else node_with_typename = node.merge(selections: [GraphQL::Language::Nodes::Field.new(name: "__typename")] + node.selections) yield(node_with_typename, parent) end else yield(node, parent) end end def on_operation_definition(node, parent) add_typename(node, parent) { |n, p| super(n, p) } end def on_field(node, parent) add_typename(node, parent) { |n, p| super(n, p) } end def on_fragment_definition(node, parent) add_typename(node, parent) { |n, p| super(n, p) } end end def self.insert_typename_fields(document, types: {}) visitor = InsertTypenameVisitor.new(document, types: types) visitor.visit visitor.result end else def self.insert_typename_fields(document, types: {}) on_selections = ->(node, _parent) do type = types[node] if node.selections.any? case type && type.unwrap when NilClass, GraphQL::InterfaceType, GraphQL::UnionType names = node_flatten_selections(node.selections).map { |s| s.respond_to?(:name) ? s.name : nil } names = Set.new(names.compact) unless names.include?("__typename") node.selections = [GraphQL::Language::Nodes::Field.new(name: "__typename")] + node.selections end end elsif type && type.unwrap.is_a?(GraphQL::ObjectType) node.selections = [GraphQL::Language::Nodes::Field.new(name: "__typename")] end end visitor = GraphQL::Language::Visitor.new(document) visitor[GraphQL::Language::Nodes::Field].leave << on_selections visitor[GraphQL::Language::Nodes::FragmentDefinition].leave << on_selections visitor[GraphQL::Language::Nodes::OperationDefinition].leave << on_selections visitor.visit document end end def self.node_flatten_selections(selections) selections.flat_map do |selection| case selection when GraphQL::Language::Nodes::Field selection when GraphQL::Language::Nodes::InlineFragment node_flatten_selections(selection.selections) else [] end end end end end end graphql-client-0.18.0/lib/graphql/client/definition_variables.rb0000644000004100000410000000603214240205174024731 0ustar www-datawww-data# frozen_string_literal: true require "graphql" module GraphQL class Client # Internal: Detect variables used in a definition. module DefinitionVariables # Internal: Detect all variables used in a given operation or fragment # definition. # # schema - A GraphQL::Schema # document - A GraphQL::Language::Nodes::Document to scan # definition_name - A String definition name. Defaults to anonymous definition. # # Returns a Hash[Symbol] to GraphQL::Type objects. def self.variables(schema, document, definition_name = nil) unless schema.is_a?(GraphQL::Schema) || (schema.is_a?(Class) && schema < GraphQL::Schema) raise TypeError, "expected schema to be a GraphQL::Schema, but was #{schema.class}" end unless document.is_a?(GraphQL::Language::Nodes::Document) raise TypeError, "expected document to be a GraphQL::Language::Nodes::Document, but was #{document.class}" end sliced_document = GraphQL::Language::DefinitionSlice.slice(document, definition_name) visitor = GraphQL::Language::Visitor.new(sliced_document) type_stack = GraphQL::StaticValidation::TypeStack.new(schema, visitor) variables = {} visitor[GraphQL::Language::Nodes::VariableIdentifier] << ->(node, parent) do if definition = type_stack.argument_definitions.last existing_type = variables[node.name.to_sym] if existing_type && existing_type.unwrap != definition.type.unwrap raise GraphQL::Client::ValidationError, "$#{node.name} was already declared as #{existing_type.unwrap}, but was #{definition.type.unwrap}" elsif !(existing_type && existing_type.kind.non_null?) variables[node.name.to_sym] = definition.type end end end visitor.visit variables end # Internal: Detect all variables used in a given operation or fragment # definition. # # schema - A GraphQL::Schema # document - A GraphQL::Language::Nodes::Document to scan # definition_name - A String definition name. Defaults to anonymous definition. # # Returns a Hash[Symbol] to VariableDefinition objects. def self.operation_variables(schema, document, definition_name = nil) variables(schema, document, definition_name).map { |name, type| GraphQL::Language::Nodes::VariableDefinition.new(name: name.to_s, type: variable_node(type)) } end # Internal: Get AST node for GraphQL type. # # type - A GraphQL::Type # # Returns GraphQL::Language::Nodes::Type. def self.variable_node(type) case type.kind.name when "NON_NULL" GraphQL::Language::Nodes::NonNullType.new(of_type: variable_node(type.of_type)) when "LIST" GraphQL::Language::Nodes::ListType.new(of_type: variable_node(type.of_type)) else GraphQL::Language::Nodes::TypeName.new(name: type.graphql_name) end end end end end graphql-client-0.18.0/lib/graphql/client.rb0000644000004100000410000003476114240205174020563 0ustar www-datawww-data# frozen_string_literal: true require "active_support/inflector" require "active_support/notifications" require "graphql" require "graphql/client/collocated_enforcement" require "graphql/client/definition_variables" require "graphql/client/definition" require "graphql/client/error" require "graphql/client/errors" require "graphql/client/fragment_definition" require "graphql/client/operation_definition" require "graphql/client/query_typename" require "graphql/client/response" require "graphql/client/schema" require "json" require "delegate" module GraphQL # GraphQL Client helps build and execute queries against a GraphQL backend. # # A client instance SHOULD be configured with a schema to enable query # validation. And SHOULD also be configured with a backend "execute" adapter # to point at a remote GraphQL HTTP service or execute directly against a # Schema object. class Client class DynamicQueryError < Error; end class NotImplementedError < Error; end class ValidationError < Error; end extend CollocatedEnforcement attr_reader :schema, :execute attr_reader :types attr_accessor :document_tracking_enabled # Public: Check if collocated caller enforcement is enabled. attr_reader :enforce_collocated_callers # Deprecated: Allow dynamically generated queries to be passed to # Client#query. # # This ability will eventually be removed in future versions. attr_accessor :allow_dynamic_queries def self.load_schema(schema) case schema when GraphQL::Schema, Class schema when Hash GraphQL::Schema.from_introspection(schema) when String if schema.end_with?(".json") && File.exist?(schema) load_schema(File.read(schema)) elsif schema =~ /\A\s*{/ load_schema(JSON.parse(schema, freeze: true)) end else if schema.respond_to?(:execute) load_schema(dump_schema(schema)) elsif schema.respond_to?(:to_h) load_schema(schema.to_h) else nil end end end IntrospectionDocument = GraphQL.parse(GraphQL::Introspection::INTROSPECTION_QUERY) def self.dump_schema(schema, io = nil, context: {}) unless schema.respond_to?(:execute) raise TypeError, "expected schema to respond to #execute(), but was #{schema.class}" end result = schema.execute( document: IntrospectionDocument, operation_name: "IntrospectionQuery", variables: {}, context: context ).to_h if io io = File.open(io, "w") if io.is_a?(String) io.write(JSON.pretty_generate(result)) io.close_write end result end def initialize(schema:, execute: nil, enforce_collocated_callers: false) @schema = self.class.load_schema(schema) @execute = execute @document = GraphQL::Language::Nodes::Document.new(definitions: []) @document_tracking_enabled = false @allow_dynamic_queries = false @enforce_collocated_callers = enforce_collocated_callers if schema.is_a?(Class) @possible_types = schema.possible_types end @types = Schema.generate(@schema) end # A cache of the schema's merged possible types # @param type_condition [Class, String] a type definition or type name def possible_types(type_condition = nil) if type_condition if defined?(@possible_types) if type_condition.respond_to?(:graphql_name) type_condition = type_condition.graphql_name end @possible_types[type_condition] else @schema.possible_types(type_condition) end elsif defined?(@possible_types) @possible_types else @schema.possible_types(type_condition) end end def parse(str, filename = nil, lineno = nil) if filename.nil? && lineno.nil? location = caller_locations(1, 1).first filename = location.path lineno = location.lineno end unless filename.is_a?(String) raise TypeError, "expected filename to be a String, but was #{filename.class}" end unless lineno.is_a?(Integer) raise TypeError, "expected lineno to be a Integer, but was #{lineno.class}" end source_location = [filename, lineno].freeze definition_dependencies = Set.new # Replace Ruby constant reference with GraphQL fragment names, # while populating `definition_dependencies` with # GraphQL Fragment ASTs which this operation depends on str = str.gsub(/\.\.\.([a-zA-Z0-9_]+(::[a-zA-Z0-9_]+)*)/) do match = Regexp.last_match const_name = match[1] if str.match(/fragment\s*#{const_name}/) # It's a fragment _definition_, not a fragment usage match[0] else # It's a fragment spread, so we should load the fragment # which corresponds to the spread. # We depend on ActiveSupport to either find the already-loaded # constant, or to load the constant by name fragment = ActiveSupport::Inflector.safe_constantize(const_name) case fragment when FragmentDefinition # We found the fragment definition that this fragment spread belongs to. # So, register the AST of this fragment in `definition_dependencies` # and update the query string to valid GraphQL syntax, # replacing the Ruby constant definition_dependencies.merge(fragment.document.definitions) "...#{fragment.definition_name}" else if fragment message = "expected #{const_name} to be a #{FragmentDefinition}, but was a #{fragment.class}." if fragment.is_a?(Module) && fragment.constants.any? message += " Did you mean #{fragment}::#{fragment.constants.first}?" end else message = "uninitialized constant #{const_name}" end error = ValidationError.new(message) error.set_backtrace(["#{filename}:#{lineno + match.pre_match.count("\n") + 1}"] + caller) raise error end end end doc = GraphQL.parse(str) document_types = DocumentTypes.analyze_types(self.schema, doc).freeze doc = QueryTypename.insert_typename_fields(doc, types: document_types) doc.definitions.each do |node| if node.name.nil? node_with_name = node.merge(name: "__anonymous__") doc = doc.replace_child(node, node_with_name) end end document_dependencies = Language::Nodes::Document.new(definitions: doc.definitions + definition_dependencies.to_a) rules = GraphQL::StaticValidation::ALL_RULES - [ GraphQL::StaticValidation::FragmentsAreUsed, GraphQL::StaticValidation::FieldsHaveAppropriateSelections ] validator = GraphQL::StaticValidation::Validator.new(schema: self.schema, rules: rules) query = GraphQL::Query.new(self.schema, document: document_dependencies) errors = validator.validate(query) errors.fetch(:errors).each do |error| error_hash = error.to_h validation_line = error_hash["locations"][0]["line"] error = ValidationError.new(error_hash["message"]) error.set_backtrace(["#{filename}:#{lineno + validation_line}"] + caller) raise error end definitions = sliced_definitions(document_dependencies, doc, source_location: source_location) visitor = RenameNodeVisitor.new(document_dependencies, definitions: definitions) visitor.visit if document_tracking_enabled @document = @document.merge(definitions: @document.definitions + doc.definitions) end if definitions["__anonymous__"] definitions["__anonymous__"] else Module.new do definitions.each do |name, definition| const_set(name, definition) end end end end class RenameNodeVisitor < GraphQL::Language::Visitor def initialize(document, definitions:) super(document) @definitions = definitions end def on_fragment_definition(node, _parent) rename_node(node) super end def on_operation_definition(node, _parent) rename_node(node) super end def on_fragment_spread(node, _parent) rename_node(node) super end private def rename_node(node) definition = @definitions[node.name] if definition node.extend(LazyName) node._definition = definition end end end # Public: A wrapper to use the more-efficient `.get_type` when it's available from GraphQL-Ruby (1.10+) def get_type(type_name) @schema.get_type(type_name) end # Public: Create operation definition from a fragment definition. # # Automatically determines operation variable set. # # Examples # # FooFragment = Client.parse <<-'GRAPHQL' # fragment on Mutation { # updateFoo(id: $id, content: $content) # } # GRAPHQL # # # mutation($id: ID!, $content: String!) { # # updateFoo(id: $id, content: $content) # # } # FooMutation = Client.create_operation(FooFragment) # # fragment - A FragmentDefinition definition. # # Returns an OperationDefinition. def create_operation(fragment, filename = nil, lineno = nil) unless fragment.is_a?(GraphQL::Client::FragmentDefinition) raise TypeError, "expected fragment to be a GraphQL::Client::FragmentDefinition, but was #{fragment.class}" end if filename.nil? && lineno.nil? location = caller_locations(1, 1).first filename = location.path lineno = location.lineno end variables = GraphQL::Client::DefinitionVariables.operation_variables(self.schema, fragment.document, fragment.definition_name) type_name = fragment.definition_node.type.name if schema.query && type_name == schema.query.graphql_name operation_type = "query" elsif schema.mutation && type_name == schema.mutation.graphql_name operation_type = "mutation" elsif schema.subscription && type_name == schema.subscription.graphql_name operation_type = "subscription" else types = [schema.query, schema.mutation, schema.subscription].compact raise Error, "Fragment must be defined on #{types.map(&:graphql_name).join(", ")}" end doc_ast = GraphQL::Language::Nodes::Document.new(definitions: [ GraphQL::Language::Nodes::OperationDefinition.new( operation_type: operation_type, variables: variables, selections: [ GraphQL::Language::Nodes::FragmentSpread.new(name: fragment.name) ] ) ]) parse(doc_ast.to_query_string, filename, lineno) end attr_reader :document def query(definition, variables: {}, context: {}) raise NotImplementedError, "client network execution not configured" unless execute unless definition.is_a?(OperationDefinition) raise TypeError, "expected definition to be a #{OperationDefinition.name} but was #{document.class.name}" end if allow_dynamic_queries == false && definition.name.nil? raise DynamicQueryError, "expected definition to be assigned to a static constant https://git.io/vXXSE" end variables = deep_stringify_keys(variables) document = definition.document operation = definition.definition_node payload = { document: document, operation_name: operation.name, operation_type: operation.operation_type, variables: variables, context: context } result = ActiveSupport::Notifications.instrument("query.graphql", payload) do execute.execute( document: document, operation_name: operation.name, variables: variables, context: context ) end deep_freeze_json_object(result) data, errors, extensions = result.values_at("data", "errors", "extensions") errors ||= [] errors = errors.map(&:dup) GraphQL::Client::Errors.normalize_error_paths(data, errors) errors.each do |error| error_payload = payload.merge(message: error["message"], error: error) ActiveSupport::Notifications.instrument("error.graphql", error_payload) end Response.new( result, data: definition.new(data, Errors.new(errors, ["data"])), errors: Errors.new(errors), extensions: extensions ) end # Internal: FragmentSpread and FragmentDefinition extension to allow its # name to point to a lazily defined Proc instead of a static string. module LazyName def name @_definition.definition_name end attr_writer :_definition end private def sliced_definitions(document_dependencies, doc, source_location:) dependencies = document_dependencies.definitions.map do |node| [node.name, find_definition_dependencies(node)] end.to_h doc.definitions.map do |node| deps = Set.new definitions = document_dependencies.definitions.map { |x| [x.name, x] }.to_h queue = [node.name] while name = queue.shift next if deps.include?(name) deps.add(name) queue.concat dependencies[name] end definitions = document_dependencies.definitions.select { |x| deps.include?(x.name) } sliced_document = Language::Nodes::Document.new(definitions: definitions) definition = Definition.for( client: self, ast_node: node, document: sliced_document, source_document: doc, source_location: source_location ) [node.name, definition] end.to_h end def find_definition_dependencies(node) names = [] visitor = Language::Visitor.new(node) visitor[Language::Nodes::FragmentSpread] << -> (node, parent) { names << node.name } visitor.visit names.uniq end def deep_freeze_json_object(obj) case obj when String obj.freeze when Array obj.each { |v| deep_freeze_json_object(v) } obj.freeze when Hash obj.each { |k, v| k.freeze; deep_freeze_json_object(v) } obj.freeze end end def deep_stringify_keys(obj) case obj when Hash obj.each_with_object({}) do |(k, v), h| h[k.to_s] = deep_stringify_keys(v) end else obj end end end end