brick 1.0.76 → 1.0.78

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: 615640b22db113a3959644ee9c2d08ff3a2b37aa10f12dc92d68effef091c228
4
- data.tar.gz: b0d3e616b0f44584cf6960f8b8b191fc449b9afc426a6486da11c2deadece7a6
3
+ metadata.gz: 52f0716e20661f909922bdcddf3401b79b87c33c7b118d1581dfdf9861dd702c
4
+ data.tar.gz: 45323c512a8c7490b0ac715595ead6e6299d06a442b9c723318b90bcdce6106d
5
5
  SHA512:
6
- metadata.gz: 02a9f1c74af24e1e23df8b64972d7af8d74bbb870f1510b69d0814f362959e2527f2a7ee9ff7ca65187eb97be4fad1136d64bea7e01a8327a57608924a8f12d8
7
- data.tar.gz: '0265977b746d0955c76bef269067d14363cda4373fc5f726f19fb284baf31d0df3bdaae6ffad7301b71cd630a2745db4d30f1da79ec43ef4ae1a92bc986058e9'
6
+ metadata.gz: 2ef6ac2089e1150bc9c74c6625fc3e7627f5ee06a42d1a2d5fe43790d99156abacc27ba2e042044ee42171f352718d99ae8b468ec9dca4350cd6631bead51ce7
7
+ data.tar.gz: 8c4da365eb99677930a9f35f0cd5981b91adc790a13ecf55b5369da03fdf1592cccd01f911457cf204aa8769767d5a8537d856ff7080dec75543524b3e680904
data/lib/brick/config.rb CHANGED
@@ -20,6 +20,15 @@ module Brick
20
20
  @serializer = Brick::Serializers::YAML
21
21
  end
22
22
 
23
+ # Any path prefixing to apply to all auto-generated Brick routes
24
+ def path_prefix
25
+ @mutex.synchronize { @path_prefix }
26
+ end
27
+
28
+ def path_prefix=(path)
29
+ @mutex.synchronize { @path_prefix = path }
30
+ end
31
+
23
32
  # Indicates whether Brick models are on or off. Default: true.
24
33
  def enable_models
25
34
  @mutex.synchronize { !!@enable_models }
@@ -256,12 +256,13 @@ module ActiveRecord
256
256
  table_name == assoc_name ? link : "#{assoc_name}-#{link}".html_safe
257
257
  end
258
258
 
259
- def self._brick_index
260
- tbl_parts = table_name.split('.')
261
- tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.first == Apartment.default_schema
262
- if (index = tbl_parts.map(&:underscore).join('_')) == index.singularize
263
- index << '_index' # Rails applies an _index suffix to that route when the resource name is singular
264
- end
259
+ def self._brick_index(mode = nil)
260
+ tbl_parts = ((mode == :singular) ? table_name.singularize : table_name).split('.')
261
+ tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.length > 1 && tbl_parts.first == Apartment.default_schema
262
+ tbl_parts.unshift(::Brick.config.path_prefix) if ::Brick.config.path_prefix
263
+ index = tbl_parts.map(&:underscore).join('_')
264
+ # Rails applies an _index suffix to that route when the resource name is singular
265
+ index << '_index' if mode != :singular && index == index.singularize
265
266
  index
266
267
  end
267
268
 
@@ -554,7 +555,10 @@ module ActiveRecord
554
555
  tbl_name = "\"#{tbl_name}\"" if ::Brick.is_oracle && rel_dupe._arel_applied_aliases.include?(tbl_name)
555
556
  field_tbl_name = nil
556
557
  v1.map { |x| [translations[x[0..-2].map(&:to_s).join('.')], x.last] }.each_with_index do |sel_col, idx|
557
- # binding.pry if chains[sel_col.first].nil?
558
+ # unless chains[sel_col.first]
559
+ # puts 'You might have some bogus DSL in your brick.rb file'
560
+ # next
561
+ # end
558
562
  field_tbl_name = (field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])).split('.').last
559
563
  # If it's Oracle, quote any AREL aliases that had been applied
560
564
  field_tbl_name = "\"#{field_tbl_name}\"" if ::Brick.is_oracle && rel_dupe._arel_applied_aliases.include?(field_tbl_name)
@@ -605,15 +609,42 @@ module ActiveRecord
605
609
  end
606
610
  end
607
611
  # Add derived table JOIN for the has_many counts
612
+ nix = []
608
613
  klass._br_hm_counts.each do |k, hm|
609
- associative = nil
610
614
  count_column = if hm.options[:through]
611
- if (fk_col = (associative = klass._br_associatives&.[](hm.name))&.foreign_key)
612
- if hm.source_reflection.macro == :belongs_to # Traditional HMT using an associative table
613
- hm.foreign_key
614
- else # A HMT that goes HM -> HM, something like Categories -> Products -> LineItems
615
- hm.source_reflection.active_record.primary_key
616
- end
615
+ # Build the chain of JOINs going to the final destination HMT table
616
+ # (Usually just one JOIN, but could be many.)
617
+ x = [hmt_assoc = hm]
618
+ x.unshift(hmt_assoc) while hmt_assoc.options[:through] && (hmt_assoc = klass.reflect_on_association(hmt_assoc.options[:through]))
619
+ from_clause = +"#{x.first.klass.table_name} br_t0"
620
+ fk_col = x.shift.foreign_key
621
+ link_back = [klass.primary_key] # %%% Inverse path back to the original object -- used to build out a link with a filter
622
+ idx = 0
623
+ bail_out = nil
624
+ x[0..-2].map do |a|
625
+ from_clause << "\n LEFT OUTER JOIN #{a.klass.table_name} br_t#{idx += 1} "
626
+ from_clause << if (src_ref = a.source_reflection).macro == :belongs_to
627
+ "ON br_t#{idx}.id = br_t#{idx - 1}.#{a.foreign_key}"
628
+ elsif src_ref.options[:as]
629
+ "ON br_t#{idx}.#{src_ref.type} = '#{src_ref.active_record.name}'" + # "polymorphable_type"
630
+ " AND br_t#{idx}.#{src_ref.foreign_key} = br_t#{idx - 1}.id"
631
+ elsif src_ref.options[:source_type]
632
+ print "Skipping #{hm.name} --HMT-> #{hm.source_reflection.name} as it uses source_type which is not supported"
633
+ nix << k
634
+ bail_out = true
635
+ break
636
+ else # Standard has_many
637
+ "ON br_t#{idx}.#{a.foreign_key} = br_t#{idx - 1}.id"
638
+ end
639
+ link_back.unshift(a.source_reflection.name)
640
+ [a.klass.table_name, a.foreign_key, a.source_reflection.macro]
641
+ end
642
+ next if bail_out
643
+ # count_column is determined from the last HMT member
644
+ if (src_ref = x.last.source_reflection).macro == :belongs_to # Traditional HMT using an associative table
645
+ "br_t#{idx}.#{x.last.foreign_key}"
646
+ else # A HMT that goes HM -> HM, something like Categories -> Products -> LineItems
647
+ "br_t#{idx}.#{src_ref.active_record.primary_key}"
617
648
  end
618
649
  else
619
650
  fk_col = hm.foreign_key
@@ -651,20 +682,38 @@ module ActiveRecord
651
682
  selects << poly_type
652
683
  on_clause << "#{tbl_alias}.#{poly_type} = '#{name}'"
653
684
  end
654
- hm_table_name = if is_mysql
655
- "`#{associative&.table_name || hm.klass.table_name}`"
656
- elsif is_postgres || is_mssql
657
- "\"#{(associative&.table_name || hm.klass.table_name).gsub('.', '"."')}\""
658
- else
659
- associative&.table_name || hm.klass.table_name
660
- end
685
+ unless from_clause
686
+ hm_table_name = if is_mysql
687
+ "`#{hm.klass.table_name}`"
688
+ elsif is_postgres || is_mssql
689
+ "\"#{(hm.klass.table_name).gsub('.', '"."')}\""
690
+ else
691
+ hm.klass.table_name
692
+ end
693
+ end
661
694
  group_bys = ::Brick.is_oracle || is_mssql ? selects : (1..selects.length).to_a
662
695
  join_clause = "LEFT OUTER
663
- JOIN (SELECT #{selects.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}#{count_column
664
- }) AS c_t_ FROM #{hm_table_name} GROUP BY #{group_bys.join(', ')}) #{tbl_alias}"
696
+ JOIN (SELECT #{selects.map { |s| "#{'br_t0.' if from_clause}#{s}" }.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}#{count_column
697
+ }) AS c_t_ FROM #{from_clause || hm_table_name} GROUP BY #{group_bys.join(', ')}) #{tbl_alias}"
665
698
  joins!("#{join_clause} ON #{on_clause.join(' AND ')}")
666
699
  end
667
- where!(wheres) unless wheres.empty?
700
+ while (n = nix.pop)
701
+ klass._br_hm_counts.delete(n)
702
+ end
703
+
704
+ unless wheres.empty?
705
+ # Rewrite the wheres to reference table and correlation names built out by AREL
706
+ wheres2 = wheres.each_with_object({}) do |v, s|
707
+ if (v_parts = v.first.split('.')).length == 1
708
+ s[v.first] = v.last
709
+ else
710
+ k1 = klass.reflect_on_association(v_parts.first)&.klass
711
+ tbl_name = (field_tbl_names[v_parts.first][k1] ||= shift_or_first(chains[k1])).split('.').last
712
+ s["#{tbl_name}.#{v_parts.last}"] = v.last
713
+ end
714
+ end
715
+ where!(wheres2)
716
+ end
668
717
  # Must parse the order_by and see if there are any symbols which refer to BT associations
669
718
  # or custom columns as they must be expanded to find the corresponding b_r_model__column
670
719
  # or br_cc_column naming for each.
@@ -783,50 +832,76 @@ end
783
832
  Module.class_exec do
784
833
  alias _brick_const_missing const_missing
785
834
  def const_missing(*args)
786
- desired_classname = (self == Object) ? args.first.to_s : "#{name}::#{args.first}"
835
+ requested = args.first.to_s
836
+ is_controller = requested.end_with?('Controller')
837
+ # self.name is nil when a model name is requested in an .erb file
838
+ if self.name && ::Brick.config.path_prefix
839
+ camelize_prefix = ::Brick.config.path_prefix.camelize
840
+ # Asking for the prefix module?
841
+ if self == Object && requested == camelize_prefix
842
+ Object.const_set(args.first, (built_module = Module.new))
843
+ puts "module #{camelize_prefix}; end\n"
844
+ return built_module
845
+ end
846
+ split_self_name.shift if (split_self_name = self.name.split('::')).first.blank?
847
+ if split_self_name.first == camelize_prefix
848
+ split_self_name.shift # Remove the identified path prefix from the split name
849
+ if is_controller
850
+ brick_root = split_self_name.empty? ? self : camelize_prefix.constantize
851
+ end
852
+ end
853
+ end
854
+ base_module = if self < ActiveRecord::Migration || !self.name
855
+ brick_root || Object
856
+ elsif (split_self_name || self.name.split('::')).length > 1 # Classic mode
857
+ begin
858
+ return self._brick_const_missing(*args)
859
+
860
+ rescue NameError # %%% Avoid the error "____ cannot be autoloaded from an anonymous class or module"
861
+ return self.const_get(args.first) if self.const_defined?(args.first)
862
+
863
+ # unless self == (prnt = (respond_to?(:parent) ? parent : module_parent))
864
+ unless self == Object
865
+ begin
866
+ return Object._brick_const_missing(*args)
867
+
868
+ rescue NameError
869
+ return Object.const_get(args.first) if Object.const_defined?(args.first)
870
+
871
+ end
872
+ end
873
+ end
874
+ Object
875
+ else
876
+ self
877
+ end
878
+ # puts "#{self.name} - #{args.first}"
879
+ desired_classname = (self == Object || !name) ? requested : "#{name}::#{requested}"
787
880
  if ((is_defined = self.const_defined?(args.first)) && (possible = self.const_get(args.first)) && possible.name == desired_classname) ||
788
881
  # Try to require the respective Ruby file
789
882
  ((filename = ActiveSupport::Dependencies.search_for_file(desired_classname.underscore) ||
790
- (self != Object && ActiveSupport::Dependencies.search_for_file((desired_classname = args.first.to_s).underscore))
883
+ (self != Object && ActiveSupport::Dependencies.search_for_file((desired_classname = requested).underscore))
791
884
  ) && (require_dependency(filename) || true) &&
792
885
  ((possible = self.const_get(args.first)) && possible.name == desired_classname)
793
886
  ) ||
794
887
  # If any class has turned up so far (and we're not in the middle of eager loading)
795
888
  # then return what we've found.
796
- (is_defined && !::Brick.is_eager_loading)
797
- return possible
798
- end
799
- class_name = ::Brick.namify(args.first.to_s)
800
- # self.name is nil when a model name is requested in an .erb file
801
- base_module = (self < ActiveRecord::Migration || !self.name) ? Object : self
802
- # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
803
- # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
804
- # that is, checking #qualified_name_for with: from_mod, const_name
805
- # If we want to support namespacing in the future, might have to utilise something like this:
806
- # path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
807
- # return self._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
808
- # If the file really exists, go and snag it:
809
- if ActiveSupport::Dependencies.search_for_file(class_name.underscore)
810
- return base_module._brick_const_missing(*args)
811
- # elsif ActiveSupport::Dependencies.search_for_file(filepath) # Last-ditch effort to pick this thing up before we fill in the gaps on our own
812
- # my_const = parent.const_missing(class_name) # ends up having: MyModule::MyClass
813
- # return my_const
814
- else
815
- filepath = base_module.name&.split('::')&.[](0..-2) unless base_module == Object
816
- filepath = ((filepath || []) + [class_name]).join('/').underscore + '.rb'
817
- if ActiveSupport::Dependencies.search_for_file(filepath) # Last-ditch effort to pick this thing up before we fill in the gaps on our own
818
- return base_module._brick_const_missing(*args)
889
+ (is_defined && !::Brick.is_eager_loading) # Used to also have: && possible != self
890
+ if (!brick_root && (filename || possible.instance_of?(Class))) ||
891
+ (possible.instance_of?(Module) && possible.module_parent == self) ||
892
+ (possible.instance_of?(Class) && possible == self) # Are we simply searching for ourselves?
893
+ return possible
819
894
  end
820
895
  end
821
-
896
+ class_name = ::Brick.namify(requested)
822
897
  relations = ::Brick.relations
823
- # puts "ON OBJECT: #{args.inspect}" if self.module_parent == Object
824
- result = if ::Brick.enable_controllers? && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
898
+ result = if ::Brick.enable_controllers? &&
899
+ is_controller && (plural_class_name = class_name[0..-11]).length.positive?
825
900
  # Otherwise now it's up to us to fill in the gaps
901
+ full_class_name = +''
902
+ full_class_name << "::#{(split_self_name&.first && split_self_name.join('::')) || self.name}" unless self == Object
826
903
  # (Go over to underscores for a moment so that if we have something come in like VABCsController then the model name ends up as
827
904
  # Vabc instead of VABC)
828
- full_class_name = +''
829
- full_class_name << "::#{self.name}" unless self == Object
830
905
  singular_class_name = ::Brick.namify(plural_class_name, :underscore).singularize.camelize
831
906
  full_class_name << "::#{singular_class_name}"
832
907
  if plural_class_name == 'BrickSwagger' ||
@@ -870,9 +945,21 @@ Module.class_exec do
870
945
  base_module._brick_const_missing(*args)
