strict_associations 0.1.0 → 0.1.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 +4 -0
- data/lib/strict_associations/validator.rb +103 -46
- data/lib/strict_associations/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 413a5326728f775abc9050baf64f35ab7f0f90d88ebb32627d360ade8156fb3e
|
|
4
|
+
data.tar.gz: e2eb32329274995e220142c30c9c7055d2f6a675849b3df7d7680cddd17f8537
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e9aa8d31880309f3338f40dff593f768b6fee510e47a160e2a0a28f76c1cf949182f2e340cc8ef9a2ca40b70b812380e74386a5623e0489425406a0f0295b85e
|
|
7
|
+
data.tar.gz: b558d3374c0b552323b73c47b5b34c2e37a873e8f0999d6fbd0687982e481854718322774b94f8444fad58ba724de899c16a784209ff07a936c3cac0e8595d29
|
data/CHANGELOG.md
CHANGED
|
@@ -13,8 +13,10 @@ module StrictAssociations
|
|
|
13
13
|
models_to_check.each do |model|
|
|
14
14
|
check_habtm(model, violations)
|
|
15
15
|
check_belongs_to_inverses(model, violations)
|
|
16
|
+
check_has_many_inverses(model, violations)
|
|
16
17
|
check_polymorphic_inverses(model, violations)
|
|
17
18
|
check_dependent_options(model, violations)
|
|
19
|
+
check_orphaned_foreign_keys(model, violations)
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
violations
|
|
@@ -84,10 +86,10 @@ module StrictAssociations
|
|
|
84
86
|
rule: :missing_inverse,
|
|
85
87
|
message: <<~MSG.squish
|
|
86
88
|
#{target} has no has_many/has_one pointing back to \
|
|
87
|
-
#{model.table_name} with foreign key #{fk}.
|
|
88
|
-
Define the inverse.
|
|
89
|
-
Or mark this association with strict: false.
|
|
90
|
-
Or call skip_strict_association :#{inverse_name}
|
|
89
|
+
#{model.table_name} with foreign key #{fk}.
|
|
90
|
+
Define the inverse.
|
|
91
|
+
Or mark this association with strict: false.
|
|
92
|
+
Or call skip_strict_association :#{inverse_name}
|
|
91
93
|
on #{target}
|
|
92
94
|
MSG
|
|
93
95
|
)
|
|
@@ -95,6 +97,34 @@ module StrictAssociations
|
|
|
95
97
|
end
|
|
96
98
|
end
|
|
97
99
|
|
|
100
|
+
def check_has_many_inverses(model, violations)
|
|
101
|
+
%i[has_many has_one].each do |macro|
|
|
102
|
+
model.reflect_on_all_associations(macro).each do |ref|
|
|
103
|
+
next if skipped?(model, ref)
|
|
104
|
+
next if ref.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
|
105
|
+
next if ref.options[:as] # skip for polymorphic inverse
|
|
106
|
+
|
|
107
|
+
target = resolve_target(ref)
|
|
108
|
+
next unless target
|
|
109
|
+
|
|
110
|
+
unless belongs_to_exists?(model, ref, target)
|
|
111
|
+
violations << Violation.new(
|
|
112
|
+
model:,
|
|
113
|
+
association_name: ref.name,
|
|
114
|
+
rule: :missing_belongs_to,
|
|
115
|
+
message: <<~MSG.squish
|
|
116
|
+
#{model} has #{macro} :#{ref.name} but #{target} has no belongs_to \
|
|
117
|
+
pointing back with foreign key #{ref.foreign_key}.
|
|
118
|
+
Define the belongs_to on #{target}.
|
|
119
|
+
Or mark with strict: false.
|
|
120
|
+
Or call skip_strict_association :#{ref.name} on #{model}.
|
|
121
|
+
MSG
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
98
128
|
def check_polymorphic_inverses(model, violations)
|
|
99
129
|
refs = model.reflect_on_all_associations(:belongs_to)
|
|
100
130
|
refs.each do |ref|
|
|
@@ -109,20 +139,16 @@ module StrictAssociations
|
|
|
109
139
|
association_name: ref.name,
|
|
110
140
|
rule: :unregistered_polymorphic,
|
|
111
141
|
message: <<~MSG.squish
|
|
112
|
-
#{model}##{ref.name} is polymorphic but has no
|
|
113
|
-
|
|
114
|
-
association. Or mark with strict: false.
|
|
142
|
+
#{model}##{ref.name} is polymorphic but has no valid_types declared.
|
|
143
|
+
Add valid_types: to the association. Or mark with strict: false.
|
|
115
144
|
MSG
|
|
116
145
|
)
|
|
117
146
|
next
|
|
118
147
|
end
|
|
119
148
|
|
|
120
|
-
type_violations, types =
|
|
121
|
-
resolved.partition { |r| r.is_a?(Violation) }
|
|
149
|
+
type_violations, types = resolved.partition { |r| r.is_a?(Violation) }
|
|
122
150
|
violations.concat(type_violations)
|
|
123
|
-
check_registered_polymorphic_types(
|
|
124
|
-
model, ref, types, violations
|
|
125
|
-
)
|
|
151
|
+
check_registered_polymorphic_types(model, ref, types, violations)
|
|
126
152
|
end
|
|
127
153
|
end
|
|
128
154
|
|
|
@@ -139,9 +165,8 @@ module StrictAssociations
|
|
|
139
165
|
association_name: ref.name,
|
|
140
166
|
rule: :invalid_valid_type,
|
|
141
167
|
message: <<~MSG.squish
|
|
142
|
-
#{model}##{ref.name} declares valid_types \
|
|
143
|
-
|
|
144
|
-
Check for typos in valid_types.
|
|
168
|
+
#{model}##{ref.name} declares valid_types containing "#{t}" but \
|
|
169
|
+
#{e.message}. Check for typos in valid_types.
|
|
145
170
|
MSG
|
|
146
171
|
)
|
|
147
172
|
end
|
|
@@ -149,26 +174,20 @@ module StrictAssociations
|
|
|
149
174
|
resolved
|
|
150
175
|
end
|
|
151
176
|
|
|
152
|
-
def check_registered_polymorphic_types(
|
|
153
|
-
model, ref, types, violations
|
|
154
|
-
)
|
|
177
|
+
def check_registered_polymorphic_types(model, ref, types, violations)
|
|
155
178
|
fk = ref.foreign_key.to_s
|
|
156
179
|
source_table = model.table_name
|
|
157
180
|
|
|
158
181
|
types.each do |type_class|
|
|
159
|
-
next if polymorphic_inverse_exists?(
|
|
160
|
-
type_class, source_table, fk
|
|
161
|
-
)
|
|
182
|
+
next if polymorphic_inverse_exists?(type_class, source_table, fk)
|
|
162
183
|
|
|
163
184
|
violations << Violation.new(
|
|
164
185
|
model:,
|
|
165
186
|
association_name: ref.name,
|
|
166
187
|
rule: :missing_polymorphic_inverse,
|
|
167
188
|
message: <<~MSG.squish
|
|
168
|
-
#{type_class} is registered for \
|
|
169
|
-
#{
|
|
170
|
-
pointing back to #{source_table} with foreign \
|
|
171
|
-
key #{fk}.
|
|
189
|
+
#{type_class} is registered for #{model}##{ref.name} but has no \
|
|
190
|
+
has_many/has_one pointing back to #{source_table} with foreign key #{fk}.
|
|
172
191
|
MSG
|
|
173
192
|
)
|
|
174
193
|
end
|
|
@@ -178,9 +197,7 @@ module StrictAssociations
|
|
|
178
197
|
%i[has_many has_one].each do |macro|
|
|
179
198
|
model.reflect_on_all_associations(macro).each do |ref|
|
|
180
199
|
next if skipped?(model, ref)
|
|
181
|
-
next if ref.is_a?(
|
|
182
|
-
ActiveRecord::Reflection::ThroughReflection
|
|
183
|
-
)
|
|
200
|
+
next if ref.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
|
184
201
|
next if target_is_view?(ref)
|
|
185
202
|
|
|
186
203
|
unless ref.options.key?(:dependent)
|
|
@@ -189,12 +206,11 @@ module StrictAssociations
|
|
|
189
206
|
association_name: ref.name,
|
|
190
207
|
rule: :missing_dependent,
|
|
191
208
|
message: <<~MSG.squish
|
|
192
|
-
#{model}##{ref.name} is missing a :dependent
|
|
193
|
-
|
|
194
|
-
:
|
|
195
|
-
Or mark with strict: false.
|
|
196
|
-
Or call skip_strict_association :#{ref.name}
|
|
197
|
-
on #{model}.
|
|
209
|
+
#{model}##{ref.name} is missing a :dependent option.
|
|
210
|
+
Add dependent: :destroy, :delete_all, :nullify, or \
|
|
211
|
+
:restrict_with_exception.
|
|
212
|
+
Or mark with strict: false.
|
|
213
|
+
Or call skip_strict_association :#{ref.name} on #{model}.
|
|
198
214
|
MSG
|
|
199
215
|
)
|
|
200
216
|
end
|
|
@@ -202,25 +218,70 @@ module StrictAssociations
|
|
|
202
218
|
end
|
|
203
219
|
end
|
|
204
220
|
|
|
221
|
+
def check_orphaned_foreign_keys(model, violations)
|
|
222
|
+
indexed_fk_columns = indexed_foreign_key_columns(model)
|
|
223
|
+
defined_fk_columns = model
|
|
224
|
+
.reflect_on_all_associations(:belongs_to)
|
|
225
|
+
.map { |ref| ref.foreign_key.to_s }
|
|
226
|
+
|
|
227
|
+
(indexed_fk_columns - defined_fk_columns).each do |column|
|
|
228
|
+
assoc_name = column.delete_suffix("_id").to_sym
|
|
229
|
+
next if model.strict_association_skipped?(assoc_name)
|
|
230
|
+
|
|
231
|
+
violations << Violation.new(
|
|
232
|
+
model:,
|
|
233
|
+
association_name: assoc_name,
|
|
234
|
+
rule: :orphaned_foreign_key,
|
|
235
|
+
message: <<~MSG.squish
|
|
236
|
+
#{model.table_name} has an indexed column #{column} but #{model} has no \
|
|
237
|
+
belongs_to association for it.
|
|
238
|
+
Define a belongs_to.
|
|
239
|
+
Or remove the index.
|
|
240
|
+
Or call skip_strict_association :#{assoc_name} on #{model}.
|
|
241
|
+
MSG
|
|
242
|
+
)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def indexed_foreign_key_columns(model)
|
|
247
|
+
model.connection.indexes(model.table_name).filter_map do |index|
|
|
248
|
+
column = index.columns.first
|
|
249
|
+
next unless index.columns.one? && column.end_with?("_id")
|
|
250
|
+
|
|
251
|
+
column
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
205
255
|
def resolve_target(reflection)
|
|
206
256
|
reflection.klass
|
|
207
257
|
rescue NameError
|
|
208
258
|
nil
|
|
209
259
|
end
|
|
210
260
|
|
|
261
|
+
def belongs_to_exists?(source_model, has_many_ref, target)
|
|
262
|
+
fk = has_many_ref.foreign_key.to_s
|
|
263
|
+
|
|
264
|
+
target.reflect_on_all_associations(:belongs_to).any? do |ref|
|
|
265
|
+
next if ref.options[:polymorphic]
|
|
266
|
+
|
|
267
|
+
begin
|
|
268
|
+
ref.klass == source_model && ref.foreign_key.to_s == fk
|
|
269
|
+
rescue NameError
|
|
270
|
+
false
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
211
275
|
def inverse_exists?(source_model, belongs_to_ref, target)
|
|
212
276
|
fk = belongs_to_ref.foreign_key.to_s
|
|
213
277
|
source_table = source_model.table_name
|
|
214
278
|
|
|
215
279
|
target.reflect_on_all_associations.any? do |ref|
|
|
216
280
|
next unless %i[has_many has_one].include?(ref.macro)
|
|
217
|
-
next if ref.is_a?(
|
|
218
|
-
ActiveRecord::Reflection::ThroughReflection
|
|
219
|
-
)
|
|
281
|
+
next if ref.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
|
220
282
|
|
|
221
283
|
begin
|
|
222
|
-
ref.klass.table_name == source_table &&
|
|
223
|
-
ref.foreign_key.to_s == fk
|
|
284
|
+
ref.klass.table_name == source_table && ref.foreign_key.to_s == fk
|
|
224
285
|
rescue NameError
|
|
225
286
|
false
|
|
226
287
|
end
|
|
@@ -230,13 +291,10 @@ module StrictAssociations
|
|
|
230
291
|
def polymorphic_inverse_exists?(type, table, fk)
|
|
231
292
|
type.reflect_on_all_associations.any? do |ref|
|
|
232
293
|
next unless %i[has_many has_one].include?(ref.macro)
|
|
233
|
-
next if ref.is_a?(
|
|
234
|
-
ActiveRecord::Reflection::ThroughReflection
|
|
235
|
-
)
|
|
294
|
+
next if ref.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
|
236
295
|
|
|
237
296
|
begin
|
|
238
|
-
ref.klass.table_name == table &&
|
|
239
|
-
ref.foreign_key.to_s == fk
|
|
297
|
+
ref.klass.table_name == table && ref.foreign_key.to_s == fk
|
|
240
298
|
rescue NameError
|
|
241
299
|
false
|
|
242
300
|
end
|
|
@@ -249,8 +307,7 @@ module StrictAssociations
|
|
|
249
307
|
end
|
|
250
308
|
|
|
251
309
|
def skipped?(model, ref)
|
|
252
|
-
ref.options[:strict] == false ||
|
|
253
|
-
model.strict_association_skipped?(ref.name)
|
|
310
|
+
ref.options[:strict] == false || model.strict_association_skipped?(ref.name)
|
|
254
311
|
end
|
|
255
312
|
end
|
|
256
313
|
end
|