graphql-fragment_cache 1.10.0 → 1.13.0

Sign up to get free protection for your applications and to get access to all the features.
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