brick 1.0.206 → 1.0.207

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: 7cd4c557cd4baa06fc68ab683444da5b018c1c0c63436d36d7648c259167c418
4
- data.tar.gz: 62d2260128b72a0a6db5d0af14054ae9518e4516ccf18a04010d2e187c8768d2
3
+ metadata.gz: 585c18ee97a6ecead8dec67a3bd9ad7ef765524bb109c72c5550f9f0dbd3b98f
4
+ data.tar.gz: a0ff5371aef56af539e2022349c0b07c0cdad0bdc93dcbb35aa54a1a8ef3ae4f
5
5
  SHA512:
6
- metadata.gz: 43743d8447db6daa39e2fc506e7eef3260ba63abc3a4cb4990ace1d33e56e38d15dd7eaf9c9bbdb2873ea2b5dae6d308c12bd04ea2f073b04fefccac564ee3e3
7
- data.tar.gz: 144317e91f03fdf2ca66db2c78d8f851e9ab21c4c016190ad3a48ef97a3594f2df8125a1d4f4a59214501429e1c16b6baca9878c22bb2d026eeee20a83929f53
6
+ metadata.gz: 73f5772ef3b440025b50c62b727a58e63392b7642f326d43e63be64f5c2d68eb9fcc1e8a0d77279f0491a284e51cdeb3ca1db90a67bf2b79d1a4f62371575c71
7
+ data.tar.gz: 9c068ca26a463307b7f931ca7dcd3c4c12bb06dcc313b284a286aa509ef039b5e4efea1108be9f83839bdb84de47249b9c93a4418dcfc51fe507fd85dba0a6dd
data/lib/brick/config.rb CHANGED
@@ -232,6 +232,15 @@ module Brick
232
232
  @mutex.synchronize { @hmts = assocs }
233
233
  end
234
234
 
235
+ # Tables to treat as associative, even when they have data columns
236
+ def treat_as_associative
237
+ @mutex.synchronize { @treat_as_associative }
238
+ end
239
+
240
+ def treat_as_associative=(tables)
241
+ @mutex.synchronize { @treat_as_associative = tables }
242
+ end
243
+
235
244
  # Polymorphic associations
236
245
  def polymorphics
237
246
  @mutex.synchronize { @polymorphics ||= {} }
@@ -154,6 +154,10 @@ module ActiveRecord
154
154
  end
155
155
  dsl
156
156
  end
157
+
158
+ def _brick_monetized_attributes
159
+ @_brick_monetized_attributes ||= respond_to?(:monetized_attributes) ? monetized_attributes.values : {}
160
+ end
157
161
  end
158
162
 
159
163
  def self.brick_parse_dsl(join_array = nil, prefix = [], translations = {}, is_polymorphic = false, dsl = nil, emit_dsl = false)
@@ -549,22 +553,16 @@ module ActiveRecord
549
553
  # things ... also if there are any HM counts then an OUTER JOIN for each of them out
550
554
  # to a derived table to do that counting. All of these things need to know proper
551
555
  # table correlation names, which will now become available from brick_links on the
552
- # @_brick_rel_dupe object.)
556
+ # rel_dupe object.)
553
557
  @_brick_links ||= begin
554
558
  # If it's a CollectionProxy (which inherits from Relation) then need to dig
555
559
  # out the core Relation object which is found in the association scope.
556
560
  rel_dupe = (is_a?(ActiveRecord::Associations::CollectionProxy) ? scope : self).dup
557
- # This will become a fully populated hash of correlation names
561
+ # Start out with a hash that has only the root table name
558
562
  rel_dupe.instance_variable_set(:@_brick_links, bl = { '' => table_name })
559
- # Walk the AST tree in order to capture all the correlation names
560
- rel_dupe.arel.ast
561
- # Now that @_brick_links are captured, we can garbage collect the @_brick_rel_dupe object
562
- # remove_instance_variable(:@_brick_rel_dupe)
563
+ rel_dupe.arel.ast # Walk the AST tree in order to capture all the other correlation names
563
564
  bl
564
565
  end
565
- # if @_brick_rel_dupe
566
- # end
567
- # @_brick_links
568
566
  end
569
567
 
