brick 1.0.103 → 1.0.105

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: 74743982216a9827513b4aaa68fe0887c9ba3e7b78c45ff52f0a2f2b99dc7b88
4
- data.tar.gz: 3857a95e891e7561d285dd3ecbdeb6c4c16cc9a8205e7b733b55867ed7a69f64
3
+ metadata.gz: 48fc69654d7cbdf5c7feb9ba7803b7a953c6c0ccd3c176614fe9a61994bb39e1
4
+ data.tar.gz: f3a4837d334cf97cb491cacd658cb6e6d8e3f3a5c640f80c151a083bf6811b7a
5
5
  SHA512:
6
- metadata.gz: 8b71aa5b1d4da8c4aa1ce88af6d0767c12bdf72b2457594575795d4b9b3f36d1bfb2adef82413a3bc80e041813e7bc6c05768016c5e4cfc197e3bbb730113934
7
- data.tar.gz: 6399e040cb4b54eac4d7a09b907cfba11037a4b3391dd3a86b4845e79bd0c4d4e169586c095fdaaaa8506f346628072c9c4c9b6d2ed60c3306a58866e6f9d030
6
+ metadata.gz: e51134710bae78f033b9dcf7827cd7c087cfd642ef8bc6244e2429f22158a80e9f804022bc703f55fa55f85cbc009ff25440289205ade1b415f5ba0af3d44c77
7
+ data.tar.gz: d6069676ad70e8401f7597b92b641f0f95e3c28fffa54747f630932500c778d7b4ac967c5bbd70059647d11d398598362d9c0bb5c82ef0500b1025371104a578
data/lib/brick/config.rb CHANGED
@@ -94,13 +94,13 @@ module Brick
94
94
  @mutex.synchronize { @enable_api = enable }
95
95
  end
96
96
 
97
- def api_root
97
+ def api_roots
98
98
  ver = api_version
99
- @mutex.synchronize { @api_root || "/api/#{ver}/" }
99
+ @mutex.synchronize { @api_roots || ["/api/#{ver}/"] }
100
100
  end
101
101
 
102
- def api_root=(path)
103
- @mutex.synchronize { @api_root = path }
102
+ def api_roots=(path)
103
+ @mutex.synchronize { @api_roots = path }
104
104
  end
105
105
 
106
106
  def api_version
@@ -257,11 +257,11 @@ module ActiveRecord
257
257
  assoc_html_name ? "#{assoc_name}-#{link}".html_safe : link
258
258
  end
259
259
 
260
- def self._brick_index(mode = nil)
260
+ def self._brick_index(mode = nil, separator = '_')
261
261
  tbl_parts = ((mode == :singular) ? table_name.singularize : table_name).split('.')
262
262
  tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.length > 1 && tbl_parts.first == ::Brick.apartment_default_tenant
263
263
  tbl_parts.unshift(::Brick.config.path_prefix) if ::Brick.config.path_prefix
264
- index = tbl_parts.map(&:underscore).join('_')
264
+ index = tbl_parts.map(&:underscore).join(separator)
265
265
  # Rails applies an _index suffix to that route when the resource name is singular
266
266
  index << '_index' if mode != :singular && index == index.singularize
267
267
  index
@@ -396,6 +396,8 @@ module ActiveRecord
396
396
  end
397
397
 
398
398
  class Relation
399
+ attr_accessor :_brick_page_num
400
+
399
401
  # Links from ActiveRecord association pathing names over to real table correlation names
400
402
  # that get chosen when the AREL AST tree is walked.
401
403
  def brick_links
