brick 1.0.155 → 1.0.157

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: fceffbcf863eb9c03b64a0cb4420a33ded5eb7f3908b40b263a345ae9bf5c164
4
- data.tar.gz: 81f9f96bd1463b8b7e7736ed4716bcf78609bae27485af4de15cb23ae12e27ee
3
+ metadata.gz: 995a300b0971af02a39ed75857ce349c99720068555cbb1d6311a75d9a13f2b8
4
+ data.tar.gz: e9153dac19552fc8dd581774f89b9ed682ac040b5a4dc0d47f4b28fe079ca36d
5
5
  SHA512:
6
- metadata.gz: c41a59ca62c80012166225b0cf80505b81fe57ccdfeb92c4add7f227d6d4a9cc0c664aaf5209c31dabd54c3e471fe140fd3dc75bf1441bbed0fca67e9801ffd9
7
- data.tar.gz: 83c874515b230b0b607da32e41366284b15e830ebd7f7022fb318db412fef94a0339495de54c9ab0277425af2635cc8ab235675074cbb50f703082979ebd28b5
6
+ metadata.gz: 4c0c8553d5ef266aa6e947d6f0dd9ee4ae23d64118cd3c69565345747be0afa4de63be999976bd8e4fed55ebec2f3551aa87a4f4b84dee0a3dced188d23260f6
7
+ data.tar.gz: a7e0dbdd27c5d0905d7acf0a7a3f6fb5e254556ef65a0633a3021c395664c65f152afd3cecc53992c0f5da0095a1246b10d14898c34d82c60fc1cec298e62a68
data/lib/brick/config.rb CHANGED
@@ -332,6 +332,14 @@ module Brick
332
332
  @mutex.synchronize { @table_name_prefixes = value }
333
333
  end
334
334
 
335
+ def treat_as_module
336
+ @mutex.synchronize { @treat_as_module || [] }
337
+ end
338
+
339
+ def treat_as_module=(mod_names)
340
+ @mutex.synchronize { @treat_as_module = mod_names }
341
+ end
342
+
335
343
  def order
336
344
  @mutex.synchronize { @order || {} }
337
345
  end
@@ -59,7 +59,7 @@ module ActiveRecord
59
59
  end
60
60
 
61
61
  def is_brick?
62
- instance_variables.include?(:@_brick_built) && instance_variable_get(:@_brick_built)
62
+ instance_variables.include?(:@_brick_relation) && instance_variable_get(:@_brick_relation)
63
63
  end
64
64
 
65
65
  def _assoc_names
@@ -544,11 +544,9 @@ module ActiveRecord
544
544
  wheres = {}
545
545
  params.each do |k, v|
546
546
  k = k.to_s # Rails < 4.2 comes in as a symbol
547
- next if ['_brick_schema', '_brick_order',
548
- '_brick_erd', '_brick_exclude', '_brick_unexclude',
549
- '_brick_page', '_brick_page_size', '_brick_offset', '_brick_limit',
550
- '_brick_is_api', 'controller', 'action'].include?(k)
547
+ next unless k.start_with?('__')
551
548
 
549
+ k = k[2..-1] # Take off leading "__"
552
550
  if (where_col = (ks = k.split('.')).last)[-1] == '!'
553
551
  where_col = where_col[0..-2]
554
552
  end
@@ -1163,6 +1161,9 @@ if ActiveSupport::Dependencies.respond_to?(:autoload_module!) # %%% Only works w
1163
1161
  end
1164
1162
 
1165
1163
  ::Brick::ADD_CONST_MISSING = lambda do
1164
+ return if @_brick_const_missing_done
1165
+
1166
+ @_brick_const_missing_done = true
1166
1167
  alias _brick_const_missing const_missing
1167
1168
  def const_missing(*args)
1168
1169
  requested = args.first.to_s
@@ -1187,9 +1188,13 @@ end
1187
1188
  end
1188
1189
  base_module = if self < ActiveRecord::Migration || !self.name
1189
1190
  brick_root || Object
1190
- elsif (split_self_name || self.name.split('::')).length > 1 # Classic mode
1191
+ elsif split_self_name&.length&.> 1 # Classic mode
1191
1192
  begin
1192
- return self._brick_const_missing(*args)
1193
+ base = self
1194
+ unless (base_goal = requested.split('::')[0..-2].join('::')).empty?
1195
+ base = base.parent while base.name != base_goal && base != Object
1196
+ end
1197
+ return base._brick_const_missing(*args)
1193
1198
 
1194
1199
  rescue NameError # %%% Avoid the error "____ cannot be autoloaded from an anonymous class or module"
1195
1200
  return self.const_get(args.first) if self.const_defined?(args.first)
@@ -1294,10 +1299,16 @@ end
1294
1299
  # Build out a module for the schema if it's namespaced
1295
1300
  # schema_name = schema_name.camelize
1296
1301
  base_module.const_set(schema_name.to_sym, (built_module = Module.new))
1297
-
1298
1302
  [built_module, "module #{schema_name}; end\n"]
1299
1303
  # %%% Perhaps an option to use the first module just as schema, and additional modules as namespace with a table name prefix applied
1300
1304
 
