graphql-remote_loader 0.0.5 → 1.0.0

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: cd7a27a207b0fccaa2e6329d51d3de738f333c09
4
- data.tar.gz: b90fd6caae8031cc12b98c4d3a64a846cf92466a
3
+ metadata.gz: 8f7f2b1ab4cf983200d1c98e42324a2965677466
4
+ data.tar.gz: 6cd70f26a9b81d8be0899e519300fbdb30a3fb53
5
5
  SHA512:
6
- metadata.gz: dab4bded8d2e3f34739b7adcd147597576b76a31b0a76290cd114a72b877a6ed2ffead7760ccc45644aff6b95558dd1f4bc8725e5a330ce3596dfbc76d916376
7
- data.tar.gz: a6337ed60323d83c87f9ccb845021884d7cafcdd0ef74bec74d4a5b38734969b97a353417ab8b4549b4f0a10f83f952d22e44cf272b340a1b9dd31288ac9fe8e
6
+ metadata.gz: 1b5dbdc65a0fbcad465a567793044d4eb7cdb9b3728f44b6339ea956a00b46d97ca1cd499433e74847c78d3bde420cd7d60558990c6b97082934aea294e15886
7
+ data.tar.gz: 8b796e95b900b6afbab007932bd41b9841f32d7284724e709d01108be5abf785affa9a547bbb5fda81520efc2431936081ff52360e4c4d1be7a21cd8befd9ccd
@@ -0,0 +1,2 @@
1
+ language: ruby
2
+ script: bundle exec rspec
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- graphql-remote_loader (0.0.5)
4
+ graphql-remote_loader (1.0.0)
5
5
  graphql (~> 1.6)
6
6
  graphql-batch (~> 0.3)
7
7
 
data/README.md CHANGED
@@ -1,37 +1,67 @@
1
1
  # GraphQL Remote Loader
2
+ [![Gem Version](https://badge.fury.io/rb/graphql-remote_loader.svg)](https://badge.fury.io/rb/graphql-remote_loader) [![Build Status](https://travis-ci.org/d12/graphql-remote_loader.svg?branch=master)](https://travis-ci.org/d12/graphql-remote_loader)
3
+
2
4
  Performant, batched GraphQL queries from within the resolvers of a [`graphql-ruby`](https://github.com/rmosolgo/graphql-ruby) API.
3
5
 
4
- ## Example
6
+
7
+ ## Snippet
5
8
 
6
9
  ```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
10
+ field :login, String, null: false, description: "The currently authenticated GitHub user's login."
11
+
12
+ def login
13
+ GitHubLoader.load("viewer { login }").then do |results|
14
+ results["viewer"]["login"]
22
15
  end
23
16
  end
24
17
  ```
25
18
 
19
+ ## Full example
20
+
21
+ To see a working example of how `graphql-remote_loader` works, see the [complete, working example application](https://github.com/d12/graphql-remote_loader_example).
22
+
26
23
  ## Description
27
- `graphql-remote_loader` allows for querying GraphQL APIs from within resolvers of a [`graphql-ruby`](https://github.com/rmosolgo/graphql-ruby) API.
24
+ `graphql-remote_loader` allows for querying GraphQL APIs from within resolvers of a [`graphql-ruby`](https://github.com/rmosolgo/graphql-ruby) API.
28
25
 
29
- This can be used to create GraphQL APIs that depend on data from other GraphQL APIs, either remote or local.
26
+ This can be used to create GraphQL APIs that depend on data from other GraphQL APIs, either remote or local.
30
27
 
31
28
  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
29
 
33
30
  You can think of it as a lightweight version of schema-stitching.
34
31
 
32
+ ## How it works
33
+
34
+ Using GraphQL batch, the loader collects all GraphQL sub-queries before combining and executing one query.
35
+
36
+ ### Merging queries
37
+
38
+ Once we have all the queries we need, we merge them all together by treating the query ASTs as prefix trees and merging the prefix trees together. For example, if the input queries were `["viewer { foo bar }", "buzz viewer { foo bazz }"]`, then the merged query would be:
39
+
40
+ ```graphql
41
+ query{
42
+ buzz
43
+ viewer{
44
+ foo
45
+ bar
46
+ bazz
47
+ }
48
+ }
49
+ ```
50
+
51
+ ### Name conflicts
52
+
53
+ Suppose we want to merge `["user(login: "foo") { url }", "user(login: "bar") { url }"]`. We can't treat these `user` fields as equal since their arguments don't match, but we also can't naively combine these queries together, the `user` key will be ambiguous.
54
+
55
+ To solve this problem, we introduce identified aliases. Every query to be merged is given a unique ID. When building the query, we include this unique ID in a new alias on all fields. This prevents conflicts between separate subqueries.
56
+
57
+ ### Fulfilling promises
58
+
59
+ We only want to fulfill each promise with the data it asked for. This is especially relevant in the case of conflicts as in the case above.
60
+
61
+ When fulfilling promises, we traverse the result hash and prune out all result keys with UIDs that don't match the query. Before returning the filtered hash, we scrub the UIDs so that the result keys are the same as the GraphQL field names.
62
+
63
+ This solution prevents all conflict issues and doesn't change the way your API uses this library.
64
+
35
65
  ## How to use
36
66
  First, you'll need to install the gem. Either do `gem install graphql-remote_loader` or add this to your Gemfile:
37
67
 
@@ -51,8 +81,8 @@ require "graphql/remote_loader"
51
81
  module MyApp
52
82
  class GitHubLoader < GraphQL::RemoteLoader::Loader
53
83
  def query(query_string)
54
- parsed_query = MyApp::Client.parse(query_string)
55
- MyApp.query(parsed_query)
84
+ parsed_query = GraphQLClient.parse(query_string)
85
+ GraphQLClient.query(parsed_query)
56
86
  end
57
87
  end
58
88
  end
@@ -60,6 +90,9 @@ end
60
90
 
61
91
  This example uses [`graphql-client`](https://github.com/github/graphql-client). Any client, or even just plain `cURL`/`HTTP` can be used.
62
92
 
63
- ## Current State
93
+ ## Running tests
64
94
 
65
- This project is very much WIP. Some TODOs are listed in the issues. Bugs and feature requests should be added as issues.
95
+ ```
96
+ bundle install
97
+ rspec
98
+ ```
@@ -1,4 +1,5 @@
1
1
  require "prime"
2
+ require "json"
2
3
  require_relative "query_merger"
3
4
 
4
5
  module GraphQL
@@ -33,17 +34,32 @@ module GraphQL
33
34
 
34
35
  def perform(queries_and_primes)
35
36
  query_string = QueryMerger.merge(queries_and_primes)
36
- response = query(query_string)
37
+ response = query(query_string).to_h
38
+
39
+ data, errors = response["data"], response["errors"]
37
40
 
38
41
  queries_and_primes.each do |query, prime|
39
- fulfill([query, prime], filter_keys_on_response(response.to_h, prime))
42
+ response = {}
43
+
44
+ response["data"] = filter_keys_on_data(data, prime)
45
+
46
+ errors_key = filter_errors(errors, prime)
47
+ response["errors"] = dup(errors_key) unless errors_key.empty?
48
+
49
+ scrub_primes_from_error_paths!(response["errors"])
50
+
51
+ fulfill([query, prime], response)
40
52
  end
41
53
  end
42
54
 
43
- def filter_keys_on_response(obj, prime)
55
+ def dup(hash)
56
+ JSON.parse(hash.to_json)
57
+ end
58
+
59
+ def filter_keys_on_data(obj, prime)
44
60
  case obj
45
61
  when Array
46
- obj.map { |element| filter_keys_on_response(element, prime) }
62
+ obj.map { |element| filter_keys_on_data(element, prime) }
47
63
  when Hash
48
64
  filtered_results = {}
49
65
 
@@ -61,7 +77,7 @@ module GraphQL
61
77
  method_name = method.match(/\Ap[0-9]+(.*)/)[1]
62
78
 
63
79
  method_value = obj[method]
64
- filtered_value = filter_keys_on_response(method_value, prime)
80
+ filtered_value = filter_keys_on_data(method_value, prime)
65
81
 
66
82
  filtered_results[underscore(method_name)] = filtered_value
67
83
  end
@@ -72,6 +88,34 @@ module GraphQL
72
88
  end
73
89
  end
74
90
 
91
+ def filter_errors(errors, prime)
92
+ return [] unless errors
93
+
94
+ errors.select do |error|
95
+ # For now, do not support global errors with no path key
96
+ next unless error["path"]
97
+
98
+ # We fulfill a promise with an error object if field in the path
99
+ # key was requested by the promise.
100
+ error["path"].all? do |path_key|
101
+ next true if path_key.is_a? Integer
102
+
103
+ path_key_prime = path_key.match(/\Ap([0-9]+)/)[1].to_i
104
+ path_key_prime % prime == 0
105
+ end
106
+ end
107
+ end
108
+
109
+ def scrub_primes_from_error_paths!(error_array)
110
+ return unless error_array
111
+
112
+ error_array.map do |error|
113
+ error["path"].map! do |path_key|
114
+ path_key.match(/\Ap[0-9]+(.*)/)[1]
115
+ end
116
+ end
117
+ end
118
+
75
119
  def underscore(str)
76
120
  str.gsub(/::/, '/').
77
121
  gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
@@ -26,7 +26,6 @@ module GraphQL
26
26
  parsed_queries.each do |query|
27
27
  merge_query_recursive(query.definitions[0], merged_query.definitions[0])
28
28
  merge_fragment_definitions(query, merged_query)
29
-
30
29
  end
31
30
 
32
31
  merged_query.definitions.each do |definition|
@@ -61,13 +60,13 @@ module GraphQL
61
60
  a_query.selections.each do |a_query_selection|
62
61
  matching_field = b_query.selections.find do |b_query_selection|
63
62
  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)
68
- end
69
63
 
70
- same_name && same_args
64
+ next same_name if exempt_node_types.any? { |type| b_query_selection.is_a?(type) }
65
+
66
+ same_args = arguments_equal?(a_query_selection, b_query_selection)
67
+ same_alias = a_query_selection.alias == b_query_selection.alias
68
+
69
+ same_name && same_args && same_alias
71
70
  end
72
71
 
73
72
  if matching_field
@@ -107,9 +106,14 @@ module GraphQL
107
106
  ]
108
107
 
109
108
  query_selections.each do |selection|
110
- unless exempt_node_types.any? { |type| selection.is_a? type }
109
+ unless exempt_node_types.any? { |type| selection.is_a? type }
111
110
  prime_factor = selection.instance_variable_get(:@prime)
112
- selection.alias = "p#{prime_factor}#{selection.name}"
111
+
112
+ selection.alias = if selection.alias
113
+ "p#{prime_factor}#{selection.alias}"
114
+ else
115
+ "p#{prime_factor}#{selection.name}"
116
+ end
113
117
  end
114
118
 
115
119
  # Some nodes don't have selections (e.g. fragment spreads)
@@ -1,5 +1,5 @@
1
1
  module GraphQL
2
2
  module RemoteLoader
3
- VERSION = "0.0.5"
3
+ VERSION = "1.0.0"
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.5
4
+ version: 1.0.0
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-14 00:00:00.000000000 Z
11
+ date: 2018-06-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -104,6 +104,7 @@ extensions: []
104
104
  extra_rdoc_files: []
105
105
  files:
106
106
  - ".gitignore"
107
+ - ".travis.yml"
107
108
  - Gemfile
108
109
  - Gemfile.lock
109
110
  - README.md