brick 1.0.24 → 1.0.27

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: '097fb194c2b1323cd051669f362d86492d9c768f1cbccf06c2fdc45a7f266e1e'
4
+ data.tar.gz: 72746f153d073256b8be063e90c28da85a50e11865250f2570d501ff320431b3
5
5
  SHA512:
6
- metadata.gz: aa73995e69947be5ca83597a71751cfffb2433551d04fb00b760bc51c199bcd9890d7a05c0deb2f2c07c2c89205acf52b942d00ae78e1d7c0a26544acf70a4d0
7
- data.tar.gz: 2328082844a0c993ada29c02eab22cf0d04efbcf3e4479c804e16f7d2a39a42d28debcd5592620fecd3bc9c404458fe3b5ff448f1f2c1b7f0d2e13557f5f9fa8
6
+ metadata.gz: 83d0c40c0af17d314d6b4cd7ab0e312bce1c8d57bdd0992da1b54fa8a3e829852c6fd800c34f3bd35f3a3306bda736dc99d14af19009926bf65971a5d4904717
7
+ data.tar.gz: c82b9c31ffe3a00e701e22a3f69c85e7b60e0b3b6d24e95df7fe3f8afb85b00ab7515e0befab7dd37e8a25924274e663127b953dab8eb64041d834aa39de4bc1
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
@@ -113,6 +122,22 @@ module Brick
113
122
  @mutex.synchronize { @sti_namespace_prefixes = prefixes }
114
123
  end
115
124
 
125
+ def schema_to_analyse
126
+ @mutex.synchronize { @schema_to_analyse }
127
+ end
128
+
129
+ def schema_to_analyse=(schema)
130
+ @mutex.synchronize { @schema_to_analyse = schema }
131
+ end
132
+
133
+ def default_route_fallback
134
+ @mutex.synchronize { @default_route_fallback }
135
+ end
136
+
137
+ def default_route_fallback=(resource_name)
138
+ @mutex.synchronize { @default_route_fallback = resource_name }
139
+ end
140
+
116
141
  def skip_database_views
117
142
  @mutex.synchronize { @skip_database_views }
118
143
  end
@@ -74,8 +74,7 @@ module ActiveRecord
74
74
  dsl
75
75
  end
76
76
 
77
- # Pass in true or a JoinArray
78
- def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {})
77
+ def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {}, is_polymorphic = false)
79
78
  build_array = ::Brick::JoinArray.new.tap { |ary| ary.replace([build_array]) } if build_array.is_a?(::Brick::JoinHash)
80
79
  build_array = ::Brick::JoinArray.new unless build_array.nil? || build_array.is_a?(Array)
81
80
  members = []
@@ -87,12 +86,17 @@ module ActiveRecord
87
86
  if bracket_name
88
87
  if ch == ']' # Time to process a bracketed thing?
89
88
  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 }
89
+ first_parts = parts[0..-2].map do |part|
90
+ klass = klass.reflect_on_association(part_sym = part.to_sym).klass
91
+ part_sym
92
+ end
91
93
  parts = prefix + first_parts + [parts[-1]]
92
94
  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
95
+ unless is_polymorphic
96
+ s = build_array
97
+ parts[0..-3].each { |v| s = s[v.to_sym] }
98
+ s[parts[-2]] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
99
+ end
96
100
  translations[parts[0..-2].join('.')] = klass
97
101
  end
98
102
  members << parts
@@ -191,7 +195,10 @@ module ActiveRecord
191
195
  private
192
196
 
193
197
  def self._brick_get_fks
194
- @_brick_get_fks ||= reflect_on_all_associations.select { |a2| a2.macro == :belongs_to }.map(&:foreign_key)
198
+ @_brick_get_fks ||= reflect_on_all_associations.select { |a2| a2.macro == :belongs_to }.each_with_object([]) do |bt, s|
199
+ s << bt.foreign_key
200
+ s << bt.foreign_type if bt.polymorphic?
201
+ end
195
202
  end
196
203
  end
197
204
 
@@ -279,23 +286,6 @@ module ActiveRecord
279
286
  # , is_add_bts, is_add_hms
280
287
  )
281
288
  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
289
 
300
290
  # %%% Skip the metadata columns
301
291
  if selects&.empty? # Default to all columns
@@ -309,8 +299,14 @@ module ActiveRecord
309
299
  if is_add_bts || is_add_hms
310
300
  bts, hms, associatives = ::Brick.get_bts_and_hms(klass)
311
301
  bts.each do |_k, bt|
302
+ next if bt[2] # Polymorphic?
303
+
312
304
  # 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)]
305
+ bt_descrip[bt.first] = if bt[1].is_a?(Array)
306
+ bt[1].each_with_object({}) { |bt_class, s| s[bt_class] = bt_class.brick_parse_dsl(join_array, bt.first, translations, true) }
307
+ else
308
+ { bt.last => bt[1].brick_parse_dsl(join_array, bt.first, translations) }
309
+ end
314
310
  end
315
311
  skip_klass_hms = ::Brick.config.skip_index_hms[klass.name] || {}
316
312
  hms.each do |k, hm|
@@ -328,7 +324,7 @@ module ActiveRecord
328
324
  when 2
329
325
  assoc_name = ks.first.to_sym
330
326
  # 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 }
327
+ next unless klass.reflect_on_association(assoc_name)&.klass&.column_names&.any?(ks.last)
332
328
 
333
329
  join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
334
330
  end
