brick 1.0.52 → 1.0.55

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: e3d0b398d1d3926fa3e59281c976fe2f7a54bfc16de4f99683928ce71d7ace97
4
- data.tar.gz: 344c0de3e8691583e9a940ee451ad29015d46b0cbc55756d3b36e10d9ce27edb
3
+ metadata.gz: 430a2dfa5ef0caee9b99bc341ccd42a9d29e8f1d54071284aa2469e1baa0c659
4
+ data.tar.gz: b36e18de169bd9032e6c383548ec30cce06eeb97a35e8c3e4f4da935aff8f603
5
5
  SHA512:
6
- metadata.gz: e0cff4e30c30d33dd13bd5a6a4a888d52e0552fc692369c42a53846da00e9ce677ab48ba840ef1396728d339984e5f7ff35d9ce33ea60f8421b9fa19edc14575
7
- data.tar.gz: 5971fcc802dd1732f71d3bcd015e4b616f7775028a10205c7e4b9b1fa41d13c0318f581c966c223e9a0661b8b9fa52e1197ae78c007144e93ec96de61b084663
6
+ metadata.gz: c51491575b9ee56c3789619bfc74a2d9c0525750687224e94e120cbae42fad8db1abda9d82978115d9266c55fa0c6f914ce273a3613915657d8a0cf4e7fe6aeb
7
+ data.tar.gz: a0646cfdc9a2c844fd1149968d0175346c48835c6245d7fe92d0126d60708cf76514704e29dc3f506d2e06e546c8c995bf7103d6ef42be481bb931b4d2bfa73f
@@ -1116,6 +1116,11 @@ class Object
1116
1116
  if namespace && (idx = lookup_context.prefixes.index(table_name))
1117
1117
  lookup_context.prefixes[idx] = "#{namespace.name.underscore}/#{lookup_context.prefixes[idx]}"
1118
1118
  end
1119
+ @_brick_excl = session[:_brick_exclude]&.split(',')&.each_with_object([]) do |excl, s|
1120
+ if (excl_parts = excl.split('.')).first == table_name
1121
+ s << excl_parts.last
1122
+ end
1123
+ end
1119
1124
  @_brick_bt_descrip = model._br_bt_descrip
1120
1125
  @_brick_hm_counts = model._br_hm_counts
1121
1126
  @_brick_join_array = join_array
@@ -1156,8 +1161,20 @@ class Object
1156
1161
  code << " end\n"
1157
1162
  self.define_method :create do
1158
1163
  ::Brick.set_db_schema(params)
1159
- instance_variable_set("@#{singular_table_name}".to_sym,
1160
- model.send(:create, send(params_name_sym)))
1164
+ if (is_json = request.content_type == 'application/json') && (col = params['_brick_exclude'])
1165
+ session[:_brick_exclude] = ((session[:_brick_exclude]&.split(',') || []) + ["#{table_name}.#{col}"]).join(',')
1166
+ render json: { result: ::Brick.exclude_column(table_name, col) }
1167
+ elsif is_json && (col = params['_brick_unexclude'])
1168
+ if (excls = ((session[:_brick_exclude]&.split(',') || []) - ["#{table_name}.#{col}"]).join(',')).empty?
1169
+ session.delete(:_brick_exclude)
1170
+ else
1171
+ session[:_brick_exclude] = excls
1172
+ end
1173
+ render json: { result: ::Brick.unexclude_column(table_name, col) }
1174
+ else
1175
+ instance_variable_set("@#{singular_table_name}".to_sym,
1176
+ model.send(:create, send(params_name_sym)))
1177
+ end
1161
1178
  end
1162
1179
 
1163
1180
  if pk_col
@@ -1272,7 +1289,10 @@ module ActiveRecord::ConnectionHandling
1272
1289
  alias _brick_establish_connection establish_connection
1273
1290
  def establish_connection(*args)
1274
1291
  conn = _brick_establish_connection(*args)
1275
- _brick_reflect_tables
1292
+ begin
1293
+ _brick_reflect_tables
1294
+ rescue ActiveRecord::NoDatabaseError
1295
+ end
1276
1296
  conn
1277
1297
  end
1278
1298
 
@@ -1296,16 +1316,21 @@ module ActiveRecord::ConnectionHandling
1296
1316
  # puts ActiveRecord::Base.execute_sql("SELECT current_setting('SEARCH_PATH')").to_a.inspect
1297
1317
 
1298
1318
  is_postgres = nil
1299
- schema_sql = 'SELECT NULL AS table_schema;'
1300
1319
  case ActiveRecord::Base.connection.adapter_name
1301
1320
  when 'PostgreSQL'
1302
1321
  is_postgres = true
1322
+ db_schemas = ActiveRecord::Base.execute_sql('SELECT DISTINCT table_schema FROM INFORMATION_SCHEMA.tables;')
1323
+ ::Brick.db_schemas = db_schemas.each_with_object({}) do |row, s|
1324
+ row = row.is_a?(String) ? row : row['table_schema']
1325
+ # Remove any system schemas
1326
+ s[row] = nil unless ['information_schema', 'pg_catalog'].include?(row)
1327
+ end
1303
1328
  if (is_multitenant = (multitenancy = ::Brick.config.schema_behavior[:multitenant]) &&
1304
- (sta = multitenancy[:schema_to_analyse]) != 'public')
1329
+ (sta = multitenancy[:schema_to_analyse]) != 'public') &&
1330
+ ::Brick.db_schemas.include?(sta)
1305
1331
  ::Brick.default_schema = schema = sta
1306
1332
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1307
1333
  end
1308
- schema_sql = 'SELECT DISTINCT table_schema FROM INFORMATION_SCHEMA.tables;'
1309
1334
  when 'Mysql2'
1310
1335
  ::Brick.default_schema = schema = ActiveRecord::Base.connection.current_database
1311
1336
  when 'SQLite'
@@ -1322,16 +1347,7 @@ module ActiveRecord::ConnectionHandling
1322
1347
  puts "Unfamiliar with connection adapter #{ActiveRecord::Base.connection.adapter_name}"
1323
1348
  end
1324
1349
 
1325
- unless (db_schemas = ActiveRecord::Base.execute_sql(schema_sql)).is_a?(Array)
1326
- db_schemas = db_schemas.to_a
1327
- end
1328
- unless db_schemas.empty?
1329
- ::Brick.db_schemas = db_schemas.each_with_object({}) do |row, s|
1330
- row = row.is_a?(String) ? row : row['table_schema']
1331
- # Remove any system schemas
1332
- s[row] = nil unless ['information_schema', 'pg_catalog'].include?(row)
1333
- end
1334
- end
1350
+ ::Brick.db_schemas ||= []
1335
1351
 
1336
1352
  if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1337
1353
  if (possible_schema = ::Brick.config.schema_behavior&.[](:multitenant)&.[](:schema_to_analyse))
@@ -132,23 +132,24 @@ module Brick
132
132
  end
133
133
  case args.first
134
134
  when 'index'
135
- hms_columns << if hm_assoc.macro == :has_many
136
- if hm_fk_name
137
- set_ct = if skip_klass_hms.key?(assoc_name.to_sym)
138
- 'nil'
139
- else
140
- # Postgres column names are limited to 63 characters
141
- attrib_name = "_br_#{assoc_name}_ct"[0..62]
142
- "#{obj_name}.#{attrib_name} || 0"
143
- end
144
- "#{hm_assoc.name}: [#{assoc_name.inspect}, #{set_ct}, #{path_keys(hm_assoc, hm_fk_name, obj_name, pk)}]"
145
- else # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
146
- "#{hm_assoc.name}: [#{assoc_name.inspect}]"
147
- end
148
- else # has_one
149
- # 0..62 because Postgres column names are limited to 63 characters
150
- "#{hm_assoc.name}: [#{assoc_name.inspect}, nil, #{path_keys(hm_assoc, hm_fk_name, obj_name, pk)}]"
135
+ hm_entry = +"'#{hm_assoc.name}' => [#{assoc_name.inspect}"
136
+ hm_entry << if hm_assoc.macro == :has_many
137
+ if hm_fk_name # %%% Can remove this check when multiple foreign keys to same destination becomes bulletproof
138
+ set_ct = if skip_klass_hms.key?(assoc_name.to_sym)
139
+ 'nil'
140
+ else
141
+ # Postgres column names are limited to 63 characters
142
+ "#{obj_name}.#{"_br_#{assoc_name}_ct"[0..62]} || 0"
143
+ end
144
+ ", #{set_ct}, #{path_keys(hm_assoc, hm_fk_name, obj_name, pk)}"
151
145
  end
146
+ else # has_one
147
+ # 0..62 because Postgres column names are limited to 63 characters
148
+ ", nil, #{path_keys(hm_assoc, hm_fk_name, obj_name, pk)}"
149
+ end
150
+ hm_entry << ']'
151
+ puts hm_entry
152
+ hms_columns << hm_entry
152
153
  when 'show', 'update'
153
154
  hm_stuff << if hm_fk_name
154
155
  "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.tr('/', '_').pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, "@#{obj_name}", pk)} }) %>\n"
@@ -174,6 +175,9 @@ module Brick
174
175
  end.html_safe
175
176
  table_options << '<option value="brick_orphans">(Orphans)</option>'.html_safe if is_orphans
176
177
  css = +"<style>
178
+ h1, h3 {
179
+ margin-bottom: 0;
180
+ }
177
181
  #dropper {
178
182
  background-color: #eee;
179
183
  }
@@ -195,19 +199,41 @@ table {
195
199
  box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
196
200
  }
