Skip to content

Commit 67424d0

Browse files
authored
Add possibility to include and exclude arguments from generated cache key (#134)
* Add possibility to include and exclude arguments from generated cache key * Fix cache key builder options handling and tests * Add documentation * Fix rubocop
1 parent ff48de5 commit 67424d0

File tree

4 files changed

+153
-27
lines changed

4 files changed

+153
-27
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,34 @@ class QueryType < BaseObject
147147
end
148148
```
149149

150+
### Query arguments processing
151+
152+
You can influence the way that graphql arguments are include in the cache key.
153+
154+
A use case might be a `:renew_cache` parameter that can be used to force a cache rewrite,
155+
but should not be included with the cache key itself. Use `cache_key: { exclude_arguments: […]}`
156+
to specify a list of arguments to be excluded from the implicit cache key.
157+
158+
```ruby
159+
class QueryType < BaseObject
160+
field :post, PostType, null: true do
161+
argument :id, ID, required: true
162+
argument :renew_cache, Boolean, required: false
163+
end
164+
165+
def post(id:, renew_cache: false)
166+
if renew_cache
167+
context.scoped_set!(:renew_cache, true)
168+
end
169+
cache_fragment(cache_key: {exclude_arguments: [:renew_cache]}) { Post.find(id) }
170+
end
171+
end
172+
```
173+
174+
Likewise, you can use `cache_key: { include_arguments: […] }` to specify an allowlist of arguments
175+
to be included in the cache key. In this case all arguments for the cache key must be specified, including
176+
parent arguments of nested fields.
177+
150178
### User-provided cache key (custom key)
151179

152180
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):

lib/graphql/fragment_cache/cache_key_builder.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,26 @@ def path_cache_key
130130

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

133-
args = lookahead.arguments.map { "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
133+
args = lookahead.arguments.select { include_argument?(_1) }.map { "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
134134
"#{lookahead.field.name}(#{args})"
135135
}.join("/")
136136
end
137137
end
138138

139+
def include_argument?(argument_name)
140+
exclude_arguments = @options.dig(:cache_key, :exclude_arguments)
141+
return false if exclude_arguments&.include?(argument_name)
142+
143+
include_arguments = @options.dig(:cache_key, :include_arguments)
144+
return false if include_arguments && !include_arguments.include?(argument_name)
145+
146+
true
147+
end
148+
139149
def traverse_argument(argument)
140150
return argument unless argument.is_a?(GraphQL::Schema::InputObject)
141151

142-
"{#{argument.map { "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")}}"
152+
"{#{argument.map { include_argument?(_1) ? "#{_1}:#{traverse_argument(_2)}" : nil }.compact.sort.join(",")}}"
143153
end
144154

145155
def object_cache_key

spec/graphql/fragment_cache/cache_key_builder_spec.rb

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@
6666
end
6767

6868
specify { is_expected.to eq "graphql/cachedPost/schema_key-cachedPost(id:#{id})[id.title.author[id.name]]" }
69+
70+
context "when excluding arguments" do
71+
let(:options) { {cache_key: {exclude_arguments: [:id]}} }
72+
73+
specify { is_expected.to eq "graphql/cachedPost/schema_key-cachedPost()[id.title.author[id.name]]" }
74+
end
75+
76+
context "when including arguments" do
77+
let(:options) { {cache_key: {include_arguments: [:id]}} }
78+
79+
specify { is_expected.to eq "graphql/cachedPost/schema_key-cachedPost(id:#{id})[id.title.author[id.name]]" }
80+
end
6981
end
7082

7183
context "when cached field has aliased selections" do
@@ -109,7 +121,7 @@
109121

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

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

132144
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]]" }
145+
146+
context "when excluding arguments" do
147+
let(:options) { {cache_key: {exclude_arguments: [:int_arg]}} }
148+
149+
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]]" }
150+
end
151+
152+
context "when including arguments" do
153+
let(:options) { {cache_key: {include_arguments: [:complex_post_input, :input_with_id, :int_arg]}} }
154+
155+
specify { is_expected.to eq "graphql/cachedPostByComplexInput/schema_key-cachedPostByComplexInput(complex_post_input:{input_with_id:{int_arg:42}})[id.title.author[id.name]]" }
156+
end
133157
end
134158
end
135159

spec/graphql/fragment_cache/cacher_spec.rb

Lines changed: 88 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -76,23 +76,6 @@ def write_multi(hash, options)
7676
end
7777

7878
context "when cached fields have different options" do
79-
let(:schema) do
80-
build_schema do
81-
query(
82-
Class.new(Types::Query) {
83-
field :post, Types::Post, null: true do
84-
argument :id, GraphQL::Types::ID, required: true
85-
argument :cache_key, GraphQL::Types::String, required: true
86-
end
87-
88-
define_method(:post) { |id:, cache_key:|
89-
cache_fragment(query_cache_key: cache_key) { Post.find(id) }
90-
}
91-
}
92-
)
93-
end
94-
end
95-
9679
let(:query) do
9780
<<~GQL
9881
query getPost($id: ID!) {
@@ -107,16 +90,97 @@ def write_multi(hash, options)
10790
GQL
10891
end
10992

110-
it "uses #write_multi two times with different options" do
111-
execute_query
93+
context "when there options are passed to cache_fragment" do
94+
let(:schema) do
95+
build_schema do
96+
query(
97+
Class.new(Types::Query) {
98+
field :post, Types::Post, null: true do
99+
argument :id, GraphQL::Types::ID, required: true
100+
argument :cache_key, GraphQL::Types::String, required: true
101+
end
102+
103+
define_method(:post) { |id:, cache_key:|
104+
cache_fragment(query_cache_key: cache_key) { Post.find(id) }
105+
}
106+
}
107+
)
108+
end
109+
end
110+
111+
it "uses #write_multi two times with different query_cache_key options" do
112+
execute_query
113+
114+
args = []
115+
expect(GraphQL::FragmentCache.cache_store).to \
116+
have_received(:write_multi).exactly(2).times do |r, options|
117+
args << options
118+
end
119+
120+
expect(args).to eq([{query_cache_key: "1"}, {query_cache_key: "2"}])
121+
end
122+
end
123+
124+
context "when cache key is autogenerated" do
125+
let(:schema) do
126+
build_schema do
127+
query(
128+
Class.new(Types::Query) {
129+
field :post, Types::Post, null: true do
130+
argument :id, GraphQL::Types::ID, required: true
131+
argument :cache_key, GraphQL::Types::String, required: true
132+
end
133+
134+
define_method(:post) { |id:, cache_key:|
135+
cache_fragment { Post.find(id) }
136+
}
137+
}
138+
)
139+
end
140+
end
141+
142+
it "writes a cache key for each argument value" do
143+
execute_query
144+
145+
args = []
146+
expect(GraphQL::FragmentCache.cache_store).to \
147+
have_received(:write_multi).once.times do |hash, options|
148+
args << hash
149+
end
112150

113-
args = []
114-
expect(GraphQL::FragmentCache.cache_store).to \
115-
have_received(:write_multi).exactly(2).times do |r, options|
116-
args << options
151+
expect(args.first.keys.length).to be(2)
152+
end
153+
154+
context "when arguments are excluded" do
155+
let(:schema) do
156+
build_schema do
157+
query(
158+
Class.new(Types::Query) {
159+
field :post, Types::Post, null: true do
160+
argument :id, GraphQL::Types::ID, required: true
161+
argument :cache_key, GraphQL::Types::String, required: true
162+
end
163+
164+
define_method(:post) { |id:, cache_key:|
165+
cache_fragment(cache_key: {exclude_arguments: [:cache_key]}) { Post.find(id) }
166+
}
167+
}
168+
)
169+
end
117170
end
118171

119-
expect(args).to eq([{query_cache_key: "1"}, {query_cache_key: "2"}])
172+
it "writes only one cache key" do
173+
execute_query
174+
175+
args = []
176+
expect(GraphQL::FragmentCache.cache_store).to \
177+
have_received(:write_multi).once.times do |hash, options|
178+
args << hash
179+
end
180+
181+
expect(args.first.keys.length).to be(1)
182+
end
183+
end
120184
end
121185
end
122186
end

0 commit comments

Comments
 (0)