graphql-remote_loader 1.0.5 → 1.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +24 -0
- data/graphql-remote_loader.gemspec +2 -0
- data/lib/graphql/remote_loader.rb +2 -0
- data/lib/graphql/remote_loader/loader.rb +57 -47
- data/lib/graphql/remote_loader/query_merger.rb +15 -13
- data/lib/graphql/remote_loader/version.rb +3 -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: acdeaf8aaaccf5ffeba1afb1f2034fb7d41fba34
|
4
|
+
data.tar.gz: c5478e13983b4ca67867571fc25553dadfd82ffa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d3c97bbe137b06cff20c4687c2088e8ac0819963e1ee5ab493650508de826ecbeee872f13e2356ee00336713d4c52089fccc13e8cefa662f034b1c4e2feccf7
|
7
|
+
data.tar.gz: cd28610ee919d38b4f59e6bb271a4ea81a79dfcf5b1559a23fa94621120787fe6324cd4e3feda388af24b52e82fc356320b0b7a5500bafcf93c9041a56a57ad2
|
@@ -0,0 +1,24 @@
|
|
1
|
+
name: Ruby
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [ master ]
|
6
|
+
pull_request:
|
7
|
+
branches: [ master ]
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
build:
|
11
|
+
|
12
|
+
runs-on: ubuntu-latest
|
13
|
+
|
14
|
+
steps:
|
15
|
+
- uses: actions/checkout@v2
|
16
|
+
- name: Set up Ruby 2.6
|
17
|
+
uses: actions/setup-ruby@v1
|
18
|
+
with:
|
19
|
+
ruby-version: 2.6.x
|
20
|
+
- name: Build and test with Rake
|
21
|
+
run: |
|
22
|
+
gem install bundler
|
23
|
+
bundle install --jobs 4 --retry 3
|
24
|
+
bundle exec rspec
|
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require "json"
|
3
4
|
require_relative "query_merger"
|
4
5
|
|
@@ -6,22 +7,18 @@ module GraphQL
|
|
6
7
|
module RemoteLoader
|
7
8
|
class Loader < GraphQL::Batch::Loader
|
8
9
|
# Delegates to GraphQL::Batch::Loader#load
|
9
|
-
# We include a unique
|
10
|
+
# We include a unique id as part of the batch key to use as part
|
10
11
|
# of the alias on all fields. This is used to
|
11
12
|
# a) Avoid name collisions in the generated query
|
12
13
|
# b) Determine which fields in the result JSON should be
|
13
14
|
# handed fulfilled to each promise
|
14
15
|
def self.load(query, context: {}, variables: {})
|
15
|
-
@index ||=
|
16
|
+
@index ||= 0
|
16
17
|
@index += 1
|
17
18
|
|
18
|
-
prime = Prime.take(@index - 1).last
|
19
|
-
|
20
19
|
store_context(context)
|
21
20
|
|
22
|
-
interpolate_variables
|
23
|
-
|
24
|
-
self.for.load([query, prime, @context])
|
21
|
+
self.for.load([interpolate_variables(query, variables), @index, @context])
|
25
22
|
end
|
26
23
|
|
27
24
|
# Loads the value, then if the query was successful, fulfills promise with
|
@@ -69,24 +66,24 @@ module GraphQL
|
|
69
66
|
|
70
67
|
private
|
71
68
|
|
72
|
-
def perform(
|
73
|
-
query_string = QueryMerger.merge(
|
74
|
-
context =
|
69
|
+
def perform(queries_and_ids)
|
70
|
+
query_string = QueryMerger.merge(queries_and_ids).gsub(/\s+/, " ")
|
71
|
+
context = queries_and_ids[-1][-1]
|
75
72
|
response = query(query_string, context: context).to_h
|
76
73
|
|
77
74
|
data, errors = response["data"], response["errors"]
|
78
75
|
|
79
|
-
|
76
|
+
queries_and_ids.each do |query, caller_id, context|
|
80
77
|
response = {}
|
81
78
|
|
82
|
-
response["data"] = filter_keys_on_data(data,
|
79
|
+
response["data"] = filter_keys_on_data(data, caller_id)
|
83
80
|
|
84
|
-
errors_key = filter_errors(errors,
|
81
|
+
errors_key = filter_errors(errors, caller_id)
|
85
82
|
response["errors"] = dup(errors_key) unless errors_key.empty?
|
86
83
|
|
87
|
-
|
84
|
+
scrub_caller_ids_from_error_paths!(response["errors"])
|
88
85
|
|
89
|
-
fulfill([query,
|
86
|
+
fulfill([query, caller_id, context], response)
|
90
87
|
end
|
91
88
|
end
|
92
89
|
|
@@ -97,23 +94,37 @@ module GraphQL
|
|
97
94
|
# E.g.
|
98
95
|
# interpolate_variables("foo(bar: $my_var)", { my_var: "buzz" })
|
99
96
|
# => "foo(bar: \"buzz\")"
|
97
|
+
def self.interpolate_variables(query, variables = {})
|
98
|
+
query.dup.tap { |mutable_query| interpolate_variables!(mutable_query, variables) }
|
99
|
+
end
|
100
|
+
|
100
101
|
def self.interpolate_variables!(query, variables = {})
|
101
|
-
variables.each
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
102
|
+
variables.each { |variable, value| query.gsub!("$#{variable.to_s}", stringify_variable(value)) }
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.stringify_variable(value)
|
106
|
+
case value
|
107
|
+
when Integer, Float, TrueClass, FalseClass
|
108
|
+
# These types are safe to directly interpolate into the query, and GraphQL does not expect these types to be quoted.
|
109
|
+
value.to_s
|
110
|
+
when Array
|
111
|
+
# Arrays can contain elements with various types, so we need to check them one by one
|
112
|
+
stringified_elements = value.map { |element| stringify_variable(element) }
|
113
|
+
"[#{stringified_elements.join(', ')}]"
|
114
|
+
when Hash
|
115
|
+
# Hashes can contain values with various types, so we need to check them one by one
|
116
|
+
stringified_key_value_pairs = value.map { |key, value| "#{key}: #{stringify_variable(value)}" }
|
117
|
+
"{#{stringified_key_value_pairs.join(', ')}}"
|
118
|
+
else
|
119
|
+
# A string is either a GraphQL String or ID type.
|
120
|
+
# This means we need to
|
121
|
+
# a) Surround the value in quotes
|
122
|
+
# b) escape special characters in the string
|
123
|
+
#
|
124
|
+
# This else also catches unknown objects, which could break the query if we directly interpolate.
|
125
|
+
# These objects get converted to strings, then escaped.
|
126
|
+
|
127
|
+
value.to_s.inspect
|
117
128
|
end
|
118
129
|
end
|
119
130
|
|
@@ -121,10 +132,10 @@ module GraphQL
|
|
121
132
|
JSON.parse(hash.to_json)
|
122
133
|
end
|
123
134
|
|
124
|
-
def filter_keys_on_data(obj,
|
135
|
+
def filter_keys_on_data(obj, caller_id)
|
125
136
|
case obj
|
126
137
|
when Array
|
127
|
-
obj.map { |element| filter_keys_on_data(element,
|
138
|
+
obj.map { |element| filter_keys_on_data(element, caller_id) }
|
128
139
|
when Hash
|
129
140
|
filtered_results = {}
|
130
141
|
|
@@ -133,27 +144,26 @@ module GraphQL
|
|
133
144
|
|
134
145
|
# Filter methods that were not requested in this sub-query
|
135
146
|
fields = fields.select do |field|
|
136
|
-
|
137
|
-
|
147
|
+
graphql_caller = field.match(/\Ap([0-9]+)/)[1].to_i
|
148
|
+
graphql_caller[caller_id] == 1 # Fixnum#[] accesses bitwise representation of num
|
138
149
|
end
|
139
150
|
|
140
|
-
# redefine
|
141
|
-
fields.each do |
|
142
|
-
|
143
|
-
|
144
|
-
method_value = obj[method]
|
145
|
-
filtered_value = filter_keys_on_data(method_value, prime)
|
151
|
+
# redefine fields on new obj, recursively filter sub-selections
|
152
|
+
fields.each do |field|
|
153
|
+
field_name = field.match(/\Ap[0-9]+(.*)/)[1]
|
146
154
|
|
147
|
-
|
155
|
+
value = obj[field]
|
156
|
+
filtered_results[underscore(field_name)] = filter_keys_on_data(value, caller_id)
|
148
157
|
end
|
149
158
|
|
150
159
|
filtered_results
|
151
160
|
else
|
161
|
+
# Base case, no more recursion needed.
|
152
162
|
return obj
|
153
163
|
end
|
154
164
|
end
|
155
165
|
|
156
|
-
def filter_errors(errors,
|
166
|
+
def filter_errors(errors, caller_id)
|
157
167
|
return [] unless errors
|
158
168
|
|
159
169
|
errors.select do |error|
|
@@ -165,13 +175,13 @@ module GraphQL
|
|
165
175
|
error["path"].all? do |path_key|
|
166
176
|
next true if path_key.is_a? Integer
|
167
177
|
|
168
|
-
|
169
|
-
|
178
|
+
path_key_caller_id = path_key.match(/\Ap([0-9]+)/)[1].to_i
|
179
|
+
path_key_caller_id[caller_id]
|
170
180
|
end
|
171
181
|
end
|
172
182
|
end
|
173
183
|
|
174
|
-
def
|
184
|
+
def scrub_caller_ids_from_error_paths!(error_array)
|
175
185
|
return unless error_array
|
176
186
|
|
177
187
|
error_array.map do |error|
|
@@ -1,15 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module GraphQL
|
2
4
|
module RemoteLoader
|
3
|
-
# Given a list of queries and their
|
5
|
+
# Given a list of queries and their caller IDs, generate the merged and labeled
|
4
6
|
# GraphQL query to be sent off to the remote backend.
|
5
7
|
class QueryMerger
|
6
8
|
class << self
|
7
|
-
def merge(
|
8
|
-
parsed_queries =
|
9
|
+
def merge(queries_and_caller_ids)
|
10
|
+
parsed_queries = queries_and_caller_ids.map do |query, caller_id|
|
9
11
|
parsed_query = parse(query)
|
10
12
|
|
11
13
|
parsed_query.definitions.each do |definition|
|
12
|
-
|
14
|
+
attach_caller_id!(definition.children, caller_id)
|
13
15
|
end
|
14
16
|
|
15
17
|
parsed_query
|
@@ -75,10 +77,10 @@ module GraphQL
|
|
75
77
|
end
|
76
78
|
|
77
79
|
if matching_field
|
78
|
-
|
79
|
-
a_query_selection.instance_variable_get(:@
|
80
|
+
new_binary_id = matching_field.instance_variable_get(:@binary_id) +
|
81
|
+
a_query_selection.instance_variable_get(:@binary_id)
|
80
82
|
|
81
|
-
matching_field.instance_variable_set(:@
|
83
|
+
matching_field.instance_variable_set(:@binary_id, new_binary_id)
|
82
84
|
merge_query_recursive(a_query_selection, matching_field) unless exempt_node_types.any? { |type| matching_field.is_a?(type) }
|
83
85
|
else
|
84
86
|
b_query.instance_variable_set(:@selections, [b_query.selections, a_query_selection].flatten)
|
@@ -97,10 +99,10 @@ module GraphQL
|
|
97
99
|
b.arguments.map { |arg| {name: arg.name, value: arg.value}.to_s }.sort
|
98
100
|
end
|
99
101
|
|
100
|
-
def
|
102
|
+
def attach_caller_id!(query_fields, caller_id)
|
101
103
|
query_fields.each do |field|
|
102
|
-
field.instance_variable_set(:@
|
103
|
-
|
104
|
+
field.instance_variable_set(:@binary_id, 2 ** caller_id)
|
105
|
+
attach_caller_id!(field.children, caller_id)
|
104
106
|
end
|
105
107
|
end
|
106
108
|
|
@@ -112,12 +114,12 @@ module GraphQL
|
|
112
114
|
|
113
115
|
query_selections.each do |selection|
|
114
116
|
unless exempt_node_types.any? { |type| selection.is_a? type }
|
115
|
-
|
117
|
+
binary_id = selection.instance_variable_get(:@binary_id)
|
116
118
|
|
117
119
|
selection.instance_variable_set(:@alias, if selection.alias
|
118
|
-
"p#{
|
120
|
+
"p#{binary_id}#{selection.alias}"
|
119
121
|
else
|
120
|
-
"p#{
|
122
|
+
"p#{binary_id}#{selection.name}"
|
121
123
|
end)
|
122
124
|
end
|
123
125
|
|
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: 1.0.
|
4
|
+
version: 1.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nathaniel Woodthorpe
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-04-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -103,6 +103,7 @@ executables: []
|
|
103
103
|
extensions: []
|
104
104
|
extra_rdoc_files: []
|
105
105
|
files:
|
106
|
+
- ".github/workflows/ruby.yml"
|
106
107
|
- ".gitignore"
|
107
108
|
- ".travis.yml"
|
108
109
|
- Gemfile
|