brick 1.0.38 → 1.0.41

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: 4fcc487d2177c77f487d6d7ec910d0cb1ec61e1a9da77fdbd7e7bd413ebe7373
4
- data.tar.gz: 618bb20e4ff8aea7a81f02191100cb9bf15905a69099a8bbaec56cea6f27e882
3
+ metadata.gz: 4455ebfde80fe8a4bd51f19438171abae2614e0740adba98ab39184952843a01
4
+ data.tar.gz: d5157748c37564156fc2f823cc3c4753b536491aa2cf81175683c8241fb2a2b9
5
5
  SHA512:
6
- metadata.gz: 4d71c229f3c9ae97866aeeb5f852504f593136de06dbb5b500917454d38a1f9b3afb17ab7ba0b1bfc21e9371739f37af0b4d88534d6db5332c88c4dafbccf67a
7
- data.tar.gz: e1354cc5c4fb808aea3eb2a3b4580cdc31a37e51c799581d8461de21af520e1dab66793815e7a3ea8b3fc31b39b1e520ffb5150e7e6646279a2e3fe10933b770
6
+ metadata.gz: 2a649c78c3e597004b36d6c33f1cd812a712fcfd561dfe94994de28e489de81508f9fea30b0896b4447c4ce0ffd0877fb8c5d7e20a34011212fd797a3cbd02ca
7
+ data.tar.gz: 58ecdc4acb12abb77169a8fda8cb8146fceaedeacdf77f1cec992008f98b05412e3a9e2f5b6a9721880caa710ba94792a187d3587248eea4d9946d309c91be56
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_record/version'
4
-
5
4
  # ActiveRecord before 4.0 didn't have #version
6
5
  unless ActiveRecord.respond_to?(:version)
7
6
  module ActiveRecord
@@ -11,6 +10,16 @@ unless ActiveRecord.respond_to?(:version)
11
10
  end
12
11
  end
13
12
 
13
+ require 'action_view'
14
+ # Older ActionView didn't have #version
15
+ unless ActionView.respond_to?(:version)
16
+ module ActionView
17
+ def self.version
18
+ ActionPack.version
19
+ end
20
+ end
21
+ end
22
+
14
23
  # In ActiveSupport older than 5.0, the duplicable? test tries to new up a BigDecimal,
15
24
  # and Ruby 2.6 and later deprecates #new. This removes the warning from BigDecimal.
16
25
  # This compatibility needs to be put into place in the application's "config/boot.rb"
data/lib/brick/config.rb CHANGED
@@ -205,5 +205,10 @@ module Brick
205
205
  def not_nullables=(columns)
206
206
  @mutex.synchronize { @not_nullables = columns }
207
207
  end
208
+
209
+ # Add a special page to show references to non-existent records ("orphans")
210
+ def add_orphans
211
+ true
212
+ end
208
213
  end
209
214
  end
@@ -70,7 +70,11 @@ module ActiveRecord
70
70
  def self._brick_primary_key(relation = nil)
71
71
  return instance_variable_get(:@_brick_primary_key) if instance_variable_defined?(:@_brick_primary_key)
72
72
 
73
- pk = primary_key.is_a?(String) ? [primary_key] : primary_key || []
73
+ pk = begin
74
+ primary_key.is_a?(String) ? [primary_key] : primary_key || []
75
+ rescue
76
+ []
77
+ end
74
78
  # Just return [] if we're missing any part of the primary key. (PK is usually just "id")
75
79
  if relation && pk.present?
76
80
  @_brick_primary_key ||= pk.any? { |pk_part| !relation[:cols].key?(pk_part) } ? [] : pk
@@ -204,7 +208,7 @@ module ActiveRecord
204
208
  assoc_name = CGI.escapeHTML(assoc_name.to_s)
205
209
  model_path = Rails.application.routes.url_helpers.send("#{model_underscore.tr('/', '_').pluralize}_path".to_sym)
206
210
  av_class = Class.new.extend(ActionView::Helpers::UrlHelper)
207
- av_class.extend(ActionView::Helpers::TagHelper) if ActionView.version < ::Gem::Version.new('6.1')
211
+ av_class.extend(ActionView::Helpers::TagHelper) if ActionView.version < ::Gem::Version.new('7')
208
212
  link = av_class.link_to(name, model_path)
209
213
  model_underscore == assoc_name ? link : "#{assoc_name}-#{link}".html_safe
210
214
  end
@@ -411,14 +415,16 @@ module ActiveRecord
411
415
  hm_counts.each do |k, hm|
412
416
  associative = nil
413
417
  count_column = if hm.options[:through]
414
- fk_col = (associative = associatives[hm.name]).foreign_key
415
- hm.foreign_key
418
+ fk_col = (associative = associatives[hm.name])&.foreign_key
419
+ hm.foreign_key if fk_col
416
420
  else
417
421
  fk_col = hm.foreign_key
418
422
  poly_type = hm.inverse_of.foreign_type if hm.options.key?(:as)
419
423
  pk = hm.klass.primary_key
420
424
  (pk.is_a?(Array) ? pk.first : pk) || '*'
421
425
  end
426
+ next unless count_column # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
427
+
422
428
  tbl_alias = "_br_#{hm.name}"
423
429
  pri_tbl = hm.active_record
424
430
  on_clause = []
@@ -552,7 +558,7 @@ Module.class_exec do
552
558
  # return my_const
553
559
  end
554
560
 
555
- relations = ::Brick.instance_variable_get(:@relations)[ActiveRecord::Base.connection_pool.object_id] || {}
561
+ relations = ::Brick.relations
556
562
  # puts "ON OBJECT: #{args.inspect}" if self.module_parent == Object
557
563
  result = if ::Brick.enable_controllers? && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
558
564
  # Otherwise now it's up to us to fill in the gaps
@@ -561,7 +567,9 @@ Module.class_exec do
561
567
  full_class_name = +''
562
568
  full_class_name << "::#{self.name}" unless self == Object
563
569
  full_class_name << "::#{plural_class_name.underscore.singularize.camelize}"
564
- if (plural_class_name == 'BrickSwagger' || model = self.const_get(full_class_name))
570
+ if (plural_class_name == 'BrickSwagger' ||
571
+ (::Brick.config.add_orphans && plural_class_name == 'BrickGem') ||
572
+ model = self.const_get(full_class_name))
565
573
  # if it's a controller and no match or a model doesn't really use the same table name, eager load all models and try to find a model class of the right name.
566
574
  Object.send(:build_controller, self, class_name, plural_class_name, model, relations)
567
575
  end
@@ -627,8 +635,8 @@ Module.class_exec do
627
635
  # module_prefixes.unshift('') unless module_prefixes.first.blank?
628
636
  # candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb')
629
637
  self._brick_const_missing(*args)
630
- elsif self != Object
631
- module_parent.const_missing(*args)
638
+ # elsif self != Object
639
+ # module_parent.const_missing(*args)
632
640
  else
633
641
  puts "MISSING! #{self.name} #{args.inspect} #{table_name}"
634
642
  self._brick_const_missing(*args)
@@ -893,6 +901,16 @@ class Object
893
901
  built_controller = Class.new(ActionController::Base) do |new_controller_class|
894
902
  (namespace || Object).const_set(class_name.to_sym, new_controller_class)
895
903
 
904
+ # Brick-specific pages
905
+ if plural_class_name == 'BrickGem'
906
+ self.define_method :orphans do
907
+ instance_variable_set(:@orphans, ::Brick.find_orphans(::Brick.set_db_schema(params)))
908
+ puts "BrickGemController #{action_name} #{params.inspect}"
909
+ # render inline: 'Brick gem!'
910
+ end
911
+ return [new_controller_class, code + ' # BrickGem controller!']
912
+ end
913
+
896
914
  unless (is_swagger = plural_class_name == 'BrickSwagger') # && request.format == :json)
897
915
  code << " def index\n"
898
916
  code << " @#{table_name} = #{model.name}#{pk&.present? ? ".order(#{pk.inspect})" : '.all'}\n"
@@ -1188,8 +1206,13 @@ module ActiveRecord::ConnectionHandling
1188
1206
  case ActiveRecord::Base.connection.adapter_name
1189
1207
  when 'PostgreSQL', 'SQLite' # These bring back a hash for each row because the query uses column aliases
1190
1208
  # schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1209
+ ar_smtn = if ActiveRecord::Base.respond_to?(:schema_migrations_table_name)
1210
+ ActiveRecord::Base.schema_migrations_table_name
1211
+ else
1212
+ 'schema_migrations'
1213
+ end
1191
1214
  ar_imtn = ActiveRecord.version >= ::Gem::Version.new('5.0') ? ActiveRecord::Base.internal_metadata_table_name : ''
1192
- ActiveRecord::Base.execute_sql(sql, ActiveRecord::Base.schema_migrations_table_name, ar_imtn).each do |r|
1215
+ ActiveRecord::Base.execute_sql(sql, ar_smtn, ar_imtn).each do |r|
1193
1216
  # If Apartment gem lists the table as being associated with a non-tenanted model then use whatever it thinks
1194
1217
  # is the default schema, usually 'public'.
1195
1218
  schema_name = if ::Brick.config.schema_behavior[:multitenant]
@@ -1466,5 +1489,72 @@ module Brick
1466
1489
  end
1467
1490
  assoc_bt[:inverse] = assoc_hm
1468
1491
  end
1492
+
1493
+ # Locate orphaned records
1494
+ def find_orphans(multi_schema)
1495
+ is_default_schema = multi_schema&.==(Apartment.default_schema)
1496
+ relations.each_with_object([]) do |v, s|
1497
+ frn_tbl = v.first
1498
+ next if (relation = v.last).key?(:isView) || config.exclude_tables.include?(frn_tbl) ||
1499
+ !(for_pk = (relation[:pkey].values.first&.first))
1500
+
1501
+ is_default_frn_schema = !is_default_schema && multi_schema &&
1502
+ ((frn_parts = frn_tbl.split('.')).length > 1 && frn_parts.first)&.==(Apartment.default_schema)
1503
+ relation[:fks].select { |_k, assoc| assoc[:is_bt] }.each do |_k, bt|
1504
+ begin
1505
+ if bt.key?(:polymorphic)
1506
+ pri_pk = for_pk
1507
+ pri_tables = Brick.config.polymorphics["#{frn_tbl}.#{bt[:fk]}"]
1508
+ .each_with_object(Hash.new { |h, k| h[k] = [] }) do |pri_class, s|
1509
+ s[Object.const_get(pri_class).table_name] << pri_class
1510
+ end
1511
+ fk_id_col = "#{bt[:fk]}_id"
1512
+ fk_type_col = "#{bt[:fk]}_type"
1513
+ selects = []
1514
+ pri_tables.each do |pri_tbl, pri_types|
1515
+ # Skip if database is multitenant, we're not focused on "public", and the foreign and primary tables
1516
+ # are both in the "public" schema
1517
+ next if is_default_frn_schema &&
1518
+ ((pri_parts = pri_tbl&.split('.'))&.length > 1 && pri_parts.first)&.==(Apartment.default_schema)
1519
+
1520
+ selects << "SELECT '#{pri_tbl}' AS pri_tbl, frn.#{fk_type_col} AS pri_type, frn.#{fk_id_col} AS pri_id, frn.#{for_pk} AS frn_id
1521
+ FROM #{frn_tbl} AS frn
1522
+ LEFT OUTER JOIN #{pri_tbl} AS pri ON pri.#{pri_pk} = frn.#{fk_id_col}
1523
+ WHERE frn.#{fk_type_col} IN (#{
1524
+ pri_types.map { |pri_type| "'#{pri_type}'" }.join(', ')
1525
+ }) AND frn.#{bt[:fk]}_id IS NOT NULL AND pri.#{pri_pk} IS NULL\n"
1526
+ end
1527
+ ActiveRecord::Base.execute_sql(selects.join("UNION ALL\n")).each do |o|
1528
+ entry = [frn_tbl, o['frn_id'], o['pri_type'], o['pri_id'], fk_id_col]
1529
+ entry << o['pri_tbl'] if (pri_class = Object.const_get(o['pri_type'])) != pri_class.base_class
1530
+ s << entry
1531
+ end
1532
+ else
1533
+ # Skip if database is multitenant, we're not focused on "public", and the foreign and primary tables
1534
+ # are both in the "public" schema
1535
+ pri_tbl = bt.key?(:inverse_table) && bt[:inverse_table]
1536
+ next if is_default_frn_schema &&
1537
+ ((pri_parts = pri_tbl&.split('.'))&.length > 1 && pri_parts.first)&.==(Apartment.default_schema)
1538
+
1539
+ pri_pk = relations[pri_tbl].fetch(:pkey, nil)&.values&.first&.first ||
1540
+ _class_pk(pri_tbl, multi_schema)
1541
+ ActiveRecord::Base.execute_sql(
1542
+ "SELECT frn.#{bt[:fk]} AS pri_id, frn.#{for_pk} AS frn_id
1543
+ FROM #{frn_tbl} AS frn
1544
+ LEFT OUTER JOIN #{pri_tbl} AS pri ON pri.#{pri_pk} = frn.#{bt[:fk]}
1545
+ WHERE frn.#{bt[:fk]} IS NOT NULL AND pri.#{pri_pk} IS NULL
1546
+ ORDER BY 1, 2"
1547
+ ).each { |o| s << [frn_tbl, o['frn_id'], pri_tbl, o['pri_id'], bt[:fk]] }
1548
+ end
1549
+ rescue StandardError => err
1550
+ puts "Strange -- #{err.inspect}"
1551
+ end
1552
+ end
1553
+ end
1554
+ end
1555
+
1556
+ def _class_pk(dotted_name, multitenant)
1557
+ Object.const_get((multitenant ? [dotted_name.split('.').last] : dotted_name.split('.')).map { |nm| "::#{nm.singularize.camelize}" }.join).primary_key
1558
+ end
1469
1559
  end