@@ -344,22 +340,31 @@ module ActiveRecord
344
340
  id_for_tables = Hash.new { |h, k| h[k] = [] }
345
341
  field_tbl_names = Hash.new { |h, k| h[k] = {} }
346
342
  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)
343
+ v.last.each do |k1, v1| # k1 is class, v1 is array of columns to snag
344
+ next if chains[k1].nil?
345
+
346
+ tbl_name = field_tbl_names[v.first][k1] ||= shift_or_first(chains[k1])
347
+ # if (col_name = v1[1].last&.last) # col_name is weak when there are multiple, using sel_col.last instead
357
348
  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|
349
+ v1.map { |x|
350
+ [translations[x[0..-2].map(&:to_s).join('.')], x.last]
351
+ }.each_with_index do |sel_col, idx|
359
352
  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
353
+
361
354
  selects << "#{"#{field_tbl_name}.#{sel_col.last}"} AS \"#{(col_alias = "_brfk_#{v.first}__#{sel_col.last}")}\""
362
- v.last[1][idx] << col_alias
355
+ v1[idx] << col_alias
356
+ end
357
+ # end
358
+
359
+ unless id_for_tables.key?(v.first)
360
+ # Accommodate composite primary key by allowing id_col to come in as an array
361
+ ((id_col = k1.primary_key).is_a?(Array) ? id_col : [id_col]).each do |id_part|
362
+ id_for_tables[v.first] << if id_part
363
+ selects << "#{"#{tbl_name}.#{id_part}"} AS \"#{(id_alias = "_brfk_#{v.first}__#{id_part}")}\""
364
+ id_alias
365
+ end
366
+ end
367
+ v1 << id_for_tables[v.first].compact
363
368
  end
364
369
  end
365
370
  end
@@ -379,21 +384,27 @@ module ActiveRecord
379
384
  hm.foreign_key
380
385
  else
381
386
  fk_col = hm.foreign_key
382
- hm.klass.primary_key || '*'
387
+ poly_type = hm.inverse_of.foreign_type if hm.options.key?(:as)
388
+ pk = hm.klass.primary_key
389
+ (pk.is_a?(Array) ? pk.first : pk) || '*'
383
390
  end
384
391
  tbl_alias = "_br_#{hm.name}"
385
392
  pri_tbl = hm.active_record
393
+ on_clause = []
386
394
  if fk_col.is_a?(Array) # Composite key?
387
- on_clause = []
388
395
  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 ')}")
396
+ selects = fk_col.dup
392
397
  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}")
398
+ selects = [fk_col]
399
+ on_clause << "#{tbl_alias}.#{fk_col} = #{pri_tbl.table_name}.#{pri_tbl.primary_key}"
400
+ end
401
+ if poly_type
402
+ selects << poly_type
403
+ on_clause << "#{tbl_alias}.#{poly_type} = '#{name}'"
396
404
  end
405
+ join_clause = "LEFT OUTER
406
+ 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}"
407
+ joins!("#{join_clause} ON #{on_clause.join(' AND ')}")
397
408
  end
398
409
  where!(wheres) unless wheres.empty?
399
410
  wheres unless wheres.empty? # Return the specific parameters that we did use
@@ -438,8 +449,7 @@ JOIN (SELECT #{fk_col}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.table
438
449
  this_module.const_get(class_name)
439
450
  else
440
451
  # Build STI subclass and place it into the namespace module
441
- # %%% Does this ever get used???
442
- puts [this_module.const_set(class_name, klass = Class.new(self)).name, class_name].inspect
452
+ this_module.const_set(class_name, klass = Class.new(self))
443
453
  klass
444
454
  end
445
455
  end
@@ -490,7 +500,7 @@ class Object
490
500
  # path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
491
501
  # return self._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
492
502
  # 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('::'))
503
+ if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = (self.name || class_name)&.split('::'))
494
504
  filepath = (filepath[0..-2] + [class_name]).join('/').underscore + '.rb'
495
505
  end
496
506
  if is_found
@@ -514,7 +524,7 @@ class Object
514
524
  singular_table_name = ActiveSupport::Inflector.underscore(model_name)
515
525
 
516
526
  # Adjust for STI if we know of a base model for the requested model name
517
- table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil))
527
+ table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, ::Brick.existing_stis[model_name]&.constantize))
518
528
  base_model.table_name
519
529
  else
520
530
  ActiveSupport::Inflector.pluralize(singular_table_name)
@@ -555,7 +565,7 @@ class Object
555
565
  return
556
566
  end
557
567
 
558
- if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil))
568
+ if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, ::Brick.existing_stis[model_name]&.constantize))
559
569
  is_sti = true
560
570
  else
561
571
  base_model = ::Brick.config.models_inherit_from || ActiveRecord::Base
@@ -603,77 +613,11 @@ class Object
603
613
  hmts = fks.each_with_object(Hash.new { |h, k| h[k] = [] }) do |fk, hmts|
604
614
  # The key in each hash entry (fk.first) is the constraint name
605
615
  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
616
+ if (invs = assoc[:inverse_table]).is_a?(Array)
617
+ invs.each { |inv| build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, inv, code) }
618
+ else
619
+ build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, invs, code)
671
620
  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
621
  hmts
678
622
  end
679
623
  hmts.each do |hmt_fk, fks|
@@ -712,6 +656,88 @@ class Object
712
656
  [built_model, code]
713
657
  end
714
658
 
659
+ def build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, inverse_table, code)
660
+ singular_table_name = inverse_table&.singularize
661
+ options = {}
662
+ macro = if assoc[:is_bt]
663
+ # Try to take care of screwy names if this is a belongs_to going to an STI subclass
664
+ assoc_name = if (primary_class = assoc.fetch(:primary_class, nil)) &&
665
+ sti_inverse_assoc = primary_class.reflect_on_all_associations.find do |a|
666
+ a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key
667
+ end
668
+ sti_inverse_assoc.options[:inverse_of]&.to_s || assoc_name
669
+ else
670
+ assoc[:assoc_name]
671
+ end
672
+ if assoc.key?(:polymorphic)
673
+ options[:polymorphic] = true
674
+ else
675
+ need_class_name = singular_table_name.underscore != assoc_name
676
+ need_fk = "#{assoc_name}_id" != assoc[:fk]
677
+ end
678
+ if (inverse = assoc[:inverse])
679
+ inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[inverse_table], inverse)
680
+ has_ones = ::Brick.config.has_ones&.fetch(inverse[:alternate_name].camelize, nil)
681
+ if has_ones&.key?(singular_inv_assoc_name = ActiveSupport::Inflector.singularize(inverse_assoc_name))
682
+ inverse_assoc_name = if has_ones[singular_inv_assoc_name]
683
+ need_inverse_of = true
684
+ has_ones[singular_inv_assoc_name]
685
+ else
686
+ singular_inv_assoc_name
687
+ end
688
+ end
689
+ end
690
+ :belongs_to
691
+ else
692
+ # need_class_name = ActiveSupport::Inflector.singularize(assoc_name) == ActiveSupport::Inflector.singularize(table_name.underscore)
693
+ # Are there multiple foreign keys out to the same table?
694
+ assoc_name, need_class_name = _brick_get_hm_assoc_name(relation, assoc)
695
+ if assoc.key?(:polymorphic)
696
+ options[:as] = assoc[:fk].to_sym
697
+ else
698
+ need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
699
+ end
700
+ # fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
701
+ if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
702
+ assoc_name = if (custom_assoc_name = has_ones[singular_assoc_name])
703
+ need_class_name = custom_assoc_name != singular_assoc_name
704
+ custom_assoc_name
705
+ else
706
+ singular_assoc_name
707
+ end
708
+ :has_one
709
+ else
710
+ :has_many
711
+ end
712
+ end
713
+ # Figure out if we need to specially call out the class_name and/or foreign key
714
+ # (and if either of those then definitely also a specific inverse_of)
715
+ options[:class_name] = assoc[:primary_class]&.name || singular_table_name.camelize if need_class_name
716
+ # Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
717
+ if need_fk # Funky foreign key?
718
+ options[:foreign_key] = if assoc[:fk].is_a?(Array)
719
+ assoc_fk = assoc[:fk].uniq
720
+ assoc_fk.length < 2 ? assoc_fk.first : assoc_fk
721
+ else
722
+ assoc[:fk].to_sym
723
+ end
724
+ end
725
+ options[:inverse_of] = inverse_assoc_name.to_sym if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of)
726
+
727
+ # Prepare a list of entries for "has_many :through"
728
+ if macro == :has_many
729
+ relations[inverse_table][:hmt_fks].each do |k, hmt_fk|
730
+ next if k == assoc[:fk]
731
+
732
+ hmts[ActiveSupport::Inflector.pluralize(hmt_fk.last)] << [assoc, hmt_fk.first]
733
+ end
734
+ end
735
+ # And finally create a has_one, has_many, or belongs_to for this association
736
+ assoc_name = assoc_name.to_sym
737
+ code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
738
+ self.send(macro, assoc_name, **options)
739
+ end
740
+
715
741
  def build_controller(class_name, plural_class_name, model, relations)
716
742
  table_name = ActiveSupport::Inflector.underscore(plural_class_name)
717
743
  singular_table_name = ActiveSupport::Inflector.singularize(table_name)
@@ -754,11 +780,15 @@ class Object
754
780
 
755
781
  if model.primary_key
756
782
  code << " def show\n"
