Skip to content

Commit 1ffd5a0

Browse files
Add union support
1 parent 26d4215 commit 1ffd5a0

File tree

9 files changed

+189
-13
lines changed

9 files changed

+189
-13
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## master
44

5+
- [PR#30](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/30) Add union support ([@DmitryTsepelev][])
6+
57
## 1.0.3 (2020-08-31)
68

79
- [PR#29](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/29) Cache result JSON instead of connection objects ([@DmitryTsepelev][])

lib/graphql/fragment_cache/cache_key_builder.rb

+19-7
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,18 @@ def alias?(val)
3232
end
3333

3434
refine ::GraphQL::Execution::Lookahead do
35-
def selection_with_alias(name, **kwargs)
36-
return selection(name, **kwargs) if selects?(name, **kwargs)
37-
alias_selection(name, **kwargs)
35+
def selection_with_alias(name, selected_type:, **kwargs)
36+
# In case of union we have to pass a type of object explicitly
37+
# More info https://github.com/rmosolgo/graphql-ruby/pull/3007
38+
kwargs[:selected_type] = selected_type if @selected_type.kind.union?
39+
40+
selection(name, **kwargs).then do |next_selection|
41+
if next_selection.is_a?(GraphQL::Execution::Lookahead::NullLookahead)
42+
alias_selection(name, **kwargs)
43+
else
44+
next_selection
45+
end
46+
end
3847
end
3948

4049
def alias_selection(name, selected_type: @selected_type, arguments: nil)
@@ -48,7 +57,9 @@ def alias_selection(name, selected_type: @selected_type, arguments: nil)
4857
# From https://github.com/rmosolgo/graphql-ruby/blob/1a9a20f3da629e63ea8e5ee8400be82218f9edc3/lib/graphql/execution/lookahead.rb#L91
4958
next_field_defn = get_class_based_field(selected_type, next_field_name)
5059

51-
alias_selections[name] =
60+
alias_name = "#{name}_#{selected_type.name}"
61+
62+
alias_selections[alias_name] =
5263
if next_field_defn
5364
next_nodes = []
5465
arguments = @query.arguments_for(alias_node, next_field_defn)
@@ -103,11 +114,12 @@ def call(**options)
103114

104115
attr_reader :query, :path, :object, :schema
105116

106-
def initialize(object: nil, query:, path:, **options)
117+
def initialize(object: nil, query:, path:, selected_type:, **options)
107118
@object = object
108119
@query = query
109120
@schema = query.schema
110121
@path = path
122+
@selected_type = selected_type
111123
@options = options
112124
end
113125

@@ -134,7 +146,7 @@ def selections_cache_key
134146
# Handle cached fields inside collections:
135147
next lkhd if field_name.is_a?(Integer)
136148

137-
lkhd.selection_with_alias(field_name)
149+
lkhd.selection_with_alias(field_name, selected_type: @selected_type)
138150
}
139151

140152
current_root.selections.to_selections_key
@@ -147,7 +159,7 @@ def path_cache_key
147159
# Handle cached fields inside collections:
148160
next field_name if field_name.is_a?(Integer)
149161

150-
lookahead = lookahead.selection_with_alias(field_name)
162+
lookahead = lookahead.selection_with_alias(field_name, selected_type: @selected_type)
151163
raise "Failed to look ahead the field: #{field_name}" if lookahead.is_a?(::GraphQL::Execution::Lookahead::NullLookahead)
152164

153165
next lookahead.field.name if lookahead.arguments.empty?

lib/graphql/fragment_cache/fragment.rb

+7-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ def persist
3030
private
3131

3232
def cache_key
33-
@cache_key ||= CacheKeyBuilder.call(path: path, query: context.query, **options)
33+
@cache_key ||= CacheKeyBuilder.call(
34+
path: path, query: context.query, selected_type: selected_type, **options
35+
)
3436
end
3537

3638
def interpreter_context
@@ -40,6 +42,10 @@ def interpreter_context
4042
def final_value
4143
@final_value ||= interpreter_context[:runtime].final_value
4244
end
45+
46+
def selected_type
47+
@selected_type ||= interpreter_context[:current_object].class
48+
end
4349
end
4450
end
4551
end

spec/graphql/fragment_cache/cache_key_builder_spec.rb

+63-3
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@
2727
let(:post) { Post.find(42) }
2828

2929
let(:object) { nil }
30+
let(:selected_type) { nil }
3031
let(:context) { {} }
3132
let(:query_obj) { GraphQL::Query.new(schema, query, variables: variables, context: context) }
3233

33-
subject { described_class.call(object: object, query: query_obj, path: path) }
34+
subject { described_class.call(object: object, query: query_obj, path: path, selected_type: selected_type) }
3435

3536
# Make cache keys raw for easier debugging
3637
let(:schema_cache_key) { "schema_key" }
@@ -57,6 +58,8 @@
5758
GQL
5859
end
5960

61+
let(:selected_type) { Types::Post }
62+
6063
specify { is_expected.to eq "schema_key/cachedPost(id:#{id})[id.title.author[id.name]]" }
6164
end
6265

@@ -77,7 +80,7 @@
7780
end
7881

7982
let(:path) { ["cachedPostByInput"] }
80-
83+
let(:selected_type) { Types::Post }
8184
let(:variables) { {inputWithId: {id: id, intArg: 42}} }
8285

8386
specify { is_expected.to eq "schema_key/cachedPostByInput(input_with_id:{id:#{id},int_arg:42})[id.title.author[id.name]]" }
@@ -108,6 +111,7 @@
108111

109112
context "when cached cached field is nested" do
110113
let(:path) { ["post", "cachedAuthor"] }
114+
let(:selected_type) { Types::User }
111115

112116
let(:query) do
113117
<<~GQL
@@ -147,6 +151,8 @@
147151
GQL
148152
end
149153

154+
let(:selected_type) { Types::Post }
155+
150156
specify { is_expected.to eq "schema_key/cachedPost(id:#{id})[id.title.author[id.name]]" }
151157

152158
context "when nested fragment is used" do
@@ -177,6 +183,7 @@
177183

178184
context "when object is passed and responds to #cache_key" do
179185
let(:object) { post }
186+
let(:selected_type) { Types::Post }
180187

181188
specify { is_expected.to eq "schema_key/cachedPost(id:#{id})[id.title]/#{post.cache_key}" }
182189
end
@@ -186,12 +193,14 @@
186193
post.singleton_class.define_method(:graphql_cache_key) { "super-cache" }
187194
end
188195

196+
let(:selected_type) { Types::Post }
189197
let(:object) { post }
190198

191199
specify { is_expected.to eq "schema_key/cachedPost(id:#{id})[id.title]/super-cache" }
192200
end
193201

194202
context "when object is passed deosn't respond to #cache_key neither #graphql_cache_key" do
203+
let(:selected_type) { Types::Post }
195204
let(:object) { post.author }
196205

197206
it "fallbacks to #to_s" do
@@ -200,6 +209,7 @@
200209
end
201210

202211
context "when array is passed as object" do
212+
let(:selected_type) { Types::Post }
203213
let(:object) { [post, :custom, 99] }
204214

205215
specify { is_expected.to eq "schema_key/cachedPost(id:#{id})[id.title]/#{post.cache_key}/custom/99" }
@@ -216,7 +226,7 @@
216226
}
217227
GQL
218228
end
219-
229+
let(:selected_type) { GraphQL::Types::String }
220230
let(:path) { ["posts", 0, "cachedTitle"] }
221231

222232
specify { is_expected.to eq "schema_key/posts/0/cachedTitle[]" }
@@ -240,6 +250,7 @@
240250
GQL
241251
end
242252

253+
let(:selected_type) { Types::User }
243254
let(:path) { ["post", "author"] }
244255

245256
specify { is_expected.to eq "schema_key/post(id:1)/cachedAuthor[name]" }
@@ -261,8 +272,57 @@
261272
GQL
262273
end
263274

275+
let(:selected_type) { Types::User }
264276
let(:path) { ["post", "author"] }
265277

266278
specify { is_expected.to eq "schema_key/post(id:1)/cachedAuthor[name]" }
267279
end
280+
281+
context "when query has union type" do
282+
let(:query) do
283+
<<~GQL
284+
query getFeed {
285+
feed {
286+
...on PostType {
287+
id
288+
cachedAvatarUrl
289+
}
290+
...on UserType {
291+
id
292+
cachedAvatarUrl
293+
}
294+
}
295+
}
296+
GQL
297+
end
298+
299+
let(:selected_type) { Types::Post }
300+
let(:path) { ["feed", 0, "cachedAvatarUrl"] }
301+
302+
specify { is_expected.to eq "schema_key/feed/0/cachedAvatarUrl[]" }
303+
304+
context "when cached field has alias" do
305+
let(:query) do
306+
<<~GQL
307+
query getFeed {
308+
feed {
309+
...on PostType {
310+
id
311+
avatarUrl: cachedAvatarUrl
312+
}
313+
...on UserType {
314+
id
315+
avatarUrl: cachedAvatarUrl
316+
}
317+
}
318+
}
319+
GQL
320+
end
321+
322+
let(:selected_type) { Types::Post }
323+
let(:path) { ["feed", 0, "avatarUrl"] }
324+
325+
specify { is_expected.to eq "schema_key/feed/0/cachedAvatarUrl[]" }
326+
end
327+
end
268328
end

spec/graphql/fragment_cache/object_helpers_spec.rb

+43
Original file line numberDiff line numberDiff line change
@@ -577,4 +577,47 @@ def post(id:, expires_in: nil)
577577
expect(::Post).not_to have_received(:all)
578578
end
579579
end
580+
581+
describe "union caching" do
582+
let!(:post) { Post.create(id: 1, title: "Post #1") }
583+
let!(:user) { User.create(id: 2, name: "User #2") }
584+
585+
let(:schema) do
586+
build_schema do
587+
query(
588+
Class.new(Types::Query) {
589+
field :feed, [Types::Activity], null: false
590+
591+
define_method(:feed, -> { ::Post.all + ::User.all })
592+
}
593+
)
594+
end
595+
end
596+
597+
let(:query) do
598+
<<~GQL
599+
query getFeed {
600+
feed {
601+
...on PostType {
602+
id
603+
cachedAvatarUrl
604+
}
605+
...on UserType {
606+
id
607+
cachedAvatarUrl
608+
}
609+
}
610+
}
611+
GQL
612+
end
613+
614+
it "returns cached data" do
615+
expect(execute_query.dig("data")).to eq(
616+
"feed" => [
617+
{"cachedAvatarUrl" => "http://example.com/img/posts/#{post.id}", "id" => post.id.to_s},
618+
{"cachedAvatarUrl" => "http://example.com/img/users/#{user.id}", "id" => user.id.to_s}
619+
]
620+
)
621+
end
622+
end
580623
end

spec/graphql/fragment_cache/rails/cache_key_builder_spec.rb

+6-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
let(:object) { Post.find(42) }
2828
let(:query_obj) { GraphQL::Query.new(schema, query, variables: variables) }
29+
let(:selected_type) { Types::Post }
2930

3031
# Make cache keys raw for easier debugging
3132
let(:schema_cache_key) { "schema_key" }
@@ -34,7 +35,11 @@
3435
allow(Digest::SHA1).to receive(:hexdigest) { |val| val }
3536
end
3637

37-
subject { described_class.call(object: object, query: query_obj, path: path) }
38+
subject do
39+
described_class.call(
40+
object: object, query: query_obj, selected_type: selected_type, path: path
41+
)
42+
end
3843

3944
it "uses Cache.expand_cache_key" do
4045
allow(ActiveSupport::Cache).to receive(:expand_cache_key).with(object) { "as:cache:key" }

spec/support/models/post.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ class Post
44
class << self
55
def find(id)
66
store.fetch(id.to_i) do
7-
author = User.new(id: id, name: "User ##{id}")
7+
author = User.fetch(id)
88
new(id: id, title: "Post ##{id}", author: author)
99
end
1010
end

spec/support/models/user.rb

+20
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,26 @@ class User
44
attr_reader :id
55
attr_accessor :name
66

7+
class << self
8+
def fetch(id)
9+
store.fetch(id.to_i) do
10+
new(id: id, name: "User ##{id}")
11+
end
12+
end
13+
14+
def all
15+
@store.values
16+
end
17+
18+
def create(id:, **attributes)
19+
store[id] = new(id: id, **attributes)
20+
end
21+
22+
def store
23+
@store ||= {}
24+
end
25+
end
26+
727
def initialize(id:, name:)
828
@id = id
929
@name = name

0 commit comments

Comments
 (0)