graphql-fragment_cache 0.1.1 → 0.1.6

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: f11f53d4ee61fae49c514e17afe6eca9711c78578e7573ad79626cb222c2bdcf
4
- data.tar.gz: 2022a81f41d7318f74255c42a86ce022acee957f4ad60324d4f6638e0bc97044
3
+ metadata.gz: d83b3ff9d3e9cc91b6f013214d2cc4d97c6d2fc8bd377bf38d1f68dd7f935c23
4
+ data.tar.gz: d349157e8c6856c06b5a3ae76915399ae77fc04153093ef9551bb75b45629cb4
5
5
  SHA512:
6
- metadata.gz: 3a5772dc50d5e9bf398c42f7c50bb58fe565b6897a59a7488a289e5dbc0f594897dfb4af05f611770bdc9dd2693c1c78b3ae5a4e7e183df9c2ef076f321a1e9c
7
- data.tar.gz: 2570227cfeef325af92d6e32df92624d5c11c2b04f0cc9f964d72641c61e0a9ddaf3add4dec3260d98707096ec0ec00aecaba01b66137e35d44f66659c2323cb
6
+ metadata.gz: 759813c8b9ae4373d9890519aa88573eb9a504771eca25ea7d7ddc2a35b972f172e1fb7e86bbd2f52447886e3c781178cfc7491c9a7257ce436ae411a32395fb
7
+ data.tar.gz: 78c75d761fc3154dd4a4622a36c5b0f7bbc4b26c31eb4bf3e3c6be4423be8ae64923d7207f02217142c213fa80e039410e2d079403c0258efd82543d9866e41d
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.1.6 (2020-05-30)
6
+
7
+ - [PR#22](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/22) Properly cache entites inside collections ([@DmitryTsepelev][])
8
+
9
+ ## 0.1.5 (2020-04-28)
10
+
11
+ - [PR#19](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/19) Add connections support ([@DmitryTsepelev][])
12
+ - [PR#18](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/18) Support aliases in cache key generation ([@palkan][], [@DmitryTsepelev][])
13
+
14
+ ## 0.1.4 (2020-04-25)
15
+
16
+ - Fix railtie to set up null store for tests ([@DmitryTsepelev][])
17
+
18
+ ## 0.1.3 (2020-04-24)
19
+
20
+ - [PR#17](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/17) Properly build cache keys based on input arguments ([@DmitryTsepelev][])
21
+
22
+ ## 0.1.2 (2020-04-24)
23
+
24
+ - [PR#16](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/16) Railtie turns off caching in test environment ([@DmitryTsepelev][])
25
+ - [PR#15](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/15) Avoid extra resolving when resolved_value is not used for building cache key ([@DmitryTsepelev][])
26
+
5
27
  ## 0.1.1 (2020-04-15)
6
28
 
7
29
  - Fix using passed object as a cache key ([@palkan][])
data/README.md CHANGED
@@ -61,6 +61,15 @@ class QueryType < BaseObject
61
61
  end
62
62
  ```
63
63
 
64
+ 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:
65
+
66
+ ```ruby
67
+ class GraphqSchema < GraphQL::Schema
68
+ # ...
69
+ use GraphQL::Pagination::Connections
70
+ end
71
+ ```
72
+
64
73
  ## Cache key generation
65
74
 
66
75
  Cache keys consist of implicit and explicit (provided by user) parts.
@@ -266,10 +275,6 @@ class QueryType < BaseObject
266
275
  end
267
276
  ```
268
277
 
269
- ## Limitations
270
-
271
- - [Field aliases](https://spec.graphql.org/June2018/#sec-Field-Alias) are not currently supported (take a look at the failing spec [here](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/7))
272
-
273
278
  ## Credits
274
279
 
275
280
  Based on the original [gist](https://gist.github.com/palkan/faad9f6ff1db16fcdb1c071ec50e4190) by [@palkan](https://github.com/palkan) and [@ssnickolay](https://github.com/ssnickolay).
@@ -18,6 +18,71 @@ module GraphQL
18
18
  }.join(".")
19
19
  end
20
20
  end
21
+
22
+ refine ::GraphQL::Language::Nodes::AbstractNode do
23
+ def alias?(_)
24
+ false
25
+ end
26
+ end
27
+
28
+ refine ::GraphQL::Language::Nodes::Field do
29
+ def alias?(val)
30
+ self.alias == val
31
+ end
32
+ end
33
+
34
+ refine ::GraphQL::Execution::Lookahead do
35
+ def selection_with_alias(name, **kwargs)
36
+ return selection(name, **kwargs) if selects?(name, **kwargs)
37
+ alias_selection(name, **kwargs)
38
+ end
39
+
40
+ def alias_selection(name, selected_type: @selected_type, arguments: nil)
41
+ return alias_selections[name] if alias_selections.key?(name)
42
+
43
+ alias_node = lookup_alias_node(ast_nodes, name)
44
+ return ::GraphQL::Execution::Lookahead::NULL_LOOKAHEAD unless alias_node
45
+
46
+ next_field_name = alias_node.name
47
+
48
+ # From https://github.com/rmosolgo/graphql-ruby/blob/1a9a20f3da629e63ea8e5ee8400be82218f9edc3/lib/graphql/execution/lookahead.rb#L91
49
+ next_field_defn = get_class_based_field(selected_type, next_field_name)
50
+
51
+ alias_selections[name] =
52
+ if next_field_defn
53
+ next_nodes = []
54
+ arguments = @query.arguments_for(alias_node, next_field_defn)
55
+ arguments = arguments.is_a?(::GraphQL::Execution::Interpreter::Arguments) ? arguments.keyword_arguments : arguments
56
+ @ast_nodes.each do |ast_node|
57
+ ast_node.selections.each do |selection|
58
+ find_selected_nodes(selection, next_field_name, next_field_defn, arguments: arguments, matches: next_nodes)
59
+ end
60
+ end
61
+
62
+ if next_nodes.any?
63
+ ::GraphQL::Execution::Lookahead.new(query: @query, ast_nodes: next_nodes, field: next_field_defn, owner_type: selected_type)
64
+ else
65
+ ::GraphQL::Execution::Lookahead::NULL_LOOKAHEAD
66
+ end
67
+ else
68
+ ::GraphQL::Execution::Lookahead::NULL_LOOKAHEAD
69
+ end
70
+ end
71
+
72
+ def alias_selections
73
+ return @alias_selections if defined?(@alias_selections)
74
+ @alias_selections ||= {}
75
+ end
76
+
77
+ def lookup_alias_node(nodes, name)
78
+ return if nodes.empty?
79
+ nodes.find do |node|
80
+ return node if node.alias?(name)
81
+ child = lookup_alias_node(node.children, name)
82
+ return child if child
83
+ end
84
+ end
85
+ end
21
86
  })
22
87
 
23
88
  # Builds cache key for fragment
@@ -59,7 +124,12 @@ module GraphQL
59
124
 
60
125
  def selections_cache_key
61
126
  current_root =
62
- path.reduce(query.lookahead) { |lkhd, name| lkhd.selection(name) }
127
+ path.reduce(query.lookahead) { |lkhd, field_name|
128
+ # Handle cached fields inside collections:
129
+ next lkhd if field_name.is_a?(Integer)
130
+
131
+ lkhd.selection_with_alias(field_name)
132
+ }
63
133
 
64
134
  current_root.selections.to_selections_key
65
135
  end
@@ -68,15 +138,25 @@ module GraphQL
68
138
  lookahead = query.lookahead
69
139
 
70
140
  path.map { |field_name|
71
- lookahead = lookahead.selection(field_name)
141
+ # Handle cached fields inside collections:
142
+ next field_name if field_name.is_a?(Integer)
143
+
144
+ lookahead = lookahead.selection_with_alias(field_name)
145
+ raise "Failed to look ahead the field: #{field_name}" if lookahead.is_a?(::GraphQL::Execution::Lookahead::NullLookahead)
72
146
 
73
- next field_name if lookahead.arguments.empty?
147
+ next lookahead.field.name if lookahead.arguments.empty?
74
148
 
75
- args = lookahead.arguments.map { |_1, _2| "#{_1}:#{_2}" }.sort.join(",")
76
- "#{field_name}(#{args})"
149
+ args = lookahead.arguments.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
150
+ "#{lookahead.field.name}(#{args})"
77
151
  }.join("/")
78
152
  end
79
153
 
154
+ def traverse_argument(argument)
155
+ return argument unless argument.is_a?(GraphQL::Schema::InputObject)
156
+
157
+ "{#{argument.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")}}"
158
+ end
159
+
80
160
  def object_key(obj)
81
161
  obj._graphql_cache_key
82
162
  end
@@ -1,6 +1,6 @@
1
1
  require "ruby-next"
2
2
 
3
3
  require "ruby-next/language/setup"
4
- RubyNext::Language.setup_gem_load_path
4
+ RubyNext::Language.setup_gem_load_path(transpile: true)
5
5
 
6
6
  require "graphql/fragment_cache"
@@ -12,7 +12,6 @@ require "graphql/fragment_cache/instrumentation"
12
12
  require "graphql/fragment_cache/memory_store"
13
13
 
14
14
  require "graphql/fragment_cache/version"
15
- require "graphql/fragment_cache/railtie" if defined?(Rails::Railtie)
16
15
 
17
16
  module GraphQL
18
17
  # Plugin definition
@@ -57,3 +56,5 @@ module GraphQL
57
56
  self.cache_store = MemoryStore.new
58
57
  end
59
58
  end
59
+
60
+ require "graphql/fragment_cache/railtie" if defined?(Rails::Railtie)
@@ -18,6 +18,71 @@ module GraphQL
18
18
  }.join(".")
19
19
  end
20
20
  end
21
+
22
+ refine ::GraphQL::Language::Nodes::AbstractNode do
23
+ def alias?(_)
24
+ false
25
+ end
26
+ end
27
+
28
+ refine ::GraphQL::Language::Nodes::Field do
29
+ def alias?(val)
30
+ self.alias == val
31
+ end
32
+ end
33
+
34
+ refine ::GraphQL::Execution::Lookahead do
35
+ def selection_with_alias(name, **kwargs)
36
+ return selection(name, **kwargs) if selects?(name, **kwargs)
37
+ alias_selection(name, **kwargs)
38
+ end
39
+
40
+ def alias_selection(name, selected_type: @selected_type, arguments: nil)
41
+ return alias_selections[name] if alias_selections.key?(name)
42
+
43
+ alias_node = lookup_alias_node(ast_nodes, name)
44
+ return ::GraphQL::Execution::Lookahead::NULL_LOOKAHEAD unless alias_node
45
+
46
+ next_field_name = alias_node.name
47
+
48
+ # From https://github.com/rmosolgo/graphql-ruby/blob/1a9a20f3da629e63ea8e5ee8400be82218f9edc3/lib/graphql/execution/lookahead.rb#L91
49
+ next_field_defn = get_class_based_field(selected_type, next_field_name)
50
+
51
+ alias_selections[name] =
52
+ if next_field_defn
53
+ next_nodes = []
54
+ arguments = @query.arguments_for(alias_node, next_field_defn)
55
+ arguments = arguments.is_a?(::GraphQL::Execution::Interpreter::Arguments) ? arguments.keyword_arguments : arguments
56
+ @ast_nodes.each do |ast_node|
57
+ ast_node.selections.each do |selection|
58
+ find_selected_nodes(selection, next_field_name, next_field_defn, arguments: arguments, matches: next_nodes)
59
+ end
60
+ end
61
+
62
+ if next_nodes.any?
63
+ ::GraphQL::Execution::Lookahead.new(query: @query, ast_nodes: next_nodes, field: next_field_defn, owner_type: selected_type)
64
+ else
65
+ ::GraphQL::Execution::Lookahead::NULL_LOOKAHEAD
66
+ end
67
+ else
68
+ ::GraphQL::Execution::Lookahead::NULL_LOOKAHEAD
69
+ end
70
+ end
71
+
72
+ def alias_selections
73
+ return @alias_selections if defined?(@alias_selections)
74
+ @alias_selections ||= {}
75
+ end
76
+
77
+ def lookup_alias_node(nodes, name)
78
+ return if nodes.empty?
79
+ nodes.find do |node|
80
+ return node if node.alias?(name)
81
+ child = lookup_alias_node(node.children, name)
82
+ return child if child
83
+ end
84
+ end
85
+ end
21
86
  })
22
87
 
23
88
  # Builds cache key for fragment
@@ -59,7 +124,12 @@ module GraphQL
59
124
 
60
125
  def selections_cache_key
61
126
  current_root =
62
- path.reduce(query.lookahead) { |lkhd, name| lkhd.selection(name) }
127
+ path.reduce(query.lookahead) { |lkhd, field_name|
128
+ # Handle cached fields inside collections:
129
+ next lkhd if field_name.is_a?(Integer)
130
+
131
+ lkhd.selection_with_alias(field_name)
132
+ }
63
133
 
64
134
  current_root.selections.to_selections_key
65
135
  end
@@ -68,15 +138,25 @@ module GraphQL
68
138
  lookahead = query.lookahead
69
139
 
70
140
  path.map { |field_name|
71
- lookahead = lookahead.selection(field_name)
141
+ # Handle cached fields inside collections:
142
+ next field_name if field_name.is_a?(Integer)
143
+
144
+ lookahead = lookahead.selection_with_alias(field_name)
145
+ raise "Failed to look ahead the field: #{field_name}" if lookahead.is_a?(::GraphQL::Execution::Lookahead::NullLookahead)
72
146
 
73
- next field_name if lookahead.arguments.empty?
147
+ next lookahead.field.name if lookahead.arguments.empty?
74
148
 
75
- args = lookahead.arguments.map { "#{_1}:#{_2}" }.sort.join(",")
76
- "#{field_name}(#{args})"
149
+ args = lookahead.arguments.map { "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
150
+ "#{lookahead.field.name}(#{args})"
77
151
  }.join("/")
78
152
  end
79
153
 
154
+ def traverse_argument(argument)
155
+ return argument unless argument.is_a?(GraphQL::Schema::InputObject)
156
+
157
+ "{#{argument.map { "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")}}"
158
+ end
159
+
80
160
  def object_key(obj)
81
161
  obj._graphql_cache_key
82
162
  end
@@ -34,20 +34,24 @@ module GraphQL
34
34
  @cache_key = @cache_options.delete(:cache_key)
35
35
  end
36
36
 
37
+ NOT_RESOLVED = Object.new
38
+
37
39
  def resolve(object:, arguments:, **_options)
38
- resolved_value = yield(object, arguments)
40
+ resolved_value = NOT_RESOLVED
39
41
 
40
42
  object_for_key = if @context_key
41
43
  Array(@context_key).map { |key| object.context[key] }
42
44
  elsif @cache_key == :object
43
45
  object.object
44
46
  elsif @cache_key == :value
45
- resolved_value
47
+ resolved_value = yield(object, arguments)
46
48
  end
47
49
 
48
50
  cache_fragment_options = @cache_options.merge(object: object_for_key)
49
51
 
50
- object.cache_fragment(cache_fragment_options) { resolved_value }
52
+ object.cache_fragment(cache_fragment_options) do
53
+ resolved_value == NOT_RESOLVED ? yield(object, arguments) : resolved_value
54
+ end
51
55
  end
52
56
  end
53
57
  end
@@ -6,7 +6,9 @@ module GraphQL
6
6
  module FragmentCache
7
7
  # Represents a single fragment to cache
8
8
  class Fragment
9
- attr_reader :options, :path, :context
9
+ attr_reader :options, :path, :context, :raw_connection
10
+
11
+ attr_writer :raw_connection
10
12
 
11
13
  def initialize(context, **options)
12
14
  @context = context
@@ -19,7 +21,7 @@ module GraphQL
19
21
  end
20
22
 
21
23
  def persist(final_value)
22
- value = resolve(final_value)
24
+ value = raw_connection || resolve(final_value)
23
25
  FragmentCache.cache_store.write(cache_key, value, **options)
24
26
  end
25
27
 
@@ -6,23 +6,78 @@ module GraphQL
6
6
  module FragmentCache
7
7
  using Ext
8
8
 
9
+ RawConnection = Struct.new(:items, :nodes, :paged_nodes_offset, :has_previous_page, :has_next_page)
10
+
9
11
  # Adds #cache_fragment method
10
12
  module ObjectHelpers
13
+ extend Forwardable
14
+
11
15
  NO_OBJECT = Object.new
12
16
 
17
+ def_delegator :field, :connection?
18
+
13
19
  def cache_fragment(object_to_cache = NO_OBJECT, **options, &block)
14
20
  raise ArgumentError, "Block or argument must be provided" unless block_given? || object_to_cache != NO_OBJECT
15
21
 
16
22
  options[:object] = object_to_cache if object_to_cache != NO_OBJECT
23
+
17
24
  fragment = Fragment.new(context, options)
18
25
 
19
26
  if (cached = fragment.read)
20
- return raw_value(cached)
27
+ return restore_cached_value(cached)
28
+ end
29
+
30
+ (block_given? ? block.call : object_to_cache).tap do |resolved_value|
31
+ cache_value(resolved_value, fragment)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def restore_cached_value(cached)
38
+ connection? ? restore_cached_connection(cached) : raw_value(cached)
39
+ end
40
+
41
+ def cache_value(resolved_value, fragment)
42
+ if connection?
43
+ unless context.schema.new_connections?
44
+ raise StandardError,
45
+ "GraphQL::Pagination::Connections should be enabled for connection caching"
46
+ end
47
+
48
+ connection = wrap_connection(resolved_value)
49
+
50
+ fragment.raw_connection = RawConnection.new(
51
+ connection.items,
52
+ connection.nodes,
53
+ connection.instance_variable_get(:@paged_nodes_offset),
54
+ connection.has_previous_page,
55
+ connection.has_next_page
56
+ )
21
57
  end
22
58
 
23
59
  context.fragments << fragment
60
+ end
61
+
62
+ def field
63
+ interpreter_context[:current_field]
64
+ end
65
+
66
+ def interpreter_context
67
+ @interpreter_context ||= context.namespace(:interpreter)
68
+ end
69
+
70
+ def restore_cached_connection(raw_connection)
71
+ wrap_connection(raw_connection.items).tap do |connection|
72
+ connection.instance_variable_set(:@nodes, raw_connection.nodes)
73
+ connection.instance_variable_set(:@paged_nodes_offset, raw_connection.paged_nodes_offset)
74
+ connection.instance_variable_set(:@has_previous_page, raw_connection.has_previous_page)
75
+ connection.instance_variable_set(:@has_next_page, raw_connection.has_next_page)
76
+ end
77
+ end
24
78
 
25
- block_given? ? block.call : object_to_cache
79
+ def wrap_connection(object)
80
+ context.schema.connections.wrap(field, object, interpreter_context[:current_arguments], context)
26
81
  end
27
82
  end
28
83
  end
@@ -23,6 +23,10 @@ module GraphQL
23
23
  end
24
24
 
25
25
  config.graphql_fragment_cache = Config
26
+
27
+ if ENV["RACK_ENV"] == "test" || ENV["RAILS_ENV"] == "test"
28
+ config.graphql_fragment_cache.store = :null_store
29
+ end
26
30
  end
27
31
  end
28
32
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module FragmentCache
5
- VERSION = "0.1.1"
5
+ VERSION = "0.1.6"
6
6
  end
7
7
  end
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: 0.1.1
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - DmitryTsepelev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-15 00:00:00.000000000 Z
11
+ date: 2020-05-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 1.10.3
19
+ version: 1.10.8
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.3
26
+ version: 1.10.8
27
27
  - !ruby/object:Gem::Dependency
28
- name: ruby-next-core
28
+ name: ruby-next
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 0.5.1
33
+ version: 0.7.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 0.5.1
40
+ version: 0.7.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: combustion
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +52,34 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '1.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activerecord
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sqlite3
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: rake
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -100,14 +128,14 @@ dependencies:
100
128
  requirements:
101
129
  - - ">="
102
130
  - !ruby/object:Gem::Version
103
- version: '0.5'
131
+ version: '0.6'
104
132
  type: :development
105
133
  prerelease: false
106
134
  version_requirements: !ruby/object:Gem::Requirement
107
135
  requirements:
108
136
  - - ">="
109
137
  - !ruby/object:Gem::Version
110
- version: '0.5'
138
+ version: '0.6'
111
139
  description: Fragment cache for graphql-ruby
112
140
  email:
113
141
  - dmitry.a.tsepelev@gmail.com