graphql-fragment_cache 1.10.0 → 1.13.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: 9c0987cb1e081b19356cfd389164150d7678c2fff2a479986f56e157ac485e79
4
- data.tar.gz: 72cc6cf9a79c003be0c83dc60447840128ba1d8395e60171ce26a5eaf8190f2b
3
+ metadata.gz: af4f8899ace77b66aaabd9af9292de07a40d2384ef8db4a24394e75bdd4be78d
4
+ data.tar.gz: 21b141a16d460b4d1b8b6cf1146e54898a87c45b4f670aa0ab6cbaac78dc48d3
5
5
  SHA512:
6
- metadata.gz: 78be339e321b1c76066e63960d615d85fc286531ba663eef4af3c62baee8b3d598867047cf1c52ca6085b7fca3cbd86ec0038718548e014aff5c8273740a3c9a
7
- data.tar.gz: 005efd512fba6e4f91ba5aec08637ce058217f7c2e580ef4afa8c6323fef035a70c72a5abd74d418892f02b607b6e5fff288adfb9830ee9125985b852153775e
6
+ metadata.gz: 6af77b269f6712c5c39983a796c0f109d6098297de2ad2c2fd87b5f4b8c0e53f9b71d14a2d6e7d8eb0ec1f03c55f43f84f766cdc4663bcf0cd8c7c1148fef506
7
+ data.tar.gz: e396f56ee9182d1147662fb484264ead58ea34efb867a9c6be9d1d018b94f9fb69089b981cb510c693ecd142a9c1fc20280d34c26bab71d9dc40af45d471944b
data/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 1.13.0 (2022-09-12)
6
+
7
+ - [PR#83](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/83) Update Lookahead usage to support graphql-2.0.14 ([@DmitryTsepelev][])
8
+
9
+ ## 1.12.0 (2022-08-05)
10
+
11
+ - [PR#70](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/70), [PR#82](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/82) Add #read_multi for fragments ([@daukadolt][], [@frostmark][])
12
+
13
+ ## 1.11.0 (2022-02-26)
14
+
15
+ - [PR#79](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/79) Support graphql-ruby 2.0.0 ([@DmitryTsepelev][])
16
+
5
17
  ## 1.10.0 (2022-01-30)
6
18
 
7
19
  - [PR#77](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/77) Drop Ruby 2.5 support, add Ruby 3.0 ([@DmitryTsepelev][])
@@ -124,3 +136,5 @@
124
136
  [@bbugh]: https://github.com/bbugh
125
137
  [@jeromedalbert]: https://github.com/jeromedalbert
126
138
  [@mretzak]: https://github.com/mretzak
139
+ [@daukadolt]: https://github.com/daukadolt
140
+ [@frostmark]: https://github.com/frostmark
data/README.md CHANGED
@@ -17,13 +17,10 @@ end
17
17
 
18
18
  ## Getting started
19
19
 
20
- Add the gem to your Gemfile `gem 'graphql-fragment_cache'` and add the plugin to your schema class (make sure to turn interpreter mode on with AST analysis!):
20
+ Add the gem to your Gemfile `gem 'graphql-fragment_cache'` and add the plugin to your schema class:
21
21
 
22
22
  ```ruby
23
23
  class GraphqSchema < GraphQL::Schema
24
- use GraphQL::Execution::Interpreter
25
- use GraphQL::Analysis::AST
26
-
27
24
  use GraphQL::FragmentCache
28
25
 
29
26
  query QueryType
@@ -69,15 +66,6 @@ class QueryType < BaseObject
69
66
  end
70
67
  ```
71
68
 
72
- If you use [connections](https://graphql-ruby.org/pagination/connection_concepts.html) and plan to cache them—please turn on [brand new](https://github.com/rmosolgo/graphql-ruby/blob/master/lib/graphql/pagination/connections.rb#L5) connections hierarchy in your schema:
73
-
74
- ```ruby
75
- class GraphqSchema < GraphQL::Schema
76
- # ...
77
- use GraphQL::Pagination::Connections
78
- end
79
- ```
80
-
81
69
  ## Cache key generation
82
70
 
83
71
  Cache keys consist of the following parts: namespace, implicit key, and explicit key.
@@ -0,0 +1,207 @@
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?
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(object: nil, query:, path:, **options)
132
+ @object = object
133
+ @query = query
134
+ @schema = query.schema
135
+ @path = path
136
+ @options = options
137
+ end
138
+
139
+ def build
140
+ [
141
+ GraphQL::FragmentCache.namespace,
142
+ implicit_cache_key,
143
+ object_cache_key
144
+ ].compact.join("/")
145
+ end
146
+
147
+ private
148
+
149
+ def implicit_cache_key
150
+ Digest::SHA1.hexdigest("#{schema_cache_key}/#{query_cache_key}")
151
+ end
152
+
153
+ def schema_cache_key
154
+ @options.fetch(:schema_cache_key) { schema.schema_cache_key }
155
+ end
156
+
157
+ def query_cache_key
158
+ @options.fetch(:query_cache_key) { "#{path_cache_key}[#{selections_cache_key}]" }
159
+ end
160
+
161
+ def selections_cache_key
162
+ current_root =
163
+ path.reduce(query.lookahead) { |lkhd, field_name|
164
+ # Handle cached fields inside collections:
165
+ next lkhd if field_name.is_a?(Integer)
166
+
167
+ lkhd.selection_with_alias(field_name)
168
+ }
169
+
170
+ current_root.selections.to_selections_key
171
+ end
172
+
173
+ def path_cache_key
174
+ @options.fetch(:path_cache_key) do
175
+ lookahead = query.lookahead
176
+
177
+ path.map { |field_name|
178
+ # Handle cached fields inside collections:
179
+ next field_name if field_name.is_a?(Integer)
180
+
181
+ lookahead = lookahead.selection_with_alias(field_name)
182
+ raise "Failed to look ahead the field: #{field_name}" if lookahead.is_a?(::GraphQL::Execution::Lookahead::NullLookahead)
183
+
184
+ next lookahead.field.name if lookahead.arguments.empty?
185
+
186
+ args = lookahead.arguments.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
187
+ "#{lookahead.field.name}(#{args})"
188
+ }.join("/")
189
+ end
190
+ end
191
+
192
+ def traverse_argument(argument)
193
+ return argument unless argument.is_a?(GraphQL::Schema::InputObject)
194
+
195
+ "{#{argument.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")}}"
196
+ end
197
+
198
+ def object_cache_key
199
+ @options[:object_cache_key] || object_key(object)
200
+ end
201
+
202
+ def object_key(obj)
203
+ ((!obj.nil?) || nil) && obj._graphql_cache_key
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ using RubyNext
4
+
5
+ module GraphQL
6
+ module FragmentCache
7
+ # Memory adapter for storing cached fragments
8
+ class MemoryStore
9
+ using RubyNext
10
+
11
+ class Entry < Struct.new(:value, :expires_at, keyword_init: true)
12
+ def expired?
13
+ expires_at && expires_at < Time.now
14
+ end
15
+ end
16
+
17
+ attr_reader :default_expires_in
18
+
19
+ def initialize(expires_in: nil, **other)
20
+ raise ArgumentError, "Unsupported options: #{other.keys.join(",")}" unless other.empty?
21
+
22
+ @default_expires_in = expires_in
23
+ @storage = {}
24
+ end
25
+
26
+ def keys
27
+ storage.keys
28
+ end
29
+
30
+ def exist?(key)
31
+ storage.key?(key)
32
+ end
33
+
34
+ def read(key)
35
+ key = key.to_s
36
+ ((!storage[key].nil?) || nil) && storage[key].then do |entry|
37
+ if entry.expired?
38
+ delete(key)
39
+ next
40
+ end
41
+ entry.value
42
+ end
43
+ end
44
+
45
+ def write(key, value, expires_in: default_expires_in, **options)
46
+ key = key.to_s
47
+ @storage[key] = Entry.new(value: value, expires_at: expires_in ? Time.now + expires_in : nil)
48
+ end
49
+
50
+ def delete(key)
51
+ key = key.to_s
52
+ storage.delete(key)
53
+ end
54
+
55
+ def clear
56
+ storage.clear
57
+ end
58
+
59
+ private
60
+
61
+ attr_reader :storage
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,207 @@
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?
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(object: nil, query:, path:, **options)
132
+ @object = object
133
+ @query = query
134
+ @schema = query.schema
135
+ @path = path
136
+ @options = options
137
+ end
138
+
139
+ def build
140
+ [
141
+ GraphQL::FragmentCache.namespace,
142
+ implicit_cache_key,
143
+ object_cache_key
144
+ ].compact.join("/")
145
+ end
146
+
147
+ private
148
+
149
+ def implicit_cache_key
150
+ Digest::SHA1.hexdigest("#{schema_cache_key}/#{query_cache_key}")
151
+ end
152
+
153
+ def schema_cache_key
154
+ @options.fetch(:schema_cache_key) { schema.schema_cache_key }
155
+ end
156
+
157
+ def query_cache_key
158
+ @options.fetch(:query_cache_key) { "#{path_cache_key}[#{selections_cache_key}]" }
159
+ end
160
+
161
+ def selections_cache_key
162
+ current_root =
163
+ path.reduce(query.lookahead) { |lkhd, field_name|
164
+ # Handle cached fields inside collections:
165
+ next lkhd if field_name.is_a?(Integer)
166
+
167
+ lkhd.selection_with_alias(field_name)
168
+ }
169
+
170
+ current_root.selections.to_selections_key
171
+ end
172
+
173
+ def path_cache_key
174
+ @options.fetch(:path_cache_key) do
175
+ lookahead = query.lookahead
176
+
177
+ path.map { |field_name|
178
+ # Handle cached fields inside collections:
179
+ next field_name if field_name.is_a?(Integer)
180
+
181
+ lookahead = lookahead.selection_with_alias(field_name)
182
+ raise "Failed to look ahead the field: #{field_name}" if lookahead.is_a?(::GraphQL::Execution::Lookahead::NullLookahead)
183
+
184
+ next lookahead.field.name if lookahead.arguments.empty?
185
+
186
+ args = lookahead.arguments.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
187
+ "#{lookahead.field.name}(#{args})"
188
+ }.join("/")
189
+ end
190
+ end
191
+
192
+ def traverse_argument(argument)
193
+ return argument unless argument.is_a?(GraphQL::Schema::InputObject)
194
+
195
+ "{#{argument.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")}}"
196
+ end
197
+
198
+ def object_cache_key
199
+ @options[:object_cache_key] || object_key(object)
200
+ end
201
+
202
+ def object_key(obj)
203
+ obj&._graphql_cache_key
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,93 @@
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
@@ -62,7 +62,12 @@ module GraphQL
62
62
  next_field_name = alias_node.name
63
63
 
64
64
  # From https://github.com/rmosolgo/graphql-ruby/blob/1a9a20f3da629e63ea8e5ee8400be82218f9edc3/lib/graphql/execution/lookahead.rb#L91
65
- next_field_defn = get_class_based_field(selected_type, next_field_name)
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
66
71
 
67
72
  alias_selections[name] =
68
73
  if next_field_defn
@@ -71,7 +76,11 @@ module GraphQL
71
76
  arguments = arguments.is_a?(::GraphQL::Execution::Interpreter::Arguments) ? arguments.keyword_arguments : arguments
72
77
  @ast_nodes.each do |ast_node|
73
78
  ast_node.selections.each do |selection|
74
- find_selected_nodes(selection, next_field_name, next_field_defn, arguments: arguments, matches: next_nodes)
79
+ if GraphQL::FragmentCache.graphql_ruby_after_2_0_13?
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
75
84
  end
76
85
  end
77
86
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "graphql/fragment_cache/schema/lazy_cache_resolver"
4
+
3
5
  module GraphQL
4
6
  module FragmentCache
5
7
  # Wraps resolver with cache method
@@ -8,19 +8,41 @@ module GraphQL
8
8
 
9
9
  # Represents a single fragment to cache
10
10
  class Fragment
11
+ NIL_IN_CACHE = Object.new
12
+
13
+ class << self
14
+ def read_multi(fragments)
15
+ unless FragmentCache.cache_store.respond_to?(:read_multi)
16
+ return fragments.map { |f| [f, f.read] }.to_h
17
+ end
18
+
19
+ fragments_to_cache_keys = fragments
20
+ .map { |f| [f, f.cache_key] }.to_h
21
+
22
+ cache_keys = fragments_to_cache_keys.values
23
+
24
+ cache_keys_to_values = FragmentCache.cache_store.read_multi(*cache_keys)
25
+
26
+ fetched_fragments_to_values = cache_keys_to_values
27
+ .map { |key, val| [fragments_to_cache_keys.key(key), val] }
28
+ .to_h
29
+
30
+ fetched_fragments_to_values
31
+ end
32
+ end
33
+
11
34
  attr_reader :options, :path, :context
