graphql-fragment_cache 0.1.4 → 0.1.5

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: f5ab2d2bd11b30a0c40476378f1b9ad0c064ccffec7f4595f9d81dbb0ad85eac
4
- data.tar.gz: 44c09dce73b8836f656f186291ed4a45b9cdab6c0ed8844dea38bb9fe9b276bc
3
+ metadata.gz: 69226c4789fd364bcd6bf0991ebd358c39a32fee383a8ce7445418d0af46eb04
4
+ data.tar.gz: d7a1901dd86a3311f60a02153422044c055ffd46b0297026ab9f434eca1667dd
5
5
  SHA512:
6
- metadata.gz: c225d6650aa383253eda036366f92d9a9ae2109c873cc9f4f69e9b736367c31c9c7b0c59c42ad830f4554184af75016239a0c3a739d2e3f1b4621fcbed9814e1
7
- data.tar.gz: 556d2eced9aef328be63f06de9404e78e958a06f5cba2394c1fda4d449c63d04399730978767a40a7b4544b41418f1bab96384aafddd819cd4c6bbad79b62af0
6
+ metadata.gz: 5a22448276b2618d0f85c67aa371fa20828ac24f3f943779d748214214a85d0a55c129b31928e96b904e6e1c8734f4cf5ef32bcb2debf38b9208bfea7fefc29f
7
+ data.tar.gz: 17033312f4e5c89c3a135da2c2e12b2980fc5b5582ce22b3089ae1e3b7c0e95de44ca1e19f42cb0a236b6fcd7b6ff9630bb300ee1592d9ec4cf6a759f7e82839
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.1.5 (2020-04-28)
6
+
7
+ - [PR#19](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/19) Add connections support ([@DmitryTsepelev][])
8
+ - [PR#18](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/18) Support aliases in cache key generation ([@palkan][], [@DmitryTsepelev][])
9
+
5
10
  ## 0.1.4 (2020-04-25)
6
11
 
7
12
  - Fix railtie to set up null store for tests ([@DmitryTsepelev][])
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,7 @@ 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, name| lkhd.selection_with_alias(name) }
63
128
 
64
129
  current_root.selections.to_selections_key
65
130
  end
@@ -68,12 +133,13 @@ module GraphQL
68
133
  lookahead = query.lookahead
69
134
 
70
135
  path.map { |field_name|
71
- lookahead = lookahead.selection(field_name)
136
+ lookahead = lookahead.selection_with_alias(field_name)
137
+ raise "Failed to look ahead the field: #{field_name}" if lookahead.is_a?(::GraphQL::Execution::Lookahead::NullLookahead)
72
138
 
73
- next field_name if lookahead.arguments.empty?
139
+ next lookahead.field.name if lookahead.arguments.empty?
74
140
 
75
141
  args = lookahead.arguments.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
76
- "#{field_name}(#{args})"
142
+ "#{lookahead.field.name}(#{args})"
77
143
  }.join("/")
78
144
  end
79
145
 
@@ -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,7 @@ 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, name| lkhd.selection_with_alias(name) }
63
128
 
64
129
  current_root.selections.to_selections_key
65
130
  end
@@ -68,12 +133,13 @@ module GraphQL
68
133
  lookahead = query.lookahead
69
134
 
70
135
  path.map { |field_name|
71
- lookahead = lookahead.selection(field_name)
136
+ lookahead = lookahead.selection_with_alias(field_name)
137
+ raise "Failed to look ahead the field: #{field_name}" if lookahead.is_a?(::GraphQL::Execution::Lookahead::NullLookahead)
72
138
 
73
- next field_name if lookahead.arguments.empty?
139
+ next lookahead.field.name if lookahead.arguments.empty?
74
140
 
75
141
  args = lookahead.arguments.map { "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
76
- "#{field_name}(#{args})"
142
+ "#{lookahead.field.name}(#{args})"
77
143
  }.join("/")
78
144
  end
79
145
 
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module FragmentCache
5
- VERSION = "0.1.4"
5
+ VERSION = "0.1.5"
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.4
4
+ version: 0.1.5
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-25 00:00:00.000000000 Z
11
+ date: 2020-04-28 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.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
28
  name: ruby-next-core
29
29
  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