brick 1.0.29 → 1.0.32

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: 1a312c2fd5baf0793b4bb4143167b4399c70a514991206a611d525d0784de95b
4
- data.tar.gz: bb1af47d2d073dab2c046ff9757fb5db9447f02dd0688330ad7ac8ad678d6751
3
+ metadata.gz: 8483d9a50013e440cce28f648d09d5322985702f952520cb83c69ad0e5742ce4
4
+ data.tar.gz: 40072d0504f4d2b1cc9a59e62fef305b939dd977da0e794445adf003dbfb8b7e
5
5
  SHA512:
6
- metadata.gz: dbfabaf60608b2094e9903cb538bab00706f434f94289b9647320bb128adf0b34317a6404ace34897cf4b2f91700ebd7c028a12b4072ba1ebf9de3380f75add3
7
- data.tar.gz: 6e5444ac763c71de71fc1aca64cdb41db6276f1df57f5b556b03bad0b743373f2720865ba7c6ce36f9887d77ffee3b091f32d233121742c4786ca21e8d512705
6
+ metadata.gz: 9740778159acb464652322ef36eff763a55b38c6579fe0a7550a5797db44928ad6832a8bccc0d924a4d4c5a14dc02658e61bf4220cecff08fc348d540b67e41c
7
+ data.tar.gz: 1b09db07a284512821b604f1d82f64c4be0c11db7f8a8973f36ab3f78b21804a7fec1eb641bcf1ac5c2ec118cb477c8086c124017c66550753ec4f9beddbd413
data/lib/brick/config.rb CHANGED
@@ -122,12 +122,32 @@ module Brick
122
122
  @mutex.synchronize { @sti_namespace_prefixes = prefixes }
123
123
  end
124
124
 
125
- def schema_to_analyse
126
- @mutex.synchronize { @schema_to_analyse }
125
+ def schema_behavior
126
+ @mutex.synchronize { @schema_behavior ||= {} }
127
127
  end
128
128
 
129
- def schema_to_analyse=(schema)
130
- @mutex.synchronize { @schema_to_analyse = schema }
129
+ def schema_behavior=(schema)
130
+ @mutex.synchronize { @schema_behavior = schema }
131
+ end
132
+
133
+ def sti_type_column
134
+ @mutex.synchronize { @sti_type_column ||= {} }
135
+ end
136
+
137
+ def sti_type_column=(type_col)
138
+ @mutex.synchronize do
139
+ (@sti_type_column = type_col).each_with_object({}) do |v, s|
140
+ if v.last.nil?
141
+ # Set an STI type column generally
142
+ ActiveRecord::Base.inheritance_column = v.first
143
+ else
144
+ # Custom STI type columns for models built from specific tables
145
+ (v.last.is_a?(Array) ? v.last : [v.last]).each do |table|
146
+ ::Brick.relations[table][:sti_col] = v.first
147
+ end
148
+ end
149
+ end
150
+ end
131
151
  end
132
152
 
133
153
  def default_route_fallback
@@ -104,7 +104,8 @@ module ActiveRecord
104
104
  if ch == ']' # Time to process a bracketed thing?
105
105
  parts = bracket_name.split('.')
106
106
  first_parts = parts[0..-2].map do |part|
107
- klass = klass.reflect_on_association(part_sym = part.to_sym).klass
107
+ klass = (orig_class = klass).reflect_on_association(part_sym = part.to_sym)&.klass
108
+ puts "Couldn't reference #{orig_class.name}##{part} that's part of the DSL \"#{dsl}\"." if klass.nil?
108
109
  part_sym
109
110
  end
110
111
  parts = prefix + first_parts + [parts[-1]]
@@ -196,7 +197,7 @@ module ActiveRecord
196
197
  def self.bt_link(assoc_name)
197
198
  model_underscore = name.underscore
198
199
  assoc_name = CGI.escapeHTML(assoc_name.to_s)
199
- model_path = Rails.application.routes.url_helpers.send("#{model_underscore.pluralize}_path".to_sym)
200
+ model_path = Rails.application.routes.url_helpers.send("#{model_underscore.tr('/', '_').pluralize}_path".to_sym)
200
201
  link = Class.new.extend(ActionView::Helpers::UrlHelper).link_to(name, model_path)
201
202
  model_underscore == assoc_name ? link : "#{assoc_name}-#{link}".html_safe
202
203
  end
@@ -306,8 +307,12 @@ module ActiveRecord
306
307
 
307
308
  # %%% Skip the metadata columns
308
309
  if selects&.empty? # Default to all columns
310
+ tbl_no_schema = table.name.split('.').last
309
311
  columns.each do |col|
310
- selects << "\"#{table.name}\".\"#{col.name}\""
312
+ if (col_name = col.name) == 'class'
313
+ col_alias = ' AS _class'
314
+ end
315
+ selects << "\"#{tbl_no_schema}\".\"#{col_name}\"#{col_alias}"
311
316
  end
312
317
  end
313
318
 
@@ -344,12 +349,13 @@ module ActiveRecord
344
349
  next unless klass.reflect_on_association(assoc_name)&.klass&.column_names&.any?(ks.last)
345
350
 
346
351
  join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
352
+ distinct!
347
353
  end
348
354
  wheres[k] = v.split(',')
349
355
  end
350
356
 
351
357
  if join_array.present?
352
- left_outer_joins!(join_array) # joins!(join_array)
358
+ left_outer_joins!(join_array)
353
359
  # Without working from a duplicate, touching the AREL ast tree sets the @arel instance variable, which causes the relation to be immutable.
354
360
  (rel_dupe = dup)._arel_alias_names
355
361
  core_selects = selects.dup
@@ -360,10 +366,10 @@ module ActiveRecord
360
366
  v.last.each do |k1, v1| # k1 is class, v1 is array of columns to snag
361
367
  next if chains[k1].nil?
362
368
 
363
- tbl_name = field_tbl_names[v.first][k1] ||= shift_or_first(chains[k1])
369
+ tbl_name = (field_tbl_names[v.first][k1] ||= shift_or_first(chains[k1])).split('.').last
364
370
  field_tbl_name = nil
365
371
  v1.map { |x| [translations[x[0..-2].map(&:to_s).join('.')], x.last] }.each_with_index do |sel_col, idx|
366
- field_tbl_name = field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])
372
+ field_tbl_name = (field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])).split('.').last
367
373
 
368
374
  selects << "#{"\"#{field_tbl_name}\".\"#{sel_col.last}\""} AS \"#{(col_alias = "_brfk_#{v.first}__#{sel_col.last}")}\""
369
375
  v1[idx] << col_alias
@@ -416,10 +422,13 @@ module ActiveRecord
416
422
  on_clause << "#{tbl_alias}.#{poly_type} = '#{name}'"
417
423
  end
418
424
  join_clause = "LEFT OUTER
419
- 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}"
425
+ JOIN (SELECT #{selects.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}#{count_column
426
+ }) AS _ct_ FROM #{associative&.table_name || hm.klass.table_name
427
+ } GROUP BY #{(1..selects.length).to_a.join(', ')}) AS #{tbl_alias}"
420
428
  joins!("#{join_clause} ON #{on_clause.join(' AND ')}")
421
429
  end
422
430
  where!(wheres) unless wheres.empty?
431
+ limit!(1000) # Don't want to get too carried away just yet
423
432
  wheres unless wheres.empty? # Return the specific parameters that we did use
424
433
  end
425
434
 
@@ -480,11 +489,13 @@ if ActiveSupport::Dependencies.respond_to?(:autoload_module!) # %%% Only works w
480
489
  alias _brick_autoload_module! autoload_module!
481
490
  def autoload_module!(*args)
482
491
  into, const_name, qualified_name, path_suffix = args
483
- if (base_class = ::Brick.config.sti_namespace_prefixes&.fetch("::#{into.name}::", nil)&.constantize)
492
+ base_class_name = ::Brick.config.sti_namespace_prefixes&.fetch("::#{into.name}::", nil)
493
+ base_class_name = "::#{base_class_name}" unless base_class_name.start_with?('::')
494
+ if (base_class = base_class_name&.constantize)
484
495
  ::Brick.sti_models[qualified_name] = { base: base_class }
485
496
  # Build subclass and place it into the specially STI-namespaced module
486
497
  into.const_set(const_name.to_sym, klass = Class.new(base_class))
487
- # %%% used to also have: autoload_once_paths.include?(base_path) ||
498
+ # %%% used to also have: autoload_once_paths.include?(base_path) ||
488
499
  autoloaded_constants << qualified_name unless autoloaded_constants.include?(qualified_name)
489
500
  klass
490
501
  elsif (base_class = ::Brick.config.sti_namespace_prefixes&.fetch("::#{const_name}", nil)&.constantize)
@@ -498,94 +509,153 @@ if ActiveSupport::Dependencies.respond_to?(:autoload_module!) # %%% Only works w
498
509
  end
499
510
  end
500
511
 
