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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +1 -13
- data/lib/.rbnext/2.3/graphql/fragment_cache/cache_key_builder.rb +207 -0
- data/lib/.rbnext/2.3/graphql/fragment_cache/memory_store.rb +64 -0
- data/lib/.rbnext/2.7/graphql/fragment_cache/cache_key_builder.rb +207 -0
- data/lib/.rbnext/2.7/graphql/fragment_cache/ext/graphql_cache_key.rb +93 -0
- data/lib/graphql/fragment_cache/cache_key_builder.rb +11 -2
- data/lib/graphql/fragment_cache/field_extension.rb +2 -0
- data/lib/graphql/fragment_cache/fragment.rb +26 -4
- data/lib/graphql/fragment_cache/object_helpers.rb +2 -8
- data/lib/graphql/fragment_cache/schema/lazy_cache_resolver.rb +41 -0
- data/lib/graphql/fragment_cache/schema/tracer.rb +1 -1
- data/lib/graphql/fragment_cache/version.rb +1 -1
- data/lib/graphql/fragment_cache.rb +14 -13
- metadata +10 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: af4f8899ace77b66aaabd9af9292de07a40d2384ef8db4a24394e75bdd4be78d
|
4
|
+
data.tar.gz: 21b141a16d460b4d1b8b6cf1146e54898a87c45b4f670aa0ab6cbaac78dc48d3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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 =
|
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
|
-
|
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
|
|
@@ -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
|
-
|
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
|
-
|
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
|
26
|
+
return if context.schema.new_connections?
|
27
27
|
|
28
28
|
raise StandardError,
|
29
29
|
"GraphQL::Pagination::Connections should be enabled for connection caching"
|
@@ -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
|
54
|
-
|
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
|
60
|
-
|
61
|
-
|
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
|
-
|
67
|
-
|
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.
|
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-
|
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.
|
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.
|
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.
|
238
|
+
rubygems_version: 3.1.6
|
234
239
|
signing_key:
|
235
240
|
specification_version: 4
|
236
241
|
summary: Fragment cache for graphql-ruby
|