brick 1.0.155 → 1.0.157

Sign up to get free protection for your applications and to get access to all the features.
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