501
- class Object
502
- class << self
503
- alias _brick_const_missing const_missing
504
- def const_missing(*args)
505
- return self.const_get(args.first) if self.const_defined?(args.first)
506
- return Object.const_get(args.first) if Object.const_defined?(args.first) unless self == Object
507
-
508
- class_name = args.first.to_s
509
- # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
510
- # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
511
- # that is, checking #qualified_name_for with: from_mod, const_name
512
- # If we want to support namespacing in the future, might have to utilise something like this:
513
- # path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
514
- # return self._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
515
- # If the file really exists, go and snag it:
516
- if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = (self.name || class_name)&.split('::'))
517
- filepath = (filepath[0..-2] + [class_name]).join('/').underscore + '.rb'
518
- end
519
- if is_found
520
- return self._brick_const_missing(*args)
521
- elsif ActiveSupport::Dependencies.search_for_file(filepath) # Last-ditch effort to pick this thing up before we fill in the gaps on our own
522
- my_const = parent.const_missing(class_name) # ends up having: MyModule::MyClass
523
- return my_const
524
- end
512
+ Module.class_exec do
513
+ alias _brick_const_missing const_missing
514
+ def const_missing(*args)
515
+ if (self.const_defined?(args.first) && (possible = self.const_get(args.first)) && possible != self) ||
516
+ (self != Object && Object.const_defined?(args.first) &&
517
+ (
518
+ (possible = Object.const_get(args.first)) &&
519
+ (possible != self || (possible == self && possible.is_a?(Class)))
520
+ )
521
+ )
522
+ return possible
523
+ end
524
+ class_name = args.first.to_s
525
+ # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
526
+ # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
527
+ # that is, checking #qualified_name_for with: from_mod, const_name
528
+ # If we want to support namespacing in the future, might have to utilise something like this:
529
+ # path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
530
+ # return self._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
531
+ # If the file really exists, go and snag it:
532
+ if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = (self.name || class_name)&.split('::'))
533
+ filepath = (filepath[0..-2] + [class_name]).join('/').underscore + '.rb'
534
+ end
535
+ if is_found
536
+ return self._brick_const_missing(*args)
537
+ # elsif ActiveSupport::Dependencies.search_for_file(filepath) # Last-ditch effort to pick this thing up before we fill in the gaps on our own
538
+ # my_const = parent.const_missing(class_name) # ends up having: MyModule::MyClass
539
+ # return my_const
540
+ end
525
541
 
526
- relations = ::Brick.instance_variable_get(:@relations)[ActiveRecord::Base.connection_pool.object_id] || {}
527
- result = if ::Brick.enable_controllers? && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
528
- # Otherwise now it's up to us to fill in the gaps
529
- if (model = plural_class_name.singularize.constantize)
530
- # if it's a controller and no match or a model doesn't really use the same table name, eager load all models and try to find a model class of the right name.
531
- build_controller(class_name, plural_class_name, model, relations)
532
- end
533
- elsif ::Brick.enable_models?
534
- # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
535
- # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
536
- plural_class_name = ActiveSupport::Inflector.pluralize(model_name = class_name)
537
- singular_table_name = ActiveSupport::Inflector.underscore(model_name)
538
-
542
+ relations = ::Brick.instance_variable_get(:@relations)[ActiveRecord::Base.connection_pool.object_id] || {}
543
+ # puts "ON OBJECT: #{args.inspect}" if self.module_parent == Object
544
+ result = if ::Brick.enable_controllers? && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
545
+ # Otherwise now it's up to us to fill in the gaps
546
+ # (Go over to underscores for a moment so that if we have something come in like VABCsController then the model name ends up as
547
+ # Vabc instead of VABC)
548
+ full_class_name = +''
549
+ full_class_name << "::#{self.name}" unless self == Object
550
+ full_class_name << "::#{plural_class_name.underscore.singularize.camelize}"
551
+ if (model = self.const_get(full_class_name))
552
+ # if it's a controller and no match or a model doesn't really use the same table name, eager load all models and try to find a model class of the right name.
553
+ Object.send(:build_controller, self, class_name, plural_class_name, model, relations)
554
+ end
555
+ elsif (::Brick.enable_models? || ::Brick.enable_controllers?) && # Schema match?
556
+ self == Object && # %%% This works for Person::Person -- but also limits us to not being able to allow more than one level of namespacing
557
+ (schema_name = [(singular_table_name = class_name.underscore),
558
+ (table_name = singular_table_name.pluralize),
559
+ class_name,
560
+ (plural_class_name = class_name.pluralize)].find { |s| Brick.db_schemas.include?(s) }&.camelize ||
561
+ (::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}::") && class_name))
562
+ # Build out a module for the schema if it's namespaced
563
+ # schema_name = schema_name.camelize
564
+ self.const_set(schema_name.to_sym, (built_module = Module.new))
565
+
566
+ [built_module, "module #{schema_name}; end\n"]
567
+ # # %%% Perhaps an option to use the first module just as schema, and additional modules as namespace with a table name prefix applied
568
+ elsif ::Brick.enable_models?
569
+ # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
570
+ # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
571
+
572
+ if self != Object || # Are we in some namespace? ...
573
+ (base_model = ::Brick.config.sti_namespace_prefixes&.fetch("::#{name}::", nil)&.constantize) # ... or part of an auto-STI namespace?
574
+ schema_name = [(singular_schema_name = name.underscore),
575
+ (schema_name = singular_schema_name.pluralize),
576
+ name,
577
+ name.pluralize].find { |s| Brick.db_schemas.include?(s) }
578
+ end
579
+ plural_class_name = ActiveSupport::Inflector.pluralize(model_name = class_name)
580
+ # If it's namespaced then we turn the first part into what would be a schema name
581
+ singular_table_name = ActiveSupport::Inflector.underscore(model_name).gsub('/', '.')
582
+
583
+ if base_model
584
+ schema_name = name.underscore # For the auto-STI namespace models
585
+ table_name = base_model.table_name
586
+ Object.send(:build_model, self, model_name, singular_table_name, table_name, relations, table_name)
587
+ else
539
588
  # Adjust for STI if we know of a base model for the requested model name
589
+ # %%% Does not yet work with namespaced model names. Perhaps prefix with plural_class_name when doing the lookups here.
540
590
  table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil) || ::Brick.existing_stis[model_name]&.constantize)
541
591
  base_model.table_name
542
592
  else
543
593
  ActiveSupport::Inflector.pluralize(singular_table_name)
544
594
  end
545
-
595
+
546
596
  # Maybe, just maybe there's a database table that will satisfy this need
547
- if (matching = [table_name, singular_table_name, plural_class_name, model_name].find { |m| relations.key?(m) })
548
- build_model(model_name, singular_table_name, table_name, relations, matching)
597
+ if (matching = [table_name, singular_table_name, plural_class_name, model_name].find { |m| relations.key?(schema_name ? "#{schema_name}.#{m}" : m) })
598
+ Object.send(:build_model, schema_name, model_name, singular_table_name, table_name, relations, matching)
549
599
  end
550
600
  end
551
- if result
552
- built_class, code = result
553
- puts "\n#{code}"
554
- built_class
555
- elsif ::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}")
601
+ end
602
+ if result
603
+ built_class, code = result
604
+ puts "\n#{code}"
605
+ built_class
606
+ elsif ::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}") && !schema_name
556
607
  # module_prefixes = type_name.split('::')
557
608
  # path = self.name.split('::')[0..-2] + []
558
609
  # module_prefixes.unshift('') unless module_prefixes.first.blank?
559
610
  # candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb')
560
- self._brick_const_missing(*args)
561
- else
562
- puts "MISSING! #{self.name} #{args.inspect} #{table_name}"
563
- self._brick_const_missing(*args)
564
- end
611
+ self._brick_const_missing(*args)
612
+ elsif self != Object
613
+ module_parent.const_missing(*args)
614
+ else
615
+ puts "MISSING! #{self.name} #{args.inspect} #{table_name}"
616
+ self._brick_const_missing(*args)
565
617
  end
618
+ end
619
+ end
620
+
621
+ class Object
622
+ class << self
566
623
 
567
624
  private
568
625
 
569
- def build_model(model_name, singular_table_name, table_name, relations, matching)
626
+ def build_model(schema_name, model_name, singular_table_name, table_name, relations, matching)
627
+ full_name = if schema_name.blank?
628
+ model_name
629
+ else # Prefix the schema to the table name + prefix the schema namespace to the class name
630
+ schema_module = if schema_name.instance_of?(Module) # from an auto-STI namespace?
631
+ schema_name
632
+ else
633
+ matching = "#{schema_name}.#{matching}"
634
+ (Brick.db_schemas[schema_name] ||= self.const_get(schema_name.singularize.camelize))
635
+ end
636
+ "#{schema_module&.name}::#{model_name}"
637
+ end
638
+
570
639
  return if ((is_view = (relation = relations[matching]).key?(:isView)) && ::Brick.config.skip_database_views) ||
571
640
  ::Brick.config.exclude_tables.include?(matching)
572
641
 
573
642
  # Are they trying to use a pluralised class name such as "Employees" instead of "Employee"?
574
643
  if table_name == singular_table_name && !ActiveSupport::Inflector.inflections.uncountable.include?(table_name)
575
- unless ::Brick.config.sti_namespace_prefixes&.key?("::#{singular_table_name.titleize}::")
644
+ unless ::Brick.config.sti_namespace_prefixes&.key?("::#{singular_table_name.camelize}::")
576
645
  puts "Warning: Class name for a model that references table \"#{matching}\" should be \"#{ActiveSupport::Inflector.singularize(model_name)}\"."
577
646
  end
578
647
  return
579
648
  end
580
649
 
581
- if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil) || ::Brick.existing_stis[model_name]&.constantize)
650
+ if (base_model = ::Brick.sti_models[full_name]&.fetch(:base, nil) || ::Brick.existing_stis[full_name]&.constantize)
582
651
  is_sti = true
583
652
  else
584
653
  base_model = ::Brick.config.models_inherit_from || ActiveRecord::Base
585
654
  end
586
- code = +"class #{model_name} < #{base_model.name}\n"
655
+ hmts = nil
656
+ code = +"class #{full_name} < #{base_model.name}\n"
587
657
  built_model = Class.new(base_model) do |new_model_class|
588
- Object.const_set(model_name.to_sym, new_model_class)
658
+ (schema_module || Object).const_set(model_name.to_sym, new_model_class)
589
659
  # Accommodate singular or camel-cased table names such as "order_detail" or "OrderDetails"
590
660
  code << " self.table_name = '#{self.table_name = matching}'\n" unless table_name == matching
591
661
 
@@ -618,6 +688,10 @@ class Object
618
688
  else
619
689
  code << " # Could not identify any column(s) to use as a primary key\n" unless is_view
620
690
  end
691
+ if (sti_col = relation.fetch(:sti_col, nil))
692
+ new_model_class.send(:'inheritance_column=', sti_col)
693
+ code << " self.inheritance_column = #{sti_col.inspect}\n"
694
+ end
621
695
 
622
696
  unless is_sti
623
697
  fks = relation[:fks] || {}
@@ -635,7 +709,21 @@ class Object
635
709
  build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, invs, code) unless invs.is_a?(Array)
636
710
  hmts
637
711
  end
638
- hmts.each do |hmt_fk, fks|
712
+ # # Not NULLables
713
+ # # %%% For the minute we've had to pull this out because it's been troublesome implementing the NotNull validator
714
+ # relation[:cols].each do |col, datatype|
715
+ # if (datatype[3] && _brick_primary_key.exclude?(col) && ::Brick.config.metadata_columns.exclude?(col)) ||
716
+ # ::Brick.config.not_nullables.include?("#{matching}.#{col}")
717
+ # code << " validates :#{col}, not_null: true\n"
718
+ # self.send(:validates, col.to_sym, { not_null: true })
719
+ # end
720
+ # end
721
+ end
722
+ end # class definition
723
+ # Having this separate -- will this now work out better?
724
+ built_model.class_exec do
725
+ hmts&.each do |hmt_fk, fks|
726
+ hmt_fk = hmt_fk.tr('.', '_')
639
727
  fks.each do |fk|
640
728
  through = fk.first[:assoc_name]
641
729
  hmt_name = if fks.length > 1
@@ -649,25 +737,19 @@ class Object
649
737
  else
650
738
  hmt_fk
651
739
  end
652
- source = fk.last unless hmt_name.singularize == fk.last
653
- code << " has_many :#{hmt_name}, through: #{(assoc_name = through.to_sym).to_sym.inspect}#{", source: :#{source}" if source}\n"
654
- options = { through: assoc_name }
655
- options[:source] = source.to_sym if source
740
+ options = { through: through.to_sym }
741
+ if relation[:fks].any? { |k, v| v[:assoc_name] == hmt_name }
742
+ hmt_name = "#{hmt_name.singularize}_#{fk.first[:assoc_name]}"
743
+ options[:class_name] = fk.first[:inverse_table].singularize.camelize
744
+ options[:foreign_key] = fk.first[:fk].to_sym
745
+ end
746
+ options[:source] = fk.last.to_sym unless hmt_name.singularize == fk.last
747
+ code << " has_many :#{hmt_name}#{options.map { |opt| ", #{opt.first}: #{opt.last.inspect}" }.join}\n"
656
748
  self.send(:has_many, hmt_name.to_sym, **options)
657
749
  end
658
750
  end
659
- # # Not NULLables
660
- # # %%% For the minute we've had to pull this out because it's been troublesome implementing the NotNull validator
661
- # relation[:cols].each do |col, datatype|
662
- # if (datatype[3] && _brick_primary_key.exclude?(col) && ::Brick.config.metadata_columns.exclude?(col)) ||
663
- # ::Brick.config.not_nullables.include?("#{matching}.#{col}")
664
- # code << " validates :#{col}, not_null: true\n"
665
- # self.send(:validates, col.to_sym, { not_null: true })
666
- # end
667
- # end
668
751
  end
669
- code << "end # model #{model_name}\n\n"
670
- end # class definition
752
+ code << "end # model #{full_name}\n\n"
671
753
  [built_model, code]
672
754
  end
673
755
 
@@ -678,12 +760,13 @@ class Object
678
760
  # Try to take care of screwy names if this is a belongs_to going to an STI subclass
679
761
  assoc_name = if (primary_class = assoc.fetch(:primary_class, nil)) &&
680
762
  sti_inverse_assoc = primary_class.reflect_on_all_associations.find do |a|
681
- a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key
763
+ a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] == a.foreign_key
682
764
  end
683
765
  sti_inverse_assoc.options[:inverse_of]&.to_s || assoc_name
684
766
  else
685
767
  assoc[:assoc_name]
686
768
  end
769
+ options[:optional] = true if assoc.key?(:optional)
687
770
  if assoc.key?(:polymorphic)
688
771
  options[:polymorphic] = true
689
772
  else
@@ -710,7 +793,7 @@ class Object
710
793
  if assoc.key?(:polymorphic)
711
794
  options[:as] = assoc[:fk].to_sym
712
795
  else
713
- need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
796
+ need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table].split('.').last)}_id" != assoc[:fk]
714
797
  end
715
798
  # fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
716
799
  if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
@@ -727,7 +810,11 @@ class Object
727
810
  end
728
811
  # Figure out if we need to specially call out the class_name and/or foreign key
729
812
  # (and if either of those then definitely also a specific inverse_of)
730
- options[:class_name] = assoc[:primary_class]&.name || singular_table_name.camelize if need_class_name
813
+ if (singular_table_parts = singular_table_name.split('.')).length > 1 &&
814
+ ::Brick.config.schema_behavior[:multitenant] && singular_table_parts.first == 'public'
815
+ singular_table_parts.shift
816
+ end
817
+ options[:class_name] = "::#{assoc[:primary_class]&.name || singular_table_parts.map(&:camelize).join('::')}" if need_class_name
731
818
  # Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
732
819
  if need_fk # Funky foreign key?
733
820
  options[:foreign_key] = if assoc[:fk].is_a?(Array)
@@ -737,7 +824,7 @@ class Object
737
824
  assoc[:fk].to_sym
738
825
  end
739
826
  end
740
- options[:inverse_of] = inverse_assoc_name.to_sym if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of)
827
+ options[:inverse_of] = inverse_assoc_name.tr('.', '_').to_sym if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of)
741
828
 
742
829
  # Prepare a list of entries for "has_many :through"
743
830
  if macro == :has_many
@@ -748,19 +835,20 @@ class Object
748
835
  end
749
836
  end
750
837
  # And finally create a has_one, has_many, or belongs_to for this association
751
- assoc_name = assoc_name.to_sym
838
+ assoc_name = assoc_name.tr('.', '_').to_sym
752
839
  code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
753
840
  self.send(macro, assoc_name, **options)
754
841
  end
755
842
 
756
- def build_controller(class_name, plural_class_name, model, relations)
843
+ def build_controller(namespace, class_name, plural_class_name, model, relations)
757
844
  table_name = ActiveSupport::Inflector.underscore(plural_class_name)
758
845
  singular_table_name = ActiveSupport::Inflector.singularize(table_name)
759
846
  pk = model._brick_primary_key(relations.fetch(table_name, nil))
760
847
 
761
- code = +"class #{class_name} < ApplicationController\n"
848
+ namespace_name = "#{namespace.name}::" if namespace
849
+ code = +"class #{namespace_name}#{class_name} < ApplicationController\n"
762
850
  built_controller = Class.new(ActionController::Base) do |new_controller_class|
763
- Object.const_set(class_name.to_sym, new_controller_class)
851
+ (namespace || Object).const_set(class_name.to_sym, new_controller_class)
764
852
 
765
853
  code << " def index\n"
