rom-repository 1.4.0 → 2.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -6
- data/{LICENSE.txt → LICENSE} +1 -1
- data/README.md +18 -1
- data/lib/rom-repository.rb +1 -2
- data/lib/rom/repository.rb +9 -216
- data/lib/rom/repository/class_interface.rb +16 -33
- data/lib/rom/repository/relation_reader.rb +46 -0
- data/lib/rom/repository/root.rb +3 -59
- data/lib/rom/repository/version.rb +1 -1
- metadata +9 -98
- data/.gitignore +0 -3
- data/.rspec +0 -3
- data/.travis.yml +0 -27
- data/.yardopts +0 -2
- data/Gemfile +0 -38
- data/Rakefile +0 -19
- data/lib/rom/open_struct.rb +0 -35
- data/lib/rom/repository/changeset.rb +0 -155
- data/lib/rom/repository/changeset/associated.rb +0 -100
- data/lib/rom/repository/changeset/create.rb +0 -16
- data/lib/rom/repository/changeset/delete.rb +0 -17
- data/lib/rom/repository/changeset/pipe.rb +0 -97
- data/lib/rom/repository/changeset/restricted.rb +0 -28
- data/lib/rom/repository/changeset/stateful.rb +0 -282
- data/lib/rom/repository/changeset/update.rb +0 -82
- data/lib/rom/repository/command_compiler.rb +0 -257
- data/lib/rom/repository/command_proxy.rb +0 -26
- data/lib/rom/repository/header_builder.rb +0 -65
- data/lib/rom/repository/mapper_builder.rb +0 -23
- data/lib/rom/repository/relation_proxy.rb +0 -337
- data/lib/rom/repository/relation_proxy/combine.rb +0 -320
- data/lib/rom/repository/relation_proxy/wrap.rb +0 -78
- data/lib/rom/repository/struct_builder.rb +0 -83
- data/lib/rom/struct.rb +0 -113
- data/log/.gitkeep +0 -0
- data/rom-repository.gemspec +0 -23
- data/spec/integration/changeset_spec.rb +0 -193
- data/spec/integration/command_macros_spec.rb +0 -191
- data/spec/integration/command_spec.rb +0 -228
- data/spec/integration/multi_adapter_spec.rb +0 -73
- data/spec/integration/repository/aggregate_spec.rb +0 -58
- data/spec/integration/repository_spec.rb +0 -406
- data/spec/integration/root_repository_spec.rb +0 -106
- data/spec/integration/typed_structs_spec.rb +0 -64
- data/spec/shared/database.rb +0 -79
- data/spec/shared/mappers.rb +0 -35
- data/spec/shared/models.rb +0 -41
- data/spec/shared/plugins.rb +0 -66
- data/spec/shared/relations.rb +0 -115
- data/spec/shared/repo.rb +0 -86
- data/spec/shared/seeds.rb +0 -30
- data/spec/shared/structs.rb +0 -140
- data/spec/spec_helper.rb +0 -83
- data/spec/support/mapper_registry.rb +0 -9
- data/spec/support/mutant.rb +0 -10
- data/spec/unit/changeset/associate_spec.rb +0 -120
- data/spec/unit/changeset/map_spec.rb +0 -111
- data/spec/unit/changeset_spec.rb +0 -186
- data/spec/unit/relation_proxy_spec.rb +0 -202
- data/spec/unit/repository/changeset_spec.rb +0 -197
- data/spec/unit/repository/inspect_spec.rb +0 -18
- data/spec/unit/repository/session_spec.rb +0 -251
- data/spec/unit/repository/transaction_spec.rb +0 -42
- data/spec/unit/session_spec.rb +0 -46
- data/spec/unit/struct_builder_spec.rb +0 -128
@@ -1,337 +0,0 @@
|
|
1
|
-
require 'dry/core/deprecations'
|
2
|
-
|
3
|
-
require 'rom/initializer'
|
4
|
-
require 'rom/relation/materializable'
|
5
|
-
|
6
|
-
require 'rom/repository/relation_proxy/combine'
|
7
|
-
require 'rom/repository/relation_proxy/wrap'
|
8
|
-
|
9
|
-
module ROM
|
10
|
-
class Repository
|
11
|
-
# RelationProxy decorates a relation and automatically generates mappers that
|
12
|
-
# will map raw tuples into rom structs
|
13
|
-
#
|
14
|
-
# Relation proxies are being registered within repositories so typically there's
|
15
|
-
# no need to instantiate them manually.
|
16
|
-
#
|
17
|
-
# @api public
|
18
|
-
class RelationProxy
|
19
|
-
extend Initializer
|
20
|
-
extend Dry::Core::Deprecations[:rom]
|
21
|
-
|
22
|
-
include Relation::Materializable
|
23
|
-
|
24
|
-
(Kernel.private_instance_methods - %i(raise)).each(&method(:undef_method))
|
25
|
-
|
26
|
-
include RelationProxy::Combine
|
27
|
-
include RelationProxy::Wrap
|
28
|
-
|
29
|
-
deprecate :combine_parents
|
30
|
-
deprecate :combine_children
|
31
|
-
deprecate :wrap_parent
|
32
|
-
|
33
|
-
RelationRegistryType = Types.Definition(RelationRegistry).constrained(type: RelationRegistry)
|
34
|
-
|
35
|
-
# @!attribute [r] relation
|
36
|
-
# @return [Relation, Relation::Composite, Relation::Graph, Relation::Curried] The decorated relation object
|
37
|
-
param :relation
|
38
|
-
|
39
|
-
option :name, type: Types::Strict::Symbol
|
40
|
-
option :mappers, default: -> { MapperBuilder.new }
|
41
|
-
option :meta, default: -> { EMPTY_HASH }
|
42
|
-
option :registry, type: RelationRegistryType, default: -> { RelationRegistry.new }
|
43
|
-
option :auto_struct, default: -> { true }
|
44
|
-
|
45
|
-
# Relation name
|
46
|
-
#
|
47
|
-
# @return [ROM::Relation::Name]
|
48
|
-
#
|
49
|
-
# @api public
|
50
|
-
def name
|
51
|
-
@name ? relation.name.with(@name) : relation.name
|
52
|
-
end
|
53
|
-
|
54
|
-
# Materializes wrapped relation and sends it through a mapper
|
55
|
-
#
|
56
|
-
# For performance reasons a combined relation will skip mapping since
|
57
|
-
# we only care about extracting key values for combining
|
58
|
-
#
|
59
|
-
# @api public
|
60
|
-
def call(*args)
|
61
|
-
((combine? || composite?) ? relation : (relation >> mapper)).call(*args)
|
62
|
-
end
|
63
|
-
|
64
|
-
# Maps the wrapped relation with other mappers available in the registry
|
65
|
-
#
|
66
|
-
# @overload map_with(model)
|
67
|
-
# Map tuples to the provided custom model class
|
68
|
-
#
|
69
|
-
# @example
|
70
|
-
# users.as(MyUserModel)
|
71
|
-
#
|
72
|
-
# @param [Class>] model Your custom model class
|
73
|
-
#
|
74
|
-
# @overload map_with(*mappers)
|
75
|
-
# Map tuples using registered mappers
|
76
|
-
#
|
77
|
-
# @example
|
78
|
-
# users.map_with(:my_mapper, :my_other_mapper)
|
79
|
-
#
|
80
|
-
# @param [Array<Symbol>] mappers A list of mapper identifiers
|
81
|
-
#
|
82
|
-
# @overload map_with(*mappers, auto_map: true)
|
83
|
-
# Map tuples using auto-mapping and custom registered mappers
|
84
|
-
#
|
85
|
-
# If `auto_map` is enabled, your mappers will be applied after performing
|
86
|
-
# default auto-mapping. This means that you can compose complex relations
|
87
|
-
# and have them auto-mapped, and use much simpler custom mappers to adjust
|
88
|
-
# resulting data according to your requirements.
|
89
|
-
#
|
90
|
-
# @example
|
91
|
-
# users.map_with(:my_mapper, :my_other_mapper, auto_map: true)
|
92
|
-
#
|
93
|
-
# @param [Array<Symbol>] mappers A list of mapper identifiers
|
94
|
-
#
|
95
|
-
# @return [RelationProxy] A new relation proxy with pipelined relation
|
96
|
-
#
|
97
|
-
# @api public
|
98
|
-
def map_with(*names, **opts)
|
99
|
-
if names.size == 1 && names[0].is_a?(Class)
|
100
|
-
map_to(names[0])
|
101
|
-
elsif names.size > 1 && names.any? { |name| name.is_a?(Class) }
|
102
|
-
raise ArgumentError, 'using custom mappers and a model is not supported'
|
103
|
-
else
|
104
|
-
if opts[:auto_map] && !meta[:combine_type]
|
105
|
-
mappers = [mapper, *names.map { |name| relation.mappers[name] }]
|
106
|
-
mappers.reduce(self) { |a, e| a >> e }
|
107
|
-
else
|
108
|
-
names.reduce(self) { |a, e| a >> relation.mappers[e] }
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
# @api public
|
114
|
-
def as(*names, **opts)
|
115
|
-
if names.size == 1 && names[0].is_a?(Class)
|
116
|
-
msg = <<-STR
|
117
|
-
Relation#as will change behavior in 4.0. Use `map_to` instead
|
118
|
-
=> Called at:
|
119
|
-
#{Kernel.caller[0..5].join("\n")}
|
120
|
-
STR
|
121
|
-
|
122
|
-
Dry::Core::Deprecations.warn(msg)
|
123
|
-
|
124
|
-
map_to(names[0])
|
125
|
-
elsif names.size > 1 && names.any? { |name| name.is_a?(Class) }
|
126
|
-
raise ArgumentError, 'using custom mappers and a model is not supported'
|
127
|
-
else
|
128
|
-
if opts[:auto_map] && !meta[:combine_type]
|
129
|
-
mappers = [mapper, *names.map { |name| relation.mappers[name] }]
|
130
|
-
mappers.reduce(self) { |a, e| a >> e }
|
131
|
-
else
|
132
|
-
names.reduce(self) { |a, e| a >> relation.mappers[e] }
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
# @api public
|
138
|
-
def map_to(model)
|
139
|
-
with(meta: meta.merge(model: model))
|
140
|
-
end
|
141
|
-
|
142
|
-
# Return a new graph with adjusted node returned from a block
|
143
|
-
#
|
144
|
-
# @example with a node identifier
|
145
|
-
# aggregate(:tasks).node(:tasks) { |tasks| tasks.prioritized }
|
146
|
-
#
|
147
|
-
# @example with a nested path
|
148
|
-
# aggregate(tasks: :tags).node(tasks: :tags) { |tags| tags.where(name: 'red') }
|
149
|
-
#
|
150
|
-
# @param [Symbol] name The node relation name
|
151
|
-
#
|
152
|
-
# @yieldparam [RelationProxy] The relation node
|
153
|
-
# @yieldreturn [RelationProxy] The new relation node
|
154
|
-
#
|
155
|
-
# @return [RelationProxy]
|
156
|
-
#
|
157
|
-
# @api public
|
158
|
-
def node(name, &block)
|
159
|
-
if name.is_a?(Symbol) && !nodes.map { |n| n.name.relation }.include?(name)
|
160
|
-
raise ArgumentError, "#{name.inspect} is not a valid aggregate node name"
|
161
|
-
end
|
162
|
-
|
163
|
-
new_nodes = nodes.map { |node|
|
164
|
-
case name
|
165
|
-
when Symbol
|
166
|
-
name == node.name.relation ? yield(node) : node
|
167
|
-
when Hash
|
168
|
-
other, *rest = name.flatten(1)
|
169
|
-
if other == node.name.relation
|
170
|
-
nodes.detect { |n| n.name.relation == other }.node(*rest, &block)
|
171
|
-
else
|
172
|
-
node
|
173
|
-
end
|
174
|
-
else
|
175
|
-
node
|
176
|
-
end
|
177
|
-
}
|
178
|
-
|
179
|
-
with_nodes(new_nodes)
|
180
|
-
end
|
181
|
-
|
182
|
-
# Return a string representation of this relation proxy
|
183
|
-
#
|
184
|
-
# @return [String]
|
185
|
-
#
|
186
|
-
# @api public
|
187
|
-
def inspect
|
188
|
-
%(#<#{relation.class} name=#{name} dataset=#{dataset.inspect}>)
|
189
|
-
end
|
190
|
-
|
191
|
-
# Infers a mapper for the wrapped relation
|
192
|
-
#
|
193
|
-
# @return [ROM::Mapper]
|
194
|
-
#
|
195
|
-
# @api private
|
196
|
-
def mapper
|
197
|
-
mappers[to_ast]
|
198
|
-
end
|
199
|
-
|
200
|
-
# Returns a new instance with new options
|
201
|
-
#
|
202
|
-
# @param new_options [Hash]
|
203
|
-
#
|
204
|
-
# @return [RelationProxy]
|
205
|
-
#
|
206
|
-
# @api private
|
207
|
-
def with(new_options)
|
208
|
-
__new__(relation, options.merge(new_options))
|
209
|
-
end
|
210
|
-
|
211
|
-
# Returns if this relation is combined aka a relation graph
|
212
|
-
#
|
213
|
-
# @return [Boolean]
|
214
|
-
#
|
215
|
-
# @api private
|
216
|
-
def combine?
|
217
|
-
meta[:combine_type]
|
218
|
-
end
|
219
|
-
|
220
|
-
# Return if this relation is a composite
|
221
|
-
#
|
222
|
-
# @return [Boolean]
|
223
|
-
#
|
224
|
-
# @api private
|
225
|
-
def composite?
|
226
|
-
relation.is_a?(Relation::Composite)
|
227
|
-
end
|
228
|
-
|
229
|
-
# @return [Symbol] The wrapped relation's adapter identifier ie :sql or :http
|
230
|
-
#
|
231
|
-
# @api private
|
232
|
-
def adapter
|
233
|
-
relation.class.adapter
|
234
|
-
end
|
235
|
-
|
236
|
-
# Returns AST for the wrapped relation
|
237
|
-
#
|
238
|
-
# @return [Array]
|
239
|
-
#
|
240
|
-
# @api private
|
241
|
-
def to_ast
|
242
|
-
@to_ast ||=
|
243
|
-
begin
|
244
|
-
attr_ast = schema.map { |attr| [:attribute, attr] }
|
245
|
-
|
246
|
-
meta = self.meta.merge(dataset: base_name.dataset)
|
247
|
-
meta.update(model: false) unless meta[:model] || auto_struct
|
248
|
-
meta.delete(:wraps)
|
249
|
-
|
250
|
-
header = attr_ast + nodes_ast + wraps_ast
|
251
|
-
|
252
|
-
[:relation, [base_name.relation, meta, [:header, header]]]
|
253
|
-
end
|
254
|
-
end
|
255
|
-
|
256
|
-
# @api private
|
257
|
-
def respond_to_missing?(meth, _include_private = false)
|
258
|
-
relation.respond_to?(meth) || super
|
259
|
-
end
|
260
|
-
|
261
|
-
private
|
262
|
-
|
263
|
-
# @api private
|
264
|
-
def schema
|
265
|
-
if meta[:wrap]
|
266
|
-
relation.schema.wrap
|
267
|
-
else
|
268
|
-
relation.schema.reject(&:wrapped?)
|
269
|
-
end
|
270
|
-
end
|
271
|
-
|
272
|
-
# @api private
|
273
|
-
def base_name
|
274
|
-
relation.base_name
|
275
|
-
end
|
276
|
-
|
277
|
-
# @api private
|
278
|
-
def nodes_ast
|
279
|
-
@nodes_ast ||= nodes.map(&:to_ast)
|
280
|
-
end
|
281
|
-
|
282
|
-
# @api private
|
283
|
-
def wraps_ast
|
284
|
-
@wraps_ast ||= wraps.map(&:to_ast)
|
285
|
-
end
|
286
|
-
|
287
|
-
# Return a new instance with another relation and options
|
288
|
-
#
|
289
|
-
# @return [RelationProxy]
|
290
|
-
#
|
291
|
-
# @api private
|
292
|
-
def __new__(relation, new_options = EMPTY_HASH)
|
293
|
-
self.class.new(
|
294
|
-
relation, new_options.size > 0 ? options.merge(new_options) : options
|
295
|
-
)
|
296
|
-
end
|
297
|
-
|
298
|
-
# Return all nodes that this relation combines
|
299
|
-
#
|
300
|
-
# @return [Array<RelationProxy>]
|
301
|
-
#
|
302
|
-
# @api private
|
303
|
-
def nodes
|
304
|
-
relation.graph? ? relation.nodes : EMPTY_ARRAY
|
305
|
-
end
|
306
|
-
|
307
|
-
# Return all nodes that this relation wraps
|
308
|
-
#
|
309
|
-
# @return [Array<RelationProxy>]
|
310
|
-
#
|
311
|
-
# @api private
|
312
|
-
def wraps
|
313
|
-
meta.fetch(:wraps, EMPTY_ARRAY)
|
314
|
-
end
|
315
|
-
|
316
|
-
# Forward to relation and wrap it with proxy if response was a relation too
|
317
|
-
#
|
318
|
-
# TODO: this will be simplified once ROM::Relation has lazy-features built-in
|
319
|
-
# and ROM::Lazy is gone
|
320
|
-
#
|
321
|
-
# @api private
|
322
|
-
def method_missing(meth, *args, &block)
|
323
|
-
if relation.respond_to?(meth)
|
324
|
-
result = relation.__send__(meth, *args, &block)
|
325
|
-
|
326
|
-
if result.kind_of?(Relation::Materializable) && !result.is_a?(Relation::Loaded)
|
327
|
-
__new__(result)
|
328
|
-
else
|
329
|
-
result
|
330
|
-
end
|
331
|
-
else
|
332
|
-
raise NoMethodError, "undefined method `#{meth}' for #{relation.class.name}"
|
333
|
-
end
|
334
|
-
end
|
335
|
-
end
|
336
|
-
end
|
337
|
-
end
|
@@ -1,320 +0,0 @@
|
|
1
|
-
require 'dry/core/inflector'
|
2
|
-
|
3
|
-
module ROM
|
4
|
-
class Repository
|
5
|
-
class RelationProxy
|
6
|
-
# Provides convenient methods for composing relations
|
7
|
-
#
|
8
|
-
# @api public
|
9
|
-
module Combine
|
10
|
-
# Returns a combine representation of a loading-proxy relation
|
11
|
-
#
|
12
|
-
# This will carry meta info used to produce a correct AST from a relation
|
13
|
-
# so that correct mapper can be generated
|
14
|
-
#
|
15
|
-
# @return [RelationProxy]
|
16
|
-
#
|
17
|
-
# @api private
|
18
|
-
def combined(name, keys, type)
|
19
|
-
meta = { keys: keys, combine_type: type, combine_name: name }
|
20
|
-
with(name: name, meta: self.meta.merge(meta))
|
21
|
-
end
|
22
|
-
|
23
|
-
# Combine with other relations
|
24
|
-
#
|
25
|
-
# @overload combine(*associations)
|
26
|
-
# Composes relations using configured associations
|
27
|
-
# @example
|
28
|
-
# users.combine(:tasks, :posts)
|
29
|
-
# @param *associations [Array<Symbol>] A list of association names
|
30
|
-
#
|
31
|
-
# @overload combine(options)
|
32
|
-
# Composes relations based on options
|
33
|
-
#
|
34
|
-
# @example
|
35
|
-
# # users have-many tasks (name and join-keys inferred, which needs associations in schema)
|
36
|
-
# users.combine(many: tasks)
|
37
|
-
#
|
38
|
-
# # users have-many tasks with custom name (join-keys inferred, which needs associations in schema)
|
39
|
-
# users.combine(many: { priority_tasks: tasks.priority })
|
40
|
-
#
|
41
|
-
# # users have-many tasks with custom view and join keys
|
42
|
-
# users.combine(many: { tasks: [tasks.for_users, id: :task_id] })
|
43
|
-
#
|
44
|
-
# # users has-one task
|
45
|
-
# users.combine(one: { task: tasks })
|
46
|
-
#
|
47
|
-
# @param options [Hash] Options for combine
|
48
|
-
# @option :many [Hash] Sets options for "has-many" type of association
|
49
|
-
# @option :one [Hash] Sets options for "has-one/belongs-to" type of association
|
50
|
-
#
|
51
|
-
# @return [RelationProxy]
|
52
|
-
#
|
53
|
-
# @api public
|
54
|
-
def combine(*args)
|
55
|
-
options = args[0].is_a?(Hash) ? args[0] : args
|
56
|
-
|
57
|
-
combine_opts = Hash.new { |h, k| h[k] = {} }
|
58
|
-
|
59
|
-
options.each do |key, value|
|
60
|
-
if key == :one || key == :many
|
61
|
-
if value.is_a?(Hash)
|
62
|
-
value.each do |name, spec|
|
63
|
-
if spec.is_a?(Array)
|
64
|
-
combine_opts[key][name] = spec
|
65
|
-
else
|
66
|
-
_, (curried, keys) = combine_opts_from_relations(spec).to_a[0]
|
67
|
-
combine_opts[key][name] = [curried, keys]
|
68
|
-
end
|
69
|
-
end
|
70
|
-
else
|
71
|
-
_, (curried, keys) = combine_opts_from_relations(value).to_a[0]
|
72
|
-
combine_opts[key][curried.combine_tuple_key(key)] = [curried, keys]
|
73
|
-
end
|
74
|
-
else
|
75
|
-
if value.is_a?(Array)
|
76
|
-
other =
|
77
|
-
if registry.key?(key)
|
78
|
-
registry[key]
|
79
|
-
else
|
80
|
-
registry[associations[key].target]
|
81
|
-
end
|
82
|
-
curried = combine_from_assoc(key, other).combine(*value)
|
83
|
-
result, _, keys = combine_opts_for_assoc(key)
|
84
|
-
combine_opts[result][key] = [curried, keys]
|
85
|
-
else
|
86
|
-
result, curried, keys = combine_opts_for_assoc(key, value)
|
87
|
-
combine_opts[result][key] = [curried, keys]
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
nodes = combine_opts.flat_map do |type, relations|
|
93
|
-
relations.map { |name, (relation, keys)|
|
94
|
-
relation.combined(name, keys, type)
|
95
|
-
}
|
96
|
-
end
|
97
|
-
|
98
|
-
__new__(relation.graph(*nodes))
|
99
|
-
end
|
100
|
-
|
101
|
-
# Shortcut for combining with parents which infers the join keys
|
102
|
-
#
|
103
|
-
# @example
|
104
|
-
# # tasks belong-to users
|
105
|
-
# tasks.combine_parents(one: users)
|
106
|
-
#
|
107
|
-
# # tasks belong-to users with custom user view
|
108
|
-
# tasks.combine_parents(one: users.task_owners)
|
109
|
-
#
|
110
|
-
# @param options [Hash] Combine options hash
|
111
|
-
#
|
112
|
-
# @return [RelationProxy]
|
113
|
-
#
|
114
|
-
# @api public
|
115
|
-
def combine_parents(options)
|
116
|
-
combine_opts = {}
|
117
|
-
|
118
|
-
options.each do |type, parents|
|
119
|
-
combine_opts[type] =
|
120
|
-
case parents
|
121
|
-
when Hash
|
122
|
-
parents.each_with_object({}) { |(name, parent), r|
|
123
|
-
keys = combine_keys(parent, relation, :parent)
|
124
|
-
curried = combine_from_assoc_with_fallback(name, parent, keys)
|
125
|
-
r[name] = [curried, keys]
|
126
|
-
}
|
127
|
-
when Array
|
128
|
-
parents.each_with_object({}) { |parent, r|
|
129
|
-
keys = combine_keys(parent, relation, :parent)
|
130
|
-
tuple_key = parent.combine_tuple_key(type)
|
131
|
-
curried = combine_from_assoc_with_fallback(parent.name, parent, keys)
|
132
|
-
r[tuple_key] = [curried, keys]
|
133
|
-
}
|
134
|
-
else
|
135
|
-
keys = combine_keys(parents, relation, :parent)
|
136
|
-
tuple_key = parents.combine_tuple_key(type)
|
137
|
-
curried = combine_from_assoc_with_fallback(parents.name, parents, keys)
|
138
|
-
{ tuple_key => [curried, keys] }
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
combine(combine_opts)
|
143
|
-
end
|
144
|
-
|
145
|
-
# Shortcut for combining with children which infers the join keys
|
146
|
-
#
|
147
|
-
# @example
|
148
|
-
# # users have-many tasks
|
149
|
-
# users.combine_children(many: tasks)
|
150
|
-
#
|
151
|
-
# # users have-many tasks with custom mapping (requires associations)
|
152
|
-
# users.combine_children(many: { priority_tasks: tasks.priority })
|
153
|
-
#
|
154
|
-
# @param [Hash] options
|
155
|
-
#
|
156
|
-
# @return [RelationProxy]
|
157
|
-
#
|
158
|
-
# @api public
|
159
|
-
def combine_children(options)
|
160
|
-
combine_opts = {}
|
161
|
-
|
162
|
-
options.each do |type, children|
|
163
|
-
combine_opts[type] =
|
164
|
-
case children
|
165
|
-
when Hash
|
166
|
-
children.each_with_object({}) { |(name, child), r|
|
167
|
-
keys = combine_keys(relation, child, :children)
|
168
|
-
curried = combine_from_assoc_with_fallback(name, child, keys)
|
169
|
-
r[name] = [curried, keys]
|
170
|
-
}
|
171
|
-
when Array
|
172
|
-
children.each_with_object({}) { |child, r|
|
173
|
-
keys = combine_keys(relation, child, :children)
|
174
|
-
tuple_key = child.combine_tuple_key(type)
|
175
|
-
curried = combine_from_assoc_with_fallback(child.name, child, keys)
|
176
|
-
r[tuple_key] = [curried, keys]
|
177
|
-
}
|
178
|
-
else
|
179
|
-
keys = combine_keys(relation, children, :children)
|
180
|
-
curried = combine_from_assoc_with_fallback(children.name, children, keys)
|
181
|
-
tuple_key = children.combine_tuple_key(type)
|
182
|
-
{ tuple_key => [curried, keys] }
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
combine(combine_opts)
|
187
|
-
end
|
188
|
-
|
189
|
-
protected
|
190
|
-
|
191
|
-
# Infer join/combine keys for a given relation and association type
|
192
|
-
#
|
193
|
-
# When source has association corresponding to target's name, it'll be
|
194
|
-
# used to get the keys. Otherwise we fall back to using default keys based
|
195
|
-
# on naming conventions.
|
196
|
-
#
|
197
|
-
# @param [Relation::Name] source The source relation name
|
198
|
-
# @param [Relation::Name] target The target relation name
|
199
|
-
# @param [Symbol] type The association type, can be either :parent or :children
|
200
|
-
#
|
201
|
-
# @return [Hash<Symbol=>Symbol>]
|
202
|
-
#
|
203
|
-
# @api private
|
204
|
-
def combine_keys(source, target, type)
|
205
|
-
source.associations.try(target.name) { |assoc|
|
206
|
-
assoc.combine_keys(__registry__)
|
207
|
-
} or infer_combine_keys(source, target, type)
|
208
|
-
end
|
209
|
-
|
210
|
-
# Build combine options from a relation mapping hash passed to `combine`
|
211
|
-
#
|
212
|
-
# This method will infer combine keys either from defined associations
|
213
|
-
# or use the keys provided explicitly for ad-hoc combines
|
214
|
-
#
|
215
|
-
# It returns a mapping like `name => [preloadable_relation, combine_keys]`
|
216
|
-
# and this mapping is used by `combine` to build a full relation graph
|
217
|
-
#
|
218
|
-
# @api private
|
219
|
-
def combine_opts_from_relations(*relations)
|
220
|
-
relations.each_with_object({}) do |spec, h|
|
221
|
-
# We assume it's a child relation
|
222
|
-
keys = combine_keys(relation, spec, :children)
|
223
|
-
rel = combine_from_assoc_with_fallback(spec.name, spec, keys)
|
224
|
-
h[spec.name.relation] = [rel, keys]
|
225
|
-
end
|
226
|
-
end
|
227
|
-
|
228
|
-
# @api private
|
229
|
-
def combine_from_assoc_with_fallback(name, other, keys)
|
230
|
-
combine_from_assoc(name, other) do
|
231
|
-
other.combine_method(relation, keys)
|
232
|
-
end
|
233
|
-
end
|
234
|
-
|
235
|
-
# Try to get a preloadable relation from a defined association
|
236
|
-
#
|
237
|
-
# If association doesn't exist we call the fallback block
|
238
|
-
#
|
239
|
-
# @return [RelationProxy]
|
240
|
-
#
|
241
|
-
# @api private
|
242
|
-
def combine_from_assoc(name, other, &fallback)
|
243
|
-
return other if other.curried?
|
244
|
-
associations.try(name) { |assoc| other.for_combine(assoc) } or fallback.call
|
245
|
-
end
|
246
|
-
|
247
|
-
# Extract result (either :one or :many), preloadable relation and its keys
|
248
|
-
# by using given association name
|
249
|
-
#
|
250
|
-
# This is used when a flat list of association names was passed to `combine`
|
251
|
-
#
|
252
|
-
# @api private
|
253
|
-
def combine_opts_for_assoc(name, opts = nil)
|
254
|
-
assoc = relation.associations[name]
|
255
|
-
curried = registry[assoc.target.relation].for_combine(assoc)
|
256
|
-
curried = curried.combine(opts) unless opts.nil?
|
257
|
-
keys = assoc.combine_keys(__registry__)
|
258
|
-
[assoc.result, curried, keys]
|
259
|
-
end
|
260
|
-
|
261
|
-
# Build a preloadable relation for relation graph
|
262
|
-
#
|
263
|
-
# When a given relation defines `for_other_relation` then it will be used
|
264
|
-
# to preload `other_relation`. ie `users` relation defines `for_tasks`
|
265
|
-
# then when we preload tasks for users, this custom method will be used
|
266
|
-
#
|
267
|
-
# This *defaults* to the built-in `for_combine` with explicitly provided
|
268
|
-
# keys
|
269
|
-
#
|
270
|
-
# @return [RelationProxy]
|
271
|
-
#
|
272
|
-
# @api private
|
273
|
-
def combine_method(other, keys)
|
274
|
-
custom_name = :"for_#{other.name.dataset}"
|
275
|
-
|
276
|
-
if relation.respond_to?(custom_name)
|
277
|
-
__send__(custom_name)
|
278
|
-
else
|
279
|
-
for_combine(keys)
|
280
|
-
end
|
281
|
-
end
|
282
|
-
|
283
|
-
# Infer key under which a combine relation will be loaded
|
284
|
-
#
|
285
|
-
# This is used in cases like ad-hoc combines where relation was passed
|
286
|
-
# in without specifying the key explicitly, ie:
|
287
|
-
#
|
288
|
-
# tasks.combine_parents(one: users)
|
289
|
-
#
|
290
|
-
# # ^^^ this will be expanded under-the-hood to:
|
291
|
-
# tasks.combine(one: { user: users })
|
292
|
-
#
|
293
|
-
# @return [Symbol]
|
294
|
-
#
|
295
|
-
# @api private
|
296
|
-
def combine_tuple_key(result)
|
297
|
-
if result == :one
|
298
|
-
Dry::Core::Inflector.singularize(base_name.relation).to_sym
|
299
|
-
else
|
300
|
-
base_name.relation
|
301
|
-
end
|
302
|
-
end
|
303
|
-
|
304
|
-
# Fallback mechanism for `combine_keys` when there's no association defined
|
305
|
-
#
|
306
|
-
# @api private
|
307
|
-
def infer_combine_keys(source, target, type)
|
308
|
-
primary_key = source.primary_key
|
309
|
-
foreign_key = target.foreign_key(source)
|
310
|
-
|
311
|
-
if type == :parent
|
312
|
-
{ foreign_key => primary_key }
|
313
|
-
else
|
314
|
-
{ primary_key => foreign_key }
|
315
|
-
end
|
316
|
-
end
|
317
|
-
end
|
318
|
-
end
|
319
|
-
end
|
320
|
-
end
|