graphql_includable 0.5.0 → 1.0.0.beta.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 014b367c0c9e017f33db0076bc473668d2033caa
4
- data.tar.gz: 61b6f1699b9d77c9ca3745be27a5f8f9b5da4e22
3
+ metadata.gz: c8ee024cd716dce8f0dee9b91066c089ba8ffab7
4
+ data.tar.gz: 5c6b1fb71d7c754f3f94285bf9571c72583667ec
5
5
  SHA512:
6
- metadata.gz: 50ea6afd3369d545c9ad72574a513c3f4de81a753cc1ab15b5cdeaea3375d7f9548f2586bb9717b6e974558126bdc87e605fe50739e3fbccaaf8c2bb9e2b1c84
7
- data.tar.gz: c32ad015198ced702ab16964905808abf86b632406e4f6acf2e9583f0c8ecfa4ce2dc00d30a5fe6b620e01db8c66c504f3b38256ae41a9c7a4c1222c45d62fa1
6
+ metadata.gz: 8cf0f027b8cf3a042053bfec69b180e5063e15df750f8e07b7c66c2d187ba3a26c25c8efb5231226ed2bacb814293660426ebca41af8bfff52c310d024bd3ec4
7
+ data.tar.gz: 402967a9b2a4d9675a718f8aed1314cfaf5793dade58ebe7e645ede3ab702a50ee2f18846ca10c89b64a33927d208c30bc70cde5c3c07da4a46b281e8f84a370
@@ -0,0 +1,67 @@
1
+ module GraphQLIncludable
2
+ class Includes
3
+ attr_reader :included_children
4
+
5
+ def initialize(parent_attribute)
6
+ @parent_attribute = parent_attribute
7
+ @included_children = {}
8
+ end
9
+
10
+ def add_child(key)
11
+ return @included_children[key] if @included_children.key?(key)
12
+ manager = Includes.new(key)
13
+ @included_children[key] = manager
14
+ manager
15
+ end
16
+
17
+ def merge_includes(includes_manager)
18
+ includes_manager.included_children.each do |key, manager|
19
+ included_children[key] = if included_children.key?(key)
20
+ included_children[key].merge_includes(manager)
21
+ else
22
+ manager
23
+ end
24
+ end
25
+ self
26
+ end
27
+
28
+ def [](key)
29
+ @included_children[key]
30
+ end
31
+
32
+ def dig(*args)
33
+ args = args[0] if args.length == 1 && args[0].is_a?(Array)
34
+ return @included_children if args.empty?
35
+ @included_children.dig(*args)
36
+ end
37
+
38
+ def empty?
39
+ @included_children.empty?
40
+ end
41
+
42
+ def active_record_includes
43
+ child_includes = {}
44
+ child_includes_arr = []
45
+ @included_children.each do |key, value|
46
+ if value.empty?
47
+ child_includes_arr << key
48
+ else
49
+ active_record_includes = value.active_record_includes
50
+ if active_record_includes.is_a?(Array)
51
+ child_includes_arr += active_record_includes
52
+ else
53
+ child_includes.merge!(active_record_includes)
54
+ end
55
+ end
56
+ end
57
+
58
+ if child_includes_arr.present?
59
+ child_includes_arr << child_includes if child_includes.present?
60
+ child_includes = child_includes_arr
61
+ end
62
+
63
+ return child_includes if @parent_attribute.nil?
64
+ { @parent_attribute => child_includes }
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,108 @@
1
+ module GraphQLIncludable
2
+ class IncludesBuilder
3
+ attr_reader :included_path, :includes
4
+
5
+ def initialize(only_one_path: true)
6
+ @only_one_path = only_one_path
7
+ @included_path = []
8
+ @includes = GraphQLIncludable::Includes.new(nil)
9
+ end
10
+
11
+ def includes?
12
+ @included_path.present?
13
+ end
14
+
15
+ def active_record_includes
16
+ @includes.active_record_includes
17
+ end
18
+
19
+ def path_leaf_includes
20
+ @includes.dig(included_path)
21
+ end
22
+
23
+ def path(*symbols, &block)
24
+ raise ArgumentError, 'Can only add path once' if @included_path.present? && @only_one_path
25
+
26
+ if symbols.present?
27
+ first, *rest = symbols
28
+ includes = @includes.add_child(first)
29
+ rest.each do |key|
30
+ includes = includes.add_child(key)
31
+ end
32
+ else
33
+ includes = @includes
34
+ end
35
+
36
+ if block_given?
37
+ nested = GraphQLIncludable::IncludesBuilder.new
38
+ nested.instance_eval(&block)
39
+ symbols += nested.included_path
40
+ includes.merge_includes(nested.includes)
41
+ end
42
+ @included_path = symbols
43
+ end
44
+
45
+ def sibling_path(*symbols, &block)
46
+ if symbols.present?
47
+ first, *rest = symbols
48
+ includes = @includes.add_child(first)
49
+ rest.each do |key|
50
+ includes = includes.add_child(key)
51
+ end
52
+ else
53
+ includes = @includes
54
+ end
55
+
56
+ return unless block_given?
57
+ nested = GraphQLIncludable::IncludesBuilder.new(only_one_path: false)
58
+ nested.instance_eval(&block)
59
+ includes.merge_includes(nested.includes)
60
+ end
61
+ end
62
+
63
+ class ConnectionIncludesBuilder
64
+ attr_reader :nodes_builder, :edges_builder
65
+
66
+ def initialize
67
+ @nodes_builder = IncludesBuilder.new
68
+ @edges_builder = ConnectionEdgesIncludesBuilder.new
69
+ end
70
+
71
+ def includes?
72
+ @nodes_builder.includes? || @edges_builder.includes?
73
+ end
74
+
75
+ def nodes(*symbols, &block)
76
+ @nodes_builder.path(*symbols, &block)
77
+ end
78
+
79
+ def edges(&block)
80
+ @edges_builder.instance_eval(&block)
81
+ end
82
+ end
83
+
84
+ class ConnectionEdgesIncludesBuilder
85
+ attr_reader :builder, :node_builder
86
+
87
+ def initialize
88
+ @builder = IncludesBuilder.new
89
+ @node_builder = IncludesBuilder.new
90
+ end
91
+
92
+ def includes?
93
+ @builder.includes? && @node_builder.includes?
94
+ end
95
+
96
+ def path(*symbols, &block)
97
+ @builder.path(*symbols, &block)
98
+ end
99
+
100
+ def sibling_path(*symbols, &block)
101
+ @builder.sibling_path(*symbols, &block)
102
+ end
103
+
104
+ def node(*symbols, &block)
105
+ @node_builder.path(*symbols, &block)
106
+ end
107
+ end
108
+ end
@@ -1,10 +1,33 @@
1
+ GraphQL::Field.accepts_definitions(
2
+ ##
3
+ # Define how to get from an edge Active Record model to the node Active Record model
4
+ connection_properties: GraphQL::Define.assign_metadata_key(:connection_properties),
5
+
6
+ ##
7
+ # Define a resolver for connection edges records
8
+ resolve_edges: GraphQL::Define.assign_metadata_key(:resolve_edges),
9
+
10
+ ##
11
+ # Define a resolver for connection nodes records
12
+ resolve_nodes: GraphQL::Define.assign_metadata_key(:resolve_nodes),
13
+
14
+ ##
15
+ # Internally used to mark a connection type that has a fetched edge
16
+ _includable_connection_marker: GraphQL::Define.assign_metadata_key(:_includable_connection_marker)
17
+ )
18
+
1
19
  module GraphQLIncludable
