/
combined.rb
147 lines (134 loc) · 4.15 KB
/
combined.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
require 'rom/relation/graph'
require 'rom/relation/commands'
module ROM
class Relation
# Represents a relation graphs which combines root relation
# with other relation nodes
#
# @api public
class Combined < Graph
include Commands
# Create a new relation combined with others
#
# @param [Relation] root
# @param [Array<Relation>] nodes
#
# @return [Combined]
#
# @api public
def self.new(root, nodes)
root_ns = root.options[:struct_namespace]
super(root, nodes.map { |node| node.struct_namespace(root_ns) })
end
# Combine this graph with more nodes
#
# @param [Array<Relation>] others A list of relations
#
# @return [Graph]
#
# @api public
def combine_with(*others)
self.class.new(root, nodes + others)
end
# Combine with other relations
#
# @see Relation#combine
#
# @return [Combined]
#
# @api public
def combine(*args)
self.class.new(root, nodes + root.combine(*args).nodes)
end
# Materialize combined relation
#
# @return [Loaded]
#
# @api public
def call(*args)
left = root.with(auto_struct: false).call(*args)
right =
if left.empty?
nodes.map { |node| Loaded.new(node, EMPTY_ARRAY) }
else
nodes.map { |node| node.call(left) }
end
if auto_map?
Loaded.new(self, mapper.([left, right]))
else
Loaded.new(self, [left, right])
end
end
# Return a new combined relation with adjusted node returned from a block
#
# @example with a node identifier
# aggregate(:tasks).node(:tasks) { |tasks| tasks.prioritized }
#
# @example with a nested path
# aggregate(tasks: :tags).node(tasks: :tags) { |tags| tags.where(name: 'red') }
#
# @param [Symbol] name The node relation name
#
# @yieldparam [Relation] relation The relation node
# @yieldreturn [Relation] The new relation node
#
# @return [Relation]
#
# @api public
def node(name, &block)
if name.is_a?(Symbol) && !nodes.map { |n| n.name.key }.include?(name)
raise ArgumentError, "#{name.inspect} is not a valid aggregate node name"
end
new_nodes = nodes.map { |node|
case name
when Symbol
name == node.name.key ? yield(node) : node
when Hash
other, *rest = name.flatten(1)
if other == node.name.key
nodes.detect { |n| n.name.key == other }.node(*rest, &block)
else
node
end
else
node
end
}
with_nodes(new_nodes)
end
# Return a `:create` command that can insert data from a nested hash.
#
# This is limited to `:create` commands only, because automatic restriction
# for `:update` commands would be quite complex. It's possible that in the
# future support for `:update` commands will be added though.
#
# Another limitation is that it can only work when you're composing
# parent and its child(ren), which follows canonical hierarchy from your
# database, so that parents are created first, then their PKs are set
# as FKs in child tuples. It should be possible to make it work with
# both directions (parent => child or child => parent), and it would
# require converting input tuples based on how they depend on each other,
# which we could do in the future.
#
# Expanding functionality of this method is planned for rom 5.0.
#
# @see Relation#command
#
# @raise NotImplementedError when type is not `:create`
#
# @api public
def command(type, *args)
if type == :create
super
else
raise NotImplementedError, "#{self.class}#command doesn't work with #{type.inspect} command type yet"
end
end
private
# @api private
def decorate?(other)
super || other.is_a?(Wrap)
end
end
end
end