graphql-client 0.0.19 → 0.0.20

Sign up to get free protection for your applications and to get access to all the features.
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