12
35
 
13
36
  def initialize(context, **options)
14
37
  @context = context
38
+ @keep_in_context = options.delete(:keep_in_context)
15
39
  @options = options
16
40
  @path = interpreter_context[:current_path]
17
41
  end
18
42
 
19
- NIL_IN_CACHE = Object.new
20
-
21
- def read(keep_in_context = false)
43
+ def read
22
44
  return nil if context[:renew_cache] == true
23
- return read_from_context { value_from_cache } if keep_in_context
45
+ return read_from_context { value_from_cache } if @keep_in_context
24
46
 
25
47
  value_from_cache
26
48
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "graphql/fragment_cache/fragment"
4
+ require "graphql/fragment_cache/schema/lazy_cache_resolver"
4
5
 
5
6
  module GraphQL
6
7
  module FragmentCache
@@ -44,14 +45,7 @@ module GraphQL
44
45
 
45
46
  fragment = Fragment.new(context_to_use, **options)
46
47
 
47
- keep_in_context = options.delete(:keep_in_context)
48
- if (cached = fragment.read(keep_in_context))
49
- return cached == Fragment::NIL_IN_CACHE ? nil : raw_value(cached)
50
- end
51
-
52
- (block_given? ? block.call : object_to_cache).tap do |resolved_value|
53
- context_to_use.fragments << fragment
54
- end
48
+ GraphQL::FragmentCache::Schema::LazyCacheResolver.new(fragment, context_to_use, object_to_cache, &block)
55
49
  end
