rom-repository 1.4.0 → 2.0.0.beta1
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 +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
|