@@ -785,8 +787,21 @@ JOIN (SELECT #{hm_selects.map { |s| "#{'br_t0.' if from_clause}#{s}" }.join(', '
785
787
  end
786
788
  self.order_values |= final_order_by # Same as: order!(*final_order_by)
787
789
  end
788
- # Don't want to get too carried away just yet
789
- self.limit_value = 1000 # Same as: limit!(1000)
790
+ if (page = params['_brick_page']&.to_i)
791
+ page = 1 if page < 1
792
+ limit = params['_brick_page_size'] || 1000
793
+ offset = (page - 1) * limit.to_i
794
+ else
795
+ offset = params['_brick_offset']
796
+ limit = params['_brick_limit']
797
+ end
798
+ if offset.is_a?(Numeric) || offset&.present?
799
+ offset = offset.to_i
800
+ self.offset_value = offset unless offset == 0
801
+ @_brick_page_num = (offset / limit.to_i) + 1 if limit&.!= 0 && (offset % limit.to_i) == 0
802
+ end
803
+ # By default just 1000 rows (Like doing: limit!(1000) but this way is compatible with AR <= 4.2)
804
+ self.limit_value = limit&.to_i || 1000 unless limit.is_a?(String) && limit.empty?
790
805
  wheres unless wheres.empty? # Return the specific parameters that we did use
791
806
  end
792
807
 
@@ -1511,7 +1526,11 @@ class Object
1511
1526
  self.protect_from_forgery unless: -> { self.request.format.js? }
1512
1527
  unless is_avo
1513
1528
  self.define_method :index do
1514
- if (is_openapi || request.env['REQUEST_PATH'].start_with?(::Brick.api_root)) &&
1529
+ current_api_root = ::Brick.config.api_roots.find do |ar|
1530
+ request.path.start_with?(ar) || # Exact match?
1531
+ request.path.split('/')[-2] == ar.split('/').last # Version at least matches?
1532
+ end
1533
+ if (current_api_root || is_openapi) &&
1515
1534
  !params&.key?('_brick_schema') &&
1516
1535
  (referrer_params = request.env['HTTP_REFERER']&.split('?')&.last&.split('&')&.map { |x| x.split('=') }).present?
1517
1536
  if params
@@ -1523,6 +1542,7 @@ class Object
1523
1542
  _schema, @_is_show_schema_list = ::Brick.set_db_schema(params || api_params)
1524
1543
 
1525
1544
  if is_openapi
1545
+ current_api_ver = current_api_root.split('/').last&.[](1..-1).to_i
1526
1546
  json = { 'openapi': '3.0.1', 'info': { 'title': Rswag::Ui.config.config_object[:urls].last&.fetch(:name, 'API documentation'), 'version': ::Brick.config.api_version },
1527
1547
  'servers': [
1528
1548
  { 'url': '{scheme}://{defaultHost}', 'variables': {
@@ -1531,15 +1551,19 @@ class Object
1531
1551
  } }
1532
1552
  ]
1533
1553
  }
1534
- json['paths'] = relations.inject({}) do |s, relation|
1554
+ json['paths'] = relations.each_with_object({}) do |relation, s|
1535
1555
  unless ::Brick.config.enable_api == false
1556
+ next if (api_vers = relation.last.fetch(:api, nil)) &&
1557
+ !(api_ver_path = api_vers[current_api_ver])
1558
+
1559
+ relation_name = api_ver_path || relation.first.tr('.', '/')
1536
1560
  table_description = relation.last[:description]
1537
- s["#{::Brick.config.api_root}#{relation.first.tr('.', '/')}"] = {
1561
+ s["#{current_api_root}#{relation_name}"] = {
1538
1562
  'get': {
1539
1563
  'summary': "list #{relation.first}",
1540
1564
  'description': table_description,
1541
1565
  'parameters': relation.last[:cols].map do |k, v|
1542
- param = { 'name' => k, 'schema': { 'type': v.first } }
1566
+ param = { in: 'query', 'name' => k, 'schema': { 'type': v.first } }
1543
1567
  if (col_descrip = relation.last.fetch(:col_descrips, nil)&.fetch(k, nil))
1544
1568
  param['description'] = col_descrip
1545
1569
  end
@@ -1549,7 +1573,7 @@ class Object
1549
1573
  }
1550
1574
  }
1551
1575
 
1552
- s["#{::Brick.config.api_root}#{relation.first.tr('.', '/')}/{id}"] = {
1576
+ s["#{current_api_root}#{relation_name}/{id}"] = {
1553
1577
  'patch': {
1554
1578
  'summary': "update a #{relation.first.singularize}",
1555
1579
  'description': table_description,
@@ -1563,7 +1587,6 @@ class Object
1563
1587
  'responses': { '200': { 'description': 'successful' } }
1564
1588
  }
1565
1589
  } unless relation.last.fetch(:isView, nil)
1566
- s
1567
1590
  end
1568
1591
  end
1569
1592
  render inline: json.to_json, content_type: request.format
@@ -1577,9 +1600,10 @@ class Object
1577
1600
  end
1578
1601
  render inline: exported_csv, content_type: request.format
1579
1602
  return
1580
- elsif request.format == :js || request.path.start_with?('/api/') # Asking for JSON?
1603
+ elsif request.format == :js || current_api_root # Asking for JSON?
1604
+ # %%% Add: where, order, page, page_size, offset, limit
1581
1605
  data = (model.is_view? || !Object.const_defined?('DutyFree')) ? model.limit(1000) : model.df_export(model.brick_import_template)
1582
- render inline: data.to_json, content_type: request.format == '*/*' ? 'application/json' : request.format
1606
+ render inline: { data: data }.to_json, content_type: request.format == '*/*' ? 'application/json' : request.format
1583
1607
  return
1584
1608
  end
1585
1609
 
@@ -228,8 +228,8 @@ window.addEventListener(\"popstate\", linkSchemas);
228
228
  # When available, add a clickable brick icon to go to the Brick version of the page
229
229
  PanelComponent.class_exec do
230
230
  alias _brick_init initialize
231
- def initialize(*args)
232
- _brick_init(*args)
231
+ def initialize(*args, **kwargs)
232
+ _brick_init(*args, **kwargs)
233
233
  @name = BrickTitle.new(@name, self)
234
234
  end
235
235
  end
@@ -263,6 +263,16 @@ window.addEventListener(\"popstate\", linkSchemas);
263
263
  _brick_resource_view_path
264
264
  end
265
265
  end
266
+
267
+ module Concerns::HasFields
268
+ class_methods do
269
+ alias _brick_field field
270
+ def field(name, *args, **kwargs, &block)
271
+ kwargs.merge!(args.pop) if args.last.is_a?(Hash)
272
+ _brick_field(name, **kwargs, &block)
273
+ end
274
+ end
275
+ end
266
276
  end # module Avo
267
277
 
268
278
  # Steer any Avo-related controller/action based URL lookups to the Avo RouteSet
@@ -454,6 +464,12 @@ window.addEventListener(\"popstate\", linkSchemas);
454
464
  table_options << "<option value=\"#{prefix}brick_orphans\">(Orphans)</option>".html_safe if is_orphans
455
465
  table_options << "<option value=\"#{prefix}brick_orphans\">(Crosstab)</option>".html_safe if is_crosstab
456
466
  css = +"<style>
467
+ #titleSticky {
468
+ position: sticky;
469
+ display: inline-block;
470
+ left: 0;
471
+ }
472
+
457
473
  h1, h3 {
458
474
  margin-bottom: 0;
459
475
  }
@@ -465,7 +481,6 @@ h1, h3 {
465
481
  cursor: pointer;
466
482
  }
467
483
  #mermaidErd {
468
- position: relative;
469
484
  display: none;
470
485
  }
471
486
  #mermaidErd .exclude {
@@ -866,6 +881,7 @@ if (grid) {
866
881
  // });
867
882
  }
868
883
  function setHeaderSizes() {
884
+ document.getElementById(\"titleBox\").style.width = grid.clientWidth;
869
885
  // console.log(\"start\");
870
886
  // See if the headerTop is already populated
871
887
  // %%% Grab the TRs from headerTop, clear it out, do this stuff, add them back
@@ -1112,13 +1128,16 @@ erDiagram
1112
1128
  %></title>
1113
1129
  </head>
1114
1130
  <body>
1131
+ <div id=\"titleBox\"><div id=\"titleSticky\">
1115
1132
  <p style=\"color: green\"><%= notice %></p>#{"
1116
1133
  #{schema_options}" if schema_options}
1117
1134
  <select id=\"tbl\">#{table_options}</select>
1118
1135
  <table id=\"resourceName\"><tr>
1119
- <td><h1><%= model.name %></h1></td>
1136
+ <td><h1><%= td_count = 2
1137
+ model.name %></h1></td>
1120
1138
  <td id=\"imgErd\" title=\"Show ERD\"></td>
1121
- <% if Object.const_defined?('Avo') && ::Avo.respond_to?(:railtie_namespace) %>
1139
+ <% if Object.const_defined?('Avo') && ::Avo.respond_to?(:railtie_namespace)
1140
+ td_count += 1 %>
1122
1141
  <td><%= link_to_brick(
1123
1142
  avo_svg,
1124
1143
  { index_proc: Proc.new do |avo_model|
@@ -1127,7 +1146,9 @@ erDiagram
1127
1146
  title: \"#\{model.name} in Avo\" }
1128
1147
  ) %></td>
1129
1148
  <% end %>
1130
- </tr></table>#{template_link}<%
1149
+ </tr><%= if (page_num = @#{table_name}._brick_page_num)
1150
+ \"<tr><td colspan=\\\"#\{td_count}\\\">Page #\{page_num}</td></tr>\".html_safe
1151
+ end %></table>#{template_link}<%
1131
1152
  if description.present? %><%=
1132
1153
  description %><br><%
1133
1154
  end
@@ -1162,6 +1183,7 @@ erDiagram
1162
1183
  });
1163
1184
  </script>
1164
1185
  <% end %>
1186
+ </div></div>
1165
1187
  #{erd_markup}
1166
1188
 
1167
1189
  <%= # Consider getting the name from the association -- hm.first.name -- if a more \"friendly\" alias should be used for a screwy table name
@@ -1327,7 +1349,7 @@ end
1327
1349
  options[:url] = send(\"#\{#{model_name}._brick_index(:singular)}_path\".to_sym, obj) if ::Brick.config.path_prefix
1328
1350
  %>
1329
1351
  <br><br>
1330
- <%= form_for(obj.becomes(#{model_name}), options) do |f| %>
1352
+ <%= form_for(obj.becomes(#{model_name}), options) do |f| %>
1331
1353
  <table class=\"shadow\">
1332
1354
  <% has_fields = false
1333
1355
  @#{obj_name}.attributes.each do |k, val|
@@ -1432,7 +1454,7 @@ end
1432
1454
  is_revert = false %>
1433
1455
  <% else %>
1434
1456
  <%= is_revert = false
1435
- display_value(col_type, val) %>
1457
+ display_value(col_type, val).html_safe %>
1436
1458
  <% end
1437
1459
  end
1438
1460
  if is_revert
@@ -1449,7 +1471,7 @@ end
1449
1471
  <tr><td colspan=\"2\">(No displayable fields)</td></tr>
1450
1472
  <% end %>
1451
1473
  </table>
1452
- <% end %>
1474
+ <% end %>
1453
1475
 
1454
1476
  #{unless args.first == 'new'
1455
1477
  # Was: confirm_are_you_sure = ActionView.version < ::Gem::Version.new('7.0') ? "data: { confirm: 'Delete #\{model_name} -- Are you sure?' }" : "form: { data: { turbo_confirm: 'Delete #\{model_name} -- Are you sure?' } }"
@@ -1538,7 +1560,7 @@ flatpickr(\".timepicker\", {enableTime: true, noCalendar: true});
1538
1560
  if (imgErd) imgErd.addEventListener(\"click\", showErd);
1539
1561
  function showErd() {
1540
1562
  imgErd.style.display = \"none\";
1541
- mermaidErd.style.display = \"inline-block\";
1563
+ mermaidErd.style.display = \"block\";
1542
1564
  if (mermaidCode) return; // Cut it short if we've already rendered the diagram
1543
1565
 
1544
1566
  mermaidCode = document.createElement(\"SCRIPT\");
@@ -56,85 +56,85 @@ module Brick::Rails::FormTags
56
56
  end
57
57
  out << "</tr></thead>
58
58
  <tbody>"
59
- # %%% Have once gotten this error with MSSQL referring to http://localhost:3000/warehouse/cold_room_temperatures__archive
60
- # ActiveRecord::StatementTimeout in Warehouse::ColdRoomTemperatures_Archive#index
61
- # TinyTds::Error: Adaptive Server connection timed out
62
- # (After restarting the server it worked fine again.)
63
- relation.each do |obj|
64
- out << "<tr>\n"
65
- out << "<td>#{link_to('⇛', send("#{klass._brick_index(:singular)}_path".to_sym,
66
- pk.map { |pk_part| obj.send(pk_part.to_sym) }), { class: 'big-arrow' })}</td>\n" if pk.present?
67
- sequence.each do |col_name|
68
- val = obj.attributes[col_name]
69
- out << '<td'
70
- out << ' class=\"dimmed\"' unless cols.key?(col_name) || (cust_col = cust_cols[col_name]) ||
71
- (col_name.is_a?(Symbol) && bts.key?(col_name)) # HOT
72
- out << '>'
73
- if (bt = bts[col_name])
74
- if bt[2] # Polymorphic?
75
- bt_class = obj.send("#{bt.first}_type")
76
- base_class_underscored = (::Brick.existing_stis[bt_class] || bt_class).constantize.base_class._brick_index(:singular)
77
- poly_id = obj.send("#{bt.first}_id")
78
- out << link_to("#{bt_class} ##{poly_id}", send("#{base_class_underscored}_path".to_sym, poly_id)) if poly_id
79
- else # BT or HOT
80
- bt_class = bt[1].first.first
81
- descrips = bt_descrip[bt.first][bt_class]
82
- bt_id_col = if descrips.nil?
83
- puts "Caught it in the act for obj / #{col_name}!"
84
- elsif descrips.length == 1
85
- [obj.class.reflect_on_association(bt.first)&.foreign_key]
86
- else
87
- descrips.last
88
- end
89
- bt_txt = bt_class.brick_descrip(
90
- # 0..62 because Postgres column names are limited to 63 characters
91
- obj, descrips[0..-2].map { |id| obj.send(id.last[0..62]) }, bt_id_col
92
- )
93
- bt_txt = display_binary(bt_txt).html_safe if bt_txt&.encoding&.name == 'ASCII-8BIT'
94
- bt_txt ||= "<span class=\"orphan\">&lt;&lt; Orphaned ID: #{val} >></span>" if val
95
- bt_id = bt_id_col&.map { |id_col| obj.respond_to?(id_sym = id_col.to_sym) ? obj.send(id_sym) : id_col }
96
- out << (bt_id&.first ? link_to(bt_txt, send("#{bt_class.base_class._brick_index(:singular)}_path".to_sym, bt_id)) : bt_txt || '')
97
- end
98
- elsif (hms_col = hms_cols[col_name])
99
- if hms_col.length == 1
100
- out << hms_col.first
101
- else
102
- hm_klass = (col = cols[col_name])[1]
103
- if col[2] == 'HO'
104
- descrips = bt_descrip[col_name.to_sym][hm_klass]
105
- if (ho_id = (ho_id_col = descrips.last).map { |id_col| obj.send(id_col.to_sym) })&.first
106
- ho_txt = hm_klass.brick_descrip(obj, descrips[0..-2].map { |id| obj.send(id.last[0..62]) }, ho_id_col)
107
- out << link_to(ho_txt, send("#{hm_klass.base_class._brick_index(:singular)}_path".to_sym, ho_id))
108
- end
59
+ # %%% Have once gotten this error with MSSQL referring to http://localhost:3000/warehouse/cold_room_temperatures__archive
60
+ # ActiveRecord::StatementTimeout in Warehouse::ColdRoomTemperatures_Archive#index
61
+ # TinyTds::Error: Adaptive Server connection timed out
62
+ # (After restarting the server it worked fine again.)
63
+ relation.each do |obj|
64
+ out << "<tr>\n"
65
+ out << "<td>#{link_to('⇛', send("#{klass._brick_index(:singular)}_path".to_sym,
66
+ pk.map { |pk_part| obj.send(pk_part.to_sym) }), { class: 'big-arrow' })}</td>\n" if pk.present?
67
+ sequence.each do |col_name|
68
+ val = obj.attributes[col_name]
69
+ out << '<td'
70
+ out << ' class=\"dimmed\"' unless cols.key?(col_name) || (cust_col = cust_cols[col_name]) ||
71
+ (col_name.is_a?(Symbol) && bts.key?(col_name)) # HOT
72
+ out << '>'
73
+ if (bt = bts[col_name])
74
+ if bt[2] # Polymorphic?
75
+ bt_class = obj.send("#{bt.first}_type")
76
+ base_class_underscored = (::Brick.existing_stis[bt_class] || bt_class).constantize.base_class._brick_index(:singular)
77
+ poly_id = obj.send("#{bt.first}_id")
78
+ out << link_to("#{bt_class} ##{poly_id}", send("#{base_class_underscored}_path".to_sym, poly_id)) if poly_id
79
+ else # BT or HOT
80
+ bt_class = bt[1].first.first
81
+ descrips = bt_descrip[bt.first][bt_class]
82
+ bt_id_col = if descrips.nil?
83
+ puts "Caught it in the act for obj / #{col_name}!"
84
+ elsif descrips.length == 1
85
+ [obj.class.reflect_on_association(bt.first)&.foreign_key]
86
+ else
87
+ descrips.last
88
+ end
89
+ bt_txt = bt_class.brick_descrip(
90
+ # 0..62 because Postgres column names are limited to 63 characters
91
+ obj, descrips[0..-2].map { |id| obj.send(id.last[0..62]) }, bt_id_col
92
+ )
93
+ bt_txt = display_binary(bt_txt).html_safe if bt_txt&.encoding&.name == 'ASCII-8BIT'
94
+ bt_txt ||= "<span class=\"orphan\">&lt;&lt; Orphaned ID: #{val} >></span>" if val
95
+ bt_id = bt_id_col&.map { |id_col| obj.respond_to?(id_sym = id_col.to_sym) ? obj.send(id_sym) : id_col }
96
+ out << (bt_id&.first ? link_to(bt_txt, send("#{bt_class.base_class._brick_index(:singular)}_path".to_sym, bt_id)) : bt_txt || '')
97
+ end
98
+ elsif (hms_col = hms_cols[col_name])
99
+ if hms_col.length == 1
100
+ out << hms_col.first
109
101
  else
110
- if (ct = obj.send(hms_col[1].to_sym)&.to_i)&.positive?
111
- out << "#{link_to("#{ct || 'View'} #{hms_col.first}",
112
- send("#{hm_klass._brick_index}_path".to_sym,
113
- hms_col[2].each_with_object({}) { |v, s| s[v.first] = v.last.is_a?(String) ? v.last : obj.send(v.last) })
114
- )}\n"
102
+ hm_klass = (col = cols[col_name])[1]
103
+ if col[2] == 'HO'
104
+ descrips = bt_descrip[col_name.to_sym][hm_klass]
105
+ if (ho_id = (ho_id_col = descrips.last).map { |id_col| obj.send(id_col.to_sym) })&.first
106
+ ho_txt = hm_klass.brick_descrip(obj, descrips[0..-2].map { |id| obj.send(id.last[0..62]) }, ho_id_col)
107
+ out << link_to(ho_txt, send("#{hm_klass.base_class._brick_index(:singular)}_path".to_sym, ho_id))
108
+ end
109
+ else
110
+ if (ct = obj.send(hms_col[1].to_sym)&.to_i)&.positive?
111
+ out << "#{link_to("#{ct || 'View'} #{hms_col.first}",
112
+ send("#{hm_klass._brick_index}_path".to_sym,
113
+ hms_col[2].each_with_object({}) { |v, s| s[v.first] = v.last.is_a?(String) ? v.last : obj.send(v.last) })
114
+ )}\n"
115
+ end
115
116
  end
116
117
  end
118
+ elsif (col = cols[col_name]).is_a?(ActiveRecord::ConnectionAdapters::Column)
119
+ binding.pry if col.is_a?(Array)
120
+ col_type = col&.sql_type == 'geography' ? col.sql_type : col&.type
121
+ out << display_value(col_type || col&.sql_type, val).to_s
122
+ elsif cust_col
123
+ data = cust_col.first.map { |cc_part| obj.send(cc_part.last) }
124
+ cust_txt = klass.brick_descrip(cust_col[-2], data)
125
+ if (link_id = obj.send(cust_col.last[1]) if cust_col.last)
126
+ out << link_to(cust_txt, send("#{cust_col.last.first._brick_index(:singular)}_path", link_id))
127
+ else
128
+ out << (cust_txt || '')
129
+ end
130
+ else # Bad column name!
131
+ out << '?'
117
132
  end
118
- elsif (col = cols[col_name]).is_a?(ActiveRecord::ConnectionAdapters::Column)
119
- binding.pry if col.is_a?(Array)
120
- col_type = col&.sql_type == 'geography' ? col.sql_type : col&.type
121
- out << display_value(col_type || col&.sql_type, val).to_s
122
- elsif cust_col
123
- data = cust_col.first.map { |cc_part| obj.send(cc_part.last) }
124
- cust_txt = klass.brick_descrip(cust_col[-2], data)
125
- if (link_id = obj.send(cust_col.last[1]) if cust_col.last)
126
- out << link_to(cust_txt, send("#{cust_col.last.first._brick_index(:singular)}_path", link_id))
127
- else
128
- out << (cust_txt || '')
129
- end
130
- else # Bad column name!
131
- out << '?'
133
+ out << '</td>'
132
134
  end
133
- out << '</td>'
135
+ out << '</tr>'
134
136
  end
135
- out << '</tr>'
136
- end
137
- out << " </tbody>
137
+ out << " </tbody>
138
138
  </table>
139
139
  "
140
140
  out.html_safe
@@ -143,6 +143,16 @@ module Brick::Rails::FormTags
143
143
  def link_to_brick(*args, **kwargs)
144
144
  return unless ::Brick.config.mode == :on
145
145
 
146
+ kwargs.merge!(args.pop) if args.last.is_a?(Hash)
147
+ # Avoid infinite recursion
148
+ if (visited = kwargs.fetch(:visited, nil))
149
+ return if visited.key?(object_id)
150
+
151
+ kwargs[:visited][object_id] = nil
152
+ else
153
+ kwargs[:visited] = {}
154
+ end
155
+
146
156
  text = ((args.first.is_a?(String) || args.first.is_a?(Proc)) && args.shift) || args[1]
147
157
  text = text.call if text.is_a?(Proc)
148
158
  klass_or_obj = ((args.first.is_a?(ActiveRecord::Relation) ||
@@ -192,13 +202,14 @@ module Brick::Rails::FormTags
192
202
  app_routes = Rails.application.routes # In case we're operating in another engine, reference the application since Brick routes are placed there.
193
203
  if (klass_or_obj&.is_a?(Class) && klass_or_obj < ActiveRecord::Base) ||
194
204
  (klass_or_obj&.is_a?(ActiveRecord::Base) && klass_or_obj.new_record? && (klass_or_obj = klass_or_obj.class))
195
- path = (proc = kwargs[:index_proc]) ? proc.call(klass_or_obj) : "#{app_routes.path_for(controller: klass_or_obj.base_class._brick_index, action: :index)}#{filter}"
205
+ path = (proc = kwargs[:index_proc]) ? proc.call(klass_or_obj) : "#{app_routes.path_for(controller: klass_or_obj.base_class._brick_index(nil, '/'), action: :index)}#{filter}"
196
206
  lt_args = [text || "Index for #{klass_or_obj.name.pluralize}", path]
197
207
  else
198
208
  # If there are multiple incoming parameters then last one is probably the actual ID, and first few might be some nested tree of stuff leading up to it
199
- path = (proc = kwargs[:show_proc]) ? proc.call(klass_or_obj) : "#{app_routes.path_for(controller: klass_or_obj.class.base_class._brick_index, action: :show, id: klass_or_obj)}#{filter}"
209
+ path = (proc = kwargs[:show_proc]) ? proc.call(klass_or_obj) : "#{app_routes.path_for(controller: klass_or_obj.class.base_class._brick_index(nil, '/'), action: :show, id: klass_or_obj)}#{filter}"
200
210
  lt_args = [text || "Show this #{klass_or_obj.class.name}", path]
201
211
  end
212
+ kwargs.delete(:visited)
202
213
  link_to(*lt_args, **kwargs)
203
214
  else
204
215
  # puts "Warning: link_to_brick could not find a class for \"#{controller_path}\" -- consider setting @_brick_model within that controller."
@@ -215,7 +226,7 @@ module Brick::Rails::FormTags
215
226
  if links.length == 1 # If there's only one match then use any text that was supplied
216
227
  link_to_brick(text || links.first.last.join('/'), links.first.first, **kwargs)
217
228
  else
218
- links.map { |k, v| link_to_brick(v.join('/'), v, **kwargs) }.join(' &nbsp; ').html_safe
229
+ links.each_with_object([]) { |v, s| s << link if link = link_to_brick(v.join('/'), v, **kwargs) }.join(' &nbsp; ').html_safe
219
230
  end
220
231
  end
221
232
  end # link_to_brick
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 103
8
+ TINY = 105
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
@@ -334,12 +334,17 @@ module Brick
334
334
 
335
335
  # @api public
336
336
  def api_root=(path)
337
- Brick.config.api_root = path
337
+ Brick.config.api_roots = [path]
338
338
  end
339
339
 
340
340
  # @api public
341
- def api_root
342
- Brick.config.api_root
341
+ def api_roots=(paths)
342
+ Brick.config.api_roots = paths
343
+ end
344
+
345
+ # @api public
346
+ def api_roots
347
+ Brick.config.api_roots
343
348
  end
344
349
 
345
350
  # @api public
@@ -657,19 +662,65 @@ In config/initializers/brick.rb appropriate entries would look something like:
657
662
  # Turn something like {"::Spouse"=>"Person", "::Friend"=>"Person"} into {"Person"=>["Spouse", "Friend"]}
658
663
  s[v.last] << v.first[2..-1] unless v.first.end_with?('::')
659
664
  end
665
+ versioned_views = {} # Track which views have already been done for each api_root
660
666
  ::Brick.relations.each do |k, v|
661
667
  next if !(controller_name = v.fetch(:resource, nil)&.pluralize) || existing_controllers.key?(controller_name)
662
668
 
669
+ schema_name = v.fetch(:schema, nil)
663
670
  options = {}
664
671
  options[:only] = [:index, :show] if v.key?(:isView)
665
- # First do the API routes
672
+ # First do the API routes if necessary
666
673
  full_resource = nil
667
- if (schema_name = v.fetch(:schema, nil))
668
- full_resource = "#{schema_name}/#{v[:resource]}"
669
- send(:get, "#{::Brick.api_root}#{full_resource}", { to: "#{controller_prefix}#{schema_name}/#{controller_name}#index" }) if Object.const_defined?('Rswag::Ui')
670
- else
671
- # Normally goes to something like: /api/v1/employees
672
- send(:get, "#{::Brick.api_root}#{v[:resource]}", { to: "#{controller_prefix}#{controller_name}#index" }) if Object.const_defined?('Rswag::Ui')
674
+ ::Brick.api_roots&.each do |api_root|
675
+ api_done_views = (versioned_views[api_root] ||= {})
676
+ found = nil
677
+ view_relation = nil
678
+ # If it's a view then see if there's a versioned one available by searching for resource names
679
+ # versioned with the closest number (equal to or less than) compared with our API version number.
680
+ if v.key?(:isView) && (ver = k.match(/^v([\d_]*)/).captures.first)[-1] == '_'
681
+ next if api_done_views.key?(unversioned = k[ver.length + 1..-1])
682
+
683
+ # # if ().length.positive? # Does it have a version number?
684
+ # try_num = (ver_num = (ver = ver[1..-1].gsub('_', '.')).to_d)
685
+
686
+ # Expect that the last item in the path generally holds versioning information
687
+ api_ver = api_root.split('/')[-1]&.gsub('_', '.')
688
+ vn_idx = api_ver.rindex(/[^\d._]/) # Position of the first numeric digit at the end of the version number
689
+ # Was: .to_d
690
+ api_ver_num = api_ver[vn_idx + 1..-1].gsub('_', '.').to_i # Attempt to turn something like "v3" into the decimal value 3
691
+ # puts [api_ver, vn_idx, api_ver_num, unversioned].inspect
692
+
693
+ next if ver.to_i > api_ver_num # Don't surface any newer views in an older API
694
+
695
+ api_ver_num -= 1 until api_ver_num.zero? ||
696
+ (view_relation = ::Brick.relations.fetch(
697
+ found = "v#{api_ver_num}_#{k[ver.length + 1..-1]}", nil
698
+ ))
699
+ api_done_views[unversioned] = nil # Mark that for this API version this view is done
700
+
701
+ # puts "Found #{found}" if view_relation
702
+ # If we haven't found "v3_view_name" or "v2_view_name" or so forth, at the last
703
+ # fall back to simply looking for "v_view_name", and then finally "view_name".
704
+ unversioned = "v_#{unversioned}"
705
+ view_relation ||= ::Brick.relations.fetch(found = unversioned,
706
+ ::Brick.relations.fetch(found = unversioned, nil)
707
+ )
708
+ if found && view_relation && k != (found = unversioned)
709
+ view_relation[:api][api_ver_num] = found
710
+ end
711
+ end
712
+
713
+ # view_ver_num = if (first_part = k.split('_').first) =~ /^v[\d_]+/
714
+ # first_part[1..-1].gsub('_', '.').to_i
715
+ # end
716
+ controller_name = view_relation.fetch(:resource, nil)&.pluralize if view_relation
717
+ if schema_name
718
+ full_resource = "#{schema_name}/#{found || v[:resource]}"
719
+ send(:get, "#{api_root}#{full_resource}", { to: "#{controller_prefix}#{schema_name}/#{controller_name}#index" })
720
+ else
721
+ # Normally goes to something like: /api/v1/employees
722
+ send(:get, "#{api_root}#{found || v[:resource]}", { to: "#{controller_prefix}#{controller_name}#index" })
723
+ end
673
724
  end
674
725
 
675
726
  # Track routes being built
@@ -716,15 +767,19 @@ In config/initializers/brick.rb appropriate entries would look something like:
716
767
  unless ::Brick.routes_done
717
768
  if Object.const_defined?('Rswag::Ui')
718
769
  rswag_path = ::Rails.application.routes.routes.find { |r| r.app.app == Rswag::Ui::Engine }&.instance_variable_get(:@path_formatter)&.instance_variable_get(:@parts)&.join
719
- if (doc_endpoint = Rswag::Ui.config.config_object[:urls]&.last)
770
+ first_endpoint_parts = nil
771
+ (doc_endpoints = Rswag::Ui.config.config_object[:urls]&.uniq!)&.each do |doc_endpoint|
720
772
  puts "Mounting OpenApi 3.0 documentation endpoint for \"#{doc_endpoint[:name]}\" on #{doc_endpoint[:url]}"
721
773
  send(:get, doc_endpoint[:url], { to: 'brick_openapi#index' })
722
774
  endpoint_parts = doc_endpoint[:url]&.split('/')
723
- if rswag_path && endpoint_parts
724
- puts "API documentation now available when navigating to: /#{endpoint_parts&.find(&:present?)}/index.html"
775
+ first_endpoint_parts ||= endpoint_parts
776
+ end
777
+ if doc_endpoints.present?
778
+ if rswag_path && first_endpoint_parts
779
+ puts "API documentation now available when navigating to: /#{first_endpoint_parts&.find(&:present?)}/index.html"
725
780
  else
726
781
  puts "In order to make documentation available you can put this into your routes.rb:"
727
- puts " mount Rswag::Ui::Engine => '/#{endpoint_parts&.find(&:present?) || 'api-docs'}'"
782
+ puts " mount Rswag::Ui::Engine => '/#{first_endpoint_parts&.find(&:present?) || 'api-docs'}'"
728
783
  end
729
784
  else
730
785
  sample_path = rswag_path || '/api-docs'
@@ -159,8 +159,9 @@ if ActiveRecord::Base.respond_to?(:brick_select)
159
159
  # Brick.enable_views = true # Setting this to \"false\" will disable views in development
160
160
 
161
161
  # # If The Brick sees that RSwag gem is present, it allows for API resources to be automatically served out.
162
- # # You can configure the root path for these resources:
163
- # ::Brick.api_root = '/api/v1/'
162
+ # # You can configure one or more root path(s) for these resources, and when there are multiple then an attempt
163
+ # # is made to return data from that version of the view or table name, or the most recent prior to that version:
164
+ # ::Brick.api_roots = ['/api/v1/']
164
165
  # # You may also want to add an OpenAPI 3.0 documentation endpoint using Rswag::Ui:
165
166
  # Rswag::Ui.configure do |config|
166
167
  # config.swagger_endpoint '/api-docs/v1/swagger.json', 'API V1 Docs'
@@ -288,16 +289,18 @@ if ActiveRecord::Base.respond_to?(:brick_select)
288
289
 
289
290
  # # POLYMORPHIC ASSOCIATIONS
290
291
 
291
- # # Database schema to use when analysing existing data, such as deriving a list of polymorphic classes in the case that
292
- # # it wasn't originally specified.
292
+ # # Polymorphic associations are set up by providing a model name and polymorphic association name#{poly}
293
+
294
+ # # For multi-tenant databases that use a separate schema for each tenant, a single representative database schema
295
+ # # can be analysed to determine the range of polymorphic classes that can be used for each association. Hopefully
296
+ # # the schema chosen is one loaded with existing data that is representative of all possible polymorphic
297
+ # # associations.
293
298
  # Brick.schema_behavior = :namespaced
294
299
  #{Brick.config.schema_behavior.present? ? " Brick.schema_behavior = { multitenant: { schema_to_analyse: #{
295
300
  Brick.config.schema_behavior[:multitenant]&.fetch(:schema_to_analyse, nil).inspect}" :
296
301
  " # Brick.schema_behavior = { multitenant: { schema_to_analyse: 'engineering'"
297
302
  } } }
298
303
 
299
- # # Polymorphic associations are set up by providing a model name and polymorphic association name#{poly}
300
-
301
304
  # # DEFAULT ROOT ROUTE
302
305
 
303
306
  # # If a default route is not supplied, Brick attempts to find the most \"central\" table and wires up the default
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.103
4
+ version: 1.0.105
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-01-09 00:00:00.000000000 Z
11
+ date: 2023-01-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord