graphql-remote_loader 1.0.5 → 1.0.6
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 +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
|