graphql_includable 0.2.12 → 0.3.0.alpha.1

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
  SHA1:
3
- metadata.gz: 7e15a080b64989dcfc9dfb9e9df39cf8f37c96ee
4
- data.tar.gz: c0c53dcc1a61dbaab44b9c585bb5a486d0d1223b
3
+ metadata.gz: e012854a25b8c65723c9903dda037159e41999e1
4
+ data.tar.gz: bb0ae8ce407dc874d6ae8c9361cab4ca6010af0b
5
5
  SHA512:
6
- metadata.gz: c7d535ba4e020ff2eac12c1aa81f63de740af241dfc3b1669167667afb8bf8ed6ca1edd8d761b37f847555d03a64e8c23691a91b857a93e0c985d561a0a091b1
7
- data.tar.gz: 320b95f48de65e16b42033b2ef45cd921d86c57e70e98ada5600155d7204a912665db781203f090d011d4b83ed8e871825a751c43491efc427a79232f2507387
6
+ metadata.gz: d2aee7ee36e05bc87d308f4576fb48ac0205105e612ee2ee140865bb54dae043376bec46af4d237992fce32eec5a9ecb248b70e7db8aeafb653b0c299bd53690
7
+ data.tar.gz: a014ad366b299f89037d0fad3e0d6b1a87d614f71c691696b55bbd7ea1a4a34ab36969ed79b0db5fdb93be50d11427c5126a477c9f8b2e7db199c3495cc59869
@@ -7,9 +7,9 @@ module GraphQLIncludable
7
7
  module ClassMethods
8
8
  def includes_from_graphql(ctx)
9
9
  node = Resolver.find_node_by_return_type(ctx.irep_node, name)
10
- generated_includes = Resolver.includes_for_node(node)
11
- puts("INCLUDES!!!: #{generated_includes}")
12
- includes(generated_includes)
10
+ manager = IncludesManager.new(nil)
11
+ Resolver.includes_for_node(node, manager)
12
+ includes(manager.includes)
13
13
  rescue => e
14
14
  Rails.logger.error(e)
15
15
  self
@@ -0,0 +1,24 @@
1
+ module GraphQLIncludable
2
+ module Relay
3
+ class EdgeWithNode < GraphQL::Relay::Edge
4
+ def initialize(node, connection)
5
+ @edge = node
6
+ edge_to_node_property = connection.field.type.fields['edges'].metadata[:edge_to_node_property]
7
+ node = @edge.public_send(edge_to_node_property)
8
+ super(node, connection)
9
+ end
10
+
11
+ def method_missing(method_name, *args, &block)
12
+ if @edge.respond_to?(method_name)
13
+ @edge.send(method_name, *args, &block)
14
+ else
15
+ super
16
+ end
17
+ end
18
+
19
+ def respond_to_missing(method_name, include_private = false)
20
+ @edge.respond_to?(method_name) || super
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,62 @@
1
+ module GraphQLIncludable
2
+ module Relay
3
+ class ConnectionEdgesAndNodes
4
+ attr_reader :parent, :edges_property, :nodes_property
5
+
6
+ def initialize(parent, edges_property, nodes_property)
7
+ @parent = parent
8
+ @edges_property = edges_property
9
+ @nodes_property = nodes_property
10
+ end
11
+ end
12
+
13
+ class EdgeWithNodeConnection < GraphQL::Relay::RelationConnection
14
+ def initialize(nodes, *args, &block)
15
+ @connection_edges_and_nodes = nodes
16
+ @loaded_nodes = nil
17
+ @loaded_edges = nil
18
+ super(nil, *args, &block)
19
+ end
20
+
21
+ def edge_nodes
22
+ raise 'This should not be called from a EdgeWithNodeConnectionType'
23
+ end
24
+
25
+ def fetch_edges
26
+ @loaded_edges ||= @connection_edges_and_nodes.parent.public_send(@connection_edges_and_nodes.edges_property)
27
+ # Set nodes to make underlying BaseConnection work
28
+ @nodes = @loaded_edges
29
+ @loaded_edges
30
+ end
31
+
32
+ def fetch_nodes
33
+ @loaded_nodes ||= @connection_edges_and_nodes.parent.public_send(@connection_edges_and_nodes.nodes_property)
34
+ # Set nodes to make underlying BaseConnection work
35
+ @nodes = @loaded_nodes
36
+ @loaded_nodes
37
+ end
38
+
39
+ def page_info
40
+ @nodes = determin_page_info_nodes
41
+ super
42
+ end
43
+
44
+ private
45
+
46
+ def determin_page_info_nodes
47
+ # If the query asks for `pageInfo` before `edges` or `nodes`, we dont directly know which to use most efficently.
48
+ # We can have a guess by checking if either of the associations are preloaded
49
+ return @loaded_nodes if @loaded_nodes.present?
50
+ return @loaded_edges if @loaded_edges.present?
51
+
52
+ nodes_preloaded = @connection_edges_and_nodes.parent.association(@connection_edges_and_nodes.nodes_property).loaded?
53
+ return fetch_nodes if nodes_preloaded
54
+
55
+ edges_preloaded = @connection_edges_and_nodes.parent.association(@connection_edges_and_nodes.edges_property).loaded?
56
+ return fetch_edges if edges_preloaded
57
+
58
+ fetch_nodes
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,25 @@
1
+ module GraphQLIncludable
2
+ module Relay
3
+ class EdgeWithNodeConnectionType
4
+ def self.create_type(
5
+ wrapped_type, edge_to_node_property:,
6
+ edge_type: wrapped_type.edge_type, edge_class: EdgeWithNode, nodes_field: GraphQL::Relay::ConnectionType.default_nodes_field, &block
7
+ )
8
+ custom_edge_class = edge_class
9
+
10
+ GraphQL::ObjectType.define do
11
+ name("#{wrapped_type.name}Connection")
12
+ description("The connection type for #{wrapped_type.name}.")
13
+ field :edges, types[edge_type], "A list of edges.", edge_class: custom_edge_class, property: :fetch_edges, edge_to_node_property: edge_to_node_property
14
+
15
+ if nodes_field
16
+ field :nodes, types[wrapped_type], "A list of nodes.", property: :fetch_nodes
17
+ end
18
+
19
+ field :pageInfo, !GraphQL::Relay::PageInfo, "Information to aid in pagination.", property: :page_info
20
+ block && instance_eval(&block)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ module GraphQLIncludable
2
+ module Relay
3
+ module Instrumentation
4
+ class Connection
5
+ def instrument(_type, field)
6
+ return field unless field.connection?
7
+
8
+ required_metadata = [:edges_property, :nodes_property]
9
+ requires_instrumentation = required_metadata.any? { |key| field.metadata.key?(key) }
10
+
11
+ return field unless requires_instrumentation
12
+ raise ArgumentError unless required_metadata.all? { |key| field.metadata.key?(key) }
13
+
14
+ raise ArgumentError if field.property.present? # TODO: Check for resolve proc too
15
+
16
+ edges_prop = field.metadata[:edges_property]
17
+ nodes_prop = field.metadata[:nodes_property]
18
+
19
+ _original_resolve = field.resolve_proc
20
+ new_resolve_proc = ->(obj, args, ctx) do
21
+ ConnectionEdgesAndNodes.new(obj, edges_prop, nodes_prop)
22
+ end
23
+
24
+ field.redefine { resolve(new_resolve_proc) }
25
+ end
26
+ end
27
+
28
+ GraphQL::Relay::BaseConnection.register_connection_implementation(GraphQLIncludable::Relay::ConnectionEdgesAndNodes, GraphQLIncludable::Relay::EdgeWithNodeConnection)
29
+ end
30
+ end
31
+ end
@@ -1,4 +1,48 @@
1
1
  module GraphQLIncludable
2
+ class IncludesManager
3
+ def initialize(parent_attribute)
4
+ @parent_attribute = parent_attribute
5
+ @included_children = {}
6
+ end
7
+
8
+ def add_child_include(association)
9
+ return @included_children[association.name] if @included_children.key?(association.name)
10
+
11
+ manager = IncludesManager.new(association.name)
12
+ @included_children[association.name] = manager
13
+ manager
14
+ end
15
+
16
+ def empty?
17
+ @included_children.empty?
18
+ end
19
+
20
+ def includes
21
+ child_includes = {}
22
+ child_includes_arr = []
23
+ @included_children.each do |key, value|
24
+ if value.empty?
25
+ child_includes_arr << key
26
+ else
27
+ includes = value.includes
28
+ if includes.is_a?(Array)
29
+ child_includes_arr += includes
30
+ else
31
+ child_includes.merge!(includes)
32
+ end
33
+ end
34
+ end
35
+
36
+ if child_includes_arr.present?
37
+ child_includes_arr << child_includes if child_includes.present?
38
+ child_includes = child_includes_arr
39
+ end
40
+
41
+ return child_includes if @parent_attribute.nil?
42
+ { @parent_attribute => child_includes }
43
+ end
44
+ end
45
+
2
46
  class Resolver
3
47
  # Returns the first node in the tree which returns a specific type
4
48
  def self.find_node_by_return_type(node, desired_return_type)
@@ -19,52 +63,80 @@ module GraphQLIncludable
19
63
  # Translate a node's selections into `includes` values
20
64
  # Combine and format children values
21
65
  # Noop on nodes that don't return AR (so no associations to include)
22
- def self.includes_for_node(node)
66
+ def self.includes_for_node(node, includes_manager)
23
67
  return_model = node_return_model(node)
24
68
  return [] if return_model.blank?
25
69
 
26
- includes = []
27
70
  children = node.scoped_children[node.return_type.unwrap]
28
- nested_includes = {}
29
71
 
30
72
  children.each_value do |child_node|
31
- child_includes = includes_for_child(child_node, return_model)
73
+ includes_for_child(child_node, return_model, includes_manager)
74
+ end
75
+ end
32
76
 
33
- if child_includes.is_a?(Hash)
34
- nested_includes.merge!(child_includes)
35
- elsif child_includes.is_a?(Array)
36
- includes += child_includes
37
- else
38
- includes << child_includes
77
+ def self.includes_from_connection(node, parent_model, associations_from_parent_model, includes_manager)
78
+ return unless node.return_type.fields['edges'].edge_class <= GraphQLIncludable::Relay::EdgeWithNode # TODO: Possibly basic support for connections with only nodes
79
+
80
+ edges_association = associations_from_parent_model[:edges]
81
+ nodes_association = associations_from_parent_model[:nodes]
82
+
83
+ edge_node_attribute = node.return_type.fields['edges'].metadata[:edge_to_node_property]
84
+ edge_model = edges_association.klass
85
+ edge_to_node_association = edge_model.reflect_on_association(edge_node_attribute)
86
+ node_model = edge_to_node_association.klass
87
+
88
+ connection_children = node.scoped_children[node.return_type.unwrap]
89
+ connection_children.each_value do |connection_node|
90
+ # connection_field {
91
+ # pageInfo {...}
92
+ # nodes {
93
+ # node_model_field ...
94
+ # }
95
+ # edges {
96
+ # edge_model_field ...
97
+ # node {
98
+ # node_model_field ...
99
+ # }
100
+ # }
101
+ # }
102
+
103
+ if connection_node.name == 'edges'
104
+ edges_includes_manager = includes_manager.add_child_include(edges_association)
105
+
106
+ edge_children = connection_node.scoped_children[connection_node.return_type.unwrap]
107
+ edge_children.each_value do |edge_child_node|
108
+ if edge_child_node.name == 'node'
109
+ node_includes_manager = edges_includes_manager.add_child_include(edge_to_node_association)
110
+
111
+ node_children = edge_child_node.scoped_children[edge_child_node.return_type.unwrap]
112
+ node_children.each_value do |node_child_node|
113
+ includes_for_child(node_child_node, node_model, node_includes_manager)
114
+ end
115
+ else
116
+ includes_for_child(edge_child_node, edge_model, edges_includes_manager)
117
+ end
118
+ end
119
+ elsif connection_node.name == 'nodes'
120
+ nodes_includes_manager = includes_manager.add_child_include(nodes_association)
121
+ node_children = connection_node.scoped_children[connection_node.return_type.unwrap]
122
+ node_children.each_value do |node_child_node|
123
+ includes_for_child(node_child_node, node_model, nodes_includes_manager)
124
+ end
39
125
  end
