brick 1.0.23 → 1.0.26

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: 24731ac9bd56261179224610406247cc4438c61e411aa3fe96205b2e97fc8cb8
4
- data.tar.gz: 785d12e36fcf788c05cff3a02ca2ec17fc418b0ea15b54cb6923c83c6f362d3b
3
+ metadata.gz: 922a55ec392e2ea9cef11c7297e4c2e4883e918130a23e49102b5a7e2442140e
4
+ data.tar.gz: 7cda213ec2e7cdccae096ef4ad8091a53a91359a1e6fabeb753cc2b579ac5c16
5
5
  SHA512:
6
- metadata.gz: '090a3c4d1b3cb4761799e05adf5caa0c83f60cf985f97ed6856beb05620014e9e7401689719209488092c6c4a7605e53781cdb71ad9e7ec204ccaddca0d92cca'
7
- data.tar.gz: 8d1f9512c948359e7d73cd7cc32302f226df7e3e744fab562e01e16170830be28d0e2e15bcd7b437f66ed79e366b0ff33b7f578e4c76ffde1dd62cdcec059069
6
+ metadata.gz: 7b817257f9c2aedd47bc2471b12e13edc8b370b71920258755f0f9a7594060e065d022e8d4b2b360cdc41f2a60def02cfc406889d526d80088670bd64acd58be
7
+ data.tar.gz: 1ea923301103dcc53853940770861ab5c2635f1280026e0740287274c0e9bb7d566ddcf96db0d3858467b788bc9ee4b73e7bcfc61145be1bbb43775cfb859b80
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,14 @@ 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
+
116
133
  def skip_database_views
117
134
  @mutex.synchronize { @skip_database_views }
118
135
  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
@@ -115,8 +119,8 @@ module ActiveRecord
115
119
  # If available, parse simple DSL attached to a model in order to provide a friendlier name.
116
120
  # Object property names can be referenced in square brackets like this:
117
121
  # { 'User' => '[profile.firstname] [profile.lastname]' }
118
- def brick_descrip
119
- self.class.brick_descrip(self)
122
+ def brick_descrip(data = nil, pk_alias = nil)
123
+ self.class.brick_descrip(self, data, pk_alias)
120
124
  end
121
125
 
122
126
  def self.brick_descrip(obj, data = nil, pk_alias = nil)
@@ -136,11 +140,7 @@ module ActiveRecord
136
140
  this_obj = obj
137
141
  bracket_name.split('.').each do |part|
138
142
  obj_name += ".#{part}"
139
- this_obj = if caches.key?(obj_name)
140
- caches[obj_name]
141
- else
142
- (caches[obj_name] = this_obj&.send(part.to_sym))
143
- end
143
+ this_obj = caches.fetch(obj_name) { caches[obj_name] = this_obj&.send(part.to_sym) }
144
144
  end
145
145
  this_obj&.to_s || ''
146
146
  end
@@ -160,7 +160,8 @@ module ActiveRecord
160
160
  end
161
161
  if is_brackets_have_content
162
162
  output
163
- elsif pk_alias
163
+ elsif (pk_alias ||= primary_key)
164
+ pk_alias = [pk_alias] unless pk_alias.is_a?(Array)
164
165
  id = []
165
166
  pk_alias.each do |pk_alias_part|
166
167
  if (pk_part = obj.send(pk_alias_part))
@@ -194,7 +195,10 @@ module ActiveRecord
194
195
  private
195
196
 
196
197
  def self._brick_get_fks
197
- @_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
198
202
  end
199
203
  end
200
204
 
@@ -295,8 +299,14 @@ module ActiveRecord
295
299
  if is_add_bts || is_add_hms
296
300
  bts, hms, associatives = ::Brick.get_bts_and_hms(klass)
297
301
  bts.each do |_k, bt|
302
+ next if bt[2] # Polymorphic?
303
+
298
304
  # join_array will receive this relation name when calling #brick_parse_dsl
299
- 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
300
310
  end
301
311
  skip_klass_hms = ::Brick.config.skip_index_hms[klass.name] || {}
302
312
  hms.each do |k, hm|
@@ -314,7 +324,7 @@ module ActiveRecord
314
324
  when 2
315
325
  assoc_name = ks.first.to_sym
316
326
  # Make sure it's a good association name and that the model has that column name
317
- 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)
318
328
 
319
329
  join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
320
330
  end
@@ -330,22 +340,31 @@ module ActiveRecord
330
340
  id_for_tables = Hash.new { |h, k| h[k] = [] }
331
341
  field_tbl_names = Hash.new { |h, k| h[k] = {} }
332
342
  bt_columns = bt_descrip.each_with_object([]) do |v, s|
