graphql-fragment_cache 1.20.5 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 241af11e5f9aa966c8c544a72a4dc2a92f63267e503636962b198b658727c9d8
4
- data.tar.gz: 4e4abd9fbc612dcdbd534466e3ea86f72f990117a98970573b1ffc66d4305bfc
3
+ metadata.gz: c2f7385deb55dd824ed24ca60059bba372ad645bd2d1a183dee49089b2788648
4
+ data.tar.gz: 991e976fc7d2a6451e7362e6d899829511d4979544aaf2f01cff25b210782f47
5
5
  SHA512:
6
- metadata.gz: 994673b1241a0de72655671fc7b3de19fa182191ed41263c184138a5a79ef338e8d167d51d54193131bbd3fb4af281caa72df3308748cf98a2f8478fb40b7029
7
- data.tar.gz: a2d13a3eb6f124cac0e5820afeac651c29e7b087649a350fe92dc29ba719b3bd5257fed7896a15a3e5332ba6502275096f36cd991b2aad0696846bcb52acb840
6
+ metadata.gz: 07ac9a5d1ba73da5561a7c433499486986ccbc9b70c07ea2aaf22b4781d331cd16ca8e9e05005cea2031d5955a188cb5d96431d04439ae229ad97441d913627c
7
+ data.tar.gz: b9fb29e2a1d1a3bf24808b6186802d4c093764d11a5b80b07e6db8cb97c9454dbff6b878bd5c3767288ff551ee560573ce8ec3834fb01375a3a24aee312bad1c
data/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 1.22.0 (2025-02-20)
6
+
7
+ - [PR#134](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/134) Add possibility to include and exclude arguments from generated cache key ([@mgruner][])
8
+
9
+ ## 1.21.0 (2025-02-01)
10
+
11
+ - [PR#130](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/130) Dataloader support ([@DmitryTsepelev][])
12
+ - [PR#125](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/125) Introduce cache lookup instrumentation hook ([@danielhartnell][])
13
+
5
14
  ## 1.20.5 (2024-11-02)
6
15
 
7
16
  - [PR#120](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/120) Fix warning on ActiveSupport::Cache.format_version ([@Drowze][])
@@ -211,3 +220,5 @@
211
220
  [@diegofigueroa]: https://github.com/diegofigueroa
212
221
  [@noma4i]: https://github.com/noma4i
213
222
  [@Drowze]: https://github.com/Drowze
223
+ [@danielhartnell]: https://github.com/danielhartnell
224
+ [@mgruner]: https://github.com/mgruner
data/README.md CHANGED
@@ -147,6 +147,34 @@ class QueryType < BaseObject
147
147
  end
148
148
  ```
149
149
 
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
+
150
178
  ### User-provided cache key (custom key)
151
179
 
152
180
  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):
@@ -381,6 +409,34 @@ class QueryType < BaseObject
381
409
  end
382
410
  ```
383
411
 
412
+ ## Dataloader
413
+
414
+ If you are using [Dataloader](https://graphql-ruby.org/dataloader/overview.html), you will need to let the gem know using `dataloader: true`:
415
+
416
+ ```ruby
417
+ class PostType < BaseObject
418
+ field :author, User, null: false
419
+
420
+ def author
421
+ cache_fragment(dataloader: true) do
422
+ dataloader.with(AuthorDataloaderSource).load(object.id)
423
+ end
424
+ end
425
+ end
426
+
427
+ # or
428
+
429
+ class PostType < BaseObject
430
+ field :author, User, null: false, cache_fragment: {dataloader: true}
431
+
432
+ def author
433
+ dataloader.with(AuthorDataloaderSource).load(object.id)
434
+ end
435
+ end
436
+ ```
437
+
438
+ The problem is that I didn't find a way to detect that dataloader (and, therefore, Fiber) is used, and the block is forced to resolve, causing the N+1 inside the Dataloader Source class.
439
+
384
440
  ## How to use `#cache_fragment` in extensions (and other places where context is not available)
385
441
 
386
442
  If you want to call `#cache_fragment` from places other that fields or resolvers, you'll need to pass `context` explicitly and turn on `raw_value` support. For instance, let's take a look at this extension:
@@ -446,6 +502,30 @@ Cache processing can be disabled if needed. For example:
446
502
  GraphQL::FragmentCache.enabled = false if Rails.env.test?
447
503
  ```
448
504
 
505
+ ## Cache lookup monitoring
506
+
507
+ It may be useful to capture cache lookup events. When monitoring is enabled, the `cache_key`, `operation_name`, `path` and a boolean indicating a cache hit or miss will be sent to a `cache_lookup_event` method. This method can be implemented in your application to handle the event.
508
+
509
+ Example handler defined in a Rails initializer:
510
+
511
+ ```ruby
512
+ module GraphQL
513
+ module FragmentCache
514
+ class Fragment
515
+ def self.cache_lookup_event(**args)
516
+ # Monitoring such as incrementing a cache hit counter metric
517
+ end
518
+ end
519
+ end
520
+ end
521
+ ```
522
+
523
+ Like managing caching itself, monitoring can be enabled if needed. It is disabled by default. For example:
524
+
525
+ ```ruby
526
+ GraphQL::FragmentCache.monitoring_enabled = true
527
+ ```
528
+
449
529
  ## Limitations
450
530
 
451
531
  1. `Schema#execute`, [graphql-batch](https://github.com/Shopify/graphql-batch) and _graphql-ruby-fragment_cache_ do not [play well](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/issues/45) together. The problem appears when `cache_fragment` is _inside_ the `.then` block:
@@ -477,7 +557,7 @@ end
477
557
  field :cached_avatar_url, String, null: false
478
558
 
479
559
  def cached_avatar_url
480
- cache_fragment(query_cache_key: "post_avatar_url(#{object.id})") { object.avatar_url }
560
+ cache_fragment(path_cache_key: "post_avatar_url(#{object.id})") { object.avatar_url }
481
561
  end
482
562
  ```
483
563
 
@@ -14,7 +14,7 @@ module GraphQL
14
14
  def traverse_argument(argument)
15
15
  return argument unless argument.is_a?(GraphQL::Schema::InputObject)
16
16
 
17
- "{#{argument.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")}}"
17
+ "{#{argument.map { |name, arg| "#{name}:#{traverse_argument(arg)}" }.sort.join(",")}}"
18
18
  end
19
19
 
20
20
  def to_selections_key
@@ -26,7 +26,7 @@ module GraphQL
26
26
  field_name = "#{field_alias}:#{field_name}" unless field_alias.empty?
27
27
 
28
28
  unless val.arguments.empty?
29
- args = val.arguments.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
29
+ args = val.arguments.map { |name, arg| "#{name}:#{traverse_argument(arg)}" }.sort.join(",")
30
30
  field_name += "(#{args})"
31
31
  end
32
32
 
@@ -53,64 +53,66 @@ module GraphQL
53
53
  alias_selection(name, **kwargs)
54
54
  end
55
55
 
56
- def alias_selection(name, selected_type: @selected_type, arguments: nil)
57
- return alias_selections[name] if alias_selections.key?(name)
56
+ if GraphRubyVersion.before_2_1_4?
57
+ def alias_selection(name, selected_type: @selected_type, arguments: nil)
58
+ return alias_selections[name] if alias_selections.key?(name)
58
59
 
59
- alias_node = lookup_alias_node(ast_nodes, name)
60
- return ::GraphQL::Execution::Lookahead::NULL_LOOKAHEAD unless alias_node
60
+ alias_node = lookup_alias_node(ast_nodes, name)
61
+ return ::GraphQL::Execution::Lookahead::NULL_LOOKAHEAD unless alias_node
61
62
 
62
- next_field_name = alias_node.name
63
+ next_field_name = alias_node.name
63
64
 
64
- # From https://github.com/rmosolgo/graphql-ruby/blob/1a9a20f3da629e63ea8e5ee8400be82218f9edc3/lib/graphql/execution/lookahead.rb#L91
65
- next_field_defn =
66
- if GraphQL::FragmentCache.graphql_ruby_before_2_0?
67
- get_class_based_field(selected_type, next_field_name)
68
- else
69
- @query.get_field(selected_type, next_field_name)
70
- end
65
+ # From https://github.com/rmosolgo/graphql-ruby/blob/1a9a20f3da629e63ea8e5ee8400be82218f9edc3/lib/graphql/execution/lookahead.rb#L91
66
+ next_field_defn =
67
+ if GraphRubyVersion.before_2_0?
68
+ get_class_based_field(selected_type, next_field_name)
69
+ else
70
+ @query.get_field(selected_type, next_field_name)
71
+ end
71
72
 
72
- alias_selections[name] =
73
- if next_field_defn
74
- next_nodes = []
75
- arguments = @query.arguments_for(alias_node, next_field_defn)
76
- arguments = arguments.is_a?(::GraphQL::Execution::Interpreter::Arguments) ? arguments.keyword_arguments : arguments
77
- @ast_nodes.each do |ast_node|
78
- ast_node.selections.each do |selection|
79
- if GraphQL::FragmentCache.graphql_ruby_after_2_0_13? && GraphQL::FragmentCache.graphql_ruby_before_2_1_4?
80
- find_selected_nodes(selection, next_field_defn, arguments: arguments, matches: next_nodes)
81
- else
82
- find_selected_nodes(selection, next_field_name, next_field_defn, arguments: arguments, matches: next_nodes)
73
+ alias_selections[name] =
74
+ if next_field_defn
75
+ next_nodes = []
76
+ arguments = @query.arguments_for(alias_node, next_field_defn)
77
+ arguments = arguments.is_a?(::GraphQL::Execution::Interpreter::Arguments) ? arguments.keyword_arguments : arguments
78
+ @ast_nodes.each do |ast_node|
79
+ ast_node.selections.each do |selection|
80
+ if GraphRubyVersion.after_2_0_13? && GraphRubyVersion.before_2_1_4?
81
+ find_selected_nodes(selection, next_field_defn, arguments: arguments, matches: next_nodes)
82
+ else
83
+ find_selected_nodes(selection, next_field_name, next_field_defn, arguments: arguments, matches: next_nodes)
84
+ end
83
85
  end
84
86
  end
85
- end
86
87
 
87
- if next_nodes.any?
88
- ::GraphQL::Execution::Lookahead.new(query: @query, ast_nodes: next_nodes, field: next_field_defn, owner_type: selected_type)
88
+ if next_nodes.any?
89
+ ::GraphQL::Execution::Lookahead.new(query: @query, ast_nodes: next_nodes, field: next_field_defn, owner_type: selected_type)
90
+ else
91
+ ::GraphQL::Execution::Lookahead::NULL_LOOKAHEAD
92
+ end
89
93
  else
90
94
  ::GraphQL::Execution::Lookahead::NULL_LOOKAHEAD
91
95
  end
92
- else
93
- ::GraphQL::Execution::Lookahead::NULL_LOOKAHEAD
94
- end
95
- end
96
+ end
96
97
 
97
- def alias_selections
98
- return @alias_selections if defined?(@alias_selections)
99
- @alias_selections ||= {}
100
- end
98
+ def alias_selections
99
+ return @alias_selections if defined?(@alias_selections)
100
+ @alias_selections ||= {}
101
+ end
101
102
 
102
- def lookup_alias_node(nodes, name)
103
- return if nodes.empty?
103
+ def lookup_alias_node(nodes, name)
104
+ return if nodes.empty?
104
105
 
105
- nodes.find do |node|
106
- if node.is_a?(GraphQL::Language::Nodes::FragmentSpread)
107
- node = @query.fragments[node.name]
108
- raise("Invariant: Can't look ahead to nonexistent fragment #{node.name} (found: #{@query.fragments.keys})") unless node
109
- end
106
+ nodes.find do |node|
107
+ if node.is_a?(GraphQL::Language::Nodes::FragmentSpread)
108
+ node = @query.fragments[node.name]
109
+ raise("Invariant: Can't look ahead to nonexistent fragment #{node.name} (found: #{@query.fragments.keys})") unless node
110
+ end
110
111
 
111
- return node if node.alias?(name)
112
- child = lookup_alias_node(node.children, name)
113
- return child if child
112
+ return node if node.alias?(name)
113
+ child = lookup_alias_node(node.children, name)
114
+ return child if child
115
+ end
114
116
  end
115
117
  end
116
118
  end
@@ -195,7 +197,7 @@ module GraphQL
195
197
 
196
198
  next lookahead.field.name if lookahead.arguments.empty?
197
199
 
198
- args = lookahead.arguments.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
200
+ args = lookahead.arguments.map { |name, arg| "#{name}:#{traverse_argument(arg)}" }.sort.join(",")
199
201
  "#{lookahead.field.name}(#{args})"
200
202
  }.join("/")
201
203
  end
@@ -204,7 +206,7 @@ module GraphQL
204
206
  def traverse_argument(argument)
205
207
  return argument unless argument.is_a?(GraphQL::Schema::InputObject)
206
208
 
207
- "{#{argument.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")}}"
209
+ "{#{argument.map { |name, arg| "#{name}:#{traverse_argument(arg)}" }.sort.join(",")}}"
208
210
  end
209
211
 
210
212
  def object_cache_key
@@ -3,8 +3,6 @@
3
3
  require "json"
4
4
  require "digest"
5
5
 
6
- using RubyNext
7
-
8
6
  module GraphQL
9
7
  module FragmentCache
10
8
  using Ext
@@ -52,76 +50,11 @@ module GraphQL
52
50
  return selection(name, **kwargs) if selects?(name, **kwargs)
53
51
  alias_selection(name, **kwargs)
54
52
  end
55
-
56
- if GraphRubyVersion.before_2_1_4?
57
- def alias_selection(name, selected_type: @selected_type, arguments: nil)
58
- return alias_selections[name] if alias_selections.key?(name)
59
-
60
- alias_node = lookup_alias_node(ast_nodes, name)
61
- return ::GraphQL::Execution::Lookahead::NULL_LOOKAHEAD unless alias_node
62
-
63
- next_field_name = alias_node.name
64
-
65
- # From https://github.com/rmosolgo/graphql-ruby/blob/1a9a20f3da629e63ea8e5ee8400be82218f9edc3/lib/graphql/execution/lookahead.rb#L91
66
- next_field_defn =
67
- if GraphRubyVersion.before_2_0?
68
- get_class_based_field(selected_type, next_field_name)
69
- else
70
- @query.get_field(selected_type, next_field_name)
71
- end
72
-
73
- alias_selections[name] =
74
- if next_field_defn
75
- next_nodes = []
76
- arguments = @query.arguments_for(alias_node, next_field_defn)
77
- arguments = arguments.is_a?(::GraphQL::Execution::Interpreter::Arguments) ? arguments.keyword_arguments : arguments
78
- @ast_nodes.each do |ast_node|
79
- ast_node.selections.each do |selection|
80
- if GraphRubyVersion.after_2_0_13? && GraphRubyVersion.before_2_1_4?
81
- find_selected_nodes(selection, next_field_defn, arguments: arguments, matches: next_nodes)
82
- else
83
- find_selected_nodes(selection, next_field_name, next_field_defn, arguments: arguments, matches: next_nodes)
84
- end
85
- end
86
- end
87
-
88
- if next_nodes.any?
89
- ::GraphQL::Execution::Lookahead.new(query: @query, ast_nodes: next_nodes, field: next_field_defn, owner_type: selected_type)
90
- else
91
- ::GraphQL::Execution::Lookahead::NULL_LOOKAHEAD
92
- end
93
- else
94
- ::GraphQL::Execution::Lookahead::NULL_LOOKAHEAD
95
- end
96
- end
97
-
98
- def alias_selections
99
- return @alias_selections if defined?(@alias_selections)
100
- @alias_selections ||= {}
101
- end
102
-
103
- def lookup_alias_node(nodes, name)
104
- return if nodes.empty?
105
-
106
- nodes.find do |node|
107
- if node.is_a?(GraphQL::Language::Nodes::FragmentSpread)
108
- node = @query.fragments[node.name]
109
- raise("Invariant: Can't look ahead to nonexistent fragment #{node.name} (found: #{@query.fragments.keys})") unless node
110
- end
111
-
112
- return node if node.alias?(name)
113
- child = lookup_alias_node(node.children, name)
114
- return child if child
115
- end
116
- end
117
- end
118
53
  end
119
54
  })
120
55
 
121
56
  # Builds cache key for fragment
122
57
  class CacheKeyBuilder
123
- using RubyNext
124
-
125
58
  class << self
126
59
  def call(**options)
127
60
  new(**options).build
@@ -197,16 +130,26 @@ module GraphQL
197
130
 
198
131
  next lookahead.field.name if lookahead.arguments.empty?
199
132
 
200
- 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(",")
201
134
  "#{lookahead.field.name}(#{args})"
202
135
  }.join("/")
203
136
  end
204
137
  end
205
138
 
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
+
206
149
  def traverse_argument(argument)
207
150
  return argument unless argument.is_a?(GraphQL::Schema::InputObject)
208
151
 
209
- "{#{argument.map { "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")}}"
152
+ "{#{argument.map { include_argument?(_1) ? "#{_1}:#{traverse_argument(_2)}" : nil }.compact.sort.join(",")}}"
210
153
  end
211
154
 
212
155
  def object_cache_key
@@ -16,12 +16,10 @@ module GraphQL
16
16
  return fragments.map { |f| [f, f.read] }.to_h
17
17
  end
18
18
 
19
- fragments_to_cache_keys = fragments
20
- .map { |f| [f, f.cache_key] }.to_h
19
+ fragments_to_cache_keys = fragments.map { |f| [f, f.cache_key] }.to_h
21
20
 
22
21
  # Filter out all the cache_keys for fragments with renew_cache: true in their context
23
- cache_keys = fragments_to_cache_keys
24
- .reject { |k, _v| k.context[:renew_cache] == true }.values
22
+ cache_keys = fragments_to_cache_keys.reject { |k, _v| k.context[:renew_cache] == true }.values
25
23
 
26
24
  # If there are cache_keys look up values with read_multi otherwise return an empty hash
27
25
  cache_keys_to_values = if cache_keys.empty?
@@ -30,9 +28,23 @@ module GraphQL
30
28
  FragmentCache.cache_store.read_multi(*cache_keys)
31
29
  end
32
30
 
31
+ if GraphQL::FragmentCache.monitoring_enabled
32
+ begin
33
+ fragments.map do |fragment|
34
+ cache_lookup_event(
35
+ cache_key: fragment.cache_key,
36
+ operation_name: fragment.context.query.operation_name,
37
+ path: fragment.path,
38
+ cache_hit: cache_keys_to_values.key?(fragment.cache_key)
39
+ )
40
+ end
41
+ rescue
42
+ # Allow cache_lookup_event to fail when we do not have all of the requested attributes
43
+ end
44
+ end
45
+
33
46
  # Fragmenst without values or with renew_cache: true in their context will have nil values like the read method
34
- fragments_to_cache_keys
35
- .map { |fragment, cache_key| [fragment, cache_keys_to_values[cache_key]] }.to_h
47
+ fragments_to_cache_keys.map { |fragment, cache_key| [fragment, cache_keys_to_values[cache_key]] }.to_h
36
48
  end
37
49
  end
38
50
 
@@ -87,6 +99,11 @@ module GraphQL
87
99
  def final_value
88
100
  @final_value ||= context.query.result["data"]
89
101
  end
102
+
103
+ def cache_lookup_event(**args)
104
+ # This method can be implemented in your application
105
+ # This provides a mechanism to monitor cache hits for a fragment
106
+ end
90
107
  end
91
108
  end
92
109
  end
@@ -1,24 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- using RubyNext
4
-
5
3
  module GraphQL
6
4
  module FragmentCache
7
5
  module GraphRubyVersion
8
6
  module_function
9
7
 
10
- def before_2_0?
11
- check_graphql_version "< 2.0.0"
12
- end
13
-
14
- def after_2_0_13?
15
- check_graphql_version "> 2.0.13"
16
- end
17
-
18
- def before_2_1_4?
19
- check_graphql_version "< 2.1.4"
20
- end
21
-
22
8
  def after_2_2_5?
23
9
  check_graphql_version "> 2.2.5"
24
10
  end
@@ -1,13 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- using RubyNext
4
-
5
3
  module GraphQL
6
4
  module FragmentCache
7
5
  # Memory adapter for storing cached fragments
8
6
  class MemoryStore
9
- using RubyNext
10
-
11
7
  class Entry < Struct.new(:value, :expires_at, keyword_init: true)
12
8
  def expired?
13
9
  expires_at && expires_at < Time.now
@@ -26,7 +26,9 @@ module GraphQL
26
26
 
27
27
  if ENV["RACK_ENV"] == "test" || ENV["RAILS_ENV"] == "test"
28
28
  initializer "graphql-fragment_cache" do
29
- config.graphql_fragment_cache.store = if Rails.version.to_f >= 7.0
29
+ config.graphql_fragment_cache.store = if Rails.version.to_f >= 8.0
30
+ [:null_store]
31
+ elsif Rails.version.to_f >= 7.0
30
32
  [:null_store, serializer: :marshal_7_0]
31
33
  else
32
34
  :null_store
@@ -16,6 +16,8 @@ module GraphQL
16
16
  @block = block
17
17
 
18
18
  @lazy_state[:pending_fragments] << @fragment
19
+
20
+ ensure_dataloader_resulution! if @fragment.options[:dataloader]
19
21
  end
20
22
 
21
23
  def resolve
@@ -35,6 +37,15 @@ module GraphQL
35
37
  @query_ctx.fragments << @fragment
36
38
  end
37
39
  end
40
+
41
+ private
42
+
43
+ def ensure_dataloader_resulution!
44
+ return if FragmentCache.cache_store.exist?(@fragment.cache_key)
45
+
46
+ @object_to_cache = @block.call
47
+ @block = nil
48
+ end
38
49
  end
39
50
  end
40
51
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module FragmentCache
5
- VERSION = "1.20.5"
5
+ VERSION = "1.22.0"
6
6
  end
7
7
  end
@@ -25,14 +25,13 @@ module GraphQL
25
25
  class << self
26
26
  attr_reader :cache_store
27
27
  attr_accessor :enabled
28
+ attr_accessor :monitoring_enabled
28
29
  attr_accessor :namespace
29
30
  attr_accessor :default_options
30
31
 
31
32
  attr_accessor :skip_cache_when_query_has_errors
32
33
 
33
34
  def use(schema_defn, options = {})
34
- verify_interpreter_and_analysis!(schema_defn)
35
-
36
35
  if GraphRubyVersion.after_2_2_5?
37
36
  schema_defn.trace_with(Schema::Instrumentation::Tracer)
38
37
  else
@@ -69,24 +68,11 @@ module GraphQL
69
68
  def check_graphql_version(predicate)
70
69
  Gem::Dependency.new("graphql", predicate).match?("graphql", GraphQL::VERSION)
71
70
  end
72
-
73
- def verify_interpreter_and_analysis!(schema_defn)
74
- if GraphRubyVersion.before_2_0?
75
- unless schema_defn.interpreter?
76
- raise StandardError,
77
- "GraphQL::Execution::Interpreter should be enabled for fragment caching"
78
- end
79
-
80
- unless schema_defn.analysis_engine == GraphQL::Analysis::AST
81
- raise StandardError,
82
- "GraphQL::Analysis::AST should be enabled for fragment caching"
83
- end
84
- end
85
- end
86
71
  end
87
72
 
88
73
  self.cache_store = MemoryStore.new
89
74
  self.enabled = true
75
+ self.monitoring_enabled = false
90
76
  self.namespace = "graphql"
91
77
  self.default_options = {}
92
78
  self.skip_cache_when_query_has_errors = false
@@ -1,6 +1 @@
1
- require "ruby-next"
2
-
3
- require "ruby-next/language/setup"
4
- RubyNext::Language.setup_gem_load_path(transpile: true)
5
-
6
1
  require "graphql/fragment_cache"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql-fragment_cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.20.5
4
+ version: 1.22.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - DmitryTsepelev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-02 00:00:00.000000000 Z
11
+ date: 2025-02-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -16,28 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 2.0.0
19
+ version: 2.1.4
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 2.0.0
27
- - !ruby/object:Gem::Dependency
28
- name: ruby-next
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: 0.15.0
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: 0.15.0
26
+ version: 2.1.4
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: combustion
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -70,16 +56,16 @@ dependencies:
70
56
  name: sqlite3
71
57
  requirement: !ruby/object:Gem::Requirement
72
58
  requirements:
73
- - - "~>"
59
+ - - ">="
74
60
  - !ruby/object:Gem::Version
75
- version: '1.4'
61
+ version: '0'
76
62
  type: :development
77
63
  prerelease: false
78
64
  version_requirements: !ruby/object:Gem::Requirement
79
65
  requirements:
80
- - - "~>"
66
+ - - ">="
81
67
  - !ruby/object:Gem::Version
82
- version: '1.4'
68
+ version: '0'
83
69
  - !ruby/object:Gem::Dependency
84
70
  name: rake
85
71
  requirement: !ruby/object:Gem::Requirement
@@ -122,20 +108,6 @@ dependencies:
122
108
  - - ">="
123
109
  - !ruby/object:Gem::Version
124
110
  version: '0'
125
- - !ruby/object:Gem::Dependency
126
- name: ruby-next
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0.10'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0.10'
139
111
  - !ruby/object:Gem::Dependency
140
112
  name: unparser
141
113
  requirement: !ruby/object:Gem::Requirement
@@ -193,8 +165,6 @@ files:
193
165
  - lib/.rbnext/2.3/graphql/fragment_cache/cache_key_builder.rb
194
166
  - lib/.rbnext/2.3/graphql/fragment_cache/memory_store.rb
195
167
  - lib/.rbnext/2.5/graphql/fragment_cache/cacher.rb
196
- - lib/.rbnext/2.7/graphql/fragment_cache/cache_key_builder.rb
197
- - lib/.rbnext/2.7/graphql/fragment_cache/ext/graphql_cache_key.rb
198
168
  - lib/graphql-fragment_cache.rb
199
169
  - lib/graphql/fragment_cache.rb
200
170
  - lib/graphql/fragment_cache/cache_key_builder.rb
@@ -230,14 +200,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
230
200
  requirements:
231
201
  - - ">="
232
202
  - !ruby/object:Gem::Version
233
- version: '3.0'
203
+ version: '3.1'
234
204
  required_rubygems_version: !ruby/object:Gem::Requirement
235
205
  requirements:
236
206
  - - ">="
237
207
  - !ruby/object:Gem::Version
238
208
  version: '0'
239
209
  requirements: []
240
- rubygems_version: 3.4.6
210
+ rubygems_version: 3.5.22
241
211
  signing_key:
242
212
  specification_version: 4
243
213
  summary: Fragment cache for graphql-ruby
@@ -1,219 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "digest"
5
-
6
- using RubyNext
7
-
8
- module GraphQL
9
- module FragmentCache
10
- using Ext
11
-
12
- using(Module.new {
13
- refine Array do
14
- def traverse_argument(argument)
15
- return argument unless argument.is_a?(GraphQL::Schema::InputObject)
16
-
17
- "{#{argument.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")}}"
18
- end
19
-
20
- def to_selections_key
21
- map { |val|
22
- children = val.selections.empty? ? "" : "[#{val.selections.to_selections_key}]"
23
-
24
- field_name = val.field.name
25
- field_alias = val.ast_nodes.map(&:alias).join
26
- field_name = "#{field_alias}:#{field_name}" unless field_alias.empty?
27
-
28
- unless val.arguments.empty?
29
- args = val.arguments.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
30
- field_name += "(#{args})"
31
- end
32
-
33
- "#{field_name}#{children}"
34
- }.join(".")
35
- end
36
- end
37
-
38
- refine ::GraphQL::Language::Nodes::AbstractNode do
39
- def alias?(_)
40
- false
41
- end
42
- end
43
-
44
- refine ::GraphQL::Language::Nodes::Field do
45
- def alias?(val)
46
- self.alias == val
47
- end
48
- end
49
-
50
- refine ::GraphQL::Execution::Lookahead do
51
- def selection_with_alias(name, **kwargs)
52
- return selection(name, **kwargs) if selects?(name, **kwargs)
53
- alias_selection(name, **kwargs)
54
- end
55
-
56
- def alias_selection(name, selected_type: @selected_type, arguments: nil)
57
- return alias_selections[name] if alias_selections.key?(name)
58
-
59
- alias_node = lookup_alias_node(ast_nodes, name)
60
- return ::GraphQL::Execution::Lookahead::NULL_LOOKAHEAD unless alias_node
61
-
62
- next_field_name = alias_node.name
63
-
64
- # From https://github.com/rmosolgo/graphql-ruby/blob/1a9a20f3da629e63ea8e5ee8400be82218f9edc3/lib/graphql/execution/lookahead.rb#L91
65
- next_field_defn =
66
- if GraphQL::FragmentCache.graphql_ruby_before_2_0?
67
- get_class_based_field(selected_type, next_field_name)
68
- else
69
- @query.get_field(selected_type, next_field_name)
70
- end
71
-
72
- alias_selections[name] =
73
- if next_field_defn
74
- next_nodes = []
75
- arguments = @query.arguments_for(alias_node, next_field_defn)
76
- arguments = arguments.is_a?(::GraphQL::Execution::Interpreter::Arguments) ? arguments.keyword_arguments : arguments
77
- @ast_nodes.each do |ast_node|
78
- ast_node.selections.each do |selection|
79
- if GraphQL::FragmentCache.graphql_ruby_after_2_0_13? && GraphQL::FragmentCache.graphql_ruby_before_2_1_4?
80
- find_selected_nodes(selection, next_field_defn, arguments: arguments, matches: next_nodes)
81
- else
82
- find_selected_nodes(selection, next_field_name, next_field_defn, arguments: arguments, matches: next_nodes)
83
- end
84
- end
85
- end
86
-
87
- if next_nodes.any?
88
- ::GraphQL::Execution::Lookahead.new(query: @query, ast_nodes: next_nodes, field: next_field_defn, owner_type: selected_type)
89
- else
90
- ::GraphQL::Execution::Lookahead::NULL_LOOKAHEAD
91
- end
92
- else
93
- ::GraphQL::Execution::Lookahead::NULL_LOOKAHEAD
94
- end
95
- end
96
-
97
- def alias_selections
98
- return @alias_selections if defined?(@alias_selections)
99
- @alias_selections ||= {}
100
- end
101
-
102
- def lookup_alias_node(nodes, name)
103
- return if nodes.empty?
104
-
105
- nodes.find do |node|
106
- if node.is_a?(GraphQL::Language::Nodes::FragmentSpread)
107
- node = @query.fragments[node.name]
108
- raise("Invariant: Can't look ahead to nonexistent fragment #{node.name} (found: #{@query.fragments.keys})") unless node
109
- end
110
-
111
- return node if node.alias?(name)
112
- child = lookup_alias_node(node.children, name)
113
- return child if child
114
- end
115
- end
116
- end
117
- })
118
-
119
- # Builds cache key for fragment
120
- class CacheKeyBuilder
121
- using RubyNext
122
-
123
- class << self
124
- def call(**options)
125
- new(**options).build
126
- end
127
- end
128
-
129
- attr_reader :query, :path, :object, :schema
130
-
131
- def initialize(query:, path:, object: nil, **options)
132
- @object = object
133
- @query = query
134
- @schema = query.schema
135
- @path = path
136
- @options = options
137
- end
138
-
139
- def build
140
- key_parts = [
141
- GraphQL::FragmentCache.namespace,
142
- simple_path_cache_key,
143
- implicit_cache_key,
144
- object_cache_key
145
- ]
146
-
147
- key_parts
148
- .compact
149
- .map { |key_part| key_part.tr("/", "-") }
150
- .join("/")
151
- end
152
-
153
- private
154
-
155
- def implicit_cache_key
156
- Digest::SHA1.hexdigest("#{schema_cache_key}/#{query_cache_key}")
157
- end
158
-
159
- def schema_cache_key
160
- @options.fetch(:schema_cache_key) { schema.schema_cache_key }
161
- end
162
-
163
- def query_cache_key
164
- @options.fetch(:query_cache_key) { "#{path_cache_key}[#{selections_cache_key}]" }
165
- end
166
-
167
- def selections_cache_key
168
- current_root =
169
- path.reduce(query.lookahead) { |lkhd, field_name|
170
- # Handle cached fields inside collections:
171
- next lkhd if field_name.is_a?(Integer)
172
-
173
- lkhd.selection_with_alias(field_name)
174
- }
175
-
176
- current_root.selections.to_selections_key
177
- end
178
-
179
- def simple_path_cache_key
180
- return if path_cache_key.nil?
181
-
182
- path_cache_key.split("(").first
183
- end
184
-
185
- def path_cache_key
186
- @path_cache_key ||= @options.fetch(:path_cache_key) do
187
- lookahead = query.lookahead
188
-
189
- path.map { |field_name|
190
- # Handle cached fields inside collections:
191
- next field_name if field_name.is_a?(Integer)
192
-
193
- lookahead = lookahead.selection_with_alias(field_name)
194
- raise "Failed to look ahead the field: #{field_name}" if lookahead.is_a?(::GraphQL::Execution::Lookahead::NullLookahead)
195
-
196
- next lookahead.field.name if lookahead.arguments.empty?
197
-
198
- args = lookahead.arguments.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
199
- "#{lookahead.field.name}(#{args})"
200
- }.join("/")
201
- end
202
- end
203
-
204
- def traverse_argument(argument)
205
- return argument unless argument.is_a?(GraphQL::Schema::InputObject)
206
-
207
- "{#{argument.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")}}"
208
- end
209
-
210
- def object_cache_key
211
- @options[:object_cache_key] || object_key(object)
212
- end
213
-
214
- def object_key(obj)
215
- obj&._graphql_cache_key
216
- end
217
- end
218
- end
219
- end
@@ -1,93 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GraphQL
4
- module FragmentCache
5
- module Ext
6
- # Adds #_graphql_cache_key method to Object,
7
- # which just call #graphql_cache_key or #cache_key.
8
- #
9
- # For other core classes returns string representation.
10
- #
11
- # Raises ArgumentError otherwise.
12
- #
13
- # We use a refinement to avoid case/if statements for type checking
14
- refine Object do
15
- def _graphql_cache_key
16
- return graphql_cache_key if respond_to?(:graphql_cache_key)
17
- return cache_key if respond_to?(:cache_key)
18
- return to_a._graphql_cache_key if respond_to?(:to_a)
19
-
20
- to_s
21
- end
22
- end
23
-
24
- refine Array do
25
- def _graphql_cache_key
26
- map { |_1| _1._graphql_cache_key }.join("/")
27
- end
28
- end
29
-
30
- refine NilClass do
31
- def _graphql_cache_key
32
- ""
33
- end
34
- end
35
-
36
- refine TrueClass do
37
- def _graphql_cache_key
38
- "t"
39
- end
40
- end
41
-
42
- refine FalseClass do
43
- def _graphql_cache_key
44
- "f"
45
- end
46
- end
47
-
48
- refine String do
49
- def _graphql_cache_key
50
- self
51
- end
52
- end
53
-
54
- refine Symbol do
55
- def _graphql_cache_key
56
- to_s
57
- end
58
- end
59
-
60
- if RUBY_PLATFORM.match?(/java/i)
61
- refine Integer do
62
- def _graphql_cache_key
63
- to_s
64
- end
65
- end
66
-
67
- refine Float do
68
- def _graphql_cache_key
69
- to_s
70
- end
71
- end
72
- else
73
- refine Numeric do
74
- def _graphql_cache_key
75
- to_s
76
- end
77
- end
78
- end
79
-
80
- refine Time do
81
- def _graphql_cache_key
82
- to_s
83
- end
84
- end
85
-
86
- refine Module do
87
- def _graphql_cache_key
88
- name
89
- end
90
- end
91
- end
92
- end
93
- end