Skip to content
Merged
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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,34 @@ class QueryType < BaseObject
end
```

### Query arguments processing

You can influence the way that graphql arguments are include in the cache key.

A use case might be a `:renew_cache` parameter that can be used to force a cache rewrite,
but should not be included with the cache key itself. Use `cache_key: { exclude_arguments: […]}`
to specify a list of arguments to be excluded from the implicit cache key.

```ruby
class QueryType < BaseObject
field :post, PostType, null: true do
argument :id, ID, required: true
argument :renew_cache, Boolean, required: false
end

def post(id:, renew_cache: false)
if renew_cache
context.scoped_set!(:renew_cache, true)
end
cache_fragment(cache_key: {exclude_arguments: [:renew_cache]}) { Post.find(id) }
end
end
```

Likewise, you can use `cache_key: { include_arguments: […] }` to specify an allowlist of arguments
to be included in the cache key. In this case all arguments for the cache key must be specified, including
parent arguments of nested fields.

### User-provided cache key (custom key)

In most cases you want your cache key to depend on the resolved object (say, `ActiveRecord` model). You can do that by passing an argument to the `#cache_fragment` method in a similar way to Rails views [`#cache` method](https://guides.rubyonrails.org/caching_with_rails.html#fragment-caching):
Expand Down
14 changes: 12 additions & 2 deletions lib/graphql/fragment_cache/cache_key_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,16 +130,26 @@ def path_cache_key

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

args = lookahead.arguments.map { "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
args = lookahead.arguments.select { include_argument?(_1) }.map { "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
"#{lookahead.field.name}(#{args})"
}.join("/")
end
end

def include_argument?(argument_name)
exclude_arguments = @options.dig(:cache_key, :exclude_arguments)
return false if exclude_arguments&.include?(argument_name)

include_arguments = @options.dig(:cache_key, :include_arguments)
return false if include_arguments && !include_arguments.include?(argument_name)

true
end

def traverse_argument(argument)
return argument unless argument.is_a?(GraphQL::Schema::InputObject)

"{#{argument.map { "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")}}"
"{#{argument.map { include_argument?(_1) ? "#{_1}:#{traverse_argument(_2)}" : nil }.compact.sort.join(",")}}"
end

def object_cache_key
Expand Down
26 changes: 25 additions & 1 deletion spec/graphql/fragment_cache/cache_key_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@
end

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

context "when excluding arguments" do
let(:options) { {cache_key: {exclude_arguments: [:id]}} }

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

context "when including arguments" do
let(:options) { {cache_key: {include_arguments: [:id]}} }

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

context "when cached field has aliased selections" do
Expand Down Expand Up @@ -109,7 +121,7 @@

specify { is_expected.to eq "graphql/cachedPostByInput/schema_key-cachedPostByInput(input_with_id:{id:#{id},int_arg:42})[id.title.author[id.name]]" }

context "when argument is complext input" do
context "when argument is complex input" do
let(:query) do
<<~GQL
query GetPostByComplexInput($complexPostInput: ComplexPostInput!) {
Expand All @@ -130,6 +142,18 @@
let(:variables) { {complexPostInput: {stringArg: "woo", inputWithId: {id: id, intArg: 42}}} }

specify { is_expected.to eq "graphql/cachedPostByComplexInput/schema_key-cachedPostByComplexInput(complex_post_input:{input_with_id:{id:#{id},int_arg:42},string_arg:woo})[id.title.author[id.name]]" }

context "when excluding arguments" do
let(:options) { {cache_key: {exclude_arguments: [:int_arg]}} }

specify { is_expected.to eq "graphql/cachedPostByComplexInput/schema_key-cachedPostByComplexInput(complex_post_input:{input_with_id:{id:#{id}},string_arg:woo})[id.title.author[id.name]]" }
end

context "when including arguments" do
let(:options) { {cache_key: {include_arguments: [:complex_post_input, :input_with_id, :int_arg]}} }

specify { is_expected.to eq "graphql/cachedPostByComplexInput/schema_key-cachedPostByComplexInput(complex_post_input:{input_with_id:{int_arg:42}})[id.title.author[id.name]]" }
end
end
end

Expand Down
112 changes: 88 additions & 24 deletions spec/graphql/fragment_cache/cacher_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,23 +76,6 @@ def write_multi(hash, options)
end

context "when cached fields have different options" do
let(:schema) do
build_schema do
query(
Class.new(Types::Query) {
field :post, Types::Post, null: true do
argument :id, GraphQL::Types::ID, required: true
argument :cache_key, GraphQL::Types::String, required: true
end

define_method(:post) { |id:, cache_key:|
cache_fragment(query_cache_key: cache_key) { Post.find(id) }
}
}
)
end
end

let(:query) do
<<~GQL
query getPost($id: ID!) {
Expand All @@ -107,16 +90,97 @@ def write_multi(hash, options)
GQL
end

it "uses #write_multi two times with different options" do
execute_query
context "when there options are passed to cache_fragment" do
let(:schema) do
build_schema do
query(
Class.new(Types::Query) {
field :post, Types::Post, null: true do
argument :id, GraphQL::Types::ID, required: true
argument :cache_key, GraphQL::Types::String, required: true
end

define_method(:post) { |id:, cache_key:|
cache_fragment(query_cache_key: cache_key) { Post.find(id) }
}
}
)
end
end

it "uses #write_multi two times with different query_cache_key options" do
execute_query

args = []
expect(GraphQL::FragmentCache.cache_store).to \
have_received(:write_multi).exactly(2).times do |r, options|
args << options
end

expect(args).to eq([{query_cache_key: "1"}, {query_cache_key: "2"}])
end
end

context "when cache key is autogenerated" do
let(:schema) do
build_schema do
query(
Class.new(Types::Query) {
field :post, Types::Post, null: true do
argument :id, GraphQL::Types::ID, required: true
argument :cache_key, GraphQL::Types::String, required: true
end

define_method(:post) { |id:, cache_key:|
cache_fragment { Post.find(id) }
}
}
)
end
end

it "writes a cache key for each argument value" do
execute_query

args = []
expect(GraphQL::FragmentCache.cache_store).to \
have_received(:write_multi).once.times do |hash, options|
args << hash
end

args = []
expect(GraphQL::FragmentCache.cache_store).to \
have_received(:write_multi).exactly(2).times do |r, options|
args << options
expect(args.first.keys.length).to be(2)
end

context "when arguments are excluded" do
let(:schema) do
build_schema do
query(
Class.new(Types::Query) {
field :post, Types::Post, null: true do
argument :id, GraphQL::Types::ID, required: true
argument :cache_key, GraphQL::Types::String, required: true
end

define_method(:post) { |id:, cache_key:|
cache_fragment(cache_key: {exclude_arguments: [:cache_key]}) { Post.find(id) }
}
}
)
end
end

expect(args).to eq([{query_cache_key: "1"}, {query_cache_key: "2"}])
it "writes only one cache key" do
execute_query

args = []
expect(GraphQL::FragmentCache.cache_store).to \
have_received(:write_multi).once.times do |hash, options|
args << hash
end

expect(args.first.keys.length).to be(1)
end
end
end
end
end
Expand Down