871
946
  # elsif base_module != Object
872
947
  # module_parent.const_missing(*args)
873
- else
874
- puts "MISSING! #{base_module.name} #{args.inspect} #{table_name}"
875
- base_module._brick_const_missing(*args)
948
+ elsif Rails.respond_to?(:autoloaders) && # After finding nothing else, if Zeitwerk is enabled ...
949
+ (Rails::Autoloaders.respond_to?(:zeitwerk_enabled?) ? Rails::Autoloaders.zeitwerk_enabled? : true)
950
+ self._brick_const_missing(*args) # ... rely solely on Zeitwerk.
951
+ else # Classic mode
952
+ unless (found = base_module._brick_const_missing(*args))
953
+ puts "MISSING! #{base_module.name} #{args.inspect} #{table_name}"
954
+ end
955
+ found
956
+ end
957
+ end
958
+
959
+ # Support Rails < 6.0 which adds #parent instead of #module_parent
960
+ unless respond_to?(:module_parent)
961
+ def module_parent # Weirdly for Grape::API does NOT come in with the proper class, but some anonymous Class thing
962
+ parent
876
963
  end
877
964
  end
878
965
  end
@@ -929,7 +1016,7 @@ class Object
929
1016
  schema_name
930
1017
  else
931
1018
  matching = "#{schema_name}.#{matching}"
932
- (Brick.db_schemas[schema_name] ||= self.const_get(schema_name.camelize))
1019
+ (::Brick.db_schemas[schema_name] || {})[:class] ||= self.const_get(schema_name.camelize.to_sym)
933
1020
  end
934
1021
  "#{schema_module&.name}::#{inheritable_name || model_name}"
935
1022
  end
@@ -939,10 +1026,10 @@ class Object
939
1026
 
940
1027
  # Are they trying to use a pluralised class name such as "Employees" instead of "Employee"?
941
1028
  if table_name == singular_table_name && !ActiveSupport::Inflector.inflections.uncountable.include?(table_name)
942
- unless ::Brick.config.sti_namespace_prefixes&.key?("::#{singular_table_name.camelize}::")
943
- puts "Warning: Class name for a model that references table \"#{matching
944
- }\" should be \"#{ActiveSupport::Inflector.singularize(inheritable_name || model_name)}\"."
945
- end
1029
+ # unless ::Brick.config.sti_namespace_prefixes&.key?("::#{singular_table_name.camelize}::")
1030
+ # puts "Warning: Class name for a model that references table \"#{matching
1031
+ # }\" should be \"#{ActiveSupport::Inflector.singularize(inheritable_name || model_name)}\"."
1032
+ # end
946
1033
  return
947
1034
  end
948
1035
 
@@ -1197,8 +1284,7 @@ class Object
1197
1284
  is_postgres = ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1198
1285
  is_mysql = ActiveRecord::Base.connection.adapter_name == 'Mysql2'
1199
1286
 
1200
- namespace_name = "#{namespace.name}::" if namespace
1201
- code = +"class #{namespace_name}#{class_name} < ApplicationController\n"
1287
+ code = +"class #{class_name} < ApplicationController\n"
1202
1288
  built_controller = Class.new(ActionController::Base) do |new_controller_class|
1203
1289
  (namespace || Object).const_set(class_name.to_sym, new_controller_class)
1204
1290
 
@@ -1341,13 +1427,6 @@ class Object
1341
1427
  code << " end\n"
1342
1428
  self.define_method :show do
1343
1429
  ::Brick.set_db_schema(params)
1344
- id = if model.columns_hash[pk.first]&.type == :string
1345
- is_pk_string = true
1346
- params[:id]
1347
- else
1348
- params[:id]&.split(/[\/,_]/)
1349
- end
1350
- id = id.first if id.is_a?(Array) && id.length == 1
1351
1430
  instance_variable_set("@#{singular_table_name}".to_sym, find_obj)
1352
1431
  end
1353
1432
  end
@@ -1388,7 +1467,7 @@ class Object
1388
1467
  end
1389
1468
 
1390
1469
  if pk.present?
1391
- # if (schema = ::Brick.config.schema_behavior[:multitenant]&.fetch(:schema_to_analyse, nil)) && ::Brick.db_schemas&.include?(schema)
1470
+ # if (schema = ::Brick.config.schema_behavior[:multitenant]&.fetch(:schema_to_analyse, nil)) && ::Brick.db_schemas&.key?(schema)
1392
1471
  # ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
1393
1472
  # end
1394
1473
 
@@ -1442,7 +1521,14 @@ class Object
1442
1521
  @#{singular_table_name} = #{model.name}.find(id.is_a?(Array) && id.length == 1 ? id.first : id)
1443
1522
  end\n"
1444
1523
  self.define_method :find_obj do
1445
- id = is_pk_string ? params[:id] : params[:id]&.split(/[\/,_]/)
1524
+ id = if model.columns_hash[pk.first]&.type == :string
1525
+ is_pk_string = true
1526
+ params[:id].gsub('^^sl^^', '/')
1527
+ else
1528
+ params[:id]&.split(/[\/,_]/).map do |val_part|
1529
+ val_part.gsub('^^sl^^', '/')
1530
+ end
1531
+ end
1446
1532
  model.find(id.is_a?(Array) && id.length == 1 ? id.first : id)
1447
1533
  end
1448
1534
  end
@@ -1465,33 +1551,55 @@ class Object
1465
1551
  # Get column names for params from relations[model.table_name][:cols].keys
1466
1552
  end
1467
1553
  end # unless is_swagger
1468
- code << "end # #{namespace_name}#{class_name}\n"
1554
+ code << "end # #{class_name}\n"
1469
1555
  end # class definition
1470
1556
  [built_controller, code]
1471
1557
  end
1472
1558
 
1473
1559
  def _brick_get_hm_assoc_name(relation, hm_assoc, source = nil)
