duty_free 1.0.8 → 1.0.10
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/lib/duty_free/extensions.rb +39 -30
- data/lib/duty_free/suggest_template.rb +378 -166
- data/lib/duty_free/util.rb +26 -12
- data/lib/duty_free/version_number.rb +1 -1
- data/lib/duty_free.rb +32 -7
- data/lib/generators/duty_free/model_generator.rb +349 -0
- data/lib/generators/duty_free/templates/create_versions.rb.erb +2 -2
- metadata +4 -9
@@ -7,7 +7,6 @@ module DutyFree
|
|
7
7
|
# Helpful suggestions to get started creating a template
|
8
8
|
# Pass in -1 for hops if you want to traverse all possible links
|
9
9
|
def suggest_template(hops = 0, do_has_many = false, show_output = true, this_klass = self)
|
10
|
-
::DutyFree.instance_variable_set(:@errored_assocs, [])
|
11
10
|
::DutyFree.instance_variable_set(:@errored_columns, [])
|
12
11
|
uniques, _required = ::DutyFree::SuggestTemplate._suggest_unique_column(this_klass, nil, '')
|
13
12
|
template, required = ::DutyFree::SuggestTemplate._suggest_template(hops, do_has_many, this_klass)
|
@@ -17,7 +16,6 @@ module DutyFree
|
|
17
16
|
all: template,
|
18
17
|
as: {}
|
19
18
|
}
|
20
|
-
# puts "Errors: #{::DutyFree.instance_variable_get(:@errored_assocs).inspect}"
|
21
19
|
|
22
20
|
if show_output
|
23
21
|
path = this_klass.name.split('::').map(&:underscore).join('/')
|
@@ -32,161 +30,369 @@ module DutyFree
|
|
32
30
|
end
|
33
31
|
end
|
34
32
|
|
35
|
-
def self._suggest_template(hops, do_has_many, this_klass
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
33
|
+
def self._suggest_template(hops, do_has_many, this_klass)
|
34
|
+
poison_links = []
|
35
|
+
requireds = []
|
36
|
+
errored_assocs = []
|
37
|
+
buggy_bts = []
|
38
|
+
is_papertrail = Object.const_defined?('::PaperTrail::Version')
|
39
|
+
# Get a list of all polymorphic models by searching out every has_many that has an :as option
|
40
|
+
all_polymorphics = Hash.new { |h, k| h[k] = [] }
|
41
|
+
_all_models.each do |model|
|
42
|
+
model.reflect_on_all_associations.select do |assoc|
|
43
|
+
# If the model name can not be found then we get something like this here:
|
44
|
+
# 1: from /home/lorin/.rvm/gems/ruby-2.6.5/gems/activerecord-5.2.4.6/lib/active_record/reflection.rb:418:in `compute_class'
|
45
|
+
# /home/lorin/.rvm/gems/ruby-2.6.5/gems/activerecord-5.2.4.6/lib/active_record/inheritance.rb:196:in `compute_type': uninitialized constant Site::SiteTeams (NameError)
|
46
|
+
ret = nil
|
47
|
+
begin
|
48
|
+
ret = assoc.polymorphic? ||
|
49
|
+
(assoc.options.include?(:as) && !(is_papertrail && assoc.klass&.<=(PaperTrail::Version)))
|
50
|
+
rescue NameError
|
51
|
+
false
|
54
52
|
end
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
53
|
+
ret
|
54
|
+
end.each do |assoc|
|
55
|
+
poly_hm = if assoc.belongs_to?
|
56
|
+
poly_key = [model, assoc.name.to_sym] if assoc.polymorphic?
|
57
|
+
nil
|
58
|
+
else
|
59
|
+
poly_key = [assoc.klass, assoc.options[:as]]
|
60
|
+
[model, assoc.macro, assoc.name.to_sym]
|
61
|
+
end
|
62
|
+
next if all_polymorphics.include?(poly_key) && all_polymorphics[poly_key].include?(poly_hm)
|
63
|
+
|
64
|
+
poly_hms = all_polymorphics[poly_key]
|
65
|
+
poly_hms << poly_hm if poly_hm
|
64
66
|
end
|
65
|
-
|
67
|
+
end
|
66
68
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
69
|
+
bad_polymorphic_hms = []
|
70
|
+
# "path" starts with ''
|
71
|
+
# "template" starts with [], and we hold on to the root piece here so we can return it after
|
72
|
+
# going through all the layers, at which point it will have grown to include the entire hierarchy.
|
73
|
+
this_layer = [[do_has_many, this_klass, '', (whole_template = [])]]
|
74
|
+
loop do
|
75
|
+
next_layer = []
|
76
|
+
this_layer.each do |klass_item|
|
77
|
+
do_has_many, this_klass, path, template = klass_item
|
78
|
+
this_primary_key = Array(this_klass.primary_key)
|
79
|
+
# Find all associations, and track all belongs_tos
|
80
|
+
this_belongs_tos = []
|
81
|
+
assocs = {}
|
82
|
+
this_klass.reflect_on_all_associations.each do |assoc|
|
83
|
+
# PolymorphicReflection AggregateReflection RuntimeReflection
|
84
|
+
is_belongs_to = assoc.belongs_to?
|
85
|
+
# Figure out if it's belongs_to, has_many, or has_one
|
86
|
+
belongs_to_or_has_many =
|
87
|
+
if is_belongs_to
|
88
|
+
'belongs_to'
|
89
|
+
elsif (is_habtm = assoc.macro == :has_and_belongs_to_many)
|
90
|
+
'has_and_belongs_to_many'
|
91
|
+
elsif assoc.macro == :has_many
|
92
|
+
'has_many'
|
93
|
+
else
|
94
|
+
'has_one'
|
95
|
+
end
|
96
|
+
# Always process belongs_to, and also process has_one and has_many if do_has_many is chosen.
|
97
|
+
# Skip any HMT or HABTM. (Maybe break out HABTM into a combo HM and BT in the future.)
|
98
|
+
if is_habtm
|
99
|
+
puts "* #{this_klass.name} model - problem with: \"has_and_belongs_to_many :#{assoc.name}\" because join table \"#{assoc.join_table}\" does not exist. You can create it with a create_join_table migration." unless ActiveRecord::Base.connection.table_exists?(assoc.join_table)
|
100
|
+
# %%% Search for other associative candidates to use instead of this HABTM contraption
|
101
|
+
puts "* #{this_klass.name} model - problem with: \"has_and_belongs_to_many :#{assoc.name}\" because it includes \"through: #{assoc.options[:through].inspect}\" which is pointless and should be removed." if assoc.options.include?(:through)
|
102
|
+
end
|
103
|
+
if (is_through = assoc.is_a?(ActiveRecord::Reflection::ThroughReflection)) && assoc.options.include?(:as)
|
104
|
+
puts "* #{this_klass.name} model - problem with: \"has_many :#{assoc.name} through: #{assoc.options[:through].inspect}\" because it also includes \"as: #{assoc.options[:as].inspect}\", " \
|
105
|
+
"so please choose either for this line to be a \"has_many :#{assoc.name} through:\" or to be a polymorphic \"has_many :#{assoc.name} as:\". It can't be both."
|
106
|
+
end
|
107
|
+
next if is_through || is_habtm || (!is_belongs_to && !do_has_many) || errored_assocs.include?(assoc)
|
79
108
|
|
80
|
-
#
|
81
|
-
|
82
|
-
|
109
|
+
# Polymorphic belongs_to?
|
110
|
+
# (checking all_polymorphics in order to handle the rare case when the belongs_to side of a polymorphic association is missing "polymorphic: true")
|
111
|
+
if is_belongs_to && (
|
112
|
+
assoc.options[:polymorphic] ||
|
113
|
+
(
|
114
|
+
all_polymorphics.include?(poly_key = [assoc.active_record, assoc.name.to_sym]) &&
|
115
|
+
all_polymorphics[poly_key].map { |p| p&.first }.include?(this_klass)
|
116
|
+
)
|
117
|
+
)
|
118
|
+
# Find all current possible polymorphic relations
|
119
|
+
_all_models.each do |model|
|
120
|
+
# Skip auto-generated HABTM_DestinationModel models
|
121
|
+
next if model.respond_to?(:table_name_resolver) &&
|
122
|
+
model.name.start_with?('HABTM_') &&
|
123
|
+
model.table_name_resolver.is_a?(
|
124
|
+
ActiveRecord::Associations::Builder::HasAndBelongsToMany::JoinTableResolver::KnownClass
|
125
|
+
)
|
83
126
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
end
|
88
|
-
else
|
89
|
-
# Is it a polymorphic has_many, which is defined using as: :somethingable ?
|
90
|
-
is_polymorphic_hm = assoc.inverse_of&.options&.fetch(:polymorphic) { nil }
|
91
|
-
begin
|
92
|
-
# Standard has_one, or has_many, and belongs_to uses assoc.klass.
|
93
|
-
# Also polymorphic belongs_to uses assoc.klass.
|
94
|
-
assoc_klass = is_polymorphic_hm ? assoc.inverse_of.active_record : assoc.klass
|
95
|
-
rescue NameError # For models which cannot be found by name
|
96
|
-
end
|
97
|
-
new_assoc =
|
98
|
-
if assoc_klass.nil?
|
99
|
-
puts "* In the #{this_klass.name} model there's a problem with: \"#{belongs_to_or_has_many} :#{assoc.name}\" because there is no \"#{assoc.class_name}\" model."
|
100
|
-
nil # Cause this one to be excluded
|
101
|
-
elsif is_belongs_to
|
102
|
-
this_belongs_tos << (fk = assoc.foreign_key.to_s)
|
103
|
-
[[[fk], assoc.active_record], assoc_klass]
|
104
|
-
else # has_many or has_one
|
105
|
-
inverse_foreign_keys = is_polymorphic_hm ? [assoc.type, assoc.foreign_key] : [assoc.inverse_of&.foreign_key&.to_s]
|
106
|
-
missing_key_columns = inverse_foreign_keys - assoc_klass.columns.map(&:name)
|
107
|
-
if missing_key_columns.empty?
|
108
|
-
puts "* Missing inverse foreign key for #{this_klass.name} #{belongs_to_or_has_many} :#{assoc.name}" if inverse_foreign_keys.first.nil?
|
109
|
-
# puts "Has columns #{inverse_foreign_keys.inspect}"
|
110
|
-
[[inverse_foreign_keys, assoc_klass], assoc_klass]
|
111
|
-
else
|
112
|
-
if inverse_foreign_keys.length > 1
|
113
|
-
puts "* The #{assoc_klass.name} model is missing #{missing_key_columns.join(' and ')} columns to allow it to support polymorphic inheritance."
|
114
|
-
else
|
115
|
-
print "* In the #{this_klass.name} model there's a problem with: \"#{belongs_to_or_has_many} :#{assoc.name}\"."
|
127
|
+
# Find applicable polymorphic has_many associations from each real model
|
128
|
+
model.reflect_on_all_associations.each do |poly_assoc|
|
129
|
+
next unless [:has_many, :has_one].include?(poly_assoc.macro) && poly_assoc.inverse_of == assoc
|
116
130
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
131
|
+
this_belongs_tos += (fkeys = [poly_assoc.type, poly_assoc.foreign_key])
|
132
|
+
assocs["#{assoc.name}_#{poly_assoc.active_record.name.underscore}".to_sym] = [[fkeys, assoc.active_record], poly_assoc.active_record]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
else
|
136
|
+
# Is it a polymorphic has_many, which is defined using as: :somethingable ?
|
137
|
+
is_polymorphic_hm = assoc.inverse_of&.options&.fetch(:polymorphic) { nil }
|
138
|
+
begin
|
139
|
+
# Standard has_one, or has_many, and belongs_to uses assoc.klass.
|
140
|
+
# Also polymorphic belongs_to uses assoc.klass.
|
141
|
+
assoc_klass = is_polymorphic_hm ? assoc.inverse_of.active_record : assoc.klass
|
142
|
+
rescue NameError # For models which cannot be found by name
|
143
|
+
end
|
144
|
+
# Skip any PaperTrail audited things
|
145
|
+
# rubocop:disable Lint/SafeNavigationConsistency
|
146
|
+
next if (Object.const_defined?('PaperTrail::Version') && assoc_klass&.<=(PaperTrail::Version) && assoc.options.include?(:as)) ||
|
147
|
+
# And any goofy self-referencing aliases
|
148
|
+
(!is_belongs_to && assoc_klass <= assoc.active_record && assoc.foreign_key.to_s == assoc.active_record.primary_key)
|
149
|
+
|
150
|
+
# rubocop:enable Lint/SafeNavigationConsistency
|
151
|
+
|
152
|
+
# Avoid getting goofed up by the belongs_to side of a broken polymorphic association
|
153
|
+
assoc_klass = nil if assoc.belongs_to? && !(assoc_klass <= ActiveRecord::Base)
|
154
|
+
|
155
|
+
if !is_polymorphic_hm && assoc.options.include?(:as)
|
156
|
+
assoc_klass = assoc.inverse_of.active_record
|
157
|
+
is_polymorphic_hm = true
|
158
|
+
bad_polymorphic_hm = [assoc_klass, assoc.inverse_of]
|
159
|
+
unless bad_polymorphic_hms.include?(bad_polymorphic_hm)
|
160
|
+
bad_polymorphic_hms << bad_polymorphic_hm
|
161
|
+
puts "* #{assoc_klass.name} model - problem with the polymorphic association \"belongs_to :#{assoc.inverse_of.name}\". You can fix this in one of two ways:"
|
162
|
+
puts ' (1) add "polymorphic: true" on this belongs_to line, or'
|
163
|
+
poly_hms = all_polymorphics.inject([]) do |s, poly_hm|
|
164
|
+
if (key = poly_hm.first).first <= assoc_klass && key.last == assoc.inverse_of.name
|
165
|
+
s += poly_hm.last
|
122
166
|
end
|
167
|
+
s
|
168
|
+
end
|
169
|
+
puts " (2) Undo #{assoc_klass.name} polymorphism by removing \"as: :#{assoc.inverse_of.name}\" in these #{poly_hms.length} places:"
|
170
|
+
poly_hms.each { |poly_hm| puts " In the #{poly_hm.first.name} class from the line: #{poly_hm[1]} :#{poly_hm.last}" }
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
new_assoc =
|
175
|
+
if assoc_klass.nil?
|
176
|
+
# In case this is a buggy polymorphic belongs_to, keep track of all of these and at the very end
|
177
|
+
# only show the pertinent ones.
|
178
|
+
if is_belongs_to
|
179
|
+
buggy_bts << [this_klass, assoc]
|
123
180
|
else
|
124
|
-
puts "
|
181
|
+
puts "* #{this_klass.name} model - problem with: \"#{belongs_to_or_has_many} :#{assoc.name}\" because there is no \"#{assoc.class_name}\" model."
|
125
182
|
end
|
183
|
+
nil # Cause this one to be excluded
|
184
|
+
elsif is_belongs_to
|
185
|
+
this_belongs_tos << (foreign_key = assoc.foreign_key.to_s)
|
186
|
+
[[[foreign_key], assoc.active_record], assoc_klass]
|
187
|
+
elsif _all_tables.include?(assoc_klass.table_name) || # has_many or has_one
|
188
|
+
(assoc_klass.table_name.start_with?('public.') && _all_tables.include?(assoc_klass.table_name[7..-1]))
|
189
|
+
inverse_foreign_keys = is_polymorphic_hm ? [assoc.type, assoc.foreign_key] : [assoc.inverse_of&.foreign_key&.to_s]
|
190
|
+
missing_key_columns = inverse_foreign_keys - assoc_klass.columns.map(&:name)
|
191
|
+
if missing_key_columns.empty?
|
192
|
+
puts "* Missing inverse foreign key for #{this_klass.name} #{belongs_to_or_has_many} :#{assoc.name}" if inverse_foreign_keys.first.nil?
|
193
|
+
# puts "Has columns #{inverse_foreign_keys.inspect}"
|
194
|
+
[[inverse_foreign_keys, assoc_klass], assoc_klass]
|
195
|
+
else
|
196
|
+
if inverse_foreign_keys.length > 1
|
197
|
+
puts "* #{assoc_klass.name} model - missing #{missing_key_columns.join(' and ')} columns to allow it to support polymorphic inheritance."
|
198
|
+
else
|
199
|
+
puts "* #{this_klass.name} model - problem with: \"#{belongs_to_or_has_many} :#{assoc.name}\"."
|
200
|
+
|
201
|
+
# Most general related parent class in case we're STI
|
202
|
+
root_class = test_class = this_klass
|
203
|
+
while (test_class = test_class.superclass) != ActiveRecord::Base
|
204
|
+
root_class = test_class unless test_class.abstract_class?
|
205
|
+
end
|
206
|
+
# If we haven't yet found a match, search for any appropriate unused foreign key that belongs_to the primary class
|
207
|
+
is_mentioned_consider = false
|
208
|
+
if (inverses = _find_assocs(:belongs_to, assoc_klass, root_class, errored_assocs, assoc.foreign_key)).empty?
|
209
|
+
if inverse_foreign_keys.first.nil?
|
210
|
+
# So we can rule them out, find the belongs_tos that already are inverses of any other relevant has_many
|
211
|
+
hm_assocs = _find_assocs(:has_many, this_klass, assoc_klass, errored_assocs)
|
212
|
+
hm_inverses = hm_assocs.each_with_object([]) do |hm, s|
|
213
|
+
s << hm.inverse_of if hm.inverse_of
|
214
|
+
s
|
215
|
+
end
|
216
|
+
# Remaining belongs_tos are also good candidates to become an inverse_of, so we'll suggest
|
217
|
+
# both to establish a :foreign_key and also duing the inverses.present? check an :inverse_of.
|
218
|
+
inverses = _find_assocs(:belongs_to, assoc_klass, root_class, errored_assocs).reject do |bt|
|
219
|
+
hm_inverses.include?(bt)
|
220
|
+
end
|
221
|
+
fks = inverses.map(&:foreign_key)
|
222
|
+
# All that and still no matches?
|
223
|
+
unless fks.present? || assoc_klass.columns.map(&:name).include?(suggested_fk = "#{root_class.name.underscore}_id")
|
224
|
+
# Find any polymorphic association on this model (that we're not already tied to) that could be used.
|
225
|
+
poly_hms = all_polymorphics.each_with_object([]) do |p, s|
|
226
|
+
if p.first.first == assoc_klass &&
|
227
|
+
p.last.none? { |poly_hm| this_klass <= poly_hm.first } # <= to deal with polymorphic inheritance
|
228
|
+
s << p.first
|
229
|
+
end
|
230
|
+
s
|
231
|
+
end
|
232
|
+
|
233
|
+
# Consider all the HMT with through: :contacts, find their source(s)
|
234
|
+
poly_hmts = this_klass.reflect_on_all_associations.each_with_object([]) do |a, s|
|
235
|
+
if [:has_many, :has_one].include?(a.macro) && a.options[:source] &&
|
236
|
+
a.options[:through] == assoc.name
|
237
|
+
s << a.options[:source]
|
238
|
+
end
|
239
|
+
s
|
240
|
+
end
|
241
|
+
poly_hms_hmts = poly_hms.select { |poly_hm| poly_hmts.include?(poly_hm.last) }
|
242
|
+
poly_hms = poly_hms_hmts unless poly_hms_hmts.blank?
|
243
|
+
|
244
|
+
poly_hms.map! { |poly_hm| "\"as: :#{poly_hm.last}\"" }
|
245
|
+
if poly_hms.blank?
|
246
|
+
puts " Consider removing this #{belongs_to_or_has_many} because the #{assoc_klass.name} model does not include a column named \"#{suggested_fk}\"."
|
247
|
+
else
|
248
|
+
puts " Consider adding #{poly_hms.join(' or ')} to establish a valid polymorphic association."
|
249
|
+
end
|
250
|
+
is_mentioned_consider = true
|
251
|
+
end
|
252
|
+
unless fks.empty? || fks.include?(assoc.foreign_key.to_s)
|
253
|
+
puts " Consider adding #{fks.map { |fk| "\"foreign_key: :#{fk}\"" }.join(' or ')} (or some other appropriate column from #{assoc_klass.name}) to this #{belongs_to_or_has_many} entry."
|
254
|
+
is_mentioned_consider = true
|
255
|
+
end
|
256
|
+
else
|
257
|
+
puts " (Cannot find foreign key \"#{inverse_foreign_keys.first.inspect}\" in #{assoc_klass.name}.)"
|
258
|
+
end
|
259
|
+
end
|
260
|
+
if inverses.empty?
|
261
|
+
opposite_macro = assoc.belongs_to? ? 'has_many or has_one' : 'belongs_to'
|
262
|
+
puts " (Could not identify any inverse #{opposite_macro} association in the #{assoc_klass.name} model.)"
|
263
|
+
else
|
264
|
+
print is_mentioned_consider ? ' Also consider ' : ' Consider '
|
265
|
+
puts "adding \"#{inverses.map { |x| "inverse_of: :#{x.name}" }.join(' or ')}\" to this entry."
|
266
|
+
end
|
267
|
+
end
|
268
|
+
nil
|
269
|
+
end
|
270
|
+
else
|
271
|
+
puts "* Missing table #{assoc_klass.table_name} for class #{assoc_klass.name}"
|
272
|
+
puts ' (Maybe try running: bin/rails db:migrate )'
|
273
|
+
nil # Related has_* is missing its table
|
126
274
|
end
|
127
|
-
|
275
|
+
if new_assoc.nil?
|
276
|
+
errored_assocs << assoc
|
277
|
+
else
|
278
|
+
assocs[assoc.name] = new_assoc
|
128
279
|
end
|
129
280
|
end
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
281
|
+
end
|
282
|
+
|
283
|
+
# Include all columns except for the primary key, any foreign keys, and excluded_columns
|
284
|
+
# %%% add EXCLUDED_ALL_COLUMNS || ...
|
285
|
+
excluded_columns = %w[created_at updated_at deleted_at]
|
286
|
+
(this_klass.columns.map(&:name) - this_primary_key - this_belongs_tos - excluded_columns).each do |column|
|
287
|
+
template << column.to_sym
|
288
|
+
end
|
289
|
+
# Okay, at this point it really searches for the uniques, and in the "strict" (not loose) kind of way
|
290
|
+
requireds += _find_requireds(this_klass, false, [this_klass.primary_key]).first.map { |r| "#{path}#{r}".to_sym }
|
291
|
+
# Now add the foreign keys and any has_manys in the form of references to associated models
|
292
|
+
assocs.each do |k, assoc|
|
293
|
+
# # assoc.first describes this foreign key and class, and is used for a "reverse poison"
|
294
|
+
# # detection so we don't fold back on ourselves
|
295
|
+
next if poison_links.include?(assoc.first)
|
296
|
+
|
297
|
+
is_has_many = (assoc.first.last == assoc.last)
|
298
|
+
if hops.zero?
|
299
|
+
# For has_one or has_many, exclude with priority the foreign key column(s) we rode in here on
|
300
|
+
priority_excluded_columns = assoc.first.first if is_has_many
|
301
|
+
# puts "Excluded: #{priority_excluded_columns.inspect}"
|
302
|
+
unique, new_requireds = _suggest_unique_column(assoc.last, priority_excluded_columns, "#{path}#{k}_")
|
303
|
+
template << { k => unique }
|
304
|
+
requireds += new_requireds
|
305
|
+
else
|
306
|
+
new_poison_links =
|
307
|
+
if is_has_many
|
308
|
+
# binding.pry if assoc.first.last.nil?
|
309
|
+
# has_many is simple, just exclude how we got here from the foreign table
|
310
|
+
[assoc.first]
|
311
|
+
else
|
312
|
+
# belongs_to is more involved since there may be multiple foreign keys which point
|
313
|
+
# from the foreign table to this primary one, so exclude all these links.
|
314
|
+
_find_assocs(:belongs_to, assoc.first.last, assoc.last, errored_assocs).map do |f_assoc|
|
315
|
+
[[f_assoc.foreign_key.to_s], f_assoc.active_record]
|
316
|
+
end
|
317
|
+
end
|
318
|
+
# puts "New Poison: #{new_poison_links.map{|a| "#{a.first.inspect} - #{a.last.name}"}.join(' / ')}"
|
319
|
+
# if (poison_links & new_poison_links).empty?
|
320
|
+
# Store the ones to do the next round
|
321
|
+
# puts "Test against #{assoc.first.inspect}"
|
322
|
+
template << { k => (next_template = []) }
|
323
|
+
next_layer << [do_has_many, assoc.last, "#{path}#{k}_", next_template]
|
324
|
+
poison_links += (new_poison_links - poison_links)
|
325
|
+
# end
|
326
|
+
end
|
134
327
|
end
|
135
328
|
end
|
329
|
+
break if hops.zero? || next_layer.empty?
|
330
|
+
|
331
|
+
hops -= 1
|
332
|
+
this_layer = next_layer
|
136
333
|
end
|
334
|
+
(buggy_bts - bad_polymorphic_hms).each do |bad_bt|
|
335
|
+
puts "* #{bad_bt.first.name} model - problem with: \"belongs_to :#{bad_bt.last.name}\" because there is no \"#{bad_bt.last.class_name}\" model."
|
336
|
+
end
|
337
|
+
[whole_template, requireds]
|
338
|
+
end
|
137
339
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
340
|
+
# Load all models
|
341
|
+
# %%% Note that this works in Rails 5.x, but may not work in Rails 6.0 and later, which uses the Zeitwerk loader by default:
|
342
|
+
def self._all_models
|
343
|
+
unless ActiveRecord::Base.instance_variable_get(:@eager_loaded_all)
|
344
|
+
if ActiveRecord.version < ::Gem::Version.new('4.0')
|
345
|
+
Rails.configuration.eager_load_paths
|
346
|
+
else
|
347
|
+
Rails.configuration.eager_load_namespaces.select { |ns| ns < Rails::Application }.each(&:eager_load!)
|
348
|
+
end
|
349
|
+
ActiveRecord::Base.instance_variable_set(:@eager_loaded_all, true)
|
350
|
+
end
|
351
|
+
ActiveRecord::Base.descendants
|
352
|
+
end
|
149
353
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
priority_excluded_columns = assoc.first.first if is_has_many
|
156
|
-
# puts "Excluded: #{priority_excluded_columns.inspect}"
|
157
|
-
_suggest_unique_column(assoc.last, priority_excluded_columns, "#{path}#{k}_")
|
158
|
-
else
|
159
|
-
new_poison_links =
|
160
|
-
if is_has_many
|
161
|
-
# has_many is simple, just exclude how we got here from the foreign table
|
162
|
-
[assoc.first]
|
354
|
+
# Load all tables
|
355
|
+
def self._all_tables
|
356
|
+
unless (all_tables = ActiveRecord::Base.instance_variable_get(:@_all_tables))
|
357
|
+
sql = if ActiveRecord::Base.connection.class.name.end_with?('::SQLite3Adapter')
|
358
|
+
"SELECT DISTINCT name AS table_name FROM sqlite_master WHERE type = 'table'"
|
163
359
|
else
|
164
|
-
#
|
165
|
-
|
166
|
-
_find_belongs_tos(assoc.first.last, assoc.last, errored_assocs).map do |f_assoc|
|
167
|
-
[[f_assoc.foreign_key.to_s], f_assoc.active_record]
|
168
|
-
end
|
360
|
+
# For everything else, which would be "::PostgreSQLAdapter", "::MysqlAdapter", or "::Mysql2Adapter":
|
361
|
+
"SELECT DISTINCT table_name FROM INFORMATION_SCHEMA.TABLES WHERE table_type = 'BASE TABLE'"
|
169
362
|
end
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
363
|
+
# The MySQL version of execute_sql returns arrays instead of a hash when there's just one column asked for.
|
364
|
+
all_tables = ActiveRecord::Base.execute_sql(sql).each_with_object({}) do |row, s|
|
365
|
+
s[row.is_a?(Array) ? row.first : row['table_name']] = nil
|
366
|
+
s
|
367
|
+
end
|
368
|
+
ActiveRecord::Base.instance_variable_set(:@_all_tables, all_tables)
|
175
369
|
end
|
176
|
-
|
370
|
+
all_tables
|
177
371
|
end
|
178
372
|
|
179
373
|
# Find belongs_tos for this model to one more more other klasses
|
180
|
-
def self.
|
181
|
-
|
182
|
-
|
183
|
-
|
374
|
+
def self._find_assocs(macro, klass, to_klass, errored_assocs, using_fk = nil)
|
375
|
+
case macro
|
376
|
+
when :belongs_to
|
377
|
+
klass.reflect_on_all_associations.each_with_object([]) do |bt_assoc, s|
|
378
|
+
next unless bt_assoc.belongs_to? && !errored_assocs.include?(bt_assoc)
|
379
|
+
|
380
|
+
begin
|
381
|
+
s << bt_assoc if !bt_assoc.options[:polymorphic] && bt_assoc.klass <= to_klass &&
|
382
|
+
(using_fk.nil? || bt_assoc.foreign_key == using_fk)
|
383
|
+
rescue NameError
|
384
|
+
errored_assocs << bt_assoc
|
385
|
+
puts "* #{bt_assoc.active_record.name} model - \"belongs_to :#{bt_assoc.name}\" could not find a model named #{bt_assoc.class_name}."
|
386
|
+
end
|
387
|
+
s
|
388
|
+
end
|
389
|
+
when :has_many # Also :has_one
|
390
|
+
klass.reflect_on_all_associations.each_with_object([]) do |hm_assoc, s|
|
391
|
+
next if ![:has_many, :has_one].include?(hm_assoc.macro) || errored_assocs.include?(hm_assoc) ||
|
392
|
+
(Object.const_defined?('PaperTrail::Version') && hm_assoc.klass <= PaperTrail::Version && hm_assoc.options.include?(:as)) # Skip any PaperTrail associations
|
184
393
|
|
185
|
-
|
186
|
-
s
|
187
|
-
rescue NameError
|
188
|
-
errored_assocs << bt_assoc
|
189
|
-
puts "* In the #{bt_assoc.active_record.name} model \"belongs_to :#{bt_assoc.name}\" could not find a model named #{bt_assoc.class_name}."
|
394
|
+
s << hm_assoc if hm_assoc.klass == to_klass && (using_fk.nil? || hm_assoc.foreign_key == using_fk)
|
395
|
+
s
|
190
396
|
end
|
191
397
|
end
|
192
398
|
end
|
@@ -197,36 +403,20 @@ module DutyFree
|
|
197
403
|
# ...
|
198
404
|
# Not available, so grasping at straws, just search for any available column
|
199
405
|
# %%% add EXCLUDED_UNIQUE_COLUMNS || ...
|
200
|
-
|
201
|
-
|
202
|
-
# Requireds takes its cues from all attributes having a presence validator
|
203
|
-
requireds = _find_requireds(klass)
|
204
|
-
klass_columns = klass_columns.reject { |col| priority_excluded_columns.include?(col.name) } if priority_excluded_columns
|
205
|
-
excluded_columns = %w[created_at updated_at deleted_at]
|
206
|
-
unique = [(
|
207
|
-
# Find the first text field of a required if one exists
|
208
|
-
klass_columns.find { |col| requireds.include?(col.name) && col.type == :string }&.name ||
|
209
|
-
# Find the first text field, now of a non-required, if one exists
|
210
|
-
klass_columns.find { |col| col.type == :string }&.name ||
|
211
|
-
# If no string then look for the first non-PK that is also not a foreign key or created_at or updated_at
|
212
|
-
klass_columns.find do |col|
|
213
|
-
requireds.include?(col.name) && col.name != klass.primary_key && !excluded_columns.include?(col.name)
|
214
|
-
end&.name ||
|
215
|
-
# And now the same but not a required, the first non-PK that is also not a foreign key or created_at or updated_at
|
216
|
-
klass_columns.find do |col|
|
217
|
-
col.name != klass.primary_key && !excluded_columns.include?(col.name)
|
218
|
-
end&.name ||
|
219
|
-
# Finally just accept the PK if nothing else
|
220
|
-
klass.primary_key
|
221
|
-
).to_sym]
|
222
|
-
|
223
|
-
[unique, requireds.map { |r| "#{path}#{r}".to_sym }]
|
406
|
+
uniques, requireds = _find_requireds(klass, true, priority_excluded_columns)
|
407
|
+
[[uniques.first.to_sym], requireds.map { |r| "#{path}#{r}".to_sym }]
|
224
408
|
end
|
225
409
|
|
226
|
-
def self._find_requireds(klass)
|
410
|
+
def self._find_requireds(klass, is_loose = false, priority_excluded_columns = nil)
|
227
411
|
errored_columns = ::DutyFree.instance_variable_get(:@errored_columns)
|
228
|
-
|
229
|
-
|
412
|
+
# %%% In case we need to exclude foreign keys in the future, this will do it:
|
413
|
+
# bts = klass.reflect_on_all_associations.each_with_object([]) do |bt_assoc, s|
|
414
|
+
# next unless bt_assoc.belongs_to?
|
415
|
+
|
416
|
+
# s << bt_assoc.name
|
417
|
+
# end
|
418
|
+
requireds = klass.validators.select do |v|
|
419
|
+
v.is_a?(ActiveRecord::Validations::PresenceValidator) # && (v.attributes & bts).empty?
|
230
420
|
end.each_with_object([]) do |v, s|
|
231
421
|
v.attributes.each do |a|
|
232
422
|
attrib = a.to_s
|
@@ -236,17 +426,39 @@ module DutyFree
|
|
236
426
|
if klass.columns.map(&:name).include?(attrib)
|
237
427
|
s << attrib
|
238
428
|
else
|
239
|
-
|
240
|
-
|
241
|
-
names
|
242
|
-
end
|
243
|
-
unless hm_and_bt_names.include?(attrib)
|
244
|
-
puts "* In the #{klass.name} model \"validates_presence_of :#{attrib}\" should be removed as it does not refer to any existing column."
|
429
|
+
unless klass.instance_methods.map(&:to_s).include?(attrib)
|
430
|
+
puts "* #{klass.name} model - \"validates_presence_of :#{attrib}\" should be removed as it does not refer to any existing column or relation."
|
245
431
|
errored_columns << klass_col
|
246
432
|
end
|
247
433
|
end
|
248
434
|
end
|
249
435
|
end
|
436
|
+
klass_columns = klass.columns
|
437
|
+
|
438
|
+
# Take our cues from all attributes having a presence validator
|
439
|
+
klass_columns = klass_columns.reject { |col| priority_excluded_columns.include?(col.name) } if priority_excluded_columns
|
440
|
+
excluded_columns = %w[created_at updated_at deleted_at]
|
441
|
+
|
442
|
+
# First find any text fields that are required
|
443
|
+
uniques = klass_columns.select { |col| requireds.include?(col.name) && [:string, :text].include?(col.type) }
|
444
|
+
# If not that then find any text field, even those not required
|
445
|
+
uniques = klass_columns.select { |col| [:string, :text].include?(col.type) } if is_loose && uniques.empty?
|
446
|
+
# If still not then look for any required non-PK that is also not a foreign key or created_at or updated_at
|
447
|
+
if uniques.empty?
|
448
|
+
uniques = klass_columns.select do |col|
|
449
|
+
requireds.include?(col.name) && col.name != klass.primary_key && !excluded_columns.include?(col.name)
|
450
|
+
end
|
451
|
+
end
|
452
|
+
# If still nothing then the same but not a required, any non-PK that is also not a foreign key or created_at or updated_at
|
453
|
+
if is_loose && uniques.empty?
|
454
|
+
uniques = klass_columns.select do |col|
|
455
|
+
col.name != klass.primary_key && !excluded_columns.include?(col.name)
|
456
|
+
end
|
457
|
+
end
|
458
|
+
uniques.map!(&:name)
|
459
|
+
# Finally if nothing else then just accept the PK, if there is one
|
460
|
+
uniques = [klass.primary_key] if klass.primary_key && uniques.empty? && (!priority_excluded_columns || priority_excluded_columns.exclude?(klass.primary_key))
|
461
|
+
[uniques, requireds]
|
250
462
|
end
|
251
463
|
|
252
464
|
# Show a "pretty" version of IMPORT_TEMPLATE, to be placed in a model
|