2
20
  module Relay
3
21
  class EdgeWithNode < GraphQL::Relay::Edge
4
22
  def initialize(node, connection)
5
23
  @edge = node
6
- node = connection.edge_to_node(@edge) # TODO: Make lazy
7
- super(node, connection)
24
+ @edge_to_node = ->() { connection.edge_to_node(@edge) }
25
+ super(nil, connection)
26
+ end
27
+
28
+ def node
29
+ @node ||= @edge_to_node.call
30
+ @node
8
31
  end
9
32
 
10
33
  def method_missing(method_name, *args, &block)
@@ -11,8 +11,8 @@ module GraphQLIncludable
11
11
  @parent = parent
12
12
  @args = args
13
13
  @ctx = ctx
14
- @edges_property = edges_property
15
- @nodes_property = nodes_property
14
+ @edges_property = edges_property # optional
15
+ @nodes_property = nodes_property # optional
16
16
  @edge_to_node_property = edge_to_node_property
17
17
  @edges_resolver = edges_resolver
18
18
  @nodes_resolver = nodes_resolver
@@ -33,14 +33,20 @@ module GraphQLIncludable
33
33
  end
34
34
 
35
35
  def fetch_edges
36
+ # This context is used within Resolver for connections
37
+ ctx.namespace(:gql_includable)[:resolving] = :edges
36
38
  @loaded_edges ||= @edges_and_nodes.edges_resolver.call(@edges_and_nodes.parent, args, ctx)
39
+ ctx.namespace(:gql_includable)[:resolving] = nil
37
40
  # Set nodes to make underlying BaseConnection work
38
41
  @nodes = @loaded_edges
39
42
  @loaded_edges
40
43
  end
41
44
 
42
45
  def fetch_nodes
46
+ # This context is used within Resolver for connections
47
+ ctx.namespace(:gql_includable)[:resolving] = :nodes
43
48
  @loaded_nodes ||= @edges_and_nodes.nodes_resolver.call(@edges_and_nodes.parent, args, ctx)
49
+ ctx.namespace(:gql_includable)[:resolving] = nil
44
50
  # Set nodes to make underlying BaseConnection work
45
51
  @nodes = @loaded_nodes
46
52
  @loaded_nodes
@@ -76,11 +82,15 @@ module GraphQLIncludable
76
82
  return @loaded_nodes if @loaded_nodes.present?
77
83
  return @loaded_edges if @loaded_edges.present?
78
84
 
79
- nodes_preloaded = @edges_and_nodes.parent.association(@edges_and_nodes.nodes_property).loaded?
80
- return fetch_nodes if nodes_preloaded
85
+ if @edges_and_nodes.nodes_property.present?
86
+ nodes_preloaded = @edges_and_nodes.parent.association(@edges_and_nodes.nodes_property).loaded?
87
+ return fetch_nodes if nodes_preloaded
88
+ end
81
89
 
82
- edges_preloaded = @edges_and_nodes.parent.association(@edges_and_nodes.edges_property).loaded?
83
- return fetch_edges if edges_preloaded
90
+ if @edges_and_nodes.edges_property.present?
91
+ edges_preloaded = @edges_and_nodes.parent.association(@edges_and_nodes.edges_property).loaded?
92
+ return fetch_edges if edges_preloaded
93
+ end
84
94
 
85
95
  fetch_nodes
86
96
  end
@@ -1,3 +1,5 @@
1
+ require_relative 'edge_with_node'
2
+
1
3
  module GraphQLIncludable
2
4
  module Relay
3
5
  class EdgeWithNodeConnectionType
@@ -0,0 +1,77 @@
1
+ require_relative 'edge_with_node_connection'
2
+
3
+ module GraphQLIncludable
4
+ module Relay
5
+ class Instrumentation
6
+ # rubocop:disable Metrics/AbcSize
7
+ def instrument(_type, field)
8
+ return field unless edge_with_node_connection?(field)
9
+
10
+ raise ArgumentError, 'Connection does not support fetching using :property' if field.property.present?
11
+
12
+ is_proc_based = proc_based?(field)
13
+
14
+ validate!(field, is_proc_based)
15
+ properties = field.metadata[:connection_properties]
16
+ edge_to_node_property = properties[:edge_to_node]
17
+ edges_prop = properties[:edges]
18
+ nodes_prop = properties[:nodes]
19
+
20
+ if is_proc_based
21
+ edges_resolver = field.metadata[:resolve_edges]
22
+ nodes_resolver = field.metadata[:resolve_nodes]
23
+ else
24
+ # Use the edges and nodes symbols from the incldues pattern as the propeties to fetch
25
+ edges_resolver = ->(obj, _args, _ctx) { obj.public_send(edges_prop) }
26
+ nodes_resolver = ->(obj, _args, _ctx) { obj.public_send(nodes_prop) }
27
+ end
28
+
29
+ _original_resolve = field.resolve_proc
30
+ new_resolve_proc = ->(obj, args, ctx) do
31
+ ConnectionEdgesAndNodes.new(obj, args, ctx,
32
+ edges_prop, nodes_prop, edge_to_node_property,
33
+ edges_resolver, nodes_resolver)
34
+ end
35
+
36
+ field.redefine { resolve(new_resolve_proc) }
37
+ end
38
+ # rubocop:enable Metrics/AbcSize
39
+
40
+ private
41
+
42
+ def edge_with_node_connection?(field)
43
+ field.connection? && field.type.fields['edges'].metadata.key?(:_includable_connection_marker)
44
+ end
45
+
46
+ def proc_based?(field)
47
+ required_metadata = [:resolve_edges, :resolve_nodes]
48
+ has_a_resolver = required_metadata.any? { |key| field.metadata.key?(key) }
49
+
50
+ return false unless has_a_resolver
51
+ unless required_metadata.all? { |key| field.metadata.key?(key) }
52
+ raise ArgumentError, "Missing one of #{required_metadata}"
53
+ end
54
+
55
+ true
56
+ end
57
+
58
+ def validate!(field, is_proc_based)
59
+ unless field.metadata.key?(:connection_properties)
60
+ raise ArgumentError, 'Missing connection_properties definition for field'
61
+ end
62
+ properties = field.metadata[:connection_properties]
63
+ unless properties.is_a?(Hash)
64
+ raise ArgumentError, 'Connection includes must be a hash containing :edges and :nodes keys'
65
+ end
66
+ raise ArgumentError, 'Missing :nodes' unless is_proc_based || properties.key?(:nodes)
67
+ raise ArgumentError, 'Missing :edges' unless is_proc_based || properties.key?(:edges)
68
+ raise ArgumentError, 'Missing :edge_to_node' unless properties.key?(:edge_to_node)
69
+ end
70
+ end
71
+
72
+ GraphQL::Relay::BaseConnection.register_connection_implementation(
73
+ GraphQLIncludable::Relay::ConnectionEdgesAndNodes,
74
+ GraphQLIncludable::Relay::EdgeWithNodeConnection
75
+ )
76
+ end
77
+ end