757
- code << (find_by_id = " @#{singular_table_name} = #{model.name}.find(params[:id].split(','))\n")
783
+ code << (find_by_id = " id = params[:id]&.split(/[\\/,_]/)
784
+ id = id.first if id.is_a?(Array) && id.length == 1
785
+ @#{singular_table_name} = #{model.name}.find(id)\n")
758
786
  code << " end\n"
759
787
  self.define_method :show do
760
788
  ::Brick.set_db_schema(params)
761
- instance_variable_set("@#{singular_table_name}".to_sym, model.find(params[:id].split(',')))
789
+ id = params[:id]&.split(/[\/,_]/)
790
+ id = id.first if id.is_a?(Array) && id.length == 1
791
+ instance_variable_set("@#{singular_table_name}".to_sym, model.find(id))
762
792
  end
763
793
  end
764
794
 
@@ -1024,12 +1054,13 @@ module Brick
1024
1054
  # rubocop:enable Style/CommentedKeyword
1025
1055
 
1026
1056
  class << self
1027
- def _add_bt_and_hm(fk, relations = nil)
1028
- relations ||= ::Brick.relations
1057
+ def _add_bt_and_hm(fk, relations, is_polymorphic = false)
1029
1058
  bt_assoc_name = fk[1].underscore
1030
1059
  bt_assoc_name = bt_assoc_name[0..-4] if bt_assoc_name.end_with?('_id')
1031
1060
 
1032
1061
  bts = (relation = relations.fetch(fk[0], nil))&.fetch(:fks) { relation[:fks] = {} }
1062
+ # %%% Do we miss out on has_many :through or even HM based on constantizing this model early?
1063
+ # Maybe it's already gotten this info because we got as far as to say there was a unique class
1033
1064
  primary_table = (is_class = fk[2].is_a?(Hash) && fk[2].key?(:class)) ? (primary_class = fk[2][:class].constantize).table_name : fk[2]
1034
1065
  hms = (relation = relations.fetch(primary_table, nil))&.fetch(:fks) { relation[:fks] = {} } unless is_class
1035
1066
 
@@ -1046,7 +1077,7 @@ module Brick
1046
1077
  puts "Brick: Additional reference #{fk.inspect} refers to non-existent #{'table'.pluralize(missing.length)} #{missing.join(' and ')}. (Available tables include #{tables.join(', ')}.)"
1047
1078
  return
1048
1079
  end
1049
- unless (cols = relations[fk[0]][:cols]).key?(fk[1])
1080
+ unless (cols = relations[fk[0]][:cols]).key?(fk[1]) || (is_polymorphic && cols.key?("#{fk[1]}_id") && cols.key?("#{fk[1]}_type"))
1050
1081
  columns = cols.map { |k, v| "#{k} (#{v.first.split(' ').first})" }
1051
1082
  puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[1]}. (Columns present in #{fk[0]} are #{columns.join(', ')}.)"
1052
1083
  return
@@ -1061,10 +1092,21 @@ module Brick
1061
1092
  end
1062
1093
  end
1063
1094
  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]}"
1095
+ if is_polymorphic
1096
+ # Assuming same fk (don't yet support composite keys for polymorphics)
1097
+ assoc_bt[:inverse_table] << fk[2]
1098
+ else # Expect we could have a composite key going
1099
+ if assoc_bt[:fk].is_a?(String)
1100
+ assoc_bt[:fk] = [assoc_bt[:fk], fk[1]] unless fk[1] == assoc_bt[:fk]
1101
+ elsif assoc_bt[:fk].exclude?(fk[1])
1102
+ assoc_bt[:fk] << fk[1]
1103
+ end
1104
+ assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[1]}"
1105
+ end
1066
1106
  else
1067
- assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[1], assoc_name: bt_assoc_name, inverse_table: primary_table }
1107
+ inverse_table = [primary_table] if is_polymorphic
1108
+ assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[1], assoc_name: bt_assoc_name, inverse_table: inverse_table || primary_table }
1109
+ assoc_bt[:polymorphic] = true if is_polymorphic
1068
1110
  end
1069
1111
  if is_class
1070
1112
  # For use in finding the proper :source for a HMT association that references an STI subclass
@@ -1076,11 +1118,16 @@ module Brick
1076
1118
  return if is_class || ::Brick.config.exclude_hms&.any? { |exclusion| fk[0] == exclusion[0] && fk[1] == exclusion[1] && primary_table == exclusion[2] }
1077
1119
 
1078
1120
  if (assoc_hm = hms.fetch((hm_cnstr_name = "hm_#{cnstr_name}"), nil))
1079
- assoc_hm[:fk] = assoc_hm[:fk].is_a?(String) ? [assoc_hm[:fk], fk[1]] : assoc_hm[:fk].concat(fk[1])
1121
+ if assoc_bt[:fk].is_a?(String)
1122
+ assoc_bt[:fk] = [assoc_bt[:fk], fk[1]] unless fk[1] == assoc_bt[:fk]
1123
+ elsif assoc_bt[:fk].exclude?(fk[1])
1124
+ assoc_bt[:fk] << fk[1]
1125
+ end
1080
1126
  assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
1081
1127
  assoc_hm[:inverse] = assoc_bt
1082
1128
  else
1083
1129
  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 }
1130
+ assoc_hm[:polymorphic] = true if is_polymorphic
1084
1131
  hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
1085
1132
  hm_counts[fk[0]] = hm_counts.fetch(fk[0]) { 0 } + 1
1086
1133
  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}", pk)} }) %>\n"
111
116
  end
112
117
  s << hm_stuff
113
118
  end
@@ -205,7 +210,19 @@ 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
+ poly_cols = []
214
+ css << "<% bts = { #{
215
+ bts.each_with_object([]) do |v, s|
216
+ foreign_models = if v.last[2] # Polymorphic?
217
+ poly_cols << @_brick_model.reflect_on_association(v[1].first).foreign_type
218
+ v.last[1].each_with_object([]) { |x, s| s << "[#{x.name}, #{x.primary_key.inspect}]" }.join(', ')
219
+ else
220
+ "[#{v.last[1].name}, #{v.last[1].primary_key.inspect}]"
221
+ end
222
+ s << "#{v.first.inspect} => [#{v.last.first.inspect}, [#{foreign_models}], #{v.last[2].inspect}]"
223
+ end.join(', ')
224
+ } }
225
+ poly_cols = #{poly_cols.inspect} %>"
209
226
  end
210
227
 
211
228
  # %%% When doing schema select, if there's an ID then remove it, or if we're on a new page go to index