1470
1560
  end
@@ -50,23 +50,28 @@ module Brick
50
50
  # ====================================
51
51
  if ::Brick.enable_views?
52
52
  ActionView::LookupContext.class_exec do
53
+ # Used by Rails 5.0 and above
53
54
  alias :_brick_template_exists? :template_exists?
54
55
  def template_exists?(*args, **options)
55
- unless (is_template_exists = _brick_template_exists?(*args, **options))
56
- # Need to return true if we can fill in the blanks for a missing one
57
- # args will be something like: ["index", ["categories"]]
58
- args[1] = args[1].each_with_object([]) { |a, s| s.concat(a.split('/')) }
59
- args[1][args[1].length - 1] = args[1].last.singularize # Make sure the last item, defining the class name, is singular
60
- model = args[1].map(&:camelize).join('::').constantize
61
- if is_template_exists = model && (
62
- ['index', 'show'].include?(args.first) || # Everything has index and show
56
+ (::Brick.config.add_orphans && args.first == 'orphans') ||
57
+ _brick_template_exists?(*args, **options) ||
58
+ set_brick_model(args)
59
+ end
60
+
61
+ def set_brick_model(find_args)
62
+ # Need to return true if we can fill in the blanks for a missing one
63
+ # args will be something like: ["index", ["categories"]]
64
+ find_args[1] = find_args[1].each_with_object([]) { |a, s| s.concat(a.split('/')) }
65
+ if (class_name = find_args[1].last&.singularize)
66
+ find_args[1][find_args[1].length - 1] = class_name # Make sure the last item, defining the class name, is singular
67
+ if (model = find_args[1].map(&:camelize).join('::').constantize) && (
68
+ ['index', 'show'].include?(find_args.first) || # Everything has index and show
63
69
  # Only CUD stuff has create / update / destroy
64
- (!model.is_view? && ['new', 'create', 'edit', 'update', 'destroy'].include?(args.first))
70
+ (!model.is_view? && ['new', 'create', 'edit', 'update', 'destroy'].include?(find_args.first))
65
71
  )
66
72
  @_brick_model = model
67
73
  end
68
74
  end
69
- is_template_exists
70
75
  end
71
76
 
72
77
  def path_keys(hm_assoc, fk_name, obj_name, pk)
@@ -82,43 +87,58 @@ module Brick
82
87
 
83
88
  alias :_brick_find_template :find_template
84
89
  def find_template(*args, **options)
85
- return _brick_find_template(*args, **options) unless @_brick_model
86
-
87
- model_name = @_brick_model.name
88
- pk = @_brick_model._brick_primary_key(::Brick.relations.fetch(model_name, nil))
89
- obj_name = model_name.split('::').last.underscore
90
- path_obj_name = model_name.underscore.tr('/', '_')
91
- table_name = obj_name.pluralize
92
- template_link = nil
93
- bts, hms, associatives = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
94
- hms_columns = [] # Used for 'index'
95
- skip_klass_hms = ::Brick.config.skip_index_hms[model_name] || {}
96
- hms_headers = hms.each_with_object([]) do |hm, s|
97
- hm_stuff = [(hm_assoc = hm.last), "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]}", (assoc_name = hm.first)]
98
- hm_fk_name = if hm_assoc.options[:through]
99
- associative = associatives[hm_assoc.name]
100
- "'#{associative.name}.#{associative.foreign_key}'"
101
- else
102
- hm_assoc.foreign_key
103
- end
104
- if args.first == 'index'
105
- hms_columns << if hm_assoc.macro == :has_many
106
- set_ct = if skip_klass_hms.key?(assoc_name.to_sym)
107
- 'nil'
108
- else
109
- # Postgres column names are limited to 63 characters
110
- attrib_name = "_br_#{assoc_name}_ct"[0..62]
111
- "#{obj_name}.#{attrib_name} || 0"
112
- end
90
+ unless (model_name = (
91
+ @_brick_model ||
92
+ (ActionView.version < ::Gem::Version.new('5.0') && args[1].is_a?(Array) ? set_brick_model(args) : nil)
93
+ )&.name) ||
94
+ (is_orphans = ::Brick.config.add_orphans && args[0..1] == ['orphans', ['brick_gem']])
95
+ return _brick_find_template(*args, **options)
96
+ end
97
+
98
+ unless is_orphans
99
+ pk = @_brick_model._brick_primary_key(::Brick.relations.fetch(model_name, nil))
100
+ obj_name = model_name.split('::').last.underscore
101
+ path_obj_name = model_name.underscore.tr('/', '_')
102
+ table_name = obj_name.pluralize
103
+ template_link = nil
104
+ bts, hms, associatives = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
105
+ hms_columns = [] # Used for 'index'
106
+ skip_klass_hms = ::Brick.config.skip_index_hms[model_name] || {}
107
+ hms_headers = hms.each_with_object([]) do |hm, s|
108
+ hm_stuff = [(hm_assoc = hm.last), "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]}", (assoc_name = hm.first)]
109
+ hm_fk_name = if hm_assoc.options[:through]
110
+ associative = associatives[hm_assoc.name]
111
+ associative && "'#{associative.name}.#{associative.foreign_key}'"
112
+ else
113
+ hm_assoc.foreign_key
114
+ end
115
+ if args.first == 'index'
116
+ hms_columns << if hm_assoc.macro == :has_many
117
+ set_ct = if skip_klass_hms.key?(assoc_name.to_sym)
118
+ 'nil'
119
+ else
120
+ # Postgres column names are limited to 63 characters
121
+ attrib_name = "_br_#{assoc_name}_ct"[0..62]
122
+ "#{obj_name}.#{attrib_name} || 0"
123
+ end
124
+ if hm_fk_name
113
125
  "<%= ct = #{set_ct}
114
126
  link_to \"#\{ct || 'View'\} #{assoc_name}\", #{hm_assoc.klass.name.underscore.tr('/', '_').pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, obj_name, pk)} }) unless ct&.zero? %>\n"
115
- else # has_one
127
+ else
128
+ "#{assoc_name}\n"
129
+ end
130
+ else # has_one
116
131
  "<%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>\n"
117
- end
118
- elsif args.first == 'show'
119
- hm_stuff << "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.tr('/', '_').pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, "@#{obj_name}", pk)} }) %>\n"
132
+ end
133
+ elsif args.first == 'show'
134
+ hm_stuff << if hm_fk_name
135
+ "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.tr('/', '_').pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, "@#{obj_name}", pk)} }) %>\n"
136
+ else
137
+ assoc_name
138
+ end
139
+ end
140
+ s << hm_stuff
120
141
  end
121
- s << hm_stuff
122
142
  end
123
143
 
124
144
  schema_options = ::Brick.db_schemas.keys.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
@@ -133,6 +153,7 @@ module Brick
133
153
  end.sort.each_with_object(+'') do |v, s|
134
154
  s << "<option value=\"#{v.underscore.gsub('.', '/').pluralize}\">#{v}</option>"
135
155
  end.html_safe
156
+ table_options << '<option value="brick_orphans">(Orphans)</option>'.html_safe if is_orphans
136
157
  css = +"<style>
137
158
  #dropper {
138
159
  background-color: #eee;
@@ -488,7 +509,7 @@ if (headerTop) {
488
509
  ::Brick.config.metadata_columns.include?(col_name) || poly_cols.include?(col_name)
489
510
 
490
511
  col_order << col_name
491
- %><th<%= \" title = \\\"#\{col.comment}\\\"\".html_safe unless col.comment.blank? %>><%
512
+ %><th<%= \" title = \\\"#\{col.comment}\\\"\".html_safe if col.respond_to?(:comment) && !col.comment.blank? %>><%
492
513
  if (bt = bts[col_name]) %>
493
514
  BT <%
494
515
  bt[1].each do |bt_pair| %><%=
@@ -500,7 +521,13 @@ if (headerTop) {
500
521
  %></th><%
501
522
  end
502
523
  # Consider getting the name from the association -- h.first.name -- if a more \"friendly\" alias should be used for a screwy table name
503
- %>#{hms_headers.map { |h| "<th>#{h[1]} <%= link_to('#{h[2]}', #{h.first.klass.name.underscore.tr('/', '_').pluralize}_path) %></th>" }.join
524
+ %>#{hms_headers.map do |h|
525
+ if h.first.options[:through] && !h.first.through_reflection
526
+ "<th>#{h[1]} #{h[2]} %></th>"
527
+ else
528
+ "<th>#{h[1]} <%= link_to('#{h[2]}', #{h.first.klass.name.underscore.tr('/', '_').pluralize}_path) %></th>"
529
+ end
530
+ end.join
504
531
  }</tr></thead>
505
532
 
506
533
  <tbody>
@@ -522,6 +549,7 @@ if (headerTop) {
522
549
  # 0..62 because Postgres column names are limited to 63 characters
523
550
  #{obj_name}, (descrips = @_brick_bt_descrip[bt.first][bt_class])[0..-2].map { |z| #{obj_name}.send(z.last[0..62]) }, (bt_id_col = descrips.last)
524
551
  )
552
+ bt_txt ||= \"<< Orphaned ID: #\{val} >>\" if val
525
553
  bt_id = #{obj_name}.send(*bt_id_col) if bt_id_col&.present? %>
526
554
  <%= bt_id ? link_to(bt_txt, send(\"#\{bt_class.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, bt_id)) : bt_txt %>
527
555
  <%#= Previously was: bt_obj = bt[1].first.first.find_by(bt[2] => val); link_to(bt_obj.brick_descrip, send(\"#\{bt[1].first.first.name.underscore\}_path\".to_sym, bt_obj.send(bt[1].first.first.primary_key.to_sym))) if bt_obj %>
@@ -539,6 +567,22 @@ if (headerTop) {
539
567
 
540
568
  #{"<hr><%= link_to \"New #{obj_name}\", new_#{path_obj_name}_path %>" unless @_brick_model.is_view?}
541
569
  #{script}"
570
+ when 'orphans'
571
+ if is_orphans
572
+ "#{css}
573
+ <p style=\"color: green\"><%= notice %></p>#{"
574
+ <select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
575
+ <select id=\"tbl\">#{table_options}</select>
576
+ <h1>Orphans<%= \" for #\{}\" if false %></h1>
577
+ <% @orphans.each do |o|
578
+ via = \" (via #\{o[4]})\" unless \"#\{o[2].split('.').last.underscore.singularize}_id\" == o[4] %>
579
+ <a href=\"/<%= o[0].split('.').last %>/<%= o[1] %>\">
580
+ <%= \"#\{o[0]} #\{o[1]} refers#\{via} to non-existent #\{o[2]} #\{o[3]}#\{\" (in table \\\"#\{o[5]}\\\")\" if o[5]}\" %>
581
+ </a><br>
582
+ <% end %>
583
+ #{script}"
584
+ end
585
+
542
586
  when 'show', 'update'
543
587
  "#{css}
544
588
  <p style=\"color: green\"><%= notice %></p>#{"
@@ -562,7 +606,7 @@ end
562
606
  <tr>
563
607
  <% next if (#{(pk || []).inspect}.include?(k) && !bts.key?(k)) ||
564
608
  ::Brick.config.metadata_columns.include?(k) %>
565
- <th class=\"show-field\"<%= \" title = \\\"#\{col.comment}\\\"\".html_safe unless col.comment.blank? %>>
609
+ <th class=\"show-field\"<%= \" title = \\\"#\{col.comment}\\\"\".html_safe if col.respond_to?(:comment) && !col.comment.blank? %>>
566
610
  <% has_fields = true
567
611
  if (bt = bts[k])
568
612
  # Add a final member in this array with descriptive options to be used in <select> drop-downs
@@ -603,7 +647,11 @@ end
603
647
  html_options = { prompt: \"Select #\{bt_name\}\" }
604
648
  html_options[:class] = 'dimmed' unless val %>
605
649
  <%= f.select k.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
606
- <%= bt_obj = bt_class&.find_by(bt_pair[1] => val); link_to('⇛', send(\"#\{bt_class.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, bt_obj.send(bt_class.primary_key.to_sym)), { class: 'show-arrow' }) if bt_obj %>
650
+ <%= if (bt_obj = bt_class&.find_by(bt_pair[1] => val))
651
+ 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' })
652
+ elsif val
653
+ \"Orphaned ID: #\{val}\"
654
+ end %>
607
655
  <% else case #{model_name}.column_for_attribute(k).type
608
656
  when :string, :text %>
609
657
  <% if is_bcrypt?(val) # || .readonly? %>
@@ -634,6 +682,8 @@ end
634
682
  <% end %>
635
683
 
636
684
  #{hms_headers.each_with_object(+'') do |hm, s|
685
+ next if hm.first.options[:through] && !hm.first.through_reflection
686
+
637
687
  if (pk = hm.first.klass.primary_key)
638
688
  hm_singular_name = (hm_name = hm.first.name.to_s).singularize.underscore
639
689
  obj_pk = (pk.is_a?(Array) ? pk : [pk]).each_with_object([]) { |pk_part, s| s << "#{hm_singular_name}.#{pk_part}" }.join(', ')
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ if Object.const_defined?('::Rake::TaskManager')
4
+ namespace :brick do
5
+ desc 'Find any seemingly-orphaned records'
6
+ task orphans: :environment do
7
+ schema_list = ((multi = ::Brick.config.schema_behavior[:multitenant]) && ::Brick.db_schemas.keys.sort) || []
8
+ schema = if schema_list.length == 1
9
+ schema_list.first
10
+ elsif schema_list.length.positive?
11
+ require 'fancy_gets'
12
+ include FancyGets
13
+ gets_list(list: schema_list, chosen: multi[:schema_to_analyse])
14
+ end
15
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema) if schema
16
+ orphans = ::Brick.find_orphans(schema)
17
+ puts "Orphans in #{schema}:\n#{'=' * (schema.length + 12)}" if schema
18
+ if orphans.empty?
19
+ puts "No orphans!"
20
+ else
21
+ orphans.each do |o|
22
+ via = " (via #{o[4]})" unless "#{o[2].split('.').last.underscore.singularize}_id" == o[4]
23
+ puts "#{o[0]} #{o[1]} refers#{via} to non-existent #{o[2]} #{o[3]}#{" (in table \"#{o[5]}\")" if o[5]}"
24
+ end
25
+ puts
26
+ end
27
+ end
28
+ end
29
+ end
data/lib/brick/util.rb CHANGED
@@ -5,7 +5,7 @@ module Brick
5
5
  module Util
6
6
  # ===================================
7
7
  # Epic require patch
8
- def self._patch_require(module_filename, folder_matcher, search_text, replacement_text, autoload_symbol = nil, is_bundler = false)
8
+ def self._patch_require(module_filename, folder_matcher, replacements, autoload_symbol = nil, is_bundler = false)
9
9
  mod_name_parts = module_filename.split('.')
10
10
  extension = case mod_name_parts.last
11
11
  when 'rb', 'so', 'o'
@@ -28,10 +28,13 @@ module Brick
28
28
  Dir.mkdir(new_part) unless Dir.exist?(new_part)
29
29
  new_part
30
30
  end
31
- if ::Brick::Util._write_patched(folder_matcher, module_filename, extension, custom_require_dir, nil, search_text, replacement_text) &&
31
+ if ::Brick::Util._write_patched(folder_matcher, module_filename, extension, custom_require_dir, nil, replacements) &&
32
32
  !alp.include?(custom_require_dir)
33
33
  alp.unshift(custom_require_dir)
34
34
  end
35
+ # require 'pry-byebug'
36
+ # binding.pry
37
+ # z = 10
35
38
  elsif is_bundler
36
39
  puts "Bundler hack"
37
40
  require 'pry-byebug'
@@ -56,13 +59,13 @@ module Brick
56
59
  define_method(:require) do |name|
57
60
  puts name if name.to_s.include?('cucu')
58
61
  if (require_override = ::Brick::Util.instance_variable_get(:@_require_overrides)[name])
59
- extension, folder_matcher, search_text, replacement_text, autoload_symbol = require_override
62
+ extension, folder_matcher, replacements, autoload_symbol = require_override
60
63
  patched_filename = "/patched_#{name.tr('/', '_')}#{extension}"
61
64
  if $LOADED_FEATURES.find { |f| f.end_with?(patched_filename) }
62
65
  false
63
66
  else
64
67
  is_replaced = false
65
- if (replacement_path = ::Brick::Util._write_patched(folder_matcher, name, extension, ::Brick::Util._custom_require_dir, patched_filename, search_text, replacement_text))
68
+ if (replacement_path = ::Brick::Util._write_patched(folder_matcher, name, extension, ::Brick::Util._custom_require_dir, patched_filename, replacements))
66
69
  is_replaced = Kernel.send(:orig_require, replacement_path)
67
70
  elsif replacement_path.nil?
68
71
  puts "Couldn't find #{name} to require it!"
@@ -75,7 +78,7 @@ module Brick
75
78
  end
76
79
  end
77
80
  end
78
- require_overrides[module_filename] = [extension, folder_matcher, search_text, replacement_text, autoload_symbol]
81
+ require_overrides[module_filename] = [extension, folder_matcher, replacements, autoload_symbol]
79
82
  end
80
83
  end
81
84
 
@@ -94,7 +97,7 @@ module Brick
94
97
 
95
98
  # Returns the full path to the replaced filename, or
96
99
  # false if the file already exists, and nil if it was unable to write anything.
97
- def self._write_patched(folder_matcher, name, extension, dir, patched_filename, search_text, replacement_text)
100
+ def self._write_patched(folder_matcher, name, extension, dir, patched_filename, replacements)
98
101
  # See if our replacement file might already exist for some reason
99
102
  name = +"/#{name}" unless name.start_with?('/')
100
103
  name << extension unless name.end_with?(extension)
@@ -111,9 +114,13 @@ module Brick
111
114
  break if path.include?(folder_matcher) && (orig_as = File.open(orig_path))
112
115
  end
113
116
  puts [folder_matcher, name].inspect
114
- if (orig_text = orig_as&.read)
115
- File.open(replacement_path, 'w') do |replacement|
116
- num_written = replacement.write(orig_text.gsub(search_text, replacement_text))
117
+ if (updated_text = orig_as&.read)
118
+ File.open(replacement_path, 'w') do |replaced_file|
119
+ replacements = [replacements] unless replacements.first.is_a?(Array)
120
+ replacements.each do |search_text, replacement_text|
121
+ updated_text.gsub!(search_text, replacement_text)
122
+ end
123
+ num_written = replaced_file.write(updated_text)
117
124
  end
118
125
  orig_as.close
119
126
  end
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 38
8
+ TINY = 41
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
@@ -2,10 +2,10 @@
2
2
 
3
3
  require 'brick/compatibility'
4
4
 
5
- # Allow ActiveRecord 4.0 and 4.1 to work with newer Ruby (>= 2.4) by avoiding a "stack level too deep"
5
+ # Allow ActiveRecord 4.2.7 and older to work with newer Ruby (>= 2.4) by avoiding a "stack level too deep"
6
6
  # error when ActiveSupport tries to smarten up Numeric by messing with Fixnum and Bignum at the end of:
7
7
  # activesupport-4.0.13/lib/active_support/core_ext/numeric/conversions.rb
8
- if ActiveRecord.version < ::Gem::Version.new('4.2') &&
8
+ if ActiveRecord.version < ::Gem::Version.new('4.2.8') &&
9
9
  ActiveRecord.version > ::Gem::Version.new('3.2') &&
10
10
  Object.const_defined?('Integer') && Integer.superclass.name == 'Numeric'
11
11
  class OurFixnum < Integer; end
@@ -31,25 +31,78 @@ if (ruby_version = ::Gem::Version.new(RUBY_VERSION)) >= ::Gem::Version.new('2.7'
31
31
  # Remove circular reference for "now"
32
32
  ::Brick::Util._patch_require(
33
33
  'active_support/values/time_zone.rb', '/activesupport',
34
- ' def parse(str, now=now)',
35
- ' def parse(str, now=now())'
34
+ [' def parse(str, now=now)',
35
+ ' def parse(str, now=now())']
36
36
  )
37
37
  # Remove circular reference for "reflection" for ActiveRecord 3.1
38
38
  if ActiveRecord.version >= ::Gem::Version.new('3.1')
39
39
  ::Brick::Util._patch_require(
40
40
  'active_record/associations/has_many_association.rb', '/activerecord',
41
- 'reflection = reflection)',
42
- 'reflection = reflection())',
41
+ ['reflection = reflection)',
42
+ 'reflection = reflection())'],
43
43
  :HasManyAssociation # Make sure the path for this guy is available to be autoloaded
44
44
  )
45
45
  end
46
46
  end
47
47
 
48
+ # Add left_outer_join! to Associations::JoinDependency and Relation::QueryMethods
49
+ if ActiveRecord.version < ::Gem::Version.new('5')
50
+ is_add_left_outer_join = true
51
+ ::Brick::Util._patch_require(
52
+ 'active_record/associations/join_dependency.rb', '/activerecord', # /associations
53
+ ["def join_constraints(outer_joins)
54
+ joins = join_root.children.flat_map { |child|
55
+ make_inner_joins join_root, child
56
+ }",
57
+ "def join_constraints(outer_joins, join_type)
58
+ joins = join_root.children.flat_map { |child|
59
+
60
+ if join_type == Arel::Nodes::OuterJoin
61
+ make_left_outer_joins join_root, child
62
+ else
63
+ make_inner_joins join_root, child
64
+ end
65
+ }"],
66
+ :JoinDependency # This one is in an "eager_autoload do" -- so how to handle it?
67
+ )
68
+
69
+ # Three changes all in the same file, query_methods.rb:
70
+ ::Brick::Util._patch_require(
71
+ 'active_record/relation/query_methods.rb', '/activerecord',
72
+ [
73
+ # Change 1 - Line 904
74
+ ['build_joins(arel, joins_values.flatten) unless joins_values.empty?',
75
+ "build_joins(arel, joins_values.flatten) unless joins_values.empty?
76
+ build_left_outer_joins(arel, left_outer_joins_values.flatten) unless left_outer_joins_values.empty?"
77
+ ],
78
+ # Change 2 - Line 992
79
+ ["raise 'unknown class: %s' % join.class.name
80
+ end
81
+ end",
82
+ "raise 'unknown class: %s' % join.class.name
83
+ end
84
+ end
85
+
86
+ build_join_query(manager, buckets, Arel::Nodes::InnerJoin)
87
+ end
88
+
89
+ def build_join_query(manager, buckets, join_type)"
90
+ ],
91
+ # Change 3 - Line 1012
92
+ ['join_infos = join_dependency.join_constraints stashed_association_joins',
93
+ 'join_infos = join_dependency.join_constraints stashed_association_joins, join_type'
94
+ ]
95
+ ],
96
+ :QueryMethods
97
+ )
98
+ end
99
+
100
+
48
101
  # puts ::Brick::Util._patch_require(
49
102
  # 'cucumber/cli/options.rb', '/cucumber/cli/options', # /cli/options
50
- # ' def extract_environment_variables',
51
- # " def extract_environment_variables\n
52
- # puts 'Patch test!'"
103
+ # [' def extract_environment_variables',
104
+ # " def extract_environment_variables\n
105
+ # puts 'Patch test!'"]
53
106
  # ).inspect
54
107
 
55
108
  # An ActiveRecord extension that uses INFORMATION_SCHEMA views to reflect on all
@@ -75,7 +128,10 @@ module Brick
75
128
 
76
129
  def set_db_schema(params)
77
130
  schema = params['_brick_schema'] || 'public'
78
- ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema) if schema && ::Brick.db_schemas&.include?(schema)
131
+ if schema && ::Brick.db_schemas&.include?(schema)
132
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
133
+ schema
134
+ end
79
135
  end
80
136
 
81
137
  # All tables and views (what Postgres calls "relations" including column and foreign key info)
@@ -295,10 +351,11 @@ module Brick
295
351
 
296
352
  relations = ::Brick.relations
297
353
  if (ars = ::Brick.config.additional_references) || ::Brick.config.polymorphics
354
+ is_optional = ActiveRecord.version >= ::Gem::Version.new('5.0')
298
355
  if ars
299
356
  ars.each do |ar|
300
357
  fk = ar.length < 5 ? [nil, +ar[0], ar[1], nil, +ar[2]] : [ar[0], +ar[1], ar[2], ar[3], +ar[4], ar[5]]
301
- ::Brick._add_bt_and_hm(fk, relations, false, true)
358
+ ::Brick._add_bt_and_hm(fk, relations, false, is_optional)
302
359
  end
303
360
  end
304
361
  if (polys = ::Brick.config.polymorphics)
@@ -311,7 +368,7 @@ module Brick
311
368
  v ||= ActiveRecord::Base.execute_sql("SELECT DISTINCT #{poly}_type AS typ FROM #{table_name}").each_with_object([]) { |result, s| s << result['typ'] if result['typ'] }
312
369
  v.each do |type|
313
370
  if relations.key?(primary_table = type.underscore.pluralize)
314
- ::Brick._add_bt_and_hm([nil, table_name, poly, nil, primary_table, "(brick) #{table_name}_#{poly}"], relations, true, true)
371
+ ::Brick._add_bt_and_hm([nil, table_name, poly, nil, primary_table, "(brick) #{table_name}_#{poly}"], relations, true, is_optional)
315
372
  else
316
373
  missing_stis[primary_table] = type unless ::Brick.existing_stis.key?(type)
317
374
  end
@@ -403,6 +460,9 @@ In config/initializers/brick.rb appropriate entries would look something like:
403
460
  send(:resources, controller_name.to_sym, **options)
404
461
  end
405
462
  end
463
+ if ::Brick.config.add_orphans && instance_variable_get(:@set).named_routes.names.exclude?(:brick_orphans)
464
+ get('/brick_orphans', to: 'brick_gem#orphans', as: 'brick_orphans')
465
+ end
406
466
  end
407
467
  send(:get, '/api-docs/v1/swagger.json', { to: 'brick_swagger#index' }) if Object.const_defined?('Rswag::Ui')
408
468
  end
@@ -414,7 +474,24 @@ end
414
474
 
415
475
  require 'brick/version_number'
416
476
 
477
+ # Older versions of ActiveRecord would only show more serious error information from "panic" level, which is
478
+ # a level only available in Postgres 12 and older. This patch will allow older and newer versions of Postgres
479
+ # to work along with fairly old versions of Rails.
480
+ if Object.const_defined?('PG::VERSION') && ActiveRecord.version < ::Gem::Version.new('4.2.6')
481
+ ::Brick::Util._patch_require(
482
+ 'active_record/connection_adapters/postgresql_adapter.rb', '/activerecord', ["'panic'", "'error'"]
483
+ )
484
+ end
485
+
417
486
  require 'active_record'
487
+ require 'active_record/relation'
488
+ require 'active_record/relation/query_methods' if is_add_left_outer_join
489
+
490
+ # Rake tasks
491
+ class Railtie < Rails::Railtie
492
+ Dir.glob("#{File.expand_path(__dir__)}/brick/tasks/**/*.rake").each { |task| load task }
493
+ end
494
+
418
495
  # Major compatibility fixes for ActiveRecord < 4.2
419
496
  # ================================================
420
497
  ActiveSupport.on_load(:active_record) do
@@ -633,6 +710,62 @@ ActiveSupport.on_load(:active_record) do
633
710
  end
634
711
  end
635
712
  end
713
+
714
+ if is_add_left_outer_join
715
+ # Final pieces for left_outer_joins support, which was derived from this commit:
716
+ # https://github.com/rails/rails/commit/3f46ef1ddab87482b730a3f53987e04308783d8b
717
+ module Associations
718
+ class JoinDependency
719
+ def make_left_outer_joins(parent, child)
720
+ tables = child.tables
721
+ join_type = Arel::Nodes::OuterJoin
722
+ info = make_constraints parent, child, tables, join_type
723
+
724
+ [info] + child.children.flat_map { |c| make_left_outer_joins(child, c) }
725
+ end
726
+ end
727
+ end
728
+ module Querying
729
+ delegate :left_outer_joins, to: :all
730
+ end
731
+ class Relation
732
+ MULTI_VALUE_METHODS = MULTI_VALUE_METHODS + [:left_outer_joins] unless MULTI_VALUE_METHODS.include?(:left_outer_joins)
733
+ end
734
+ module QueryMethods
735
+ attr_writer :left_outer_joins_values
736
+ def left_outer_joins_values
737
+ @left_outer_joins_values ||= []
738
+ end
739
+
740
+ def left_outer_joins(*args)
741
+ check_if_method_has_arguments!(:left_outer_joins, args)
742
+
743
+ args.compact!
744
+ args.flatten!
745
+
746
+ spawn.left_outer_joins!(*args)
747
+ end
748
+
749
+ def left_outer_joins!(*args) # :nodoc:
750
+ self.left_outer_joins_values += args
751
+ self
752
+ end
753
+
754
+ def build_left_outer_joins(manager, outer_joins)
755
+ buckets = outer_joins.group_by do |join|
756
+ case join
757
+ when Hash, Symbol, Array
758
+ :association_join
759
+ else
760
+ raise ArgumentError, 'only Hash, Symbol and Array are allowed'
761
+ end
762
+ end
763
+
764
+ build_join_query(manager, buckets, Arel::Nodes::OuterJoin)
765
+ end
766
+ end
767
+ end
768
+ # (End of left_outer_joins support)
636
769
  end
637
770
  end
638
771
 
@@ -209,7 +209,7 @@ module Brick
209
209
  # # Specify STI subclasses either directly by name or as a general module prefix that should always relate to a specific
210
210
  # # parent STI class. The prefixed :: here for these examples is mandatory. Also having a suffixed :: means instead of
211
211
  # # a class reference, this is for a general namespace reference. So in this case requests for, say, either of the
212
- # # non-existant classes Animals::Cat or Animals::Goat (or anything else with the module prefix of \"Animals::\" would
212
+ # # non-existent classes Animals::Cat or Animals::Goat (or anything else with the module prefix of \"Animals::\" would
213
213
  # # build a model that inherits from Animal. And a request specifically for the class Snake would build a new model
214
214
  # # that inherits from Reptile, and no other request would do this -- only specifically for Snake. The ending ::
215
215
  # # indicates that it's a module prefix instead of a specific class name.
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.38
4
+ version: 1.0.41
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-06-27 00:00:00.000000000 Z
11
+ date: 2022-07-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,7 +16,7 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '3.0'
19
+ version: '4.2'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
22
  version: '7.2'
@@ -26,10 +26,24 @@ dependencies:
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: '3.0'
29
+ version: '4.2'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '7.2'
33
+ - !ruby/object:Gem::Dependency
34
+ name: fancy_gets
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
33
47
  - !ruby/object:Gem::Dependency
34
48
  name: appraisal
35
49
  requirement: !ruby/object:Gem::Requirement
@@ -225,6 +239,7 @@ files:
225
239
  - lib/brick/join_array.rb
226
240
  - lib/brick/serializers/json.rb
227
241
  - lib/brick/serializers/yaml.rb
242
+ - lib/brick/tasks/orphans.rake
228
243
  - lib/brick/util.rb
229
244
  - lib/brick/version_number.rb
230
245
  - lib/generators/brick/USAGE