1474
- if (relation[:hm_counts][hm_assoc[:inverse_table]]&.> 1) &&
1475
- hm_assoc[:alternate_name] != (source || name.underscore)
1476
- plural = "#{hm_assoc[:assoc_name]}_#{ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name])}"
1477
- new_alt_name = (hm_assoc[:alternate_name] == name.underscore) ? "#{hm_assoc[:assoc_name].singularize}_#{plural}" : plural
1478
- # uniq = 1
1479
- # while same_name = relation[:fks].find { |x| x.last[:assoc_name] == hm_assoc[:assoc_name] && x.last != hm_assoc }
1480
- # hm_assoc[:assoc_name] = "#{hm_assoc_name}_#{uniq += 1}"
1481
- # end
1482
- # puts new_alt_name
1483
- # hm_assoc[:assoc_name] = new_alt_name
1484
- [new_alt_name, true]
1485
- else
1486
- assoc_name = ::Brick.namify(hm_assoc[:inverse_table]).pluralize
1487
- if (needs_class = assoc_name.include?('.')) # If there is a schema name present, use a downcased version for the :has_many
1488
- assoc_parts = assoc_name.split('.')
1489
- assoc_parts[0].downcase! if assoc_parts[0] =~ /^[A-Z0-9_]+$/
1490
- assoc_name = assoc_parts.join('.')
1560
+ assoc_name, needs_class = if (relation[:hm_counts][hm_assoc[:inverse_table]]&.> 1) &&
1561
+ hm_assoc[:alternate_name] != (source || name.underscore)
1562
+ plural = "#{hm_assoc[:assoc_name]}_#{ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name])}"
1563
+ new_alt_name = (hm_assoc[:alternate_name] == name.underscore) ? "#{hm_assoc[:assoc_name].singularize}_#{plural}" : plural
1564
+ # uniq = 1
1565
+ # while same_name = relation[:fks].find { |x| x.last[:assoc_name] == hm_assoc[:assoc_name] && x.last != hm_assoc }
1566
+ # hm_assoc[:assoc_name] = "#{hm_assoc_name}_#{uniq += 1}"
1567
+ # end
1568
+ # puts new_alt_name
1569
+ # hm_assoc[:assoc_name] = new_alt_name
1570
+ [new_alt_name, true]
1571
+ else
1572
+ assoc_name = ::Brick.namify(hm_assoc[:inverse_table]).pluralize
1573
+ if (needs_class = assoc_name.include?('.')) # If there is a schema name present, use a downcased version for the :has_many
1574
+ assoc_parts = assoc_name.split('.')
1575
+ assoc_parts[0].downcase! if assoc_parts[0] =~ /^[A-Z0-9_]+$/
1576
+ assoc_name = assoc_parts.join('.')
1577
+ end
1578
+ # hm_assoc[:assoc_name] = assoc_name
1579
+ [assoc_name, needs_class]
1580
+ end
1581
+ # Already have the HM class around?
1582
+ begin
1583
+ if (hm_class = Object._brick_const_missing(hm_class_name = relation[:class_name].to_sym))
1584
+ existing_hm_assocs = hm_class.reflect_on_all_associations.select do |assoc|
1585
+ assoc.macro != :belongs_to && assoc.klass == self && assoc.foreign_key == hm_assoc[:fk]
1586
+ end
1587
+ # Missing a has_many in an existing class?
1588
+ if existing_hm_assocs.empty?
1589
+ options = { inverse_of: hm_assoc[:inverse][:assoc_name].to_sym }
1590
+ # Add class_name and foreign_key where necessary
1591
+ unless hm_assoc[:alternate_name] == (source || name.underscore)
1592
+ options[:class_name] = self.name
1593
+ options[:foreign_key] = hm_assoc[:fk].to_sym
1594
+ end
1595
+ hm_class.send(:has_many, assoc_name.to_sym, options)
1596
+ puts "# ** Adding a missing has_many to #{hm_class.name}:\nclass #{hm_class.name} < #{hm_class.superclass.name}"
1597
+ puts " has_many :#{assoc_name}, #{options.inspect}\nend\n"
1598
+ end
1491
1599
  end
1492
- # hm_assoc[:assoc_name] = assoc_name
1493
- [assoc_name, needs_class]
1600
+ rescue NameError
1494
1601
  end
1602
+ [assoc_name, needs_class]
1495
1603
  end
1496
1604
  end
1497
1605
  end
@@ -1504,6 +1612,7 @@ module ActiveRecord::ConnectionHandling
1504
1612
  alias _brick_establish_connection establish_connection
1505
1613
  def establish_connection(*args)
1506
1614
  conn = _brick_establish_connection(*args)
1615
+ begin
1507
1616
  # Overwrite SQLite's #begin_db_transaction so it opens in IMMEDIATE mode instead of
1508
1617
  # the default DEFERRED mode.
1509
1618
  # https://discuss.rubyonrails.org/t/failed-write-transaction-upgrades-in-sqlite3/81480/2
@@ -1525,9 +1634,10 @@ module ActiveRecord::ConnectionHandling
1525
1634
  end
1526
1635
  end
1527
1636
  end
1528
- begin
1637
+ # ::Brick.is_db_present = true
1529
1638
  _brick_reflect_tables
1530
1639
  rescue ActiveRecord::NoDatabaseError
1640
+ # ::Brick.is_db_present = false
1531
1641
  end
1532
1642
  conn
1533
1643
  end
@@ -1535,7 +1645,10 @@ module ActiveRecord::ConnectionHandling
1535
1645
  # This is done separately so that during testing it can be called right after a migration
1536
1646
  # in order to make sure everything is good.
1537
1647
  def _brick_reflect_tables
1648
+ # return if ActiveRecord::Base.connection.current_database == 'postgres'
1649
+
1538
1650
  initializer_loaded = false
1651
+ orig_schema = nil
1539
1652
  if (relations = ::Brick.relations).empty?
1540
1653
  # If there's schema things configured then we only expect our initializer to be named exactly this
1541
1654
  if File.exist?(brick_initializer = ::Rails.root.join('config/initializers/brick.rb'))
@@ -1543,8 +1656,8 @@ module ActiveRecord::ConnectionHandling
1543
1656
  end
1544
1657
  # Load the initializer for the Apartment gem a little early so that if .excluded_models and
1545
1658
  # .default_schema are specified then we can work with non-tenanted models more appropriately
1546
- apartment = Object.const_defined?('Apartment')
1547
- if apartment && File.exist?(apartment_initializer = ::Rails.root.join('config/initializers/apartment.rb'))
1659
+ if (apartment = Object.const_defined?('Apartment')) &&
1660
+ File.exist?(apartment_initializer = ::Rails.root.join('config/initializers/apartment.rb'))
1548
1661
  load apartment_initializer
1549
1662
  apartment_excluded = Apartment.excluded_models
1550
1663
  end
@@ -1556,23 +1669,27 @@ module ActiveRecord::ConnectionHandling
1556
1669
  case ActiveRecord::Base.connection.adapter_name
1557
1670
  when 'PostgreSQL', 'SQLServer'
1558
1671
  is_postgres = !is_mssql
1559
- db_schemas = ActiveRecord::Base.execute_sql('SELECT DISTINCT table_schema FROM INFORMATION_SCHEMA.tables;')
1672
+ db_schemas = if is_postgres
1673
+ ActiveRecord::Base.execute_sql('SELECT nspname AS table_schema, MAX(oid) AS dt FROM pg_namespace GROUP BY 1 ORDER BY 1;')
1674
+ else
1675
+ ActiveRecord::Base.execute_sql('SELECT DISTINCT table_schema, NULL AS dt FROM INFORMATION_SCHEMA.tables;')
1676
+ end
1560
1677
  ::Brick.db_schemas = db_schemas.each_with_object({}) do |row, s|
1561
1678
  row = case row
1562
- when String
1563
- row
1564
1679
  when Array
1565
- row.first
1680
+ row
1566
1681
  else
1567
- row['table_schema']
1682
+ [row['table_schema'], row['dt']]
1568
1683
  end
1569
1684
  # Remove any system schemas
1570
- s[row] = nil unless ['information_schema', 'pg_catalog',
1571
- 'INFORMATION_SCHEMA', 'sys'].include?(row)
1685
+ s[row.first] = { dt: row.last } unless ['information_schema', 'pg_catalog', 'pg_toast', 'heroku_ext',
1686
+ 'INFORMATION_SCHEMA', 'sys'].include?(row.first)
1572
1687
  end
1573
1688
  if (is_multitenant = (multitenancy = ::Brick.config.schema_behavior[:multitenant]) &&
1574
1689
  (sta = multitenancy[:schema_to_analyse]) != 'public') &&
1575
- ::Brick.db_schemas.include?(sta)
1690
+ ::Brick.db_schemas.key?(sta)
1691
+ # Take note of the current schema so we can go back to it at the end of all this
1692
+ orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
1576
1693
  ::Brick.default_schema = schema = sta
1577
1694
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1578
1695
  end
@@ -1582,7 +1699,7 @@ module ActiveRecord::ConnectionHandling
1582
1699
  # ActiveRecord::Base.connection.current_database will be something like "XEPDB1"
1583
1700
  ::Brick.default_schema = schema = ActiveRecord::Base.connection.raw_connection.username
1584
1701
  ::Brick.db_schemas = {}
1585
- ActiveRecord::Base.execute_sql("SELECT username FROM sys.all_users WHERE ORACLE_MAINTAINED != 'Y'").each { |s| ::Brick.db_schemas[s.first] = nil }
1702
+ ActiveRecord::Base.execute_sql("SELECT username FROM sys.all_users WHERE ORACLE_MAINTAINED != 'Y'").each { |s| ::Brick.db_schemas[s.first] = {} }
1586
1703
  when 'SQLite'