@@ -370,13 +387,16 @@ function changeout(href, param, value) {
370
387
  <table id=\"#{table_name}\">
371
388
  <thead><tr>#{'<th></th>' if pk}
372
389
  <% @#{table_name}.columns.map(&:name).each do |col| %>
373
- <% next if col == '#{pk}' || ::Brick.config.metadata_columns.include?(col) %>
390
+ <% next if col == '#{pk}' || ::Brick.config.metadata_columns.include?(col) || poly_cols.include?(col) %>
374
391
  <th>
375
392
  <% if (bt = bts[col]) %>
376
- BT <%= bt[1].bt_link(bt.first) %>
377
- <% else %>
378
- <%= col %>
379
- <% end %>
393
+ BT <%
394
+ bt[1].each do |bt_pair| %><%=
395
+ bt_pair.first.bt_link(bt.first) %> <%
396
+ end %><%
397
+ else %><%=
398
+ col %><%
399
+ end %>
380
400
  </th>
381
401
  <% end %>
382
402
  <%# 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 +408,24 @@ function changeout(href, param, value) {
388
408
  <tr>#{"
389
409
  <td><%= link_to '⇛', #{obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
390
410
  <% #{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')) %>
411
+ <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) || poly_cols.include?(k) || k.start_with?('_brfk_') || (k.start_with?('_br_') && (k.length == 63 || k.end_with?('_ct'))) %>
392
412
  <td>
393
413
  <% if (bt = bts[k]) %>
394
414
  <%# 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 %>