56
50
  end
57
51
  end
@@ -0,0 +1,41 @@
1
+ require "graphql/fragment_cache/fragment"
2
+
3
+ module GraphQL
4
+ module FragmentCache
5
+ module Schema
6
+ using Ext
7
+ class LazyCacheResolver
8
+ def initialize(fragment, query_ctx, object_to_cache, &block)
9
+ @fragment = fragment
10
+ @query_ctx = query_ctx
11
+ @object_to_cache = object_to_cache
12
+ @lazy_state = query_ctx[:lazy_cache_resolver_statez] ||= {
13
+ pending_fragments: Set.new,
14
+ resolved_fragments: {}
15
+ }
16
+ @block = block
17
+
18
+ @lazy_state[:pending_fragments] << @fragment
19
+ end
20
+
21
+ def resolve
22
+ unless @lazy_state[:resolved_fragments].key?(@fragment)
23
+ resolved_fragments = Fragment.read_multi(@lazy_state[:pending_fragments].to_a)
24
+ @lazy_state[:pending_fragments].clear
25
+ resolved_fragments.each { |key, value| @lazy_state[:resolved_fragments][key] = value }
26
+ end
27
+
28
+ cached = @lazy_state[:resolved_fragments][@fragment]
29
+
30
+ if cached
31
+ return cached == Fragment::NIL_IN_CACHE ? nil : GraphQL::Execution::Interpreter::RawValue.new(cached)
32
+ end
33
+
34
+ (@block ? @block.call : @object_to_cache).tap do |resolved_value|
35
+ @query_ctx.fragments << @fragment
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -23,7 +23,7 @@ module GraphQL
23
23
  end
24
24
 
25
25
  def verify_connections!(context)
26
- return if GraphQL::FragmentCache.graphql_ruby_1_12_or_later? || context.schema.new_connections?
26
+ return if context.schema.new_connections?
27
27
 
28
28
  raise StandardError,
29
29
  "GraphQL::Pagination::Connections should be enabled for connection caching"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module FragmentCache
5
- VERSION = "1.10.0"
5
+ VERSION = "1.13.0"
6
6
  end
7
7
  end
@@ -11,6 +11,7 @@ require "graphql/fragment_cache/connections/patch"
11
11
  require "graphql/fragment_cache/schema/patch"
12
12
  require "graphql/fragment_cache/schema/tracer"
13
13
  require "graphql/fragment_cache/schema/instrumentation"
14
+ require "graphql/fragment_cache/schema/lazy_cache_resolver"
14
15
 
15
16
  require "graphql/fragment_cache/memory_store"
16
17
 
@@ -30,6 +31,7 @@ module GraphQL
30
31
  schema_defn.tracer(Schema::Tracer)
31
32
  schema_defn.instrument(:query, Schema::Instrumentation)
32
33
  schema_defn.extend(Schema::Patch)
34
+ schema_defn.lazy_resolve(Schema::LazyCacheResolver, :resolve)
33
35
 
34
36
  GraphQL::Pagination::Connections.prepend(Connections::Patch)
35
37
  end
@@ -50,29 +52,28 @@ module GraphQL
50
52
  @cache_store = store
51
53
  end
52
54
 
53
- def graphql_ruby_1_12_or_later?
54
- Gem::Dependency.new("graphql", ">= 1.12.0").match?("graphql", GraphQL::VERSION)
55
+ def graphql_ruby_before_2_0?
56
+ check_graphql_version "< 2.0.0"
57
+ end
58
+
59
+ def graphql_ruby_after_2_0_13?
60
+ check_graphql_version "> 2.0.13"
55
61
  end
56
62
 
57
63
  private
58
64
 
59
- def verify_interpreter_and_analysis!(schema_defn)
60
- if graphql_ruby_1_12_or_later?
61
- unless schema_defn.interpreter?
62
- raise StandardError,
63
- "GraphQL::Execution::Execute should not be enabled for fragment caching"
64
- end
65
+ def check_graphql_version(predicate)
66
+ Gem::Dependency.new("graphql", predicate).match?("graphql", GraphQL::VERSION)
67
+ end
65
68
 
66
- unless schema_defn.analysis_engine == GraphQL::Analysis::AST
67
- raise StandardError,
68
- "GraphQL::Analysis should not be enabled for fragment caching"
69
- end
70
- else
69
+ def verify_interpreter_and_analysis!(schema_defn)
70
+ if graphql_ruby_before_2_0?
71
71
  unless schema_defn.interpreter?
72
72
  raise StandardError,
73
73
  "GraphQL::Execution::Interpreter should be enabled for fragment caching"
74
74
  end
75
75
 
76
+ puts "schema_defn.analysis_engine #{schema_defn.analysis_engine}"
76
77
  unless schema_defn.analysis_engine == GraphQL::Analysis::AST
77
78
  raise StandardError,
78
79
  "GraphQL::Analysis::AST should be enabled for fragment caching"
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.10.0
4
+ version: 1.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - DmitryTsepelev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-30 00:00:00.000000000 Z
11
+ date: 2022-09-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 1.10.8
19
+ version: 1.12.0
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: 1.10.8
26
+ version: 1.12.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: ruby-next
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -190,6 +190,10 @@ files:
190
190
  - README.md
191
191
  - bin/console
192
192
  - bin/setup
193
+ - lib/.rbnext/2.3/graphql/fragment_cache/cache_key_builder.rb
194
+ - lib/.rbnext/2.3/graphql/fragment_cache/memory_store.rb
195
+ - lib/.rbnext/2.7/graphql/fragment_cache/cache_key_builder.rb
196
+ - lib/.rbnext/2.7/graphql/fragment_cache/ext/graphql_cache_key.rb
193
197
  - lib/graphql-fragment_cache.rb
194
198
  - lib/graphql/fragment_cache.rb
195
199
  - lib/graphql/fragment_cache/cache_key_builder.rb
@@ -205,6 +209,7 @@ files:
205
209
  - lib/graphql/fragment_cache/rails/cache_key_builder.rb
206
210
  - lib/graphql/fragment_cache/railtie.rb
207
211
  - lib/graphql/fragment_cache/schema/instrumentation.rb
212
+ - lib/graphql/fragment_cache/schema/lazy_cache_resolver.rb
208
213
  - lib/graphql/fragment_cache/schema/patch.rb
209
214
  - lib/graphql/fragment_cache/schema/tracer.rb
210
215
  - lib/graphql/fragment_cache/version.rb
@@ -230,7 +235,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
230
235
  - !ruby/object:Gem::Version
231
236
  version: '0'
232
237
  requirements: []
233
- rubygems_version: 3.0.3.1
238
+ rubygems_version: 3.1.6
234
239
  signing_key:
235
240
  specification_version: 4
236
241
  summary: Fragment cache for graphql-ruby