Skip to content

Commit ef25147

Browse files
Unions support (in progress)
1 parent 26d4215 commit ef25147

File tree

9 files changed

+282
-7
lines changed

9 files changed

+282
-7
lines changed

.ruby-version

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2.5.3

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-3
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,23 @@ def alias?(val)
3232
end
3333

3434
refine ::GraphQL::Execution::Lookahead do
35+
using RubyNext
36+
3537
def selection_with_alias(name, **kwargs)
36-
return selection(name, **kwargs) if selects?(name, **kwargs)
37-
alias_selection(name, **kwargs)
38+
# In case of union we have to pass a type of object explicitly
39+
# More info https://github.com/rmosolgo/graphql-ruby/pull/3007
40+
if @selected_type.kind.union?
41+
# TODO: we need to guess a type of an object at path to pass it
42+
kwargs[:selected_type] = @query.context.namespace(:interpreter)[:current_object].class
43+
end
44+
45+
selection(name, **kwargs).then do |next_selection|
46+
if next_selection.is_a?(GraphQL::Execution::Lookahead::NullLookahead)
47+
alias_selection(name, **kwargs)
48+
else
49+
next_selection
50+
end
51+
end
3852
end
3953

4054
def alias_selection(name, selected_type: @selected_type, arguments: nil)
@@ -48,7 +62,9 @@ def alias_selection(name, selected_type: @selected_type, arguments: nil)
4862
# From https://github.com/rmosolgo/graphql-ruby/blob/1a9a20f3da629e63ea8e5ee8400be82218f9edc3/lib/graphql/execution/lookahead.rb#L91
4963
next_field_defn = get_class_based_field(selected_type, next_field_name)
5064

51-
alias_selections[name] =
65+
alias_name = "#{name}_#{selected_type.name}"
66+
67+
alias_selections[alias_name] =
5268
if next_field_defn
5369
next_nodes = []
5470
arguments = @query.arguments_for(alias_node, next_field_defn)

spec/graphql/fragment_cache/cache_key_builder_spec.rb

+71-2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
GQL
5858
end
5959

60+
6061
specify { is_expected.to eq "schema_key/cachedPost(id:#{id})[id.title.author[id.name]]" }
6162
end
6263

@@ -77,7 +78,6 @@
7778
end
7879

7980
let(:path) { ["cachedPostByInput"] }
80-
8181
let(:variables) { {inputWithId: {id: id, intArg: 42}} }
8282

8383
specify { is_expected.to eq "schema_key/cachedPostByInput(input_with_id:{id:#{id},int_arg:42})[id.title.author[id.name]]" }
@@ -147,6 +147,7 @@
147147
GQL
148148
end
149149

150+
150151
specify { is_expected.to eq "schema_key/cachedPost(id:#{id})[id.title.author[id.name]]" }
151152

152153
context "when nested fragment is used" do
@@ -216,7 +217,6 @@
216217
}
217218
GQL
218219
end
219-
220220
let(:path) { ["posts", 0, "cachedTitle"] }
221221

222222
specify { is_expected.to eq "schema_key/posts/0/cachedTitle[]" }
@@ -265,4 +265,73 @@
265265

266266
specify { is_expected.to eq "schema_key/post(id:1)/cachedAuthor[name]" }
267267
end
268+
269+
context "when query has union type" do
270+
let(:path) { ["lastActivity", "cachedAvatarUrl"] }
271+
272+
let(:query) do
273+
<<~GQL
274+
query getLastActivity {
275+
lastActivity {
276+
...on PostType {
277+
id
278+
cachedAvatarUrl
279+
}
280+
...on UserType {
281+
id
282+
cachedAvatarUrl
283+
}
284+
}
285+
}
286+
GQL
287+
end
288+
289+
specify { is_expected.to eq "schema_key/lastActivity/cachedAvatarUrl[]" }
290+
291+
context "when array of union typed objects is returned" do
292+
let(:query) do
293+
<<~GQL
294+
query getFeed {
295+
feed {
296+
...on PostType {
297+
id
298+
cachedAvatarUrl
299+
}
300+
...on UserType {
301+
id
302+
cachedAvatarUrl
303+
}
304+
}
305+
}
306+
GQL
307+
end
308+
309+
let(:path) { ["feed", 0, "cachedAvatarUrl"] }
310+
311+
specify { is_expected.to eq "schema_key/feed/0/cachedAvatarUrl[]" }
312+
313+
context "when cached field has alias" do
314+
let(:query) do
315+
<<~GQL
316+
query getFeed {
317+
feed {
318+
...on PostType {
319+
id
320+
avatarUrl: cachedAvatarUrl
321+
}
322+
...on UserType {
323+
id
324+
avatarUrl: cachedAvatarUrl
325+
}
326+
}
327+
}
328+
GQL
329+
end
330+
331+
let(:path) { ["feed", 0, "avatarUrl"] }
332+
333+
specify { is_expected.to eq "schema_key/feed/0/cachedAvatarUrl[]" }
334+
end
335+
end
336+
end
268337
end

spec/graphql/fragment_cache/object_helpers_spec.rb

+120
Original file line numberDiff line numberDiff line change
@@ -577,4 +577,124 @@ 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 :last_activity, Types::Activity, null: false
590+
591+
define_method(:last_activity, -> { ::Post.find(1) })
592+
}
593+
)
594+
end
595+
end
596+
597+
let(:query) do
598+
<<~GQL
599+
query getLastActivity {
600+
lastActivity {
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+
"lastActivity" =>
617+
{"cachedAvatarUrl" => "http://example.com/img/posts/#{post.id}", "id" => post.id.to_s}
618+
)
619+
end
620+
621+
context "when unions are nested" do
622+
let(:query) do
623+
<<~GQL
624+
query getLastActivity {
625+
lastActivity {
626+
...on PostType {
627+
id
628+
relatedActivity {
629+
...on PostType {
630+
id
631+
cachedAvatarUrl
632+
}
633+
...on UserType {
634+
id
635+
cachedAvatarUrl
636+
}
637+
}
638+
}
639+
...on UserType {
640+
id
641+
}
642+
}
643+
}
644+
GQL
645+
end
646+
647+
it "returns cached data" do
648+
expect(execute_query.dig("data")).to eq(
649+
"lastActivity" => {
650+
"id" => "1",
651+
"relatedActivity" => {
652+
"cachedAvatarUrl" => "http://example.com/img/posts/#{user.id}",
653+
"id" => user.id.to_s
654+
}
655+
}
656+
)
657+
end
658+
end
659+
660+
context "when array of union typed objects is returned" do
661+
let(:schema) do
662+
build_schema do
663+
query(
664+
Class.new(Types::Query) {
665+
field :feed, [Types::Activity], null: false
666+
667+
define_method(:feed, -> { ::Post.all + ::User.all })
668+
}
669+
)
670+
end
671+
end
672+
673+
let(:query) do
674+
<<~GQL
675+
query getFeed {
676+
feed {
677+
...on PostType {
678+
id
679+
cachedAvatarUrl
680+
}
681+
...on UserType {
682+
id
683+
cachedAvatarUrl
684+
}
685+
}
686+
}
687+
GQL
688+
end
689+
690+
it "returns cached data" do
691+
expect(execute_query.dig("data")).to eq(
692+
"feed" => [
693+
{"cachedAvatarUrl" => "http://example.com/img/posts/#{post.id}", "id" => post.id.to_s},
694+
{"cachedAvatarUrl" => "http://example.com/img/users/#{user.id}", "id" => user.id.to_s}
695+
]
696+
)
697+
end
698+
end
699+
end
580700
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)