40
126
  end
41
-
42
- includes << nested_includes if nested_includes.present?
43
- includes.uniq
44
127
  end
45
128
 
46
- def self.includes_for_child(node, parent_model)
47
- attribute_name = node_predicted_association_name(node)
48
- delegated_through = includes_delegated_through(parent_model, attribute_name)
49
- delegated_model = model_name_to_class(delegated_through.last) if delegated_through.present?
50
- association = (delegated_model || parent_model).reflect_on_association(attribute_name)
51
- interceding_includes = []
52
- # association = get_model_association(parent_model, attribute_name, interceding_includes)
53
-
54
- if association
55
- child_includes = includes_for_node(node)
56
- child_includes.delete(association.plural_name.singularize.to_sym) if association.options[:through]
57
-
58
- array_to_nested_hash(interceding_includes + [attribute_name, child_includes].reject(&:blank?))
59
- # if node_is_relay_connection?(node)
60
- # join_name = association.options[:through]
61
- # edge_includes_chain = [association.name]
62
- # edge_includes_chain << child_includes.pop[association.name.to_s.singularize.to_sym] if child_includes.last&.is_a?(Hash)
63
- # edge_includes = array_to_nested_hash(edge_includes_chain)
64
- # end
65
- else
66
- # TODO: specified includes?
67
- [array_to_nested_hash(interceding_includes)].reject(&:blank?)
129
+ def self.includes_for_child(node, parent_model, includes_manager)
130
+ associations = possible_associations(node, parent_model)
131
+
132
+ if associations.present?
133
+ if node_is_relay_connection?(node)
134
+ includes_from_connection(node, parent_model, associations, includes_manager)
135
+ else
136
+ association = associations[:default] # should only be one
137
+ child_includes_manager = includes_manager.add_child_include(association)
138
+ includes_for_node(node, child_includes_manager)
139
+ end
68
140
  end
69
141
  end
70
142
 
@@ -84,9 +156,34 @@ module GraphQLIncludable
84
156
  rescue NameError
85
157
  end
86
158
 
87
- def self.node_predicted_association_name(node)
159
+ def self.node_is_relay_connection?(node)
160
+ node.return_type.unwrap.name =~ /Connection$/
161
+ end
162
+
163
+ def self.possible_associations(node, parent_model)
164
+ attribute_names = node_possible_association_names(node)
165
+ attribute_names.transform_values { |attribute_name| figure_out_association(attribute_name, parent_model) }.compact
166
+ end
167
+
168
+ def self.node_possible_association_names(node)
88
169
  definition = node.definitions.first
89
- definition.metadata[:includes] || (definition.property || definition.name).to_sym
170
+
171
+ association_names = {}
172
+ if node_is_relay_connection?(node)
173
+ association_names[:edges] = definition.metadata[:edges_property] if definition.metadata.key?(:edges_property)
174
+ association_names[:nodes] = definition.metadata[:nodes_property] if definition.metadata.key?(:nodes_property)
175
+ return association_names if association_names.present? # This should be an includable connection with no :property or name fallback.
176
+ end
177
+
178
+ association_names[:default] = definition.metadata[:includes] || (definition.property || definition.name).to_sym
179
+ association_names
180
+ end
181
+
182
+ def self.figure_out_association(attribute_name, parent_model)
183
+ delegated_through = includes_delegated_through(parent_model, attribute_name)
184
+ delegated_model = model_name_to_class(delegated_through.last) if delegated_through.present?
185
+ association = (delegated_model || parent_model).reflect_on_association(attribute_name)
186
+ association
90
187
  end
91
188
 
92
189
  # If method_name is delegated from base_model, return an array of
@@ -102,14 +199,5 @@ module GraphQLIncludable
102
199
  end
103
200
  chain
104
201
  end
105
-
106
- # Right-reduce an array into a nested hash
107
- def self.array_to_nested_hash(arr)
108
- arr.reverse.inject { |acc, item| { item => acc } } || {}
109
- end
110
-
111
- # def self.node_is_relay_connection?(node)
112
- # node.return_type.unwrap.name =~ /Connection$/
113
- # end
114
202
  end
115
203
  end
@@ -2,19 +2,35 @@ require 'graphql'
2
2
  require 'graphql_includable/resolver'
3
3
  require 'graphql_includable/concern'
4
4
  require 'graphql_includable/edge'
5
+ require 'graphql_includable/relay/edge_with_node'
6
+ require 'graphql_includable/relay/edge_with_node_connection'
7
+ require 'graphql_includable/relay/edge_with_node_connection_type'
8
+ require 'graphql_includable/relay/instrumentation/connection'
5
9
 
6
10
  GraphQL::Field.accepts_definitions(
7
- includes: GraphQL::Define.assign_metadata_key(:includes)
11
+ includes: GraphQL::Define.assign_metadata_key(:includes),
12
+ edges_property: GraphQL::Define.assign_metadata_key(:edges_property),
13
+ nodes_property: GraphQL::Define.assign_metadata_key(:nodes_property),
14
+ edge_to_node_property: GraphQL::Define.assign_metadata_key(:edge_to_node_property)
8
15
  )
9
16
 
10
17
  module GraphQL
11
18
  class BaseType
12
19
  def define_includable_connection(**kwargs, &block)
20
+ warn '[DEPRECATION] `define_includable_connection` is deprecated. Please use `define_connection_with_fetched_edge` instead.'
13
21
  define_connection(
14
22
  edge_class: GraphQLIncludable::Edge,
15
23
  **kwargs,
16
24
  &block
17
25
  )
18
26
  end
27
+
28
+ def define_connection_with_fetched_edge(**kwargs, &block)
29
+ GraphQLIncludable::Relay::EdgeWithNodeConnectionType.create_type(
30
+ self,
31
+ **kwargs,
32
+ &block
33
+ )
34
+ end
19
35
  end
20
36
  end
metadata CHANGED
@@ -1,20 +1,22 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql_includable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.12
4
+ version: 0.3.0.alpha.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Rouse
8
8
  - Josh Vickery
9
+ - Jordan Hamill
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2019-06-25 00:00:00.000000000 Z
13
+ date: 2019-08-05 00:00:00.000000000 Z
13
14
  dependencies: []
14
15
  description:
15
16
  email:
16
17
  - dan.rouse@squarefoot.com
17
18
  - jvickery@squarefoot.com
19
+ - jordan@squarefoot.com
18
20
  executables: []
19
21
  extensions: []
20
22
  extra_rdoc_files: []
@@ -22,6 +24,10 @@ files:
22
24
  - lib/graphql_includable.rb
23
25
  - lib/graphql_includable/concern.rb
24
26
  - lib/graphql_includable/edge.rb
27
+ - lib/graphql_includable/relay/edge_with_node.rb
28
+ - lib/graphql_includable/relay/edge_with_node_connection.rb
29
+ - lib/graphql_includable/relay/edge_with_node_connection_type.rb
30
+ - lib/graphql_includable/relay/instrumentation/connection.rb
25
31
  - lib/graphql_includable/resolver.rb
26
32
  homepage: https://github.com/thesquarefoot/graphql_includable
27
33
  licenses:
@@ -38,12 +44,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
38
44
  version: '0'
39
45
  required_rubygems_version: !ruby/object:Gem::Requirement
40
46
  requirements:
41
- - - ">="
47
+ - - ">"
42
48
  - !ruby/object:Gem::Version
43
- version: '0'
49
+ version: 1.3.1
44
50
  requirements: []
45
51
  rubyforge_project:
46
- rubygems_version: 2.6.11
52
+ rubygems_version: 2.6.14.1
47
53
  signing_key:
48
54
  specification_version: 4
49
55
  summary: An ActiveSupport::Concern for GraphQL Ruby to eager-load query data