766
854
  code << " @#{table_name} = #{model.name}#{pk&.present? ? ".order(#{pk.inspect})" : '.all'}\n"
@@ -787,9 +875,10 @@ class Object
787
875
  # %%% Add custom HM count columns
788
876
  # %%% What happens when the PK is composite?
789
877
  counts = hm_counts.each_with_object([]) { |v, s| s << "_br_#{v.first}._ct_ AS _br_#{v.first}_ct" }
790
- # *selects,
791
878
  instance_variable_set("@#{table_name}".to_sym, ar_relation.dup._select!(*selects, *counts))
792
- # binding.pry
879
+ if namespace && (idx = lookup_context.prefixes.index(table_name))
880
+ lookup_context.prefixes[idx] = "#{namespace.name.underscore}/#{lookup_context.prefixes[idx]}"
881
+ end
793
882
  @_brick_bt_descrip = bt_descrip
794
883
  @_brick_hm_counts = hm_counts
795
884
  @_brick_join_array = join_array
@@ -814,9 +903,9 @@ class Object
814
903
  code << " # (Define :new, :create)\n"
815
904
 
816
905
  if model.primary_key
817
- if (schema = ::Brick.config.schema_to_analyse) && ::Brick.db_schemas&.include?(schema)
818
- ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
819
- end
906
+ # if (schema = ::Brick.config.schema_behavior[:multitenant]&.fetch(:schema_to_analyse, nil)) && ::Brick.db_schemas&.include?(schema)
907
+ # ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
908
+ # end
820
909
 
821
910
  is_need_params = true
822
911
  # code << " # (Define :edit, and :destroy)\n"
@@ -827,7 +916,6 @@ class Object
827
916
  code << " end\n"
828
917
  self.define_method :update do
829
918
  ::Brick.set_db_schema(params)
830
-
831
919
  if request.format == :csv # Importing CSV?
832
920
  require 'csv'
833
921
  # See if internally it's likely a TSV file (tab-separated)
@@ -843,7 +931,9 @@ class Object
843
931
  # return
844
932
  end
845
933
 
846
- instance_variable_set("@#{singular_table_name}".to_sym, (obj = model.find(params[:id].split(','))))
934
+ id = params[:id]&.split(/[\/,_]/)
935
+ id = id.first if id.is_a?(Array) && id.length == 1
936
+ instance_variable_set("@#{singular_table_name}".to_sym, (obj = model.find(id)))
847
937
  obj = obj.first if obj.is_a?(Array)
848
938
  obj.send(:update, send(params_name = params_name.to_sym))
849
939
  end
@@ -851,7 +941,7 @@ class Object
851
941
 
852
942
  if is_need_params
853
943
  code << "private\n"
854
- code << " def params\n"
944
+ code << " def #{params_name}\n"
855
945
  code << " params.require(:#{singular_table_name}).permit(#{model.columns_hash.keys.map { |c| c.to_sym.inspect }.join(', ')})\n"
856
946
  code << " end\n"
857
947
  self.define_method(params_name) do
@@ -861,7 +951,7 @@ class Object
861
951
  # Get column names for params from relations[model.table_name][:cols].keys
862
952
  end
863
953
  end
864
- code << "end # #{class_name}\n\n"
954
+ code << "end # #{namespace_name}#{class_name}\n\n"
865
955
  end # class definition
866
956
  [built_controller, code]
867
957
  end
@@ -871,7 +961,8 @@ class Object
871
961
  plural = ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name])
872
962
  [hm_assoc[:alternate_name] == name.underscore ? "#{hm_assoc[:assoc_name].singularize}_#{plural}" : plural, true]
873
963
  else
874
- [ActiveSupport::Inflector.pluralize(hm_assoc[:inverse_table]), nil]
964
+ assoc_name = hm_assoc[:inverse_table].pluralize
965
+ [assoc_name, assoc_name.include?('.')]
875
966
  end
876
967
  end
877
968
  end
@@ -889,18 +980,29 @@ module ActiveRecord::ConnectionHandling
889
980
  conn
890
981
  end
891
982
 
983
+ # This is done separately so that during testing it can be called right after a migration
984
+ # in order to make sure everything is good.
892
985
  def _brick_reflect_tables
986
+ initializer_loaded = false
893
987
  if (relations = ::Brick.relations).empty?
894
- # Only for Postgres? (Doesn't work in sqlite3)
895
- # puts ActiveRecord::Base.execute_sql("SELECT current_setting('SEARCH_PATH')").to_a.inspect
988
+ # If there's schema things configured then we only expect our initializer to be named exactly this
989
+ if File.exist?(brick_initializer = Rails.root.join('config/initializers/brick.rb'))
990
+ initializer_loaded = load brick_initializer
991
+ end
992
+ # Only for Postgres? (Doesn't work in sqlite3)
993
+ # puts ActiveRecord::Base.execute_sql("SELECT current_setting('SEARCH_PATH')").to_a.inspect
896
994
 
897
- schema_sql = 'SELECT NULL AS table_schema;'
898
- case ActiveRecord::Base.connection.adapter_name
995
+ schema_sql = 'SELECT NULL AS table_schema;'
996
+ case ActiveRecord::Base.connection.adapter_name
899
997
  when 'PostgreSQL'
900
- schema = 'public' # Too early at this point to be able to pick up: Brick.config.schema_to_analyse
998
+ if (is_multitenant = (multitenancy = ::Brick.config.schema_behavior[:multitenant]) &&
999
+ (sta = multitenancy[:schema_to_analyse]) != 'public')
1000
+ ::Brick.default_schema = schema = sta
1001
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1002
+ end
901
1003
  schema_sql = 'SELECT DISTINCT table_schema FROM INFORMATION_SCHEMA.tables;'
902
1004
  when 'Mysql2'
903
- schema = ActiveRecord::Base.connection.current_database
1005
+ ::Brick.default_schema = schema = ActiveRecord::Base.connection.current_database
904
1006
  when 'SQLite'
905
1007
  sql = "SELECT m.name AS relation_name, UPPER(m.type) AS table_type,
906
1008
  p.name AS column_name, p.type AS data_type,
@@ -913,8 +1015,29 @@ module ActiveRecord::ConnectionHandling
913
1015
  puts "Unfamiliar with connection adapter #{ActiveRecord::Base.connection.adapter_name}"
914
1016
  end
915
1017
 
916
- sql ||= ActiveRecord::Base.send(:sanitize_sql_array, [
917
- "SELECT t.table_name AS relation_name, t.table_type,
1018
+ unless (db_schemas = ActiveRecord::Base.execute_sql(schema_sql)).is_a?(Array)
1019
+ db_schemas = db_schemas.to_a
1020
+ end
1021
+ unless db_schemas.empty?
1022
+ ::Brick.db_schemas = db_schemas.each_with_object({}) do |row, s|
1023
+ row = row.is_a?(String) ? row : row['table_schema']
1024
+ # Remove any system schemas
1025
+ s[row] = nil unless ['information_schema', 'pg_catalog'].include?(row)
1026
+ end
1027
+ end
1028
+
1029
+ if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1030
+ if (possible_schema = ::Brick.config.schema_behavior&.[](:multitenant)&.[](:schema_to_analyse))
1031
+ if ::Brick.db_schemas.key?(possible_schema)
1032
+ ::Brick.default_schema = schema = possible_schema
1033
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1034
+ else
1035
+ puts "*** In the brick.rb initializer the line \"::Brick.schema_behavior = ...\" refers to a schema called \"#{possible_schema}\". This schema does not exist. ***"
1036
+ end
1037
+ end
1038
+ end
1039
+
1040
+ sql ||= "SELECT t.table_schema AS schema, t.table_name AS relation_name, t.table_type,
918
1041
  c.column_name, c.data_type,
919
1042
  COALESCE(c.character_maximum_length, c.numeric_precision) AS max_length,
920
1043
  tc.constraint_type AS const, kcu.constraint_name AS \"key\",
@@ -932,18 +1055,22 @@ module ActiveRecord::ConnectionHandling
932
1055
  ON kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
933
1056
  AND kcu.TABLE_NAME = tc.TABLE_NAME
934
1057
  AND kcu.CONSTRAINT_NAME = tc.constraint_name
935
- WHERE t.table_schema = ? -- COALESCE(current_setting('SEARCH_PATH'), 'public')
1058
+ WHERE t.table_schema NOT IN ('information_schema', 'pg_catalog')#{"
1059
+ AND t.table_schema = COALESCE(current_setting('SEARCH_PATH'), 'public')" if schema }
936
1060
  -- AND t.table_type IN ('VIEW') -- 'BASE TABLE', 'FOREIGN TABLE'
937
1061
  AND t.table_name NOT IN ('pg_stat_statements', 'ar_internal_metadata', 'schema_migrations')
938
- ORDER BY 1, t.table_type DESC, c.ordinal_position", schema
939
- ])
940
-
1062
+ ORDER BY 1, t.table_type DESC, c.ordinal_position"
941
1063
  measures = []
942
1064
  case ActiveRecord::Base.connection.adapter_name
943
1065
  when 'PostgreSQL', 'SQLite' # These bring back a hash for each row because the query uses column aliases
1066
+ # schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
944
1067
  ActiveRecord::Base.execute_sql(sql).each do |r|
