brick 1.0.103 → 1.0.105

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: 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