graphql-fragment_cache 0.1.1 → 0.1.6

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: 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