graphql-remote_loader 0.0.2 → 0.0.3

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: 994f881ffc8571f96fbe6c969cba79f030b6bbb6
4
- data.tar.gz: 4bdf0b71e51d6e9acc7611e46197615165cd33cb
3
+ metadata.gz: b4e6ea2888b09a78e64eeec68a7905e5932c0aa2
4
+ data.tar.gz: 93ff402aaff2686821e835ecf1200b894f00c61a
5
5
  SHA512:
6
- metadata.gz: 3e2caaa027c826d9a116c892792f65f915f850a8d324b118809806def027e1f3eecf6aa37748a6bac23b276bad54606519a80c9127121bc1ebeefb917c86677d
7
- data.tar.gz: 9225082887eb70fc5e42eea716df857b928ab0b6ba6bef9150d064785520005fa40c3bb0cfed373963e2634ba6c3903fcebf6af931f1982191e355a136f3ec88
6
+ metadata.gz: 103d053506f99d3b285122a1eef0bf5dbb6cd685978a0cd78a316644211eb97a6bd6ccc1a0e18374b922011e94a7e54278b03650e147aa47b765f048179a3a18
7
+ data.tar.gz: f22cc0abee65eaa738c640abff3dce91fa7fd17f58523d4abde0094f1000c4d7f7a05e80fe8608172a95dc51383f8db0ad28a469ff660199433c1449235cb1e7
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- graphql-remote_loader (0.0.1)
4
+ graphql-remote_loader (0.0.2)
5
5
  graphql (~> 1.6)
6
6
  graphql-batch (~> 0.3)
7
7
 
@@ -46,7 +46,7 @@ DEPENDENCIES
46
46
  graphql-remote_loader!
47
47
  pry-byebug (~> 3.4)
48
48
  rake (~> 10.0)
49
- rspec
49
+ rspec (~> 3.6)
50
50
 
51
51
  BUNDLED WITH
52
52
  1.15.4