197
201
 
198
- table thead tr th, table tr th {
202
+ tr th {
199
203
  background-color: #009879;
200
204
  color: #fff;
201
205
  text-align: left;
202
206
  }
203
- #headerTop th:hover, #headerTop th:hover {
207
+ #headerTop tr th {
208
+ position: relative;
209
+ }
210
+ #headerTop tr th .exclude {
211
+ position: absolute;
212
+ display: none;
213
+ top: 0;
214
+ right: 0;
215
+ }
216
+ #headerTop tr th:hover {
204
217
  background-color: #18B090;
205
218
  }
206
- table thead tr th a, table tr th a {
219
+ #exclusions {
220
+ font-size: 0.7em;
221
+ }
222
+ #exclusions div {
223
+ border: 1px solid blue;
224
+ display: inline-block;
225
+ cursor: copy;
226
+ }
227
+ #headerTop tr th:hover .exclude {
228
+ display: inline;
229
+ cursor: pointer;
230
+ color: red;
231
+ }
232
+ tr th a {
207
233
  color: #80FFB8;
208
234
  }
209
235
 
210
- table th, table td {
236
+ tr th, tr td {
211
237
  padding: 0.2em 0.5em;
212
238
  }
213
239
 
@@ -253,6 +279,7 @@ a.big-arrow {
253
279
  }
254
280
  .dimmed {
255
281
  background-color: #C0C0C0;
282
+ text-align: center;
256
283
  }
257
284
  .orphan {
258
285
  color: red;
@@ -414,8 +441,19 @@ function setHeaderSizes() {
414
441
  for (var i = 0; i < row.childNodes.length; ++i) {
415
442
  node = row.childNodes[i];
416
443
  if (node.nodeType === 1) {
417
- var style = tr.childNodes[i].style;
418
- style.minWidth = style.maxWidth = getComputedStyle(node).width;
444
+ var th = tr.childNodes[i];
445
+ th.style.minWidth = th.style.maxWidth = getComputedStyle(node).width;
446
+ if (#{pk&.present? ? 'i > 0' : 'true'}) {
447
+ // Add <span> at the end
448
+ var span = document.createElement(\"SPAN\");
449
+ span.className = \"exclude\";
450
+ span.innerHTML = \"X\";
451
+ span.addEventListener(\"click\", function (e) {
452
+ e.stopPropagation();
453
+ doFetch(\"POST\", {_brick_exclude: this.parentElement.getAttribute(\"x-order\")});
454
+ });
455
+ th.appendChild(span);
456
+ }
419
457
  }
420
458
  }
421
459
  if (isEmpty) headerTop.appendChild(tr);
@@ -423,6 +461,18 @@ function setHeaderSizes() {
423
461
  grid.style.marginTop = \"-\" + getComputedStyle(headerTop).height;
424
462
  // console.log(\"end\");
425
463
  }
464
+ function doFetch(method, payload, success) {
465
+ payload.authenticity_token = <%= session[:_csrf_token].inspect.html_safe %>;
466
+ if (!success) {
467
+ success = function (p) {p.text().then(function (response) {
468
+ var result = JSON.parse(response).result;
469
+ if (result) location.href = location.href;
470
+ });};
471
+ }
472
+ var options = {method: method, headers: {\"Content-Type\": \"application/json\"}};
473
+ if (payload) options.body = JSON.stringify(payload);
474
+ return fetch(location.href, options).then(success);
475
+ }
426
476
  if (headerTop) {
427
477
  setHeaderSizes();
428
478
  window.addEventListener('resize', function(event) {
@@ -457,8 +507,8 @@ if (headerTop) {
457
507
  });
458
508
  btnImport.addEventListener(\"click\", function () {
459
509
  fetch(changeout(<%= #{path_obj_name}_path(-1, format: :csv).inspect.html_safe %>, \"_brick_schema\", brickSchema), {
460
- method: 'PATCH',
461
- headers: { 'Content-Type': 'text/tab-separated-values' },
510
+ method: \"PATCH\",
511
+ headers: { \"Content-Type\": \"text/tab-separated-values\" },
462
512
  body: droppedTSV
463
513
  }).then(function (tsvResponse) {
464
514
  btnImport.style.display = \"none\";
@@ -536,6 +586,7 @@ if (headerTop) {
536
586
  if (description = (relation = Brick.relations[#{model_name}.table_name])&.fetch(:description, nil)) %><%=
537
587
  description %><br><%
538
588
  end
589
+ # FILTER PARAMETERS
539
590
  if @_brick_params&.present? %>
540
591
  <% if @_brick_params.length == 1 # %%% Does not yet work with composite keys
541
592
  k, id = @_brick_params.first
@@ -547,38 +598,62 @@ if (headerTop) {
547
598
  end
548
599
  end %>
549
600
  (<%= link_to 'See all #{model_plural.split('::').last}', #{path_obj_name.pluralize}_path %>)
601
+ <% end
602
+ # COLUMN EXCLUSIONS
603
+ if @_brick_excl&.present? %>
604
+ <div id=\"exclusions\">Excluded columns:
605
+ <% @_brick_excl.each do |excl| %>
606
+ <div class=\"colExclusion\"><%= excl %></div>
607
+ <% end %>
608
+ </div>
609
+ <script>
610
+ [... document.getElementsByClassName(\"colExclusion\")].forEach(function (excl) {
611
+ excl.addEventListener(\"click\", function () {
612
+ doFetch(\"POST\", {_brick_unexclude: this.innerHTML});
613
+ });
614
+ });
615
+ </script>
550
616
  <% end %>
551
- <br>
552
617
  <table id=\"headerTop\">
553
618
  <table id=\"#{table_name}\">
554
619
  <thead><tr>#{"<th x-order=\"#{pk.join(',')}\"></th>" if pk.present?}<%=
555
- col_order = []
556
- # Consider getting the name from the association -- h.first.name -- if a more \"friendly\" alias should be used for a screwy table name
557
- hms_hdrs = {#{hms_headers.map do |hm|
558
- "#{hm.first.name}: [#{hm.first.name.inspect}, #{(hm.first.options[:through] && !hm.first.through_reflection).inspect}, #{hm.first.klass.name}, #{hm[1].inspect}, #{hm[2].inspect}]"
559
- end.join(', ')}}
560
- (@#{table_name}.columns + hms_hdrs.values).each_with_object(+'') do |col, s|
561
- if col.is_a?(ActiveRecord::ConnectionAdapters::Column)
562
- next if (#{(pk || []).inspect}.include?(col_name = col.name) && col.type == :integer && !bts.key?(col_name)) ||
563
- ::Brick.config.metadata_columns.include?(col_name) || poly_cols.include?(col_name)
564
-
565
- col_order << col_name
620
+ # Consider getting the name from the association -- hm.first.name -- if a more \"friendly\" alias should be used for a screwy table name
621
+ cols = {#{hms_keys = []
622
+ hms_headers.map do |hm|
623
+ hms_keys << (assoc_name = (assoc = hm.first).name.to_s)
624
+ "#{assoc_name.inspect} => [#{(assoc.options[:through] && !assoc.through_reflection).inspect}, #{assoc.klass.name}, #{hm[1].inspect}, #{hm[2].inspect}]"
625
+ end.join(', ')}}
626
+ col_keys = @#{table_name}.columns.each_with_object([]) do |col, s|
627
+ col_name = col.name
628
+ next if @_brick_incl&.exclude?(col_name) ||
629
+ (#{(pk || []).inspect}.include?(col_name) && col.type == :integer && !bts.key?(col_name)) ||
630
+ ::Brick.config.metadata_columns.include?(col_name) || poly_cols.include?(col_name)
631
+
632
+ s << col_name
633
+ cols[col_name] = col
634
+ end
635
+ unless @_brick_sequence # If no sequence is defined, start with all inclusions
636
+ @_brick_sequence = col_keys + #{(hms_keys).inspect}.reject { |assoc_name| @_brick_incl&.exclude?(assoc_name) }
637
+ end
638
+ @_brick_sequence.reject! { |nm| @_brick_excl.include?(nm) } if @_brick_excl # Reject exclusions
639
+ @_brick_sequence.each_with_object(+'') do |col_name, s|
640
+ if (col = cols[col_name]).is_a?(ActiveRecord::ConnectionAdapters::Column)
641
+ s << '<th'
642
+ s << \" title=\\\"#\{col.comment}\\\"\" if col.respond_to?(:comment) && !col.comment.blank?
566
643
  s << if (bt = bts[col_name])
567
- \"<th#\{' x-order=\"' + bt.first.to_s + '\"' unless bt[2]}>BT \" + # Allow sorting any BT except polymorphics
644
+ # Allow sorting for any BT except polymorphics
645
+ \"#\{' x-order=\"' + bt.first.to_s + '\"' unless bt[2]}>BT \" +
568
646
  bt[1].map { |bt_pair| bt_pair.first.bt_link(bt.first) }.join(' ')
569
- else
570
- # Currently we always allow click to sort on non-BT columns
571
- \"<th#\{' x-order=\"' + col_name + '\"' if true}>#\{col_name}\"
572
- end + '</th>'
573
- else # Currently we always allow click to sort on all HM columns (col is the hm array)
574
- col_order << col.first # hm.name
575
- s << \"<th#\{' x-order=\"' + col.first.to_s + '\"' if true}>\"
576
- s << if col[1]
577
- \"#\{col[3]} #\{col[4]}\" # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
578
- else
579
- \"#\{col[3]} #\{link_to(col[4], send(\"#\{col[2].name.underscore.tr('/', '_').pluralize}_path\"))}\"
580
- end + '</th>'
647
+ else # Normal column
648
+ \"#\{' x-order=\"' + col_name + '\"' if true}>#\{col_name}\"
649
+ end
650
+ elsif col # HM column
651
+ s << \"<th#\{' x-order=\"' + col_name + '\"' if true}>#\{col[2]} \"
652
+ s << (col.first ? \"#\{col[3]}\" : \"#\{link_to(col[3], send(\"#\{col[1].name.underscore.tr('/', '_').pluralize}_path\"))}\")
653
+ else # Bad column name!
654
+ s << \"<th title=\\\"<< Unknown column >>\\\">#\{col_name}\"
581
655
  end
656
+ s << '</th>'
582
657
  end.html_safe
583
658
  %></tr></thead>
584
659
  <tbody>
@@ -586,16 +661,15 @@ if (headerTop) {
586
661
  hms_cols = {#{hms_columns.join(', ')}} %>
587
662
  <tr>#{"
588
663
  <td><%= link_to '⇛', #{path_obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
589
- <% col_order.each do |col_name|
664
+ <% @_brick_sequence.each do |col_name|
590
665
  val = #{obj_name}.attributes[col_name] %>
591
- <td><%
666
+ <td<%= ' class=\"dimmed\"'.html_safe unless cols.key?(col_name)%>><%
592
667
  if (bt = bts[col_name])
593
668
  if bt[2] # Polymorphic?
594
669
  bt_class = #{obj_name}.send(\"#\{bt.first\}_type\")
595
670
  base_class = (::Brick.existing_stis[bt_class] || bt_class).constantize.base_class.name.underscore
596
671
  poly_id = #{obj_name}.send(\"#\{bt.first\}_id\")
597
- %><%= link_to(\"#\{bt_class\} ##\{poly_id\}\",
598
- send(\"#\{base_class\}_path\".to_sym, poly_id)) if poly_id %><%
672
+ %><%= link_to(\"#\{bt_class\} ##\{poly_id\}\", send(\"#\{base_class\}_path\".to_sym, poly_id)) if poly_id %><%
599
673
  else
600
674
  bt_txt = (bt_class = bt[1].first.first).brick_descrip(
601
675
  # 0..62 because Postgres column names are limited to 63 characters
@@ -604,15 +678,14 @@ if (headerTop) {
604
678
  bt_txt ||= \"<span class=\\\"orphan\\\">&lt;&lt; Orphaned ID: #\{val} >></span>\".html_safe if val
605
679
  bt_id = bt_id_col.map { |id_col| #{obj_name}.send(id_col.to_sym) } %>
606
680
  <%= bt_id&.first ? link_to(bt_txt, send(\"#\{bt_class.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, bt_id)) : bt_txt %>
607
- <%#= Previously was: bt_obj = bt[1].first.first.find_by(bt[2] => val); link_to(bt_obj.brick_descrip, send(\"#\{bt[1].first.first.name.underscore\}_path\".to_sym, bt_obj.send(bt[1].first.first.primary_key.to_sym))) if bt_obj %>
608
- <% end %>
609
- <% elsif (hms_col = hms_cols[col_name])
681
+ <% end
682
+ elsif (hms_col = hms_cols[col_name])
610
683
  if hms_col.length == 1 %>
611
684
  <%= hms_col.first %>
612
685
  <% else
613
- klass = (col = hms_hdrs[col_name])[2]
614
- txt = if col[3] == 'HO'
615
- descrips = @_brick_bt_descrip[col_name][klass]
686
+ klass = (col = cols[col_name])[1]
687
+ txt = if col[2] == 'HO'
688
+ descrips = @_brick_bt_descrip[col_name.to_sym][klass]
616
689
  ho_txt = klass.brick_descrip(#{obj_name}, descrips[0..-2].map { |id| #{obj_name}.send(id.last[0..62]) }, (ho_id_col = descrips.last))
617
690
  ho_id = ho_id_col.map { |id_col| #{obj_name}.send(id_col.to_sym) }
618
691
  ho_id&.first ? link_to(ho_txt, send(\"#\{klass.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, ho_id)) : ho_txt
@@ -620,9 +693,11 @@ if (headerTop) {
620
693
  \"#\{hms_col[1] || 'View'\} #\{hms_col.first}\"
621
694
  end %>
622
695
  <%= link_to txt, send(\"#\{klass.name.underscore.tr('/', '_').pluralize}_path\".to_sym, hms_col[2]) unless hms_col[1]&.zero? %>
623
- <% end %>
624
- <% else
696
+ <% end
697
+ elsif cols.key?(col_name)
625
698
  %><%= hide_bcrypt(val) %><%
699
+ else # Bad column name!
700
+ %>?<%
626
701
  end
627
702
  %></td>
628
703
  <% end %>
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 52
8
+ TINY = 55
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
@@ -190,6 +190,15 @@ module Brick
190
190
  [bts, hms]
191
191
  end
192
192
 
193
+ def exclude_column(table, col)
194
+ puts "Excluding #{table}.#{col}"
195
+ true
196
+ end
197
+ def unexclude_column(table, col)
198
+ puts "Unexcluding #{table}.#{col}"
199
+ true
200
+ end
201
+
193
202
  # Switches Brick auto-models on or off, for all threads
194
203
  # @api public
195
204
  def enable_models=(value)
@@ -7,13 +7,13 @@ module Brick
7
7
  class InstallGenerator < ::Rails::Generators::Base
8
8
  # include ::Rails::Generators::Migration
9
9
 
10
- source_root File.expand_path('templates', __dir__)
11
- class_option(
12
- :with_changes,
13
- type: :boolean,
14
- default: false,
15
- desc: 'Store changeset (diff) with each version'
16
- )
10
+ # source_root File.expand_path('templates', __dir__)
11
+ # class_option(
12
+ # :with_changes,
13
+ # type: :boolean,
14
+ # default: false,
15
+ # desc: 'Store changeset (diff) with each version'
16
+ # )
17
17
 
18
18
  desc 'Generates an initializer file for configuring Brick'
19
19
 
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+ require 'fancy_gets'
6
+
7
+ module Brick
8
+ # Auto-generates migrations
9
+ class MigrationsGenerator < ::Rails::Generators::Base
10
+ include FancyGets
11
+ # include ::Rails::Generators::Migration
12
+
13
+ # SQL types that are the same as their migration data type name: text, integer, bigint, date, boolean, decimal, float
14
+ SQL_TYPES = { 'character varying' => 'string',
15
+ 'character' => 'string', # %%% Need to put in "limit: 1"
16
+ 'xml' => 'text',
17
+ 'bytea' => 'binary',
18
+ 'timestamp without time zone' => 'timestamp',
19
+ 'timestamp with time zone' => 'timestamp',
20
+ 'time without time zone' => 'time',
21
+ 'time with time zone' => 'time',
22
+ 'double precision' => 'float', # might work with 'double'
23
+ 'smallint' => 'integer' } # %%% Need to put in "limit: 2"
24
+ # (Still need to find what "inet" and "json" data types map to.)
25
+
26
+ # # source_root File.expand_path('templates', __dir__)
27
+ # class_option(
28
+ # :with_changes,
29
+ # type: :boolean,
30
+ # default: false,
31
+ # desc: 'Add IMPORT_TEMPLATE to model'
32
+ # )
33
+
34
+ desc 'Auto-generates migrations for an existing database.'
35
+
36
+ def brick_migrations
37
+ # If Apartment is active, see if a default schema to analyse is indicated
38
+
39
+ # # Load all models
40
+ # Rails.configuration.eager_load_namespaces.select { |ns| ns < Rails::Application }.each(&:eager_load!)
41
+
42
+ if (tables = ::Brick.relations.reject { |k, v| v.key?(:isView) && v[:isView] == true }.map(&:first).sort).empty?
43
+ puts "No tables found in database #{ActiveRecord::Base.connection.current_database}."
44
+ return
45
+ end
46
+
47
+ key_type = (ActiveRecord.version < ::Gem::Version.new('5.1') ? 'integer' : 'bigint')
48
+ is_4x_rails = ActiveRecord.version < ::Gem::Version.new('5.0')
49
+ ar_version = "[#{ActiveRecord.version.segments[0..1].join('.')}]" unless is_4x_rails
50
+ default_mig_path = (mig_path = ActiveRecord::Migrator.migrations_paths.first || "#{::Rails.root}/db/migrate")
51
+ if Dir.exist?(mig_path)
52
+ if Dir["#{mig_path}/**/*.rb"].present?
53
+ puts "WARNING: migrations folder #{mig_path} appears to already have ruby files present."
54
+ mig_path2 = "#{::Rails.root}/tmp/brick_migrations"
55
+ if Dir.exist?(mig_path2)
56
+ if Dir["#{mig_path2}/**/*.rb"].present?
57
+ puts "As well, temporary folder #{mig_path2} also has ruby files present."
58
+ puts "Choose a destination -- all existing .rb files will be removed:"
59
+ mig_path2 = gets_list(list: ['Cancel operation!', "Append migration files into #{mig_path} anyway", mig_path, mig_path2])
60
+ return if mig_path2.start_with?('Cancel')
61
+
62
+ if mig_path2.start_with?('Append migration files into ')
63
+ mig_path2 = mig_path
64
+ else
65
+ Dir["#{mig_path2}/**/*.rb"].each { |rb| File.delete(rb) }
66
+ end
67
+ else
68
+ puts "Using temporary folder #{mig_path2} for created migration files.\n\n"
69
+ end
70
+ else
71
+ puts "Creating the temporary folder #{mig_path2} for created migration files.\n\n"
72
+ Dir.mkdir(mig_path2)
73
+ end
74
+ mig_path = mig_path2
75
+ else
76
+ puts "Using standard migration folder #{mig_path} for created migration files.\n\n"
77
+ end
78
+ else
79
+ puts "Creating standard ActiveRecord migration folder #{mig_path} to hold new migration files.\n\n"
80
+ Dir.mkdir(mig_path)
81
+ end
82
+
83
+ # Generate a list of tables that can be chosen
84
+ chosen = gets_list(list: tables, chosen: tables.dup)
85
+ # Start the timestamps back the same number of minutes from now as expected number of migrations to create
86
+ current_mig_time = Time.now - chosen.length.minutes
87
+ done = []
88
+ fks = {}
89
+ stuck = {}
90
+ # Start by making migrations for fringe tables (those with no foreign keys).
91
+ # Continue layer by layer, creating migrations for tables that reference ones already done, until
92
+ # no more migrations can be created. (At that point hopefully all tables are accounted for.)
93
+ while (fringe = chosen.reject do |tbl|
94
+ snags = ::Brick.relations.fetch(tbl, nil)&.fetch(:fks, nil)&.select do |_k, v|
95
+ v[:is_bt] && !v[:polymorphic] &&
96
+ tbl != v[:inverse_table] && # Ignore self-referencing associations (stuff like "parent_id")
97
+ !done.include?(v[:inverse_table])
98
+ end
99
+ stuck[tbl] = snags if snags&.present?
100
+ end).present?
101
+ fringe.each do |tbl|
102
+ next unless (relation = ::Brick.relations.fetch(tbl, nil))&.fetch(:cols, nil)&.present?
103
+
104
+ pkey_cols = (rpk = relation[:pkey].values.flatten) & (arpk = [ActiveRecord::Base.primary_key].flatten)
105
+ # In case things aren't as standard
106
+ if pkey_cols.empty?
107
+ pkey_cols = if rpk.empty? && relation[:cols][arpk.first]&.first == key_type
108
+ arpk
109
+ elsif rpk.first
110
+ rpk
111
+ end
112
+ end
113
+ schema = if (tbl_parts = tbl.split('.')).length > 1
114
+ if tbl_parts.first == 'public'
115
+ tbl_parts.shift
116
+ nil
117
+ else
118
+ tbl_parts.first
119
+ end
120
+ end
121
+ # %%% For the moment we're skipping polymorphics
122
+ fkey_cols = relation[:fks].values.select { |assoc| assoc[:is_bt] && !assoc[:polymorphic] }
123
+ mig = +"class Create#{(full_table_name = tbl_parts.join('_')).camelize} < ActiveRecord::Migration#{ar_version}\n"
124
+ # Support missing primary key (by adding: ,id: false)
125
+ # also integer / uuid / other non-standard data types for primary key
126
+ id_option = unless (pkey_col_first = relation[:cols][pkey_cols&.first]&.first) == key_type
127
+ unless pkey_cols&.present?
128
+ ', id: false'
129
+ else
130
+ case pkey_col_first
131
+ when 'integer'
132
+ ', id: :serial'
133
+ when 'bigint'
134
+ ', id: :bigserial'
135
+ else
136
+ ", id: :#{SQL_TYPES[pkey_col_first] || pkey_col_first}" # Something like: id: :integer, primary_key: :businessentityid
137
+ end + (pkey_cols.first ? ", primary_key: :#{pkey_cols.first}" : '')
138
+ end
139
+ end
140
+ # Refer to this table name as a symbol or dotted string as appropriate
141
+ tbl = tbl_parts.length == 1 ? ":#{tbl_parts.first}" : "'#{tbl}'"
142
+ mig << " def change\n return unless reverting? || !table_exists?(#{tbl})\n\n"
143
+ mig << " create_schema :#{schema} unless schema_exists?(:#{schema})\n" if schema
144
+ mig << " create_table #{tbl}#{id_option} do |t|\n"
145
+ possible_ts = [] # Track possible generic timestamps
146
+ add_fks = [] # Track foreign keys to add after table creation
147
+ relation[:cols].each do |col, col_type|
148
+ next if !id_option&.end_with?('id: false') && pkey_cols.include?(col)
149
+
150
+ # See if there are generic timestamps
151
+ if (sql_type = SQL_TYPES[col_type.first]) == 'timestamp' &&
152
+ ['created_at','updated_at'].include?(col)
153
+ possible_ts << [col, !col_type[3]]
154
+ next
155
+ end
156
+
157
+ sql_type ||= col_type.first
158
+ suffix = col_type[3] ? +', null: false' : +''
159
+ # Determine if this column is used as part of a foreign key
160
+ if fk = fkey_cols.find { |assoc| col == assoc[:fk] }
161
+ to_table = fk[:inverse_table].split('.')
162
+ to_table = to_table.length == 1 ? ":#{to_table.first}" : "'#{fk[:inverse_table]}'"
163
+ if fk[:fk] != "#{fk[:assoc_name].singularize}_id" # Need to do our own foreign_key tricks, not use references?
164
+ column = fk[:fk]
165
+ mig << " t.#{sql_type} :#{column}#{suffix}\n"
166
+ add_fks << [to_table, column, ::Brick.relations[fk[:inverse_table]]]
167
+ else
168
+ suffix << ", type: :#{sql_type}" unless sql_type == key_type
169
+ mig << " t.references :#{fk[:assoc_name]}#{suffix}, foreign_key: { to_table: #{to_table} }\n"
170
+ end
171
+ else
172
+ mig << emit_column(sql_type, col, suffix)
173
+ end
174
+ end
175
+ if possible_ts.length == 2 && # Both created_at and updated_at
176
+ # Rails 5 and later timestamps default to NOT NULL
177
+ (possible_ts.first.last == is_4x_rails && possible_ts.last.last == is_4x_rails)
178
+ mig << "\n t.timestamps\n"
179
+ else # Just one or the other, or a nullability mismatch
180
+ possible_ts.each { |ts| emit_column('timestamp', ts.first, nil) }
181
+ end
182
+ mig << " end\n"
183
+ add_fks.each do |add_fk|
184
+ is_commented = false
185
+ # add_fk[2] holds the inverse relation
186
+ unless (pk = add_fk[2][:pkey].values.flatten&.first)
187
+ is_commented = true
188
+ mig << " # (Unable to create relationship because primary key is missing on table #{add_fk[0]})\n"
189
+ # No official PK, but if coincidentally there's a column of the same name, take a chance on it
190
+ pk = (add_fk[2][:cols].key?(add_fk[1]) && add_fk[1]) || '???'
191
+ end
192
+ # to_table column
193
+ mig << " #{'# ' if is_commented}add_foreign_key #{tbl}, #{add_fk[0]}, column: :#{add_fk[1]}, primary_key: :#{pk}\n"
194
+ end
195
+ mig << " end\nend\n"
196
+ current_mig_time += 1.minute
197
+ File.open("#{mig_path}/#{current_mig_time.strftime('%Y%m%d%H%M00')}_create_#{full_table_name}.rb", "w") { |f| f.write mig }
198
+ end
199
+ done.concat(fringe)
200
+ chosen -= done
201
+ end
202
+ stuck_counts = Hash.new { |h, k| h[k] = 0 }
203
+ chosen.each do |leftover|
204
+ puts "Can't do #{leftover} because:\n #{stuck[leftover].map do |snag|
205
+ stuck_counts[snag.last[:inverse_table]] += 1
206
+ snag.last[:assoc_name]
207
+ end.join(', ')}"
208
+ end
209
+ if mig_path.start_with?(cur_path = ::Rails.root.to_s)
210
+ pretty_mig_path = mig_path[cur_path.length..-1]
211
+ end
212
+ puts "*** Created #{done.length} migration files under #{pretty_mig_path || mig_path} ***"
213
+ if (stuck_sorted = stuck_counts.to_a.sort { |a, b| b.last <=> a.last }).length.positive?
214
+ puts "-----------------------------------------"
215
+ puts "Unable to create migrations for #{stuck_sorted.length} tables#{
216
+ ". Here's the top 5 blockers" if stuck_sorted.length > 5
217
+ }:"
218
+ pp stuck_sorted[0..4]
219
+ end
220
+ end
221
+
222
+ private
223
+
224
+ def emit_column(type, name, suffix)
225
+ " t.#{type.start_with?('numeric') ? 'decimal' : type} :#{name}#{suffix}\n"
226
+ end
227
+ end
228
+ end
@@ -59,7 +59,6 @@ module Brick
59
59
  end
60
60
  end
61
61
  models.each do |m| # Find longest name in the list for future use to show lists on the right side of the screen
62
- # Strangely this can't be inlined since it assigns to "len"
63
62
  if longest_length < (len = m.name.length)
64
63
  longest_length = len
65
64
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brick
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.52
4
+ version: 1.0.55
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorin Thwaits
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-04 00:00:00.000000000 Z
11
+ date: 2022-08-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -238,6 +238,7 @@ files:
238
238
  - lib/brick/version_number.rb
239
239
  - lib/generators/brick/USAGE
240
240
  - lib/generators/brick/install_generator.rb
241
+ - lib/generators/brick/migrations_generator.rb
241
242
  - lib/generators/brick/model_generator.rb
242
243
  - lib/generators/brick/templates/add_object_changes_to_versions.rb.erb
243
244
  - lib/generators/brick/templates/create_versions.rb.erb