brick 1.0.40 → 1.0.43
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/config.rb +5 -0
- data/lib/brick/extensions.rb +85 -5
- data/lib/brick/frameworks/rails/engine.rb +193 -52
- data/lib/brick/tasks/orphans.rake +15 -43
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +7 -1
- data/lib/generators/brick/install_generator.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8387796a98b0bcf24586c6b31f091cfdc4cca1b45b73d02de3bdba17430625f7
|
4
|
+
data.tar.gz: 1c7509e21a1a9a6c8c52f3015b0a7e34222ff944433e028be314f8525ee948b8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 75222971d2407d8ffb3b8412694da3fe6ad0d3f27f147159f6d75aa712d1dfe8a9d54627cb2c816fdfc8cb7c44de4407f180fbc559b9c811719c22a016c913f5
|
7
|
+
data.tar.gz: 44e03a9d2ddec85969d5b1aca67a9426a4b3dd33f196c058d57b380fa77e78424768dd3e97a40227b2372bec4ccd81dfd1b7760b65e2c5b292c8b47c0d27e2c7
|
data/lib/brick/config.rb
CHANGED
data/lib/brick/extensions.rb
CHANGED
@@ -415,14 +415,16 @@ module ActiveRecord
|
|
415
415
|
hm_counts.each do |k, hm|
|
416
416
|
associative = nil
|
417
417
|
count_column = if hm.options[:through]
|
418
|
-
fk_col = (associative = associatives[hm.name])
|
419
|
-
hm.foreign_key
|
418
|
+
fk_col = (associative = associatives[hm.name])&.foreign_key
|
419
|
+
hm.foreign_key if fk_col
|
420
420
|
else
|
421
421
|
fk_col = hm.foreign_key
|
422
422
|
poly_type = hm.inverse_of.foreign_type if hm.options.key?(:as)
|
423
423
|
pk = hm.klass.primary_key
|
424
424
|
(pk.is_a?(Array) ? pk.first : pk) || '*'
|
425
425
|
end
|
426
|
+
next unless count_column # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
|
427
|
+
|
426
428
|
tbl_alias = "_br_#{hm.name}"
|
427
429
|
pri_tbl = hm.active_record
|
428
430
|
on_clause = []
|
@@ -565,7 +567,9 @@ Module.class_exec do
|
|
565
567
|
full_class_name = +''
|
566
568
|
full_class_name << "::#{self.name}" unless self == Object
|
567
569
|
full_class_name << "::#{plural_class_name.underscore.singularize.camelize}"
|
568
|
-
if (plural_class_name == 'BrickSwagger' ||
|
570
|
+
if (plural_class_name == 'BrickSwagger' ||
|
571
|
+
(::Brick.config.add_orphans && plural_class_name == 'BrickGem') ||
|
572
|
+
model = self.const_get(full_class_name))
|
569
573
|
# if it's a controller and no match or a model doesn't really use the same table name, eager load all models and try to find a model class of the right name.
|
570
574
|
Object.send(:build_controller, self, class_name, plural_class_name, model, relations)
|
571
575
|
end
|
@@ -897,6 +901,14 @@ class Object
|
|
897
901
|
built_controller = Class.new(ActionController::Base) do |new_controller_class|
|
898
902
|
(namespace || Object).const_set(class_name.to_sym, new_controller_class)
|
899
903
|
|
904
|
+
# Brick-specific pages
|
905
|
+
if plural_class_name == 'BrickGem'
|
906
|
+
self.define_method :orphans do
|
907
|
+
instance_variable_set(:@orphans, ::Brick.find_orphans(::Brick.set_db_schema(params)))
|
908
|
+
end
|
909
|
+
return [new_controller_class, code + ' # BrickGem controller']
|
910
|
+
end
|
911
|
+
|
900
912
|
unless (is_swagger = plural_class_name == 'BrickSwagger') # && request.format == :json)
|
901
913
|
code << " def index\n"
|
902
914
|
code << " @#{table_name} = #{model.name}#{pk&.present? ? ".order(#{pk.inspect})" : '.all'}\n"
|
@@ -1047,10 +1059,11 @@ class Object
|
|
1047
1059
|
if is_need_params
|
1048
1060
|
code << "private\n"
|
1049
1061
|
code << " def #{params_name}\n"
|
1050
|
-
code << " params.require(:#{
|
1062
|
+
code << " params.require(:#{require_name = model.name.underscore.tr('/', '_')
|
1063
|
+
}).permit(#{model.columns_hash.keys.map { |c| c.to_sym.inspect }.join(', ')})\n"
|
1051
1064
|
code << " end\n"
|
1052
1065
|
self.define_method(params_name) do
|
1053
|
-
params.require(
|
1066
|
+
params.require(require_name.to_sym).permit(model.columns_hash.keys)
|
1054
1067
|
end
|
1055
1068
|
private params_name
|
1056
1069
|
# Get column names for params from relations[model.table_name][:cols].keys
|
@@ -1475,5 +1488,72 @@ module Brick
|
|
1475
1488
|
end
|
1476
1489
|
assoc_bt[:inverse] = assoc_hm
|
1477
1490
|
end
|
1491
|
+
|
1492
|
+
# Locate orphaned records
|
1493
|
+
def find_orphans(multi_schema)
|
1494
|
+
is_default_schema = multi_schema&.==(Apartment.default_schema)
|
1495
|
+
relations.each_with_object([]) do |v, s|
|
1496
|
+
frn_tbl = v.first
|
1497
|
+
next if (relation = v.last).key?(:isView) || config.exclude_tables.include?(frn_tbl) ||
|
1498
|
+
!(for_pk = (relation[:pkey].values.first&.first))
|
1499
|
+
|
1500
|
+
is_default_frn_schema = !is_default_schema && multi_schema &&
|
1501
|
+
((frn_parts = frn_tbl.split('.')).length > 1 && frn_parts.first)&.==(Apartment.default_schema)
|
1502
|
+
relation[:fks].select { |_k, assoc| assoc[:is_bt] }.each do |_k, bt|
|
1503
|
+
begin
|
1504
|
+
if bt.key?(:polymorphic)
|
1505
|
+
pri_pk = for_pk
|
1506
|
+
pri_tables = Brick.config.polymorphics["#{frn_tbl}.#{bt[:fk]}"]
|
1507
|
+
.each_with_object(Hash.new { |h, k| h[k] = [] }) do |pri_class, s|
|
1508
|
+
s[Object.const_get(pri_class).table_name] << pri_class
|
1509
|
+
end
|
1510
|
+
fk_id_col = "#{bt[:fk]}_id"
|
1511
|
+
fk_type_col = "#{bt[:fk]}_type"
|
1512
|
+
selects = []
|
1513
|
+
pri_tables.each do |pri_tbl, pri_types|
|
1514
|
+
# Skip if database is multitenant, we're not focused on "public", and the foreign and primary tables
|
1515
|
+
# are both in the "public" schema
|
1516
|
+
next if is_default_frn_schema &&
|
1517
|
+
((pri_parts = pri_tbl&.split('.'))&.length > 1 && pri_parts.first)&.==(Apartment.default_schema)
|
1518
|
+
|
1519
|
+
selects << "SELECT '#{pri_tbl}' AS pri_tbl, frn.#{fk_type_col} AS pri_type, frn.#{fk_id_col} AS pri_id, frn.#{for_pk} AS frn_id
|
1520
|
+
FROM #{frn_tbl} AS frn
|
1521
|
+
LEFT OUTER JOIN #{pri_tbl} AS pri ON pri.#{pri_pk} = frn.#{fk_id_col}
|
1522
|
+
WHERE frn.#{fk_type_col} IN (#{
|
1523
|
+
pri_types.map { |pri_type| "'#{pri_type}'" }.join(', ')
|
1524
|
+
}) AND frn.#{bt[:fk]}_id IS NOT NULL AND pri.#{pri_pk} IS NULL\n"
|
1525
|
+
end
|
1526
|
+
ActiveRecord::Base.execute_sql(selects.join("UNION ALL\n")).each do |o|
|
1527
|
+
entry = [frn_tbl, o['frn_id'], o['pri_type'], o['pri_id'], fk_id_col]
|
1528
|
+
entry << o['pri_tbl'] if (pri_class = Object.const_get(o['pri_type'])) != pri_class.base_class
|
1529
|
+
s << entry
|
1530
|
+
end
|
1531
|
+
else
|
1532
|
+
# Skip if database is multitenant, we're not focused on "public", and the foreign and primary tables
|
1533
|
+
# are both in the "public" schema
|
1534
|
+
pri_tbl = bt.key?(:inverse_table) && bt[:inverse_table]
|
1535
|
+
next if is_default_frn_schema &&
|
1536
|
+
((pri_parts = pri_tbl&.split('.'))&.length > 1 && pri_parts.first)&.==(Apartment.default_schema)
|
1537
|
+
|
1538
|
+
pri_pk = relations[pri_tbl].fetch(:pkey, nil)&.values&.first&.first ||
|
1539
|
+
_class_pk(pri_tbl, multi_schema)
|
1540
|
+
ActiveRecord::Base.execute_sql(
|
1541
|
+
"SELECT frn.#{bt[:fk]} AS pri_id, frn.#{for_pk} AS frn_id
|
1542
|
+
FROM #{frn_tbl} AS frn
|
1543
|
+
LEFT OUTER JOIN #{pri_tbl} AS pri ON pri.#{pri_pk} = frn.#{bt[:fk]}
|
1544
|
+
WHERE frn.#{bt[:fk]} IS NOT NULL AND pri.#{pri_pk} IS NULL
|
1545
|
+
ORDER BY 1, 2"
|
1546
|
+
).each { |o| s << [frn_tbl, o['frn_id'], pri_tbl, o['pri_id'], bt[:fk]] }
|
1547
|
+
end
|
1548
|
+
rescue StandardError => err
|
1549
|
+
puts "Strange -- #{err.inspect}"
|
1550
|
+
end
|
1551
|
+
end
|
1552
|
+
end
|
1553
|
+
end
|
1554
|
+
|
1555
|
+
def _class_pk(dotted_name, multitenant)
|
1556
|
+
Object.const_get((multitenant ? [dotted_name.split('.').last] : dotted_name.split('.')).map { |nm| "::#{nm.singularize.camelize}" }.join).primary_key
|
1557
|
+
end
|
1478
1558
|
end
|
1479
1559
|
end
|
@@ -53,7 +53,9 @@ module Brick
|
|
53
53
|
# Used by Rails 5.0 and above
|
54
54
|
alias :_brick_template_exists? :template_exists?
|
55
55
|
def template_exists?(*args, **options)
|
56
|
-
|
56
|
+
(::Brick.config.add_orphans && args.first == 'orphans') ||
|
57
|
+
_brick_template_exists?(*args, **options) ||
|
58
|
+
set_brick_model(args)
|
57
59
|
end
|
58
60
|
|
59
61
|
def set_brick_model(find_args)
|
@@ -88,44 +90,56 @@ module Brick
|
|
88
90
|
unless (model_name = (
|
89
91
|
@_brick_model ||
|
90
92
|
(ActionView.version < ::Gem::Version.new('5.0') && args[1].is_a?(Array) ? set_brick_model(args) : nil)
|
91
|
-
)&.name)
|
93
|
+
)&.name) ||
|
94
|
+
(is_orphans = ::Brick.config.add_orphans && args[0..1] == ['orphans', ['brick_gem']])
|
92
95
|
return _brick_find_template(*args, **options)
|
93
96
|
end
|
94
97
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
98
|
+
unless is_orphans
|
99
|
+
pk = @_brick_model._brick_primary_key(::Brick.relations.fetch(model_name, nil))
|
100
|
+
obj_name = model_name.split('::').last.underscore
|
101
|
+
path_obj_name = model_name.underscore.tr('/', '_')
|
102
|
+
table_name = obj_name.pluralize
|
103
|
+
template_link = nil
|
104
|
+
bts, hms, associatives = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
|
105
|
+
hms_columns = [] # Used for 'index'
|
106
|
+
skip_klass_hms = ::Brick.config.skip_index_hms[model_name] || {}
|
107
|
+
hms_headers = hms.each_with_object([]) do |hm, s|
|
108
|
+
hm_stuff = [(hm_assoc = hm.last), "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]}", (assoc_name = hm.first)]
|
109
|
+
hm_fk_name = if hm_assoc.options[:through]
|
110
|
+
associative = associatives[hm_assoc.name]
|
111
|
+
associative && "'#{associative.name}.#{associative.foreign_key}'"
|
112
|
+
else
|
113
|
+
hm_assoc.foreign_key
|
114
|
+
end
|
115
|
+
case args.first
|
116
|
+
when 'index'
|
117
|
+
hms_columns << if hm_assoc.macro == :has_many
|
118
|
+
set_ct = if skip_klass_hms.key?(assoc_name.to_sym)
|
119
|
+
'nil'
|
120
|
+
else
|
121
|
+
# Postgres column names are limited to 63 characters
|
122
|
+
attrib_name = "_br_#{assoc_name}_ct"[0..62]
|
123
|
+
"#{obj_name}.#{attrib_name} || 0"
|
124
|
+
end
|
125
|
+
if hm_fk_name
|
120
126
|
"<%= ct = #{set_ct}
|
121
127
|
link_to \"#\{ct || 'View'\} #{assoc_name}\", #{hm_assoc.klass.name.underscore.tr('/', '_').pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, obj_name, pk)} }) unless ct&.zero? %>\n"
|
122
|
-
|
128
|
+
else # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
|
129
|
+
"#{assoc_name}\n"
|
130
|
+
end
|
131
|
+
else # has_one
|
123
132
|
"<%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>\n"
|
124
|
-
|
125
|
-
|
126
|
-
|
133
|
+
end
|
134
|
+
when 'show', 'update'
|
135
|
+
hm_stuff << if hm_fk_name
|
136
|
+
"<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.tr('/', '_').pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, "@#{obj_name}", pk)} }) %>\n"
|
137
|
+
else # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
|
138
|
+
assoc_name
|
139
|
+
end
|
140
|
+
end
|
141
|
+
s << hm_stuff
|
127
142
|
end
|
128
|
-
s << hm_stuff
|
129
143
|
end
|
130
144
|
|
131
145
|
schema_options = ::Brick.db_schemas.keys.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
|
@@ -140,6 +154,7 @@ module Brick
|
|
140
154
|
end.sort.each_with_object(+'') do |v, s|
|
141
155
|
s << "<option value=\"#{v.underscore.gsub('.', '/').pluralize}\">#{v}</option>"
|
142
156
|
end.html_safe
|
157
|
+
table_options << '<option value="brick_orphans">(Orphans)</option>'.html_safe if is_orphans
|
143
158
|
css = +"<style>
|
144
159
|
#dropper {
|
145
160
|
background-color: #eee;
|
@@ -199,6 +214,9 @@ table tbody tr.active-row {
|
|
199
214
|
color: #009879;
|
200
215
|
}
|
201
216
|
|
217
|
+
td.val {
|
218
|
+
display: block;
|
219
|
+
}
|
202
220
|
a.show-arrow {
|
203
221
|
font-size: 1.5em;
|
204
222
|
text-decoration: none;
|
@@ -212,11 +230,27 @@ a.big-arrow {
|
|
212
230
|
overflow: hidden;
|
213
231
|
}
|
214
232
|
.wide-input input[type=text] {
|
215
|
-
|
233
|
+
display: inline-block;
|
234
|
+
width: 90%;
|
216
235
|
}
|
217
236
|
.dimmed {
|
218
237
|
background-color: #C0C0C0;
|
219
238
|
}
|
239
|
+
|
240
|
+
#revertTemplate {
|
241
|
+
display: none;
|
242
|
+
}
|
243
|
+
svg.revert {
|
244
|
+
display: none;
|
245
|
+
margin-left: 0.25em;
|
246
|
+
}
|
247
|
+
.wide-input > svg.revert {
|
248
|
+
float: right;
|
249
|
+
}
|
250
|
+
input+svg.revert {
|
251
|
+
top: 0.5em;
|
252
|
+
}
|
253
|
+
|
220
254
|
input[type=submit] {
|
221
255
|
background-color: #004998;
|
222
256
|
color: #FFF;
|
@@ -225,7 +259,9 @@ input[type=submit] {
|
|
225
259
|
text-align: right;
|
226
260
|
}
|
227
261
|
</style>
|
228
|
-
<%
|
262
|
+
<% is_includes_dates = nil
|
263
|
+
|
264
|
+
def is_bcrypt?(val)
|
229
265
|
val.is_a?(String) && val.length == 60 && val.start_with?('$2a$')
|
230
266
|
end
|
231
267
|
def hide_bcrypt(val, max_len = 200)
|
@@ -297,7 +333,13 @@ window.addEventListener(\"pageshow\", function() {
|
|
297
333
|
});
|
298
334
|
|
299
335
|
if (tblSelect) { // Always present
|
300
|
-
|
336
|
+
var i = schemaSelect ? 1 : 0,
|
337
|
+
changeoutList = changeout(location.href);
|
338
|
+
for (; i < changeoutList.length; ++i) {
|
339
|
+
tblSelect.value = changeoutList[i];
|
340
|
+
if (tblSelect.value !== \"\") break;
|
341
|
+
}
|
342
|
+
|
301
343
|
tblSelect.addEventListener(\"change\", function () {
|
302
344
|
var lhr = changeout(location.href, null, this.value);
|
303
345
|
if (brickSchema)
|
@@ -314,13 +356,13 @@ function changeout(href, param, value, trimAfter) {
|
|
314
356
|
var pathParts = hrefParts[hrefParts.length - 1].split(\"/\");
|
315
357
|
if (value === undefined)
|
316
358
|
// A couple possibilities if it's namespaced, starting with two parts in the path -- and then try just one
|
317
|
-
return [pathParts.slice(1, 3).join('/'), pathParts.slice(1, 2)];
|
359
|
+
return [pathParts.slice(1, 3).join('/'), pathParts.slice(1, 2)[0]];
|
318
360
|
else
|
319
361
|
return hrefParts[0] + \"://\" + pathParts[0] + \"/\" + value;
|
320
362
|
}
|
321
363
|
if (trimAfter) {
|
322
364
|
var pathParts = hrefParts[0].split(\"/\");
|
323
|
-
while (pathParts.lastIndexOf(trimAfter)
|
365
|
+
while (pathParts.lastIndexOf(trimAfter) !== pathParts.length - 1) pathParts.pop();
|
324
366
|
hrefParts[0] = pathParts.join(\"/\");
|
325
367
|
}
|
326
368
|
var params = hrefParts.length > 1 ? hrefParts[1].split(\"&\") : [];
|
@@ -464,7 +506,7 @@ if (headerTop) {
|
|
464
506
|
end
|
465
507
|
# %%% Instead of our current "for Janet Leverling (Employee)" kind of link we previously had this code that did a "where x = 123" thing:
|
466
508
|
# (where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %>)
|
467
|
-
"#{css}
|
509
|
+
+"#{css}
|
468
510
|
<p style=\"color: green\"><%= notice %></p>#{"
|
469
511
|
<select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
|
470
512
|
<select id=\"tbl\">#{table_options}</select>
|
@@ -490,8 +532,7 @@ if (headerTop) {
|
|
490
532
|
<thead><tr>#{'<th></th>' if pk.present?}<%
|
491
533
|
col_order = []
|
492
534
|
@#{table_name}.columns.each do |col|
|
493
|
-
col_name = col.name
|
494
|
-
next if (#{(pk || []).inspect}.include?(col_name) && col.type == :integer && !bts.key?(col_name)) ||
|
535
|
+
next if (#{(pk || []).inspect}.include?(col_name = col.name) && col.type == :integer && !bts.key?(col_name)) ||
|
495
536
|
::Brick.config.metadata_columns.include?(col_name) || poly_cols.include?(col_name)
|
496
537
|
|
497
538
|
col_order << col_name
|
@@ -504,10 +545,16 @@ if (headerTop) {
|
|
504
545
|
else %><%=
|
505
546
|
col_name %><%
|
506
547
|
end
|
507
|
-
|
548
|
+
%></th><%
|
508
549
|
end
|
509
550
|
# Consider getting the name from the association -- h.first.name -- if a more \"friendly\" alias should be used for a screwy table name
|
510
|
-
%>#{hms_headers.map
|
551
|
+
%>#{hms_headers.map do |h|
|
552
|
+
if h.first.options[:through] && !h.first.through_reflection
|
553
|
+
"<th>#{h[1]} #{h[2]} %></th>" # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
|
554
|
+
else
|
555
|
+
"<th>#{h[1]} <%= link_to('#{h[2]}', #{h.first.klass.name.underscore.tr('/', '_').pluralize}_path) %></th>"
|
556
|
+
end
|
557
|
+
end.join
|
511
558
|
}</tr></thead>
|
512
559
|
|
513
560
|
<tbody>
|
@@ -547,8 +594,31 @@ if (headerTop) {
|
|
547
594
|
|
548
595
|
#{"<hr><%= link_to \"New #{obj_name}\", new_#{path_obj_name}_path %>" unless @_brick_model.is_view?}
|
549
596
|
#{script}"
|
597
|
+
when 'orphans'
|
598
|
+
if is_orphans
|
599
|
+
+"#{css}
|
600
|
+
<p style=\"color: green\"><%= notice %></p>#{"
|
601
|
+
<select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
|
602
|
+
<select id=\"tbl\">#{table_options}</select>
|
603
|
+
<h1>Orphans<%= \" for #\{}\" if false %></h1>
|
604
|
+
<% @orphans.each do |o|
|
605
|
+
via = \" (via #\{o[4]})\" unless \"#\{o[2].split('.').last.underscore.singularize}_id\" == o[4] %>
|
606
|
+
<a href=\"/<%= o[0].split('.').last %>/<%= o[1] %>\">
|
607
|
+
<%= \"#\{o[0]} #\{o[1]} refers#\{via} to non-existent #\{o[2]} #\{o[3]}#\{\" (in table \\\"#\{o[5]}\\\")\" if o[5]}\" %>
|
608
|
+
</a><br>
|
609
|
+
<% end %>
|
610
|
+
#{script}"
|
611
|
+
end
|
612
|
+
|
550
613
|
when 'show', 'update'
|
551
|
-
"#{css}
|
614
|
+
+"#{css}
|
615
|
+
|
616
|
+
<svg id=\"revertTemplate\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"
|
617
|
+
width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" xml:space=\"preserve\">
|
618
|
+
<path id=\"revertPath\" fill=\"#2020A0\" d=\"M271.844,119.641c-78.531,0-148.031,37.875-191.813,96.188l-80.172-80.188v256h256l-87.094-87.094
|
619
|
+
c23.141-70.188,89.141-120.906,167.063-120.906c97.25,0,176,78.813,176,176C511.828,227.078,404.391,119.641,271.844,119.641z\" />
|
620
|
+
</svg>
|
621
|
+
|
552
622
|
<p style=\"color: green\"><%= notice %></p>#{"
|
553
623
|
<select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
|
554
624
|
<select id=\"tbl\">#{table_options}</select>
|
@@ -606,31 +676,39 @@ end
|
|
606
676
|
<%= k %>
|
607
677
|
<% end %>
|
608
678
|
</th>
|
609
|
-
<td>
|
610
|
-
<%
|
679
|
+
<td class=\"val\">
|
680
|
+
<% dt_pickers = { datetime: 'datetimepicker', timestamp: 'datetimepicker', time: 'timepicker', date: 'datepicker' }
|
681
|
+
if bt
|
611
682
|
html_options = { prompt: \"Select #\{bt_name\}\" }
|
612
683
|
html_options[:class] = 'dimmed' unless val %>
|
613
684
|
<%= f.select k.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
|
614
685
|
<%= if (bt_obj = bt_class&.find_by(bt_pair[1] => val))
|
615
686
|
link_to('⇛', send(\"#\{bt_class.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, bt_obj.send(bt_class.primary_key.to_sym)), { class: 'show-arrow' })
|
616
687
|
elsif val
|
617
|
-
\"Orphaned ID: #\{val}
|
618
|
-
end
|
619
|
-
<% else case #{model_name}.column_for_attribute(k).type
|
688
|
+
\"<span>Orphaned ID: #\{val}</span>\".html_safe
|
689
|
+
end %><svg class=\"revert\" width=\"1.5em\" viewBox=\"0 0 512 512\"><use xlink:href=\"#revertPath\" /></svg>
|
690
|
+
<% else case (col_type = #{model_name}.column_for_attribute(k).type)
|
620
691
|
when :string, :text %>
|
621
692
|
<% if is_bcrypt?(val) # || .readonly? %>
|
622
693
|
<%= hide_bcrypt(val, 1000) %>
|
623
694
|
<% else %>
|
624
|
-
<div class=\"wide-input\"><%= f.text_field k.to_sym
|
695
|
+
<div class=\"wide-input\"><%= f.text_field k.to_sym %><svg class=\"revert\" width=\"1.5em\" viewBox=\"0 0 512 512\"><use xlink:href=\"#revertPath\" /></svg></div>
|
625
696
|
<% end %>
|
626
697
|
<% when :boolean %>
|
627
|
-
<%= f.check_box k.to_sym
|
628
|
-
<% when :integer, :decimal, :float
|
698
|
+
<%= f.check_box k.to_sym %><svg class=\"revert\" width=\"1.5em\" viewBox=\"0 0 512 512\"><use xlink:href=\"#revertPath\" /></svg>
|
699
|
+
<% when :integer, :decimal, :float
|
629
700
|
# What happens when keys are UUID?
|
630
701
|
# Postgres naturally uses the +uuid_generate_v4()+ function from the uuid-ossp extension
|
631
702
|
# If it's not yet enabled then: enable_extension 'uuid-ossp'
|
632
703
|
# ActiveUUID gem created a new :uuid type %>
|
633
|
-
<%=
|
704
|
+
<%= if col_type == :integer
|
705
|
+
f.text_field k.to_sym, { pattern: '\\d*', class: 'check-validity' }
|
706
|
+
else
|
707
|
+
f.number_field k.to_sym
|
708
|
+
end %><svg class=\"revert\" width=\"1.5em\" viewBox=\"0 0 512 512\"><use xlink:href=\"#revertPath\" /></svg>
|
709
|
+
<% when *dt_pickers.keys
|
710
|
+
is_includes_dates = true %>
|
711
|
+
<%= f.text_field k.to_sym, { class: dt_pickers[col_type] } %><svg class=\"revert\" width=\"1.5em\" viewBox=\"0 0 512 512\"><use xlink:href=\"#revertPath\" /></svg>
|
634
712
|
<% when :binary, :primary_key %>
|
635
713
|
<% end %>
|
636
714
|
<% end %>
|
@@ -646,6 +724,9 @@ end
|
|
646
724
|
<% end %>
|
647
725
|
|
648
726
|
#{hms_headers.each_with_object(+'') do |hm, s|
|
727
|
+
# %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
|
728
|
+
next if hm.first.options[:through] && !hm.first.through_reflection
|
729
|
+
|
649
730
|
if (pk = hm.first.klass.primary_key)
|
650
731
|
hm_singular_name = (hm_name = hm.first.name.to_s).singularize.underscore
|
651
732
|
obj_pk = (pk.is_a?(Array) ? pk : [pk]).each_with_object([]) { |pk_part, s| s << "#{hm_singular_name}.#{pk_part}" }.join(', ')
|
@@ -669,6 +750,66 @@ end
|
|
669
750
|
#{script}"
|
670
751
|
|
671
752
|
end
|
753
|
+
inline << "
|
754
|
+
<% if is_includes_dates %>
|
755
|
+
<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css\">
|
756
|
+
<style>
|
757
|
+
.flatpickr-calendar {
|
758
|
+
background: #A0FFA0;
|
759
|
+
}
|
760
|
+
</style>
|
761
|
+
<script src=\"https://cdn.jsdelivr.net/npm/flatpickr\"></script>
|
762
|
+
<script>
|
763
|
+
flatpickr(\".datepicker\");
|
764
|
+
flatpickr(\".datetimepicker\", {enableTime: true});
|
765
|
+
flatpickr(\".timepicker\", {enableTime: true, noCalendar: true});
|
766
|
+
</script>
|
767
|
+
<% end %>
|
768
|
+
<script>
|
769
|
+
document.querySelectorAll(\"input, select\").forEach(function (inp) {
|
770
|
+
var origVal = getInpVal(),
|
771
|
+
prevVal = origVal;
|
772
|
+
var revert;
|
773
|
+
if ((revert = ((inp.tagName === \"SELECT\" && inp.nextElementSibling.nextElementSibling) ||
|
774
|
+
inp.nextElementSibling ||
|
775
|
+
inp.parentElement.nextElementSibling)) && revert.tagName.toLowerCase() === \"svg\")
|
776
|
+
revert.addEventListener(\"click\", function (e) {
|
777
|
+
if (inp.type === \"checkbox\")
|
778
|
+
inp.checked = origVal;
|
779
|
+
else
|
780
|
+
inp.value = origVal;
|
781
|
+
revert.style.display = \"none\";
|
782
|
+
if (inp._flatpickr)
|
783
|
+
inp._flatpickr.setDate(origVal);
|
784
|
+
else
|
785
|
+
inp.focus();
|
786
|
+
});
|
787
|
+
inp.addEventListener(inp.type === \"checkbox\" ? \"change\" : \"input\", function (e) {
|
788
|
+
if(inp.className.split(\" \").indexOf(\"check-validity\") > 0) {
|
789
|
+
if (inp.checkValidity()) {
|
790
|
+
prevVal = getInpVal();
|
791
|
+
} else {
|
792
|
+
inp.value = prevVal;
|
793
|
+
}
|
794
|
+
} else {
|
795
|
+
// If this is the result of changing an hour or minute, keep the calendar open.
|
796
|
+
// And if it was the result of selecting a date, the calendar can now close.
|
797
|
+
if (inp._flatpickr &&
|
798
|
+
// Test only for changes in the date portion of a date or datetime
|
799
|
+
((giv = getInpVal()) && (giv1 = giv.split(' ')[0])) !== (prevVal && prevVal.split(' ')[0]) &&
|
800
|
+
giv1.indexOf(\":\") < 0 // (definitely not any part of a time thing)
|
801
|
+
)
|
802
|
+
inp._flatpickr.close();
|
803
|
+
prevVal = getInpVal();
|
804
|
+
}
|
805
|
+
// Show or hide the revert button
|
806
|
+
if (revert) revert.style.display = getInpVal() === origVal ? \"none\" : \"inline-block\";
|
807
|
+
});
|
808
|
+
function getInpVal() {
|
809
|
+
return inp.type === \"checkbox\" ? inp.checked : inp.value;
|
810
|
+
}
|
811
|
+
});
|
812
|
+
</script>"
|
672
813
|
# As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
|
673
814
|
keys = options.has_key?(:locals) ? options[:locals].keys : []
|
674
815
|
handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
|
@@ -4,53 +4,25 @@ if Object.const_defined?('::Rake::TaskManager')
|
|
4
4
|
namespace :brick do
|
5
5
|
desc 'Find any seemingly-orphaned records'
|
6
6
|
task orphans: :environment do
|
7
|
-
def class_pk(dotted_name, multitenant)
|
8
|
-
Object.const_get((multitenant ? [dotted_name.split('.').last] : dotted_name.split('.')).map { |nm| "::#{nm.singularize.camelize}" }.join).primary_key
|
9
|
-
end
|
10
|
-
|
11
7
|
schema_list = ((multi = ::Brick.config.schema_behavior[:multitenant]) && ::Brick.db_schemas.keys.sort) || []
|
12
|
-
if schema_list.length
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
elsif schema_list.length.positive?
|
20
|
-
schema = schema_list.first
|
21
|
-
end
|
8
|
+
schema = if schema_list.length == 1
|
9
|
+
schema_list.first
|
10
|
+
elsif schema_list.length.positive?
|
11
|
+
require 'fancy_gets'
|
12
|
+
include FancyGets
|
13
|
+
gets_list(list: schema_list, chosen: multi[:schema_to_analyse])
|
14
|
+
end
|
22
15
|
ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema) if schema
|
23
|
-
orphans =
|
24
|
-
|
25
|
-
|
26
|
-
!(pri_pk = v[:pkey].values.first&.first) ||
|
27
|
-
!(pri_pk = class_pk(k, multi))
|
28
|
-
v[:fks].each do |k1, v1|
|
29
|
-
next if v1[:is_bt] ||
|
30
|
-
!(for_rel = ::Brick.relations.fetch(v1[:inverse_table], nil)) ||
|
31
|
-
v1[:inverse]&.key?(:polymorphic) ||
|
32
|
-
!(for_pk = for_rel.fetch(:pkey, nil)&.values&.first&.first) ||
|
33
|
-
!(for_pk = class_pk(v1[:inverse_table], multi))
|
34
|
-
begin
|
35
|
-
ActiveRecord::Base.execute_sql(
|
36
|
-
"SELECT DISTINCT frn.#{v1[:fk]} AS pri_id, frn.#{for_pk} AS fk_id
|
37
|
-
FROM #{v1[:inverse_table]} AS frn
|
38
|
-
LEFT OUTER JOIN #{k} AS pri ON pri.#{pri_pk} = frn.#{v1[:fk]}
|
39
|
-
WHERE frn.#{v1[:fk]} IS NOT NULL AND pri.#{pri_pk} IS NULL
|
40
|
-
ORDER BY 1, 2"
|
41
|
-
).each do |o|
|
42
|
-
orphans << "#{v1[:inverse_table]} #{o['fk_id']} refers to non-existant #{k} #{o['pri_id']}\n"
|
43
|
-
end
|
44
|
-
rescue StandardError => err
|
45
|
-
puts "Strange -- #{err.inspect}"
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
puts "For #{schema}:\n#{'=' * (schema.length + 5)}" if schema
|
50
|
-
if orphans.blank?
|
16
|
+
orphans = ::Brick.find_orphans(schema)
|
17
|
+
puts "Orphans in #{schema}:\n#{'=' * (schema.length + 12)}" if schema
|
18
|
+
if orphans.empty?
|
51
19
|
puts "No orphans!"
|
52
20
|
else
|
53
|
-
|
21
|
+
orphans.each do |o|
|
22
|
+
via = " (via #{o[4]})" unless "#{o[2].split('.').last.underscore.singularize}_id" == o[4]
|
23
|
+
puts "#{o[0]} #{o[1]} refers#{via} to non-existent #{o[2]} #{o[3]}#{" (in table \"#{o[5]}\")" if o[5]}"
|
24
|
+
end
|
25
|
+
puts
|
54
26
|
end
|
55
27
|
end
|
56
28
|
end
|
data/lib/brick/version_number.rb
CHANGED
data/lib/brick.rb
CHANGED
@@ -128,7 +128,10 @@ module Brick
|
|
128
128
|
|
129
129
|
def set_db_schema(params)
|
130
130
|
schema = params['_brick_schema'] || 'public'
|
131
|
-
|
131
|
+
if schema && ::Brick.db_schemas&.include?(schema)
|
132
|
+
ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
|
133
|
+
schema
|
134
|
+
end
|
132
135
|
end
|
133
136
|
|
134
137
|
# All tables and views (what Postgres calls "relations" including column and foreign key info)
|
@@ -457,6 +460,9 @@ In config/initializers/brick.rb appropriate entries would look something like:
|
|
457
460
|
send(:resources, controller_name.to_sym, **options)
|
458
461
|
end
|
459
462
|
end
|
463
|
+
if ::Brick.config.add_orphans && instance_variable_get(:@set).named_routes.names.exclude?(:brick_orphans)
|
464
|
+
get('/brick_orphans', to: 'brick_gem#orphans', as: 'brick_orphans')
|
465
|
+
end
|
460
466
|
end
|
461
467
|
send(:get, '/api-docs/v1/swagger.json', { to: 'brick_swagger#index' }) if Object.const_defined?('Rswag::Ui')
|
462
468
|
end
|
@@ -209,7 +209,7 @@ module Brick
|
|
209
209
|
# # Specify STI subclasses either directly by name or as a general module prefix that should always relate to a specific
|
210
210
|
# # parent STI class. The prefixed :: here for these examples is mandatory. Also having a suffixed :: means instead of
|
211
211
|
# # a class reference, this is for a general namespace reference. So in this case requests for, say, either of the
|
212
|
-
# # non-
|
212
|
+
# # non-existent classes Animals::Cat or Animals::Goat (or anything else with the module prefix of \"Animals::\" would
|
213
213
|
# # build a model that inherits from Animal. And a request specifically for the class Snake would build a new model
|
214
214
|
# # that inherits from Reptile, and no other request would do this -- only specifically for Snake. The ending ::
|
215
215
|
# # indicates that it's a module prefix instead of a specific class name.
|
@@ -225,7 +225,7 @@ module Brick
|
|
225
225
|
# # it wasn't originally specified.
|
226
226
|
# Brick.schema_behavior = :namespaced
|
227
227
|
#{Brick.config.schema_behavior ? "Brick.schema_behavior = { multitenant: { schema_to_analyse: #{
|
228
|
-
Brick.config.schema_behavior[:multitenant]
|
228
|
+
Brick.config.schema_behavior[:multitenant]&.fetch(:schema_to_analyse, nil).inspect}" :
|
229
229
|
"# Brick.schema_behavior = { multitenant: { schema_to_analyse: 'engineering'"
|
230
230
|
} } }
|
231
231
|
|
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.43
|
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-07-
|
11
|
+
date: 2022-07-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|