570
568
  def brick_select(*args, params: {}, order_by: nil, translations: {},
@@ -835,7 +833,7 @@ module ActiveRecord
835
833
 
836
834
  idx = 0
837
835
  bail_out = nil
838
- through_sources.map do |a|
836
+ the_chain = through_sources.map do |a|
839
837
  from_clause << "\n LEFT OUTER JOIN #{a.table_name} br_t#{idx += 1} "
840
838
  from_clause << if (src_ref = a.source_reflection).macro == :belongs_to
841
839
  link_back << (nm = hmt_assoc.source_reflection.inverse_of&.name)
@@ -892,7 +890,12 @@ module ActiveRecord
892
890
 
893
891
  pri_tbl = hm.active_record
894
892
  pri_key = hm.options[:primary_key] || pri_tbl.primary_key
895
- if hm.active_record.abstract_class || hm.active_record.column_names.exclude?(pri_key)
893
+ if hm.active_record.abstract_class || case pri_key
894
+ when String
895
+ hm.active_record.column_names.exclude?(pri_key)
896
+ when Array
897
+ (pri_key - hm.active_record.column_names).length > 0
898
+ end
896
899
  # %%% When this gets hit then if an attempt is made to display the ERD, it might end up being blank
897
900
  nix << k
898
901
  next
@@ -908,7 +911,7 @@ module ActiveRecord
908
911
  on_clause << "#{_br_quoted_name("#{tbl_alias}.#{fk_col}")} = #{_br_quoted_name("#{pri_tbl.table_name}.#{pri_key}")}"
909
912
  [fk_col]
910
913
  else # Composite key
911
- fk_col.each_with_index { |fk_col_part, idx| on_clause << "#{tbl_alias}.#{fk_col_part} = #{pri_tbl.table_name}.#{pri_key[idx]}" }
914
+ fk_col.each_with_index { |fk_col_part, idx| on_clause << "#{_br_quoted_name("#{tbl_alias}.#{fk_col_part}")} = #{_br_quoted_name("#{pri_tbl.table_name}.#{pri_key[idx]}")}" }
912
915
  fk_col.dup
913
916
  end
914
917
  if poly_type
@@ -2006,16 +2009,18 @@ class Object
2006
2009
  instance_variable_set(:@resources, ::Brick.get_status_of_resources)
2007
2010
  add_csp_hash
2008
2011
  end
2009
- self.define_method :schema_create do
2010
- if (base_class = (model = params['modelName']&.constantize).base_class) &&
2011
- base_class.column_names.exclude?(col_name = params['colName'])
2012
- ActiveRecord::Base.connection.add_column(base_class.table_name.to_sym, col_name, (col_type = params['colType']).to_sym)
2013
- base_class.reset_column_information
2014
- ::Brick.relations[base_class.table_name]&.fetch(:cols, nil)&.[]=(col_name, [col_type, nil, false, false])
2015
- # instance_variable_set(:@schema, ::Brick.find_schema(::Brick.set_db_schema(params).first))
2016
- add_csp_hash
2017
- end
2018
- end
2012
+ # # if ::Brick.config.add_schema
2013
+ # # Currently can only do adding columns
2014
+ # self.define_method :schema_create do
2015
+ # if (base_class = (model = params['modelName']&.constantize).base_class) &&
2016
+ # base_class.column_names.exclude?(col_name = params['colName'])
2017
+ # ActiveRecord::Base.connection.add_column(base_class.table_name.to_sym, col_name, (col_type = params['colType']).to_sym)
2018
+ # base_class.reset_column_information
2019
+ # ::Brick.relations[base_class.table_name]&.fetch(:cols, nil)&.[]=(col_name, [col_type, nil, false, false])
2020
+ # # instance_variable_set(:@schema, ::Brick.find_schema(::Brick.set_db_schema(params).first))
2021
+ # add_csp_hash
2022
+ # end
2023
+ # end
2019
2024
  self.define_method :orphans do
2020
2025
  instance_variable_set(:@orphans, ::Brick.find_orphans(::Brick.set_db_schema(params).first))
2021
2026
  add_csp_hash
@@ -2403,7 +2408,7 @@ class Object
2403
2408
  index
2404
2409
  render :index
2405
2410
  else # Surface errors to the user in a flash message
2406
- flash.alert = (created_obj.errors.errors.map { |err| "<b>#{err.attribute}</b> #{err.message}" }.join(', '))
2411
+ flash.now.alert = (created_obj.errors.errors.map { |err| "<b>#{err.attribute}</b> #{err.message}" }.join(', '))
2407
2412
  new
2408
2413
  render :new
2409
2414
  end
@@ -2476,7 +2481,7 @@ class Object
2476
2481
  end
2477
2482
  obj.send(:update, upd_hash || upd_params)
2478
2483
  if obj.errors.any? # Surface errors to the user in a flash message
2479
- flash.alert = (obj.errors.errors.map { |err| "<b>#{err.attribute}</b> #{err.message}" }.join(', '))
2484
+ flash.now.alert = (obj.errors.errors.map { |err| "<b>#{err.attribute}</b> #{err.message}" }.join(', '))
2480
2485
  end
2481
2486
  end
2482
2487
 
@@ -2,115 +2,6 @@
2
2
 
3
3
  module Brick
4
4
  module Rails
5
- class << self
6
- def display_value(col_type, val, lat_lng = nil)
7
- is_mssql_geography = nil
8
- # Some binary thing that really looks like a Microsoft-encoded WGS84 point? (With the first two bytes, E6 10, indicating an EPSG code of 4326)
9
- if col_type == :binary && val && ::Brick.is_geography?(val)
10
- col_type = 'geography'
11
- is_mssql_geography = true
12
- end
13
- case col_type
14
- when 'geometry', 'geography'
15
- if Object.const_defined?('RGeo')
16
- @is_mysql = ['Mysql2', 'Trilogy'].include?(ActiveRecord::Base.connection.adapter_name) if @is_mysql.nil?
17
- @is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer' if @is_mssql.nil?
18
- val_err = nil
19
-
20
- if @is_mysql || (is_mssql_geography ||=
21
- (@is_mssql ||
22
- (val && ::Brick.is_geography?(val))
23
- )
24
- )
25
- # MySQL's \"Internal Geometry Format\" and MSSQL's Geography are like WKB, but with an initial 4 bytes that indicates the SRID.
26
- if (srid = val&.[](0..3)&.unpack('I'))
27
- val = val.dup.force_encoding('BINARY')[4..-1].bytes
28
-
29
- # MSSQL spatial bitwise flags, often 0C for a point:
30
- # xxxx xxx1 = HasZValues
31
- # xxxx xx1x = HasMValues
32
- # xxxx x1xx = IsValid
33
- # xxxx 1xxx = IsSinglePoint
34
- # xxx1 xxxx = IsSingleLineSegment
35
- # xx1x xxxx = IsWholeGlobe
36
- # Convert Microsoft's unique geography binary to standard WKB
37
- # (MSSQL point usually has two doubles, lng / lat, and can also have Z)
38
- if is_mssql_geography
39
- if val[0] == 1 && (val[1] & 8 > 0) && # Single point?
40
- (val.length - 2) % 8 == 0 && val.length < 27 # And containing up to three 8-byte values?
41
- val = [0, 0, 0, 0, 1] + val[2..-1].reverse
42
- else
43
- val_err = '(Microsoft internal SQL geography type)'
44
- end
45
- end
46
- end
47
- end
48
- unless val_err || val.nil?
49
- val = if ((geometry = RGeo::WKRep::WKBParser.new.parse(val.pack('c*'))).is_a?(RGeo::Cartesian::PointImpl) ||
50
- geometry.is_a?(RGeo::Geos::CAPIPointImpl)) &&
51
- !(geometry.y == 0.0 && geometry.x == 0.0)
52
- # Create a POINT link to this style of Google maps URL: https://www.google.com/maps/place/38.7071296+-121.2810649/@38.7071296,-121.2810649,12z
53
- "<a href=\"https://www.google.com/maps/place/#{geometry.y}+#{geometry.x}/@#{geometry.y},#{geometry.x},12z\" target=\"blank\">#{geometry.to_s}</a>"
54
- end
55
- end
56
- val_err || val
57
- else
58
- '(Add RGeo gem to parse geometry detail)'
59
- end
60
- when :binary
61
- ::Brick::Rails.display_binary(val)
62
- else
63
- if col_type
64
- if lat_lng && !(lat_lng.first.zero? && lat_lng.last.zero?)
65
- # Create a link to this style of Google maps URL: https://www.google.com/maps/place/38.7071296+-121.2810649/@38.7071296,-121.2810649,12z
66
- "<a href=\"https://www.google.com/maps/place/#{lat_lng.first}+#{lat_lng.last}/@#{lat_lng.first},#{lat_lng.last},12z\" target=\"blank\">#{val}</a>"
67
- elsif val.is_a?(Numeric) && ::ActiveSupport.const_defined?(:NumberHelper)
68
- ::ActiveSupport::NumberHelper.number_to_delimited(val, delimiter: ',')
69
- else
70
- ::Brick::Rails::FormBuilder.hide_bcrypt(val, col_type == :xml)
71
- end
72
- else
73
- '?'
74
- end
75
- end
76
- end
77
-
78
- def display_binary(val, max_size = 100_000)
79
- return unless val
80
-
81
- @image_signatures ||= { (+"\xFF\xD8\xFF\xEE").force_encoding('ASCII-8BIT') => 'jpeg',
82
- (+"\xFF\xD8\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00\x01").force_encoding('ASCII-8BIT') => 'jpeg',
83
- (+"\xFF\xD8\xFF\xDB").force_encoding('ASCII-8BIT') => 'jpeg',
84
- (+"\xFF\xD8\xFF\xE1").force_encoding('ASCII-8BIT') => 'jpeg',
85
- (+"\x89PNG\r\n\x1A\n").force_encoding('ASCII-8BIT') => 'png',
86
- '<svg' => 'svg+xml', # %%% Not yet very good detection for SVG
87
- (+'BM').force_encoding('ASCII-8BIT') => 'bmp',
88
- (+'GIF87a').force_encoding('ASCII-8BIT') => 'gif',
89
- (+'GIF89a').force_encoding('ASCII-8BIT') => 'gif' }
90
-
91
- if val[0..1] == "\x15\x1C" # One of those goofy Microsoft OLE containers?
92
- package_header_length = val[2..3].bytes.reverse.inject(0) {|m, b| (m << 8) + b }
93
- # This will often be just FF FF FF FF
94
- # object_size = val[16..19].bytes.reverse.inject(0) {|m, b| (m << 8) + b }
95
- friendly_and_class_names = val[20...package_header_length].split("\0")
96
- object_type_name_length = val[package_header_length + 8..package_header_length+11].bytes.reverse.inject(0) {|m, b| (m << 8) + b }
97
- friendly_and_class_names << val[package_header_length + 12...package_header_length + 12 + object_type_name_length].strip
98
- # friendly_and_class_names will now be something like: ['Bitmap Image', 'Paint.Picture', 'PBrush']
99
- real_object_size = val[package_header_length + 20 + object_type_name_length..package_header_length + 23 + object_type_name_length].bytes.reverse.inject(0) {|m, b| (m << 8) + b }
100
- object_start = package_header_length + 24 + object_type_name_length
101
- val = val[object_start...object_start + real_object_size]
102
- end
103
-
104
- if ((signature = @image_signatures.find { |k, _v| val[0...k.length] == k }&.last) ||
105
- (val[0..3] == 'RIFF' && val[8..11] == 'WEBP' && binding.local_variable_set(:signature, 'webp'))) &&
106
- val.length < max_size
107
- "<img src=\"data:image/#{signature.last};base64,#{Base64.encode64(val)}\">"
108
- else
109
- "&lt;&nbsp;#{signature ? "#{signature} image" : 'Binary'}, #{val.length} bytes&nbsp;>"
110
- end
111
- end
112
- end
113
-
114
5
  # See http://guides.rubyonrails.org/engines.html
115
6
  class Engine < ::Rails::Engine
116
7
  JS_CHANGEOUT = "function changeout(href, param, value, trimAfter) {
@@ -209,10 +100,7 @@ function linkSchemas() {
209
100
  # paths['app/models'] << 'lib/brick/frameworks/active_record/models'
210
101
  config.brick = ActiveSupport::OrderedOptions.new
211
102
  ActiveSupport.on_load(:before_initialize) do |app|
212
-
213
- # --------------------------------------------
214
- # 1. Load three initializers early
215
- # (inflectsions.rb, brick.rb, apartment.rb)
103
+ # Load three initializers early (inflections.rb, brick.rb, apartment.rb)
216
104
  # Very first thing, load inflections since we'll be using .pluralize and .singularize on table and model names
217
105
  if File.exist?(inflections = ::Rails.root&.join('config/initializers/inflections.rb') || '')
218
106
  load inflections
@@ -1194,125 +1082,6 @@ if (window.brickFontFamily) {
1194
1082
  Apartment::Tenant.switch!(apartment_default_schema)
1195
1083
  end %>"
1196
1084
 
1197
- erd_markup = if @_brick_model
1198
- "<div id=\"mermaidErd\">
1199
- <div id=\"mermaidDiagram\" class=\"mermaid\">
1200
- erDiagram
1201
- <% def sidelinks(shown_classes, klass)
1202
- links = []
1203
- # %%% Not yet showing these as they can get just a bit intense!
1204
- # klass.reflect_on_all_associations.select { |a| shown_classes.key?(a.klass) }.each do |assoc|
1205
- # unless shown_classes[assoc.klass].key?(klass.name)
1206
- # links << \" #\{klass.name.split('::').last} #\{assoc.macro == :belongs_to ? '}o--||' : '||--o{'} #\{assoc.klass.name.split('::').last} : \\\"\\\"\"n\"
1207
- # shown_classes[assoc.klass][klass.name] = nil
1208
- # end
1209
- # end
1210
- # shown_classes[klass] ||= {}
1211
- links.join
1212
- end
1213
-
1214
- model_short_name = #{@_brick_model.name.split('::').last.inspect}
1215
- shown_classes = {}
1216
- @_brick_bt_descrip&.each do |bt|
1217
- bt_class = bt[1].first.first
1218
- callbacks[bt_name = bt_class.name.split('::').last] = bt_class
1219
- is_has_one = #{@_brick_model.name}.reflect_on_association(bt.first)&.inverse_of&.macro == :has_one ||
1220
- ::Brick.config.has_ones&.fetch('#{@_brick_model.name}', nil)&.key?(bt.first.to_s)
1221
- %> <%= \"#\{model_short_name} #\{is_has_one ? '||' : '}o'}--|| #\{bt_name} : \\\"#\{
1222
- bt_underscored = bt[1].first.first.name.underscore.singularize
1223
- bt.first unless bt.first.to_s == bt_underscored.split('/').last # Was: bt_underscored.tr('/', '_')
1224
- }\\\"\".html_safe %>
1225
- <%= sidelinks(shown_classes, bt_class).html_safe %>
1226
- <% end
1227
- last_hm = nil
1228
- @_brick_hm_counts&.each do |hm|
1229
- # Skip showing self-referencing HM links since they would have already been drawn while evaluating the BT side
1230
- next if (hm_class = hm.last&.klass) == #{@_brick_model.name}
1231
-
1232
- callbacks[hm_name = hm_class.name.split('::').last] = hm_class
1233
- if (through = hm.last.options[:through]&.to_s) # has_many :through (HMT)
1234
- through_name = (through_assoc = hm.last.source_reflection).active_record.name.split('::').last
1235
- callbacks[through_name] = through_assoc.active_record
1236
- if last_hm == through # Same HM, so no need to build it again, and for clarity just put in a blank line
1237
- %><%= \"\n\"
1238
- %><% else
1239
- %> <%= \"#\{model_short_name} ||--o{ #\{through_name}\".html_safe %> : \"\"
1240
- <%= sidelinks(shown_classes, through_assoc.active_record).html_safe %>
1241
- <% last_hm = through
1242
- end
1243
- %> <%= \"#\{through_name} }o--|| #\{hm_name}\".html_safe %> : \"\"
1244
- <%= \"#\{model_short_name} }o..o{ #\{hm_name} : \\\"#\{hm.first}\\\"\".html_safe %><%
1245
- else # has_many
1246
- %> <%= \"#\{model_short_name} ||--o{ #\{hm_name} : \\\"#\{
1247
- hm.first.to_s unless (last_hm = hm.first.to_s).downcase == hm_class.name.underscore.pluralize.tr('/', '_')
1248
- }\\\"\".html_safe %><%
1249
- end %>
1250
- <%= sidelinks(shown_classes, hm_class).html_safe %>
1251
- <% end
1252
- def dt_lookup(dt)
1253
- { 'integer' => 'int', }[dt] || dt&.tr(' ', '_') || 'int'
1254
- end
1255
- callbacks.merge({model_short_name => #{@_brick_model.name}}).each do |cb_k, cb_class|
1256
- cb_relation = ::Brick.relations[cb_class.table_name]
1257
- pkeys = cb_relation[:pkey]&.first&.last
1258
- fkeys = cb_relation[:fks]&.values&.each_with_object([]) { |fk, s| s << fk[:fk] if fk.fetch(:is_bt, nil) }
1259
- cols = cb_relation[:cols]
1260
- %> <%= cb_k %> {<%
1261
- pkeys&.each do |pk| %>
1262
- <%= \"#\{dt_lookup(cols[pk].first)} #\{pk} \\\"PK#\{' fk' if fkeys&.include?(pk)}\\\"\".html_safe %><%
1263
- end %><%
1264
- fkeys&.each do |fk|
1265
- if fk.is_a?(Array)
1266
- fk.each do |fk_part| %>
1267
- <%= \"#\{dt_lookup(cols[fk_part].first)} #\{fk_part} \\\"&nbsp;&nbsp;&nbsp;&nbsp;fk\\\"\".html_safe unless pkeys&.include?(fk_part) %><%
1268
- end
1269
- else # %%% Does not yet accommodate polymorphic BTs
1270
- %>
1271
- <%= \"#\{dt_lookup(cols[fk]&.first)} #\{fk} \\\"&nbsp;&nbsp;&nbsp;&nbsp;fk\\\"\".html_safe unless pkeys&.include?(fk) %><%
1272
- end
1273
- end %>
1274
- }
1275
- <% end
1276
- # callback < %= cb_k % > erdClick
1277
- @_brick_monetized_attributes = model.respond_to?(:monetized_attributes) ? model.monetized_attributes.values : {}
1278
- %>
1279
- </div>#{
1280
- add_column = nil
1281
- # Make into a server control with a javascript snippet
1282
- # Have post back go to a common "brick_schema" endpoint, this one for add_column
1283
- "
1284
- <table id=\"tblAddCol\"><tr>
1285
- <td rowspan=\"2\">Add<br>Column</td>
1286
- <td class=\"paddingBottomZero\">Type</td><td class=\"paddingBottomZero\">Name</td>
1287
- <td rowspan=\"2\"><input type=\"button\" id=\"btnAddCol\" value=\"+\"></td>
1288
- </tr><tr><td class=\"paddingTopZero\">
1289
- <select id=\"ddlColType\">
1290
- <option value=\"string\">String</option>
1291
- <option value=\"text\">Text</option>
1292
- <option value=\"integer\">Integer</option>
1293
- <option value=\"bool\">Boolean</option>
1294
- </select></td>
1295
- <td class=\"paddingTopZero\"><input id=\"txtColName\"></td>
1296
- </tr></table>
1297
- <script>
1298
- var btnAddCol = document.getElementById(\"btnAddCol\");
1299
- btnAddCol.addEventListener(\"click\", function () {
1300
- var txtColName = document.getElementById(\"txtColName\");
1301
- var ddlColType = document.getElementById(\"ddlColType\");
1302
- doFetch(\"POST\", {modelName: \"#{@_brick_model.name}\",
1303
- colName: txtColName.value, colType: ddlColType.value,
1304
- _brick_action: \"/#{prefix}brick_schema\"},
1305
- function () { // If it returns successfully, do a page refresh
1306
- location.href = location.href;
1307
- }
1308
- );
1309
- });
1310
- </script>
1311
- " unless add_column == false}
1312
-
1313
- </div>
1314
- "
1315
- end
1316
1085
  inline = case args.first
1317
1086
  when 'index'
1318
1087
  if Object.const_defined?('DutyFree')
@@ -1506,7 +1275,7 @@ end
1506
1275
  </script>
1507
1276
  <% end %>
1508
1277
  </div></div>
1509
- #{erd_markup}
1278
+ #{::Brick::Rails.erd_markup(@_brick_model, prefix) if @_brick_model}
1510
1279
 
1511
1280
  <%= # Consider getting the name from the association -- hm.first.name -- if a more \"friendly\" alias should be used for a screwy table name
1512
1281
  # If the resource is missing, has the user simply created an inappropriately pluralised name for a table?
@@ -1714,7 +1483,7 @@ if (description = rel&.fetch(:description, nil)) %>
1714
1483
  <span class=\"__brick\"><%= description %></span><br><%
1715
1484
  end
1716
1485
  %><%= link_to \"(See all #\{model_name.pluralize})\", see_all_path, { class: '__brick' } %>
1717
- #{erd_markup}
1486
+ #{::Brick::Rails.erd_markup(@_brick_model, prefix) if @_brick_model}
1718
1487
  <% if obj
1719
1488
  # path_options = [obj.#{pk}]
1720
1489
  # path_options << { '_brick_schema': } if
@@ -230,7 +230,7 @@ module Brick::Rails::FormTags
230
230
  end
231
231
  elsif (col = cols[col_name]).is_a?(ActiveRecord::ConnectionAdapters::Column)
232
232
  # binding.pry if col.is_a?(Array)
233
- out << if @_brick_monetized_attributes&.include?(col_name)
233
+ out << if klass._brick_monetized_attributes&.include?(col_name)
234
234
  val ? Money.new(val.to_i).format : ''
235
235
  elsif klass.respond_to?(:uploaders) && klass.uploaders.key?(col_name.to_sym) &&
236
236
  (url = obj.send(col_name)&.url) # Carrierwave image?
@@ -424,6 +424,7 @@ function onImagesLoaded(event) {
424
424
  out.html_safe
425
425
  end # brick_grid
426
426
 
427
+ # -----------------------------
427
428
  # Our mega show/new/update form
428
429
  def brick_form_for(obj, options = {}, model = obj.class, bts = {}, pk = (obj.class.primary_key || []))
429
430
  pk = [pk] unless pk.is_a?(Array)
@@ -439,7 +440,8 @@ function onImagesLoaded(event) {
439
440
  end if obj.new_record?
440
441
  rtans = model.rich_text_association_names if model.respond_to?(:rich_text_association_names)
441
442
  (model.column_names + (rtans || [])).each do |k|
442
- next if (pk.include?(k) && !bts.key?(k)) ||
443
+ pk_pos = (pk.index(k)&.+ 1)
444
+ next if (pk_pos && pk.length == 1 && !bts.key?(k)) ||
443
445
  ::Brick.config.metadata_columns.include?(k)
444
446
 
445
447
  col = model.columns_hash[k]
@@ -452,7 +454,7 @@ function onImagesLoaded(event) {
452
454
  end
453
455
  val = obj.attributes[k]
454
456
  out << "
455
- <tr>
457
+ <tr>
456
458
  <th class=\"show-field\"#{" title=\"#{col&.comment}\"".html_safe if col&.respond_to?(:comment) && !col&.comment.blank?}>"
457
459
  has_fields = true
458
460
  if (bt = bts[k])
@@ -496,10 +498,17 @@ function onImagesLoaded(event) {
496
498
  else
497
499
  out << model.human_attribute_name(k, { default: k })
498
500
  end
501
+ out << " (PK #{pk_pos})" if pk_pos
499
502
  out << "
500
503
  </th>
501
504
  <td>
502
- #{f.brick_field(k, html_options = {}, val, col, bt, bt_class, bt_name, bt_pair)}
505
+ "
506
+ if pk_pos
507
+ out << val.to_s
508
+ else
509
+ out << f.brick_field(k, html_options = {}, val, col, bt, bt_class, bt_name, bt_pair)
510
+ end
511
+ out << "
503
512
  </td>
504
513
  </tr>"
505
514
  end
@@ -519,6 +528,7 @@ function onImagesLoaded(event) {
519
528
  end
520
529
  end # brick_form_for
521
530
 
531
+ # --------------------------------
522
532
  def link_to_brick(*args, **kwargs)
523
533
  return unless ::Brick.config.mode == :on
524
534
 
@@ -616,6 +626,41 @@ function onImagesLoaded(event) {
616
626
  end
617
627
  end # link_to_brick
618
628
 
629
+ # ---------------------------------
630
+ def brick_add_column(model, prefix)
631
+ # TODO: Make a server control architecture that has separate javascript snippets
632
+ # Have post back go to a common "brick_schema" endpoint, this one for add_column
633
+ "
634
+ <table id=\"tblAddCol\"><tr>
635
+ <td rowspan=\"2\">Add<br>Column</td>
636
+ <td class=\"paddingBottomZero\">Type</td><td class=\"paddingBottomZero\">Name</td>
637
+ <td rowspan=\"2\"><input type=\"button\" id=\"btnAddCol\" value=\"+\"></td>
638
+ </tr><tr><td class=\"paddingTopZero\">
639
+ <select id=\"ddlColType\">
640
+ <option value=\"string\">String</option>
641
+ <option value=\"text\">Text</option>
642
+ <option value=\"integer\">Integer</option>
643
+ <option value=\"bool\">Boolean</option>
644
+ </select></td>
645
+ <td class=\"paddingTopZero\"><input id=\"txtColName\"></td>
646
+ </tr></table>
647
+ <script>
648
+ var btnAddCol = document.getElementById(\"btnAddCol\");
649
+ btnAddCol.addEventListener(\"click\", function () {
650
+ var txtColName = document.getElementById(\"txtColName\");
651
+ var ddlColType = document.getElementById(\"ddlColType\");
652
+ doFetch(\"POST\", {modelName: \"#{model.name}\",
653
+ colName: txtColName.value, colType: ddlColType.value,
654
+ _brick_action: \"/#{prefix}brick_schema\"},
655
+ function () { // If it returns successfully, do a page refresh
656
+ location.href = location.href;
657
+ }
658
+ );
659
+ });
660
+ </script>
661
+ "
662
+ end
663
+
619
664
  private
620
665
 
621
666
  def _brick_resource_from_iv(trim_ampersand = false)
@@ -4,6 +4,205 @@
4
4
  require 'brick/frameworks/rails/engine'
5
5
 
6
6
  module ::Brick::Rails
7
+ class << self
8
+ def display_value(col_type, val, lat_lng = nil)
9
+ is_mssql_geography = nil
10
+ # Some binary thing that really looks like a Microsoft-encoded WGS84 point? (With the first two bytes, E6 10, indicating an EPSG code of 4326)
11
+ if col_type == :binary && val && ::Brick.is_geography?(val)
12
+ col_type = 'geography'
13
+ is_mssql_geography = true
14
+ end
15
+ case col_type
16
+ when 'geometry', 'geography'
17
+ if Object.const_defined?('RGeo')
18
+ @is_mysql = ['Mysql2', 'Trilogy'].include?(ActiveRecord::Base.connection.adapter_name) if @is_mysql.nil?
19
+ @is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer' if @is_mssql.nil?
20
+ val_err = nil
21
+
22
+ if @is_mysql || (is_mssql_geography ||=
23
+ (@is_mssql ||
24
+ (val && ::Brick.is_geography?(val))
25
+ )
26
+ )
27
+ # MySQL's \"Internal Geometry Format\" and MSSQL's Geography are like WKB, but with an initial 4 bytes that indicates the SRID.
28
+ if (srid = val&.[](0..3)&.unpack('I'))
29
+ val = val.dup.force_encoding('BINARY')[4..-1].bytes
30
+
31
+ # MSSQL spatial bitwise flags, often 0C for a point:
32
+ # xxxx xxx1 = HasZValues
33
+ # xxxx xx1x = HasMValues
34
+ # xxxx x1xx = IsValid
35
+ # xxxx 1xxx = IsSinglePoint
36
+ # xxx1 xxxx = IsSingleLineSegment
37
+ # xx1x xxxx = IsWholeGlobe
38
+ # Convert Microsoft's unique geography binary to standard WKB
39
+ # (MSSQL point usually has two doubles, lng / lat, and can also have Z)
40
+ if is_mssql_geography
41
+ if val[0] == 1 && (val[1] & 8 > 0) && # Single point?
42
+ (val.length - 2) % 8 == 0 && val.length < 27 # And containing up to three 8-byte values?
43
+ val = [0, 0, 0, 0, 1] + val[2..-1].reverse
44
+ else
45
+ val_err = '(Microsoft internal SQL geography type)'
46
+ end
47
+ end
48
+ end
49
+ end
50
+ unless val_err || val.nil?
51
+ val = if ((geometry = RGeo::WKRep::WKBParser.new.parse(val.pack('c*'))).is_a?(RGeo::Cartesian::PointImpl) ||
52
+ geometry.is_a?(RGeo::Geos::CAPIPointImpl)) &&
53
+ !(geometry.y == 0.0 && geometry.x == 0.0)
54
+ # Create a POINT link to this style of Google maps URL: https://www.google.com/maps/place/38.7071296+-121.2810649/@38.7071296,-121.2810649,12z
55
+ "<a href=\"https://www.google.com/maps/place/#{geometry.y}+#{geometry.x}/@#{geometry.y},#{geometry.x},12z\" target=\"blank\">#{geometry.to_s}</a>"
56
+ end
57
+ end
58
+ val_err || val
59
+ else
60
+ '(Add RGeo gem to parse geometry detail)'
61
+ end
62
+ when :binary
63
+ ::Brick::Rails.display_binary(val)
64
+ else
65
+ if col_type
66
+ if lat_lng && !(lat_lng.first.zero? && lat_lng.last.zero?)
67
+ # Create a link to this style of Google maps URL: https://www.google.com/maps/place/38.7071296+-121.2810649/@38.7071296,-121.2810649,12z
68
+ "<a href=\"https://www.google.com/maps/place/#{lat_lng.first}+#{lat_lng.last}/@#{lat_lng.first},#{lat_lng.last},12z\" target=\"blank\">#{val}</a>"
69
+ elsif val.is_a?(Numeric) && ::ActiveSupport.const_defined?(:NumberHelper)
70
+ ::ActiveSupport::NumberHelper.number_to_delimited(val, delimiter: ',')
71
+ else
72
+ ::Brick::Rails::FormBuilder.hide_bcrypt(val, col_type == :xml)
73
+ end
74
+ else
75
+ '?'
76
+ end
77
+ end
78
+ end
79
+
80
+ def display_binary(val, max_size = 100_000)
81
+ return unless val
82
+
83
+ @image_signatures ||= { (+"\xFF\xD8\xFF\xEE").force_encoding('ASCII-8BIT') => 'jpeg',
84
+ (+"\xFF\xD8\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00\x01").force_encoding('ASCII-8BIT') => 'jpeg',
85
+ (+"\xFF\xD8\xFF\xDB").force_encoding('ASCII-8BIT') => 'jpeg',
86
+ (+"\xFF\xD8\xFF\xE1").force_encoding('ASCII-8BIT') => 'jpeg',
87
+ (+"\x89PNG\r\n\x1A\n").force_encoding('ASCII-8BIT') => 'png',
88
+ '<svg' => 'svg+xml', # %%% Not yet very good detection for SVG
89
+ (+'BM').force_encoding('ASCII-8BIT') => 'bmp',
90
+ (+'GIF87a').force_encoding('ASCII-8BIT') => 'gif',
91
+ (+'GIF89a').force_encoding('ASCII-8BIT') => 'gif' }
92
+
93
+ if val[0..1] == "\x15\x1C" # One of those goofy Microsoft OLE containers?
94
+ package_header_length = val[2..3].bytes.reverse.inject(0) {|m, b| (m << 8) + b }
95
+ # This will often be just FF FF FF FF
96
+ # object_size = val[16..19].bytes.reverse.inject(0) {|m, b| (m << 8) + b }
97
+ friendly_and_class_names = val[20...package_header_length].split("\0")
98
+ object_type_name_length = val[package_header_length + 8..package_header_length+11].bytes.reverse.inject(0) {|m, b| (m << 8) + b }
99
+ friendly_and_class_names << val[package_header_length + 12...package_header_length + 12 + object_type_name_length].strip
100
+ # friendly_and_class_names will now be something like: ['Bitmap Image', 'Paint.Picture', 'PBrush']
101
+ real_object_size = val[package_header_length + 20 + object_type_name_length..package_header_length + 23 + object_type_name_length].bytes.reverse.inject(0) {|m, b| (m << 8) + b }
102
+ object_start = package_header_length + 24 + object_type_name_length
103
+ val = val[object_start...object_start + real_object_size]
104
+ end
105
+
106
+ if ((signature = @image_signatures.find { |k, _v| val[0...k.length] == k }&.last) ||
107
+ (val[0..3] == 'RIFF' && val[8..11] == 'WEBP' && binding.local_variable_set(:signature, 'webp'))) &&
108
+ val.length < max_size
109
+ "<img src=\"data:image/#{signature.last};base64,#{Base64.encode64(val)}\">"
110
+ else
111
+ "&lt;&nbsp;#{signature ? "#{signature} image" : 'Binary'}, #{val.length} bytes&nbsp;>"
112
+ end
113
+ end
114
+
115
+ # Generate MermaidJS markup to create a partial ERD for this model
116
+ def erd_markup(model, prefix)
117
+ model_short_name = model.name.split('::').last
118
+ "<div id=\"mermaidErd\">
119
+ <div id=\"mermaidDiagram\" class=\"mermaid\">
120
+ erDiagram
121
+ <% shown_classes = {}
122
+
123
+ def erd_sidelinks(shown_classes, klass)
124
+ links = []
125
+ # %%% Not yet showing these as they can get just a bit intense!
126
+ # klass.reflect_on_all_associations.select { |a| shown_classes.key?(a.klass) }.each do |assoc|
127
+ # unless shown_classes[assoc.klass].key?(klass.name)
128
+ # links << \" #\{klass.name.split('::').last} #\{assoc.macro == :belongs_to ? '}o--||' : '||--o{'} #\{assoc.klass.name.split('::').last} : \\\"\\\"\"n\"
129
+ # shown_classes[assoc.klass][klass.name] = nil
130
+ # end
131
+ # end
132
+ # shown_classes[klass] ||= {}
133
+ links.join
134
+ end
135
+
136
+ @_brick_bt_descrip&.each do |bt|
137
+ bt_class = bt[1].first.first
138
+ callbacks[bt_name = bt_class.name.split('::').last] = bt_class
139
+ is_has_one = #{model.name}.reflect_on_association(bt.first)&.inverse_of&.macro == :has_one ||
140
+ ::Brick.config.has_ones&.fetch('#{model.name}', nil)&.key?(bt.first.to_s)
141
+ %> <%= \"#{model_short_name} #\{is_has_one ? '||' : '}o'}--|| #\{bt_name} : \\\"#\{
142
+ bt_underscored = bt[1].first.first.name.underscore.singularize
143
+ bt.first unless bt.first.to_s == bt_underscored.split('/').last # Was: bt_underscored.tr('/', '_')
144
+ }\\\"\".html_safe %>
145
+ <%= erd_sidelinks(shown_classes, bt_class).html_safe %>
146
+ <% end
147
+ last_hm = nil
148
+ @_brick_hm_counts&.each do |hm|
149
+ # Skip showing self-referencing HM links since they would have already been drawn while evaluating the BT side
150
+ next if (hm_class = hm.last&.klass) == #{model.name}
151
+
152
+ callbacks[hm_name = hm_class.name.split('::').last] = hm_class
153
+ if (through = hm.last.options[:through]&.to_s) # has_many :through (HMT)
154
+ through_name = (through_assoc = hm.last.source_reflection).active_record.name.split('::').last
155
+ callbacks[through_name] = through_assoc.active_record
156
+ if last_hm == through # Same HM, so no need to build it again, and for clarity just put in a blank line
157
+ %><%= \"\n\"
158
+ %><% else
159
+ %> <%= \"#{model_short_name} ||--o{ #\{through_name}\".html_safe %> : \"\"
160
+ <%= erd_sidelinks(shown_classes, through_assoc.active_record).html_safe %>
161
+ <% last_hm = through
162
+ end
163
+ %> <%= \"#\{through_name} }o--|| #\{hm_name}\".html_safe %> : \"\"
164
+ <%= \"#{model_short_name} }o..o{ #\{hm_name} : \\\"#\{hm.first}\\\"\".html_safe %><%
165
+ else # has_many
166
+ %> <%= \"#{model_short_name} ||--o{ #\{hm_name} : \\\"#\{
167
+ hm.first.to_s unless (last_hm = hm.first.to_s).downcase == hm_class.name.underscore.pluralize.tr('/', '_')
168
+ }\\\"\".html_safe %><%
169
+ end %>
170
+ <%= erd_sidelinks(shown_classes, hm_class).html_safe %>
171
+ <% end
172
+ def dt_lookup(dt)
173
+ { 'integer' => 'int', }[dt] || dt&.tr(' ', '_') || 'int'
174
+ end
175
+ callbacks.merge({#{model_short_name} => #{model.name}}).each do |cb_k, cb_class|
176
+ cb_relation = ::Brick.relations[cb_class.table_name]
177
+ pkeys = cb_relation[:pkey]&.first&.last
178
+ fkeys = cb_relation[:fks]&.values&.each_with_object([]) { |fk, s| s << fk[:fk] if fk.fetch(:is_bt, nil) }
179
+ cols = cb_relation[:cols]
180
+ %> <%= cb_k %> {<%
181
+ pkeys&.each do |pk| %>
182
+ <%= \"#\{dt_lookup(cols[pk].first)} #\{pk} \\\"PK#\{' fk' if fkeys&.include?(pk)}\\\"\".html_safe %><%
183
+ end %><%
184
+ fkeys&.each do |fk|
185
+ if fk.is_a?(Array)
186
+ fk.each do |fk_part| %>
187
+ <%= \"#\{dt_lookup(cols[fk_part].first)} #\{fk_part} \\\"&nbsp;&nbsp;&nbsp;&nbsp;fk\\\"\".html_safe unless pkeys&.include?(fk_part) %><%
188
+ end
189
+ else # %%% Does not yet accommodate polymorphic BTs
190
+ %>
191
+ <%= \"#\{dt_lookup(cols[fk]&.first)} #\{fk} \\\"&nbsp;&nbsp;&nbsp;&nbsp;fk\\\"\".html_safe unless pkeys&.include?(fk) %><%
192
+ end
193
+ end %>
194
+ }
195
+ <% end
196
+ # callback < %= cb_k % > erdClick
197
+ %>
198
+ </div>#{
199
+ add_column = false # For the moment, disable all schema modification things
200
+ "<%= brick_add_column(#{model.name}, #{prefix.inspect}).html_safe %>" unless add_column == false}
201
+ </div>
202
+ "
203
+ end
204
+ end
205
+
7
206
  AVO_SVG = "<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 84 90\" height=\"30\" fill=\"#3096F7\">
8
207
  <path d=\"M83.8304 81.0201C83.8343 82.9343 83.2216 84.7996 82.0822 86.3423C80.9427 87.8851 79.3363 89.0244 77.4984 89.5931C75.6606 90.1618 73.6878 90.1302 71.8694 89.5027C70.0509 88.8753 68.4823 87.6851 67.3935 86.1065L67.0796 85.6029C66.9412 85.378 66.8146 85.1463 66.6998 84.9079L66.8821 85.3007C64.1347 81.223 60.419 77.8817 56.0639 75.5723C51.7087 73.263 46.8484 72.057 41.9129 72.0609C31.75 72.0609 22.372 77.6459 16.9336 85.336C17.1412 84.7518 17.7185 83.6137 17.9463 83.0446L19.1059 80.5265L19.1414 80.456C25.2533 68.3694 37.7252 59.9541 52.0555 59.9541C53.1949 59.9541 54.3241 60.0095 55.433 60.1102C60.748 60.6134 65.8887 62.2627 70.4974 64.9433C75.1061 67.6238 79.0719 71.2712 82.1188 75.6314C82.1188 75.6314 82.1441 75.6717 82.1593 75.6868C82.1808 75.717 82.1995 75.749 82.215 75.7825C82.2821 75.8717 82.3446 75.9641 82.4024 76.0595C82.4682 76.1653 82.534 76.4221 82.5999 76.5279C82.6657 76.6336 82.772 76.82 82.848 76.9711L83.1822 77.7063C83.6094 78.7595 83.8294 79.8844 83.8304 81.0201V81.0201Z\" fill=\"currentColor\" fill-opacity=\"0.22\"></path>
9
208
  <path opacity=\"0.25\" d=\"M83.8303 81.015C83.8354 82.9297 83.2235 84.7956 82.0844 86.3393C80.9453 87.8829 79.339 89.0229 77.5008 89.5923C75.6627 90.1617 73.6895 90.1304 71.8706 89.5031C70.0516 88.8758 68.4826 87.6854 67.3935 86.1065L67.0796 85.6029C66.9412 85.3746 66.8146 85.1429 66.6998 84.9079L66.8821 85.3007C64.1353 81.222 60.4199 77.8797 56.0647 75.5695C51.7095 73.2593 46.8488 72.0524 41.9129 72.0558C31.75 72.0558 22.372 77.6408 16.9336 85.3309C17.1412 84.7467 17.7185 83.6086 17.9463 83.0395L19.1059 80.5214L19.1414 80.4509C22.1906 74.357 26.8837 69.2264 32.6961 65.6326C38.5086 62.0387 45.2114 60.1232 52.0555 60.1001C53.1949 60.1001 54.3241 60.1555 55.433 60.2562C60.7479 60.7594 65.8887 62.4087 70.4974 65.0893C75.1061 67.7698 79.0719 71.4172 82.1188 75.7775C82.1188 75.7775 82.1441 75.8177 82.1593 75.8328C82.1808 75.863 82.1995 75.895 82.215 75.9285C82.2821 76.0177 82.3446 76.1101 82.4024 76.2055L82.5999 76.5228C82.6859 76.6638 82.772 76.8149 82.848 76.966L83.1822 77.7012C83.6093 78.7544 83.8294 79.8793 83.8303 81.015Z\" fill=\"currentColor\" fill-opacity=\"0.22\"></path>
@@ -37,7 +37,7 @@ module Brick
37
37
  # puts ActiveRecord::Base.execute_sql("SELECT current_setting('SEARCH_PATH')").to_a.inspect
38
38
 
39
39
  # ---------------------------
40
- # 2. Figure out schema things
40
+ # 1. Figure out schema things
41
41
  is_postgres = nil
42
42
  is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer'
43
43
  case ActiveRecord::Base.connection.adapter_name
@@ -111,7 +111,7 @@ module Brick
111
111
  ::Brick.db_schemas ||= {}
112
112
 
113
113
  # ---------------------
114
- # 3. Tables and columns
114
+ # 2. Tables and columns
115
115
  # %%% Retrieve internal ActiveRecord table names like this:
116
116
  # ActiveRecord::Base.internal_metadata_table_name, ActiveRecord::Base.schema_migrations_table_name
117
117
  # For if it's not SQLite -- so this is the Postgres and MySQL version
@@ -243,7 +243,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
243
243
  # schema = ::Brick.default_schema # Reset back for this next round of fun
244
244
 
245
245
  # ---------------------------------------------
246
- # 4. Foreign key info
246
+ # 3. Foreign key info
247
247
  # (done in two parts which get JOINed together in Ruby code)
248
248
  kcus = nil
249
249
  entry_type = nil
@@ -277,14 +277,15 @@ module Brick
277
277
  get("/#{controller_prefix}brick_status", to: 'brick_gem#status', as: status_as.to_s)
278
278
  end
279
279
 
280
- # ::Brick.config.add_schema &&
281
- if (schema_as = "#{controller_prefix.tr('/', '_')}brick_schema".to_sym)
282
- (
283
- !(schema_route = instance_variable_get(:@set).named_routes.find { |route| route.first == schema_as }&.last) ||
284
- !schema_route.ast.to_s.include?("/#{controller_prefix}brick_schema/")
285
- )
286
- post("/#{controller_prefix}brick_schema", to: 'brick_gem#schema_create', as: schema_as.to_s)
287
- end
280
+ # # ::Brick.config.add_schema &&
281
+ # # Currently can only do adding columns
282
+ # if (schema_as = "#{controller_prefix.tr('/', '_')}brick_schema".to_sym)
283
+ # (
284
+ # !(schema_route = instance_variable_get(:@set).named_routes.find { |route| route.first == schema_as }&.last) ||
285
+ # !schema_route.ast.to_s.include?("/#{controller_prefix}brick_schema/")
286
+ # )
287
+ # post("/#{controller_prefix}brick_schema", to: 'brick_gem#schema_create', as: schema_as.to_s)
288
+ # end
288
289
 
289
290
  if ::Brick.config.add_orphans && (orphans_as = "#{controller_prefix.tr('/', '_')}brick_orphans".to_sym)
290
291
  (
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 206
8
+ TINY = 207
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
@@ -565,6 +565,12 @@ module Brick
565
565
  Brick.config.hmts = assocs
566
566
  end
567
567
 
568
+ # Tables to treat as associative, even when they have data columns
569
+ # @api public
570
+ def treat_as_associative=(tables)
571
+ Brick.config.treat_as_associative = tables
572
+ end
573
+
568
574
  # Polymorphic associations
569
575
  def polymorphics=(polys)
570
576
  polys = polys.each_with_object({}) { |poly, s| s[poly] = nil } if polys.is_a?(Array)
@@ -674,15 +680,18 @@ In config/initializers/brick.rb appropriate entries would look something like:
674
680
  end
675
681
 
676
682
  # Find associative tables that can be set up for has_many :through
677
- ::Brick.relations.each do |key, tbl|
678
- next if key.is_a?(Symbol)
679
-
680
- tbl_cols = tbl[:cols].keys
681
- fks = tbl[:fks].each_with_object({}) { |fk, s| s[fk.last[:fk]] = [fk.last[:assoc_name], fk.last[:inverse_table]] if fk.last[:is_bt]; s }
682
- # Aside from the primary key and the metadata columns created_at, updated_at, and deleted_at, if this table only has
683
- # foreign keys then it can act as an associative table and thus be used with has_many :through.
684
- if fks.length > 1 && (tbl_cols - fks.keys - (::Brick.config.metadata_columns || []) - (tbl[:pkey].values.first || [])).length.zero?
685
- fks.each { |fk| tbl[:hmt_fks][fk.first] = fk.last }
683
+ ::Brick.relations.each do |tbl_name, relation|
684
+ next if tbl_name.is_a?(Symbol)
685
+
686
+ tbl_cols = relation[:cols].keys
687
+ fks = relation[:fks].each_with_object({}) { |fk, s| s[fk.last[:fk]] = [fk.last[:assoc_name], fk.last[:inverse_table]] if fk.last[:is_bt]; s }
688
+ # Aside from the primary key and the metadata columns created_at, updated_at, and deleted_at, if this table
689
+ # only has foreign keys then it can act as an associative table and thus be used with has_many :through.
690
+ if fks.length > 1 && (
691
+ ::Brick.config.treat_as_associative&.include?(tbl_name) ||
692
+ (tbl_cols - fks.keys - (::Brick.config.metadata_columns || []) - (relation[:pkey].values.first || [])).length.zero?
693
+ )
694
+ fks.each { |fk| relation[:hmt_fks][fk.first] = fk.last }
686
695
  end
687
696
  end
688
697
  end
@@ -304,6 +304,12 @@ if ActiveRecord::Base.respond_to?(:brick_select) && !::Brick.initializer_loaded
304
304
  # # Auto-create specific has_many ___, through: ___ associations
305
305
  # Brick.hmts = [['recipes', 'recipe_ingredients', 'ingredients']]
306
306
 
307
+ # # Treat specific tables as being associative, using them to wire up HMT relationships. (This is normally the
308
+ # # default when a table contains only foreign keys, but when that otherwise associative \"JOIN\" table has any
309
+ # # other data columns, it is considered a data table and not really associative. This overrides in order to
310
+ # # have a table to still be treated as associative, causing HMTs to be auto-generated.)
311
+ # Brick.treat_as_associative = ['flights']
312
+
307
313
  # # We normally don't show the timestamp columns \"created_at\", \"updated_at\", and \"deleted_at\", and also do
308
314
  # # not consider them when finding associative tables to support an N:M association. (That is, ones that can be a
309
315
  # # part of a has_many :through association.) If you want to use different exclusion columns than our defaults
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.206
4
+ version: 1.0.207
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorin Thwaits
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-13 00:00:00.000000000 Z
11
+ date: 2024-03-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord