brick 1.0.24 → 1.0.25
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/brick/config.rb +9 -0
- data/lib/brick/extensions.rb +171 -125
- data/lib/brick/frameworks/rails/engine.rb +78 -40
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +25 -4
- data/lib/generators/brick/install_generator.rb +4 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b0dfa64d7a8a148b4c2ad990f5af1604d5855b80c6e95f39cd371a43a591b7f1
|
4
|
+
data.tar.gz: bc7644a08678136d91696a68e0ad670f4f04d41772f36ba1bd806ebe45f23b04
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a39a8adc0c72288db5bd08e2483cefb701db9c2122fe1b4012c19bef07c7d048fed7f6e051473a9b4b6c80e321e4a781a16543d40f3e569f23c31f420a5117b6
|
7
|
+
data.tar.gz: 3ddcc914e3143a02a4582d6f525417c3012ae4eafc9ae889d5935223f6173afb4fb5ff3a40dc6c9caf95dfbf7a3c5be8e7bc4890efb385867ef5fa6f3ccbfb9e
|
data/lib/brick/config.rb
CHANGED
@@ -97,6 +97,15 @@ module Brick
|
|
97
97
|
@mutex.synchronize { @has_ones = hos }
|
98
98
|
end
|
99
99
|
|
100
|
+
# Polymorphic associations
|
101
|
+
def polymorphics
|
102
|
+
@mutex.synchronize { @polymorphics }
|
103
|
+
end
|
104
|
+
|
105
|
+
def polymorphics=(polys)
|
106
|
+
@mutex.synchronize { @polymorphics = polys }
|
107
|
+
end
|
108
|
+
|
100
109
|
def model_descrips
|
101
110
|
@mutex.synchronize { @model_descrips ||= {} }
|
102
111
|
end
|
data/lib/brick/extensions.rb
CHANGED
@@ -52,6 +52,14 @@ module Arel
|
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
|
+
# module ActiveModel
|
56
|
+
# class NotNullValidator < EachValidator
|
57
|
+
# def validate_each(record, attribute, value)
|
58
|
+
# record.errors[attribute] << "must not be null" if value.nil?
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
|
55
63
|
module ActiveRecord
|
56
64
|
class Base
|
57
65
|
def self._assoc_names
|
@@ -74,8 +82,8 @@ module ActiveRecord
|
|
74
82
|
dsl
|
75
83
|
end
|
76
84
|
|
77
|
-
# Pass in true or a JoinArray
|
78
|
-
def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {})
|
85
|
+
# Pass in true for build_array, or just pass in a JoinArray
|
86
|
+
def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {}, is_polymorphic = false)
|
79
87
|
build_array = ::Brick::JoinArray.new.tap { |ary| ary.replace([build_array]) } if build_array.is_a?(::Brick::JoinHash)
|
80
88
|
build_array = ::Brick::JoinArray.new unless build_array.nil? || build_array.is_a?(Array)
|
81
89
|
members = []
|
@@ -87,12 +95,17 @@ module ActiveRecord
|
|
87
95
|
if bracket_name
|
88
96
|
if ch == ']' # Time to process a bracketed thing?
|
89
97
|
parts = bracket_name.split('.')
|
90
|
-
first_parts = parts[0..-2].map
|
98
|
+
first_parts = parts[0..-2].map do |part|
|
99
|
+
klass = klass.reflect_on_association(part_sym = part.to_sym).klass
|
100
|
+
part_sym
|
101
|
+
end
|
91
102
|
parts = prefix + first_parts + [parts[-1]]
|
92
103
|
if parts.length > 1
|
93
|
-
|
94
|
-
|
95
|
-
|
104
|
+
unless is_polymorphic
|
105
|
+
s = build_array
|
106
|
+
parts[0..-3].each { |v| s = s[v.to_sym] }
|
107
|
+
s[parts[-2]] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
|
108
|
+
end
|
96
109
|
translations[parts[0..-2].join('.')] = klass
|
97
110
|
end
|
98
111
|
members << parts
|
@@ -191,7 +204,10 @@ module ActiveRecord
|
|
191
204
|
private
|
192
205
|
|
193
206
|
def self._brick_get_fks
|
194
|
-
@_brick_get_fks ||= reflect_on_all_associations.select { |a2| a2.macro == :belongs_to }.
|
207
|
+
@_brick_get_fks ||= reflect_on_all_associations.select { |a2| a2.macro == :belongs_to }.each_with_object([]) do |bt, s|
|
208
|
+
s << bt.foreign_key
|
209
|
+
s << bt.foreign_type if bt.polymorphic?
|
210
|
+
end
|
195
211
|
end
|
196
212
|
end
|
197
213
|
|
@@ -279,23 +295,6 @@ module ActiveRecord
|
|
279
295
|
# , is_add_bts, is_add_hms
|
280
296
|
)
|
281
297
|
is_add_bts = is_add_hms = true
|
282
|
-
wheres = {}
|
283
|
-
has_hm = false
|
284
|
-
params.each do |k, v|
|
285
|
-
case (ks = k.split('.')).length
|
286
|
-
when 1
|
287
|
-
next unless klass._brick_get_fks.include?(k)
|
288
|
-
when 2
|
289
|
-
assoc_name = ks.first.to_sym
|
290
|
-
# Make sure it's a good association name and that the model has that column name
|
291
|
-
next unless (assoc = klass.reflect_on_association(assoc_name))&.klass&.columns&.map(&:name)&.include?(ks.last)
|
292
|
-
|
293
|
-
# There is some potential for duplicates when there is an HM-based where in play. De-duplicate if so.
|
294
|
-
has_hm ||= assoc.macro == :has_many
|
295
|
-
join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
|
296
|
-
end
|
297
|
-
wheres[k] = v.split(',')
|
298
|
-
end
|
299
298
|
|
300
299
|
# %%% Skip the metadata columns
|
301
300
|
if selects&.empty? # Default to all columns
|
@@ -310,7 +309,11 @@ module ActiveRecord
|
|
310
309
|
bts, hms, associatives = ::Brick.get_bts_and_hms(klass)
|
311
310
|
bts.each do |_k, bt|
|
312
311
|
# join_array will receive this relation name when calling #brick_parse_dsl
|
313
|
-
bt_descrip[bt.first] =
|
312
|
+
bt_descrip[bt.first] = if bt[1].is_a?(Array)
|
313
|
+
bt[1].each_with_object({}) { |bt_class, s| s[bt_class] = bt_class.brick_parse_dsl(join_array, bt.first, translations, true) }
|
314
|
+
else
|
315
|
+
{ bt.last => bt[1].brick_parse_dsl(join_array, bt.first, translations) }
|
316
|
+
end
|
314
317
|
end
|
315
318
|
skip_klass_hms = ::Brick.config.skip_index_hms[klass.name] || {}
|
316
319
|
hms.each do |k, hm|
|
@@ -328,7 +331,7 @@ module ActiveRecord
|
|
328
331
|
when 2
|
329
332
|
assoc_name = ks.first.to_sym
|
330
333
|
# Make sure it's a good association name and that the model has that column name
|
331
|
-
next unless klass.reflect_on_association(assoc_name)&.klass&.
|
334
|
+
next unless klass.reflect_on_association(assoc_name)&.klass&.column_names&.any?(ks.last)
|
332
335
|
|
333
336
|
join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
|
334
337
|
end
|
@@ -344,23 +347,31 @@ module ActiveRecord
|
|
344
347
|
id_for_tables = Hash.new { |h, k| h[k] = [] }
|
345
348
|
field_tbl_names = Hash.new { |h, k| h[k] = {} }
|
346
349
|
bt_columns = bt_descrip.each_with_object([]) do |v, s|
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
id_for_tables[v.first] << id_alias
|
353
|
-
end
|
354
|
-
v.last << id_for_tables[v.first]
|
355
|
-
end
|
356
|
-
if (col_name = v.last[1].last&.last)
|
350
|
+
v.last.each do |k1, v1| # k1 is class, v1 is array of columns to snag
|
351
|
+
next if chains[k1].nil?
|
352
|
+
|
353
|
+
tbl_name = field_tbl_names[v.first][k1] ||= shift_or_first(chains[k1])
|
354
|
+
# if (col_name = v1[1].last&.last) # col_name is weak when there are multiple, using sel_col.last instead
|
357
355
|
field_tbl_name = nil
|
358
|
-
|
356
|
+
v1.map { |x|
|
357
|
+
[translations[x[0..-2].map(&:to_s).join('.')], x.last]
|
358
|
+
}.each_with_index do |sel_col, idx|
|
359
359
|
field_tbl_name ||= field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])
|
360
|
-
|
360
|
+
|
361
361
|
selects << "#{"#{field_tbl_name}.#{sel_col.last}"} AS \"#{(col_alias = "_brfk_#{v.first}__#{sel_col.last}")}\""
|
362
|
-
|
362
|
+
v1[idx] << col_alias
|
363
|
+
end
|
364
|
+
# end
|
365
|
+
|
366
|
+
if (id_col = k1.primary_key) && !id_for_tables.key?(v.first) # was tbl_name
|
367
|
+
# Accommodate composite primary key by allowing id_col to come in as an array
|
368
|
+
(id_col.is_a?(Array) ? id_col : [id_col]).each do |id_part|
|
369
|
+
selects << "#{"#{tbl_name}.#{id_part}"} AS \"#{(id_alias = "_brfk_#{v.first}__#{id_part}")}\""
|
370
|
+
id_for_tables[v.first] << id_alias
|
371
|
+
end
|
372
|
+
v1 << id_for_tables[v.first]
|
363
373
|
end
|
374
|
+
|
364
375
|
end
|
365
376
|
end
|
366
377
|
join_array.each do |assoc_name|
|
@@ -379,21 +390,26 @@ module ActiveRecord
|
|
379
390
|
hm.foreign_key
|
380
391
|
else
|
381
392
|
fk_col = hm.foreign_key
|
393
|
+
poly_type = hm.inverse_of.foreign_type if hm.options.key?(:as)
|
382
394
|
hm.klass.primary_key || '*'
|
383
395
|
end
|
384
396
|
tbl_alias = "_br_#{hm.name}"
|
385
397
|
pri_tbl = hm.active_record
|
398
|
+
on_clause = []
|
386
399
|
if fk_col.is_a?(Array) # Composite key?
|
387
|
-
on_clause = []
|
388
400
|
fk_col.each_with_index { |fk_col_part, idx| on_clause << "#{tbl_alias}.#{fk_col_part} = #{pri_tbl.table_name}.#{pri_tbl.primary_key[idx]}" }
|
389
|
-
|
390
|
-
JOIN (SELECT #{fk_col.join(', ')}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.table_name || hm.klass.table_name} GROUP BY #{(1..fk_col.length).to_a.join(', ')}) AS #{tbl_alias}
|
391
|
-
ON #{on_clause.join(' AND ')}")
|
401
|
+
selects = fk_col.dup
|
392
402
|
else
|
393
|
-
|
394
|
-
|
395
|
-
ON #{tbl_alias}.#{fk_col} = #{pri_tbl.table_name}.#{pri_tbl.primary_key}")
|
403
|
+
selects = [fk_col]
|
404
|
+
on_clause << "#{tbl_alias}.#{fk_col} = #{pri_tbl.table_name}.#{pri_tbl.primary_key}"
|
396
405
|
end
|
406
|
+
if poly_type
|
407
|
+
selects << poly_type
|
408
|
+
on_clause << "#{tbl_alias}.#{poly_type} = '#{name}'"
|
409
|
+
end
|
410
|
+
join_clause = "LEFT OUTER
|
411
|
+
JOIN (SELECT #{selects.join(', ')}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.table_name || hm.klass.table_name} GROUP BY #{(1..selects.length).to_a.join(', ')}) AS #{tbl_alias}"
|
412
|
+
joins!("#{join_clause} ON #{on_clause.join(' AND ')}")
|
397
413
|
end
|
398
414
|
where!(wheres) unless wheres.empty?
|
399
415
|
wheres unless wheres.empty? # Return the specific parameters that we did use
|
@@ -490,7 +506,7 @@ class Object
|
|
490
506
|
# path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
|
491
507
|
# return self._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
|
492
508
|
# If the file really exists, go and snag it:
|
493
|
-
if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = self.name&.split('::'))
|
509
|
+
if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = (self.name || class_name)&.split('::'))
|
494
510
|
filepath = (filepath[0..-2] + [class_name]).join('/').underscore + '.rb'
|
495
511
|
end
|
496
512
|
if is_found
|
@@ -603,77 +619,11 @@ class Object
|
|
603
619
|
hmts = fks.each_with_object(Hash.new { |h, k| h[k] = [] }) do |fk, hmts|
|
604
620
|
# The key in each hash entry (fk.first) is the constraint name
|
605
621
|
inverse_assoc_name = (assoc = fk.last)[:inverse]&.fetch(:assoc_name, nil)
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
assoc_name = if (primary_class = assoc.fetch(:primary_class, nil)) &&
|
611
|
-
sti_inverse_assoc = primary_class.reflect_on_all_associations.find do |a|
|
612
|
-
a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key
|
613
|
-
end
|
614
|
-
sti_inverse_assoc.options[:inverse_of]&.to_s || assoc_name
|
615
|
-
else
|
616
|
-
assoc[:assoc_name]
|
617
|
-
end
|
618
|
-
need_class_name = singular_table_name.underscore != assoc_name
|
619
|
-
need_fk = "#{assoc_name}_id" != assoc[:fk]
|
620
|
-
if (inverse = assoc[:inverse])
|
621
|
-
inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[assoc[:inverse_table]], inverse)
|
622
|
-
if (has_ones = ::Brick.config.has_ones&.fetch(inverse[:alternate_name].camelize, nil))&.key?(singular_inv_assoc_name = ActiveSupport::Inflector.singularize(inverse_assoc_name))
|
623
|
-
inverse_assoc_name = if has_ones[singular_inv_assoc_name]
|
624
|
-
need_inverse_of = true
|
625
|
-
has_ones[singular_inv_assoc_name]
|
626
|
-
else
|
627
|
-
singular_inv_assoc_name
|
628
|
-
end
|
629
|
-
end
|
630
|
-
end
|
631
|
-
:belongs_to
|
632
|
-
else
|
633
|
-
# need_class_name = ActiveSupport::Inflector.singularize(assoc_name) == ActiveSupport::Inflector.singularize(table_name.underscore)
|
634
|
-
# Are there multiple foreign keys out to the same table?
|
635
|
-
assoc_name, need_class_name = _brick_get_hm_assoc_name(relation, assoc)
|
636
|
-
need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
|
637
|
-
# fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
|
638
|
-
if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
|
639
|
-
assoc_name = if (custom_assoc_name = has_ones[singular_assoc_name])
|
640
|
-
need_class_name = custom_assoc_name != singular_assoc_name
|
641
|
-
custom_assoc_name
|
642
|
-
else
|
643
|
-
singular_assoc_name
|
644
|
-
end
|
645
|
-
:has_one
|
646
|
-
else
|
647
|
-
:has_many
|
648
|
-
end
|
649
|
-
end
|
650
|
-
# Figure out if we need to specially call out the class_name and/or foreign key
|
651
|
-
# (and if either of those then definitely also a specific inverse_of)
|
652
|
-
options[:class_name] = assoc[:primary_class]&.name || singular_table_name.camelize if need_class_name
|
653
|
-
# Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
|
654
|
-
if need_fk # Funky foreign key?
|
655
|
-
options[:foreign_key] = if assoc[:fk].is_a?(Array)
|
656
|
-
assoc_fk = assoc[:fk].uniq
|
657
|
-
assoc_fk.length < 2 ? assoc_fk.first : assoc_fk
|
658
|
-
else
|
659
|
-
assoc[:fk].to_sym
|
660
|
-
end
|
661
|
-
end
|
662
|
-
options[:inverse_of] = inverse_assoc_name.to_sym if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of)
|
663
|
-
|
664
|
-
# Prepare a list of entries for "has_many :through"
|
665
|
-
if macro == :has_many
|
666
|
-
relations[assoc[:inverse_table]][:hmt_fks].each do |k, hmt_fk|
|
667
|
-
next if k == assoc[:fk]
|
668
|
-
|
669
|
-
hmts[ActiveSupport::Inflector.pluralize(hmt_fk.last)] << [assoc, hmt_fk.first]
|
670
|
-
end
|
622
|
+
if (invs = assoc[:inverse_table]).is_a?(Array)
|
623
|
+
invs.each { |inv| build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, inv, code) }
|
624
|
+
else
|
625
|
+
build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, invs, code)
|
671
626
|
end
|
672
|
-
|
673
|
-
# And finally create a has_one, has_many, or belongs_to for this association
|
674
|
-
assoc_name = assoc_name.to_sym
|
675
|
-
code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
|
676
|
-
self.send(macro, assoc_name, **options)
|
677
627
|
hmts
|
678
628
|
end
|
679
629
|
hmts.each do |hmt_fk, fks|
|
@@ -712,6 +662,89 @@ class Object
|
|
712
662
|
[built_model, code]
|
713
663
|
end
|
714
664
|
|
665
|
+
def build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, inverse_table, code)
|
666
|
+
singular_table_name = inverse_table&.singularize
|
667
|
+
options = {}
|
668
|
+
macro = if assoc[:is_bt]
|
669
|
+
# Try to take care of screwy names if this is a belongs_to going to an STI subclass
|
670
|
+
assoc_name = if (primary_class = assoc.fetch(:primary_class, nil)) &&
|
671
|
+
sti_inverse_assoc = primary_class.reflect_on_all_associations.find do |a|
|
672
|
+
a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key
|
673
|
+
end
|
674
|
+
sti_inverse_assoc.options[:inverse_of]&.to_s || assoc_name
|
675
|
+
else
|
676
|
+
assoc[:assoc_name]
|
677
|
+
end
|
678
|
+
if assoc.key?(:polymorphic)
|
679
|
+
options[:polymorphic] = true
|
680
|
+
else
|
681
|
+
need_class_name = singular_table_name.underscore != assoc_name
|
682
|
+
need_fk = "#{assoc_name}_id" != assoc[:fk]
|
683
|
+
end
|
684
|
+
if (inverse = assoc[:inverse])
|
685
|
+
inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[inverse_table], inverse)
|
686
|
+
has_ones = ::Brick.config.has_ones&.fetch(inverse[:alternate_name].camelize, nil)
|
687
|
+
if has_ones&.key?(singular_inv_assoc_name = ActiveSupport::Inflector.singularize(inverse_assoc_name))
|
688
|
+
inverse_assoc_name = if has_ones[singular_inv_assoc_name]
|
689
|
+
need_inverse_of = true
|
690
|
+
has_ones[singular_inv_assoc_name]
|
691
|
+
else
|
692
|
+
singular_inv_assoc_name
|
693
|
+
end
|
694
|
+
end
|
695
|
+
end
|
696
|
+
:belongs_to
|
697
|
+
else
|
698
|
+
# need_class_name = ActiveSupport::Inflector.singularize(assoc_name) == ActiveSupport::Inflector.singularize(table_name.underscore)
|
699
|
+
# Are there multiple foreign keys out to the same table?
|
700
|
+
assoc_name, need_class_name = _brick_get_hm_assoc_name(relation, assoc)
|
701
|
+
# binding.pry if assoc.key?(:polymorphic)
|
702
|
+
if assoc.key?(:polymorphic)
|
703
|
+
options[:as] = assoc[:fk].to_sym if assoc.key?(:polymorphic)
|
704
|
+
else
|
705
|
+
need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
|
706
|
+
end
|
707
|
+
# fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
|
708
|
+
if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
|
709
|
+
assoc_name = if (custom_assoc_name = has_ones[singular_assoc_name])
|
710
|
+
need_class_name = custom_assoc_name != singular_assoc_name
|
711
|
+
custom_assoc_name
|
712
|
+
else
|
713
|
+
singular_assoc_name
|
714
|
+
end
|
715
|
+
:has_one
|
716
|
+
else
|
717
|
+
:has_many
|
718
|
+
end
|
719
|
+
end
|
720
|
+
# Figure out if we need to specially call out the class_name and/or foreign key
|
721
|
+
# (and if either of those then definitely also a specific inverse_of)
|
722
|
+
options[:class_name] = assoc[:primary_class]&.name || singular_table_name.camelize if need_class_name
|
723
|
+
# Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
|
724
|
+
if need_fk # Funky foreign key?
|
725
|
+
options[:foreign_key] = if assoc[:fk].is_a?(Array)
|
726
|
+
assoc_fk = assoc[:fk].uniq
|
727
|
+
assoc_fk.length < 2 ? assoc_fk.first : assoc_fk
|
728
|
+
else
|
729
|
+
assoc[:fk].to_sym
|
730
|
+
end
|
731
|
+
end
|
732
|
+
options[:inverse_of] = inverse_assoc_name.to_sym if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of)
|
733
|
+
|
734
|
+
# Prepare a list of entries for "has_many :through"
|
735
|
+
if macro == :has_many
|
736
|
+
relations[inverse_table][:hmt_fks].each do |k, hmt_fk|
|
737
|
+
next if k == assoc[:fk]
|
738
|
+
|
739
|
+
hmts[ActiveSupport::Inflector.pluralize(hmt_fk.last)] << [assoc, hmt_fk.first]
|
740
|
+
end
|
741
|
+
end
|
742
|
+
# And finally create a has_one, has_many, or belongs_to for this association
|
743
|
+
assoc_name = assoc_name.to_sym
|
744
|
+
code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
|
745
|
+
self.send(macro, assoc_name, **options)
|
746
|
+
end
|
747
|
+
|
715
748
|
def build_controller(class_name, plural_class_name, model, relations)
|
716
749
|
table_name = ActiveSupport::Inflector.underscore(plural_class_name)
|
717
750
|
singular_table_name = ActiveSupport::Inflector.singularize(table_name)
|
@@ -1024,14 +1057,19 @@ module Brick
|
|
1024
1057
|
# rubocop:enable Style/CommentedKeyword
|
1025
1058
|
|
1026
1059
|
class << self
|
1027
|
-
def _add_bt_and_hm(fk, relations =
|
1028
|
-
relations ||= ::Brick.relations
|
1060
|
+
def _add_bt_and_hm(fk, relations, is_polymorphic = false)
|
1029
1061
|
bt_assoc_name = fk[1].underscore
|
1030
1062
|
bt_assoc_name = bt_assoc_name[0..-4] if bt_assoc_name.end_with?('_id')
|
1031
1063
|
|
1032
1064
|
bts = (relation = relations.fetch(fk[0], nil))&.fetch(:fks) { relation[:fks] = {} }
|
1033
|
-
|
1034
|
-
|
1065
|
+
# %%% Do we miss out on has_many :through or even HM based on constantizing this model early?
|
1066
|
+
# Maybe it's already gotten this info because we got as far as to say there was a unique class
|
1067
|
+
# if is_polymorphic
|
1068
|
+
# primary_table = fk[]
|
1069
|
+
# else
|
1070
|
+
primary_table = (is_class = fk[2].is_a?(Hash) && fk[2].key?(:class)) ? (primary_class = fk[2][:class].constantize).table_name : fk[2]
|
1071
|
+
hms = (relation = relations.fetch(primary_table, nil))&.fetch(:fks) { relation[:fks] = {} } unless is_class
|
1072
|
+
# end
|
1035
1073
|
|
1036
1074
|
unless (cnstr_name = fk[3])
|
1037
1075
|
# For any appended references (those that come from config), arrive upon a definitely unique constraint name
|
@@ -1046,7 +1084,7 @@ module Brick
|
|
1046
1084
|
puts "Brick: Additional reference #{fk.inspect} refers to non-existent #{'table'.pluralize(missing.length)} #{missing.join(' and ')}. (Available tables include #{tables.join(', ')}.)"
|
1047
1085
|
return
|
1048
1086
|
end
|
1049
|
-
unless (cols = relations[fk[0]][:cols]).key?(fk[1])
|
1087
|
+
unless (cols = relations[fk[0]][:cols]).key?(fk[1]) || (is_polymorphic && cols.key?("#{fk[1]}_id") && cols.key?("#{fk[1]}_type"))
|
1050
1088
|
columns = cols.map { |k, v| "#{k} (#{v.first.split(' ').first})" }
|
1051
1089
|
puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[1]}. (Columns present in #{fk[0]} are #{columns.join(', ')}.)"
|
1052
1090
|
return
|
@@ -1061,10 +1099,17 @@ module Brick
|
|
1061
1099
|
end
|
1062
1100
|
end
|
1063
1101
|
if (assoc_bt = bts[cnstr_name])
|
1064
|
-
|
1065
|
-
|
1102
|
+
if is_polymorphic
|
1103
|
+
# Assuming same fk (don't yet support composite keys for polymorphics)
|
1104
|
+
assoc_bt[:inverse_table] << fk[2]
|
1105
|
+
else # Expect we've got a composite key going
|
1106
|
+
assoc_bt[:fk] = assoc_bt[:fk].is_a?(String) ? [assoc_bt[:fk], fk[1]] : assoc_bt[:fk].concat(fk[1])
|
1107
|
+
assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[1]}"
|
1108
|
+
end
|
1066
1109
|
else
|
1067
|
-
|
1110
|
+
inverse_table = [primary_table] if is_polymorphic
|
1111
|
+
assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[1], assoc_name: bt_assoc_name, inverse_table: inverse_table || primary_table }
|
1112
|
+
assoc_bt[:polymorphic] = true if is_polymorphic
|
1068
1113
|
end
|
1069
1114
|
if is_class
|
1070
1115
|
# For use in finding the proper :source for a HMT association that references an STI subclass
|
@@ -1081,6 +1126,7 @@ module Brick
|
|
1081
1126
|
assoc_hm[:inverse] = assoc_bt
|
1082
1127
|
else
|
1083
1128
|
assoc_hm = hms[hm_cnstr_name] = { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0], inverse: assoc_bt }
|
1129
|
+
assoc_hm[:polymorphic] = true if is_polymorphic
|
1084
1130
|
hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
|
1085
1131
|
hm_counts[fk[0]] = hm_counts.fetch(fk[0]) { 0 } + 1
|
1086
1132
|
end
|
@@ -38,6 +38,9 @@ module Brick
|
|
38
38
|
|
39
39
|
# Has one relationships
|
40
40
|
::Brick.has_ones = app.config.brick.fetch(:has_ones, nil)
|
41
|
+
|
42
|
+
# Polymorphic associations
|
43
|
+
::Brick.polymorphics = app.config.brick.fetch(:polymorphics, nil)
|
41
44
|
end
|
42
45
|
|
43
46
|
# After we're initialized and before running the rest of stuff, put our configuration in place
|
@@ -64,12 +67,14 @@ module Brick
|
|
64
67
|
is_template_exists
|
65
68
|
end
|
66
69
|
|
67
|
-
def path_keys(fk_name, obj_name, pk)
|
68
|
-
if fk_name.is_a?(Array) && pk.is_a?(Array) # Composite keys?
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
70
|
+
def path_keys(hm_assoc, fk_name, obj_name, pk)
|
71
|
+
keys = if fk_name.is_a?(Array) && pk.is_a?(Array) # Composite keys?
|
72
|
+
fk_name.zip(pk.map { |pk_part| "#{obj_name}.#{pk_part}" })
|
73
|
+
else
|
74
|
+
[[fk_name, "#{obj_name}.#{pk}"]]
|
75
|
+
end
|
76
|
+
keys << [hm_assoc.inverse_of.foreign_type, "#{hm_assoc.active_record.name}"] if hm_assoc.options.key?(:as)
|
77
|
+
keys.map { |x| "#{x.first}: #{x.last}"}.join(', ')
|
73
78
|
end
|
74
79
|
|
75
80
|
alias :_brick_find_template :find_template
|
@@ -102,12 +107,12 @@ module Brick
|
|
102
107
|
"#{obj_name}.#{attrib_name} || 0"
|
103
108
|
end
|
104
109
|
"<%= ct = #{set_ct}
|
105
|
-
link_to \"#\{ct || 'View'\} #{assoc_name}\", #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_fk_name, obj_name, pk)} }) unless ct&.zero? %>\n"
|
110
|
+
link_to \"#\{ct || 'View'\} #{assoc_name}\", #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, obj_name, pk)} }) unless ct&.zero? %>\n"
|
106
111
|
else # has_one
|
107
112
|
"<%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>\n"
|
108
113
|
end
|
109
114
|
elsif args.first == 'show'
|
110
|
-
hm_stuff << "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_fk_name, "@#{obj_name}&.first&", pk)} }) %>\n"
|
115
|
+
hm_stuff << "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, "@#{obj_name}&.first&", pk)} }) %>\n"
|
111
116
|
end
|
112
117
|
s << hm_stuff
|
113
118
|
end
|
@@ -205,7 +210,16 @@ def hide_bcrypt(val)
|
|
205
210
|
end %>"
|
206
211
|
|
207
212
|
if ['index', 'show', 'update'].include?(args.first)
|
208
|
-
css << "<% bts = { #{
|
213
|
+
css << "<% bts = { #{
|
214
|
+
bts.each_with_object([]) do |v, s|
|
215
|
+
foreign_models = if v.last[1].is_a?(Array)
|
216
|
+
v.last[1].each_with_object([]) { |x, s| s << "[#{x.name}, #{x.primary_key.inspect}]" }.join(', ')
|
217
|
+
else
|
218
|
+
"[#{v.last[1].name}, #{v.last[1].primary_key.inspect}]"
|
219
|
+
end
|
220
|
+
s << "#{v.first.inspect} => [#{v.last.first.inspect}, [#{foreign_models}]]"
|
221
|
+
end.join(', ')
|
222
|
+
} } %>"
|
209
223
|
end
|
210
224
|
|
211
225
|
# %%% When doing schema select, if there's an ID then remove it, or if we're on a new page go to index
|
@@ -373,10 +387,13 @@ function changeout(href, param, value) {
|
|
373
387
|
<% next if col == '#{pk}' || ::Brick.config.metadata_columns.include?(col) %>
|
374
388
|
<th>
|
375
389
|
<% if (bt = bts[col]) %>
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
390
|
+
BT <%
|
391
|
+
bt[1].each do |bt_pair| %><%=
|
392
|
+
bt_pair.first.bt_link(bt.first) %> <%
|
393
|
+
end %><%
|
394
|
+
else %><%=
|
395
|
+
col %><%
|
396
|
+
end %>
|
380
397
|
</th>
|
381
398
|
<% end %>
|
382
399
|
<%# Consider getting the name from the association -- h.first.name -- if a more \"friendly\" alias should be used for a screwy table name %>
|
@@ -388,14 +405,24 @@ function changeout(href, param, value) {
|
|
388
405
|
<tr>#{"
|
389
406
|
<td><%= link_to '⇛', #{obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
|
390
407
|
<% #{obj_name}.attributes.each do |k, val| %>
|
391
|
-
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) || k.start_with?('_brfk_') || (k.start_with?('_br_') && k.end_with?('_ct')) %>
|
408
|
+
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) || k.start_with?('_brfk_') || (k.start_with?('_br_') && (k.length == 63 || k.end_with?('_ct'))) %>
|
392
409
|
<td>
|
393
410
|
<% if (bt = bts[k]) %>
|
394
411
|
<%# binding.pry # Postgres column names are limited to 63 characters %>
|
395
|
-
<%
|
396
|
-
|
397
|
-
|
398
|
-
|
412
|
+
<% if (pairs = bt[1].length > 1)
|
413
|
+
bt_class = #{obj_name}.send(\"#\{bt.first\}_type\")
|
414
|
+
# descrips = @_brick_bt_descrip[bt.first][bt_class]
|
415
|
+
poly_id = #{obj_name}.send(\"#\{bt.first\}_id\")
|
416
|
+
%><%= link_to(\"#\{bt_class\} ##\{poly_id\}\",
|
417
|
+
send(\"#\{bt_class.underscore\}_path\".to_sym, poly_id)) if poly_id %><%
|
418
|
+
else # We should do something other than [0..-2] for when there is no primary key (or maybe have an empty final array there in that case?)
|
419
|
+
bt_txt = (bt_class = bt[1].first.first).brick_descrip(
|
420
|
+
#{obj_name}, (descrips = @_brick_bt_descrip[bt.first][bt_class])[0..-2].map { |z| #{obj_name}.send(z.last[0..62]) }, (bt_id_col = descrips.last)
|
421
|
+
)
|
422
|
+
bt_id = #{obj_name}.send(*bt_id_col) if bt_id_col&.present? %>
|
423
|
+
<%= bt_id ? link_to(bt_txt, send(\"#\{bt_class.name.underscore\}_path\".to_sym, bt_id)) : bt_txt %>
|
424
|
+
<%#= Previously was: bt_obj = bt[1].first.first.find_by(bt[2] => val); link_to(bt_obj.brick_descrip, send(\"#\{bt[1].first.first.name.underscore\}_path\".to_sym, bt_obj.send(bt[1].first.first.primary_key.to_sym))) if bt_obj %>
|
425
|
+
<% end %>
|
399
426
|
<% else %>
|
400
427
|
<%= hide_bcrypt(val) %>
|
401
428
|
<% end %>
|
@@ -418,9 +445,9 @@ function changeout(href, param, value) {
|
|
418
445
|
<%= link_to '(See all #{obj_name.pluralize})', #{table_name}_path %>
|
419
446
|
<% if obj %>
|
420
447
|
<%= # path_options = [obj.#{pk}]
|
421
|
-
|
422
|
-
|
423
|
-
|
448
|
+
# path_options << { '_brick_schema': } if
|
449
|
+
# url = send(:#{model_name.underscore}_path, obj.#{pk})
|
450
|
+
form_for(obj.becomes(#{model_name})) do |f| %>
|
424
451
|
<table>
|
425
452
|
<% has_fields = false
|
426
453
|
@#{obj_name}.first.attributes.each do |k, val| %>
|
@@ -430,25 +457,35 @@ function changeout(href, param, value) {
|
|
430
457
|
<th class=\"show-field\">
|
431
458
|
<% has_fields = true
|
432
459
|
if (bt = bts[k])
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
460
|
+
# Add a final member in this array with descriptive options to be used in <select> drop-downs
|
461
|
+
bt_name = bt[1].map { |x| x.first.name }.join('/')
|
462
|
+
# %%% Only do this if the user has permissions to edit this bt field
|
463
|
+
if (pairs = bt[1]).length > 1
|
464
|
+
poly_class_name = @#{obj_name}.first.send(\"#\{bt.first\}_type\")
|
465
|
+
bt_pair = pairs.find { |pair| pair.first.name == poly_class_name }
|
466
|
+
# descrips = @_brick_bt_descrip[bt.first][bt_class]
|
467
|
+
poly_id = @#{obj_name}.first.send(\"#\{bt.first\}_id\")
|
468
|
+
# bt_class.order(obj_pk = bt_class.primary_key).each { |obj| option_detail << [obj.brick_descrip(nil, obj_pk), obj.send(obj_pk)] }
|
469
|
+
else # No polymorphism, so just get the first one
|
470
|
+
bt_pair = bt[1].first
|
471
|
+
end
|
472
|
+
bt_class = bt_pair.first
|
473
|
+
if bt.length < 3
|
474
|
+
bt << (option_detail = [[\"(No #\{bt_name\} chosen)\", '^^^brick_NULL^^^']])
|
475
|
+
# %%% Accommodate composite keys for obj.pk at the end here
|
476
|
+
bt_class.order(obj_pk = bt_class.primary_key).each { |obj| option_detail << [obj.brick_descrip(nil, obj_pk), obj.send(obj_pk)] }
|
477
|
+
end %>
|
478
|
+
BT <%= bt_class.bt_link(bt.first) %>
|
442
479
|
<% else %>
|
443
480
|
<%= k %>
|
444
481
|
<% end %>
|
445
482
|
</th>
|
446
483
|
<td>
|
447
|
-
<% if
|
484
|
+
<% if bt
|
448
485
|
html_options = { prompt: \"Select #\{bt_name\}\" }
|
449
486
|
html_options[:class] = 'dimmed' unless val %>
|
450
|
-
<%= f.select k.to_sym, bt[
|
451
|
-
<%= bt_obj =
|
487
|
+
<%= f.select k.to_sym, bt[2], { value: val || '^^^brick_NULL^^^' }, html_options %>
|
488
|
+
<%= bt_obj = bt_class.find_by(bt_pair[1] => val); link_to('⇛', send(\"#\{bt_class.name.underscore\}_path\".to_sym, bt_obj.send(bt_class.primary_key.to_sym)), { class: 'show-arrow' }) if bt_obj %>
|
452
489
|
<% else case #{model_name}.column_for_attribute(k).type
|
453
490
|
when :string, :text %>
|
454
491
|
<% if is_bcrypt?(val) # || .readonly? %>
|
@@ -469,12 +506,12 @@ function changeout(href, param, value) {
|
|
469
506
|
<% end %>
|
470
507
|
</td>
|
471
508
|
</tr>
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
509
|
+
<% end
|
510
|
+
if has_fields %>
|
511
|
+
<tr><td colspan=\"2\" class=\"right\"><%= f.submit %></td></tr>
|
512
|
+
<% else %>
|
513
|
+
<tr><td colspan=\"2\">(No displayable fields)</td></tr>
|
514
|
+
<% end %>
|
478
515
|
</table>
|
479
516
|
<% end %>
|
480
517
|
|
@@ -501,6 +538,7 @@ function changeout(href, param, value) {
|
|
501
538
|
#{script}"
|
502
539
|
|
503
540
|
end
|
541
|
+
puts inline
|
504
542
|
# As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
|
505
543
|
keys = options.has_key?(:locals) ? options[:locals].keys : []
|
506
544
|
handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
|
@@ -517,7 +555,7 @@ function changeout(href, param, value) {
|
|
517
555
|
end
|
518
556
|
|
519
557
|
# Just in case it hadn't been done previously when we tried to load the brick initialiser,
|
520
|
-
# go make sure we've loaded additional references (virtual foreign keys).
|
558
|
+
# go make sure we've loaded additional references (virtual foreign keys and polymorphic associations).
|
521
559
|
::Brick.load_additional_references
|
522
560
|
end
|
523
561
|
end
|
data/lib/brick/version_number.rb
CHANGED
data/lib/brick.rb
CHANGED
@@ -103,11 +103,17 @@ module Brick
|
|
103
103
|
|
104
104
|
def get_bts_and_hms(model)
|
105
105
|
bts, hms = model.reflect_on_all_associations.each_with_object([{}, {}]) do |a, s|
|
106
|
-
next if !const_defined?(a.name.to_s.singularize.camelize) && ::Brick.config.exclude_tables.include?(a.plural_name)
|
106
|
+
next if (!const_defined?(a.name.to_s.singularize.camelize) && ::Brick.config.exclude_tables.include?(a.plural_name))
|
107
107
|
|
108
108
|
case a.macro
|
109
109
|
when :belongs_to
|
110
|
-
s.first[a.foreign_key] =
|
110
|
+
s.first[a.foreign_key] = if a.polymorphic?
|
111
|
+
primary_tables = relations[model.table_name][:fks].find { |_k, fk| fk[:assoc_name] == a.name.to_s }&.last&.fetch(:inverse_table, [])
|
112
|
+
models = primary_tables&.map { |table| table.singularize.camelize.constantize }
|
113
|
+
[a.name, models]
|
114
|
+
else
|
115
|
+
[a.name, a.klass]
|
116
|
+
end
|
111
117
|
when :has_many, :has_one # This gets has_many as well as has_many :through
|
112
118
|
# %%% weed out ones that don't have an available model to reference
|
113
119
|
s.last[a.name] = a
|
@@ -256,6 +262,11 @@ module Brick
|
|
256
262
|
end
|
257
263
|
end
|
258
264
|
|
265
|
+
# Polymorphic associations
|
266
|
+
def polymorphics=(polys)
|
267
|
+
Brick.config.polymorphics = polys || {}
|
268
|
+
end
|
269
|
+
|
259
270
|
# DSL templates for individual models to provide prettier descriptions of objects
|
260
271
|
# @api public
|
261
272
|
def model_descrips=(descrips)
|
@@ -274,8 +285,18 @@ module Brick
|
|
274
285
|
def load_additional_references
|
275
286
|
return if @_additional_references_loaded
|
276
287
|
|
277
|
-
|
278
|
-
|
288
|
+
relations = ::Brick.relations
|
289
|
+
if (ars = ::Brick.config.additional_references) || ::Brick.config.polymorphics
|
290
|
+
ars.each { |fk| ::Brick._add_bt_and_hm(fk[0..2], relations) } if ars
|
291
|
+
if (polys = ::Brick.config.polymorphics)
|
292
|
+
polys.each do |k, v|
|
293
|
+
table_name, poly = k.split('.')
|
294
|
+
v ||= ActiveRecord::Base.execute_sql("SELECT DISTINCT #{poly}_type AS typ FROM #{table_name}").map { |result| result['typ'] }
|
295
|
+
v.each do |type|
|
296
|
+
::Brick._add_bt_and_hm([table_name, poly, type.underscore.pluralize, "(brick) #{table_name}_#{poly}"], relations, true)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
279
300
|
@_additional_references_loaded = true
|
280
301
|
end
|
281
302
|
|
@@ -157,6 +157,10 @@ Brick.skip_index_hms = ['User.litany_of_woes']
|
|
157
157
|
# Brick.sti_namespace_prefixes = { '::Animals::' => 'Animal',
|
158
158
|
# '::Snake' => 'Reptile' }
|
159
159
|
|
160
|
+
# # Polymorphic associations must be explicitly specified, which is as easy as providing a model name and polymorphic
|
161
|
+
# # association name like this:
|
162
|
+
# Brick.polymorphics = ['Comment.commentable', 'Image.imageable']
|
163
|
+
|
160
164
|
# # If a default route is not supplied, Brick attempts to find the most \"central\" table and wires up the default
|
161
165
|
# # route to go to the :index action for what would be a controller for that table. You can specify any controller
|
162
166
|
# # name and action you wish in order to override this and have that be the default route when none other has been
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: brick
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.25
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lorin Thwaits
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-05-
|
11
|
+
date: 2022-05-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|