1305
+ # MODULE (overrides from "treat_as_module")
1306
+ elsif (::Brick.enable_models? || ::Brick.enable_controllers?) &&
1307
+ (possible_module = (base_module == Object ? '' : "#{base_module.name}::") + class_name) &&
1308
+ ::Brick.config.treat_as_module.include?(possible_module)
1309
+ base_module.const_set(class_name.to_sym, (built_module = Module.new))
1310
+ [built_module, "module #{possible_module}; end\n"]
1311
+
1301
1312
  # AVO Resource
1302
1313
  elsif base_module == Object && Object.const_defined?('Avo') && ::Avo.respond_to?(:railtie_namespace) && requested.end_with?('Resource') &&
1303
1314
  # Expect that anything called MotorResource or SpinaResource could be from those administrative gems
@@ -1464,12 +1475,12 @@ class Object
1464
1475
  rescue StandardError => ex
1465
1476
  ::ActiveRecord::Base
1466
1477
  end))
1467
-
1468
1478
  end
1469
1479
  hmts = nil
1470
1480
  code = +"class #{full_name} < #{base_model.name}\n"
1471
1481
  built_model = Class.new(base_model) do |new_model_class|
1472
1482
  (schema_module || Object).const_set((chosen_name = (inheritable_name || model_name)).to_sym, new_model_class)
1483
+ @_brick_relation = relation
1473
1484
  if inheritable_name
1474
1485
  new_model_class.define_singleton_method :inherited do |subclass|
1475
1486
  super(subclass)
@@ -1479,7 +1490,7 @@ class Object
1479
1490
  puts "should be \"class #{model_name} < #{inheritable_name}\"\n (not \"#{subclass.name} < #{inheritable_name}\")"
1480
1491
  end
1481
1492
  end
1482
- self.abstract_class = true
1493
+ new_model_class.abstract_class = true
1483
1494
  code << " self.abstract_class = true\n"
1484
1495
  elsif Object.const_defined?('BCrypt') && relation[:cols].include?('password_digest') &&
1485
1496
  !instance_methods.include?(:password) && respond_to?(:has_secure_password)
@@ -1489,6 +1500,10 @@ class Object
1489
1500
  end
1490
1501
  # Accommodate singular or camel-cased table names such as "order_detail" or "OrderDetails"
1491
1502
  code << " self.table_name = '#{self.table_name = matching}'\n" if inheritable_name || self.table_name != matching
1503
+ if (inh_col = ::Brick.config.sti_type_column.find { |_k, v| v.include?(matching) }&.first)
1504
+ new_model_class.inheritance_column = inh_col
1505
+ code << " self.inheritance_column = '#{inh_col}'\n"
1506
+ end
1492
1507
 
1493
1508
  # Override models backed by a view so they return true for #is_view?
1494
1509
  # (Dynamically-created controllers and view templates for such models will then act in a read-only way)
@@ -1539,19 +1554,19 @@ class Object
1539
1554
  unless is_sti
1540
1555
  fks = relation[:fks] || {}
1541
1556
  # Do the bulk of the has_many / belongs_to processing, and store details about HMT so they can be done at the very last
1542
- fks.each_with_object(Hash.new { |h, k| h[k] = [] }) do |fk, hmts|
1543
- # The key in each hash entry (fk.first) is the constraint name
1544
- inverse_assoc_name = (assoc = fk.last)[:inverse]&.fetch(:assoc_name, nil)
1545
- if (invs = assoc[:inverse_table]).is_a?(Array)
1546
- if assoc[:is_bt]
1547
- invs = invs.first # Just do the first one of what would be multiple identical polymorphic belongs_to
1548
- else
1549
- invs.each { |inv| build_bt_or_hm(full_name, relations, relation, hmts, assoc, inverse_assoc_name, inv, code) }
1550
- end
1551
- end
1552
- build_bt_or_hm(full_name, relations, relation, hmts, assoc, inverse_assoc_name, invs, code) unless invs.is_a?(Array)
1553
- hmts
1554
- end
1557
+ hmts = fks.each_with_object(Hash.new { |h, k| h[k] = [] }) do |fk, hmts2|
1558
+ # The key in each hash entry (fk.first) is the constraint name
1559
+ inverse_assoc_name = (assoc = fk.last)[:inverse]&.fetch(:assoc_name, nil)
1560
+ if (invs = assoc[:inverse_table]).is_a?(Array)
1561
+ if assoc[:is_bt]
1562
+ invs = invs.first # Just do the first one of what would be multiple identical polymorphic belongs_to
1563
+ else
1564
+ invs.each { |inv| build_bt_or_hm(full_name, relations, relation, hmts2, assoc, inverse_assoc_name, inv, code) }
1565
+ end
1566
+ else
1567
+ build_bt_or_hm(full_name, relations, relation, hmts2, assoc, inverse_assoc_name, invs, code)
1568
+ end
1569
+ end
1555
1570
  # # Not NULLables
1556
1571
  # # %%% For the minute we've had to pull this out because it's been troublesome implementing the NotNull validator
1557
1572
  # relation[:cols].each do |col, datatype|
@@ -1569,58 +1584,54 @@ class Object
1569
1584
  # self.broadcasts_to ->(model) { (model&.class&.name || chosen_name).underscore.pluralize.to_sym }