data/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # GraphQL Remote Loader
2
+ Performant, batched GraphQL queries from within the resolvers of a [`graphql-ruby`](https://github.com/rmosolgo/graphql-ruby) API.
3
+
4
+ ## Example
5
+
6
+ ```ruby
7
+ field :repositoryUrl, !types.String do
8
+ description "The repository URL"
9
+
10
+ resolve ->(obj, args, ctx) do
11
+ query = <<-GQL
12
+ node(id: "#{obj.global_relay_id}"){
13
+ ... on Repository {
14
+ url
15
+ }
16
+ }
17
+ GQL
18
+
19
+ GitHubLoader.load(query).then do |results|
20
+ results["node"]["url"]
21
+ end
22
+ end
23
+ end
24
+ ```
25
+
26
+ ## Description
27
+ `graphql-remote_loader` allows for querying GraphQL APIs from within resolvers of a [`graphql-ruby`](https://github.com/rmosolgo/graphql-ruby) API.
28
+
29
+ This can be used to create GraphQL APIs that depend on data from other GraphQL APIs, either remote or local.
30
+
31
+ A promise-based resolution strategy from Shopify's [`graphql-batch`](https://github.com/Shopify/graphql-batch) is used to batch all requested data into a single GraphQL query. Promises are fulfilled with only the data they requested.
32
+
33
+ You can think of it as a lightweight version of schema-stitching.
34
+
35
+ ## How to use
36
+ First, you'll need to install the gem. Either do `gem install graphql-remote_loader` or add this to your Gemfile:
37
+
38
+ ```
39
+ gem "graphql-remote_loader"
40
+ ```
41
+
42
+ The gem provides a base loader `GraphQL::RemoteLoader::Loader` which does most of the heavy lifting. In order to remain client-agnostic, there's an unimplemented no-op that queries the external GraphQL API.
43
+
44
+ To use, create a new class that inherits from `GraphQL::RemoteLoader::Loader` and define `def query(query_string)`. The method takes a query String as input. The expected output is a response `Hash`, or some object that responds to `#to_h`.
45
+
46
+ Example:
47
+
48
+ ```ruby
49
+ require "graphql/remote_loader"
50
+
51
+ module MyApp
52
+ class GitHubLoader < GraphQL::RemoteLoader::Loader
53
+ def query(query_string)
54
+ parsed_query = MyApp::Client.parse(query_string)
55
+ MyApp.query(parsed_query)
56
+ end
57
+ end
58
+ end
59
+ ```
60
+
61
+ This example uses [`graphql-client`](https://github.com/github/graphql-client). Any client, or even just plain `cURL`/`HTTP` can be used.
62
+
63
+ ## Current State
64
+
65
+ This project is very much WIP. Some TODOs are listed in the issues. Bugs and feature requests should be added as issues.
@@ -19,6 +19,10 @@ module GraphQL
19
19
  self.for.load([query, prime])
20
20
  end
21
21
 
22
+ def self.reset_index
23
+ @index = nil
24
+ end
25
+
22
26
  # Given a query string, return a response JSON
23
27
  def query(query_string)
24
28
  raise NotImplementedError,
@@ -41,7 +45,7 @@ module GraphQL
41
45
  filtered_results = {}
42
46
 
43
47
  # Select field keys on the results hash
44
- fields = obj.keys.select { |k| k.match? /\Ap[0-9]+.*[^?]\z/ }
48
+ fields = obj.keys.select { |k| k.match /\Ap[0-9]+.*[^?]\z/ }
45
49
 
46
50
  # Filter methods that were not requested in this sub-query
47
51
  fields = fields.select do |field|
@@ -56,11 +60,19 @@ module GraphQL
56
60
  method_value = obj[method]
57
61
  filtered_value = filter_keys_on_response(method_value, prime)
58
62
 
59
- filtered_results[method_name.underscore] = filtered_value
63
+ filtered_results[underscore(method_name)] = filtered_value
60
64
  end
61
65
 
62
66
  filtered_results
63
67
  end
68
+
69
+ def underscore(str)
70
+ str.gsub(/::/, '/').
71
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
72
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
73
+ tr("-", "_").
74
+ downcase
75
+ end
64
76
  end
65
77
  end
66
78
  end
@@ -1,299 +1,127 @@
1
1
  module GraphQL
2
2
  module RemoteLoader
3
-
4
3
  # Given a list of queries and their prime UIDs, generate the merged and labeled
5
4
  # GraphQL query to be sent off to the remote backend.
6
5
  class QueryMerger
7
6
  class << self
8
7
  def merge(queries_and_primes)
9
- new_ast = []
10
-
11
- queries_and_primes.each do |query, prime|
12
- parsed_query = [parse(query)]
13
- attach_primes!(parsed_query, prime)
14
- merge_query(parsed_query, new_ast)
15
- end
16
-
17
- query_string_from_ast(new_ast)
18
- end
8
+ parsed_queries = queries_and_primes.map do |query, prime|
9
+ parsed_query = parse(query)
19
10
 
20
- private
21
-
22
- def merge_query(query, ast)
23
- query.each do |query_field|
24
- matching_field = ast.find do |ast_field|
25
- query_field[:field] == ast_field[:field] &&
26
- query_field[:arguments] == ast_field[:arguments]
11
+ parsed_query.definitions.each do |definition|
12
+ attach_primes!(definition.children, prime)
27
13
  end
28
14
 
29
- if matching_field
30
- matching_field[:primes] *= query_field[:primes]
31
- merge_query(query_field[:body], matching_field[:body])
32
- else
33
- ast << query_field
34
- end
15
+ parsed_query
35
16
  end
36
- end
37
17
 
38
- def attach_primes!(query, prime)
39
- query.each do |field|
40
- field[:primes] = prime
41
- attach_primes!(field[:body], prime)
42
- end
18
+ merge_parsed_queries(parsed_queries).to_query_string
43
19
  end
44
20
 
45
- def query_string_from_ast(ast)
46
- ast_node_strings = ast.map do |ast_node|
47
- query_string_for_node(ast_node)
48
- end.join(" ")
49
- ast_node_strings
50
- end
51
-
52
- def query_string_for_node(node)
53
- field = node[:field]
54
-
55
- # TODO: This won't work for fields named `query`. Fix once
56
- # I move to Roberts non-jank parser
57
- unless field.include?("...") || field == "query"
58
- # assign preix if not a spread
59
- field = "p#{node[:primes]}#{field}: #{field}"
60
- end
21
+ private
61
22
 
62
- args = node[:arguments].map do |arg, value|
63
- value = "\"#{value}\"" if value.is_a? String
23
+ def merge_parsed_queries(parsed_queries)
24
+ merged_query = parsed_queries.pop
64
25
 
65
- "#{arg}: #{value}"
66
- end
26
+ parsed_queries.each do |query|
27
+ merge_query_recursive(query.definitions[0], merged_query.definitions[0])
28
+ merge_fragment_definitions(query, merged_query)
67
29
 
68
- arg_string = unless args.empty?
69
- "(#{args.join(",")})"
70
- else
71
- ""
72
30
  end
73
31
 
74
- body_string = unless node[:body].empty?
75
- str = node[:body].map do |node|
76
- query_string_for_node(node)
77
- end
78
-
79
- "{ #{str.join(" ")} }"
32
+ merged_query.definitions.each do |definition|
33
+ apply_aliases!(definition.selections)
80
34
  end
81
35
 
82
- "#{field}#{arg_string} #{body_string}".strip
36
+ merged_query
83
37
  end
84
38
 
85
- def parse(query)
86
- tokenizer = QueryTokenizer.new("query { #{query} }")
87
- QueryAST.build(tokenizer)
88
- end
89
-
90
- class QueryAST
91
- class ParseException < Exception; end
92
-
93
- class << self
94
- def build(tokenizer)
95
- result = build_node(tokenizer)
39
+ # merges a_query's fragment definitions into b_query
40
+ def merge_fragment_definitions(a_query, b_query)
41
+ a_query.definitions[1..-1].each do |a_definition|
42
+ matching_fragment_definition = b_query.definitions.find do |b_definition|
43
+ a_definition.name == b_definition.name
96
44
  end
97
45
 
98
- def build_node(tokenizer)
99
- {
100
- field: get_field(tokenizer),
101
- arguments: get_args(tokenizer),
102
- body: build_body(tokenizer)
103
- }
104
- end
105
-
106
- def get_field(tokenizer)
107
- token = tokenizer.pop
108
-
109
- if token[:type] == :spread
110
- validate_token_type(tokenizer.pop, :on)
111
- spread_type = tokenizer.pop[:string]
112
-
113
- return "... on #{spread_type}"
114
- end
115
-
116
- validate_token_type(token, :field)
117
-
118
- throw_parse_exception(token, "field") unless (token[:type] == :field)
119
-
120
- token[:string]
46
+ if matching_fragment_definition
47
+ merge_query_recursive(a_definition, matching_fragment_definition)
48
+ else
49
+ b_query.definitions << a_definition
121
50
  end
51
+ end
52
+ end
122
53
 
123
- def get_args(tokenizer)
124
- args = {}
125
- return args unless (tokenizer.peek[:type] == :left_paren)
126
-
127
- # remove "("
128
- tokenizer.pop
129
-
130
- while true
131
- token = tokenizer.pop
132
- validate_token_type(token, :key)
133
-
134
- # Remove :
135
- key = token[:string].sub(":", "")
136
-
137
- token = tokenizer.pop
138
- value = case token[:type]
139
- when :string
140
- token[:string][1..-2] # remove ""
141
- when :int
142
- Integer(token[:string])
143
- when :float
144
- Float(token[:string])
145
- when :true
146
- true
147
- when :false
148
- false
149
- else
150
- raise ParseException, "Expected string, int, float, or boolean, got #{token[:string]}"
151
- end
152
-
153
- args[key.to_sym] = value
154
-
155
- if tokenizer.peek[:type] == :comma
156
- tokenizer.pop
157
- elsif tokenizer.peek[:type] == :right_paren
158
- tokenizer.pop
159
- break
160
- else
161
- raise ParseException, "Expected comma or right paren, got #{tokenizer[:string]}"
162
- end
54
+ # merges a_query into b_query
55
+ def merge_query_recursive(a_query, b_query)
56
+ exempt_node_types = [
57
+ GraphQL::Language::Nodes::InlineFragment,
58
+ GraphQL::Language::Nodes::FragmentSpread
59
+ ]
60
+
61
+ a_query.selections.each do |a_query_selection|
62
+ matching_field = b_query.selections.find do |b_query_selection|
63
+ same_name = a_query_selection.name == b_query_selection.name
64
+ same_args = if exempt_node_types.any? { |type| b_query_selection.is_a?(type) }
65
+ true
66
+ else
67
+ arguments_equal?(a_query_selection, b_query_selection)
163
68
  end
164
69
 
165
- args
70
+ same_name && same_args
166
71
  end
167
72
 
168
- def build_body(tokenizer)
169
- body = []
170
- return body unless (tokenizer.peek[:type] == :left_brace)
171
-
172
- # remove "{"
173
- tokenizer.pop
174
-
175
- while true
176
- body << build_node(tokenizer)
177
- break if (tokenizer.peek[:type] == :right_brace)
178
- end
179
-
180
- # remove "}"
181
- tokenizer.pop
182
-
183
- body
184
- end
73
+ if matching_field
74
+ new_prime = matching_field.instance_variable_get(:@prime) *
75
+ a_query_selection.instance_variable_get(:@prime)
185
76
 
186
- def validate_token_type(token, expected_type)
187
- unless token[:type] == expected_type
188
- raise ParseException, "Expected #{expected_type.to_s} token, got #{token[:string]}"
189
- end
77
+ matching_field.instance_variable_set(:@prime, new_prime)
78
+ merge_query_recursive(a_query_selection, matching_field) unless exempt_node_types.any? { |type| matching_field.is_a?(type) }
79
+ else
80
+ b_query.selections << a_query_selection
190
81
  end
191
82
  end
192
83
  end
193
84
 
194
- class QueryTokenizer
195
- def initialize(query)
196
- @query = query.chars
197
- set_next_token
198
- end
199
-
200
- # Get next token, without removing from stream
201
- def peek
202
- @next_token
203
- end
204
-
205
- # Pop next token
206
- def pop
207
- token = @next_token
208
- set_next_token if token
209
-
210
- token
211
- end
212
-
213
- private
214
-
215
- # Read @query to set @next_token
216
- def set_next_token
217
- next_token_string = extract_token_string_from_query
218
- @next_token = tokenize_string(next_token_string)
219
- end
220
-
221
- def extract_token_string_from_query
222
- trim_leading_whitespace
223
-
224
- token_string = ""
225
- while true
226
- next_char = @query.first || break
85
+ # Are two lists of arguments equal?
86
+ def arguments_equal?(a, b)
87
+ # Return true if both don't have args.
88
+ # Return false if only one doesn't have args
89
+ return true unless a.respond_to?(:arguments) && b.respond_to?(:arguments)
90
+ return false unless a.respond_to?(:arguments) || b.respond_to?(:arguments)
227
91
 
228
- if is_brace_or_bracket_or_comma?(next_char)
229
- if token_string.length == 0
230
- token_string << @query.shift
231
- end
232
-
233
- break
234
- else
235
- if !is_whitespace?(@query.first)
236
- token_string << @query.shift
237
- else
238
- break
239
- end
240
- end
241
- end
92
+ a.arguments.map { |arg| {name: arg.name, value: arg.value}.to_s }.sort ==
93
+ b.arguments.map { |arg| {name: arg.name, value: arg.value}.to_s }.sort
94
+ end
242
95
 
243
- token_string
96
+ def attach_primes!(query_fields, prime)
97
+ query_fields.each do |field|
98
+ field.instance_variable_set(:@prime, prime)
99
+ attach_primes!(field.children, prime)
244
100
  end
101
+ end
245
102
 
246
- def tokenize_string(next_token_string)
247
- token = case next_token_string
248
- when '{'
249
- :left_brace
250
- when '}'
251
- :right_brace
252
- when '('
253
- :left_paren
254
- when ')'
255
- :right_paren
256
- when ','
257
- :comma
258
- when /\A[[:digit:]]+\z/
259
- :int
260
- when /\A[[:digit:]]+\.[[:digit:]]+\z/
261
- :float
262
- when 'true'
263
- :true
264
- when 'false'
265
- :false
266
- when 'on'
267
- :on
268
- when /\A".*"\z/
269
- :string
270
- when /\A.+:\z/
271
- :key
272
- when ''
273
- next_token_string = "END OF STREAM"
274
- :empty
275
- when '...'
276
- :spread
277
- else
278
- :field
279
- end
280
-
281
- { type: token, string: next_token_string }
282
- end
103
+ def apply_aliases!(query_selections)
104
+ exempt_node_types = [
105
+ GraphQL::Language::Nodes::InlineFragment,
106
+ GraphQL::Language::Nodes::FragmentSpread
107
+ ]
283
108
 
284
- def trim_leading_whitespace
285
- while is_whitespace?(@query.first)
286
- @query.shift
109
+ query_selections.each do |selection|
110
+ unless exempt_node_types.any? { |type| selection.is_a? type }
111
+ prime_factor = selection.instance_variable_get(:@prime)
112
+ selection.alias = "p#{prime_factor}#{selection.name}"
287
113
  end
288
- end
289
114
 
290
- def is_whitespace?(char)
291
- char == "\n" || char == " "
115
+ # Some nodes don't have selections (e.g. fragment spreads)
116
+ apply_aliases!(selection.selections) if selection.respond_to? :selections
292
117
  end
118
+ end
293
119
 
294
- def is_brace_or_bracket_or_comma?(char)
295
- char == "{" || char == "}" || char == "(" || char == ")" || char == ","
296
- end
120
+ # Allows "foo" or "query { foo }"
121
+ def parse(query)
122
+ GraphQL.parse(query)
123
+ rescue GraphQL::ParseError
124
+ GraphQL.parse("query { #{query} }")
297
125
  end
298
126
  end
299
127
  end
@@ -1,5 +1,5 @@
1
1
  module GraphQL
2
2
  module RemoteLoader
3
- VERSION = "0.0.2"
3
+ VERSION = "0.0.3"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql-remote_loader
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathaniel Woodthorpe
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-06-09 00:00:00.000000000 Z
11
+ date: 2018-06-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -106,6 +106,7 @@ files:
106
106
  - ".gitignore"
107
107
  - Gemfile
108
108
  - Gemfile.lock
109
+ - README.md
109
110
  - graphql-remote_loader.gemspec
110
111
  - lib/graphql/remote_loader.rb
111
112
  - lib/graphql/remote_loader/loader.rb