415
+ <% if bt[2] # Polymorphic?
416
+ bt_class = #{obj_name}.send(\"#\{bt.first\}_type\")
417
+ base_class = (::Brick.existing_stis[bt_class] || bt_class).constantize.base_class.name.underscore
418
+ poly_id = #{obj_name}.send(\"#\{bt.first\}_id\")
419
+ %><%= link_to(\"#\{bt_class\} ##\{poly_id\}\",
420
+ send(\"#\{base_class\}_path\".to_sym, poly_id)) if poly_id %><%
421
+ else
422
+ bt_txt = (bt_class = bt[1].first.first).brick_descrip(
423
+ #{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)
424
+ )
425
+ bt_id = #{obj_name}.send(*bt_id_col) if bt_id_col&.present? %>
426
+ <%= bt_id ? link_to(bt_txt, send(\"#\{bt_class.base_class.name.underscore\}_path\".to_sym, bt_id)) : bt_txt %>
427
+ <%#= 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 %>
428
+ <% end %>
399
429
  <% else %>
400
430
  <%= hide_bcrypt(val) %>
401
431
  <% end %>
@@ -414,41 +444,56 @@ function changeout(href, param, value) {
414
444
  <p style=\"color: green\"><%= notice %></p>#{"
415
445
  <select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
416
446
  <select id=\"tbl\">#{table_options}</select>
417
- <h1>#{model_name}: <%= (obj = @#{obj_name}&.first)&.brick_descrip || controller_name %></h1>
447
+ <h1>#{model_name}: <%= (obj = @#{obj_name})&.brick_descrip || controller_name %></h1>
418
448
  <%= link_to '(See all #{obj_name.pluralize})', #{table_name}_path %>
419
449
  <% if obj %>
420
450
  <%= # 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| %>
451
+ # path_options << { '_brick_schema': } if
452
+ # url = send(:#{model_name.underscore}_path, obj.#{pk})
453
+ form_for(obj.becomes(#{model_name})) do |f| %>
424
454
  <table>
425
455
  <% has_fields = false
426
- @#{obj_name}.first.attributes.each do |k, val| %>
456
+ @#{obj_name}.attributes.each do |k, val| %>
427
457
  <tr>
428
458
  <%# %%% Accommodate composite keys %>
429
459
  <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
430
460
  <th class=\"show-field\">
431
461
  <% has_fields = true
432
462
  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) %>
463
+ # Add a final member in this array with descriptive options to be used in <select> drop-downs
464
+ bt_name = bt[1].map { |x| x.first.name }.join('/')
465
+ # %%% Only do this if the user has permissions to edit this bt field
466
+ if bt[2] # Polymorphic?
467
+ poly_class_name = @#{obj_name}.send(\"#\{bt.first\}_type\")
468
+ bt_pair = nil
469
+ loop do
470
+ bt_pair = bt[1].find { |pair| pair.first.name == poly_class_name }
471
+ # Acxommodate any valid STI by going up the chain of inheritance
472
+ break unless bt_pair.nil? && poly_class_name = ::Brick.existing_stis[poly_class_name]
473
+ end
474
+ # descrips = @_brick_bt_descrip[bt.first][bt_class]
475
+ poly_id = @#{obj_name}.send(\"#\{bt.first\}_id\")
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
+ else # No polymorphism, so just get the first one
478
+ bt_pair = bt[1].first
479
+ end
480
+ bt_class = bt_pair.first
481
+ if bt.length < 4
482
+ bt << (option_detail = [[\"(No #\{bt_name\} chosen)\", '^^^brick_NULL^^^']])
483
+ # %%% Accommodate composite keys for obj.pk at the end here
484
+ bt_class.order(obj_pk = bt_class.primary_key).each { |obj| option_detail << [obj.brick_descrip(nil, obj_pk), obj.send(obj_pk)] }
485
+ end %>
486
+ BT <%= bt_class.bt_link(bt.first) %>
442
487
  <% else %>
443
488
  <%= k %>
444
489
  <% end %>
445
490
  </th>
446
491
  <td>
447
- <% if (bt = bts[k]) # bt_obj.brick_descrip
492
+ <% if bt
448
493
  html_options = { prompt: \"Select #\{bt_name\}\" }
449
494
  html_options[:class] = 'dimmed' unless val %>
450
495
  <%= 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 %>
496
+ <%= bt_obj = bt_class.find_by(bt_pair[1] => val); link_to('⇛', send(\"#\{bt_class.base_class.name.underscore\}_path\".to_sym, bt_obj.send(bt_class.primary_key.to_sym)), { class: 'show-arrow' }) if bt_obj %>
452
497
  <% else case #{model_name}.column_for_attribute(k).type
453
498
  when :string, :text %>
454
499
  <% if is_bcrypt?(val) # || .readonly? %>
@@ -469,27 +514,28 @@ function changeout(href, param, value) {
469
514
  <% end %>
470
515
  </td>
471
516
  </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 %>
517
+ <% end
518
+ if has_fields %>
519
+ <tr><td colspan=\"2\" class=\"right\"><%= f.submit %></td></tr>
520
+ <% else %>
521
+ <tr><td colspan=\"2\">(No displayable fields)</td></tr>
522
+ <% end %>
478
523
  </table>
479
524
  <% end %>
480
525
 
481
526
  #{hms_headers.each_with_object(+'') do |hm, s|
482
527
  if (pk = hm.first.klass.primary_key)
483
- s << "<table id=\"#{hm_name = hm.first.name.to_s}\">
528
+ hm_singular_name = (hm_name = hm.first.name.to_s).singularize.underscore
529
+ obj_pk = (pk.is_a?(Array) ? pk : [pk]).each_with_object([]) { |pk_part, s| s << "#{hm_singular_name}.#{pk_part}" }.join(', ')
530
+ s << "<table id=\"#{hm_name}\">
484
531
  <tr><th>#{hm[3]}</th></tr>
485
- <% collection = @#{obj_name}.first.#{hm_name}
532
+ <% collection = @#{obj_name}.#{hm_name}
486
533
  collection = collection.is_a?(ActiveRecord::Associations::CollectionProxy) ? collection.order(#{pk.inspect}) : [collection]
487
534
  if collection.empty? %>
488
535
  <tr><td>(none)</td></tr>
489
536
  <% else %>
490
- <% collection.uniq.each do |#{hm_singular_name = hm_name.singularize.underscore}| %>
491
- <%# %%% accommodate composite primary key %>
492
- <tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.name.underscore}_path(#{hm_singular_name}.#{pk})) %></td></tr>
537
+ <% collection.uniq.each do |#{hm_singular_name}| %>
538
+ <tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.name.underscore}_path([#{obj_pk}])) %></td></tr>
493
539
  <% end %>
494
540
  <% end %>
495
541
  </table>"
@@ -517,7 +563,7 @@ function changeout(href, param, value) {
517
563
  end
518
564
 
519
565
  # 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).
566
+ # go make sure we've loaded additional references (virtual foreign keys and polymorphic associations).
521
567
  ::Brick.load_additional_references
522
568
  end
523
569
  end
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 24
8
+ TINY = 27
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
@@ -85,12 +85,16 @@ module Brick
85
85
  @sti_models ||= {}
86
86
  end
87
87
 
88
+ def self.existing_stis
89
+ @existing_stis ||= Brick.config.sti_namespace_prefixes.each_with_object({}) { |snp, s| s[snp.first[2..-1]] = snp.last unless snp.first.end_with?('::') }
90
+ end
91
+
88
92
  class << self
89
93
  attr_accessor :db_schemas
90
94
 
91
95
  def set_db_schema(params)
92
96
  schema = params['_brick_schema'] || 'public'
93
- ActiveRecord::Base.connection.execute("SET SEARCH_PATH='#{schema}';") if schema && ::Brick.db_schemas&.include?(schema)
97
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema) if schema && ::Brick.db_schemas&.include?(schema)
94
98
  end
95
99
 
96
100
  # All tables and views (what Postgres calls "relations" including column and foreign key info)
@@ -107,7 +111,13 @@ module Brick
107
111
 
108
112
  case a.macro
109
113
  when :belongs_to
110
- s.first[a.foreign_key] = [a.name, a.klass]
114
+ s.first[a.foreign_key] = if a.polymorphic?
115
+ primary_tables = relations[model.table_name][:fks].find { |_k, fk| fk[:assoc_name] == a.name.to_s }&.last&.fetch(:inverse_table, [])
116
+ models = primary_tables&.map { |table| table.singularize.camelize.constantize }
117
+ [a.name, models, true]
118
+ else
119
+ [a.name, a.klass]
120
+ end
111
121
  when :has_many, :has_one # This gets has_many as well as has_many :through
112
122
  # %%% weed out ones that don't have an available model to reference
113
123
  s.last[a.name] = a
@@ -256,6 +266,12 @@ module Brick
256
266
  end
257
267
  end
258
268
 
269
+ # Polymorphic associations
270
+ def polymorphics=(polys)
271
+ polys = polys.each_with_object({}) { |poly, s| s[poly] = nil } if polys.is_a?(Array)
272
+ Brick.config.polymorphics = polys || {}
273
+ end
274
+
259
275
  # DSL templates for individual models to provide prettier descriptions of objects
260
276
  # @api public
261
277
  def model_descrips=(descrips)
@@ -268,14 +284,52 @@ module Brick
268
284
  Brick.config.sti_namespace_prefixes = snp
269
285
  end
270
286
 
287
+ # Database schema to use when analysing existing data, such as deriving a list of polymorphic classes
288
+ # for polymorphics in which it wasn't originally specified.
289
+ # @api public
290
+ def schema_to_analyse=(schema)
291
+ Brick.config.schema_to_analyse = schema
292
+ end
293
+
294
+ def default_route_fallback=(resource_name)
295
+ Brick.config.default_route_fallback = resource_name
296
+ end
297
+
271
298
  # Load additional references (virtual foreign keys)
272
299
  # This is attempted early if a brick initialiser file is found, and then again as a failsafe at the end of our engine's initialisation
273
300
  # %%% Maybe look for differences the second time 'round and just add new stuff instead of entirely deferring
274
301
  def load_additional_references
275
302
  return if @_additional_references_loaded
276
303
 
277
- if (ars = ::Brick.config.additional_references)
278
- ars.each { |fk| ::Brick._add_bt_and_hm(fk[0..2]) }
304
+ relations = ::Brick.relations
305
+ if (ars = ::Brick.config.additional_references) || ::Brick.config.polymorphics
306
+ ars.each { |fk| ::Brick._add_bt_and_hm(fk[0..2], relations) } if ars
307
+ if (polys = ::Brick.config.polymorphics)
308
+ if (schema = ::Brick.config.schema_to_analyse) && ::Brick.db_schemas&.include?(schema)
309
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
310
+ end
311
+ missing_stis = {}
312
+ polys.each do |k, v|
313
+ table_name, poly = k.split('.')
314
+ v ||= ActiveRecord::Base.execute_sql("SELECT DISTINCT #{poly}_type AS typ FROM #{table_name}").map { |result| result['typ'] }
315
+ v.each do |type|
316
+ if relations.key?(primary_table = type.underscore.pluralize)
317
+ ::Brick._add_bt_and_hm([table_name, poly, primary_table, "(brick) #{table_name}_#{poly}"], relations, true)
318
+ else
319
+ missing_stis[primary_table] = type unless ::Brick.existing_stis.key?(type)
320
+ end
321
+ end
322
+ end
323
+ unless missing_stis.empty?
324
+ print "
325
+ You might be missing an STI namespace prefix entry for these tables: #{missing_stis.keys.join(', ')}.
326
+ In config/initializers/brick.rb appropriate entries would look something like:
327
+ Brick.sti_namespace_prefixes = {"
328
+ puts missing_stis.map { |_k, missing_sti| "\n '::#{missing_sti}' => 'YourParentModel'" }.join(',')
329
+ puts " }
330
+ (Just trade out YourParentModel with some more appropriate one.)"
331
+ end
332
+ end
279
333
  @_additional_references_loaded = true
280
334
  end
281
335
 
@@ -331,6 +385,9 @@ module Brick
331
385
  def finalize!
332
386
  existing_controllers = routes.each_with_object({}) { |r, s| c = r.defaults[:controller]; s[c] = nil if c }
333
387
  ::Rails.application.routes.append do
388
+ unless ::Brick.config.default_route_fallback.blank? || ::Rails.application.routes.named_routes.send(:routes)[:root]
389
+ send(:root, "#{::Brick.config.default_route_fallback}#index")
390
+ end
334
391
  # %%% TODO: If no auto-controllers then enumerate the controllers folder in order to build matching routes
335
392
  # If auto-controllers and auto-models are both enabled then this makes sense:
336
393
  ::Brick.relations.each do |k, v|
@@ -19,11 +19,31 @@ module Brick
19
19
 
20
20
  def create_initializer_file
21
21
  unless File.exist?(filename = 'config/initializers/brick.rb')
22
- # See if we can make suggestions for additional_references
23
- resembles_fks = []
24
- possible_additional_references = (relations = ::Brick.relations).each_with_object([]) do |v, s|
25
- v.last[:cols].each do |col, _type|
22
+ # See if we can make suggestions for additional_references and polymorphic associations
23
+ resembles_fks = Hash.new { |h, k| h[k] = [] }
24
+ possible_polymorphics = {}
25
+ possible_additional_references = (relations = ::Brick.relations).each_with_object(Hash.new { |h, k| h[k] = [] }) do |v, s|
26
+ model_filename = "app/models/#{ActiveSupport::Inflector.singularize(v.first)}.rb"
27
+ v.last[:cols].each do |col, type|
26
28
  col_down = col.downcase
29
+
30
+ if (is_possible_poly = ['character varying', 'text'].include?(type.first))
31
+ if col_down.end_with?('_type') &&
32
+ poly_type_cut_length = -6
33
+ col_down = col_down[0..-6]
34
+ elsif col_down.end_with?('type')
35
+ poly_type_cut_length = -5
36
+ col_down = col_down[0..-5]
37
+ else
38
+ is_possible_poly = false
39
+ end
40
+ is_possible_poly = false if col_down.length < 6 # Was it simply called "type" or something else really short?
41
+ if is_possible_poly && !File.exist?(model_filename) # Make sure a model file isn't present
42
+ possible_polymorphics["#{v.first}.#{col_down}"] = "'#{v.first}.#{col[0..poly_type_cut_length]}'"
43
+ next
44
+ end
45
+ end
46
+
27
47
  is_possible = true
28
48
  if col_down.end_with?('_id')
29
49
  col_down = col_down[0..-4]
@@ -40,30 +60,48 @@ module Brick
40
60
  if col_down.start_with?('fk_')
41
61
  is_possible = true
42
62
  col_down = col_down[3..-1]
63
+ elsif col_down.start_with?('fk')
64
+ is_possible = true
65
+ col_down = col_down[2..-1]
43
66
  end
44
67
  # This possible key not really a primary key and not yet used as a foreign key?
45
68
  if is_possible && !(relation = relations.fetch(v.first, {}))[:pkey].first&.last&.include?(col) &&
46
69
  !relations.fetch(v.first, {})[:fks]&.any? { |_k, v| v[:is_bt] && v[:fk] == col }
47
- if (relations.fetch(f_table = col_down, nil) ||
48
- relations.fetch(f_table = ActiveSupport::Inflector.pluralize(col_down), nil)) &&
49
- # Looks pretty promising ... just make sure a model file isn't present
50
- !File.exist?("app/models/#{ActiveSupport::Inflector.singularize(v.first)}.rb")
51
- s << "['#{v.first}', '#{col}', '#{f_table}']"
52
- else
53
- resembles_fks << "#{v.first}.#{col}"
70
+ # Starting to look promising ... make sure a model file isn't present
71
+ if !File.exist?(model_filename)
72
+ if (relations.fetch(f_table = col_down, nil) ||
73
+ relations.fetch(f_table = ActiveSupport::Inflector.pluralize(col_down), nil)) &&
74
+ s["#{v.first}.#{col_down}"] << "['#{v.first}', '#{col}', '#{f_table}']"
75
+ else
76
+ resembles_fks["#{v.first}.#{col_down}"] << "#{v.first}.#{col}"
77
+ end
54
78
  end
55
79
  end
56
80
  end
57
- s
58
81
  end
59
82
 
60
- bar = case possible_additional_references.length
83
+ possible_polymorphics.each_key do |k|
84
+ # Also matching one of the FK suggestions means it could be polymorphic,
85
+ # so delete any suggestions for a FK of the same name and only recommend
86
+ # the polymorphic association.
87
+ if resembles_fks.key?(k)
88
+ resembles_fks.delete(k)
89
+ elsif possible_additional_references.key?(k)
90
+ possible_additional_references.delete(k)
91
+ else
92
+ # While this one has a type, it's missing a corresponding ID column so it isn't polymorphic
93
+ possible_polymorphics.delete(k)
94
+ end
95
+ end
96
+ resembles_fks = resembles_fks.values.flatten
97
+
98
+ bar = case (possible_additional_references = possible_additional_references.values.flatten).length
61
99
  when 0
62
100
  +"# Brick.additional_references = [['orders', 'customer_id', 'customer'],
63
101
  # ['customer', 'region_id', 'regions']]"
64
102
  when 1
65
103
  +"# # Here is a possible additional reference that has been auto-identified for the #{ActiveRecord::Base.connection.current_database} database:
66
- # Brick.additional_references = [[#{possible_additional_references.first}]"
104
+ # Brick.additional_references = [#{possible_additional_references.first}]"
67
105
  else
68
106
  +"# # Here are possible additional references that have been auto-identified for the #{ActiveRecord::Base.connection.current_database} database:
69
107
  # Brick.additional_references = [
@@ -75,6 +113,26 @@ module Brick
75
113
  # # #{resembles_fks.join(', ')}"
76
114
  end
77
115
 
116
+ poly = case (possible_polymorphics = possible_polymorphics.values.flatten).length
117
+ when 0
118
+ " like this:
119
+ # Brick.polymorphics = [
120
+ # 'comments.commentable',
121
+ # 'images.imageable'
122
+ # ]"
123
+ when 1
124
+ ".
125
+ # # Here is a possible polymorphic association that has been auto-identified for the #{ActiveRecord::Base.connection.current_database} database:
126
+ # Brick.polymorphics = [#{possible_additional_references.first}]"
127
+
128
+ else
129
+ ".
130
+ # # Here are possible polymorphic associations that have been auto-identified for the #{ActiveRecord::Base.connection.current_database} database:
131
+ # Brick.polymorphics = [
132
+ # #{possible_polymorphics.join(",\n# ")}
133
+ # ]"
134
+ end
135
+
78
136
  create_file(filename, "# frozen_string_literal: true
79
137
 
80
138
  # # Settings for the Brick gem
@@ -119,7 +177,7 @@ module Brick
119
177
  # # Skip showing counts for these specific has_many associations when building auto-generated #index views.
120
178
  # # When there are related tables with a significant number of records, this can lessen the load on the database
121
179
  # # considerably, sometimes fixing what might appear to be an index page that just \"hangs\" for no apparent reason.
122
- Brick.skip_index_hms = ['User.litany_of_woes']
180
+ # Brick.skip_index_hms = ['User.litany_of_woes']
123
181
 
124
182
  # # By default primary tables involved in a foreign key relationship will indicate a \"has_many\" relationship pointing
125
183
  # # back to the foreign table. In order to represent a \"has_one\" association instead, an override can be provided
@@ -157,6 +215,12 @@ Brick.skip_index_hms = ['User.litany_of_woes']
157
215
  # Brick.sti_namespace_prefixes = { '::Animals::' => 'Animal',
158
216
  # '::Snake' => 'Reptile' }
159
217
 
218
+ # # Database schema to use when analysing existing data, such as deriving a list of polymorphic classes in the case that
219
+ # # it wasn't originally specified.
220
+ # Brick.schema_to_analyse = 'engineering'
221
+
222
+ # # Polymorphic associations are set up by providing a model name and polymorphic association name#{poly}
223
+
160
224
  # # If a default route is not supplied, Brick attempts to find the most \"central\" table and wires up the default
161
225
  # # route to go to the :index action for what would be a controller for that table. You can specify any controller
162
226
  # # 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.27
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-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord