-
Notifications
You must be signed in to change notification settings - Fork 368
Expand file tree
/
Copy pathvcap_relations.rb
More file actions
207 lines (172 loc) · 6.48 KB
/
vcap_relations.rb
File metadata and controls
207 lines (172 loc) · 6.48 KB
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
module Sequel::Plugins::VcapRelations
# Depend on the instance_hooks plugin.
def self.apply(model)
model.plugin(:instance_hooks)
end
module InstanceMethods
def has_one_to_many?(association)
association_type(association) == :one_to_many && send(association).any?
end
def has_one_to_one?(association)
association_type(association) == :one_to_one && !!send(association)
end
def association_type(association)
self.class.association_reflection(association)[:type]
end
def relationship_dataset(association)
reflection = self.class.association_reflection(association)
if (dataset = reflection[:dataset])
if dataset.arity == 1
instance_exec(reflection, &dataset)
else
instance_exec(&dataset)
end
else
reflection.associated_class.dataset
end
end
end
module ClassMethods
# Override many_to_one in order to add <relation>_guid
# and <relation>_guid= methods.
#
# See the default many_to_one implementation for a description of the args
# and return values.
def many_to_one(name, opts={})
define_guid_accessors(name) unless opts.fetch(:without_guid_generation, false)
opts[:reciprocal] ||= self.name.split('::').last.underscore.pluralize.to_sym
super
end
def one_through_one(name, opts={})
define_guid_reader(name)
super
end
# Override many_to_many in order to add an override the default Sequel
# methods for many_to_many relationships.
#
# In particular, this enables support of bulk modifying relationships.
#
# See the default many_to_many implementation for a description of the args
# and return values.
def many_to_many(name, opts={})
singular_name = name.to_s.singularize
ids_attr = "#{singular_name}_ids"
guids_attr = "#{singular_name}_guids"
define_method("add_#{singular_name}") do |other|
# sequel is not capable of merging adds to a many_to_many association
# like it is for a one_to_many and nds up throwing a db exception,
# so lets squash the add
db.transaction(savepoint: true) do
if other.is_a?(Integer)
super(other) unless send(ids_attr).include? other
else
super(other) unless send(name).include? other
end
rescue Sequel::UniqueConstraintViolation => e
# ignore the error and rollback the inner transaction
raise Sequel::Rollback if opts[:ignored_unique_constraint_violation_errors]&.any? { |pattern| e.message.include?(pattern) }
raise e
end
end
opts[:reciprocal] ||=
self.name.split('::').last.underscore.pluralize.to_sym
define_to_many_methods(name, singular_name, ids_attr, guids_attr)
super
end
# Override one_to_many in order to add an override the default Sequel
# methods for one_to_many relationships.
#
# In particular, this enables support of bulk modifying relationships.
#
# See the default one_to_many implementation for a description of the args
# and return values.
def one_to_many(name, opts={})
singular_name = name.to_s.singularize
ids_attr = "#{singular_name}_ids"
guids_attr = "#{singular_name}_guids"
opts[:reciprocal] ||= self.name.split('::').last.underscore.to_sym
define_to_many_methods(name, singular_name, ids_attr, guids_attr)
super
end
private
def define_guid_accessors(name)
define_guid_reader(name)
define_guid_writer(name)
end
def define_guid_writer(name)
define_method("#{guid_attr(name)}=") do |val|
other = nil
if val
ar = self.class.association_reflection(name)
other = ar.associated_class[guid: val]
raise CloudController::Errors::ApiError.new_from_details('InvalidRelation', "Could not find #{ar.associated_class.name.demodulize} with guid: #{val}") if other.nil?
end
send("#{name}=", other)
end
end
def define_guid_reader(name)
define_method(guid_attr(name)) do
other = send(name)
other.guid unless other.nil?
end
end
def guid_attr(name)
"#{name}_guid"
end
# rubocop:todo Metrics/CyclomaticComplexity
def define_to_many_methods(name, singular_name, ids_attr, guids_attr)
diff_collections = proc do |a, b|
cur_set = Set.new(a)
new_set = Set.new(b)
intersection = cur_set & new_set
added = new_set - intersection
removed = cur_set - intersection
[added, removed]
end
define_method(ids_attr) do
send(name).collect(&:id)
end
# greppable: add_domain_by_guid
define_method("add_#{singular_name}_by_guid") do |guid|
ar = self.class.association_reflection(name)
other = ar.associated_class[guid:]
raise CloudController::Errors::ApiError.new_from_details('InvalidRelation', "Could not find #{ar.associated_class.name} with guid: #{guid}") if other.nil?
if pk
send("add_#{singular_name}", other)
else
after_save_hook { send("add_#{singular_name}", other) }
end
end
define_method("#{ids_attr}=") do |ids|
return unless ids
ds = send(name)
ds.each { |r| send("remove_#{singular_name}", r) unless ids.include?(r.id) }
ids.each { |i| send("add_#{singular_name}", i) }
end
define_method(guids_attr) do
send(name).collect(&:guid)
end
define_method("#{guids_attr}=") do |guids|
return unless guids
current_guids = send(name).map(&:guid)
(added, removed) = diff_collections.call(current_guids, guids)
added.each { |g| send("add_#{singular_name}_by_guid", g) }
removed.each { |g| send("remove_#{singular_name}_by_guid", g) }
end
define_method("remove_#{singular_name}_by_guid") do |guid|
ar = self.class.association_reflection(name)
other = ar.associated_class[guid:]
raise CloudController::Errors::ApiError.new_from_details('InvalidRelation', "Could not find #{ar.associated_class.name} with guid: #{guid}") if other.nil?
send("remove_#{singular_name}", other)
end
define_method("remove_#{singular_name}") do |other|
if other.is_a?(Integer)
super(other) if send(ids_attr).include? other
elsif send(name).map(&:pk).include? other.pk
super(other)
end
end
end
# rubocop:enable Metrics/CyclomaticComplexity
end
end