1587
1704
  sql = "SELECT m.name AS relation_name, UPPER(m.type) AS table_type,
1588
1705
  p.name AS column_name, p.type AS data_type,
@@ -1601,6 +1718,12 @@ module ActiveRecord::ConnectionHandling
1601
1718
  if (possible_schema = ::Brick.config.schema_behavior&.[](:multitenant)&.[](:schema_to_analyse))
1602
1719
  if ::Brick.db_schemas.key?(possible_schema)
1603
1720
  ::Brick.default_schema = schema = possible_schema
1721
+ orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
1722
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1723
+ elsif Rails.env == 'test' # When testing, just find the most recently-created schema
1724
+ ::Brick.default_schema = schema = ::Brick.db_schemas.to_a.sort { |a, b| b.last[:dt] <=> a.last[:dt] }.first.first
1725
+ puts "While running tests, had noticed in the brick.rb initializer that the line \"::Brick.schema_behavior = ...\" refers to a schema called \"#{possible_schema}\" which does not exist. Reading table structure from the most recently-created schema, #{schema}."
1726
+ orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
1604
1727
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1605
1728
  else
1606
1729
  puts "*** In the brick.rb initializer the line \"::Brick.schema_behavior = ...\" refers to a schema called \"#{possible_schema}\". This schema does not exist. ***"
@@ -1614,7 +1737,7 @@ module ActiveRecord::ConnectionHandling
1614
1737
  measures = []
1615
1738
  ::Brick.is_oracle = true if ActiveRecord::Base.connection.adapter_name == 'OracleEnhanced'
1616
1739
  case ActiveRecord::Base.connection.adapter_name
1617
- when 'PostgreSQL', 'SQLite' # These bring back a hash for each row because the query uses column aliases
1740
+ when 'PostgreSQL', 'SQLite', 'SQLServer' # These bring back a hash for each row because the query uses column aliases
1618
1741
  # schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1619
1742
  ActiveRecord::Base.retrieve_schema_and_tables(sql, is_postgres, is_mssql, schema).each do |r|
1620
1743
  # If Apartment gem lists the table as being associated with a non-tenanted model then use whatever it thinks
@@ -1804,6 +1927,11 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
1804
1927
  v[:class_name] = (schema_names + [rel_name.last.singularize]).map(&:camelize).join('::')
1805
1928
  end
1806
1929
  ::Brick.load_additional_references if initializer_loaded
1930
+
1931
+ if orig_schema && (orig_schema = (orig_schema - ['pg_catalog', 'heroku_ext']).first)
1932
+ puts "Now switching back to \"#{orig_schema}\" schema."
1933
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", orig_schema)
1934
+ end
1807
1935
  end
1808
1936
 
1809
1937
  def retrieve_schema_and_tables(sql = nil, is_postgres = nil, is_mssql = nil, schema = nil)
@@ -1823,7 +1951,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
1823
1951
  LEFT OUTER JOIN INFORMATION_SCHEMA.columns AS c ON t.table_schema = c.table_schema
1824
1952
  AND t.table_name = c.table_name
1825
1953
  LEFT OUTER JOIN
1826
- (SELECT kcu1.constraint_schema, kcu1.table_name, kcu1.ordinal_position,
1954
+ (SELECT kcu1.constraint_schema, kcu1.table_name, kcu1.column_name, kcu1.ordinal_position,
1827
1955
  tc.constraint_type, kcu1.constraint_name
1828
1956
  FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu1
1829
1957
  INNER JOIN INFORMATION_SCHEMA.table_constraints AS tc
@@ -1834,9 +1962,9 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
1834
1962
  ) AS kcu ON
1835
1963
  -- kcu.CONSTRAINT_CATALOG = t.table_catalog AND
1836
1964
  kcu.CONSTRAINT_SCHEMA = c.table_schema
1837
- AND kcu.TABLE_NAME = c.table_name#{"
1965
+ AND kcu.TABLE_NAME = c.table_name
1966
+ AND kcu.column_name = c.column_name#{"
1838
1967
  -- AND kcu.position_in_unique_constraint IS NULL" unless is_mssql}
1839
- AND kcu.ordinal_position = c.ordinal_position
1840
1968
  WHERE t.table_schema #{is_postgres || is_mssql ?
1841
1969
  "NOT IN ('information_schema', 'pg_catalog',
1842
1970
  'INFORMATION_SCHEMA', 'sys')"
@@ -1845,7 +1973,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
1845
1973
  AND t.table_schema = COALESCE(current_setting('SEARCH_PATH'), 'public')" if is_postgres && schema }
1846
1974
  -- AND t.table_type IN ('VIEW') -- 'BASE TABLE', 'FOREIGN TABLE'
1847
1975
  AND t.table_name NOT IN ('pg_stat_statements', ?, ?)
1848
- ORDER BY 1, t.table_type DESC, 2, c.ordinal_position"
1976
+ ORDER BY 1, t.table_type DESC, 2, kcu.ordinal_position"
1849
1977
  ActiveRecord::Base.execute_sql(sql, *ar_tables)
1850
1978
  end
1851
1979
 
@@ -2090,7 +2218,12 @@ module Brick
2090
2218
  end
2091
2219
  end
2092
2220
  end
2093
- ::Brick.relations.keys.map { |v| [(r = v.pluralize), (model = models[r])&.last&.table_name || v, migrations&.fetch(r, nil), model&.first] }
2221
+ ::Brick.relations.keys.map do |v|
2222
+ tbl_parts = v.split('.')
2223
+ tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.length > 1 && tbl_parts.first == Apartment.default_schema
2224
+ res = tbl_parts.join('.')
2225
+ [v, (model = models[res])&.last&.table_name, migrations&.fetch(res, nil), model&.first]
2226
+ end
2094
2227
  end
2095
2228
 
2096
2229
  def ensure_unique(name, *sources)
@@ -114,7 +114,7 @@ module Brick
114
114
  if @_brick_model
115
115
  pk = @_brick_model._brick_primary_key(::Brick.relations.fetch(@_brick_model&.table_name, nil))
116
116
  obj_name = model_name.split('::').last.underscore
117
- path_obj_name = model_name.underscore.tr('/', '_')
117
+ path_obj_name = @_brick_model._brick_index(:singular)
118
118
  table_name = obj_name.pluralize
119
119
  template_link = nil
120
120
  bts, hms = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
@@ -125,9 +125,11 @@ module Brick
125
125
  "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]}",
126
126
  (assoc_name = hm.first)]
127
127
  hm_fk_name = if (through = hm_assoc.options[:through])
128
- next unless @_brick_model.instance_methods.include?(through)
128
+ next unless @_brick_model._br_hm_counts.key?(hm_assoc.name) # Skip any weird HMTs that go through a HM with a source_type
129
+
130
+ next unless @_brick_model.instance_methods.include?(through) &&
131
+ (associative = @_brick_model._br_associatives.fetch(hm.first, nil))
129
132
 
130
- associative = @_brick_model._br_associatives[hm.first]
131
133
  tbl_nm = if hm_assoc.options[:source]
132
134
  associative.klass.reflect_on_association(hm_assoc.options[:source]).inverse_of&.name
133
135
  else
@@ -177,6 +179,7 @@ module Brick
177
179
  # %%% If we are not auto-creating controllers (or routes) then omit by default, and if enabled anyway, such as in a development
178
180
  # environment or whatever, then get either the controllers or routes list instead
179
181
  apartment_default_schema = ::Brick.apartment_multitenant && Apartment.default_schema
182
+ prefix = "#{::Brick.config.path_prefix}/" if ::Brick.config.path_prefix
180
183
  table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables).each_with_object({}) do |tbl, s|
181
184
  binding.pry if tbl.is_a?(Symbol)
182
185
  if (tbl_parts = tbl.split('.')).first == apartment_default_schema
