brick 1.0.24 → 1.0.25

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96ba9e8b4ddc54e5feaaf8652c93e5d9e1ffe15345a8cad719674e051b296718
4
- data.tar.gz: 3901467e7918ece559f55740759bd555bababe96c407488307fcb01298c14034
3
+ metadata.gz: b0dfa64d7a8a148b4c2ad990f5af1604d5855b80c6e95f39cd371a43a591b7f1
4
+ data.tar.gz: bc7644a08678136d91696a68e0ad670f4f04d41772f36ba1bd806ebe45f23b04
5
5
  SHA512:
6
- metadata.gz: aa73995e69947be5ca83597a71751cfffb2433551d04fb00b760bc51c199bcd9890d7a05c0deb2f2c07c2c89205acf52b942d00ae78e1d7c0a26544acf70a4d0
7
- data.tar.gz: 2328082844a0c993ada29c02eab22cf0d04efbcf3e4479c804e16f7d2a39a42d28debcd5592620fecd3bc9c404458fe3b5ff448f1f2c1b7f0d2e13557f5f9fa8
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
@@ -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 { |part| klass = klass.reflect_on_association(part_sym = part.to_sym).klass; part_sym }
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
- s = build_array
94
- parts[0..-3].each { |v| s = s[v.to_sym] }
95
- s[parts[-2]] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
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 }.map(&:foreign_key)
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] = [bt.last, bt.last.brick_parse_dsl(join_array, bt.first, translations)]
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&.columns&.any? { |col| col.name == ks.last }
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
- tbl_name = field_tbl_names[v.first][v.last.first] ||= shift_or_first(chains[v.last.first])
348
- if (id_col = v.last.first.primary_key) && !id_for_tables.key?(v.first) # was tbl_name
349
- # Accommodate composite primary key by allowing id_col to come in as an array
350
- (id_col.is_a?(Array) ? id_col : [id_col]).each do |id_part|
351
- selects << "#{"#{tbl_name}.#{id_part}"} AS \"#{(id_alias = "_brfk_#{v.first}__#{id_part}")}\""
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
- v.last[1].map { |x| [translations[x[0..-2].map(&:to_s).join('.')], x.last] }.each_with_index do |sel_col, idx|
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
- # col_name is weak when there are multiple, using sel_col.last instead
360
+
361
361
  selects << "#{"#{field_tbl_name}.#{sel_col.last}"} AS \"#{(col_alias = "_brfk_#{v.first}__#{sel_col.last}")}\""
362
- v.last[1][idx] << col_alias
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
- joins!("LEFT OUTER
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
- joins!("LEFT OUTER
394
- JOIN (SELECT #{fk_col}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.table_name || hm.klass.table_name} GROUP BY 1) AS #{tbl_alias}
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
- options = {}
607
- singular_table_name = ActiveSupport::Inflector.singularize(assoc[:inverse_table])
608
- macro = if assoc[:is_bt]
609
- # Try to take care of screwy names if this is a belongs_to going to an STI subclass
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 = nil)
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
- primary_table = (is_class = fk[2].is_a?(Hash) && fk[2].key?(:class)) ? (primary_class = fk[2][:class].constantize).table_name : fk[2]
1034
- hms = (relation = relations.fetch(primary_table, nil))&.fetch(:fks) { relation[:fks] = {} } unless is_class
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
- assoc_bt[:fk] = assoc_bt[:fk].is_a?(String) ? [assoc_bt[:fk], fk[1]] : assoc_bt[:fk].concat(fk[1])
1065
- assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[1]}"
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
- assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[1], assoc_name: bt_assoc_name, inverse_table: primary_table }
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
- fk_name.zip(pk.map { |pk_part| "#{obj_name}.#{pk_part}" })
70
- else
71
- [[fk_name, "#{obj_name}.#{pk}"]]
72
- end.map { |x| "#{x.first}: #{x.last}"}.join(', ')
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 = { #{bts.each_with_object([]) { |v, s| s << "#{v.first.inspect} => [#{v.last.first.inspect}, #{v.last[1].name}, #{v.last[1].primary_key.inspect}]"}.join(', ')} } %>"
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
- BT <%= bt[1].bt_link(bt.first) %>
377
- <% else %>
378
- <%= col %>
379
- <% end %>
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
- <% bt_txt = bt[1].brick_descrip(#{obj_name}, @_brick_bt_descrip[bt.first][1].map { |z| #{obj_name}.send(z.last[0..62]) }, @_brick_bt_descrip[bt.first][2]) %>
396
- <% bt_id_col = @_brick_bt_descrip[bt.first][2]; bt_id = #{obj_name}.send(*bt_id_col) if bt_id_col&.present? %>
397
- <%= bt_id ? link_to(bt_txt, send(\"#\{bt_obj_path_base = bt[1].name.underscore\}_path\".to_sym, bt_id)) : bt_txt %>
398
- <%#= Previously was: bt_obj = bt[1].find_by(bt[2] => val); link_to(bt_obj.brick_descrip, send(\"#\{bt_obj_path_base = bt[1].name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym))) if bt_obj %>
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
- # path_options << { '_brick_schema': } if
422
- # url = send(:#{model_name.underscore}_path, obj.#{pk})
423
- form_for(obj.becomes(#{model_name})) do |f| %>
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
- # Add a final member in this array with descriptive options to be used in <select> drop-downs
434
- bt_name = bt[1].name
435
- # %%% Only do this if the user has permissions to edit this bt field
436
- if bt.length < 4
437
- bt << (option_detail = [[\"(No #\{bt_name\} chosen)\", '^^^brick_NULL^^^']])
438
- # %%% Accommodate composite keys for obj.pk at the end here
439
- bt[1].order(obj_pk = bt[1].primary_key).each { |obj| option_detail << [obj.brick_descrip(nil, obj_pk), obj.send(obj_pk)] }
440
- end %>
441
- BT <%= bt[1].bt_link(bt.first) %>
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 (bt = bts[k]) # bt_obj.brick_descrip
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[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
451
- <%= bt_obj = bt[1].find_by(bt[2] => val); link_to('⇛', send(\"#\{bt_obj_path_base = bt_name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym)), { class: 'show-arrow' }) if 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
- <% end
473
- if has_fields %>
474
- <tr><td colspan=\"2\" class=\"right\"><%= f.submit %></td></tr>
475
- <% else %>
476
- <tr><td colspan=\"2\">(No displayable fields)</td></tr>
477
- <% end %>
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
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 24
8
+ TINY = 25
9
9
 
10
10
  # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
11
  PRE = nil
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] = [a.name, a.klass]
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
- if (ars = ::Brick.config.additional_references)
278
- ars.each { |fk| ::Brick._add_bt_and_hm(fk[0..2]) }
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.24
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-13 00:00:00.000000000 Z
11
+ date: 2022-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord