duty_free 1.0.7 → 1.0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/duty_free/column.rb +2 -2
- data/lib/duty_free/extensions.rb +603 -446
- data/lib/duty_free/suggest_template.rb +381 -170
- data/lib/duty_free/util.rb +129 -11
- data/lib/duty_free/version_number.rb +1 -1
- data/lib/duty_free.rb +227 -52
- data/lib/generators/duty_free/model_generator.rb +349 -0
- data/lib/generators/duty_free/templates/create_versions.rb.erb +2 -2
- metadata +10 -29
@@ -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,163 +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
|
-
|
54
|
-
'has_one'
|
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
|
55
52
|
end
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
65
66
|
end
|
66
|
-
|
67
|
+
end
|
67
68
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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)
|
80
108
|
|
81
|
-
#
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
+
)
|
85
126
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
end
|
90
|
-
else
|
91
|
-
# Is it a polymorphic has_many, which is defined using as: :somethingable ?
|
92
|
-
is_polymorphic_hm = assoc.inverse_of&.options&.fetch(:polymorphic) { nil }
|
93
|
-
begin
|
94
|
-
# Standard has_one, or has_many, and belongs_to uses assoc.klass.
|
95
|
-
# Also polymorphic belongs_to uses assoc.klass.
|
96
|
-
assoc_klass = is_polymorphic_hm ? assoc.inverse_of.active_record : assoc.klass
|
97
|
-
rescue NameError # For models which cannot be found by name
|
98
|
-
end
|
99
|
-
new_assoc =
|
100
|
-
if assoc_klass.nil?
|
101
|
-
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."
|
102
|
-
nil # Cause this one to be excluded
|
103
|
-
elsif is_belongs_to
|
104
|
-
this_belongs_tos << (fk = assoc.foreign_key.to_s)
|
105
|
-
[[[fk], assoc.active_record], assoc_klass]
|
106
|
-
else # has_many or has_one
|
107
|
-
inverse_foreign_keys = is_polymorphic_hm ? [assoc.type, assoc.foreign_key] : [assoc.inverse_of&.foreign_key&.to_s]
|
108
|
-
missing_key_columns = inverse_foreign_keys - assoc_klass.columns.map(&:name)
|
109
|
-
if missing_key_columns.empty?
|
110
|
-
puts "* Missing inverse foreign key for #{this_klass.name} #{belongs_to_or_has_many} :#{assoc.name}" if inverse_foreign_keys.first.nil?
|
111
|
-
# puts "Has columns #{inverse_foreign_keys.inspect}"
|
112
|
-
[[inverse_foreign_keys, assoc_klass], assoc_klass]
|
113
|
-
else
|
114
|
-
if inverse_foreign_keys.length > 1
|
115
|
-
puts "* The #{assoc_klass.name} model is missing #{missing_key_columns.join(' and ')} columns to allow it to support polymorphic inheritance."
|
116
|
-
else
|
117
|
-
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
|
118
130
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
124
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]
|
125
180
|
else
|
126
|
-
puts "
|
181
|
+
puts "* #{this_klass.name} model - problem with: \"#{belongs_to_or_has_many} :#{assoc.name}\" because there is no \"#{assoc.class_name}\" model."
|
127
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
|
128
274
|
end
|
129
|
-
|
275
|
+
if new_assoc.nil?
|
276
|
+
errored_assocs << assoc
|
277
|
+
else
|
278
|
+
assocs[assoc.name] = new_assoc
|
130
279
|
end
|
131
280
|
end
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
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
|
136
327
|
end
|
137
328
|
end
|
329
|
+
break if hops.zero? || next_layer.empty?
|
330
|
+
|
331
|
+
hops -= 1
|
332
|
+
this_layer = next_layer
|
138
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
|
139
339
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
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
|
151
353
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
priority_excluded_columns = assoc.first.first if is_has_many
|
158
|
-
# puts "Excluded: #{priority_excluded_columns.inspect}"
|
159
|
-
_suggest_unique_column(assoc.last, priority_excluded_columns, "#{path}#{k}_")
|
160
|
-
else
|
161
|
-
new_poison_links =
|
162
|
-
if is_has_many
|
163
|
-
# has_many is simple, just exclude how we got here from the foreign table
|
164
|
-
[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'"
|
165
359
|
else
|
166
|
-
#
|
167
|
-
|
168
|
-
_find_belongs_tos(assoc.first.last, assoc.last, errored_assocs).map do |f_assoc|
|
169
|
-
[[f_assoc.foreign_key.to_s], f_assoc.active_record]
|
170
|
-
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'"
|
171
362
|
end
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
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)
|
177
369
|
end
|
178
|
-
|
370
|
+
all_tables
|
179
371
|
end
|
180
372
|
|
181
373
|
# Find belongs_tos for this model to one more more other klasses
|
182
|
-
def self.
|
183
|
-
|
184
|
-
|
185
|
-
|
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
|
186
393
|
|
187
|
-
|
188
|
-
s
|
189
|
-
rescue NameError
|
190
|
-
errored_assocs << bt_assoc
|
191
|
-
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
|
192
396
|
end
|
193
397
|
end
|
194
398
|
end
|
@@ -199,36 +403,20 @@ module DutyFree
|
|
199
403
|
# ...
|
200
404
|
# Not available, so grasping at straws, just search for any available column
|
201
405
|
# %%% add EXCLUDED_UNIQUE_COLUMNS || ...
|
202
|
-
|
203
|
-
|
204
|
-
# Requireds takes its cues from all attributes having a presence validator
|
205
|
-
requireds = _find_requireds(klass)
|
206
|
-
klass_columns = klass_columns.reject { |col| priority_excluded_columns.include?(col.name) } if priority_excluded_columns
|
207
|
-
excluded_columns = %w[created_at updated_at deleted_at]
|
208
|
-
unique = [(
|
209
|
-
# Find the first text field of a required if one exists
|
210
|
-
klass_columns.find { |col| requireds.include?(col.name) && col.type == :string }&.name ||
|
211
|
-
# Find the first text field, now of a non-required, if one exists
|
212
|
-
klass_columns.find { |col| col.type == :string }&.name ||
|
213
|
-
# If no string then look for the first non-PK that is also not a foreign key or created_at or updated_at
|
214
|
-
klass_columns.find do |col|
|
215
|
-
requireds.include?(col.name) && col.name != klass.primary_key && !excluded_columns.include?(col.name)
|
216
|
-
end&.name ||
|
217
|
-
# 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
|
218
|
-
klass_columns.find do |col|
|
219
|
-
col.name != klass.primary_key && !excluded_columns.include?(col.name)
|
220
|
-
end&.name ||
|
221
|
-
# Finally just accept the PK if nothing else
|
222
|
-
klass.primary_key
|
223
|
-
).to_sym]
|
224
|
-
|
225
|
-
[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 }]
|
226
408
|
end
|
227
409
|
|
228
|
-
def self._find_requireds(klass)
|
410
|
+
def self._find_requireds(klass, is_loose = false, priority_excluded_columns = nil)
|
229
411
|
errored_columns = ::DutyFree.instance_variable_get(:@errored_columns)
|
230
|
-
|
231
|
-
|
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?
|
232
420
|
end.each_with_object([]) do |v, s|
|
233
421
|
v.attributes.each do |a|
|
234
422
|
attrib = a.to_s
|
@@ -238,17 +426,39 @@ module DutyFree
|
|
238
426
|
if klass.columns.map(&:name).include?(attrib)
|
239
427
|
s << attrib
|
240
428
|
else
|
241
|
-
|
242
|
-
|
243
|
-
names
|
244
|
-
end
|
245
|
-
unless ho_and_bt_names.include?(attrib)
|
246
|
-
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."
|
247
431
|
errored_columns << klass_col
|
248
432
|
end
|
249
433
|
end
|
250
434
|
end
|
251
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]
|
252
462
|
end
|
253
463
|
|
254
464
|
# Show a "pretty" version of IMPORT_TEMPLATE, to be placed in a model
|
@@ -287,10 +497,11 @@ module DutyFree
|
|
287
497
|
v.each_with_index do |item, idx|
|
288
498
|
# This is where most of the commas get printed, so you can do "#{child_count}," to diagnose things
|
289
499
|
print ',' if idx.positive? && indent >= 0
|
290
|
-
|
500
|
+
case item
|
501
|
+
when Hash
|
291
502
|
# puts '^' unless child_count < 5 || indent.negative?
|
292
503
|
child_count = _template_pretty_print(item, indent + 2, child_count)
|
293
|
-
|
504
|
+
when Symbol
|
294
505
|
if indent.negative?
|
295
506
|
child_count += 1
|
296
507
|
else
|