@@ -184,7 +187,7 @@ module Brick
184
187
  end
185
188
  s[tbl] = nil
186
189
  end.keys.sort.each_with_object(+'') do |v, s|
187
- s << "<option value=\"#{v.underscore.gsub('.', '/')}\">#{v}</option>"
190
+ s << "<option value=\"#{prefix}#{v.underscore.gsub('.', '/')}\">#{v}</option>"
188
191
  end.html_safe
189
192
  table_options << '<option value="brick_status">(Status)</option>'.html_safe if ::Brick.config.add_status
190
193
  table_options << '<option value="brick_orphans">(Orphans)</option>'.html_safe if is_orphans
@@ -350,11 +353,8 @@ def hide_bcrypt(val, max_len = 200)
350
353
  '(hidden)'
351
354
  else
352
355
  if val.is_a?(String)
353
- if val.length > max_len
354
- val = val[0...max_len]
355
- val << '...'
356
- end
357
- val.force_encoding('UTF-8') unless val.encoding.name == 'UTF-8'
356
+ val = \"#\{val[0...max_len]}...\" if val.length > max_len
357
+ val = val.dup.force_encoding('UTF-8') unless val.encoding.name == 'UTF-8'
358
358
  end
359
359
  val
360
360
  end
@@ -369,7 +369,7 @@ def display_value(col_type, val)
369
369
  if @is_mysql || @is_mssql
370
370
  # MySQL's \"Internal Geometry Format\" and MSSQL's Geography are like WKB, but with an initial 4 bytes that indicates the SRID.
371
371
  if (srid = val&.[](0..3)&.unpack('I'))
372
- val = val.force_encoding('BINARY')[4..-1].bytes
372
+ val = val.dup.force_encoding('BINARY')[4..-1].bytes
373
373
 
374
374
  # MSSQL spatial bitwise flags, often 0C for a point:
375
375
  # xxxx xxx1 = HasZValues
@@ -405,6 +405,10 @@ def display_value(col_type, val)
405
405
  end
406
406
  end
407
407
  end
408
+ # Accommodate composite primary keys that include strings with forward-slash characters
409
+ def slashify(*vals)
410
+ vals.map { |val_part| val_part.is_a?(String) ? val_part.gsub('/', '^^sl^^') : val_part }
411
+ end
408
412
  callbacks = {} %>"
409
413
 
410
414
  if ['index', 'show', 'new', 'update'].include?(args.first)
@@ -461,7 +465,7 @@ window.addEventListener(\"pageshow\", function() {
461
465
  });
462
466
 
463
467
  if (tblSelect) { // Always present
464
- var i = schemaSelect ? 1 : 0,
468
+ var i = #{::Brick.config.path_prefix ? '0' : 'schemaSelect ? 1 : 0'},
465
469
  changeoutList = changeout(location.href);
466
470
  for (; i < changeoutList.length; ++i) {
467
471
  tblSelect.value = changeoutList[i];
@@ -523,15 +527,19 @@ if (grid) {
523
527
  function gridMove(evt) {
524
528
  var lastHighCell = gridHighCell;
525
529
  gridHighCell = document.elementFromPoint(evt.x, evt.y);
526
- if (lastHighCell !== gridHighCell) {
527
- gridHighCell.classList.add(\"highlight\");
528
- if (lastHighCell) lastHighCell.classList.remove(\"highlight\");
529
- }
530
- var lastHighHeader = gridHighHeader;
531
- gridHighHeader = headerCols[gridHighCell.cellIndex];
532
- if (lastHighHeader !== gridHighHeader) {
533
- if (gridHighHeader) gridHighHeader.classList.add(\"highlight\");
534
- if (lastHighHeader) lastHighHeader.classList.remove(\"highlight\");
530
+ while (gridHighCell && gridHighCell.tagName !== \"TD\" && gridHighCell.tagName !== \"TH\")
531
+ gridHighCell = gridHighCell.parentElement;
532
+ if (gridHighCell) {
533
+ if (lastHighCell !== gridHighCell) {
534
+ gridHighCell.classList.add(\"highlight\");
535
+ if (lastHighCell) lastHighCell.classList.remove(\"highlight\");
536
+ }
537
+ var lastHighHeader = gridHighHeader;
538
+ gridHighHeader = headerCols[gridHighCell.cellIndex];
539
+ if (lastHighHeader !== gridHighHeader) {
540
+ if (gridHighHeader) gridHighHeader.classList.add(\"highlight\");
541
+ if (lastHighHeader) lastHighHeader.classList.remove(\"highlight\");
542
+ }
535
543
  }
536
544
  }
537
545
  }
@@ -661,7 +669,7 @@ erDiagram
661
669
  inline = case args.first
662
670
  when 'index'
663
671
  obj_pk = if pk&.is_a?(Array) # Composite primary key?
664
- "[#{pk.map { |pk_part| "#{obj_name}.#{pk_part}" }.join(', ')}]" unless pk.empty?
672
+ "#{pk.map { |pk_part| "#{obj_name}.#{pk_part}" }.join(', ')}" unless pk.empty?
665
673
  elsif pk
666
674
  "#{obj_name}.#{pk}"
667
675
  end
@@ -775,7 +783,7 @@ erDiagram
775
783
  origin = (key_parts = k.split('.')).length == 1 ? #{model_name} : #{model_name}.reflect_on_association(key_parts.first).klass
776
784
  if (destination_fk = Brick.relations[origin.table_name][:fks].values.find { |fk| fk[:fk] == key_parts.last }) &&
777
785
  (obj = (destination = origin.reflect_on_association(destination_fk[:assoc_name])&.klass)&.find(id)) %>
778
- <h3>for <%= link_to \"#{"#\{obj.brick_descrip\} (#\{destination.name\})\""}, send(\"#\{destination._brick_index\}_path\".to_sym, id) %></h3><%
786
+ <h3>for <%= link_to \"#{"#\{obj.brick_descrip\} (#\{destination.name\})\""}, send(\"#\{destination._brick_index(:singular)\}_path\".to_sym, id) %></h3><%
779
787
  end
780
788
  end %>
781
789
  (<%= link_to 'See all #{model_name.split('::').last.pluralize}', #{@_brick_model._brick_index}_path %>)
@@ -849,24 +857,25 @@ erDiagram
849
857
  @#{table_name}.each do |#{obj_name}|
850
858
  hms_cols = {#{hms_columns.join(', ')}} %>
851
859
  <tr>#{"
852
- <td><%= link_to '⇛', #{path_obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
860
+ <td><%= link_to '⇛', #{path_obj_name}_path(slashify(#{obj_pk})), { class: 'big-arrow' } %></td>" if obj_pk}
853
861
  <% @_brick_sequence.each do |col_name|
854
862
  val = #{obj_name}.attributes[col_name] %>
855
863
  <td<%= ' class=\"dimmed\"'.html_safe unless cols.key?(col_name) || (cust_col = cust_cols[col_name])%>><%
856
864
  if (bt = bts[col_name])
857
865
  if bt[2] # Polymorphic?
858
- bt_class = #{obj_name}.send(\"#\{bt.first\}_type\")
859
- base_class = (::Brick.existing_stis[bt_class] || bt_class).constantize.base_class.name.underscore
860
- poly_id = #{obj_name}.send(\"#\{bt.first\}_id\")
861
- %><%= link_to(\"#\{bt_class\} ##\{poly_id\}\", send(\"#\{base_class\}_path\".to_sym, poly_id)) if poly_id %><%
866
+ bt_class = #{obj_name}.send(\"#\{bt.first}_type\")
867
+ base_class_underscored = (::Brick.existing_stis[bt_class] || bt_class).constantize.base_class._brick_index(:singular)
868
+ poly_id = #{obj_name}.send(\"#\{bt.first}_id\")
869
+ %><%= link_to(\"#\{bt_class} ##\{poly_id}\", send(\"#\{base_class_underscored}_path\".to_sym, poly_id)) if poly_id %><%
862
870
  else
871
+ # binding.pry if @_brick_bt_descrip[bt.first][bt[1].first.first].nil?
863
872
  bt_txt = (bt_class = bt[1].first.first).brick_descrip(
864
873
  # 0..62 because Postgres column names are limited to 63 characters
865
874
  #{obj_name}, (descrips = @_brick_bt_descrip[bt.first][bt_class])[0..-2].map { |id| #{obj_name}.send(id.last[0..62]) }, (bt_id_col = descrips.last)
866
875
  )
867
876
  bt_txt ||= \"<span class=\\\"orphan\\\">&lt;&lt; Orphaned ID: #\{val} >></span>\".html_safe if val
868
877
  bt_id = bt_id_col.map { |id_col| #{obj_name}.send(id_col.to_sym) } %>
869
- <%= bt_id&.first ? link_to(bt_txt, send(\"#\{bt_class.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, bt_id)) : bt_txt %>
878
+ <%= bt_id&.first ? link_to(bt_txt, send(\"#\{bt_class.base_class._brick_index(:singular)}_path\".to_sym, bt_id)) : bt_txt %>
870
879
  <% end
871
880
  elsif (hms_col = hms_cols[col_name])
872
881
  if hms_col.length == 1 %>
@@ -877,9 +886,9 @@ erDiagram
877
886
  descrips = @_brick_bt_descrip[col_name.to_sym][klass]
878
887
  ho_txt = klass.brick_descrip(#{obj_name}, descrips[0..-2].map { |id| #{obj_name}.send(id.last[0..62]) }, (ho_id_col = descrips.last))
879
888
  ho_id = ho_id_col.map { |id_col| #{obj_name}.send(id_col.to_sym) }
880
- ho_id&.first ? link_to(ho_txt, send(\"#\{klass.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, ho_id)) : ho_txt
889
+ ho_id&.first ? link_to(ho_txt, send(\"#\{klass.base_class._brick_index(:singular)}_path\".to_sym, ho_id)) : ho_txt
881
890
  else
882
- \"#\{hms_col[1] || 'View'\} #\{hms_col.first}\"
891
+ \"#\{hms_col[1] || 'View'} #\{hms_col.first}\"
883
892
  end %>
884
893
  <%= link_to txt, send(\"#\{klass._brick_index}_path\".to_sym, hms_col[2]) unless hms_col[1]&.zero? %>
885
894
  <% end
@@ -927,7 +936,11 @@ erDiagram
927
936
  @resources.each do |r|
928
937
  %>
929
938
  <tr>
930
- <td><%= link_to(r[0], \"/#\{r[0].underscore.tr('.', '/')}\") %></td>
939
+ <td><%= begin
940
+ kls = Object.const_get(::Brick.relations[r[0]].fetch(:class_name, nil))
941
+ rescue
942
+ end
943
+ kls ? link_to(r[0], send(\"#\{kls._brick_index}_path\".to_sym)) : r[0] %></td>
931
944
  <td<%= if r[1]
932
945
  ' class=\"orphan\"' unless ::Brick.relations.key?(r[1])
933
946
  else
@@ -984,12 +997,15 @@ if (description = (relation = Brick.relations[#{model_name}.table_name])&.fetch(
984
997
  end
985
998
  %><%= link_to '(See all #{obj_name.pluralize})', #{@_brick_model._brick_index}_path %>
986
999
  #{erd_markup}
987
- <% if obj %>
1000
+ <% if obj
1001
+ # path_options = [obj.#{pk}]
1002
+ # path_options << { '_brick_schema': } if
1003
+ # url = send(:#\{model_name._brick_index(:singular)}_path, obj.#{pk})
1004
+ options = {}
1005
+ options[:url] = send(\"#\{#{model_name}._brick_index(:singular)}_path\".to_sym, obj) if ::Brick.config.path_prefix
1006
+ %>
988
1007
  <br><br>
989
- <%= # path_options = [obj.#{pk}]
990
- # path_options << { '_brick_schema': } if
991
- # url = send(:#{model_name.underscore}_path, obj.#{pk})
992
- form_for(obj.becomes(#{model_name})) do |f| %>
1008
+ <%= form_for(obj.becomes(#{model_name}), options) do |f| %>
993
1009
  <table class=\"shadow\">
994
1010
  <% has_fields = false
995
1011
  @#{obj_name}.attributes.each do |k, val|
@@ -1008,7 +1024,7 @@ end
1008
1024
  bt_pair = nil
1009
1025
  loop do
1010
1026
  bt_pair = bt[1].find { |pair| pair.first.name == poly_class_name }
1011
- # Acxommodate any valid STI by going up the chain of inheritance
1027
+ # Accommodate any valid STI by going up the chain of inheritance
1012
1028
  break unless bt_pair.nil? && poly_class_name = ::Brick.existing_stis[poly_class_name]
1013
1029
  end
1014
1030
  puts \"*** Might be missing an STI class called #\{orig_poly_name\} whose base class should have this:
@@ -1043,7 +1059,7 @@ end
1043
1059
  html_options[:prompt] = \"Select #\{bt_name\}\" %>
1044
1060
  <%= f.select k.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
1045
1061
  <%= if (bt_obj = bt_class&.find_by(bt_pair[1] => val))
1046
- 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' })
1062
+ link_to('⇛', send(\"#\{bt_class.base_class._brick_index(:singular)\}_path\".to_sym, bt_obj.send(bt_class.primary_key.to_sym)), { class: 'show-arrow' })
1047
1063
  elsif val
1048
1064
  \"<span class=\\\"orphan\\\">Orphaned ID: #\{val}</span>\".html_safe
1049
1065
  end %>
@@ -1101,9 +1117,10 @@ end
1101
1117
  <tr><td colspan=\"2\">(No displayable fields)</td></tr>
1102
1118
  <% end %>
1103
1119
  </table>
1104
- <% end %>
1120
+ <% end %>
1105
1121
 
1106
- #{hms_headers.each_with_object(+'') do |hm, s|
1122
+ #{unless args.first == 'new'
1123
+ hms_headers.each_with_object(+'') do |hm, s|
1107
1124
  # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
1108
1125
  next if hm.first.options[:through] && !hm.first.through_reflection
1109
1126
 
@@ -1118,14 +1135,15 @@ end
1118
1135
  <tr><td>(none)</td></tr>
1119
1136
  <% else %>
1120
1137
  <% collection.uniq.each do |#{hm_singular_name}| %>
1121
- <tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.name.underscore.tr('/', '_')}_path([#{obj_pk}])) %></td></tr>
1138
+ <tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass._brick_index(:singular)}_path(slashify(#{obj_pk}))) %></td></tr>
1122
1139
  <% end %>
1123
1140
  <% end %>
1124
1141
  </table>"
1125
1142
  else
1126
1143
  s
1127
1144
  end
1128
- end}
1145
+ end
1146
+ end}
1129
1147
  <% end %>
1130
1148
  #{script}"
1131
1149
 
@@ -1259,6 +1277,9 @@ document.querySelectorAll(\"input, select\").forEach(function (inp) {
1259
1277
  }
1260
1278
  });
1261
1279
  </script>"
1280
+ # puts "==============="
1281
+ # puts inline
1282
+ # puts "==============="
1262
1283
  # As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
1263
1284
  keys = options.has_key?(:locals) ? options[:locals].keys : []
1264
1285
  handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 76
8
+ TINY = 78
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
@@ -126,10 +126,15 @@ module Brick
126
126
  attr_accessor :default_schema, :db_schemas, :routes_done, :is_oracle, :is_eager_loading
127
127
 
128
128
  def set_db_schema(params = nil)
129
- schema = (params ? params['_brick_schema'] : ::Brick.default_schema) || 'public'
130
- if schema && ::Brick.db_schemas&.include?(schema)
129
+ schema = (params ? params['_brick_schema'] : ::Brick.default_schema)
130
+ if schema && ::Brick.db_schemas&.key?(schema)
131
131
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
132
132
  schema
133
+ elsif ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
134
+ # Just return the current schema
135
+ orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
136
+ # ::Brick.apartment_multitenant && tbl_parts.first == Apartment.default_schema
137
+ (orig_schema - ['pg_catalog']).first
133
138
  end
134
139
  end
135
140
 
@@ -216,6 +221,12 @@ module Brick
216
221
  true
217
222
  end
218
223
 
224
+ # Any path prefixing to apply to all auto-generated Brick routes
225
+ # @api public
226
+ def path_prefix=(path)
227
+ Brick.config.path_prefix = path
228
+ end
229
+
219
230
  # Switches Brick auto-models on or off, for all threads
220
231
  # @api public
221
232
  def enable_models=(value)
@@ -429,7 +440,7 @@ module Brick
429
440
  end
430
441
  end
431
442
  if (polys = ::Brick.config.polymorphics)
432
- if (schema = ::Brick.config.schema_behavior[:multitenant]&.fetch(:schema_to_analyse, nil)) && ::Brick.db_schemas&.include?(schema)
443
+ if (schema = ::Brick.config.schema_behavior[:multitenant]&.fetch(:schema_to_analyse, nil)) && ::Brick.db_schemas&.key?(schema)
433
444
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
434
445
  end
435
446
  missing_stis = {}
@@ -519,9 +530,9 @@ In config/initializers/brick.rb appropriate entries would look something like:
519
530
  abstract_ar_bases
520
531
  end
521
532
 
522
- def display_classes(rels, max_length)
533
+ def display_classes(prefix, rels, max_length)
523
534
  rels.sort.each do |rel|
524
- puts "#{rel.first}#{' ' * (max_length - rel.first.length)} /#{rel.last}"
535
+ puts "#{rel.first}#{' ' * (max_length - rel.first.length)} /#{prefix}#{rel.last}"
525
536
  end
526
537
  puts "\n"
527
538
  end
@@ -538,23 +549,40 @@ In config/initializers/brick.rb appropriate entries would look something like:
538
549
  view_class_length = 37 # Length of "Classes that can be built from views:"
539
550
  existing_controllers = routes.each_with_object({}) { |r, s| c = r.defaults[:controller]; s[c] = nil if c }
540
551
  ::Rails.application.routes.append do
552
+ brick_routes_create = lambda do |schema_name, controller_name, v, options|
553
+ if schema_name # && !Object.const_defined('Apartment')
554
+ send(:namespace, schema_name) do
555
+ send(:resources, v[:resource].to_sym, **options)
556
+ end
557
+ else
558
+ send(:resources, v[:resource].to_sym, **options)
559
+ end
560
+ end
561
+
541
562
  # %%% TODO: If no auto-controllers then enumerate the controllers folder in order to build matching routes
542
563
  # If auto-controllers and auto-models are both enabled then this makes sense:
564
+ controller_prefix = (::Brick.config.path_prefix ? "#{::Brick.config.path_prefix}/" : '')
543
565
  ::Brick.relations.each do |k, v|
544
566
  unless !(controller_name = v.fetch(:resource, nil)&.pluralize) || existing_controllers.key?(controller_name)
545
567
  options = {}
546
568
  options[:only] = [:index, :show] if v.key?(:isView)
569
+ # First do the API routes
547
570
  full_resource = nil
548
- if (schema_name = v.fetch(:schema, nil)) # && !Object.const_defined('Apartment')
549
- send(:namespace, schema_name) do
550
- send(:resources, v[:resource].to_sym, **options)
551
- end
571
+ if (schema_name = v.fetch(:schema, nil))
552
572
  full_resource = "#{schema_name}/#{v[:resource]}"
553
- send(:get, "#{::Brick.api_root}#{full_resource}", { to: "#{schema_name}/#{controller_name}#index" }) if Object.const_defined?('Rswag::Ui')
573
+ send(:get, "#{::Brick.api_root}#{full_resource}", { to: "#{controller_prefix}#{schema_name}/#{controller_name}#index" }) if Object.const_defined?('Rswag::Ui')
554
574
  else
555
- send(:resources, v[:resource].to_sym, **options)
556
575
  # Normally goes to something like: /api/v1/employees
557
- send(:get, "#{::Brick.api_root}#{v[:resource]}", { to: "#{controller_name}#index" }) if Object.const_defined?('Rswag::Ui')
576
+ send(:get, "#{::Brick.api_root}#{v[:resource]}", { to: "#{controller_prefix}#{controller_name}#index" }) if Object.const_defined?('Rswag::Ui')
577
+ end
578
+ # Now the normal routes
579
+ if ::Brick.config.path_prefix
580
+ # Was: send(:scope, path: ::Brick.config.path_prefix) do
581
+ send(:namespace, ::Brick.config.path_prefix) do
582
+ brick_routes_create.call(schema_name, controller_name, v, options)
583
+ end
584
+ else
585
+ brick_routes_create.call(schema_name, controller_name, v, options)
558
586
  end
559
587
 
560
588
  if (class_name = v.fetch(:class_name, nil))
@@ -572,19 +600,19 @@ In config/initializers/brick.rb appropriate entries would look something like:
572
600
  if tables.present?
573
601
  puts "Classes that can be built from tables:#{' ' * (table_class_length - 38)} Path:"
574
602
  puts "======================================#{' ' * (table_class_length - 38)} ====="
575
- ::Brick.display_classes(tables, table_class_length)
603
+ ::Brick.display_classes(controller_prefix, tables, table_class_length)
576
604
  end
577
605
  if views.present?
578
606
  puts "Classes that can be built from views:#{' ' * (view_class_length - 37)} Path:"
579
607
  puts "=====================================#{' ' * (view_class_length - 37)} ====="
580
- ::Brick.display_classes(views, view_class_length)
608
+ ::Brick.display_classes(controller_prefix, views, view_class_length)
581
609
  end
582
610
 
583
611
  if ::Brick.config.add_status && instance_variable_get(:@set).named_routes.names.exclude?(:brick_status)
584
- get('/brick_status', to: 'brick_gem#status', as: 'brick_status')
612
+ get("/#{controller_prefix}brick_status", to: 'brick_gem#status', as: 'brick_status')
585
613
  end
586
614
  if ::Brick.config.add_orphans && instance_variable_get(:@set).named_routes.names.exclude?(:brick_orphans)
587
- get('/brick_orphans', to: 'brick_gem#orphans', as: 'brick_orphans')
615
+ get("/#{controller_prefix}brick_orphans", to: 'brick_gem#orphans', as: 'brick_orphans')
588
616
  end
589
617
  if Object.const_defined?('Rswag::Ui') && doc_endpoint = Rswag::Ui.config.config_object[:urls].last
590
618
  # Serves JSON swagger info from a path such as '/api-docs/v1/swagger.json'
@@ -139,6 +139,10 @@ module Brick
139
139
  # # Settings for the Brick gem
140
140
  # # (By default this auto-creates models, controllers, views, and routes on-the-fly.)
141
141
 
142
+ # # Custom path prefix to apply to all auto-generated Brick routes. Also causes auto-generated controllers
143
+ # # to be created inside a module with the same name.
144
+ # ::Brick.path_prefix = 'admin'
145
+
142
146
  # # Normally all are enabled in development mode, and for security reasons only models are enabled in production
143
147
  # # and test. This allows you to either (a) turn off models entirely, or (b) enable controllers, views, and routes
144
148
  # # in production.
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.76
4
+ version: 1.0.78
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-10-05 00:00:00.000000000 Z
11
+ date: 2022-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord