Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add union support #30

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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