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 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