rom-repository 0.3.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +6 -0
- data/lib/rom/repository/relation_proxy/combine.rb +283 -0
- data/lib/rom/repository/version.rb +1 -1
- data/rom-repository.gemspec +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: db1338c97747a9adc2b9f00febe402d61ac1a6fd
|
4
|
+
data.tar.gz: bd6ca3140fe897a0d6b9cccc3082e0fdaf63ed9a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 04f9d30b32805886118b5eb36451aeba065cc510d1a92d091c1e99a41585eb821cf69b68fa56b1f049610b390d1403316a662dacfe4ba32b7f65ba6ac60b8e84
|
7
|
+
data.tar.gz: 48a3b4136e064e00a562a584555765e8fb47a05388457b3cb31ab3b762dda4dd221299a1b621dfaf3705167412af644c8244c7ebbde22da285a8985f3afb712b
|
data/CHANGELOG.md
CHANGED
@@ -0,0 +1,283 @@
|
|
1
|
+
module ROM
|
2
|
+
class Repository
|
3
|
+
class RelationProxy
|
4
|
+
# Provides convenient methods for composing relations
|
5
|
+
#
|
6
|
+
# @api public
|
7
|
+
module Combine
|
8
|
+
# Returns a combine representation of a loading-proxy relation
|
9
|
+
#
|
10
|
+
# This will carry meta info used to produce a correct AST from a relation
|
11
|
+
# so that correct mapper can be generated
|
12
|
+
#
|
13
|
+
# @return [RelationProxy]
|
14
|
+
#
|
15
|
+
# @api private
|
16
|
+
def combined(name, keys, type)
|
17
|
+
meta = { keys: keys, combine_type: type, combine_name: name }
|
18
|
+
with(name: name, meta: meta)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Combine with other relations
|
22
|
+
#
|
23
|
+
# @overload combine(*associations)
|
24
|
+
# Composes relations using configured associations
|
25
|
+
# @example
|
26
|
+
# users.combine(:tasks, :posts)
|
27
|
+
# @param *associations [Array<Symbol>] A list of association names
|
28
|
+
#
|
29
|
+
# @overload combine(options)
|
30
|
+
# Composes relations based on options
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
# # users have-many tasks (name and join-keys inferred, which needs associations in schema)
|
34
|
+
# users.combine(many: tasks)
|
35
|
+
#
|
36
|
+
# # users have-many tasks with custom name (join-keys inferred, which needs associations in schema)
|
37
|
+
# users.combine(many: { priority_tasks: tasks.priority })
|
38
|
+
#
|
39
|
+
# # users have-many tasks with custom view and join keys
|
40
|
+
# users.combine(many: { tasks: [tasks.for_users, id: :task_id] })
|
41
|
+
#
|
42
|
+
# # users has-one task
|
43
|
+
# users.combine(one: { task: tasks })
|
44
|
+
#
|
45
|
+
# @param options [Hash] Options for combine
|
46
|
+
# @option :many [Hash] Sets options for "has-many" type of association
|
47
|
+
# @option :one [Hash] Sets options for "has-one/belongs-to" type of association
|
48
|
+
#
|
49
|
+
# @param [Hash] options
|
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 |(type, relations)|
|
60
|
+
if relations
|
61
|
+
combine_opts[type] = combine_opts_from_relations(relations)
|
62
|
+
else
|
63
|
+
result, curried, keys = combine_opts_for_assoc(type)
|
64
|
+
combine_opts[result][type] = [curried, keys]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
nodes = combine_opts.flat_map do |type, relations|
|
69
|
+
relations.map { |name, (relation, keys)|
|
70
|
+
relation.combined(name, keys, type)
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
__new__(relation.combine(*nodes))
|
75
|
+
end
|
76
|
+
|
77
|
+
# Shortcut for combining with parents which infers the join keys
|
78
|
+
#
|
79
|
+
# @example
|
80
|
+
# # tasks belong-to users
|
81
|
+
# tasks.combine_parents(one: users)
|
82
|
+
#
|
83
|
+
# # tasks belong-to users with custom user view
|
84
|
+
# tasks.combine_parents(one: users.task_owners)
|
85
|
+
#
|
86
|
+
# @param options [Hash] Combine options hash
|
87
|
+
#
|
88
|
+
# @return [RelationProxy]
|
89
|
+
#
|
90
|
+
# @api public
|
91
|
+
def combine_parents(options)
|
92
|
+
combine_opts = {}
|
93
|
+
|
94
|
+
options.each do |type, parents|
|
95
|
+
combine_opts[type] =
|
96
|
+
case parents
|
97
|
+
when Hash
|
98
|
+
parents.each_with_object({}) { |(name, parent), r|
|
99
|
+
keys = combine_keys(parent, relation, :parent)
|
100
|
+
r[name] = [parent, keys]
|
101
|
+
}
|
102
|
+
when Array
|
103
|
+
parents.each_with_object({}) { |parent, r|
|
104
|
+
tuple_key = parent.combine_tuple_key(type)
|
105
|
+
keys = combine_keys(parent, relation, :parent)
|
106
|
+
r[tuple_key] = [parent, keys]
|
107
|
+
}
|
108
|
+
else
|
109
|
+
tuple_key = parents.combine_tuple_key(type)
|
110
|
+
keys = combine_keys(parents, relation, :parent)
|
111
|
+
{ tuple_key => [parents, keys] }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
combine(combine_opts)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Shortcut for combining with children which infers the join keys
|
119
|
+
#
|
120
|
+
# @example
|
121
|
+
# # users have-many tasks
|
122
|
+
# users.combine_children(many: tasks)
|
123
|
+
#
|
124
|
+
# # users have-many tasks with custom mapping (requires associations)
|
125
|
+
# users.combine_children(many: { priority_tasks: tasks.priority })
|
126
|
+
#
|
127
|
+
# @param [Hash] options
|
128
|
+
#
|
129
|
+
# @return [RelationProxy]
|
130
|
+
#
|
131
|
+
# @api public
|
132
|
+
def combine_children(options)
|
133
|
+
combine_opts = {}
|
134
|
+
|
135
|
+
options.each do |type, children|
|
136
|
+
combine_opts[type] =
|
137
|
+
case children
|
138
|
+
when Hash
|
139
|
+
children.each_with_object({}) { |(name, child), r|
|
140
|
+
keys = combine_keys(relation, child, :children)
|
141
|
+
r[name] = [child, keys]
|
142
|
+
}
|
143
|
+
when Array
|
144
|
+
parents.each_with_object({}) { |child, r|
|
145
|
+
tuple_key = parent.combine_tuple_key(type)
|
146
|
+
keys = combine_keys(relation, child, :children)
|
147
|
+
r[tuple_key] = [parent, keys]
|
148
|
+
}
|
149
|
+
else
|
150
|
+
tuple_key = children.combine_tuple_key(type)
|
151
|
+
keys = combine_keys(relation, children, :children)
|
152
|
+
{ tuple_key => [children, keys] }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
combine(combine_opts)
|
157
|
+
end
|
158
|
+
|
159
|
+
protected
|
160
|
+
|
161
|
+
# Infer join/combine keys for a given relation and association type
|
162
|
+
#
|
163
|
+
# When source has association corresponding to target's name, it'll be
|
164
|
+
# used to get the keys. Otherwise we fall back to using default keys based
|
165
|
+
# on naming conventions.
|
166
|
+
#
|
167
|
+
# @param [RelationProxy] relation
|
168
|
+
# @param [Symbol] type The type can be either :parent or :children
|
169
|
+
#
|
170
|
+
# @return [Hash<Symbol=>Symbol>]
|
171
|
+
#
|
172
|
+
# @api private
|
173
|
+
def combine_keys(source, target, type)
|
174
|
+
source.associations.try(target.name) { |assoc|
|
175
|
+
assoc.combine_keys(__registry__)
|
176
|
+
} or infer_combine_keys(source, target, type)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Build combine options from a relation mapping hash passed to `combine`
|
180
|
+
#
|
181
|
+
# This method will infer combine keys either from defined associations
|
182
|
+
# or use the keys provided explicitly for ad-hoc combines
|
183
|
+
#
|
184
|
+
# It returns a mapping like `name => [preloadable_relation, combine_keys]`
|
185
|
+
# and this mapping is used by `combine` to build a full relation graph
|
186
|
+
#
|
187
|
+
# @api private
|
188
|
+
def combine_opts_from_relations(relations)
|
189
|
+
relations.each_with_object({}) do |(name, (other, keys)), h|
|
190
|
+
h[name] =
|
191
|
+
if other.curried?
|
192
|
+
[other, keys]
|
193
|
+
else
|
194
|
+
rel = combine_from_assoc(name, other) { other.combine_method(relation, keys) }
|
195
|
+
[rel, keys]
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Try to get a preloadable relation from a defined association
|
201
|
+
#
|
202
|
+
# If association doesn't exist we call the fallback block
|
203
|
+
#
|
204
|
+
# @return [RelationProxy]
|
205
|
+
#
|
206
|
+
# @api private
|
207
|
+
def combine_from_assoc(name, other, &fallback)
|
208
|
+
associations.try(name) { |assoc| other.for_combine(assoc) } or fallback.call
|
209
|
+
end
|
210
|
+
|
211
|
+
# Extract result (either :one or :many), preloadable relation and its keys
|
212
|
+
# by using given association name
|
213
|
+
#
|
214
|
+
# This is used when a flat list of association names was passed to `combine`
|
215
|
+
#
|
216
|
+
# @api private
|
217
|
+
def combine_opts_for_assoc(name)
|
218
|
+
assoc = relation.associations[name]
|
219
|
+
curried = registry[assoc.target.relation].for_combine(assoc)
|
220
|
+
keys = assoc.combine_keys(__registry__)
|
221
|
+
[assoc.result, curried, keys]
|
222
|
+
end
|
223
|
+
|
224
|
+
# Build a preloadable relation for relation graph
|
225
|
+
#
|
226
|
+
# When a given relation defines `for_other_relation` then it will be used
|
227
|
+
# to preload `other_relation`. ie `users` relation defines `for_tasks`
|
228
|
+
# then when we preload tasks for users, this custom method will be used
|
229
|
+
#
|
230
|
+
# This *defaults* to the built-in `for_combine` with explicitly provided
|
231
|
+
# keys
|
232
|
+
#
|
233
|
+
# @return [RelationProxy]
|
234
|
+
#
|
235
|
+
# @api private
|
236
|
+
def combine_method(other, keys)
|
237
|
+
custom_name = :"for_#{other.name.dataset}"
|
238
|
+
|
239
|
+
if relation.respond_to?(custom_name)
|
240
|
+
__send__(custom_name)
|
241
|
+
else
|
242
|
+
for_combine(keys)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Infer key under which a combine relation will be loaded
|
247
|
+
#
|
248
|
+
# This is used in cases like ad-hoc combines where relation was passed
|
249
|
+
# in without specifying the key explicitly, ie:
|
250
|
+
#
|
251
|
+
# tasks.combine_parents(one: users)
|
252
|
+
#
|
253
|
+
# # ^^^ this will be expanded under-the-hood to:
|
254
|
+
# tasks.combine(one: { user: users })
|
255
|
+
#
|
256
|
+
# @return [Symbol]
|
257
|
+
#
|
258
|
+
# @api private
|
259
|
+
def combine_tuple_key(result)
|
260
|
+
if result == :one
|
261
|
+
Inflector.singularize(base_name.relation).to_sym
|
262
|
+
else
|
263
|
+
base_name.relation
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# Fallback mechanism for `combine_keys` when there's no association defined
|
268
|
+
#
|
269
|
+
# @api private
|
270
|
+
def infer_combine_keys(source, target, type)
|
271
|
+
primary_key = source.primary_key
|
272
|
+
foreign_key = target.foreign_key(source)
|
273
|
+
|
274
|
+
if type == :parent
|
275
|
+
{ foreign_key => primary_key }
|
276
|
+
else
|
277
|
+
{ primary_key => foreign_key }
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
data/rom-repository.gemspec
CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |gem|
|
|
11
11
|
gem.homepage = 'http://rom-rb.org'
|
12
12
|
gem.require_paths = ['lib']
|
13
13
|
gem.version = ROM::Repository::VERSION.dup
|
14
|
-
gem.files = `git ls-files`.split("\n").reject { |name| name.include?('benchmarks') || name.include?('examples') || name.include?('bin') }
|
14
|
+
gem.files = `git ls-files`.split("\n").reject { |name| name.include?('benchmarks') || name.include?('examples') || name.include?('bin/console') }
|
15
15
|
gem.test_files = `git ls-files -- {spec}/*`.split("\n")
|
16
16
|
gem.license = 'MIT'
|
17
17
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rom-repository
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Piotr Solnica
|
@@ -107,6 +107,7 @@ files:
|
|
107
107
|
- lib/rom/repository/header_builder.rb
|
108
108
|
- lib/rom/repository/mapper_builder.rb
|
109
109
|
- lib/rom/repository/relation_proxy.rb
|
110
|
+
- lib/rom/repository/relation_proxy/combine.rb
|
110
111
|
- lib/rom/repository/relation_proxy/wrap.rb
|
111
112
|
- lib/rom/repository/root.rb
|
112
113
|
- lib/rom/repository/struct_attributes.rb
|