rom-repository 0.3.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|