graphql-client 0.0.19 → 0.0.20

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
  SHA1:
3
- metadata.gz: c087161bf28d989bf274ba68bf76223ff4cc7b84
4
- data.tar.gz: 3054f8552e6e6a532ebd09a15e7f5914b8dfa994
3
+ metadata.gz: aeedc712dec17a0235ba00379f0a47b115d7b813
4
+ data.tar.gz: e143f70cb64be0f0fbddeb63d491db75293ef7e0
5
5
  SHA512:
6
- metadata.gz: 4044f2bf520a94bcaf5f6a918baf4daa8eb72479f7fd9de12bacfe58f40a362f57d675207ef0a25d9508113ccc3a0985c2095fa4534385ab4d33b08db2592815
7
- data.tar.gz: 8d19a14603c36df798b2280432d54e5dceb98d31a1d949c10bd2ac349b82dcf559359253e10b51a3d655f3e3cf22505f1b4ea97ff4a9acdf8e1992e91c462f9b
6
+ metadata.gz: f23928689fa9b1adde4574ef9ac26a60aa2919ca8122593f0425a27f9d6808883d18d8cc4cfc270c40921c88863d43fa5ce87a0565d2dfcda7df3a6c4c7d69f9
7
+ data.tar.gz: 5b5bdfcaf4b76a610448cdd80d1b3e391b97ee71f0f589bcc529fb6bda8df9f658e948f4eb122d7699f90755c4c9da155394d21275b6101bfbae23cb2faa845a
@@ -3,16 +3,21 @@ require "active_support/notifications"
3
3
  require "graphql"
4
4
  require "graphql/client/error"
5
5
  require "graphql/client/query_result"
6
- require "graphql/client/query"
7
6
  require "graphql/client/response"
8
7
  require "graphql/language/nodes/deep_freeze_ext"
9
8
  require "graphql/language/operation_slice"
10
9
 
11
10
  module GraphQL
11
+ # GraphQL Client helps build and execute queries against a GraphQL backend.
12
+ #
13
+ # A client instance SHOULD be configured with a schema to enable query
14
+ # validation. And SHOULD also be configured with a backend "execute" adapter
15
+ # to point at a remote GraphQL HTTP service or execute directly against a
16
+ # Schema object.
12
17
  class Client
13
18
  class ValidationError < Error; end
14
19
 
15
- attr_reader :schema, :fetch
20
+ attr_reader :schema, :execute
16
21
 
17
22
  attr_accessor :document_tracking_enabled
18
23
 
@@ -31,13 +36,18 @@ module GraphQL
31
36
  end
32
37
  end
33
38
 
34
- def initialize(schema: nil, fetch: nil)
39
+ def initialize(schema: nil, execute: nil)
35
40
  @schema = self.class.load_schema(schema)
36
- @fetch = fetch
41
+ @execute = execute
37
42
  @document = GraphQL::Language::Nodes::Document.new(definitions: [])
38
43
  @document_tracking_enabled = false
39
44
  end
40
45
 
46
+ # Definitions are constructed by Client.parse and wrap a parsed AST of the
47
+ # query string as well as hold references to any external query definition
48
+ # dependencies.
49
+ #
50
+ # Definitions MUST be assigned to a constant.
41
51
  class Definition < Module
42
52
  def self.for(node:, **kargs)
43
53
  case node
@@ -86,21 +96,23 @@ module GraphQL
86
96
  attr_reader :document
87
97
 
88
98
  def new(*args)
89
- query_result_class.new(*args)
99
+ type.new(*args)
90
100
  end
91
101
 
92
- private
93
-
94
- def query_result_class
95
- @query_result_class ||= GraphQL::Client::QueryResult.wrap(definition_node, name: name)
102
+ def type
103
+ # TODO: Fix type indirection
104
+ @type ||= GraphQL::Client::QueryResult.wrap(definition_node, name: "#{name}.type")
96
105
  end
97
106
  end
98
107
 
108
+ # Specific operation definition subtype for queries, mutations or
109
+ # subscriptions.
99
110
  class OperationDefinition < Definition
100
111
  # Public: Alias for definition name.
101
112
  alias operation_name definition_name
102
113
  end
103
114
 
115
+ # Specific fragment definition subtype.
104
116
  class FragmentDefinition < Definition
105
117
  end
106
118
 
@@ -120,16 +132,16 @@ module GraphQL
120
132
  definition_dependencies.merge(fragment.document.definitions)
121
133
  "...#{fragment.definition_name}"
122
134
  else
123
- if fragment
124
- error = TypeError.new("expected #{const_name} to be a #{FragmentDefinition}, but was a #{fragment.class}")
125
- else
126
- error = NameError.new("uninitialized constant #{const_name}")
127
- end
135
+ message = if fragment
136
+ "expected #{const_name} to be a #{FragmentDefinition}, but was a #{fragment.class}"
137
+ else
138
+ "uninitialized constant #{const_name}"
139
+ end
128
140
 
141
+ error = ValidationError.new(message)
129
142
  if filename && lineno
130
143
  error.set_backtrace(["#{filename}:#{lineno + match.pre_match.count("\n") + 1}"] + caller)
131
144
  end
132
-
133
145
  raise error
134
146
  end
135
147
  end
@@ -195,47 +207,48 @@ module GraphQL
195
207
  attr_reader :document
196
208
 
197
209
  def query(definition, variables: {}, context: {})
198
- raise Error, "client network fetching not configured" unless fetch
210
+ raise Error, "client network execution not configured" unless execute
199
211
 
200
- query = Query.new(definition.document,
201
- operation_name: definition.operation_name,
202
- variables: variables,
203
- context: context)
204
-
205
- result = ActiveSupport::Notifications.instrument("query.graphql", query.payload) do
206
- fetch.call(query)
212
+ unless definition.is_a?(OperationDefinition)
213
+ raise TypeError, "expected definition to be a #{OperationDefinition.name} but was #{document.class.name}"
207
214
  end
208
215
 