333
- tbl_name = field_tbl_names[v.first][v.last.first] ||= shift_or_first(chains[v.last.first])
334
- if (id_col = v.last.first.primary_key) && !id_for_tables.key?(v.first) # was tbl_name
335
- # Accommodate composite primary key by allowing id_col to come in as an array
336
- (id_col.is_a?(Array) ? id_col : [id_col]).each do |id_part|
337
- selects << "#{"#{tbl_name}.#{id_part}"} AS \"#{(id_alias = "_brfk_#{v.first}__#{id_part}")}\""
338
- id_for_tables[v.first] << id_alias
339
- end
340
- v.last << id_for_tables[v.first]
341
- end
342
- 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
343
348
  field_tbl_name = nil
344
- 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|
345
352
  field_tbl_name ||= field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])
346
- # col_name is weak when there are multiple, using sel_col.last instead
353
+
347
354
  selects << "#{"#{field_tbl_name}.#{sel_col.last}"} AS \"#{(col_alias = "_brfk_#{v.first}__#{sel_col.last}")}\""
348
- 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
349
368
  end
350
369
  end
351
370
  end
@@ -365,21 +384,26 @@ module ActiveRecord
365
384
  hm.foreign_key
366
385
  else
367
386
  fk_col = hm.foreign_key
387
+ poly_type = hm.inverse_of.foreign_type if hm.options.key?(:as)
368
388
  hm.klass.primary_key || '*'
369
389
  end
370
390
  tbl_alias = "_br_#{hm.name}"
371
391
  pri_tbl = hm.active_record
392
+ on_clause = []
372
393
  if fk_col.is_a?(Array) # Composite key?
373
- on_clause = []
374
394
  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]}" }
375
- joins!("LEFT OUTER
376
- 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}
377
- ON #{on_clause.join(' AND ')}")
395
+ selects = fk_col.dup
378
396
  else
379
- joins!("LEFT OUTER
380
- JOIN (SELECT #{fk_col}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.table_name || hm.klass.table_name} GROUP BY 1) AS #{tbl_alias}
381
- ON #{tbl_alias}.#{fk_col} = #{pri_tbl.table_name}.#{pri_tbl.primary_key}")
397
+ selects = [fk_col]
398
+ on_clause << "#{tbl_alias}.#{fk_col} = #{pri_tbl.table_name}.#{pri_tbl.primary_key}"
399
+ end
400
+ if poly_type
401
+ selects << poly_type
402
+ on_clause << "#{tbl_alias}.#{poly_type} = '#{name}'"
382
403
  end
404
+ join_clause = "LEFT OUTER
405
+ 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}"
406
+ joins!("#{join_clause} ON #{on_clause.join(' AND ')}")
383
407
  end
384
408
  where!(wheres) unless wheres.empty?
385
409
  wheres unless wheres.empty? # Return the specific parameters that we did use
@@ -424,8 +448,7 @@ JOIN (SELECT #{fk_col}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.table
424
448
  this_module.const_get(class_name)
425
449
  else
426
450
  # Build STI subclass and place it into the namespace module
427
- # %%% Does this ever get used???
428
- puts [this_module.const_set(class_name, klass = Class.new(self)).name, class_name].inspect
451
+ this_module.const_set(class_name, klass = Class.new(self))
429
452
  klass
430
453
  end
431
454
  end
@@ -476,7 +499,7 @@ class Object
476
499
  # path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
477
500
  # return self._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
478
501
  # If the file really exists, go and snag it:
479
- if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = self.name&.split('::'))
502
+ if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = (self.name || class_name)&.split('::'))
480
503
  filepath = (filepath[0..-2] + [class_name]).join('/').underscore + '.rb'
481
504
  end
482
505
  if is_found
@@ -500,7 +523,7 @@ class Object
500
523
  singular_table_name = ActiveSupport::Inflector.underscore(model_name)
501
524
 
502
525
  # Adjust for STI if we know of a base model for the requested model name
503
- table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil))
526
+ table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, ::Brick.existing_stis[model_name]&.constantize))
504
527
  base_model.table_name
505
528
  else
506
529
  ActiveSupport::Inflector.pluralize(singular_table_name)
@@ -541,7 +564,7 @@ class Object
541
564
  return
542
565
  end
543
566
 
544
- if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil))
567
+ if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, ::Brick.existing_stis[model_name]&.constantize))
545
568
  is_sti = true
546
569
  else
547
570
  base_model = ::Brick.config.models_inherit_from || ActiveRecord::Base
@@ -589,77 +612,11 @@ class Object
589
612
  hmts = fks.each_with_object(Hash.new { |h, k| h[k] = [] }) do |fk, hmts|
590
613
  # The key in each hash entry (fk.first) is the constraint name
591
614
  inverse_assoc_name = (assoc = fk.last)[:inverse]&.fetch(:assoc_name, nil)