1570
1585
  # code << " broadcasts_to ->(#{chosen_name}) { #{chosen_name}&.class&.name&.underscore&.pluralize&.to_sym }\n"
1571
1586
  # end
1572
- end # class definition
1573
- # Having this separate -- will this now work out better?
1574
- built_model.class_exec do
1575
- @_brick_built = true
1576
- @_brick_relation = relation
1577
- hmts&.each do |hmt_fk, hms|
1578
- hmt_fk = hmt_fk.tr('.', '_')
1579
- hms.each do |hm|
1580
- # %%% Need to confirm that HMTs work when they are built from has_manys with custom names
1581
- through = ::Brick.config.schema_behavior[:multitenant] ? hm.first[:assoc_name] : hm.first[:inverse_table].tr('.', '_').pluralize
1582
- options = {}
1583
- hmt_name = if hms.length > 1
1584
- if hms[0].first[:inverse][:assoc_name] == hms[1].first[:inverse][:assoc_name] # Same BT names pointing back to us? (Most common scenario)
1585
- "#{hmt_fk}_through_#{hm.first[:assoc_name]}"
1586
- else # Use BT names to provide uniqueness
1587
- if self.name.underscore.singularize == hm.first[:alternate_name]
1588
- # Has previously been:
1589
- # # If it folds back on itself then look at the other side
1590
- # # (At this point just infer the source be the inverse of the first has_many that
1591
- # # we find that is not ourselves. If there are more than two then uh oh, can't
1592
- # # yet handle that rare circumstance!)
1593
- # other = hms.find { |hm1| hm1 != hm } # .first[:fk]
1594
- # options[:source] = other.first[:inverse][:assoc_name].to_sym
1595
- # And also has been:
1596
- # hm.first[:inverse][:assoc_name].to_sym
1597
- options[:source] = hm.last.to_sym
1598
- else
1599
- through = hm.first.fetch(:alternate_chosen_name, hm.first[:alternate_name])
1600
- end
1601
- singular_assoc_name = hm.first[:inverse][:assoc_name].singularize
1602
- "#{singular_assoc_name}_#{hmt_fk}"
1603
- end
1604
- else
1605
- hmt_fk
1606
- end
1607
- options[:through] = through.to_sym
1608
- if relation[:fks].any? { |k, v| v[:assoc_name] == hmt_name }
1609
- hmt_name = "#{hmt_name.singularize}_#{hm.first[:assoc_name]}"
1610
- # Was:
1611
- # options[:class_name] = hm.first[:inverse_table].singularize.camelize
1612
- # options[:foreign_key] = hm.first[:fk].to_sym
1613
- far_assoc = relations[hm.first[:inverse_table]][:fks].find { |_k, v| v[:assoc_name] == hm.last }
1614
- options[:class_name] = far_assoc.last[:inverse_table].singularize.camelize
1615
- options[:foreign_key] = far_assoc.last[:fk].to_sym
1616
- end
1617
- options[:source] ||= hm.last.to_sym unless hmt_name.singularize == hm.last
1618
- code << " has_many :#{hmt_name}#{options.map { |opt| ", #{opt.first}: #{opt.last.inspect}" }.join}\n"
1619
- self.send(:has_many, hmt_name.to_sym, **options)
1587
+
1588
+ hmts&.each do |hmt_fk, hms|
1589
+ hmt_fk = hmt_fk.tr('.', '_')
1590
+ hms.each do |hm|
1591
+ # %%% Need to confirm that HMTs work when they are built from has_manys with custom names
1592
+ through = ::Brick.config.schema_behavior[:multitenant] ? hm.first[:assoc_name] : hm.first[:inverse_table].tr('.', '_').pluralize
1593
+ options = {}
1594
+ hmt_name = if hms.length > 1
1595
+ if hms[0].first[:inverse][:assoc_name] == hms[1].first[:inverse][:assoc_name] # Same BT names pointing back to us? (Most common scenario)
1596
+ "#{hmt_fk}_through_#{hm.first[:assoc_name]}"
1597
+ else # Use BT names to provide uniqueness
1598
+ if self.name.underscore.singularize == hm.first[:alternate_name]
1599
+ # Has previously been:
1600
+ # # If it folds back on itself then look at the other side
1601
+ # # (At this point just infer the source be the inverse of the first has_many that
1602
+ # # we find that is not ourselves. If there are more than two then uh oh, can't
1603
+ # # yet handle that rare circumstance!)
1604
+ # other = hms.find { |hm1| hm1 != hm } # .first[:fk]
1605
+ # options[:source] = other.first[:inverse][:assoc_name].to_sym
1606
+ # And also has been:
1607
+ # hm.first[:inverse][:assoc_name].to_sym
1608
+ options[:source] = hm.last.to_sym
1609
+ else
1610
+ through = hm.first.fetch(:alternate_chosen_name, hm.first[:alternate_name])
1611
+ end
1612
+ singular_assoc_name = hm.first[:inverse][:assoc_name].singularize
1613
+ "#{singular_assoc_name}_#{hmt_fk}"
1614
+ end
1615
+ else
1616
+ hmt_fk
1617
+ end
1618
+ options[:through] = through.to_sym
1619
+ if relation[:fks].any? { |k, v| v[:assoc_name] == hmt_name }
1620
+ hmt_name = "#{hmt_name.singularize}_#{hm.first[:assoc_name]}"
1621
+ # Was:
1622
+ # options[:class_name] = hm.first[:inverse_table].singularize.camelize
1623
+ # options[:foreign_key] = hm.first[:fk].to_sym
1624
+ far_assoc = relations[hm.first[:inverse_table]][:fks].find { |_k, v| v[:assoc_name] == hm.last }
1625
+ options[:class_name] = far_assoc.last[:inverse_table].singularize.camelize
1626
+ options[:foreign_key] = far_assoc.last[:fk].to_sym
1620
1627
  end