945
- # next if internal_views.include?(r['relation_name']) # Skip internal views such as v_all_assessments
946
- relation = relations[(relation_name = r['relation_name'])]
1068
+ relation_name = if ![schema, 'public'].include?(r['schema']) && !::Brick.config.schema_behavior[:multitenant]
1069
+ "#{schema_name = r['schema']}.#{r['relation_name']}"
1070
+ else
1071
+ r['relation_name']
1072
+ end
1073
+ relation = relations[relation_name]
947
1074
  relation[:isView] = true if r['table_type'] == 'VIEW'
948
1075
  col_name = r['column_name']
949
1076
  key = case r['const']
@@ -961,7 +1088,6 @@ module ActiveRecord::ConnectionHandling
961
1088
  end
962
1089
  else # MySQL2 acts a little differently, bringing back an array for each row
963
1090
  ActiveRecord::Base.execute_sql(sql).each do |r|
964
- # next if internal_views.include?(r['relation_name']) # Skip internal views such as v_all_assessments
965
1091
  relation = relations[(relation_name = r[0])] # here relation represents a table or view from the database
966
1092
  relation[:isView] = true if r[1] == 'VIEW' # table_type
967
1093
  col_name = r[2]
@@ -1000,11 +1126,11 @@ module ActiveRecord::ConnectionHandling
1000
1126
  # end
1001
1127
  # end
1002
1128
  # end
1003
-
1129
+ # schema = ::Brick.default_schema # Reset back for this next round of fun
1004
1130
  case ActiveRecord::Base.connection.adapter_name
1005
1131
  when 'PostgreSQL', 'Mysql2'
1006
- sql = ActiveRecord::Base.send(:sanitize_sql_array, [
1007
- "SELECT kcu1.TABLE_NAME, kcu1.COLUMN_NAME, kcu2.TABLE_NAME AS primary_table, kcu1.CONSTRAINT_NAME
1132
+ sql = "SELECT kcu1.CONSTRAINT_SCHEMA, kcu1.TABLE_NAME, kcu1.COLUMN_NAME,
1133
+ kcu2.CONSTRAINT_SCHEMA AS primary_schema, kcu2.TABLE_NAME AS primary_table, kcu1.CONSTRAINT_NAME AS CONSTRAINT_SCHEMA_FK
1008
1134
  FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS rc
1009
1135
  INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu1
1010
1136
  ON kcu1.CONSTRAINT_CATALOG = rc.CONSTRAINT_CATALOG
@@ -1014,10 +1140,9 @@ module ActiveRecord::ConnectionHandling
1014
1140
  ON kcu2.CONSTRAINT_CATALOG = rc.UNIQUE_CONSTRAINT_CATALOG
1015
1141
  AND kcu2.CONSTRAINT_SCHEMA = rc.UNIQUE_CONSTRAINT_SCHEMA
1016
1142
  AND kcu2.CONSTRAINT_NAME = rc.UNIQUE_CONSTRAINT_NAME
1017
- AND kcu2.ORDINAL_POSITION = kcu1.ORDINAL_POSITION
1018
- WHERE kcu1.CONSTRAINT_SCHEMA = ? -- COALESCE(current_setting('SEARCH_PATH'), 'public')", schema
1143
+ AND kcu2.ORDINAL_POSITION = kcu1.ORDINAL_POSITION#{"
1144
+ WHERE kcu1.CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if schema }"
1019
1145
  # AND kcu2.TABLE_NAME = ?;", Apartment::Tenant.current, table_name
1020
- ])
1021
1146
  when 'SQLite'
1022
1147
  sql = "SELECT m.name, fkl.\"from\", fkl.\"table\", m.name || '_' || fkl.\"from\" AS constraint_name
1023
1148
  FROM sqlite_master m
@@ -1026,12 +1151,12 @@ module ActiveRecord::ConnectionHandling
1026
1151
  else
1027
1152
  end
1028
1153
  if sql
1029
- ::Brick.db_schemas = ActiveRecord::Base.execute_sql(schema_sql)
1030
- ::Brick.db_schemas = ::Brick.db_schemas.to_a unless ::Brick.db_schemas.is_a?(Array)
1031
- ::Brick.db_schemas.map! { |row| row['table_schema'] } unless ::Brick.db_schemas.empty? || ::Brick.db_schemas.first.is_a?(String)
1032
- ::Brick.db_schemas -= ['information_schema', 'pg_catalog']
1154
+ # ::Brick.default_schema ||= schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1033
1155
  ActiveRecord::Base.execute_sql(sql).each do |fk|
1034
1156
  fk = fk.values unless fk.is_a?(Array)
1157
+ # Multitenancy makes things a little more general overall
1158
+ fk[0] = nil if fk[0] == 'public' || (is_multitenant && fk[0] == schema)
1159
+ fk[3] = nil if fk[3] == 'public' || (is_multitenant && fk[3] == schema)
1035
1160
  ::Brick._add_bt_and_hm(fk, relations)
1036
1161
  end
1037
1162
  end
@@ -1044,11 +1169,7 @@ module ActiveRecord::ConnectionHandling
1044
1169
  views.keys.each { |k| puts ActiveSupport::Inflector.singularize(k).camelize }
1045
1170
  end
1046
1171
 
1047
- # Try to load the initializer pretty danged early
1048
- if File.exist?(brick_initialiser = Rails.root.join('config/initializers/brick.rb'))
1049
- load brick_initialiser
1050
- ::Brick.load_additional_references
1051
- end
1172
+ ::Brick.load_additional_references if initializer_loaded
1052
1173
  end
1053
1174
  end
1054
1175
 
@@ -1075,35 +1196,53 @@ module Brick
1075
1196
  # rubocop:enable Style/CommentedKeyword
1076
1197
 
1077
1198
  class << self
1078
- def _add_bt_and_hm(fk, relations, is_polymorphic = false)
1079
- bt_assoc_name = fk[1].underscore
1080
- bt_assoc_name = bt_assoc_name[0..-4] if bt_assoc_name.end_with?('_id')
1081
-
1082
- bts = (relation = relations.fetch(fk[0], nil))&.fetch(:fks) { relation[:fks] = {} }
1199
+ def _add_bt_and_hm(fk, relations, is_polymorphic = false, is_optional = false)
1200
+ bt_assoc_name = fk[2]
1201
+ unless is_polymorphic
1202
+ bt_assoc_name = if bt_assoc_name.underscore.end_with?('_id')
1203
+ bt_assoc_name[0..-4]
1204
+ elsif bt_assoc_name.downcase.end_with?('id') && bt_assoc_name.exclude?('_')
1205
+ bt_assoc_name[0..-3] # Make the bold assumption that we can just peel off any final ID part
1206
+ else
1207
+ "#{bt_assoc_name}_bt"
1208
+ end
1209
+ end
1210
+ # %%% Temporary schema patch
1211
+ for_tbl = fk[1]
1212
+ fk[1] = "#{fk[0]}.#{fk[1]}" if fk[0] # && fk[0] != ::Brick.default_schema
1213
+ bts = (relation = relations.fetch(fk[1], nil))&.fetch(:fks) { relation[:fks] = {} }
1083
1214
  # %%% Do we miss out on has_many :through or even HM based on constantizing this model early?
1084
1215
  # Maybe it's already gotten this info because we got as far as to say there was a unique class
1085
- primary_table = (is_class = fk[2].is_a?(Hash) && fk[2].key?(:class)) ? (primary_class = fk[2][:class].constantize).table_name : fk[2]
1216
+ primary_table = if (is_class = fk[4].is_a?(Hash) && fk[4].key?(:class))
1217
+ pri_tbl = (primary_class = fk[4][:class].constantize).table_name
1218
+ else
1219
+ is_schema = fk[3] != ::Brick.default_schema && (::Brick.config.schema_behavior[:multitenant] || fk[3] != 'public')
1220
+ pri_tbl = fk[4]
1221
+ fk[3] && is_schema ? "#{fk[3]}.#{pri_tbl}" : pri_tbl
1222
+ end
1086
1223
  hms = (relation = relations.fetch(primary_table, nil))&.fetch(:fks) { relation[:fks] = {} } unless is_class
1087
1224
 
1088
- unless (cnstr_name = fk[3])
1225
+ unless (cnstr_name = fk[5])
1089
1226
  # For any appended references (those that come from config), arrive upon a definitely unique constraint name
1090
- cnstr_base = cnstr_name = "(brick) #{fk[0]}_#{is_class ? fk[2][:class].underscore : fk[2]}"
1227
+ pri_tbl = is_class ? fk[4][:class].underscore : pri_tbl
1228
+ pri_tbl = "#{bt_assoc_name}_#{pri_tbl}" if pri_tbl.singularize != bt_assoc_name
1229
+ cnstr_base = cnstr_name = "(brick) #{for_tbl}_#{pri_tbl}"
1091
1230
  cnstr_added_num = 1
1092
1231
  cnstr_name = "#{cnstr_base}_#{cnstr_added_num += 1}" while bts&.key?(cnstr_name) || hms&.key?(cnstr_name)
1093
1232
  missing = []
1094
- missing << fk[0] unless relations.key?(fk[0])
1233
+ missing << fk[1] unless relations.key?(fk[1])
1095
1234
  missing << primary_table unless is_class || relations.key?(primary_table)
1096
1235
  unless missing.empty?
1097
1236
  tables = relations.reject { |_k, v| v.fetch(:isView, nil) }.keys.sort
1098
1237
  puts "Brick: Additional reference #{fk.inspect} refers to non-existent #{'table'.pluralize(missing.length)} #{missing.join(' and ')}. (Available tables include #{tables.join(', ')}.)"
1099
1238
  return
1100
1239
  end
1101
- unless (cols = relations[fk[0]][:cols]).key?(fk[1]) || (is_polymorphic && cols.key?("#{fk[1]}_id") && cols.key?("#{fk[1]}_type"))
1240
+ unless (cols = relations[fk[1]][:cols]).key?(fk[2]) || (is_polymorphic && cols.key?("#{fk[2]}_id") && cols.key?("#{fk[2]}_type"))
1102
1241
  columns = cols.map { |k, v| "#{k} (#{v.first.split(' ').first})" }
1103
- puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[1]}. (Columns present in #{fk[0]} are #{columns.join(', ')}.)"
1242
+ puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[2]}. (Columns present in #{fk[1]} are #{columns.join(', ')}.)"
1104
1243
  return
1105
1244
  end
1106
- if (redundant = bts.find { |_k, v| v[:inverse]&.fetch(:inverse_table, nil) == fk[0] && v[:fk] == fk[1] && v[:inverse_table] == primary_table })
1245
+ if (redundant = bts.find { |_k, v| v[:inverse]&.fetch(:inverse_table, nil) == fk[1] && v[:fk] == fk[2] && v[:inverse_table] == primary_table })
1107
1246
  if is_class && !redundant.last.key?(:class)
1108
1247
  redundant.last[:primary_class] = primary_class # Round out this BT so it can find the proper :source for a HMT association that references an STI subclass
1109
1248
  else
@@ -1115,18 +1254,19 @@ module Brick
1115
1254
  if (assoc_bt = bts[cnstr_name])
1116
1255
  if is_polymorphic
1117
1256
  # Assuming same fk (don't yet support composite keys for polymorphics)
1118
- assoc_bt[:inverse_table] << fk[2]
1257
+ assoc_bt[:inverse_table] << fk[4]
1119
1258
  else # Expect we could have a composite key going
1120
1259
  if assoc_bt[:fk].is_a?(String)
1121
- assoc_bt[:fk] = [assoc_bt[:fk], fk[1]] unless fk[1] == assoc_bt[:fk]
1122
- elsif assoc_bt[:fk].exclude?(fk[1])
1123
- assoc_bt[:fk] << fk[1]
1260
+ assoc_bt[:fk] = [assoc_bt[:fk], fk[2]] unless fk[2] == assoc_bt[:fk]
1261
+ elsif assoc_bt[:fk].exclude?(fk[2])
1262
+ assoc_bt[:fk] << fk[2]
1124
1263
  end
1125
- assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[1]}"
1264
+ assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[2]}"
1126
1265
  end
1127
1266
  else
1128
1267
  inverse_table = [primary_table] if is_polymorphic
1129
- assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[1], assoc_name: bt_assoc_name, inverse_table: inverse_table || primary_table }
1268
+ assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[2], assoc_name: bt_assoc_name, inverse_table: inverse_table || primary_table }
1269
+ assoc_bt[:optional] = true if is_optional
1130
1270
  assoc_bt[:polymorphic] = true if is_polymorphic
1131
1271
  end
1132
1272
  if is_class
@@ -1136,21 +1276,21 @@ module Brick
1136
1276
  # assoc_bt[:inverse_of] = primary_class.reflect_on_all_associations.find { |a| a.foreign_key == bt[1] }
1137
1277
  end
1138
1278
 
1139
- return if is_class || ::Brick.config.exclude_hms&.any? { |exclusion| fk[0] == exclusion[0] && fk[1] == exclusion[1] && primary_table == exclusion[2] }
1279
+ return if is_class || ::Brick.config.exclude_hms&.any? { |exclusion| fk[1] == exclusion[0] && fk[2] == exclusion[1] && primary_table == exclusion[2] } || hms.nil?
1140
1280
 
1141
1281
  if (assoc_hm = hms.fetch((hm_cnstr_name = "hm_#{cnstr_name}"), nil))
1142
1282
  if assoc_hm[:fk].is_a?(String)
1143
- assoc_hm[:fk] = [assoc_hm[:fk], fk[1]] unless fk[1] == assoc_hm[:fk]
1144
- elsif assoc_hm[:fk].exclude?(fk[1])
1145
- assoc_hm[:fk] << fk[1]
1283
+ assoc_hm[:fk] = [assoc_hm[:fk], fk[2]] unless fk[2] == assoc_hm[:fk]
1284
+ elsif assoc_hm[:fk].exclude?(fk[2])
1285
+ assoc_hm[:fk] << fk[2]
1146
1286
  end
1147
1287
  assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
1148
1288
  assoc_hm[:inverse] = assoc_bt
1149
1289
  else
1150
- 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 }
1290
+ assoc_hm = hms[hm_cnstr_name] = { is_bt: false, fk: fk[2], assoc_name: for_tbl.pluralize, alternate_name: bt_assoc_name, inverse_table: fk[1], inverse: assoc_bt }
1151
1291
  assoc_hm[:polymorphic] = true if is_polymorphic
1152
1292
  hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
1153
- hm_counts[fk[0]] = hm_counts.fetch(fk[0]) { 0 } + 1
1293
+ hm_counts[fk[1]] = hm_counts.fetch(fk[1]) { 0 } + 1
1154
1294
  end
1155
1295
  assoc_bt[:inverse] = assoc_hm
1156
1296
  end
@@ -55,7 +55,9 @@ module Brick
55
55
  unless (is_template_exists = _brick_template_exists?(*args, **options))
56
56
  # Need to return true if we can fill in the blanks for a missing one
57
57
  # args will be something like: ["index", ["categories"]]
58
- model = args[1].map(&:camelize).join('::').singularize.constantize
58
+ args[1] = args[1].each_with_object([]) { |a, s| s.concat(a.split('/')) }
59
+ args[1][args[1].length - 1] = args[1].last.singularize # Make sure the last item, defining the class name, is singular
60
+ model = args[1].map(&:camelize).join('::').constantize
59
61
  if is_template_exists = model && (
60
62
  ['index', 'show'].include?(args.first) || # Everything has index and show
61
63
  # Only CUD stuff has create / update / destroy
@@ -72,9 +74,9 @@ module Brick
72
74
  fk_name.zip(pk.map { |pk_part| "#{obj_name}.#{pk_part}" })
73
75
  else
74
76
  pk = pk.each_with_object([]) { |pk_part, s| s << "#{obj_name}.#{pk_part}" }
75
- [[fk_name, "#{pk.length == 1 ? pk.first : pk.inspect}"]]
77
+ [[fk_name, pk.length == 1 ? pk.first : pk.inspect]]
76
78
  end
77
- keys << [hm_assoc.inverse_of.foreign_type, "#{hm_assoc.active_record.name}"] if hm_assoc.options.key?(:as)
79
+ keys << [hm_assoc.inverse_of.foreign_type, hm_assoc.active_record.name] if hm_assoc.options.key?(:as)
78
80
  keys.map { |x| "#{x.first}: #{x.last}"}.join(', ')
79
81
  end
80
82
 
@@ -84,8 +86,9 @@ module Brick
84
86
 
85
87
  model_name = @_brick_model.name
86
88
  pk = @_brick_model._brick_primary_key(::Brick.relations.fetch(model_name, nil))
87
- obj_name = model_name.underscore
88
- table_name = model_name.pluralize.underscore
89
+ obj_name = model_name.split('::').last.underscore
90
+ path_obj_name = model_name.underscore.tr('/', '_')
91
+ table_name = obj_name.pluralize
89
92
  template_link = nil
90
93
  bts, hms, associatives = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
91
94
  hms_columns = [] # Used for 'index'
@@ -108,21 +111,21 @@ module Brick
108
111
  "#{obj_name}.#{attrib_name} || 0"
109
112
  end
110
113
  "<%= ct = #{set_ct}
111
- 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"
114
+ link_to \"#\{ct || 'View'\} #{assoc_name}\", #{hm_assoc.klass.name.underscore.tr('/', '_').pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, obj_name, pk)} }) unless ct&.zero? %>\n"
112
115
  else # has_one
113
116
  "<%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>\n"
114
117
  end
115
118
  elsif args.first == 'show'
116
- hm_stuff << "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, "@#{obj_name}", pk)} }) %>\n"
119
+ hm_stuff << "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.tr('/', '_').pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, "@#{obj_name}", pk)} }) %>\n"
117
120
  end
118
121
  s << hm_stuff
119
122
  end
120
123
 
121
- schema_options = ::Brick.db_schemas.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
124
+ schema_options = ::Brick.db_schemas.keys.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
122
125
  # %%% If we are not auto-creating controllers (or routes) then omit by default, and if enabled anyway, such as in a development
123
126
  # environment or whatever, then get either the controllers or routes list instead
124
- table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables)
125
- .each_with_object(+'') { |v, s| s << "<option value=\"#{v.underscore.pluralize}\">#{v}</option>" }.html_safe
127
+ table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables).sort
128
+ .each_with_object(+'') { |v, s| s << "<option value=\"#{v.underscore.gsub('.', '/').pluralize}\">#{v}</option>" }.html_safe
126
129
  css = +"<style>
127
130
  #dropper {
128
131
  background-color: #eee;
@@ -219,7 +222,7 @@ end %>"
219
222
  if ['index', 'show', 'update'].include?(args.first)
220
223
  poly_cols = []
221
224
  css << "<% bts = { #{
222
- bts.each_with_object([]) do |v, s|
225
+ bt_items = bts.each_with_object([]) do |v, s|
223
226
  foreign_models = if v.last[2] # Polymorphic?
224
227
  poly_cols << @_brick_model.reflect_on_association(v[1].first).foreign_type
225
228
  v.last[1].each_with_object([]) { |x, s| s << "[#{x.name}, #{x.primary_key.inspect}]" }.join(', ')
@@ -227,7 +230,10 @@ end %>"
227
230
  "[#{v.last[1].name}, #{v.last[1].primary_key.inspect}]"
228
231
  end
229
232
  s << "#{v.first.inspect} => [#{v.last.first.inspect}, [#{foreign_models}], #{v.last[2].inspect}]"
230
- end.join(', ')
233
+ end
234
+ # # %%% Need to fix poly going to an STI class
235
+ # binding.pry unless poly_cols.empty?
236
+ bt_items.join(', ')
231
237
  } }
232
238
  poly_cols = #{poly_cols.inspect} %>"
233
239
  end
@@ -261,7 +267,8 @@ if (schemaSelect) {
261
267
 
262
268
  var tblSelect = document.getElementById(\"tbl\");
263
269
  if (tblSelect) {
264
- tblSelect.value = changeout(location.href);
270
+ tblSelect.value = changeout(location.href)[0];
271
+ if (tblSelect.selectedIndex < 0) tblSelect.value = changeout(location.href)[1];
265
272
  tblSelect.addEventListener(\"change\", function () {
266
273
  var lhr = changeout(location.href, null, this.value);
267
274
  if (brickSchema)
@@ -276,7 +283,8 @@ function changeout(href, param, value) {
276
283
  hrefParts = hrefParts[0].split(\"://\");
277
284
  var pathParts = hrefParts[hrefParts.length - 1].split(\"/\");
278
285
  if (value === undefined)
279
- return pathParts[1];
286
+ // A couple possibilities if it's namespaced, starting with two parts in the path -- and then try just one
287
+ return [pathParts.slice(1, 3).join('/'), pathParts.slice(1, 2)];
280
288
  else
281
289
  return hrefParts[0] + \"://\" + pathParts[0] + \"/\" + value;
282
290
  }
@@ -313,7 +321,7 @@ function changeout(href, param, value) {
313
321
  btnImport.style.display = droppedTSV.length > 0 ? \"block\" : \"none\";
314
322
  });
315
323
  btnImport.addEventListener(\"click\", function () {
316
- fetch(changeout(<%= #{obj_name}_path(-1, format: :csv).inspect.html_safe %>, \"_brick_schema\", brickSchema), {
324
+ fetch(changeout(<%= #{path_obj_name}_path(-1, format: :csv).inspect.html_safe %>, \"_brick_schema\", brickSchema), {
317
325
  method: 'PATCH',
318
326
  headers: { 'Content-Type': 'text/tab-separated-values' },
319
327
  body: droppedTSV
@@ -384,17 +392,32 @@ function changeout(href, param, value) {
384
392
  <script async defer src=\"https://apis.google.com/js/api.js\" onload=\"gapiLoaded()\"></script>
385
393
  "
386
394
  end
395
+ # %%% Instead of our current "for Janet Leverling (Employee)" kind of link we previously had this code that did a "where x = 123" thing:
396
+ # (where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %>)
387
397
  "#{css}
388
398
  <p style=\"color: green\"><%= notice %></p>#{"
389
- <select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
399
+ <select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
390
400
  <select id=\"tbl\">#{table_options}</select>
391
- <h1>#{model_name.pluralize}</h1>#{template_link}
392
-
393
- <% if @_brick_params&.present? %><h3>where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %></h3><% end %>
401
+ <h1>#{model_plural = model_name.pluralize}</h1>#{template_link}
402
+
403
+ <% if @_brick_params&.present? %>
404
+ <% if @_brick_params.length == 1 # %%% Does not yet work with composite keys
405
+ k, id = @_brick_params.first
406
+ id = id.first if id.is_a?(Array) && id.length == 1
407
+ origin = (key_parts = k.split('.')).length == 1 ? #{model_name} : #{model_name}.reflect_on_association(key_parts.first).klass
408
+ # binding.pry
409
+ if (destination_fk = Brick.relations[origin.table_name][:fks].values.find { |fk| puts fk.inspect; fk[:fk] == key_parts.last }) &&
410
+ (obj = (destination = origin.reflect_on_association(destination_fk[:assoc_name])&.klass)&.find(id)) %>
411
+ <h3>for <%= link_to \"#{"#\{obj.brick_descrip\} (#\{destination.name\})\""}, send(\"#\{destination.name.underscore.tr('/', '_')\}_path\".to_sym, id) %></h3><%
412
+ end
413
+ end %>
414
+ (<%= link_to 'See all #{model_plural.split('::').last}', #{path_obj_name.pluralize}_path %>)
415
+ <% end %>
394
416
  <table id=\"#{table_name}\">
395
417
  <thead><tr>#{'<th></th>' if pk.present?}
396
418
  <% @#{table_name}.columns.map(&:name).each do |col| %>
397
- <% next if #{(pk || []).inspect}.include?(col) || ::Brick.config.metadata_columns.include?(col) || poly_cols.include?(col) %>
419
+ <% next if (#{(pk || []).inspect}.include?(col) && #{model_name}.column_for_attribute(col).type == :integer && !bts.key?(col)) ||
420
+ ::Brick.config.metadata_columns.include?(col) || poly_cols.include?(col) %>
398
421
  <th>
399
422
  <% if (bt = bts[col]) %>
400
423
  BT <%
@@ -407,15 +430,16 @@ function changeout(href, param, value) {
407
430
  </th>
408
431
  <% end %>
409
432
  <%# Consider getting the name from the association -- h.first.name -- if a more \"friendly\" alias should be used for a screwy table name %>
410
- #{hms_headers.map { |h| "<th>#{h[1]} <%= link_to('#{h[2]}', #{h.first.klass.name.underscore.pluralize}_path) %></th>\n" }.join}
433
+ #{hms_headers.map { |h| "<th>#{h[1]} <%= link_to('#{h[2]}', #{h.first.klass.name.underscore.tr('/', '_').pluralize}_path) %></th>\n" }.join}
411
434
  </tr></thead>
412
435
 
413
436
  <tbody>
414
437
  <% @#{table_name}.each do |#{obj_name}| %>
415
438
  <tr>#{"
416
- <td><%= link_to '⇛', #{obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
439
+ <td><%= link_to '⇛', #{path_obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
417
440
  <% #{obj_name}.attributes.each do |k, val| %>
418
- <% next if #{(obj_pk || []).inspect}.include?(k) || ::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'))) %>
441
+ <% next if (#{(obj_pk || []).inspect}.include?(k) && #{model_name}.column_for_attribute(k).type == :integer && !bts.key?(k)) ||
442
+ ::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'))) %>
419
443
  <td>
420
444
  <% if (bt = bts[k]) %>
421
445
  <%# binding.pry # Postgres column names are limited to 63 characters %>
@@ -430,7 +454,7 @@ function changeout(href, param, value) {
430
454
  #{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)
431
455
  )
432
456
  bt_id = #{obj_name}.send(*bt_id_col) if bt_id_col&.present? %>
433
- <%= bt_id ? link_to(bt_txt, send(\"#\{bt_class.base_class.name.underscore\}_path\".to_sym, bt_id)) : bt_txt %>
457
+ <%= bt_id ? link_to(bt_txt, send(\"#\{bt_class.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, bt_id)) : bt_txt %>
434
458
  <%#= 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 %>
435
459
  <% end %>
436
460
  <% else %>
@@ -440,19 +464,19 @@ function changeout(href, param, value) {
440
464
  <% end %>
441
465
  #{hms_columns.each_with_object(+'') { |hm_col, s| s << "<td>#{hm_col}</td>" }}
442
466
  </tr>
443
- </tbody>
444
467
  <% end %>
468
+ </tbody>
445
469
  </table>
446
470
 
447
- #{"<hr><%= link_to \"New #{obj_name}\", new_#{obj_name}_path %>" unless @_brick_model.is_view?}
471
+ #{"<hr><%= link_to \"New #{obj_name}\", new_#{path_obj_name}_path %>" unless @_brick_model.is_view?}
448
472
  #{script}"
449
473
  when 'show', 'update'
450
474
  "#{css}
451
475
  <p style=\"color: green\"><%= notice %></p>#{"
452
- <select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
476
+ <select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
453
477
  <select id=\"tbl\">#{table_options}</select>
454
478
  <h1>#{model_name}: <%= (obj = @#{obj_name})&.brick_descrip || controller_name %></h1>
455
- <%= link_to '(See all #{obj_name.pluralize})', #{table_name}_path %>
479
+ <%= link_to '(See all #{obj_name.pluralize})', #{path_obj_name.pluralize}_path %>
456
480
  <% if obj %>
457
481
  <%= # path_options = [obj.#{pk}]
458
482
  # path_options << { '_brick_schema': } if
@@ -462,7 +486,8 @@ function changeout(href, param, value) {
462
486
  <% has_fields = false
463
487
  @#{obj_name}.attributes.each do |k, val| %>
464
488
  <tr>
465
- <% next if #{(pk || []).inspect}.include?(k) || ::Brick.config.metadata_columns.include?(k) %>
489
+ <% next if (#{(pk || []).inspect}.include?(k) && !bts.key?(k)) ||
490
+ ::Brick.config.metadata_columns.include?(k) %>
466
491
  <th class=\"show-field\">
467
492
  <% has_fields = true
468
493
  if (bt = bts[k])
@@ -504,7 +529,7 @@ function changeout(href, param, value) {
504
529
  html_options = { prompt: \"Select #\{bt_name\}\" }
505
530
  html_options[:class] = 'dimmed' unless val %>
506
531
  <%= f.select k.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
507
- <%= 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 %>
532
+ <%= bt_obj = bt_class&.find_by(bt_pair[1] => val); link_to('⇛', send(\"#\{bt_class.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, bt_obj.send(bt_class.primary_key.to_sym)), { class: 'show-arrow' }) if bt_obj %>
508
533
  <% else case #{model_name}.column_for_attribute(k).type
509
534
  when :string, :text %>
510
535
  <% if is_bcrypt?(val) # || .readonly? %>
@@ -546,7 +571,7 @@ function changeout(href, param, value) {
546
571
  <tr><td>(none)</td></tr>
547
572
  <% else %>
548
573
  <% collection.uniq.each do |#{hm_singular_name}| %>
549
- <tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.name.underscore}_path([#{obj_pk}])) %></td></tr>
574
+ <tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.name.underscore.tr('/', '_')}_path([#{obj_pk}])) %></td></tr>
550
575
  <% end %>
551
576
  <% end %>
552
577
  </table>"
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 29
8
+ TINY = 32
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
@@ -90,7 +90,7 @@ module Brick
90
90
  end
91
91
 
92
92
  class << self
93
- attr_accessor :db_schemas
93
+ attr_accessor :default_schema, :db_schemas
94
94
 
95
95
  def set_db_schema(params)
96
96
  schema = params['_brick_schema'] || 'public'
@@ -99,10 +99,13 @@ module Brick
99
99
 
100
100
  # All tables and views (what Postgres calls "relations" including column and foreign key info)
101
101
  def relations
102
- connections = Brick.instance_variable_get(:@relations) ||
103
- Brick.instance_variable_set(:@relations, (connections = {}))
104
102
  # Key our list of relations for this connection off of the connection pool's object_id
105
- (connections[ActiveRecord::Base.connection_pool.object_id] ||= Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = {} } })
103
+ (@relations ||= {})[ActiveRecord::Base.connection_pool.object_id] ||= Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = {} } }
104
+ end
105
+
106
+ # If multitenancy is enabled, a list of non-tenanted "global" models
107
+ def non_tenanted_models
108
+ @pending_models ||= {}
106
109
  end
107
110
 
108
111
  def get_bts_and_hms(model)
@@ -287,8 +290,16 @@ module Brick
287
290
  # Database schema to use when analysing existing data, such as deriving a list of polymorphic classes
288
291
  # for polymorphics in which it wasn't originally specified.
289
292
  # @api public
290
- def schema_to_analyse=(schema)
291
- Brick.config.schema_to_analyse = schema
293
+ def schema_behavior=(behavior)
294
+ Brick.config.schema_behavior = (behavior.is_a?(Symbol) ? { behavior => nil } : behavior)
295
+ end
296
+ # For any Brits out there
297
+ def schema_behaviour=(behavior)
298
+ Brick.schema_behavior = behavior
299
+ end
300
+
301
+ def sti_type_column=(type_col)
302
+ Brick.config.sti_type_column = (type_col.is_a?(String) ? { type_col => nil } : type_col)
292
303
  end
293
304
 
294
305
  def default_route_fallback=(resource_name)
@@ -303,18 +314,23 @@ module Brick
303
314
 
304
315
  relations = ::Brick.relations
305
316
  if (ars = ::Brick.config.additional_references) || ::Brick.config.polymorphics
306
- ars.each { |fk| ::Brick._add_bt_and_hm(fk[0..2], relations) } if ars
317
+ if ars
318
+ ars.each do |ar|
319
+ fk = ar.length < 5 ? [nil, +ar[0], ar[1], nil, +ar[2]] : [ar[0], +ar[1], ar[2], ar[3], +ar[4], ar[5]]
320
+ ::Brick._add_bt_and_hm(fk, relations, false, true)
321
+ end
322
+ end
307
323
  if (polys = ::Brick.config.polymorphics)
308
- if (schema = ::Brick.config.schema_to_analyse) && ::Brick.db_schemas&.include?(schema)
324
+ if (schema = ::Brick.config.schema_behavior[:multitenant]&.fetch(:schema_to_analyse, nil)) && ::Brick.db_schemas&.include?(schema)
309
325
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
310
326
  end
311
327
  missing_stis = {}
312
328
  polys.each do |k, v|
313
329
  table_name, poly = k.split('.')
314
- v ||= ActiveRecord::Base.execute_sql("SELECT DISTINCT #{poly}_type AS typ FROM #{table_name}").map { |result| result['typ'] }
330
+ v ||= ActiveRecord::Base.execute_sql("SELECT DISTINCT #{poly}_type AS typ FROM #{table_name}").each_with_object([]) { |result, s| s << result['typ'] if result['typ'] }
315
331
  v.each do |type|
316
332
  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)
333
+ ::Brick._add_bt_and_hm([nil, table_name, poly, nil, primary_table, "(brick) #{table_name}_#{poly}"], relations, true, true)
318
334
  else
319
335
  missing_stis[primary_table] = type unless ::Brick.existing_stis.key?(type)
320
336
  end
@@ -390,11 +406,20 @@ In config/initializers/brick.rb appropriate entries would look something like:
390
406
  end
391
407
  # %%% TODO: If no auto-controllers then enumerate the controllers folder in order to build matching routes
392
408
  # If auto-controllers and auto-models are both enabled then this makes sense:
393
- ::Brick.relations.each do |k, v|
394
- unless existing_controllers.key?(controller_name = k.underscore.pluralize)
409
+ ::Brick.relations.each do |rel_name, v|
410
+ rel_name = rel_name.split('.').map(&:underscore)
411
+ schema_names = rel_name[0..-2]
412
+ k = rel_name.last
413
+ unless existing_controllers.key?(controller_name = k.pluralize)
395
414
  options = {}
396
415
  options[:only] = [:index, :show] if v.key?(:isView)
397
- send(:resources, controller_name.to_sym, **options)
416
+ if schema_names.present? # && !Object.const_defined('Apartment')
417
+ send(:namespace, schema_names.first) do
418
+ send(:resources, controller_name.to_sym, **options)
419
+ end
420
+ else
421
+ send(:resources, controller_name.to_sym, **options)
422
+ end
398
423
  end
399
424
  end
400
425
  end
@@ -215,9 +215,15 @@ module Brick
215
215
  # Brick.sti_namespace_prefixes = { '::Animals::' => 'Animal',
216
216
  # '::Snake' => 'Reptile' }
217
217
 
218
+ # # Custom inheritance_column to be used for STI. This is by default \"type\", and applies to all models. With this
219
+ # # option you can change this either for specific models, or apply a new overall name generally:
220
+ # Brick.sti_type_column = 'sti_type'
221
+ # Brick.sti_type_column = { 'rails_type' => ['sales.specialoffer'] }
222
+
218
223
  # # Database schema to use when analysing existing data, such as deriving a list of polymorphic classes in the case that
219
224
  # # it wasn't originally specified.
220
- # Brick.schema_to_analyse = 'engineering'
225
+ # Brick.schema_behavior = :namespaced
226
+ # Brick.schema_behavior = { multitenant: { schema_to_analyse: 'engineering' } }
221
227
 
222
228
  # # Polymorphic associations are set up by providing a model name and polymorphic association name#{poly}
223
229
 
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.29
4
+ version: 1.0.32
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-24 00:00:00.000000000 Z
11
+ date: 2022-06-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord