Skip to content

Commit

Permalink
Unions support (in progress)
Browse files Browse the repository at this point in the history
  • Loading branch information
DmitryTsepelev committed Oct 2, 2020
1 parent 26d4215 commit ef25147
Show file tree
Hide file tree
Showing 9 changed files with 282 additions and 7 deletions.
1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2.5.3
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## master

- [PR#30](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/30) Add union support ([@DmitryTsepelev][])

## 1.0.3 (2020-08-31)

- [PR#29](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/29) Cache result JSON instead of connection objects ([@DmitryTsepelev][])
Expand Down
22 changes: 19 additions & 3 deletions lib/graphql/fragment_cache/cache_key_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,23 @@ def alias?(val)
end

refine ::GraphQL::Execution::Lookahead do
using RubyNext

def selection_with_alias(name, **kwargs)
return selection(name, **kwargs) if selects?(name, **kwargs)
alias_selection(name, **kwargs)
# In case of union we have to pass a type of object explicitly
# More info https://github.com/rmosolgo/graphql-ruby/pull/3007
if @selected_type.kind.union?
# TODO: we need to guess a type of an object at path to pass it
kwargs[:selected_type] = @query.context.namespace(:interpreter)[:current_object].class
end

selection(name, **kwargs).then do |next_selection|
if next_selection.is_a?(GraphQL::Execution::Lookahead::NullLookahead)
alias_selection(name, **kwargs)
else
next_selection
end
end
end

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

alias_selections[name] =
alias_name = "#{name}_#{selected_type.name}"

alias_selections[alias_name] =
if next_field_defn
next_nodes = []
arguments = @query.arguments_for(alias_node, next_field_defn)
Expand Down
73 changes: 71 additions & 2 deletions spec/graphql/fragment_cache/cache_key_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
GQL
end


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

Expand All @@ -77,7 +78,6 @@
end

let(:path) { ["cachedPostByInput"] }

let(:variables) { {inputWithId: {id: id, intArg: 42}} }

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


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

context "when nested fragment is used" do
Expand Down Expand Up @@ -216,7 +217,6 @@
}
GQL
end

let(:path) { ["posts", 0, "cachedTitle"] }

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

specify { is_expected.to eq "schema_key/post(id:1)/cachedAuthor[name]" }
end

context "when query has union type" do
let(:path) { ["lastActivity", "cachedAvatarUrl"] }

let(:query) do
<<~GQL
query getLastActivity {
lastActivity {
...on PostType {
id
cachedAvatarUrl
}
...on UserType {
id
cachedAvatarUrl
}
}
}
GQL
end

specify { is_expected.to eq "schema_key/lastActivity/cachedAvatarUrl[]" }

context "when array of union typed objects is returned" do
let(:query) do
<<~GQL
query getFeed {
feed {
...on PostType {
id
cachedAvatarUrl
}
...on UserType {
id
cachedAvatarUrl
}
}
}
GQL
end

let(:path) { ["feed", 0, "cachedAvatarUrl"] }

specify { is_expected.to eq "schema_key/feed/0/cachedAvatarUrl[]" }

context "when cached field has alias" do
let(:query) do
<<~GQL
query getFeed {
feed {
...on PostType {
id
avatarUrl: cachedAvatarUrl
}
...on UserType {
id
avatarUrl: cachedAvatarUrl
}
}
}
GQL
end

let(:path) { ["feed", 0, "avatarUrl"] }

specify { is_expected.to eq "schema_key/feed/0/cachedAvatarUrl[]" }
end
end
end
end
120 changes: 120 additions & 0 deletions spec/graphql/fragment_cache/object_helpers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -577,4 +577,124 @@ def post(id:, expires_in: nil)
expect(::Post).not_to have_received(:all)
end
end

describe "union caching" do
let!(:post) { Post.create(id: 1, title: "Post #1") }
let!(:user) { User.create(id: 2, name: "User #2") }

let(:schema) do
build_schema do
query(
Class.new(Types::Query) {
field :last_activity, Types::Activity, null: false

define_method(:last_activity, -> { ::Post.find(1) })
}
)
end
end

let(:query) do
<<~GQL
query getLastActivity {
lastActivity {
...on PostType {
id
cachedAvatarUrl
}
...on UserType {
id
cachedAvatarUrl
}
}
}
GQL
end

it "returns cached data" do
expect(execute_query.dig("data")).to eq(
"lastActivity" =>
{"cachedAvatarUrl" => "http://example.com/img/posts/#{post.id}", "id" => post.id.to_s}
)
end

context "when unions are nested" do
let(:query) do
<<~GQL
query getLastActivity {
lastActivity {
...on PostType {
id
relatedActivity {
...on PostType {
id
cachedAvatarUrl
}
...on UserType {
id
cachedAvatarUrl
}
}
}
...on UserType {
id
}
}
}
GQL
end

it "returns cached data" do
expect(execute_query.dig("data")).to eq(
"lastActivity" => {
"id" => "1",
"relatedActivity" => {
"cachedAvatarUrl" => "http://example.com/img/posts/#{user.id}",
"id" => user.id.to_s
}
}
)
end
end

context "when array of union typed objects is returned" do
let(:schema) do
build_schema do
query(
Class.new(Types::Query) {
field :feed, [Types::Activity], null: false

define_method(:feed, -> { ::Post.all + ::User.all })
}
)
end
end

let(:query) do
<<~GQL
query getFeed {
feed {
...on PostType {
id
cachedAvatarUrl
}
...on UserType {
id
cachedAvatarUrl
}
}
}
GQL
end

it "returns cached data" do
expect(execute_query.dig("data")).to eq(
"feed" => [
{"cachedAvatarUrl" => "http://example.com/img/posts/#{post.id}", "id" => post.id.to_s},
{"cachedAvatarUrl" => "http://example.com/img/users/#{user.id}", "id" => user.id.to_s}
]
)
end
end
end
end
7 changes: 6 additions & 1 deletion spec/graphql/fragment_cache/rails/cache_key_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

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

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

subject { described_class.call(object: object, query: query_obj, path: path) }
subject do
described_class.call(
object: object, query: query_obj, selected_type: selected_type, path: path
)
end

it "uses Cache.expand_cache_key" do
allow(ActiveSupport::Cache).to receive(:expand_cache_key).with(object) { "as:cache:key" }
Expand Down
2 changes: 1 addition & 1 deletion spec/support/models/post.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class Post
class << self
def find(id)
store.fetch(id.to_i) do
author = User.new(id: id, name: "User ##{id}")
author = User.fetch(id)
new(id: id, title: "Post ##{id}", author: author)
end
end
Expand Down
20 changes: 20 additions & 0 deletions spec/support/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@ class User
attr_reader :id
attr_accessor :name

class << self
def fetch(id)
store.fetch(id.to_i) do
new(id: id, name: "User ##{id}")
end
end

def all
@store.values
end

def create(id:, **attributes)
store[id] = new(id: id, **attributes)
end

def store
@store ||= {}
end
end

def initialize(id:, name:)
@id = id
@name = name
Expand Down
Loading

0 comments on commit ef25147

Please sign in to comment.