1628
+ options[:source] ||= hm.last.to_sym unless hmt_name.singularize == hm.last
1629
+ code << " has_many :#{hmt_name}#{options.map { |opt| ", #{opt.first}: #{opt.last.inspect}" }.join}\n"
1630
+ new_model_class.send(:has_many, hmt_name.to_sym, **options)
1621
1631
  end
1622
1632
  end
1623
1633
  code << "end # model #{full_name}\n"
1634
+ end # model class definition
1624
1635
  [built_model, code]
1625
1636
  end
1626
1637
 
@@ -2147,6 +2158,17 @@ class Object
2147
2158
  new_obj.send("#{k}=", ActiveStorage::Filename.new('')) if v.is_a?(ActiveStorage::Filename) && !v.instance_variable_get(:@filename)
2148
2159
  end if Object.const_defined?('ActiveStorage')
2149
2160
  end
2161
+ new_obj.attribute_names.each do |a|
2162
+ if (val = params["__#{a}"])
2163
+ # val = case new_obj.class.column_for_attribute(a).type
2164
+ # when :datetime, :date, :time, :timestamp
2165
+ # val.
2166
+ # else
2167
+ # val
2168
+ # end
2169
+ new_obj.send("#{a}=", val)
2170
+ end
2171
+ end
2150
2172
  instance_variable_set("@#{singular_table_name}".to_sym, new_obj)
2151
2173
  add_csp_hash
2152
2174
  end
@@ -750,12 +750,12 @@ window.addEventListener(\"popstate\", linkSchemas);
750
750
  (hm_fk_name.is_a?(String) && hm_fk_name.include?('.')) # HMT? (Could do a better check for this)
751
751
  predicates = path_keys(hm_assoc, hm_fk_name, pk).map do |k, v|
752
752
  if v == '[sti_type]'
753
- "'#{k}': (@#{obj_name}.#{hm_assoc.active_record.inheritance_column})&.constantize&.base_class&.name"
753
+ "'__#{k}': (@#{obj_name}.#{hm_assoc.active_record.inheritance_column})&.constantize&.base_class&.name"
754
754
  else
755
- v.is_a?(String) ? "'#{k}': '#{v}'" : "'#{k}': @#{obj_name}.#{v}"
755
+ v.is_a?(String) ? "'__#{k}': '#{v}'" : "'__#{k}': @#{obj_name}.#{v}"
756
756
  end
757
757
  end.join(', ')
758
- "<%= link_to '#{assoc_name}', #{hm_assoc.klass._brick_index}_path({ #{predicates} }) %>\n"
758
+ "<%= link_to '#{assoc_name}', #{hm_assoc.klass._brick_index}_path(predicates = { #{predicates} }) %>\n"
759
759
  else
760
760
  puts "Warning: has_many :#{hm_assoc.name} in model #{hm_assoc.active_record.name} currently looks for a foreign key called \"#{hm_assoc.foreign_key}\". "\
761
761
  "Instead it should use the clause \"foreign_key: :#{hm_assoc.inverse_of&.foreign_key}\"."
@@ -897,6 +897,9 @@ tr th {
897
897
  tr th a {
898
898
  color: #80FFB8;
899
899
  }
900
+ .add-hm-related {
901
+ float: right;
902
+ }
900
903
 
901
904
  tr th, tr td {
902
905
  padding: 0.2em 0.5em;
@@ -1169,7 +1172,10 @@ if (window.brickFontFamily) {
1169
1172
  x.style.fontFamily = brickFontFamily.toString();
1170
1173
  });
1171
1174
  }
1172
- </script>"
1175
+ </script>
1176
+ <% if (apartment_default_schema = ::Brick.apartment_multitenant && ::Brick.apartment_default_tenant)
1177
+ Apartment::Tenant.switch!(apartment_default_schema)
1178
+ end %>"
1173
1179
 
1174
1180
  erd_markup = if @_brick_model
1175
1181
  "<div id=\"mermaidErd\" class=\"mermaid\">
@@ -1658,7 +1664,7 @@ end
1658
1664
  # path_options = [obj.#{pk}]
1659
1665
  # path_options << { '_brick_schema': } if
1660
1666
  options = {}
1661
- if ::Brick.config.path_prefix
1667
+ if ::Brick.config.path_prefix || (obj.class.table_name.singularize == obj.class.table_name)
1662
1668
  path_helper = obj.new_record? ? #{model_name}._brick_index : #{model_name}._brick_index(:singular)
1663
1669
  options[:url] = send(\"#\{path_helper}_path\".to_sym, obj)
1664
1670
  end
@@ -1763,7 +1769,14 @@ end
1763
1769
  end"}"
1764
1770
  end
1765
1771
  s << "<table id=\"#{hm_name}\" class=\"shadow\">
1766
- <tr><th>#{hm[1]}#{' poly' if hm[0].options[:as]} #{hm[3]}</th></tr>
1772
+ <tr><th>#{hm[1]}#{' poly' if hm[0].options[:as]} #{hm[3]}
1773
+ <span class = \"add-hm-related\"><%=
1774
+ pk_val = (obj_pk = model.primary_key).is_a?(String) ? obj.send(obj_pk) : obj_pk.map { |pk_part| obj.send(pk_part) }
1775
+ pk_val_arr = [pk_val] unless pk_val.is_a?(Array)
1776
+ link_to('<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"#fff\" d=\"M24 10h-10v-10h-4v10h-10v4h10v10h4v-10h10z\"/></svg>'.html_safe,
1777
+ new_#{hm.first.klass._brick_index(:singular)}_path(predicates))
1778
+ %></span>
1779
+ </th></tr>
1767
1780
  <% if (assoc = @#{obj_name}.class.reflect_on_association(:#{hm_name})).macro == :has_one &&
1768
1781
  assoc.options&.fetch(:through, nil).nil?
1769
1782
  # In order to apply DSL properly, evaluate this HO the other way around as if it were as a BT
@@ -2034,9 +2047,7 @@ document.querySelectorAll(\"input, select\").forEach(function (inp) {
2034
2047
  alias _brick_render_template render_template
2035
2048
  def render_template(view, template, layout_name, *args)
2036
2049
  layout_name = nil if (is_brick = template.instance_variable_get(:@is_brick)) && layout_name.is_a?(Proc)
2037
- result = _brick_render_template(view, template, layout_name, *args)
2038
- Apartment::Tenant.switch!(::Brick.apartment_default_tenant) if is_brick && ::Brick.apartment_multitenant
2039
- result
2050
+ _brick_render_template(view, template, layout_name, *args)
2040
2051
  end
2041
2052
  end # TemplateRenderer
2042
2053
  end
@@ -71,11 +71,11 @@ module Brick::Rails::FormBuilder
71
71
  # Postgres naturally uses the +uuid_generate_v4()+ function from the uuid-ossp extension
72
72
  # If it's not yet enabled then: create extension \"uuid-ossp\";
73
73
  # ActiveUUID gem created a new :uuid type
74
- out << val
74
+ out << val if val
75
75
  when :ltree
76
76
  # In Postgres labels of data stored in a hierarchical tree-like structure
77
77
  # If it's not yet enabled then: create extension ltree;
78
- out << val
78
+ out << val if val
79
79
  when :binary
80
80
  is_revert = false
81
81
  if val
@@ -95,7 +95,7 @@ module Brick::Rails::FormBuilder
95
95
  else
96
96
  eheij = ActiveSupport::JSON::Encoding.escape_html_entities_in_json
97
97
  ActiveSupport::JSON::Encoding.escape_html_entities_in_json = false if eheij
98
- val_str = val.to_json
98
+ val_str = val&.to_json
99
99
  ActiveSupport::JSON::Encoding.escape_html_entities_in_json = eheij
100
100
  end
101
101
  # Because there are so danged many quotes in JSON, escape them specially by converting to backticks.
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 155
8
+ TINY = 157
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
@@ -113,21 +113,23 @@ module Brick
113
113
  def set_db_schema(params = nil)
114
114
  # If Apartment::Tenant.current is not still the default (usually 'public') then an elevator has brought us into
115
115
  # a different tenant. If so then don't allow schema navigation.
116
- chosen = if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' &&
117
- (current_schema = (ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2]
118
- .split(',') - ['pg_catalog', 'pg_toast', 'heroku_ext']).first) &&
119
- (is_show_schema_list = (apartment_multitenant && current_schema == ::Brick.default_schema)) &&
120
- (schema = (params ? params['_brick_schema'] : ::Brick.default_schema)) &&
121
- ::Brick.db_schemas&.key?(schema)
122
- Apartment::Tenant.switch!(schema)
123
- schema
124
- elsif ::Brick.test_schema
125
- is_show_schema_list = true
126
- Apartment::Tenant.switch!(::Brick.test_schema)
127
- ::Brick.test_schema
128
- else
129
- current_schema # Just return the current schema
130
- end
116
+ if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' && apartment_multitenant
117
+ current_schema = (ActiveRecord::Base.execute_sql('SELECT current_schemas(true)')
118
+ .first['current_schemas'][1..-2]
119
+ .split(',') - ['pg_catalog', 'pg_toast', 'heroku_ext']).first
120
+ is_show_schema_list = current_schema == ::Brick.default_schema
121
+ schema = (is_show_schema_list && params && params['_brick_schema']) || ::Brick.default_schema
122
+ chosen = if is_show_schema_list && ::Brick.db_schemas&.key?(schema)
123
+ Apartment::Tenant.switch!(schema)
124
+ schema
125
+ elsif ::Brick.test_schema
126
+ is_show_schema_list = true
127
+ Apartment::Tenant.switch!(::Brick.test_schema)
128
+ ::Brick.test_schema
129
+ else
130
+ current_schema # Just return the current schema
131
+ end
132
+ end
131
133
  [chosen == ::Brick.default_schema ? nil : chosen, is_show_schema_list]
132
134
  end
133
135
 
@@ -416,6 +418,12 @@ module Brick
416
418
  Brick.config.table_name_prefixes = value
417
419
  end
418
420
 
421
+ # @api public
422
+ # Causes Decidim to work with this line: Brick.treat_as_module = ['Decidim::ContentBlocks']
423
+ def treat_as_module=(value)
424
+ Brick.config.treat_as_module = value
425
+ end
426
+
419
427
  # @api public
420
428
  def metadata_columns=(value)
421
429
  Brick.config.metadata_columns = value
@@ -1147,8 +1155,11 @@ ActiveSupport.on_load(:active_record) do
1147
1155
  class << self
1148
1156
  def execute_sql(sql, *param_array)
1149
1157
  param_array = param_array.first if param_array.length == 1 && param_array.first.is_a?(Array)
1150
- if ['OracleEnhanced', 'SQLServer'].include?(ActiveRecord::Base.connection.adapter_name)
1158
+ case ActiveRecord::Base.connection.adapter_name
1159
+ when 'OracleEnhanced', 'SQLServer'
1151
1160
  connection.exec_query(send(:sanitize_sql_array, [sql] + param_array)).rows
1161
+ when 'Trilogy'
1162
+ connection.execute(send(:sanitize_sql_array, [sql] + param_array)).rows
1152
1163
  else
1153
1164
  connection.execute(send(:sanitize_sql_array, [sql] + param_array))
1154
1165
  end
@@ -1159,8 +1170,7 @@ ActiveSupport.on_load(:active_record) do
1159
1170
  # :singleton-method:
1160
1171
  # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling
1161
1172
  # dates and times from the database. This is set to :utc by default.
1162
- unless respond_to?(:default_timezone)
1163
- puts "ADDING!!! 4.w"
1173
+ unless ::ActiveRecord.respond_to?(:default_timezone) || respond_to?(:default_timezone)
1164
1174
  mattr_accessor :default_timezone, instance_writer: false
1165
1175
  self.default_timezone = :utc
1166
1176
  end
@@ -1453,7 +1463,8 @@ ActiveSupport.on_load(:active_record) do
1453
1463
  class << self
1454
1464
  alias _original_load load
1455
1465
  def load(yaml, *args, **kwargs)
1456
- if kwargs[:aliases].nil? && caller[0..4].any? { |line| line.end_with?("`database_configuration'") }
1466
+ if kwargs[:aliases].nil? && caller[0..4].any? { |line| line.end_with?("`database_configuration'") ||
1467
+ line.end_with?("`secrets'") }
1457
1468
  kwargs[:aliases] = true
1458
1469
  end
1459
1470
  _original_load(yaml, *args, **kwargs)
@@ -23,7 +23,8 @@ module Brick
23
23
  'time with time zone' => 'time',
24
24
  'double precision' => 'float',
25
25
  'smallint' => 'integer', # %%% Need to put in "limit: 2"
26
- # # Oracle data types
26
+ 'ARRAY' => 'string', # Note that we'll also add ", array: true"
27
+ # Oracle data types
27
28
  'VARCHAR2' => 'string',
28
29
  'CHAR' => 'string',
29
30
  ['NUMBER', 22] => 'integer',
@@ -54,8 +55,8 @@ module Brick
54
55
  def brick_migrations
55
56
  # If Apartment is active, see if a default schema to analyse is indicated
56
57
 
57
- # # Load all models
58
- # ::Brick.eager_load_classes
58
+ ::Brick.mode = :on
59
+ ActiveRecord::Base.establish_connection
59
60
 
60
61
  if (tables = ::Brick.relations.reject { |k, v| v.key?(:isView) && v[:isView] == true }.map(&:first).sort).empty?
61
62
  puts "No tables found in database #{ActiveRecord::Base.connection.current_database}."
@@ -176,7 +177,7 @@ module Brick
176
177
  # Support missing primary key (by adding: , id: false)
177
178
  id_option = if pk_is_also_fk || !pkey_cols&.present?
178
179
  needs_serial_col = true
179
- ', id: false'
180
+ +', id: false'
180
181
  elsif ((pkey_col_first = (col_def = relation[:cols][pkey_cols&.first])&.first) &&
181
182
  (pkey_col_first = SQL_TYPES[pkey_col_first] || SQL_TYPES[col_def&.[](0..1)] ||
182
183
  SQL_TYPES.find { |r| r.first.is_a?(Regexp) && pkey_col_first =~ r.first }&.last ||
@@ -185,16 +186,16 @@ module Brick
185
186
  )
186
187
  case pkey_col_first
187
188
  when 'integer'
188
- ', id: :serial'
189
+ +', id: :serial'
189
190
  when 'bigint'
190
- ', id: :bigserial'
191
+ +', id: :bigserial'
191
192
  else
192
- ", id: :#{pkey_col_first}" # Something like: id: :integer, primary_key: :businessentityid
193
+ +", id: :#{pkey_col_first}" # Something like: id: :integer, primary_key: :businessentityid
193
194
  end +
194
195
  (pkey_cols.first ? ", primary_key: :#{pkey_cols.first}" : '')
195
196
  end
196
197
  if !id_option && pkey_cols.sort != arpk
197
- id_option = ", primary_key: :#{pkey_cols.first}"
198
+ id_option = +", primary_key: :#{pkey_cols.first}"
198
199
  end
199
200
  if !is_4x_rails && (comment = relation&.fetch(:description, nil))&.present?
200
201
  (id_option ||= +'') << ", comment: #{comment.inspect}"
@@ -223,6 +224,7 @@ module Brick
223
224
  SQL_TYPES.find { |r| r.first.is_a?(Regexp) && col_type.first =~ r.first }&.last ||
224
225
  col_type.first
225
226
  suffix = col_type[3] || pkey_cols&.include?(col) ? +', null: false' : +''
227
+ suffix << ', array: true' if (col_type.first == 'ARRAY')
226
228
  if !is_4x_rails && klass && (comment = klass.columns_hash.fetch(col, nil)&.comment)&.present?
227
229
  suffix << ", comment: #{comment.inspect}"
228
230
  end
@@ -15,6 +15,9 @@ module Brick
15
15
  def brick_models
16
16
  # %%% If Apartment is active and there's no schema_to_analyse, ask which schema they want
17
17
 
18
+ ::Brick.mode = :on
19
+ ActiveRecord::Base.establish_connection
20
+
18
21
  # Load all models
19
22
  ::Brick.eager_load_classes
20
23
 
@@ -9,29 +9,50 @@ module Brick
9
9
 
10
10
  desc 'Auto-generates a seeds file from existing data.'
11
11
 
12
+ SeedModel = Struct.new(:table_name, :klass, :is_brick)
13
+ SeedModel.define_method(:to_s) do
14
+ "#{klass.name}#{' (brick-generated)' if is_brick}"
15
+ end
16
+
12
17
  def brick_seeds
13
18
  # %%% If Apartment is active and there's no schema_to_analyse, ask which schema they want
14
19
 
15
20
  ::Brick.mode = :on
16
21
  ActiveRecord::Base.establish_connection
17
22
 
18
- if (tables = ::Brick.relations.reject { |k, v| v.key?(:isView) && v[:isView] == true }.map(&:first).sort).empty?
19
- puts "No tables found in database #{ActiveRecord::Base.connection.current_database}."
23
+ # Load all models
24
+ ::Brick.eager_load_classes
25
+
26
+ # Generate a list of viable models that can be chosen
27
+ # First start with any existing models that have been defined ...
28
+ existing_models = ActiveRecord::Base.descendants.each_with_object({}) do |m, s|
29
+ s[m.table_name] = SeedModel.new(m.table_name, m, false) if !m.abstract_class? && m.table_exists?
30
+ end
31
+ models = (existing_models.values +
32
+ # ... then add models which can be auto-built by Brick
33
+ ::Brick.relations.reject do |k, v|
34
+ (v.key?(:isView) && v[:isView] == true) || existing_models.key?(k)
35
+ end.map { |k, v| SeedModel.new(k, v[:class_name].constantize, true) }
36
+ ).sort { |a, b| a.to_s <=> b.to_s }
37
+ if models.empty?
38
+ puts "No viable models found for database #{ActiveRecord::Base.connection.current_database}."
20
39
  return
21
40
  end
22
41
 
23
42
  if File.exist?(seed_file_path = "#{::Rails.root}/db/seeds.rb")
24
- puts "WARNING: seeds file #{seed_file_path} appears to already be present."
43
+ puts "WARNING: seeds file #{seed_file_path} appears to already be present.\nOverwrite?"
44
+ return unless gets_list(list: ['No', 'Yes']) == 'Yes'
45
+
46
+ puts "\n"
25
47
  end
26
48
 
27
- # Generate a list of tables that can be chosen
28
- chosen = gets_list(list: tables, chosen: tables.dup)
49
+ chosen = gets_list(list: models, chosen: models.dup)
29
50
  schemas = chosen.each_with_object({}) do |v, s|
30
- if (v_parts = v.split('.')).length > 1
51
+ if (v_parts = v.table_name.split('.')).length > 1
31
52
  s[v_parts.first] = nil unless [::Brick.default_schema, 'public'].include?(v_parts.first)
32
53
  end
33
54
  end
34
- seeds = +"# Seeds file for #{ActiveRecord::Base.connection.current_database}:"
55
+ seeds = +"# Seeds file for #{ActiveRecord::Base.connection.current_database}:\n"
35
56
  done = []
36
57
  fks = {}
37
58
  stuck = {}
@@ -40,12 +61,13 @@ module Brick
40
61
  # Start by making entries for fringe models (those with no foreign keys).
41
62
  # Continue layer by layer, creating entries for models that reference ones already done, until
42
63
  # no more entries can be created. (At that point hopefully all models are accounted for.)
43
- while (fringe = chosen.reject do |tbl|
64
+ while (fringe = chosen.reject do |seed_model|
65
+ tbl = seed_model.table_name
44
66
  snag_fks = []
45
67
  snags = ::Brick.relations.fetch(tbl, nil)&.fetch(:fks, nil)&.select do |_k, v|
46
68
  v[:is_bt] && !v[:polymorphic] &&
47
69
  tbl != v[:inverse_table] && # Ignore self-referencing associations (stuff like "parent_id")
48
- !done.include?(v[:inverse_table]) &&
70
+ !done.any? { |done_seed_model| done_seed_model.table_name == v[:inverse_table] } &&
49
71
  ::Brick.config.ignore_migration_fks.exclude?(snag_fk = "#{tbl}.#{v[:fk]}") &&
50
72
  snag_fks << snag_fk
51
73
  end
@@ -53,12 +75,14 @@ module Brick
53
75
  # puts snag_fks.inspect
54
76
  stuck[tbl] = snags
55
77
  end
56
- end).present?
78
+ end
79
+ ).present?
57
80
  seeds << "\n"
58
- fringe.each do |tbl|
81
+ fringe.each do |seed_model|
82
+ tbl = seed_model.table_name
59
83
  next unless ::Brick.config.exclude_tables.exclude?(tbl) &&
60
84
  (relation = ::Brick.relations.fetch(tbl, nil))&.fetch(:cols, nil)&.present? &&
61
- (klass = Object.const_get(class_name = relation[:class_name])).table_exists?
85
+ (klass = seed_model.klass).table_exists?
62
86
 
63
87
  pkey_cols = (rpk = relation[:pkey].values.flatten) & (arpk = [ar_base.primary_key].flatten.sort)
64
88
  # In case things aren't as standard
@@ -82,10 +106,14 @@ module Brick
82
106
  fkeys = relation[:fks].values.select { |assoc| assoc[:is_bt] && !assoc[:polymorphic] }
83
107
  # Refer to this table name as a symbol or dotted string as appropriate
84
108
  # tbl_code = tbl_parts.length == 1 ? ":#{tbl_parts.first}" : "'#{tbl}'"
85
- seeds << " # #{class_name}\n"
86
109
 
110
+ has_rows = false
87
111
  is_empty = true
88
112
  klass.order(*pkey_cols).each do |obj|
113
+ unless has_rows
114
+ has_rows = true
115
+ seeds << " puts 'Seeding: #{seed_model.klass.name}'\n"
116
+ end
89
117
  is_empty = false
90
118
  pk_val = obj.send(pkey_cols.first)
91
119
  fk_vals = []
@@ -98,19 +126,55 @@ module Brick
98
126
  val = val.to_s
99
127
  end
100
128
  if fk
101
- fk_vals << "#{fk[:assoc_name]}: #{fk[:inverse_table]}_#{val.inspect}" if val
129
+ inv_tbl = fk[:inverse_table].gsub('.', '__')
130
+ fk_vals << "#{fk[:assoc_name]}: #{inv_tbl}_#{brick_escape(val)}" if val
102
131
  else
103
132
  data << "#{col}: #{val.inspect}"
104
133
  end
105
134
  end
106
- seeds << "#{tbl}_#{pk_val} = #{class_name}.create(#{(fk_vals + data).join(', ')})\n"
135
+ seeds << "#{tbl.gsub('.', '__')}_#{brick_escape(pk_val)} = #{seed_model.klass.name}.create(#{(fk_vals + data).join(', ')})\n"
107
136
  end
137
+ seeds << " # (Skipping #{seed_model.klass.name} as it has no rows)\n" unless has_rows
108
138
  File.open(seed_file_path, "w") { |f| f.write seeds }
109
139
  end
110
140
  done.concat(fringe)
111
141
  chosen -= done
112
142
  end
143
+ stuck_counts = Hash.new { |h, k| h[k] = 0 }
144
+ chosen.each do |leftover|
145
+ puts "Can't do #{leftover.klass.name} because:\n #{stuck[leftover.table_name].map do |snag|
146
+ stuck_counts[snag.last[:inverse_table]] += 1
147
+ snag.last[:assoc_name]
148
+ end.join(', ')}"
149
+ end
113
150
  puts "\n*** Created seeds for #{done.length} models in db/seeds.rb ***"
151
+ if (stuck_sorted = stuck_counts.to_a.sort { |a, b| b.last <=> a.last }).length.positive?
152
+ puts "-----------------------------------------"
153
+ puts "Unable to create migrations for #{stuck_sorted.length} tables#{
154
+ ". Here's the top 5 blockers" if stuck_sorted.length > 5
155
+ }:"
156
+ pp stuck_sorted[0..4]
157
+ end
158
+ end
159
+
160
+ private
161
+
162
+ def brick_escape(val)
163
+ val = val.to_s if val.is_a?(Date) || val.is_a?(Time) # Accommodate when for whatever reason a primary key is a date or time
164
+ case val
165
+ when String
166
+ ret = +''
167
+ val.each_char do |ch|
168
+ if ch < '0' || (ch > '9' && ch < 'A') || ch > 'Z'
169
+ ret << (ch == '_' ? ch : "x#{'K'.unpack('H*')[0]}")
170
+ else
171
+ ret << ch
172
+ end
173
+ end
174
+ ret
175
+ else
176
+ val
177
+ end
114
178
  end
115
179
  end
116
180
  end
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.155
4
+ version: 1.0.157
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-07-09 00:00:00.000000000 Z
11
+ date: 2023-07-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord