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 +4 -4
- data/.travis.yml +2 -0
- data/Gemfile.lock +1 -1
- data/README.md +55 -22
- data/lib/graphql/remote_loader/loader.rb +49 -5
- data/lib/graphql/remote_loader/query_merger.rb +13 -9
- data/lib/graphql/remote_loader/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8f7f2b1ab4cf983200d1c98e42324a2965677466
|
4
|
+
data.tar.gz: 6cd70f26a9b81d8be0899e519300fbdb30a3fb53
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1b5dbdc65a0fbcad465a567793044d4eb7cdb9b3728f44b6339ea956a00b46d97ca1cd499433e74847c78d3bde420cd7d60558990c6b97082934aea294e15886
|
7
|
+
data.tar.gz: 8b796e95b900b6afbab007932bd41b9841f32d7284724e709d01108be5abf785affa9a547bbb5fda81520efc2431936081ff52360e4c4d1be7a21cd8befd9ccd
|
data/.travis.yml
ADDED
data/Gemfile.lock
CHANGED
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
|
-
|
6
|
+
|
7
|
+
## Snippet
|
5
8
|
|
6
9
|
```ruby
|
7
|
-
field :
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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 =
|
55
|
-
|
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
|
-
##
|
93
|
+
## Running tests
|
64
94
|
|
65
|
-
|
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
|
-
|
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
|
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|
|
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 =
|
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
|
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
|
-
|
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)
|
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
|
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-
|
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
|