592
- options = {}
593
- singular_table_name = ActiveSupport::Inflector.singularize(assoc[:inverse_table])
594
- macro = if assoc[:is_bt]
595
- # Try to take care of screwy names if this is a belongs_to going to an STI subclass
596
- assoc_name = if (primary_class = assoc.fetch(:primary_class, nil)) &&
597
- sti_inverse_assoc = primary_class.reflect_on_all_associations.find do |a|
598
- a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key
599
- end
600
- sti_inverse_assoc.options[:inverse_of]&.to_s || assoc_name
601
- else
602
- assoc[:assoc_name]
603
- end
604
- need_class_name = singular_table_name.underscore != assoc_name
605
- need_fk = "#{assoc_name}_id" != assoc[:fk]
606
- if (inverse = assoc[:inverse])
607
- inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[assoc[:inverse_table]], inverse)
608
- if (has_ones = ::Brick.config.has_ones&.fetch(inverse[:alternate_name].camelize, nil))&.key?(singular_inv_assoc_name = ActiveSupport::Inflector.singularize(inverse_assoc_name))
609
- inverse_assoc_name = if has_ones[singular_inv_assoc_name]
610
- need_inverse_of = true
611
- has_ones[singular_inv_assoc_name]
612
- else
613
- singular_inv_assoc_name
614
- end
615
- end
616
- end
617
- :belongs_to
618
- else
619
- # need_class_name = ActiveSupport::Inflector.singularize(assoc_name) == ActiveSupport::Inflector.singularize(table_name.underscore)
620
- # Are there multiple foreign keys out to the same table?
621
- assoc_name, need_class_name = _brick_get_hm_assoc_name(relation, assoc)
622
- need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
623
- # fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
624
- if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
625
- assoc_name = if (custom_assoc_name = has_ones[singular_assoc_name])
626
- need_class_name = custom_assoc_name != singular_assoc_name
627
- custom_assoc_name
628
- else
629
- singular_assoc_name
630
- end
631
- :has_one
632
- else
633
- :has_many
634
- end
635
- end
636
- # Figure out if we need to specially call out the class_name and/or foreign key
637
- # (and if either of those then definitely also a specific inverse_of)
638
- options[:class_name] = assoc[:primary_class]&.name || singular_table_name.camelize if need_class_name
639
- # Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
640
- if need_fk # Funky foreign key?
641
- options[:foreign_key] = if assoc[:fk].is_a?(Array)
642
- assoc_fk = assoc[:fk].uniq
643
- assoc_fk.length < 2 ? assoc_fk.first : assoc_fk
644
- else
645
- assoc[:fk].to_sym
646
- end
647
- end
648
- options[:inverse_of] = inverse_assoc_name.to_sym if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of)
649
-
650
- # Prepare a list of entries for "has_many :through"
651
- if macro == :has_many
652
- relations[assoc[:inverse_table]][:hmt_fks].each do |k, hmt_fk|
653
- next if k == assoc[:fk]
654
-
655
- hmts[ActiveSupport::Inflector.pluralize(hmt_fk.last)] << [assoc, hmt_fk.first]
656
- end
615
+ if (invs = assoc[:inverse_table]).is_a?(Array)
616
+ invs.each { |inv| build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, inv, code) }
617
+ else
618
+ build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, invs, code)
657
619
  end
658
-
659
- # And finally create a has_one, has_many, or belongs_to for this association
660
- assoc_name = assoc_name.to_sym
661
- code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
662
- self.send(macro, assoc_name, **options)
663
620
  hmts
664
621
  end
665
622
  hmts.each do |hmt_fk, fks|
@@ -698,6 +655,88 @@ class Object
698
655
  [built_model, code]
699
656
  end
700
657
 
658
+ def build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, inverse_table, code)
659
+ singular_table_name = inverse_table&.singularize
660
+ options = {}
661
+ macro = if assoc[:is_bt]
662
+ # Try to take care of screwy names if this is a belongs_to going to an STI subclass
663
+ assoc_name = if (primary_class = assoc.fetch(:primary_class, nil)) &&
664
+ sti_inverse_assoc = primary_class.reflect_on_all_associations.find do |a|
665
+ a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key
666
+ end
667
+ sti_inverse_assoc.options[:inverse_of]&.to_s || assoc_name
668
+ else
669
+ assoc[:assoc_name]
670
+ end
671
+ if assoc.key?(:polymorphic)
672
+ options[:polymorphic] = true
673
+ else
674
+ need_class_name = singular_table_name.underscore != assoc_name
675
+ need_fk = "#{assoc_name}_id" != assoc[:fk]
676
+ end
677
+ if (inverse = assoc[:inverse])
678
+ inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[inverse_table], inverse)
679
+ has_ones = ::Brick.config.has_ones&.fetch(inverse[:alternate_name].camelize, nil)
680
+ if has_ones&.key?(singular_inv_assoc_name = ActiveSupport::Inflector.singularize(inverse_assoc_name))
681
+ inverse_assoc_name = if has_ones[singular_inv_assoc_name]
682
+ need_inverse_of = true
683
+ has_ones[singular_inv_assoc_name]
684
+ else
685
+ singular_inv_assoc_name
686
+ end
687
+ end
688
+ end
689
+ :belongs_to
690
+ else
691
+ # need_class_name = ActiveSupport::Inflector.singularize(assoc_name) == ActiveSupport::Inflector.singularize(table_name.underscore)
692
+ # Are there multiple foreign keys out to the same table?
693
+ assoc_name, need_class_name = _brick_get_hm_assoc_name(relation, assoc)
694
+ if assoc.key?(:polymorphic)
695
+ options[:as] = assoc[:fk].to_sym
696
+ else
697
+ need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
698
+ end
699
+ # fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
700
+ if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
701
+ assoc_name = if (custom_assoc_name = has_ones[singular_assoc_name])
702
+ need_class_name = custom_assoc_name != singular_assoc_name
703
+ custom_assoc_name
704
+ else
705
+ singular_assoc_name
706
+ end
707
+ :has_one
708
+ else
709
+ :has_many
710
+ end
711
+ end
712
+ # Figure out if we need to specially call out the class_name and/or foreign key
713
+ # (and if either of those then definitely also a specific inverse_of)
714
+ options[:class_name] = assoc[:primary_class]&.name || singular_table_name.camelize if need_class_name
715
+ # Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
716
+ if need_fk # Funky foreign key?
717
+ options[:foreign_key] = if assoc[:fk].is_a?(Array)
718
+ assoc_fk = assoc[:fk].uniq
719
+ assoc_fk.length < 2 ? assoc_fk.first : assoc_fk
720
+ else
721
+ assoc[:fk].to_sym
722
+ end
723
+ end
724
+ options[:inverse_of] = inverse_assoc_name.to_sym if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of)
725
+
726
+ # Prepare a list of entries for "has_many :through"
727
+ if macro == :has_many
728
+ relations[inverse_table][:hmt_fks].each do |k, hmt_fk|
729
+ next if k == assoc[:fk]
730
+
731
+ hmts[ActiveSupport::Inflector.pluralize(hmt_fk.last)] << [assoc, hmt_fk.first]
732
+ end
733
+ end
734
+ # And finally create a has_one, has_many, or belongs_to for this association
735
+ assoc_name = assoc_name.to_sym
736
+ code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
737
+ self.send(macro, assoc_name, **options)
738
+ end
739
+
701
740
  def build_controller(class_name, plural_class_name, model, relations)
702
741
  table_name = ActiveSupport::Inflector.underscore(plural_class_name)
703
742
  singular_table_name = ActiveSupport::Inflector.singularize(table_name)
@@ -1010,12 +1049,13 @@ module Brick
1010
1049
  # rubocop:enable Style/CommentedKeyword
1011
1050
 
1012
1051
  class << self
1013
- def _add_bt_and_hm(fk, relations = nil)
1014
- relations ||= ::Brick.relations
1052
+ def _add_bt_and_hm(fk, relations, is_polymorphic = false)
1015
1053
  bt_assoc_name = fk[1].underscore
1016
1054
  bt_assoc_name = bt_assoc_name[0..-4] if bt_assoc_name.end_with?('_id')
1017
1055
 
1018
1056
  bts = (relation = relations.fetch(fk[0], nil))&.fetch(:fks) { relation[:fks] = {} }
1057
+ # %%% Do we miss out on has_many :through or even HM based on constantizing this model early?
1058
+ # Maybe it's already gotten this info because we got as far as to say there was a unique class
1019
1059
  primary_table = (is_class = fk[2].is_a?(Hash) && fk[2].key?(:class)) ? (primary_class = fk[2][:class].constantize).table_name : fk[2]
1020
1060
  hms = (relation = relations.fetch(primary_table, nil))&.fetch(:fks) { relation[:fks] = {} } unless is_class
1021
1061
 
@@ -1032,7 +1072,7 @@ module Brick
1032
1072
  puts "Brick: Additional reference #{fk.inspect} refers to non-existent #{'table'.pluralize(missing.length)} #{missing.join(' and ')}. (Available tables include #{tables.join(', ')}.)"
1033
1073
  return
1034
1074
  end
1035
- unless (cols = relations[fk[0]][:cols]).key?(fk[1])
1075
+ unless (cols = relations[fk[0]][:cols]).key?(fk[1]) || (is_polymorphic && cols.key?("#{fk[1]}_id") && cols.key?("#{fk[1]}_type"))
1036
1076
  columns = cols.map { |k, v| "#{k} (#{v.first.split(' ').first})" }
1037
1077
  puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[1]}. (Columns present in #{fk[0]} are #{columns.join(', ')}.)"
1038
1078
  return
@@ -1047,10 +1087,21 @@ module Brick
1047
1087
  end
1048
1088
  end
1049
1089
  if (assoc_bt = bts[cnstr_name])
1050
- assoc_bt[:fk] = assoc_bt[:fk].is_a?(String) ? [assoc_bt[:fk], fk[1]] : assoc_bt[:fk].concat(fk[1])
1051
- assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[1]}"
1090
+ if is_polymorphic
1091
+ # Assuming same fk (don't yet support composite keys for polymorphics)
1092
+ assoc_bt[:inverse_table] << fk[2]
1093
+ else # Expect we could have a composite key going
1094
+ if assoc_bt[:fk].is_a?(String)
1095
+ assoc_bt[:fk] = [assoc_bt[:fk], fk[1]] unless fk[1] == assoc_bt[:fk]
1096
+ elsif assoc_bt[:fk].exclude?(fk[1])
1097
+ assoc_bt[:fk] << fk[1]
1098
+ end
1099
+ assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[1]}"
1100
+ end
1052
1101
  else
1053
- assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[1], assoc_name: bt_assoc_name, inverse_table: primary_table }
1102
+ inverse_table = [primary_table] if is_polymorphic
1103
+ assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[1], assoc_name: bt_assoc_name, inverse_table: inverse_table || primary_table }
1104
+ assoc_bt[:polymorphic] = true if is_polymorphic
1054
1105
  end
1055
1106
  if is_class
1056
1107
  # For use in finding the proper :source for a HMT association that references an STI subclass
@@ -1062,11 +1113,16 @@ module Brick
1062
1113
  return if is_class || ::Brick.config.exclude_hms&.any? { |exclusion| fk[0] == exclusion[0] && fk[1] == exclusion[1] && primary_table == exclusion[2] }
1063
1114
 
1064
1115
  if (assoc_hm = hms.fetch((hm_cnstr_name = "hm_#{cnstr_name}"), nil))
1065
- assoc_hm[:fk] = assoc_hm[:fk].is_a?(String) ? [assoc_hm[:fk], fk[1]] : assoc_hm[:fk].concat(fk[1])
1116
+ if assoc_bt[:fk].is_a?(String)
1117
+ assoc_bt[:fk] = [assoc_bt[:fk], fk[1]] unless fk[1] == assoc_bt[:fk]
1118
+ elsif assoc_bt[:fk].exclude?(fk[1])
1119
+ assoc_bt[:fk] << fk[1]
1120
+ end
1066
1121
  assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
1067
1122
  assoc_hm[:inverse] = assoc_bt
1068
1123
  else
1069
1124
  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 }
1125
+ assoc_hm[:polymorphic] = true if is_polymorphic
1070
1126
  hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
1071
1127
  hm_counts[fk[0]] = hm_counts.fetch(fk[0]) { 0 } + 1
1072
1128
  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,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
@@ -274,108 +291,112 @@ function changeout(href, param, value) {
274
291
  template_link = "
275
292
  <%= link_to 'CSV', #{table_name}_path(format: :csv) %> &nbsp; <a href=\"#\" id=\"sheetsLink\">Sheets</a>
276
293
  <div id=\"dropper\" contenteditable=\"true\"></div>
277
- <input type=\"button\" id=\"btnImport\" value=\"Import\">"
278
- end
279
- "#{css}
280
- <p style=\"color: green\"><%= notice %></p>#{"
281
- <select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
282
- <select id=\"tbl\">#{table_options}</select>
283
- <h1>#{model_name.pluralize}</h1>#{template_link}
294
+ <input type=\"button\" id=\"btnImport\" value=\"Import\">
295
+
284
296
  <script>
285
- var dropperDiv = document.getElementById(\"dropper\");
286
- var btnImport = document.getElementById(\"btnImport\");
287
- var droppedTSV;
288
- if (dropperDiv) { // Other interesting events: blur keyup input
289
- dropperDiv.addEventListener(\"paste\", function (evt) {
290
- droppedTSV = evt.clipboardData.getData('text/plain');
291
- var html = evt.clipboardData.getData('text/html');
292
- var tbl = html.substring(html.indexOf(\"<tbody>\") + 7, html.lastIndexOf(\"</tbody>\"));
293
- console.log(tbl);
294
- btnImport.style.display = droppedTSV.length > 0 ? \"block\" : \"none\";
295
- });
296
- btnImport.addEventListener(\"click\", function () {
297
- fetch(changeout(<%= #{obj_name}_path(-1, format: :csv).inspect.html_safe %>, \"_brick_schema\", brickSchema), {
298
- method: 'PATCH',
299
- headers: { 'Content-Type': 'text/tab-separated-values' },
300
- body: droppedTSV
301
- }).then(function (tsvResponse) {
302
- btnImport.style.display = \"none\";
303
- console.log(\"toaster\", tsvResponse);
297
+ var dropperDiv = document.getElementById(\"dropper\");
298
+ var btnImport = document.getElementById(\"btnImport\");
299
+ var droppedTSV;
300
+ if (dropperDiv) { // Other interesting events: blur keyup input
301
+ dropperDiv.addEventListener(\"paste\", function (evt) {
302
+ droppedTSV = evt.clipboardData.getData('text/plain');
303
+ var html = evt.clipboardData.getData('text/html');
304
+ var tbl = html.substring(html.indexOf(\"<tbody>\") + 7, html.lastIndexOf(\"</tbody>\"));
305
+ console.log(tbl);
306
+ btnImport.style.display = droppedTSV.length > 0 ? \"block\" : \"none\";
304
307
  });
305
- });
306
- }
307
- var sheetUrl;
308
- var spreadsheetId;
309
- var sheetsLink = document.getElementById(\"sheetsLink\");
310
- function gapiLoaded() {
311
- // Have a click on the sheets link to bring up the sign-in window. (Must happen from some kind of user click.)
312
- sheetsLink.addEventListener(\"click\", async function (evt) {
313
- evt.preventDefault();
314
- await gapi.load(\"client\", function () {
315
- gapi.client.init({ // Load the discovery doc to initialize the API
316
- clientId: \"487319557829-fgj4u660igrpptdji7ev0r5hb6kh05dh.apps.googleusercontent.com\",
317
- scope: \"https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.file\",
318
- discoveryDocs: [\"https://sheets.googleapis.com/$discovery/rest?version=v4\"]
319
- }).then(function () {
320
- gapi.auth2.getAuthInstance().isSignedIn.listen(updateSignInStatus);
321
- updateSignInStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
308
+ btnImport.addEventListener(\"click\", function () {
309
+ fetch(changeout(<%= #{obj_name}_path(-1, format: :csv).inspect.html_safe %>, \"_brick_schema\", brickSchema), {
310
+ method: 'PATCH',
311
+ headers: { 'Content-Type': 'text/tab-separated-values' },
312
+ body: droppedTSV
313
+ }).then(function (tsvResponse) {
314
+ btnImport.style.display = \"none\";
315
+ console.log(\"toaster\", tsvResponse);
322
316
  });
323
317
  });
324
- });
325
- }
318
+ }
319
+ var sheetUrl;
320
+ var spreadsheetId;
321
+ var sheetsLink = document.getElementById(\"sheetsLink\");
322
+ function gapiLoaded() {
323
+ // Have a click on the sheets link to bring up the sign-in window. (Must happen from some kind of user click.)
324
+ sheetsLink.addEventListener(\"click\", async function (evt) {
325
+ evt.preventDefault();
326
+ await gapi.load(\"client\", function () {
327
+ gapi.client.init({ // Load the discovery doc to initialize the API
328
+ clientId: \"487319557829-fgj4u660igrpptdji7ev0r5hb6kh05dh.apps.googleusercontent.com\",
329
+ scope: \"https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.file\",
330
+ discoveryDocs: [\"https://sheets.googleapis.com/$discovery/rest?version=v4\"]
331
+ }).then(function () {
332
+ gapi.auth2.getAuthInstance().isSignedIn.listen(updateSignInStatus);
333
+ updateSignInStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
334
+ });
335
+ });
336
+ });
337
+ }
326
338
 
327
- async function updateSignInStatus(isSignedIn) {
328
- if (isSignedIn) {
329
- console.log(\"turds!\");
330
- await gapi.client.sheets.spreadsheets.create({
331
- properties: {
332
- title: #{table_name.inspect},
333
- },
334
- sheets: [
335
- // sheet1, sheet2, sheet3
336
- ]
337
- }).then(function (response) {
338
- sheetUrl = response.result.spreadsheetUrl;
339
- spreadsheetId = response.result.spreadsheetId;
340
- sheetsLink.setAttribute(\"href\", sheetUrl); // response.result.spreadsheetUrl
341
- console.log(\"x1\", sheetUrl);
342
-
343
- // Get JSON data
344
- fetch(changeout(<%= #{table_name}_path(format: :js).inspect.html_safe %>, \"_brick_schema\", brickSchema)).then(function (response) {
345
- response.json().then(function (data) {
346
- gapi.client.sheets.spreadsheets.values.append({
347
- spreadsheetId: spreadsheetId,
348
- range: \"Sheet1\",
349
- valueInputOption: \"RAW\",
350
- insertDataOption: \"INSERT_ROWS\"
351
- }, {
352
- range: \"Sheet1\",
353
- majorDimension: \"ROWS\",
354
- values: data,
355
- }).then(function (response2) {
356
- // console.log(\"beefcake\", response2);
339
+ async function updateSignInStatus(isSignedIn) {
340
+ if (isSignedIn) {
341
+ console.log(\"turds!\");
342
+ await gapi.client.sheets.spreadsheets.create({
343
+ properties: {
344
+ title: #{table_name.inspect},
345
+ },
346
+ sheets: [
347
+ // sheet1, sheet2, sheet3
348
+ ]
349
+ }).then(function (response) {
350
+ sheetUrl = response.result.spreadsheetUrl;
351
+ spreadsheetId = response.result.spreadsheetId;
352
+ sheetsLink.setAttribute(\"href\", sheetUrl); // response.result.spreadsheetUrl
353
+ console.log(\"x1\", sheetUrl);
354
+
355
+ // Get JSON data
356
+ fetch(changeout(<%= #{table_name}_path(format: :js).inspect.html_safe %>, \"_brick_schema\", brickSchema)).then(function (response) {
357
+ response.json().then(function (data) {
358
+ gapi.client.sheets.spreadsheets.values.append({
359
+ spreadsheetId: spreadsheetId,
360
+ range: \"Sheet1\",
361
+ valueInputOption: \"RAW\",
362
+ insertDataOption: \"INSERT_ROWS\"
363
+ }, {
364
+ range: \"Sheet1\",
365
+ majorDimension: \"ROWS\",
366
+ values: data,
367
+ }).then(function (response2) {
368
+ // console.log(\"beefcake\", response2);
369
+ });
357
370
  });
358
371
  });
359
372
  });
360
- });
361
- window.open(sheetUrl, '_blank');
373
+ window.open(sheetUrl, '_blank');
374
+ }
362
375
  }
363
- }
364
376
  </script>
365
377
  <script async defer src=\"https://apis.google.com/js/api.js\" onload=\"gapiLoaded()\"></script>
366
-
378
+ "
379
+ end
380
+ "#{css}
381
+ <p style=\"color: green\"><%= notice %></p>#{"
382
+ <select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
383
+ <select id=\"tbl\">#{table_options}</select>
384
+ <h1>#{model_name.pluralize}</h1>#{template_link}
367
385
 
368
386
  <% if @_brick_params&.present? %><h3>where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %></h3><% end %>
369
387
  <table id=\"#{table_name}\">
370
388
  <thead><tr>#{'<th></th>' if pk}
371
389
  <% @#{table_name}.columns.map(&:name).each do |col| %>
372
- <% 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) %>
373
391
  <th>
374
392
  <% if (bt = bts[col]) %>
375
- BT <%= bt[1].bt_link(bt.first) %>
376
- <% else %>
377
- <%= col %>
378
- <% 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 %>
379
400
  </th>
380
401
  <% end %>
381
402
  <%# Consider getting the name from the association -- h.first.name -- if a more \"friendly\" alias should be used for a screwy table name %>
@@ -387,21 +408,30 @@ async function updateSignInStatus(isSignedIn) {
387
408
  <tr>#{"
388
409
  <td><%= link_to '⇛', #{obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
389
410
  <% #{obj_name}.attributes.each do |k, val| %>
390
- <% 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'))) %>
391
412
  <td>
392
413
  <% if (bt = bts[k]) %>
393
414
  <%# binding.pry # Postgres column names are limited to 63 characters %>
394
- <% 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]) %>
395
- <% bt_id_col = @_brick_bt_descrip[bt.first][2]; bt_id = #{obj_name}.send(*bt_id_col) if bt_id_col&.present? %>
396
- <%= bt_id ? link_to(bt_txt, send(\"#\{bt_obj_path_base = bt[1].name.underscore\}_path\".to_sym, bt_id)) : bt_txt %>
397
- <%#= 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 %>
398
429
  <% else %>
399
430
  <%= hide_bcrypt(val) %>
400
431
  <% end %>
401
432
  </td>
402
433
  <% end %>
403
- <td>#{hms_columns.join('</td><td>')}</td>
404
- <!-- td>X</td -->
434
+ #{hms_columns.each_with_object(+'') { |hm_col, s| s << "<td>#{hm_col}</td>" }}
405
435
  </tr>
406
436
  </tbody>
407
437
  <% end %>
@@ -418,35 +448,47 @@ async function updateSignInStatus(isSignedIn) {
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
- <% @#{obj_name}.first.attributes.each do |k, val| %>
455
+ <% has_fields = false
456
+ @#{obj_name}.first.attributes.each do |k, val| %>
426
457
  <tr>
427
458
  <%# %%% Accommodate composite keys %>
428
459
  <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
429
460
  <th class=\"show-field\">
430
- <% if (bt = bts[k])
431
- # Add a final member in this array with descriptive options to be used in <select> drop-downs
432
- bt_name = bt[1].name
433
- # %%% Only do this if the user has permissions to edit this bt field
434
- if bt.length < 4
435
- bt << (option_detail = [[\"(No #\{bt_name\} chosen)\", '^^^brick_NULL^^^']])
436
- # %%% Accommodate composite keys for obj.pk at the end here
437
- bt[1].order(bt[1].primary_key).each { |obj| option_detail << [obj.brick_descrip, obj.send(bt[1].primary_key)] }
438
- end %>
439
- BT <%= bt[1].bt_link(bt.first) %>
461
+ <% has_fields = true
462
+ if (bt = bts[k])
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}.first.send(\"#\{bt.first\}_type\")
468
+ bt_pair = bt[1].find { |pair| pair.first.name == poly_class_name }
469
+ # descrips = @_brick_bt_descrip[bt.first][bt_class]
470
+ poly_id = @#{obj_name}.first.send(\"#\{bt.first\}_id\")
471
+ # bt_class.order(obj_pk = bt_class.primary_key).each { |obj| option_detail << [obj.brick_descrip(nil, obj_pk), obj.send(obj_pk)] }
472
+ else # No polymorphism, so just get the first one
473
+ bt_pair = bt[1].first
474
+ end
475
+ bt_class = bt_pair.first
476
+ if bt.length < 4
477
+ bt << (option_detail = [[\"(No #\{bt_name\} chosen)\", '^^^brick_NULL^^^']])
478
+ # %%% Accommodate composite keys for obj.pk at the end here
479
+ bt_class.order(obj_pk = bt_class.primary_key).each { |obj| option_detail << [obj.brick_descrip(nil, obj_pk), obj.send(obj_pk)] }
480
+ end %>
481
+ BT <%= bt_class.bt_link(bt.first) %>
440
482
  <% else %>
441
483
  <%= k %>
442
484
  <% end %>
443
485
  </th>
444
486
  <td>
445
- <% if (bt = bts[k]) # bt_obj.brick_descrip
487
+ <% if bt
446
488
  html_options = { prompt: \"Select #\{bt_name\}\" }
447
489
  html_options[:class] = 'dimmed' unless val %>
448
490
  <%= f.select k.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
449
- <%= 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 %>
491
+ <%= 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 %>
450
492
  <% else case #{model_name}.column_for_attribute(k).type
451
493
  when :string, :text %>
452
494
  <% if is_bcrypt?(val) # || .readonly? %>
@@ -467,8 +509,12 @@ async function updateSignInStatus(isSignedIn) {
467
509
  <% end %>
468
510
  </td>
469
511
  </tr>
470
- <% end %>
512
+ <% end
513
+ if has_fields %>
471
514
  <tr><td colspan=\"2\" class=\"right\"><%= f.submit %></td></tr>
515
+ <% else %>
516
+ <tr><td colspan=\"2\">(No displayable fields)</td></tr>
517
+ <% end %>
472
518
  </table>
473
519
  <% end %>
474
520
 
@@ -482,6 +528,7 @@ async function updateSignInStatus(isSignedIn) {
482
528
  <tr><td>(none)</td></tr>
483
529
  <% else %>
484
530
  <% collection.uniq.each do |#{hm_singular_name = hm_name.singularize.underscore}| %>
531
+ <%# %%% accommodate composite primary key %>
485
532
  <tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.name.underscore}_path(#{hm_singular_name}.#{pk})) %></td></tr>
486
533
  <% end %>
487
534
  <% end %>
@@ -510,7 +557,7 @@ async function updateSignInStatus(isSignedIn) {
510
557
  end
511
558
 
512
559
  # Just in case it hadn't been done previously when we tried to load the brick initialiser,
513
- # go make sure we've loaded additional references (virtual foreign keys).
560
+ # go make sure we've loaded additional references (virtual foreign keys and polymorphic associations).
514
561
  ::Brick.load_additional_references
515
562
  end
516
563
  end
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 23
8
+ TINY = 26
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,48 @@ 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
+
271
294
  # Load additional references (virtual foreign keys)
272
295
  # 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
296
  # %%% Maybe look for differences the second time 'round and just add new stuff instead of entirely deferring
274
297
  def load_additional_references
275
298
  return if @_additional_references_loaded
276
299
 
277
- if (ars = ::Brick.config.additional_references)
278
- ars.each { |fk| ::Brick._add_bt_and_hm(fk[0..2]) }
300
+ relations = ::Brick.relations
301
+ if (ars = ::Brick.config.additional_references) || ::Brick.config.polymorphics
302
+ ars.each { |fk| ::Brick._add_bt_and_hm(fk[0..2], relations) } if ars
303
+ if (polys = ::Brick.config.polymorphics)
304
+ if (schema = ::Brick.config.schema_to_analyse) && ::Brick.db_schemas&.include?(schema)
305
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
306
+ end
307
+ missing_stis = {}
308
+ polys.each do |k, v|
309
+ table_name, poly = k.split('.')
310
+ v ||= ActiveRecord::Base.execute_sql("SELECT DISTINCT #{poly}_type AS typ FROM #{table_name}").map { |result| result['typ'] }
311
+ v.each do |type|
312
+ if relations.key?(primary_table = type.underscore.pluralize)
313
+ ::Brick._add_bt_and_hm([table_name, poly, primary_table, "(brick) #{table_name}_#{poly}"], relations, true)
314
+ else
315
+ missing_stis[primary_table] = type unless ::Brick.existing_stis.key?(type)
316
+ end
317
+ end
318
+ end
319
+ unless missing_stis.empty?
320
+ print "
321
+ You might be missing an STI namespace prefix entry for these tables: #{missing_stis.keys.join(', ')}.
322
+ In config/initializers/brick.rb appropriate entries would look something like:
323
+ Brick.sti_namespace_prefixes = {"
324
+ puts missing_stis.map { |_k, missing_sti| "\n '::#{missing_sti}' => 'YourParentModel'" }.join(',')
325
+ puts " }
326
+ (Just trade out YourParentModel with some more appropriate one.)"
327
+ end
328
+ end
279
329
  @_additional_references_loaded = true
280
330
  end
281
331
 
@@ -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.23
4
+ version: 1.0.26
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-12 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