class GraphQL::Client
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.
Constants
- Erubis
Public: Extended
Erubis
implementation that supportsGraphQL
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
Public: Extended
Erubis
implementation that supportsGraphQL
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
- IntrospectionDocument
- WHITELISTED_GEM_NAMES
Collocation will not be enforced if a stack trace includes any of these gems.
Attributes
Deprecated: Allow dynamically generated queries to be passed to Client#query
.
This ability will eventually be removed in future versions.
Public: Check if collocated caller enforcement is enabled.
Public Class Methods
# File lib/graphql/client.rb, line 72 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 payload = { document: IntrospectionDocument, operation_name: "IntrospectionQuery", variables: {}, context: context } result = schema.execute(payload).to_h _, errors, __ = eval_query_result(payload, result) if errors.any? errors_detail = errors.map { |e| e["message"] }.join("; ") raise QueryError, "The query returned an error (#{errors_detail})" end if io io = File.open(io, "w") if io.is_a?(String) io.write(JSON.pretty_generate(result)) io.close_write end result end
# File lib/graphql/client.rb, line 47 def self.load_schema(schema) case schema when GraphQL::Schema, Class schema when Hash GraphQL::Schema::Loader.load(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)) 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
# File lib/graphql/client.rb, line 101 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 @types = Schema.generate(@schema) end
Private Class Methods
# File lib/graphql/client.rb, line 440 def self.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
# File lib/graphql/client.rb, line 424 def self.eval_query_result(payload, result) 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 [data, errors, extensions] end
Public Instance Methods
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
.
# File lib/graphql/client.rb, line 330 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
Public: A wrapper to use the more-efficient `.get_type` when it's available from GraphQL-Ruby (1.10+)
# File lib/graphql/client.rb, line 302 def get_type(type_name) if @schema.respond_to?(:get_type) @schema.get_type(type_name) else @schema.types[type_name] end end
# File lib/graphql/client.rb, line 112 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 begin fragment = ActiveSupport::Inflector.constantize(const_name) rescue NameError fragment = nil end 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? if node.respond_to?(:merge) # GraphQL 1.9 + node_with_name = node.merge(name: "__anonymous__") doc = doc.replace_child(node, node_with_name) else node.name = "__anonymous__" end 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 = {} doc.definitions.each do |node| sliced_document = Language::DefinitionSlice.slice(document_dependencies, node.name) definition = Definition.for( client: self, ast_node: node, document: sliced_document, source_document: doc, source_location: source_location ) definitions[node.name] = definition end if @document.respond_to?(:merge) # GraphQL 1.9+ visitor = RenameNodeVisitor.new(document_dependencies, definitions: definitions) visitor.visit else name_hook = RenameNodeHook.new(definitions) visitor = Language::Visitor.new(document_dependencies) visitor[Language::Nodes::FragmentDefinition].leave << name_hook.method(:rename_node) visitor[Language::Nodes::OperationDefinition].leave << name_hook.method(:rename_node) visitor[Language::Nodes::FragmentSpread].leave << name_hook.method(:rename_node) visitor.visit end if document_tracking_enabled if @document.respond_to?(:merge) # GraphQL 1.9+ @document = @document.merge(definitions: @document.definitions + doc.definitions) else @document.definitions.concat(doc.definitions) end end if definitions["__anonymous__"] definitions["__anonymous__"] else Module.new do definitions.each do |name, definition| const_set(name, definition) end end end end
# File lib/graphql/client.rb, line 369 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 data, errors, extensions = self.class.eval_query_result(payload, result) Response.new( result, data: definition.new(data, Errors.new(errors, ["data"])), errors: Errors.new(errors), extensions: extensions ) end
Private Instance Methods
# File lib/graphql/client.rb, line 453 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