209
- data, errors, extensions = result.values_at("data", "errors", "extensions")
210
-
211
- if data && errors
212
- PartialResponse.new(
213
- data: definition.new(data),
214
- errors: ResponseErrors.new(definition, errors),
215
- extensions: extensions
216
+ document = definition.document
217
+ operation = definition.definition_node
218
+
219
+ payload = {
220
+ document: document,
221
+ operation_name: operation.name,
222
+ operation_type: operation.operation_type,
223
+ variables: variables,
224
+ context: context
225
+ }
226
+
227
+ result = ActiveSupport::Notifications.instrument("query.graphql", payload) do
228
+ execute.execute(
229
+ document: document,
230
+ operation_name: operation.name,
231
+ variables: variables,
232
+ context: context
216
233
  )
217
- elsif data && !errors
218
- SuccessfulResponse.new(
219
- data: definition.new(data),
220
- extensions: extensions
221
- )
222
- elsif !data && errors
223
- FailedResponse.new(
224
- errors: ResponseErrors.new(definition, errors),
225
- extensions: extensions
226
- )
227
- else
228
- raise Error, "invalid GraphQL response, expected data or errors"
229
234
  end
235
+
236
+ Response.for(definition, result)
230
237
  end
231
238
 
232
239
  IntrospectionDocument = GraphQL.parse(GraphQL::Introspection::INTROSPECTION_QUERY).deep_freeze
233
- IntrospectionQuery = Query.new(IntrospectionDocument)
234
240
 
235
241
  def fetch_schema
236
- fetch.call(IntrospectionQuery)
242
+ execute.execute(
243
+ document: IntrospectionDocument,
244
+ operation_name: "IntrospectionQuery",
245
+ variables: {},
246
+ context: {}
247
+ )
237
248
  end
238
249
 
250
+ # Internal: FragmentSpread and FragmentDefinition extension to allow its
251
+ # name to point to a lazily defined Proc instead of a static string.
239
252
  module LazyName
240
253
  def name
241
254
  @name.call
@@ -1,5 +1,7 @@
1
1
  module GraphQL
2
2
  class Client
3
- class Error < StandardError; end
3
+ # Public: Abstract base class for all errors raised by GraphQL::Client.
4
+ class Error < StandardError
5
+ end
4
6
  end
5
7
  end
@@ -7,7 +7,7 @@ module GraphQL
7
7
  # Public: Basic HTTP network adapter.
8
8
  #
9
9
  # GraphQL::Client::Client.new(
10
- # fetch: GraphQL::Client::HTTP.new("http://graphql-swapi.parseapp.com/")
10
+ # execute: GraphQL::Client::HTTP.new("http://graphql-swapi.parseapp.com/")
11
11
  # )
12
12
  #
13
13
  # Assumes GraphQL endpoint follows the express-graphql endpoint conventions.
@@ -20,7 +20,7 @@ module GraphQL
20
20
  # Public: Create HTTP adapter instance for a single GraphQL endpoint.
21
21
  #
22
22
  # GraphQL::Client::HTTP.new("http://graphql-swapi.parseapp.com/") do
23
- # def headers(query)
23
+ # def headers(context)
24
24
  # { "User-Agent": "My Client" }
25
25
  # end
26
26
  # end
@@ -39,21 +39,22 @@ module GraphQL
39
39
 
40
40
  # Public: Extension point for subclasses to set custom request headers.
41
41
  #
42
- # query - The GraphQL::Client::Query being sent
43
- #
44
42
  # Returns Hash of String header names and values.
45
- def headers(_)
43
+ def headers(_context)
46
44
  {}
47
45
  end
48
46
 
49
47
  # Public: Make an HTTP request for GraphQL query.
50
48
  #
51
- # Implements Client's "fetch" adapter interface.
49
+ # Implements Client's "execute" adapter interface.
52
50
  #
53
- # query - The GraphQL::Client::Query being sent
51
+ # document - The Query GraphQL::Language::Nodes::Document
52
+ # operation_name - The String operation definition name
53
+ # variables - Hash of query variables
54
+ # context - An arbitrary Hash of values which you can access
54
55
  #
55
56
  # Returns { "data" => ... , "errors" => ... } Hash.
56
- def call(query)
57
+ def execute(document:, operation_name: nil, variables: {}, context: {})
57
58
  http = Net::HTTP.new(uri.host, uri.port)
58
59
  http.use_ssl = uri.scheme == "https"
59
60
 
@@ -61,12 +62,12 @@ module GraphQL
61
62
 
62
63
  request["Accept"] = "application/json"
63
64
  request["Content-Type"] = "application/json"
64
- headers(query).each { |name, value| request[name] = value }
65
+ headers(context).each { |name, value| request[name] = value }
65
66
 
66
67
  body = {}
67
- body["query"] = query.to_s
68
- body["variables"] = JSON.generate(query.variables) if query.variables.any?
69
- body["operationName"] = query.operation_name if query.operation_name
68
+ body["query"] = document.to_query_string
69
+ body["variables"] = JSON.generate(variables) if variables.any?
70
+ body["operationName"] = operation_name if operation_name
70
71
  request.body = JSON.generate(body)
71
72
 
72
73
  response = http.request(request)
@@ -6,8 +6,8 @@ module GraphQL
6
6
  #
7
7
  # Logs GraphQL queries to Rails logger.
8
8
  #
9
- # QUERY (123ms) UsersController::ShowQuery
10
- # MUTATION (456ms) UsersController::UpdateMutation
9
+ # UsersController::ShowQuery QUERY (123ms)
10
+ # UsersController::UpdateMutation MUTATION (456ms)
11
11
  #
12
12
  # Enable GraphQL Client query logging.
13
13
  #
@@ -16,13 +16,10 @@ module GraphQL
16
16
  #
17
17
  class LogSubscriber < ActiveSupport::LogSubscriber
18
18
  def query(event)
19
- # TODO: Colorize output
20
19
  info do
21
- [
22
- event.payload[:operation_type].upcase,
23
- "(#{event.duration.round(1)}ms)",
24
- event.payload[:operation_name].gsub("__", "::")
25
- ].join(" ")
20
+ name = event.payload[:operation_name].gsub("__", "::")
21
+ type = event.payload[:operation_type].upcase
22
+ color("#{name} #{type} (#{event.duration.round(1)}ms)", nil, true)
26
23
  end
27
24
 
28
25
  debug do
@@ -4,6 +4,12 @@ require "set"
4
4
 
5
5
  module GraphQL
6
6
  class Client
7
+ # A QueryResult struct wraps data returned from a GraphQL response.
8
+ #
9
+ # Wrapping the JSON-like Hash allows access with nice Ruby accessor methods
10
+ # rather than using `obj["key"]` access.
11
+ #
12
+ # Wrappers also limit field visibility to fragment definitions.
7
13
  class QueryResult
8
14
  # Internal: Get QueryResult class for result of query.
9
15
  #
@@ -16,7 +22,7 @@ module GraphQL
16
22
  when Language::Nodes::FragmentSpread
17
23
  when Language::Nodes::Field
18
24
  field_name = selection.alias || selection.name
19
- field_klass = selection.selections.any? ? wrap(selection, name: "#{name}.#{field_name}") : nil
25
+ field_klass = selection.selections.any? ? wrap(selection, name: "#{name}[:#{field_name}]") : nil
20
26
  fields[field_name] ? fields[field_name] |= field_klass : fields[field_name] = field_klass
21
27
  when Language::Nodes::InlineFragment
22
28
  wrap(selection, name: name).fields.each do |fragment_name, klass|
@@ -84,10 +90,12 @@ module GraphQL
84
90
 
85
91
  class << self
86
92
  attr_reader :source_node
87
- end
88
93
 
89
- class << self
90
94
  attr_reader :fields
95
+
96
+ def [](name)
97
+ fields[name]
98
+ end
91
99
  end
92
100
 
93
101
  def self.name
@@ -163,7 +171,14 @@ module GraphQL
163
171
  alias to_h data
164
172
 
165
173
  def inspect
166
- ivars = self.class.fields.keys.map { |sym| "#{sym}=#{instance_variable_get("@#{sym}").inspect}" }
174
+ ivars = self.class.fields.keys.map do |sym|
175
+ value = instance_variable_get("@#{sym}")
176
+ if value.is_a?(QueryResult)
177
+ "#{sym}=#<#{value.class.name}>"
178
+ else
179
+ "#{sym}=#{value.inspect}"
180
+ end
181
+ end
167
182
  buf = "#<#{self.class.name}"
168
183
  buf << " " << ivars.join(" ") if ivars.any?
169
184
  buf << ">"
@@ -4,6 +4,13 @@ require "rails/railtie"
4
4
 
5
5
  module GraphQL
6
6
  class Client
7
+ # Optional Rails configuration for GraphQL::Client.
8
+ #
9
+ # Simply require this file to activate in the application.
10
+ #
11
+ # # config/application.rb
12
+ # require "graphql/client/railtie"
13
+ #
7
14
  class Railtie < Rails::Railtie
8
15
  config.graphql = ActiveSupport::OrderedOptions.new
9
16
  config.graphql.client = GraphQL::Client.new
@@ -29,6 +36,10 @@ module GraphQL
29
36
  require "graphql/client/view_module"
30
37
 
31
38
  path = app.paths["app/views"].first
39
+
40
+ # TODO: Accessing config.graphql.client during the initialization
41
+ # process seems error prone. The application may reassign
42
+ # config.graphql.client after this block is executed.
32
43
  client = config.graphql.client
33
44
 
34
45
  config.watchable_dirs[path] = [:erb]
@@ -6,6 +6,33 @@ module GraphQL
6
6
  #
7
7
  # https://facebook.github.io/graphql/#sec-Response-Format
8
8
  class Response
9
+ # Internal: Initialize Response subclass.
10
+ def self.for(definition, result)
11
+ data, errors, extensions = result.values_at("data", "errors", "extensions")
12
+
13
+ if data && errors
14
+ PartialResponse.new(
15
+ data: definition.new(data),
16
+ errors: ResponseErrors.new(definition, errors),
17
+ extensions: extensions
18
+ )
19
+ elsif data && !errors
20
+ SuccessfulResponse.new(
21
+ data: definition.new(data),
22
+ extensions: extensions
23
+ )
24
+ elsif !data && errors
25
+ FailedResponse.new(
26
+ errors: ResponseErrors.new(definition, errors),
27
+ extensions: extensions
28
+ )
29
+ else
30
+ FailedResponse.new(
31
+ errors: ResponseErrors.new(definition, [{ "message" => "invalid GraphQL response" }])
32
+ )
33
+ end
34
+ end
35
+
9
36
  # Public: Hash of server specific extension metadata.
10
37
  attr_reader :extensions
11
38
 
@@ -15,6 +42,8 @@ module GraphQL
15
42
  end
16
43
  end
17
44
 
45
+ # Public: A successful response means the query executed without any errors
46
+ # and returned all the requested data.
18
47
  class SuccessfulResponse < Response
19
48
  # Public: Wrapped QueryResult of data returned from the server.
20
49
  #
@@ -30,12 +59,16 @@ module GraphQL
30
59
  end
31
60
  end
32
61
 
62
+ # Public: A partial response means the query executed with some errors but
63
+ # returned all non-nullable fields. PartialResponse is still considered a
64
+ # SuccessfulResponse as it returns data and the client may still proceed
65
+ # with its normal render flow.
33
66
  class PartialResponse < SuccessfulResponse
34
67
  # Public: Get partial failures from response.
35
68
  #
36
69
  # https://facebook.github.io/graphql/#sec-Errors
37
70
  #
38
- # Returns ResponseErrors collection object.
71
+ # Returns ResponseErrors collection object with zero or more errors.
39
72
  attr_reader :errors
40
73
 
41
74
  # Internal: Initialize PartialResponse.
@@ -45,12 +78,15 @@ module GraphQL
45
78
  end
46
79
  end
47
80
 
81
+ # Public: A failed response returns no data and at least one error message.
82
+ # Cases may likely be a query validation error, missing authorization,
83
+ # or internal server crash.
48
84
  class FailedResponse < Response
49
85
  # Public: Get errors from response.
50
86
  #
51
87
  # https://facebook.github.io/graphql/#sec-Errors
52
88
  #
53
- # Returns ResponseErrors collection object.
89
+ # Returns ResponseErrors collection object with one or more errors.
54
90
  attr_reader :errors
55
91
 
56
92
  # Internal: Initialize FailedResponse.
@@ -60,7 +96,16 @@ module GraphQL
60
96
  end
61
97
  end
62
98
 
99
+ # Public: An error received from the server on execution.
100
+ #
101
+ # Extends StandardError hierarchy so you may raise this instance.
102
+ #
103
+ # Examples
104
+ #
105
+ # raise response.errors.first
106
+ #
63
107
  class ResponseError < Error
108
+ # Internal: Initialize ResponseError.
64
109
  def initialize(definition, error)
65
110
  @request_definition = definition
66
111
  @locations = error["locations"]
@@ -68,11 +113,20 @@ module GraphQL
68
113
  end
69
114
  end
70
115
 
116
+ # Public: A collection of errors received from the server on execution.
117
+ #
118
+ # Extends StandardError hierarchy so you may raise this instance.
119
+ #
120
+ # Examples
121
+ #
122
+ # raise response.errors
123
+ #
71
124
  class ResponseErrors < Error
72
125
  include Enumerable
73
126
 
74
127
  attr_reader :errors
75
128
 
129
+ # Internal: Initialize ResponseErrors.
76
130
  def initialize(definition, errors)
77
131
  @request_definition = definition
78
132
  @errors = errors.map { |error| ResponseError.new(definition, error) }
@@ -4,6 +4,17 @@ require "graphql/client/erubis"
4
4
 
5
5
  module GraphQL
6
6
  class Client
7
+ # Allows a magic namespace to map to app/views/**/*.erb files to retrieve
8
+ # statically defined GraphQL definitions.
9
+ #
10
+ # # app/views/users/show.html.erb
11
+ # <%grapql
12
+ # fragment UserFragment on User { }
13
+ # %>
14
+ #
15
+ # # Loads graphql section from app/views/users/show.html.erb
16
+ # Views::Users::Show::UserFragment
17
+ #
7
18
  module ViewModule
8
19
  attr_accessor :client
9
20
 
@@ -3,6 +3,7 @@ require "graphql"
3
3
  module GraphQL
4
4
  module Language
5
5
  module Nodes
6
+ # :nodoc:
6
7
  class AbstractNode
7
8
  # Public: Freeze entire Node tree
8
9
  #
@@ -2,6 +2,8 @@ require "graphql"
2
2
 
3
3
  module GraphQL
4
4
  module Language
5
+ # Public: Document transformations to return a minimal document to represent
6
+ # operation.
5
7
  module OperationSlice
6
8
  # Public: Return's minimal document to represent operation.
7
9
  #
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.19
4
+ version: 0.0.20
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub
@@ -118,7 +118,6 @@ files:
118
118
  - lib/graphql/client/erubis.rb
119
119
  - lib/graphql/client/http.rb
120
120
  - lib/graphql/client/log_subscriber.rb
121
- - lib/graphql/client/query.rb
122
121
  - lib/graphql/client/query_result.rb
123
122
  - lib/graphql/client/railtie.rb
124
123
  - lib/graphql/client/response.rb
@@ -1,65 +0,0 @@
1
- module GraphQL
2
- class Client
3
- class Query
4
- # Internal: Construct Query.
5
- #
6
- # Avoid creating queries with this constructor, perfer using Client#query.
7
- #
8
- # document - A parsed GraphQL::Language::Nodes::Document of the query
9
- # operation_name - String operation to execute
10
- # variables - Hash of variables to execute with the operation
11
- # context - Hash of metadata to pass to network adapter
12
- def initialize(document, operation_name: nil, variables: {}, context: {})
13
- @document = document
14
- @operation_name = operation_name
15
- @variables = variables
16
- @context = context
17
- end
18
-
19
- # Public: A parsed GraphQL::Language::Nodes::Document of the query.
20
- attr_reader :document
21
-
22
- # Public: String name of operation to execute.
23
- attr_reader :operation_name
24
-
25
- # Public: Hash of variables to execute with the operation.
26
- attr_reader :variables
27
-
28
- # Public: Hash of contextual metadata.
29
- attr_reader :context
30
-
31
- # Public: Serialized query string
32
- #
33
- # Returns String.
34
- def to_s
35
- document.to_query_string
36
- end
37
-
38
- # Public: Get operation definition node.
39
- #
40
- # Returns GraphQL::Language::Nodes::OperationDefinition.
41
- def operation
42
- document.definitions.find { |node| node.name == operation_name }
43
- end
44
-
45
- # Public: Query operation type
46
- #
47
- # Returns "query", "mutation" or "subscription".
48
- def operation_type
49
- operation.operation_type
50
- end
51
-
52
- # Internal: Payload object to pass to ActiveSupport::Notifications.
53
- #
54
- # Returns Hash.
55
- def payload
56
- {
57
- document: document,
58
- operation_name: operation_name,
59
- operation_type: operation_type,
60
- variables: variables
61
- }
62
- end
63
- end
64
- end
65
- end