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 +4 -4
- data/lib/brick/extensions.rb +32 -16
- data/lib/brick/frameworks/rails/engine.rb +136 -61
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +9 -0
- data/lib/generators/brick/install_generator.rb +7 -7
- data/lib/generators/brick/migrations_generator.rb +228 -0
- data/lib/generators/brick/model_generator.rb +0 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 430a2dfa5ef0caee9b99bc341ccd42a9d29e8f1d54071284aa2469e1baa0c659
|
4
|
+
data.tar.gz: b36e18de169bd9032e6c383548ec30cce06eeb97a35e8c3e4f4da935aff8f603
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c51491575b9ee56c3789619bfc74a2d9c0525750687224e94e120cbae42fad8db1abda9d82978115d9266c55fa0c6f914ce273a3613915657d8a0cf4e7fe6aeb
|
7
|
+
data.tar.gz: a0646cfdc9a2c844fd1149968d0175346c48835c6245d7fe92d0126d60708cf76514704e29dc3f506d2e06e546c8c995bf7103d6ef42be481bb931b4d2bfa73f
|
data/lib/brick/extensions.rb
CHANGED
@@ -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
|
-
|
1160
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
-
|
202
|
+
tr th {
|
199
203
|
background-color: #009879;
|
200
204
|
color: #fff;
|
201
205
|
text-align: left;
|
202
206
|
}
|
203
|
-
#headerTop
|
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
|
-
|
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
|
-
|
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
|
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:
|
461
|
-
headers: {
|
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
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
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
|
-
|
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
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
s <<
|
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
|
-
<%
|
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\\\"><< 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
|
-
|
608
|
-
|
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 =
|
614
|
-
txt = if col[
|
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
|
-
|
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 %>
|
data/lib/brick/version_number.rb
CHANGED
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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.
|
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-
|
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
|