brick 1.0.138 → 1.0.140

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: 2f044e21a6e4c2120a453b6cb3dc404d4d3e3e15034c638d316cbc418bcb996b
4
- data.tar.gz: f3360e5e7778bacde155e58bf0683d6b75c74d7dd63d336131e82b0a1865199a
3
+ metadata.gz: 995f6ed970029309b465f80193521b91c63335e12400cbb21e0ef0d60fe60dd2
4
+ data.tar.gz: c610df65f26a9e9bf4f081c8d0172fea6fc69cda350d26cf5287362237e7ceca
5
5
  SHA512:
6
- metadata.gz: 413c6fe2526752eab0e24998b852e9ae68ef7f56a6488bc14b777e13a4ad281f6c352af465be9e49f05e20fa597e7750d3c706df762f74eb70a18fda5112a319
7
- data.tar.gz: cb5040c5333dfa0c0fcbd9d1c04c229e538db1b33fc1526ace987d6da8bd1634e50269e5e05249da6205980ac79c3bad792dd5cc2242115349846365a881bf81
6
+ metadata.gz: 534bde690d45d11d8371d0e666df86badc47879242bcf67a219aeff6dc71fb1ac668b02bba5059f2fceafa63ddb6312cf3e42e993c5ba1de47ebf276abd9bb7c
7
+ data.tar.gz: 18e0c976540749fbc93fd89be05735d2e9544db37d04a3738bc06e82fdc2811d9478398fa66881d17e9e18cb44e78720f66cc8db74660ddd6b5e23e523ea2db7
@@ -26,6 +26,7 @@ if Object.const_defined?('ActionPack') && !ActionPack.respond_to?(:version)
26
26
  end
27
27
  end
28
28
  end
29
+ require 'action_view' # Needed for Rails <= 4.0
29
30
  if Object.const_defined?('ActionView') && !ActionView.respond_to?(:version)
30
31
  module ActionView
31
32
  def self.version
data/lib/brick/config.rb CHANGED
@@ -347,6 +347,14 @@ module Brick
347
347
  @mutex.synchronize { @not_nullables = columns }
348
348
  end
349
349
 
350
+ def always_load_fields
351
+ @mutex.synchronize { @always_load_fields || Hash.new { |h, k| h[k] = [] } }
352
+ end
353
+
354
+ def always_load_fields=(field_set)
355
+ @mutex.synchronize { @always_load_fields = field_set }
356
+ end
357
+
350
358
  # Add status page showing all resources and what files have been built out for them
351
359
  def add_status
352
360
  true
@@ -82,7 +82,7 @@ module ActiveRecord
82
82
 
83
83
  def json_column?(col)
84
84
  col.type == :json || ::Brick.config.json_columns[table_name]&.include?(col.name) ||
85
- ((attr_types = attribute_types[col.name]).respond_to?(:coder) &&
85
+ (respond_to?(:attribute_types) && (attr_types = attribute_types[col.name]).respond_to?(:coder) &&
86
86
  (attr_types.coder.is_a?(Class) ? attr_types.coder : attr_types.coder&.class)&.name&.end_with?('JSON'))
87
87
  end
88
88
  end
@@ -235,7 +235,7 @@ module ActiveRecord
235
235
  caches.fetch(obj_name) { caches[obj_name] = this_obj&.send(part.to_sym) }
236
236
  rescue
237
237
  clsnm = part.camelize
238
- if (possible = this_obj.class.reflect_on_all_associations.select { |a| a.class_name == clsnm || a.klass.base_class.name == clsnm }.first)
238
+ if (possible = this_obj.class.reflect_on_all_associations.select { |a| !a.polymorphic? && (a.class_name == clsnm || a.klass.base_class.name == clsnm) }.first)
239
239
  caches[obj_name] = this_obj&.send(possible.name)
240
240
  end
241
241
  end
@@ -286,7 +286,7 @@ module ActiveRecord
286
286
  assoc_html_name = unless (assoc_name = assoc_name.to_s).camelize == name
287
287
  CGI.escapeHTML(assoc_name)
288
288
  end
289
- model_path = ::Rails.application.routes.url_helpers.send("#{_brick_index}_path".to_sym)
289
+ model_path = ::Rails.application.routes.url_helpers.send("#{_brick_index || table_name}_path".to_sym)
290
290
  model_path << "?#{self.inheritance_column}=#{self.name}" if self != base_class
291
291
  av_class = Class.new.extend(ActionView::Helpers::UrlHelper)
292
292
  av_class.extend(ActionView::Helpers::TagHelper) if ActionView.version < ::Gem::Version.new('7')
@@ -296,6 +296,8 @@ module ActiveRecord
296
296
 
297
297
  # Providing a relation object allows auto-modules built from table name prefixes to work
298
298
  def self._brick_index(mode = nil, separator = '_', relation = nil)
299
+ return if abstract_class
300
+
299
301
  tbl_parts = ((mode == :singular) ? table_name.singularize : table_name).split('.')
300
302
  tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.length > 1 && tbl_parts.first == ::Brick.apartment_default_tenant
301
303
  if (aps = relation&.fetch(:auto_prefixed_schema, nil)) && tbl_parts.last.start_with?(aps)
@@ -352,7 +354,7 @@ module ActiveRecord
352
354
  pieces, my_dsl = brick_parse_dsl(join_array, [], translations, false, cc, true)
353
355
  _br_cust_cols[k] = [pieces, my_dsl, fk_col]
354
356
  end
355
- bts, hms, associatives = ::Brick.get_bts_and_hms(self)
357
+ bts, hms, associatives = ::Brick.get_bts_and_hms(self, true)
356
358
  bts.each do |_k, bt|
357
359
  next if bt[2] # Polymorphic?
358
360
 
@@ -431,8 +433,16 @@ module ActiveRecord
431
433
  [order_by, order_by_txt]
432
434
  end
433
435
 
434
- def self.brick_select(params = {}, selects = [], *args)
435
- (relation = all).brick_select(params, selects, *args)
436
+ def self.brick_select(*args, params: {}, brick_col_names: false, **kwargs)
437
+ selects = if args[0].is_a?(Array)
438
+ other_args = args[1..-1]
439
+ args[0]
440
+ else
441
+ other_args = []
442
+ args
443
+ end
444
+ (relation = all).brick_select(selects, *other_args,
445
+ params: params, brick_col_names: brick_col_names, **kwargs)
436
446
  relation.select(selects)
437
447
  end
438
448
 
@@ -472,10 +482,29 @@ module ActiveRecord
472
482
  @brick_links ||= { '' => table_name }
473
483
  end
474
484
 
475
- def brick_select(params, selects = [], order_by = nil, translations = {},
476
- join_array = ::Brick::JoinArray.new,
477
- cust_col_override = nil)
478
- is_add_bts = is_add_hms = true
485
+ def brick_select(*args, params: {}, order_by: nil, translations: {},
486
+ join_array: ::Brick::JoinArray.new,
487
+ cust_col_override: nil,
488
+ brick_col_names: true)
489
+ selects = args[0].is_a?(Array) ? args[0] : args
490
+ if selects.present? && cust_col_override.nil? # See if there's any fancy ones in the select list
491
+ idx = 0
492
+ while idx < selects.length
493
+ v = selects[idx]
494
+ if v.is_a?(String) && v.index('.')
495
+ # No prefixes and not polymorphic
496
+ pieces = self.brick_parse_dsl(join_array, [], translations, false, dsl = "[#{v}]")
497
+ (cust_col_override ||= {})[v.tr('.', '_').to_sym] = [pieces, dsl, true]
498
+ selects.delete_at(idx)
499
+ else
500
+ idx += 1
501
+ end
502
+ end
503
+ elsif selects.is_a?(Hash) && params.empty? && cust_col_override.nil? # Make sense of things if they've passed in only params
504
+ params = selects
505
+ selects = []
506
+ end
507
+ is_add_bts = is_add_hms = !cust_col_override
479
508
 
480
509
  # Build out cust_cols, bt_descrip and hm_counts now so that they are available on the
481
510
  # model early in case the user wants to do an ORDER BY based on any of that.
@@ -487,6 +516,7 @@ module ActiveRecord
487
516
  is_distinct = nil
488
517
  wheres = {}
489
518
  params.each do |k, v|
519
+ k = k.to_s # Rails < 4.2 comes in as a symbol
490
520
  next if ['_brick_schema', '_brick_order',
491
521
  '_brick_erd', '_brick_exclude', '_brick_unexclude',
492
522
  '_brick_page', '_brick_page_size', '_brick_offset', '_brick_limit',
@@ -544,9 +574,19 @@ module ActiveRecord
544
574
  "'<#{typ.end_with?('_TYP') ? typ[0..-5] : typ}>' AS #{col.name}"
545
575
  end
546
576
  end
577
+ else # Having some select columns chosen, add any missing always_load_fields for this model
578
+ ::Brick.config.always_load_fields.fetch(klass.name, nil)&.each do |alf|
579
+ selects << alf unless selects.include?(alf)
580
+ end
547
581
  end
548
582
 
549
- left_outer_joins!(join_array) if join_array.present?
583
+ if join_array.present?
584
+ if ActiveRecord.version < Gem::Version.new('4.2')
585
+ joins!(join_array)
586
+ else
587
+ left_outer_joins!(join_array)
588
+ end
589
+ end
550
590
 
551
591
  # If it's a CollectionProxy (which inherits from Relation) then need to dig out the
552
592
  # core Relation object which is found in the association scope.
@@ -594,11 +634,12 @@ module ActiveRecord
594
634
  # Deal with the conflict if there are two parts in the custom column named the same,
595
635
  # "category.name" and "product.name" for instance will end up with aliases of "name"
596
636
  # and "product__name".
637
+ col_prefix = 'br_cc_' if brick_col_names
597
638
  if (cc_part_idx = cc_part.length - 1).zero?
598
- col_alias = "br_cc_#{k}__#{table_name.tr('.', '_')}_#{cc_part.first}"
639
+ col_alias = "#{col_prefix}#{k}__#{table_name.tr('.', '_')}_#{cc_part.first}"
599
640
  else
600
641
  while cc_part_idx > 0 &&
601
- (col_alias = "br_cc_#{k}__#{cc_part[cc_part_idx..-1].map(&:to_s).join('__').tr('.', '_')}") &&
642
+ (col_alias = "#{col_prefix}#{k}__#{cc_part[cc_part_idx..-1].map(&:to_s).join('__').tr('.', '_')}") &&
602
643
  used_col_aliases.key?(col_alias)
603
644
  cc_part_idx -= 1
604
645
  end
@@ -611,7 +652,7 @@ module ActiveRecord
611
652
  key_tbl_name = tbl_name
612
653
  cc_part_idx = cc_part.length - 1
613
654
  while cc_part_idx > 0 &&
614
- (key_alias = "br_cc_#{k}__#{(cc_part[cc_part_idx..-2] + [dest_pk]).map(&:to_s).join('__')}") &&
655
+ (key_alias = "#{col_prefix}#{k}__#{(cc_part[cc_part_idx..-2] + [dest_pk]).map(&:to_s).join('__')}") &&
615
656
  key_alias != col_alias && # We break out if this key alias does exactly match the col_alias
616
657
  used_col_aliases.key?(key_alias)
617
658
  cc_part_idx -= 1
@@ -781,7 +822,7 @@ module ActiveRecord
781
822
 
782
823
  pri_tbl = hm.active_record
783
824
  pri_key = hm.options[:primary_key] || pri_tbl.primary_key
784
- unless hm.active_record.column_names.include?(pri_key)
825
+ if hm.active_record.abstract_class || hm.active_record.column_names.exclude?(pri_key)
785
826
  # %%% When this gets hit then if an attempt is made to display the ERD, it might end up being blank
786
827
  nix << k
787
828
  next
@@ -907,21 +948,43 @@ JOIN (SELECT #{hm_selects.map { |s| "#{'br_t0.' if from_clause}#{s}" }.join(', '
907
948
  # Get foreign keys for anything marked to be auto-preloaded, or a self-referencing JOIN
908
949
  klass_cols = klass.column_names
909
950
  reflect_on_all_associations.each do |a|
910
- selects << a.foreign_key if a.belongs_to? && (preload_values.include?(a.name) ||
911
- (!a.options[:polymorphic] && a.klass == klass && klass_cols.include?(a.foreign_key)))
951
+ selects << a.foreign_key if a.belongs_to? &&
952
+ (preload_values.include?(a.name) ||
953
+ (!a.options[:polymorphic] && a.klass == klass && klass_cols.include?(a.foreign_key))
954
+ )
912
955
  end
913
956
  # ActiveStorage compatibility
914
957
  selects << 'service_name' if klass.name == 'ActiveStorage::Blob' && ActiveStorage::Blob.columns_hash.key?('service_name')
915
958
  selects << 'blob_id' if klass.name == 'ActiveStorage::Attachment' && ActiveStorage::Attachment.columns_hash.key?('blob_id')
916
959
  pieces, my_dsl = klass.brick_parse_dsl(join_array = ::Brick::JoinArray.new, [], translations = {}, false, nil, true)
917
960
  brick_select(
918
- where_values_hash, selects, nil, translations, join_array,
919
- { '_br' => (descrip_cols = [pieces, my_dsl]) }
961
+ selects, where_values_hash, nil, translations: translations, join_array: join_array,
962
+ cust_col_override: { '_br' => (descrip_cols = [pieces, my_dsl]) }
920
963
  )
921
- order_values = klass.primary_key
964
+ order_values = "#{klass.table_name}.#{klass.primary_key}"
922
965
  [self.select(selects), descrip_cols]
923
966
  end
924
967
 
968
+ def brick_uniq
969
+ begin
970
+ uniq
971
+ rescue ActiveModel::MissingAttributeError => e
972
+ # If this model has an #after_initialize then it might try to reference attributes we haven't brought in
973
+ if (err_msg = e.message).start_with?('missing attribute: ') &&
974
+ klass.column_names.include?(col_name = e.message[19..-1])
975
+ (dup_rel = dup).select_values << col_name
976
+ ret = dup_rel.brick_uniq
977
+ puts "*** WARNING: Missing field!
978
+ Might want to add this in your brick.rb:
979
+ ::Brick.always_load_fields = { #{klass.name.inspect} => [#{col_name.inspect}]}"
980
+ ::Brick.config.always_load_fields[klass.name] << col_name
981
+ ret
982
+ else
983
+ []
984
+ end
985
+ end
986
+ end
987
+
925
988
  private
926
989
 
927
990
  def shift_or_first(ary)
@@ -935,6 +998,8 @@ JOIN (SELECT #{hm_selects.map { |s| "#{'br_t0.' if from_clause}#{s}" }.join(', '
935
998
 
936
999
  alias _brick_find_sti_class find_sti_class
937
1000
  def find_sti_class(type_name)
1001
+ return if type_name.is_a?(Numeric)
1002
+
938
1003
  if ::Brick.sti_models.key?(type_name ||= name)
939
1004
  ::Brick.sti_models[type_name].fetch(:base, nil) || _brick_find_sti_class(type_name)
940
1005
  else
@@ -1042,7 +1107,7 @@ if ActiveSupport::Dependencies.respond_to?(:autoload_module!) # %%% Only works w
1042
1107
  end
1043
1108
  end
1044
1109
 
1045
- Module.class_exec do
1110
+ ::Brick::ADD_CONST_MISSING = lambda do
1046
1111
  alias _brick_const_missing const_missing
1047
1112
  def const_missing(*args)
1048
1113
  requested = args.first.to_s
@@ -1319,7 +1384,14 @@ class Object
1319
1384
  if (base_model = ::Brick.sti_models[full_model_name]&.fetch(:base, nil) || ::Brick.existing_stis[full_model_name]&.constantize)
1320
1385
  is_sti = true
1321
1386
  else
1322
- base_model = ::Brick.config.models_inherit_from
1387
+ # Class for auto-generated models to inherit from
1388
+ base_model = (::Brick.config.models_inherit_from ||= (app.config.brick.fetch(:models_inherit_from, nil) ||
1389
+ begin
1390
+ ::ApplicationRecord
1391
+ rescue StandardError => ex
1392
+ ::ActiveRecord::Base
1393
+ end))
1394
+
1323
1395
  end
1324
1396
  hmts = nil
1325
1397
  code = +"class #{full_name} < #{base_model.name}\n"
@@ -1486,8 +1558,10 @@ class Object
1486
1558
  assoc[:assoc_name]
1487
1559
  end
1488
1560
  options[:optional] = true if assoc.key?(:optional)
1489
- if assoc.key?(:polymorphic)
1490
- options[:polymorphic] = true
1561
+ if assoc.key?(:polymorphic) ||
1562
+ # If a polymorphic association is missing but could be established then go ahead and put it into place.
1563
+ relations[assoc[:inverse_table]][:class_name].constantize.reflect_on_all_associations.find { |inv_assoc| !inv_assoc.belongs_to? && inv_assoc.options[:as].to_s == assoc[:assoc_name] }
1564
+ assoc[:polymorphic] ||= (options[:polymorphic] = true)
1491
1565
  else
1492
1566
  need_class_name = singular_table_name.underscore != assoc_name
1493
1567
  need_fk = "#{assoc_name}_id" != assoc[:fk]
@@ -1548,7 +1622,11 @@ class Object
1548
1622
  assoc[:fk].to_sym
1549
1623
  end
1550
1624
  end
1551
- options[:inverse_of] = inverse_assoc_name.tr('.', '_').to_sym if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of)
1625
+ if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of) &&
1626
+ (klass = options[:class_name]&.constantize) && (ian = inverse_assoc_name.tr('.', '_').to_sym) &&
1627
+ (klass.is_brick? || klass.reflect_on_association(ian))
1628
+ options[:inverse_of] = ian
1629
+ end
1552
1630
 
1553
1631
  # Prepare a list of entries for "has_many :through"
1554
1632
  if macro == :has_many
@@ -1591,14 +1669,38 @@ class Object
1591
1669
  built_controller = Class.new(controller_base || ActionController::Base) do |new_controller_class|
1592
1670
  (namespace || Object).const_set(class_name.to_sym, new_controller_class)
1593
1671
 
1672
+ # Add a hash for the inline style to the content-security-policy if one is present
1673
+ self.define_method(:add_csp_hash) do |style_value = nil|
1674
+ if request.respond_to?(:content_security_policy) && (csp = request.content_security_policy)
1675
+ if (cspd = csp.directives.fetch('style-src'))
1676
+ if style_value
1677
+ if (nonce = ::ActionDispatch::ContentSecurityPolicy::Request::NONCE)
1678
+ request.env[nonce] = '' # Generally 'action_dispatch.content_security_policy_nonce'
1679
+ end
1680
+ # Keep only self, if present, and also add this value
1681
+ cspd.select! { |val| val == "'self'" }
1682
+ cspd << style_value
1683
+ else
1684
+ cspd << "'sha256-QHKxqKcUq7AER1QwEu5uQXRQwC8j4iTWkE8mpOmP7ms='"
1685
+ end
1686
+ cspd << 'https://cdn.jsdelivr.net'
1687
+ end
1688
+ if (cspd = csp.directives.fetch('script-src'))
1689
+ cspd << 'https://cdn.jsdelivr.net'
1690
+ end
1691
+ end
1692
+ end
1693
+
1594
1694
  # Brick-specific pages
1595
1695
  case plural_class_name
1596
1696
  when 'BrickGem'
1597
1697
  self.define_method :status do
1598
1698
  instance_variable_set(:@resources, ::Brick.get_status_of_resources)
1699
+ add_csp_hash
1599
1700
  end
1600
1701
  self.define_method :orphans do
1601
1702
  instance_variable_set(:@orphans, ::Brick.find_orphans(::Brick.set_db_schema(params).first))
1703
+ add_csp_hash
1602
1704
  end
1603
1705
  self.define_method :crosstab do
1604
1706
  @relations = ::Brick.relations.each_with_object({}) do |r, s|
@@ -1813,9 +1915,9 @@ class Object
1813
1915
 
1814
1916
  ar_relation = ActiveRecord.version < Gem::Version.new('4') ? real_model.preload : real_model.all
1815
1917
  params['_brick_is_api'] = true if (is_api = request.format == :js || current_api_root)
1816
- @_brick_params = ar_relation.brick_select(params, (selects ||= []), order_by,
1817
- translations = {},
1818
- join_array = ::Brick::JoinArray.new)
1918
+ @_brick_params = ar_relation.brick_select((selects ||= []), params: params, order_by: order_by,
1919
+ translations: (translations = {}),
1920
+ join_array: (join_array = ::Brick::JoinArray.new))
1819
1921
 
1820
1922
  if is_api # Asking for JSON?
1821
1923
  # Apply column renaming
@@ -1881,6 +1983,7 @@ class Object
1881
1983
  @_brick_hm_counts = real_model._br_hm_counts
1882
1984
  @_brick_join_array = join_array
1883
1985
  @_brick_erd = params['_brick_erd']&.to_i
1986
+ add_csp_hash
1884
1987
  end
1885
1988
  end
1886
1989
 
@@ -1911,6 +2014,7 @@ class Object
1911
2014
  self.define_method :show do
1912
2015
  _schema, @_is_show_schema_list = ::Brick.set_db_schema(params)
1913
2016
  instance_variable_set("@#{singular_table_name}".to_sym, find_obj)
2017
+ add_csp_hash("'unsafe-inline'")
1914
2018
  end
1915
2019
  end
1916
2020
 
@@ -1928,6 +2032,7 @@ class Object
1928
2032
  end if Object.const_defined?('ActiveStorage')
1929
2033
  end
1930
2034
  instance_variable_set("@#{singular_table_name}".to_sym, new_obj)
2035
+ add_csp_hash
1931
2036
  end
1932
2037
 
1933
2038
  params_name_sym = (params_name = "#{singular_table_name}_params").to_sym
@@ -1968,6 +2073,7 @@ class Object
1968
2073
  self.define_method :edit do
1969
2074
  _schema, @_is_show_schema_list = ::Brick.set_db_schema(params)
1970
2075
  instance_variable_set("@#{singular_table_name}".to_sym, find_obj)
2076
+ add_csp_hash
1971
2077
  end
1972
2078
 
1973
2079
  code << " def update\n"
@@ -209,10 +209,15 @@ function linkSchemas() {
209
209
  # paths['app/models'] << 'lib/brick/frameworks/active_record/models'
210
210
  config.brick = ActiveSupport::OrderedOptions.new
211
211
  ActiveSupport.on_load(:before_initialize) do |app|
212
+ if ::Rails.application.respond_to?(:reloader)
213
+ ::Rails.application.reloader.to_prepare { Module.class_exec &::Brick::ADD_CONST_MISSING }
214
+ else # For Rails < 5.0, just load it once at the start
215
+ Module.class_exec &::Brick::ADD_CONST_MISSING
216
+ end
217
+ require 'brick/join_array'
212
218
  is_development = (ENV['RAILS_ENV'] || ENV['RACK_ENV']) == 'development'
213
219
  ::Brick.enable_models = app.config.brick.fetch(:enable_models, true)
214
220
  ::Brick.enable_controllers = app.config.brick.fetch(:enable_controllers, is_development)
215
- require 'brick/join_array' if ::Brick.enable_controllers?
216
221
  ::Brick.enable_views = app.config.brick.fetch(:enable_views, is_development)
217
222
  ::Brick.enable_routes = app.config.brick.fetch(:enable_routes, is_development)
218
223
  ::Brick.skip_database_views = app.config.brick.fetch(:skip_database_views, false)
@@ -598,12 +603,14 @@ window.addEventListener(\"popstate\", linkSchemas);
598
603
  # ====================================
599
604
  if ::Brick.enable_views?
600
605
  # Add the params to the lookup_context so that we have context about STI classes when setting @_brick_model
601
- ActionView::ViewPaths.class_exec do
602
- alias :_brick_lookup_context :lookup_context
603
- def lookup_context(*args)
604
- ret = _brick_lookup_context(*args)
605
- @_lookup_context.instance_variable_set(:@_brick_req_params, params)
606
- ret
606
+ if ActionView.const_defined?('ViewPaths')
607
+ ActionView::ViewPaths.class_exec do
608
+ alias :_brick_lookup_context :lookup_context
609
+ def lookup_context(*args)
610
+ ret = _brick_lookup_context(*args)
611
+ @_lookup_context.instance_variable_set(:@_brick_req_params, params)
612
+ ret
613
+ end
607
614
  end
608
615
  end
609
616
 
@@ -678,11 +685,11 @@ window.addEventListener(\"popstate\", linkSchemas);
678
685
  rescue StandardError => e
679
686
  # Search through the routes to confirm that something might match (Devise stuff for instance, which has its own view templates),
680
687
  # and bubble the same exception (probably an ActionView::MissingTemplate) if a legitimate option is found.
681
- raise if ::Rails.application.routes.set.find { |x| args[1].include?(x.defaults[:controller]) && args[0] == x.defaults[:action] }
688
+ raise if ::Rails.application.routes.set.find { |x| args[1].include?(x.defaults[:controller]) && args[0] == x.defaults[:action] } &&
689
+ ActionView.version >= ::Gem::Version.new('5.0')
682
690
 
683
691
  find_template_err = e
684
692
  end
685
- # Used to also have: ActionView.version < ::Gem::Version.new('5.0') &&
686
693
  model_name = set_brick_model(args, @_brick_req_params)&.name
687
694
  end
688
695
 
@@ -736,11 +743,11 @@ window.addEventListener(\"popstate\", linkSchemas);
736
743
  end
737
744
  when 'show', 'new', 'update'
738
745
  hm_stuff << if hm_fk_name
739
- if hm_assoc.klass.column_names.include?(hm_fk_name) ||
746
+ if hm_assoc.klass.column_names.include?(hm_fk_name.to_s) ||
740
747
  (hm_fk_name.is_a?(String) && hm_fk_name.include?('.')) # HMT? (Could do a better check for this)
741
748
  predicates = path_keys(hm_assoc, hm_fk_name, pk).map do |k, v|
742
749
  if v == '[sti_type]'
743
- "'#{k}': (@#{obj_name}.#{hm_assoc.active_record.inheritance_column}).constantize.base_class.name"
750
+ "'#{k}': (@#{obj_name}.#{hm_assoc.active_record.inheritance_column})&.constantize&.base_class&.name"
744
751
  else
745
752
  v.is_a?(String) ? "'#{k}': '#{v}'" : "'#{k}': @#{obj_name}.#{v}"
746
753
  end
@@ -798,6 +805,10 @@ window.addEventListener(\"popstate\", linkSchemas);
798
805
  left: 0;
799
806
  }
800
807
 
808
+ .flashNotice {
809
+ color: green;
810
+ }
811
+
801
812
  h1, h3 {
802
813
  margin-bottom: 0;
803
814
  }
@@ -1313,7 +1324,7 @@ erDiagram
1313
1324
  </head>
1314
1325
  <body>
1315
1326
  <div id=\"titleBox\"><div id=\"titleSticky\">
1316
- <p style=\"color: green\"><%= notice if request.respond_to?(:flash) %></p>#{"
1327
+ <p class=\"flashNotice\"><%= notice if request.respond_to?(:flash) %></p>#{"
1317
1328
  #{schema_options}" if schema_options}
1318
1329
  <select id=\"tbl\">#{table_options}</select>
1319
1330
  <table id=\"resourceName\"><tr>
@@ -1453,11 +1464,11 @@ end
1453
1464
  # Must load all models, and then find what table names are represented
1454
1465
  # Easily could be multiple files involved (STI for instance)
1455
1466
  +"#{css}
1456
- <p style=\"color: green\"><%= notice if request.respond_to?(:flash) %></p>#{"
1467
+ <p class=\"flashNotice\"><%= notice if request.respond_to?(:flash) %></p>#{"
1457
1468
  #{schema_options}" if schema_options}
1458
1469
  <select id=\"tbl\">#{table_options}</select>
1459
1470
  <h1>Status</h1>
1460
- <table id=\"status\" class=\"shadow\"><thead><tr>
1471
+ <table id=\"resourceName\" class=\"shadow\"><thead><tr>
1461
1472
  <th>Resource</th>
1462
1473
  <th>Table</th>
1463
1474
  <th>Migration</th>
@@ -1476,7 +1487,11 @@ end
1476
1487
  kls = Object.const_get(::Brick.relations.fetch(r[0], nil)&.fetch(:class_name, nil))
1477
1488
  rescue
1478
1489
  end
1479
- kls.is_a?(Class) ? link_to(r[0], send(\"#\{kls._brick_index}_path\".to_sym)) : r[0] %></td>
1490
+ if kls.is_a?(Class) && (path_helper = respond_to?(bi_path = \"#\{kls._brick_index}_path\".to_sym) ? bi_path : nil)
1491
+ link_to(r[0], send(path_helper))
1492
+ else
1493
+ r[0]
1494
+ end %></td>
1480
1495
  <td<%= if r[1]
1481
1496
  ' class=\"orphan\"' unless ::Brick.relations.key?(r[1])
1482
1497
  else
@@ -1503,7 +1518,7 @@ end
1503
1518
  when 'orphans'
1504
1519
  if is_orphans
1505
1520
  +"#{css}
1506
- <p style=\"color: green\"><%= notice if request.respond_to?(:flash) %></p>#{"
1521
+ <p class=\"flashNotice\"><%= notice if request.respond_to?(:flash) %></p>#{"
1507
1522
  #{schema_options}" if schema_options}
1508
1523
  <select id=\"tbl\">#{table_options}</select>
1509
1524
  <h1>Orphans<%= \" for #\{}\" if false %></h1>
@@ -1536,9 +1551,11 @@ end
1536
1551
  base_model = (model = (obj = @#{obj_name})&.class).base_class
1537
1552
  see_all_path = send(\"#\{base_model._brick_index}_path\")
1538
1553
  #{(inh_col = @_brick_model.inheritance_column).present? &&
1539
- " if obj.respond_to?(:#{inh_col}) && (model_name = @#{obj_name}.#{inh_col}) != base_model.name
1554
+ " if obj.respond_to?(:#{inh_col}) && (model_name = @#{obj_name}.#{inh_col}) &&
1555
+ !model_name.is_a?(Numeric) && model_name != base_model.name
1540
1556
  see_all_path << \"?#{inh_col}=#\{model_name}\"
1541
- end"}
1557
+ end
1558
+ model_name = base_model.name if model_name.is_a?(Numeric)"}
1542
1559
  page_title = (\"#\{model_name ||= model.name}: #\{obj&.brick_descrip || controller_name}\")
1543
1560
  %></title>
1544
1561
  </head>
@@ -1550,7 +1567,7 @@ end
1550
1567
  c23.141-70.188,89.141-120.906,167.063-120.906c97.25,0,176,78.813,176,176C511.828,227.078,404.391,119.641,271.844,119.641z\" />
1551
1568
  </svg>
1552
1569
 
1553
- <p style=\"color: green\"><%= notice if request.respond_to?(:flash) %></p>#{"
1570
+ <p class=\"flashNotice\"><%= notice if request.respond_to?(:flash) %></p>#{"
1554
1571
  #{schema_options}" if schema_options}
1555
1572
  <select id=\"tbl\">#{table_options}</select>
1556
1573
  <table id=\"resourceName\"><td><h1><%= page_title %></h1></td>
@@ -1631,7 +1648,7 @@ end
1631
1648
  if bt.length < 4
1632
1649
  bt << (option_detail = [[\"(No #\{bt_name\} chosen)\", '^^^brick_NULL^^^']])
1633
1650
  # %%% Accommodate composite keys for obj.pk at the end here
1634
- collection, descrip_cols = bt_class&.order(obj_pk = bt_class.primary_key)&.brick_list
1651
+ collection, descrip_cols = bt_class&.order(Arel.sql(\"#\{bt_class.table_name}.#\{obj_pk = bt_class.primary_key}\"))&.brick_list
1635
1652
  collection&.each do |obj|
1636
1653
  option_detail << [
1637
1654
  obj.brick_descrip(
@@ -1712,15 +1729,22 @@ end
1712
1729
  else # We get an array back when AR < 4.2
1713
1730
  collection2 = collection.to_a.compact
1714
1731
  end
1715
- collection2 = collection2.uniq
1732
+ collection2 = collection2.brick_uniq
1716
1733
  if collection2.empty? %>
1717
1734
  <tr><td>(none)</td></tr>
1718
1735
  <% else
1719
- collection2.each do |#{hm_singular_name}| %>
1720
- <tr><td><%= br_descrip = #{hm_singular_name}.brick_descrip(
1721
- descrip_cols&.first&.map { |col| #{hm_singular_name}.send(col.last) }
1722
- )
1723
- link_to(br_descrip, #{hm.first.klass._brick_index(:singular)}_path(slashify(#{obj_pk}))) %></td></tr>
1736
+ collection2.each do |br_#{hm_singular_name}| %>
1737
+ <tr><td><%= br_descrip = if br_#{hm_singular_name}.respond_to?(descrip_cols&.first&.first&.last)
1738
+ br_#{hm_singular_name}.brick_descrip(
1739
+ descrip_cols&.first&.map { |col| br_#{hm_singular_name}.send(col.last) }
1740
+ )
1741
+ else # If the HM association has a scope, might not have picked up our SELECT detail
1742
+ pks = (klass = br_#{hm_singular_name}.class).primary_key
1743
+ pks = [pks] unless pks.is_a?(Array)
1744
+ pks.map! { |pk| br_#{hm_singular_name}.send(pk).to_s }
1745
+ \"#\{klass.name} ##\{pks.join(', ')}\"
1746
+ end
1747
+ link_to(br_descrip, #{hm.first.klass._brick_index(:singular)}_path(slashify(br_#{obj_pk}))) %></td></tr>
1724
1748
  <% end %>
1725
1749
  <% end %>
1726
1750
  </table>"
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 138
8
+ TINY = 140
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
@@ -56,7 +56,7 @@ if (is_ruby_2_7 = (ruby_version = ::Gem::Version.new(RUBY_VERSION)) >= ::Gem::Ve
56
56
  # end
57
57
  end
58
58
 
59
- # Add left_outer_join! to Associations::JoinDependency and Relation::QueryMethods
59
+ # Add left_outer_joins! to Associations::JoinDependency and Relation::QueryMethods
60
60
  if ActiveRecord.version >= ::Gem::Version.new('4') && ActiveRecord.version < ::Gem::Version.new('5')
61
61
  ::Brick::Util._patch_require(
62
62
  'active_record/associations/join_dependency.rb', '/activerecord', # /associations
@@ -84,7 +84,7 @@ if ActiveRecord.version >= ::Gem::Version.new('4') && ActiveRecord.version < ::G
84
84
  ['build_joins(arel, joins_values.flatten) unless joins_values.empty?',
85
85
  "build_joins(arel, joins_values.flatten) unless joins_values.empty?
86
86
  build_left_outer_joins(arel, left_outer_joins_values.flatten) unless left_outer_joins_values.empty?"
87
- ],
87
+ ],
88
88
  # Change 2 - Line 992
89
89
  ["raise 'unknown class: %s' % join.class.name
90
90
  end
@@ -99,8 +99,8 @@ if ActiveRecord.version >= ::Gem::Version.new('4') && ActiveRecord.version < ::G
99
99
  def build_join_query(manager, buckets, join_type)"
100
100
  ],
101
101
  # Change 3 - Line 1012
102
- ['join_infos = join_dependency.join_constraints stashed_association_joins',
103
- 'join_infos = join_dependency.join_constraints stashed_association_joins, join_type'
102
+ ['s = join_dependency.join_constraints stashed_association_joins',
103
+ 's = join_dependency.join_constraints stashed_association_joins, join_type'
104
104
  ]
105
105
  ],
106
106
  :QueryMethods
@@ -202,7 +202,11 @@ module Brick
202
202
  end
203
203
  end
204
204
 
205
- def get_bts_and_hms(model)
205
+ def get_bts_and_hms(model, recalculate = nil)
206
+ if !recalculate && (ret = model.instance_variable_get(:@_brick_bts_and_hms))
207
+ return ret
208
+ end
209
+
206
210
  model_cols = model.columns_hash
207
211
  pk_type = if (mpk = model.primary_key).is_a?(Array)
208
212
  # Composite keys should really use: model.primary_key.map { |pk_part| model_cols[pk_part].type }
@@ -228,8 +232,24 @@ module Brick
228
232
  # This will come up when using Devise invitable when invited_by_class_name is not
229
233
  # specified because in that circumstance it adds a polymorphic :invited_by association,
230
234
  # along with appropriate invited_by_type and invited_by_id columns.
231
- puts "Missing any real indication as to which models \"has_many\" this polymorphic BT in model #{a.active_record.name}:"
232
- puts " belongs_to :#{a.name}, polymorphic: true"
235
+
236
+ # See if any currently-loaded models have a has_many association over to this polymorphic belongs_to
237
+ hm_models = ActiveRecord::Base.descendants.select do |m|
238
+ m.reflect_on_all_associations.any? { |assoc| !assoc.belongs_to? && assoc.options[:as]&.to_sym == a.name }
239
+ end
240
+ # No need to include subclassed models if their parent is already in the list
241
+ hm_models.reject! { |m| hm_models.any? { |parent| parent != m && m < parent } }
242
+ if hm_models.empty?
243
+ puts "Missing any real indication as to which models \"has_many\" this polymorphic BT in model #{a.active_record.name}:"
244
+ puts " belongs_to :#{a.name}, polymorphic: true"
245
+ else
246
+ puts "Having analysed all currently-loaded models to infer the various polymorphic has_many associations for #{model.name}, here are the current results:"
247
+ puts "::Brick.polymorphics = { \"#{model.table_name}.#{a.name}\" =>
248
+ #{hm_models.map(&:name).inspect}
249
+ }"
250
+ puts 'If you add the above to your brick.rb, it will "cement" these options into place, and avoid this lookup process.'
251
+ s.first[a.foreign_key.to_s] = [a.name, hm_models, true]
252
+ end
233
253
  end
234
254
  else
235
255
  bt_key = a.foreign_key.is_a?(Array) ? a.foreign_key : a.foreign_key.to_s
@@ -254,7 +274,7 @@ module Brick
254
274
  next
255
275
  end
256
276
  else
257
- if !a.options.key?(:as) && a.klass.column_names.exclude?(a.foreign_key)
277
+ if !a.options.key?(:as) && a.klass.column_names.exclude?(a.foreign_key.to_s)
258
278
  options = ", #{a.options.map { |k, v| "#{k.inspect} => #{v.inspect}" }.join(', ')}" if a.options.present?
259
279
  puts "WARNING: Model #{model.name} has this association:
260
280
  has_many :#{a.name}#{options}
@@ -279,7 +299,7 @@ module Brick
279
299
  end
280
300
  end
281
301
  skip_hms.each { |k, _v| hms.delete(k) }
282
- [bts, hms]
302
+ model.instance_variable_set(:@_brick_bts_and_hms, [bts, hms]) # Cache and return this result
283
303
  end
284
304
 
285
305
  def exclude_column(table, col)
@@ -523,6 +543,10 @@ module Brick
523
543
  Brick.config.license = key
524
544
  end
525
545
 
546
+ def always_load_fields=(field_set)
547
+ Brick.config.always_load_fields = field_set
548
+ end
549
+
526
550
  # Load additional references (virtual foreign keys)
527
551
  # This is attempted early if a brick initialiser file is found, and then again as a failsafe at the end of our engine's initialisation
528
552
  # %%% Maybe look for differences the second time 'round and just add new stuff instead of entirely deferring
@@ -922,11 +946,19 @@ In config/initializers/brick.rb appropriate entries would look something like:
922
946
  end
923
947
  end
924
948
 
925
- if ::Brick.config.add_status && instance_variable_get(:@set).named_routes.names.exclude?(:brick_status)
926
- get("/#{controller_prefix}brick_status", to: 'brick_gem#status', as: 'brick_status')
949
+ if ::Brick.config.add_status && (status_as = "#{controller_prefix.tr('/', '_')}brick_status".to_sym)
950
+ (
951
+ !(status_route = instance_variable_get(:@set).named_routes.find { |route| route.first == status_as }&.last) ||
952
+ !status_route.ast.to_s.include?("/#{controller_prefix}brick_status/")
953
+ )
954
+ get("/#{controller_prefix}brick_status", to: 'brick_gem#status', as: status_as.to_s)
927
955
  end
928
956
 
929
- if ::Brick.config.add_orphans && instance_variable_get(:@set).named_routes.names.exclude?(:brick_orphans)
957
+ if ::Brick.config.add_orphans && (orphans_as = "#{controller_prefix.tr('/', '_')}brick_orphans".to_sym)
958
+ (
959
+ !(orphans_route = instance_variable_get(:@set).named_routes.find { |route| route.first == orphans_as }&.last) ||
960
+ !orphans_route.ast.to_s.include?("/#{controller_prefix}brick_orphans/")
961
+ )
930
962
  get("/#{controller_prefix}brick_orphans", to: 'brick_gem#orphans', as: 'brick_orphans')
931
963
  end
932
964
 
@@ -1286,7 +1318,8 @@ ActiveSupport.on_load(:active_record) do
1286
1318
  # rubocop:enable Lint/ConstantDefinitionInBlock
1287
1319
 
1288
1320
  arsc = ::ActiveRecord::StatementCache
1289
- if is_ruby_2_7 && (params = arsc.method(:create).parameters).length == 2 && params.last == [:opt, :block]
1321
+ if is_ruby_2_7 && arsc.respond_to?(:create) &&
1322
+ (params = arsc.method(:create).parameters).length == 2 && params.last == [:opt, :block]
1290
1323
  arsc.class_exec do
1291
1324
  def self.create(connection, callable = nil, &block)
1292
1325
  relation = (callable || block).call ::ActiveRecord::StatementCache::Params.new
@@ -1379,7 +1412,7 @@ ActiveSupport.on_load(:active_record) do
1379
1412
  # def aliased_table_for(table_name, aliased_name, type_caster)
1380
1413
 
1381
1414
  class ActiveRecord::Associations::JoinDependency
1382
- if JoinBase.instance_method(:initialize).arity == 2 # Older ActiveRecord <= 5.1?
1415
+ if JoinBase.instance_method(:initialize).arity < 3 # Older ActiveRecord <= 5.1?
1383
1416
  def initialize(base, associations, joins, eager_loading: true)
1384
1417
  araat = ::ActiveRecord::Associations::AliasTracker
1385
1418
  if araat.respond_to?(:create_with_joins) # Rails 5.0 and 5.1
@@ -1387,17 +1420,28 @@ ActiveSupport.on_load(:active_record) do
1387
1420
  cwj_options << base.type_caster if araat.method(:create_with_joins).arity > 3 # Rails <= 5.1
1388
1421
  @alias_tracker = araat.create_with_joins(*cwj_options)
1389
1422
  @eager_loading = eager_loading # (Unused in Rails 5.0)
1390
- else # Rails 4.2
1423
+ elsif araat.respond_to?(:create) # Rails 4.1 and 4.2
1391
1424
  @alias_tracker = araat.create(base.connection, joins)
1392
1425
  @alias_tracker.aliased_table_for(base, base.table_name) # Updates the count for base.table_name to 1
1426
+ else # Rails 4.0
1427
+ is_rails_4 = true
1428
+ @base_klass = base
1429
+ @table_joins = joins
1430
+ @join_parts = [JoinBase.new(base)]
1431
+ @associations = {}
1432
+ @reflections = []
1433
+ @alias_tracker = araat.new(base.connection, joins)
1434
+ @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1
1435
+ tree = build(associations)
1393
1436
  end
1394
- tree = self.class.make_tree associations
1437
+ tree ||= self.class.make_tree associations
1395
1438
 
1396
1439
  # Provide a way to find the original relation that this tree is being used for
1397
1440
  # (so that we can maintain a list of links for all tables used in JOINs)
1398
1441
  if (relation = associations.instance_variable_get(:@relation))
1399
1442
  tree.instance_variable_set(:@relation, relation)
1400
1443
  end
1444
+ return if is_rails_4 # Rails 4.0 doesn't know about the rest
1401
1445
 
1402
1446
  @join_root = JoinBase.new base, build(tree, base)
1403
1447
  @join_root.children.each { |child| construct_tables! @join_root, child }
@@ -1530,6 +1574,21 @@ if ActiveRecord.version < ::Gem::Version.new('5.2')
1530
1574
  end
1531
1575
  end
1532
1576
 
1577
+ # By default the awesome_nested_set gem from CollectiveIdea does not prefix the ORDER BY column with its table name.
1578
+ # You can see this snag in action in the popular Spree project -- check out the Taxonomy model. Here is a fix:
1579
+ if Gem::Specification.all_names.find { |g| g.start_with?('awesome_nested_set-') }
1580
+ require 'awesome_nested_set/columns'
1581
+ ::CollectiveIdea::Acts::NestedSet::Columns.class_exec do
1582
+ alias _brick_order_column_name order_column_name
1583
+ def order_column_name
1584
+ unless (ord_col = _brick_order_column_name).start_with?(tbl_prefix = "#{table_name}.")
1585
+ ord_col = tbl_prefix << ord_col
1586
+ end
1587
+ ord_col
1588
+ end
1589
+ end
1590
+ end
1591
+
1533
1592
  # The "brick_links" patch -- this finds how every AR chain of association names
1534
1593
  # relates back to an exact table correlation name chosen by AREL when the AST tree is
1535
1594
  # walked. For instance, from a Customer model there could be a join_tree such as
@@ -1559,21 +1618,40 @@ module ActiveRecord
1559
1618
  _brick_build_join_query(manager, buckets, *args) # , **kwargs)
1560
1619
  end
1561
1620
 
1562
- else
1563
-
1621
+ else # elsif private_instance_methods.include?(:select_association_list)
1564
1622
  alias _brick_select_association_list select_association_list
1565
1623
  def select_association_list(associations, stashed_joins = nil)
1566
1624
  result = _brick_select_association_list(associations, stashed_joins)
1567
1625
  result.instance_variable_set(:@relation, self)
1568
1626
  result
1569
1627
  end
1628
+
1629
+ # else # Rails 4.1 ? and older
1630
+ # alias _brick_build_joins build_joins
1631
+ # def build_joins(manager, joins)
1632
+ # result = _brick_build_joins(manager, joins)
1633
+ # result.instance_variable_set(:@relation, self)
1634
+ # result
1635
+ # end
1570
1636
  end
1571
1637
  end
1572
1638
 
1573
1639
  # require 'active_record/associations/join_dependency'
1574
1640
  module Associations
1575
- # For AR >= 4.2
1576
- if self.const_defined?('JoinDependency')
1641
+ if self.const_defined?('JoinHelper') # ActiveRecord < 4.1
1642
+ module JoinHelper
1643
+ alias _brick_construct_tables construct_tables
1644
+ def construct_tables
1645
+ result = _brick_construct_tables
1646
+ # Capture the table alias name that was chosen
1647
+ # if (relation = node.instance_variable_get(:@assocs)&.instance_variable_get(:@relation))
1648
+ # link_path = node.instance_variable_get(:@link_path)
1649
+ # relation.brick_links[link_path] = result.first.table_alias || result.first.table_name
1650
+ # end
1651
+ result
1652
+ end
1653
+ end
1654
+ else # For AR >= 4.2
1577
1655
  class JoinDependency
1578
1656
  # An intelligent .eager_load() and .includes() that creates t0_r0 style aliases only for the columns
1579
1657
  # used in .select(). To enable this behaviour, include the flag :_brick_eager_load as the first
@@ -1647,7 +1725,7 @@ module ActiveRecord
1647
1725
  associations.map do |name, right|
1648
1726
  reflection = find_reflection base_klass, name
1649
1727
  reflection.check_validity!
1650
- reflection.check_eager_loadable!
1728
+ reflection.check_eager_loadable! if reflection.respond_to?(:check_eager_loadable!) # Used in AR >= 4.2
1651
1729
 
1652
1730
  if reflection.polymorphic?
1653
1731
  raise EagerLoadPolymorphicError.new(reflection)
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.138
4
+ version: 1.0.140
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorin Thwaits
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-05-06 00:00:00.000000000 Z
11
+ date: 2023-05-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord