graphql-client 0.16.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6810301cbdd84362dc4b8584e24deec3aec4248f4471734748cf1a33feaa5232
4
- data.tar.gz: bb68c48d70c276c0443da6620506b37dbe152e20fb931a2ecf3a0cba45050f96
3
+ metadata.gz: aab80b9e2051c30118b8db5fc9dbd813926d8a180bf49371bf23e6a09aaaf4eb
4
+ data.tar.gz: 20de5ca5f098763481ffa6bfa79308b1db5339376878f98c7d3ecb8d4442d5cd
5
5
  SHA512:
6
- metadata.gz: 6b7f114d4330ae5168669d7a663a0afdbd508ba056aa252a206efca9071f558c8749ecfd809ca0677ab9e3a5e95cc90eec3a6bcc2e4020875aab6621d9059dd8
7
- data.tar.gz: b1a24192dc4ee004ae6d546d4d724d0c49da4a17a4fb86a1a7e81d1aefbe16618f32ed27a63a8c2297bd4074ec8dd91794ab057d3826d08ccfefe21d689e7e76
6
+ metadata.gz: 81eb7e3021e3cdf8201e86971ed7d63f58d60e132d22b7c4ee35fd6b0a42fa730f4a9c9dd6638027f583d98638d270ef1d7994f0b5976ad4200a8c4164f58864
7
+ data.tar.gz: 7a752365bd27b213506bf6958c8accf9941f85bc8a7c73518950b431c1b7563584d29c0569f3a8a8a4c221e704c9ea3ff05333f005af004e60454b21f1acaaf9
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # graphql-client [![Gem Version](https://badge.fury.io/rb/graphql-client.svg)](https://badge.fury.io/rb/graphql-client) [![Build Status](https://travis-ci.org/github/graphql-client.svg?branch=master)](https://travis-ci.org/github/graphql-client)
1
+ # graphql-client [![Gem Version](https://badge.fury.io/rb/graphql-client.svg)](https://badge.fury.io/rb/graphql-client) [![CI](https://github.com/github-community-projects/graphql-client/workflows/CI/badge.svg)](https://github.com/github-community-projects/graphql-client/actions?query=workflow)
2
2
 
3
3
  GraphQL Client is a Ruby library for declaring, composing and executing GraphQL queries.
4
4
 
@@ -12,6 +12,8 @@ module GraphQL
12
12
 
13
13
  # Enforcements collocated object access best practices.
14
14
  module CollocatedEnforcement
15
+ extend self
16
+
15
17
  # Public: Ignore collocated caller enforcement for the scope of the block.
16
18
  def allow_noncollocated_callers
17
19
  Thread.current[:query_result_caller_location_ignore] = true
@@ -20,6 +22,23 @@ module GraphQL
20
22
  Thread.current[:query_result_caller_location_ignore] = nil
21
23
  end
22
24
 
25
+ def verify_collocated_path(location, path, method = "method")
26
+ return yield if Thread.current[:query_result_caller_location_ignore]
27
+
28
+ if (location.path != path) && !(WHITELISTED_GEM_NAMES.any? { |g| location.path.include?("gems/#{g}") })
29
+ error = NonCollocatedCallerError.new("#{method} was called outside of '#{path}' https://github.com/github-community-projects/graphql-client/blob/master/guides/collocated-call-sites.md")
30
+ error.set_backtrace(caller(2))
31
+ raise error
32
+ end
33
+
34
+ begin
35
+ Thread.current[:query_result_caller_location_ignore] = true
36
+ yield
37
+ ensure
38
+ Thread.current[:query_result_caller_location_ignore] = nil
39
+ end
40
+ end
41
+
23
42
  # Internal: Decorate method with collocated caller enforcement.
24
43
  #
25
44
  # mod - Target Module/Class
@@ -31,21 +50,9 @@ module GraphQL
31
50
  mod.prepend(Module.new do
32
51
  methods.each do |method|
33
52
  define_method(method) do |*args, &block|
34
- return super(*args, &block) if Thread.current[:query_result_caller_location_ignore]
35
-
36
- locations = caller_locations(1, 1)
37
-
38
- if (locations.first.path != path) && !(caller_locations.any? { |cl| WHITELISTED_GEM_NAMES.any? { |g| cl.path.include?("gems/#{g}") } })
39
- error = NonCollocatedCallerError.new("#{method} was called outside of '#{path}' https://git.io/v1syX")
40
- error.set_backtrace(caller(1))
41
- raise error
42
- end
43
-
44
- begin
45
- Thread.current[:query_result_caller_location_ignore] = true
53
+ location = caller_locations(1, 1)[0]
54
+ CollocatedEnforcement.verify_collocated_path(location, path, method) do
46
55
  super(*args, &block)
47
- ensure
48
- Thread.current[:query_result_caller_location_ignore] = nil
49
56
  end
50
57
  end
51
58
  end
@@ -115,9 +115,24 @@ module GraphQL
115
115
  else
116
116
  cast_object(obj)
117
117
  end
118
+ when GraphQL::Client::Schema::ObjectType::WithDefinition
119
+ case obj
120
+ when schema_class.klass
121
+ if obj._definer == schema_class
122
+ obj
123
+ else
124
+ cast_object(obj)
125
+ end
126
+ when nil
127
+ nil
128
+ when Hash
129
+ schema_class.new(obj, errors)
130
+ else
131
+ cast_object(obj)
132
+ end
118
133
  when GraphQL::Client::Schema::ObjectType
119
134
  case obj
120
- when NilClass, schema_class
135
+ when nil, schema_class
121
136
  obj
122
137
  when Hash
123
138
  schema_class.new(obj, errors)
@@ -132,41 +147,58 @@ module GraphQL
132
147
  # Internal: Nodes AST indexes.
133
148
  def indexes
134
149
  @indexes ||= begin
135
- visitor = GraphQL::Language::Visitor.new(document)
136
- definitions = index_node_definitions(visitor)
137
- spreads = index_spreads(visitor)
150
+ visitor = DefinitionVisitor.new(document)
138
151
  visitor.visit
139
- { definitions: definitions, spreads: spreads }
152
+ { definitions: visitor.definitions, spreads: visitor.spreads }
140
153
  end
141
154
  end
142
155
 
143
- private
156
+ class DefinitionVisitor < GraphQL::Language::Visitor
157
+ attr_reader :spreads, :definitions
144
158
 
145
- def cast_object(obj)
146
- if obj.class.is_a?(GraphQL::Client::Schema::ObjectType)
147
- unless obj.class._spreads.include?(definition_node.name)
148
- raise TypeError, "#{definition_node.name} is not included in #{obj.class.source_definition.name}"
149
- end
150
- schema_class.cast(obj.to_h, obj.errors)
151
- else
152
- raise TypeError, "unexpected #{obj.class}"
153
- end
159
+ def initialize(doc)
160
+ super
161
+ @spreads = {}
162
+ @definitions = {}
163
+ @current_definition = nil
154
164
  end
155
165
 
156
- EMPTY_SET = Set.new.freeze
166
+ def on_field(node, parent)
167
+ @definitions[node] = @current_definition
168
+ @spreads[node] = get_spreads(node)
169
+ super
170
+ end
157
171
 
158
- def index_spreads(visitor)
159
- spreads = {}
160
- on_node = ->(node, _parent) do
161
- node_spreads = flatten_spreads(node).map(&:name)
162
- spreads[node] = node_spreads.empty? ? EMPTY_SET : Set.new(node_spreads).freeze
163
- end
172
+ def on_fragment_definition(node, parent)
173
+ @current_definition = node
174
+ @definitions[node] = @current_definition
175
+ @spreads[node] = get_spreads(node)
176
+ super
177
+ ensure
178
+ @current_definition = nil
179
+ end
164
180
 
165
- visitor[GraphQL::Language::Nodes::Field] << on_node
166
- visitor[GraphQL::Language::Nodes::FragmentDefinition] << on_node
167
- visitor[GraphQL::Language::Nodes::OperationDefinition] << on_node
181
+ def on_operation_definition(node, parent)
182
+ @current_definition = node
183
+ @definitions[node] = @current_definition
184
+ @spreads[node] = get_spreads(node)
185
+ super
186
+ ensure
187
+ @current_definition = nil
188
+ end
168
189
 
169
- spreads
190
+ def on_inline_fragment(node, parent)
191
+ @definitions[node] = @current_definition
192
+ super
193
+ end
194
+
195
+ private
196
+
197
+ EMPTY_SET = Set.new.freeze
198
+
199
+ def get_spreads(node)
200
+ node_spreads = flatten_spreads(node).map(&:name)
201
+ node_spreads.empty? ? EMPTY_SET : Set.new(node_spreads).freeze
170
202
  end
171
203
 
172
204
  def flatten_spreads(node)
@@ -183,25 +215,20 @@ module GraphQL
183
215
  end
184
216
  spreads
185
217
  end
218
+ end
219
+
220
+ private
186
221
 
187
- def index_node_definitions(visitor)
188
- current_definition = nil
189
- enter_definition = ->(node, _parent) { current_definition = node }
190
- leave_definition = ->(node, _parent) { current_definition = nil }
191
-
192
- visitor[GraphQL::Language::Nodes::FragmentDefinition].enter << enter_definition
193
- visitor[GraphQL::Language::Nodes::FragmentDefinition].leave << leave_definition
194
- visitor[GraphQL::Language::Nodes::OperationDefinition].enter << enter_definition
195
- visitor[GraphQL::Language::Nodes::OperationDefinition].leave << leave_definition
196
-
197
- definitions = {}
198
- on_node = ->(node, _parent) { definitions[node] = current_definition }
199
- visitor[GraphQL::Language::Nodes::Field] << on_node
200
- visitor[GraphQL::Language::Nodes::FragmentDefinition] << on_node
201
- visitor[GraphQL::Language::Nodes::InlineFragment] << on_node
202
- visitor[GraphQL::Language::Nodes::OperationDefinition] << on_node
203
- definitions
222
+ def cast_object(obj)
223
+ if obj.class.is_a?(GraphQL::Client::Schema::ObjectType)
224
+ unless obj._spreads.include?(definition_node.name)
225
+ raise TypeError, "#{definition_node.name} is not included in #{obj.source_definition.name}"
226
+ end
227
+ schema_class.cast(obj.to_h, obj.errors)
228
+ else
229
+ raise TypeError, "unexpected #{obj.class}"
204
230
  end
231
+ end
205
232
  end
206
233
  end
207
234
  end
@@ -24,26 +24,33 @@ module GraphQL
24
24
 
25
25
  sliced_document = GraphQL::Language::DefinitionSlice.slice(document, definition_name)
26
26
 
27
- visitor = GraphQL::Language::Visitor.new(sliced_document)
28
- type_stack = GraphQL::StaticValidation::TypeStack.new(schema, visitor)
27
+ visitor = VariablesVisitor.new(sliced_document, schema: schema)
28
+ visitor.visit
29
+ visitor.variables
30
+ end
31
+
32
+ class VariablesVisitor < GraphQL::Language::Visitor
33
+ prepend GraphQL::Client::TypeStack
34
+
35
+ def initialize(*_args, **_kwargs)
36
+ super
37
+ @variables = {}
38
+ end
29
39
 
30
- variables = {}
40
+ attr_reader :variables
31
41
 
32
- visitor[GraphQL::Language::Nodes::VariableIdentifier] << ->(node, parent) do
33
- if definition = type_stack.argument_definitions.last
34
- existing_type = variables[node.name.to_sym]
42
+ def on_variable_identifier(node, parent)
43
+ if definition = @argument_definitions.last
44
+ existing_type = @variables[node.name.to_sym]
35
45
 
36
46
  if existing_type && existing_type.unwrap != definition.type.unwrap
37
47
  raise GraphQL::Client::ValidationError, "$#{node.name} was already declared as #{existing_type.unwrap}, but was #{definition.type.unwrap}"
38
48
  elsif !(existing_type && existing_type.kind.non_null?)
39
- variables[node.name.to_sym] = definition.type
49
+ @variables[node.name.to_sym] = definition.type
40
50
  end
41
51
  end
52
+ super
42
53
  end
43
-
44
- visitor.visit
45
-
46
- variables
47
54
  end
48
55
 
49
56
  # Internal: Detect all variables used in a given operation or fragment
@@ -1,10 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
  require "graphql"
3
+ require "graphql/client/type_stack"
3
4
 
4
5
  module GraphQL
5
6
  class Client
6
7
  # Internal: Use schema to detect definition and field types.
7
8
  module DocumentTypes
9
+ class AnalyzeTypesVisitor < GraphQL::Language::Visitor
10
+ prepend GraphQL::Client::TypeStack
11
+ attr_reader :fields
12
+
13
+ def initialize(*a, **kw)
14
+ @fields = {}
15
+ super
16
+ end
17
+
18
+ def on_operation_definition(node, _parent)
19
+ @fields[node] = @object_types.last
20
+ super
21
+ end
22
+
23
+ def on_fragment_definition(node, _parent)
24
+ @fields[node] = @object_types.last
25
+ super
26
+ end
27
+
28
+ def on_inline_fragment(node, _parent)
29
+ @fields[node] = @object_types.last
30
+ super
31
+ end
32
+
33
+ def on_field(node, _parent)
34
+ @fields[node] = @field_definitions.last.type
35
+ super
36
+ end
37
+ end
38
+
8
39
  # Internal: Detect all types used in a given document
9
40
  #
10
41
  # schema - A GraphQL::Schema
@@ -20,32 +51,15 @@ module GraphQL
20
51
  raise TypeError, "expected schema to be a GraphQL::Language::Nodes::Document, but was #{document.class}"
21
52
  end
22
53
 
23
- visitor = GraphQL::Language::Visitor.new(document)
24
- type_stack = GraphQL::StaticValidation::TypeStack.new(schema, visitor)
25
-
26
- fields = {}
27
-
28
- visitor[GraphQL::Language::Nodes::OperationDefinition] << ->(node, _parent) do
29
- fields[node] = type_stack.object_types.last
30
- end
31
- visitor[GraphQL::Language::Nodes::FragmentDefinition] << ->(node, _parent) do
32
- fields[node] = type_stack.object_types.last
33
- end
34
- visitor[GraphQL::Language::Nodes::InlineFragment] << ->(node, _parent) do
35
- fields[node] = type_stack.object_types.last
36
- end
37
- visitor[GraphQL::Language::Nodes::Field] << ->(node, _parent) do
38
- fields[node] = type_stack.field_definitions.last.type
39
- end
54
+ visitor = AnalyzeTypesVisitor.new(document, schema: schema)
40
55
  visitor.visit
41
-
42
- fields
56
+ visitor.fields
43
57
  rescue StandardError => err
44
58
  if err.is_a?(TypeError)
45
59
  raise
46
60
  end
47
61
  # FIXME: TypeStack my crash on invalid documents
48
- fields
62
+ visitor.fields
49
63
  end
50
64
  end
51
65
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require "action_view"
3
+ require "logger"
3
4
 
4
5
  module GraphQL
5
6
  class Client
@@ -21,7 +21,7 @@ module GraphQL
21
21
  errors.each do |error|
22
22
  path = ["data"]
23
23
  current = data
24
- error.fetch("path", []).each do |key|
24
+ error["path"].to_a.each do |key|
25
25
  break unless current
26
26
  path << key
27
27
  current = current[key]
@@ -7,14 +7,14 @@ module GraphQL
7
7
  class Client
8
8
  # Public: Basic HTTP network adapter.
9
9
  #
10
- # GraphQL::Client::Client.new(
10
+ # GraphQL::Client.new(
11
11
  # execute: GraphQL::Client::HTTP.new("http://graphql-swapi.parseapp.com/")
12
12
  # )
13
13
  #
14
14
  # Assumes GraphQL endpoint follows the express-graphql endpoint conventions.
15
15
  # https://github.com/graphql/express-graphql#http-usage
16
16
  #
17
- # Production applications should consider implementing there own network
17
+ # Production applications should consider implementing their own network
18
18
  # adapter. This class exists for trivial stock usage and allows for minimal
19
19
  # request header configuration.
20
20
  class HTTP
@@ -45,6 +45,11 @@ module GraphQL
45
45
  {}
46
46
  end
47
47
 
48
+ # Public: full reponse from last request
49
+ #
50
+ # Returns Hash.
51
+ attr_reader :last_response
52
+
48
53
  # Public: Make an HTTP request for GraphQL query.
49
54
  #
50
55
  # Implements Client's "execute" adapter interface.
@@ -71,6 +76,7 @@ module GraphQL
71
76
  request.body = JSON.generate(body)
72
77
 
73
78
  response = connection.request(request)
79
+ @last_response = response.to_hash
74
80
  case response
75
81
  when Net::HTTPOK, Net::HTTPBadRequest
76
82
  JSON.parse(response.body)
@@ -16,11 +16,18 @@ module GraphQL
16
16
  # GraphQL::Client::LogSubscriber.attach_to :graphql
17
17
  #
18
18
  class LogSubscriber < ActiveSupport::LogSubscriber
19
+ SHOULD_USE_KWARGS = private_instance_methods.include?(:mode_from)
20
+
19
21
  def query(event)
20
22
  logger.info do
21
23
  name = event.payload[:operation_name].gsub("__", "::")
22
24
  type = event.payload[:operation_type].upcase
23
- color("#{name} #{type} (#{event.duration.round(1)}ms)", nil, true)
25
+
26
+ if SHOULD_USE_KWARGS
27
+ color("#{name} #{type} (#{event.duration.round(1)}ms)", nil, bold: true)
28
+ else
29
+ color("#{name} #{type} (#{event.duration.round(1)}ms)", nil, true)
30
+ end
24
31
  end
25
32
 
26
33
  logger.debug do
@@ -32,7 +39,12 @@ module GraphQL
32
39
  logger.error do
33
40
  name = event.payload[:operation_name].gsub("__", "::")
34
41
  message = event.payload[:message]
35
- color("#{name} ERROR: #{message}", nil, true)
42
+
43
+ if SHOULD_USE_KWARGS
44
+ color("#{name} ERROR: #{message}", nil, bold: true)
45
+ else
46
+ color("#{name} ERROR: #{message}", nil, true)
47
+ end
36
48
  end
37
49
  end
38
50
  end
@@ -12,6 +12,7 @@ module GraphQL
12
12
  # Returns Hash.
13
13
  attr_reader :original_hash
14
14
  alias_method :to_h, :original_hash
15
+ alias_method :to_hash, :original_hash
15
16
 
16
17
  # Public: Wrapped ObjectType of data returned from the server.
17
18
  #
@@ -30,12 +31,18 @@ module GraphQL
30
31
  # Public: Hash of server specific extension metadata.
31
32
  attr_reader :extensions
32
33
 
34
+ # Public: Complete response hash returned from server.
35
+ #
36
+ # Returns Hash
37
+ attr_reader :full_response
38
+
33
39
  # Internal: Initialize base class.
34
- def initialize(hash, data: nil, errors: Errors.new, extensions: {})
40
+ def initialize(hash, data: nil, errors: Errors.new, extensions: {}, full_response: nil)
35
41
  @original_hash = hash
36
42
  @data = data
37
43
  @errors = errors
38
44
  @extensions = extensions
45
+ @full_response = full_response
39
46
  end
40
47
  end
41
48
  end
@@ -16,6 +16,10 @@ module GraphQL
16
16
  @enum = enum
17
17
  end
18
18
 
19
+ def unknown_enum_value?
20
+ false
21
+ end
22
+
19
23
  def respond_to_missing?(method_name, include_private = false)
20
24
  if method_name[-1] == "?" && @enum.include?(method_name[0..-2])
21
25
  true
@@ -37,6 +41,12 @@ module GraphQL
37
41
  end
38
42
  end
39
43
 
44
+ class UnexpectedEnumValue < String
45
+ def unknown_enum_value?
46
+ true
47
+ end
48
+ end
49
+
40
50
  # Internal: Construct enum wrapper from another GraphQL::EnumType.
41
51
  #
42
52
  # type - GraphQL::EnumType instance
@@ -78,8 +88,13 @@ module GraphQL
78
88
  def cast(value, _errors = nil)
79
89
  case value
80
90
  when String
81
- raise Error, "unexpected enum value #{value}" unless @values.key?(value)
82
- @values[value]
91
+ if @values.key?(value)
92
+ @values[value]
93
+ elsif schema_module.raise_on_unknown_enum_value
94
+ raise Error, "unexpected enum value #{value}"
95
+ else
96
+ UnexpectedEnumValue.new(value).freeze
97
+ end
83
98
  when NilClass
84
99
  value
85
100
  else
@@ -21,7 +21,7 @@ module GraphQL
21
21
  end
22
22
 
23
23
  def define_class(definition, ast_nodes)
24
- possible_type_names = definition.client.schema.possible_types(type).map(&:graphql_name)
24
+ possible_type_names = definition.client.possible_types(type).map(&:graphql_name)
25
25
  possible_types = possible_type_names.map { |concrete_type_name|
26
26
  schema_module.get_class(concrete_type_name).define_class(definition, ast_nodes)
27
27
  }