brick 1.0.39 → 1.0.42
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 +90 -6
- data/lib/brick/frameworks/rails/engine.rb +185 -49
- data/lib/brick/tasks/orphans.rake +29 -0
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +12 -1
- data/lib/generators/brick/install_generator.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a385c606282392cbefea1eaa045dc6a023bd05f56044013bc3835ef06a630b87
|
4
|
+
data.tar.gz: 261e21a2eb02ac1b199c831e1bba5eeec3add7b5faf0b2567fbb94a3252d817f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 686a6ad73671511c36c6871e86fd6d02f7b8b48b2e1bacb033ec5348a85cedfc1c6848c92293e4144c0ca95ab79888c5993dcdbb7f440d93265a6c65b5c74a6b
|
7
|
+
data.tar.gz: 04e0cb25404beb9fead5d07ff3c76066bc23d097fb2a01c53b689737e1a5d70cfef94b432b89f45f61497074171bb171fae0064aef970c2c025f3ce05de23557
|
data/lib/brick/config.rb
CHANGED
data/lib/brick/extensions.rb
CHANGED
@@ -70,7 +70,11 @@ module ActiveRecord
|
|
70
70
|
def self._brick_primary_key(relation = nil)
|
71
71
|
return instance_variable_get(:@_brick_primary_key) if instance_variable_defined?(:@_brick_primary_key)
|
72
72
|
|
73
|
-
pk =
|
73
|
+
pk = begin
|
74
|
+
primary_key.is_a?(String) ? [primary_key] : primary_key || []
|
75
|
+
rescue
|
76
|
+
[]
|
77
|
+
end
|
74
78
|
# Just return [] if we're missing any part of the primary key. (PK is usually just "id")
|
75
79
|
if relation && pk.present?
|
76
80
|
@_brick_primary_key ||= pk.any? { |pk_part| !relation[:cols].key?(pk_part) } ? [] : pk
|
@@ -411,14 +415,16 @@ module ActiveRecord
|
|
411
415
|
hm_counts.each do |k, hm|
|
412
416
|
associative = nil
|
413
417
|
count_column = if hm.options[:through]
|
414
|
-
fk_col = (associative = associatives[hm.name])
|
415
|
-
hm.foreign_key
|
418
|
+
fk_col = (associative = associatives[hm.name])&.foreign_key
|
419
|
+
hm.foreign_key if fk_col
|
416
420
|
else
|
417
421
|
fk_col = hm.foreign_key
|
418
422
|
poly_type = hm.inverse_of.foreign_type if hm.options.key?(:as)
|
419
423
|
pk = hm.klass.primary_key
|
420
424
|
(pk.is_a?(Array) ? pk.first : pk) || '*'
|
421
425
|
end
|
426
|
+
next unless count_column # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
|
427
|
+
|
422
428
|
tbl_alias = "_br_#{hm.name}"
|
423
429
|
pri_tbl = hm.active_record
|
424
430
|
on_clause = []
|
@@ -561,7 +567,9 @@ Module.class_exec do
|
|
561
567
|
full_class_name = +''
|
562
568
|
full_class_name << "::#{self.name}" unless self == Object
|
563
569
|
full_class_name << "::#{plural_class_name.underscore.singularize.camelize}"
|
564
|
-
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))
|
565
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.
|
566
574
|
Object.send(:build_controller, self, class_name, plural_class_name, model, relations)
|
567
575
|
end
|
@@ -893,6 +901,14 @@ class Object
|
|
893
901
|
built_controller = Class.new(ActionController::Base) do |new_controller_class|
|
894
902
|
(namespace || Object).const_set(class_name.to_sym, new_controller_class)
|
895
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
|
+
|
896
912
|
unless (is_swagger = plural_class_name == 'BrickSwagger') # && request.format == :json)
|
897
913
|
code << " def index\n"
|
898
914
|
code << " @#{table_name} = #{model.name}#{pk&.present? ? ".order(#{pk.inspect})" : '.all'}\n"
|
@@ -1043,10 +1059,11 @@ class Object
|
|
1043
1059
|
if is_need_params
|
1044
1060
|
code << "private\n"
|
1045
1061
|
code << " def #{params_name}\n"
|
1046
|
-
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"
|
1047
1064
|
code << " end\n"
|
1048
1065
|
self.define_method(params_name) do
|
1049
|
-
params.require(
|
1066
|
+
params.require(require_name.to_sym).permit(model.columns_hash.keys)
|
1050
1067
|
end
|
1051
1068
|
private params_name
|
1052
1069
|
# Get column names for params from relations[model.table_name][:cols].keys
|
@@ -1471,5 +1488,72 @@ module Brick
|
|
1471
1488
|
end
|
1472
1489
|
assoc_bt[:inverse] = assoc_hm
|
1473
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
|
1474
1558
|
end
|
1475
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,10 @@ input[type=submit] {
|
|
225
259
|
text-align: right;
|
226
260
|
}
|
227
261
|
</style>
|
228
|
-
<%
|
262
|
+
<%
|
263
|
+
is_includes_dates = nil
|
264
|
+
|
265
|
+
def is_bcrypt?(val)
|
229
266
|
val.is_a?(String) && val.length == 60 && val.start_with?('$2a$')
|
230
267
|
end
|
231
268
|
def hide_bcrypt(val, max_len = 200)
|
@@ -314,7 +351,7 @@ function changeout(href, param, value, trimAfter) {
|
|
314
351
|
var pathParts = hrefParts[hrefParts.length - 1].split(\"/\");
|
315
352
|
if (value === undefined)
|
316
353
|
// 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)];
|
354
|
+
return [pathParts.slice(1, 3).join('/'), pathParts.slice(1, 2)[0]];
|
318
355
|
else
|
319
356
|
return hrefParts[0] + \"://\" + pathParts[0] + \"/\" + value;
|
320
357
|
}
|
@@ -464,7 +501,7 @@ if (headerTop) {
|
|
464
501
|
end
|
465
502
|
# %%% Instead of our current "for Janet Leverling (Employee)" kind of link we previously had this code that did a "where x = 123" thing:
|
466
503
|
# (where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %>)
|
467
|
-
"#{css}
|
504
|
+
+"#{css}
|
468
505
|
<p style=\"color: green\"><%= notice %></p>#{"
|
469
506
|
<select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
|
470
507
|
<select id=\"tbl\">#{table_options}</select>
|
@@ -490,8 +527,7 @@ if (headerTop) {
|
|
490
527
|
<thead><tr>#{'<th></th>' if pk.present?}<%
|
491
528
|
col_order = []
|
492
529
|
@#{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)) ||
|
530
|
+
next if (#{(pk || []).inspect}.include?(col_name = col.name) && col.type == :integer && !bts.key?(col_name)) ||
|
495
531
|
::Brick.config.metadata_columns.include?(col_name) || poly_cols.include?(col_name)
|
496
532
|
|
497
533
|
col_order << col_name
|
@@ -504,10 +540,16 @@ if (headerTop) {
|
|
504
540
|
else %><%=
|
505
541
|
col_name %><%
|
506
542
|
end
|
507
|
-
|
543
|
+
%></th><%
|
508
544
|
end
|
509
545
|
# 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
|
546
|
+
%>#{hms_headers.map do |h|
|
547
|
+
if h.first.options[:through] && !h.first.through_reflection
|
548
|
+
"<th>#{h[1]} #{h[2]} %></th>" # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
|
549
|
+
else
|
550
|
+
"<th>#{h[1]} <%= link_to('#{h[2]}', #{h.first.klass.name.underscore.tr('/', '_').pluralize}_path) %></th>"
|
551
|
+
end
|
552
|
+
end.join
|
511
553
|
}</tr></thead>
|
512
554
|
|
513
555
|
<tbody>
|
@@ -529,6 +571,7 @@ if (headerTop) {
|
|
529
571
|
# 0..62 because Postgres column names are limited to 63 characters
|
530
572
|
#{obj_name}, (descrips = @_brick_bt_descrip[bt.first][bt_class])[0..-2].map { |z| #{obj_name}.send(z.last[0..62]) }, (bt_id_col = descrips.last)
|
531
573
|
)
|
574
|
+
bt_txt ||= \"<< Orphaned ID: #\{val} >>\" if val
|
532
575
|
bt_id = #{obj_name}.send(*bt_id_col) if bt_id_col&.present? %>
|
533
576
|
<%= bt_id ? link_to(bt_txt, send(\"#\{bt_class.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, bt_id)) : bt_txt %>
|
534
577
|
<%#= 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 %>
|
@@ -546,8 +589,31 @@ if (headerTop) {
|
|
546
589
|
|
547
590
|
#{"<hr><%= link_to \"New #{obj_name}\", new_#{path_obj_name}_path %>" unless @_brick_model.is_view?}
|
548
591
|
#{script}"
|
592
|
+
when 'orphans'
|
593
|
+
if is_orphans
|
594
|
+
+"#{css}
|
595
|
+
<p style=\"color: green\"><%= notice %></p>#{"
|
596
|
+
<select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
|
597
|
+
<select id=\"tbl\">#{table_options}</select>
|
598
|
+
<h1>Orphans<%= \" for #\{}\" if false %></h1>
|
599
|
+
<% @orphans.each do |o|
|
600
|
+
via = \" (via #\{o[4]})\" unless \"#\{o[2].split('.').last.underscore.singularize}_id\" == o[4] %>
|
601
|
+
<a href=\"/<%= o[0].split('.').last %>/<%= o[1] %>\">
|
602
|
+
<%= \"#\{o[0]} #\{o[1]} refers#\{via} to non-existent #\{o[2]} #\{o[3]}#\{\" (in table \\\"#\{o[5]}\\\")\" if o[5]}\" %>
|
603
|
+
</a><br>
|
604
|
+
<% end %>
|
605
|
+
#{script}"
|
606
|
+
end
|
607
|
+
|
549
608
|
when 'show', 'update'
|
550
|
-
"#{css}
|
609
|
+
+"#{css}
|
610
|
+
|
611
|
+
<svg id=\"revertTemplate\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"
|
612
|
+
width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" xml:space=\"preserve\">
|
613
|
+
<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
|
614
|
+
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\" />
|
615
|
+
</svg>
|
616
|
+
|
551
617
|
<p style=\"color: green\"><%= notice %></p>#{"
|
552
618
|
<select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
|
553
619
|
<select id=\"tbl\">#{table_options}</select>
|
@@ -605,27 +671,39 @@ end
|
|
605
671
|
<%= k %>
|
606
672
|
<% end %>
|
607
673
|
</th>
|
608
|
-
<td>
|
609
|
-
<%
|
674
|
+
<td class=\"val\">
|
675
|
+
<% dt_pickers = { datetime: 'datetimepicker', timestamp: 'datetimepicker', time: 'timepicker', date: 'datepicker' }
|
676
|
+
if bt
|
610
677
|
html_options = { prompt: \"Select #\{bt_name\}\" }
|
611
678
|
html_options[:class] = 'dimmed' unless val %>
|
612
679
|
<%= f.select k.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
|
613
|
-
<%= bt_obj = bt_class&.find_by(bt_pair[1] => val)
|
614
|
-
|
680
|
+
<%= if (bt_obj = bt_class&.find_by(bt_pair[1] => val))
|
681
|
+
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' })
|
682
|
+
elsif val
|
683
|
+
\"<span>Orphaned ID: #\{val}</span>\".html_safe
|
684
|
+
end %><svg class=\"revert\" width=\"1.5em\" viewBox=\"0 0 512 512\"><use xlink:href=\"#revertPath\" /></svg>
|
685
|
+
<% else case (col_type = #{model_name}.column_for_attribute(k).type)
|
615
686
|
when :string, :text %>
|
616
687
|
<% if is_bcrypt?(val) # || .readonly? %>
|
617
688
|
<%= hide_bcrypt(val, 1000) %>
|
618
689
|
<% else %>
|
619
|
-
<div class=\"wide-input\"><%= f.text_field k.to_sym
|
690
|
+
<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>
|
620
691
|
<% end %>
|
621
692
|
<% when :boolean %>
|
622
|
-
<%= f.check_box k.to_sym
|
623
|
-
<% when :integer, :decimal, :float
|
693
|
+
<%= f.check_box k.to_sym %><svg class=\"revert\" width=\"1.5em\" viewBox=\"0 0 512 512\"><use xlink:href=\"#revertPath\" /></svg>
|
694
|
+
<% when :integer, :decimal, :float
|
624
695
|
# What happens when keys are UUID?
|
625
696
|
# Postgres naturally uses the +uuid_generate_v4()+ function from the uuid-ossp extension
|
626
697
|
# If it's not yet enabled then: enable_extension 'uuid-ossp'
|
627
698
|
# ActiveUUID gem created a new :uuid type %>
|
628
|
-
<%=
|
699
|
+
<%= if col_type == :integer
|
700
|
+
f.text_field k.to_sym, { pattern: '\\d*', class: 'check-validity' }
|
701
|
+
else
|
702
|
+
f.number_field k.to_sym
|
703
|
+
end %><svg class=\"revert\" width=\"1.5em\" viewBox=\"0 0 512 512\"><use xlink:href=\"#revertPath\" /></svg>
|
704
|
+
<% when *dt_pickers.keys
|
705
|
+
is_includes_dates = true %>
|
706
|
+
<%= 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>
|
629
707
|
<% when :binary, :primary_key %>
|
630
708
|
<% end %>
|
631
709
|
<% end %>
|
@@ -641,6 +719,9 @@ end
|
|
641
719
|
<% end %>
|
642
720
|
|
643
721
|
#{hms_headers.each_with_object(+'') do |hm, s|
|
722
|
+
# %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
|
723
|
+
next if hm.first.options[:through] && !hm.first.through_reflection
|
724
|
+
|
644
725
|
if (pk = hm.first.klass.primary_key)
|
645
726
|
hm_singular_name = (hm_name = hm.first.name.to_s).singularize.underscore
|
646
727
|
obj_pk = (pk.is_a?(Array) ? pk : [pk]).each_with_object([]) { |pk_part, s| s << "#{hm_singular_name}.#{pk_part}" }.join(', ')
|
@@ -664,6 +745,61 @@ end
|
|
664
745
|
#{script}"
|
665
746
|
|
666
747
|
end
|
748
|
+
inline << "
|
749
|
+
<% if is_includes_dates %>
|
750
|
+
<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css\">
|
751
|
+
<style>
|
752
|
+
.flatpickr-calendar {
|
753
|
+
background: #A0FFA0;
|
754
|
+
}
|
755
|
+
</style>
|
756
|
+
<script src=\"https://cdn.jsdelivr.net/npm/flatpickr\"></script>
|
757
|
+
<script>
|
758
|
+
flatpickr(\".datepicker\");
|
759
|
+
flatpickr(\".datetimepicker\", {enableTime: true});
|
760
|
+
</script>
|
761
|
+
<% end %>
|
762
|
+
<script>
|
763
|
+
document.querySelectorAll(\"input, select\").forEach(function (inp) {
|
764
|
+
var origVal = getInpVal(),
|
765
|
+
prevVal = origVal;
|
766
|
+
var revert;
|
767
|
+
if ((revert = ((inp.tagName === \"SELECT\" && inp.nextElementSibling.nextElementSibling) ||
|
768
|
+
inp.nextElementSibling ||
|
769
|
+
inp.parentElement.nextElementSibling)) && revert.tagName.toLowerCase() === \"svg\")
|
770
|
+
revert.addEventListener(\"click\", function (e) {
|
771
|
+
if (inp.type === \"checkbox\")
|
772
|
+
inp.checked = origVal;
|
773
|
+
else
|
774
|
+
inp.value = origVal;
|
775
|
+
revert.style.display = \"none\";
|
776
|
+
if (!inp._flatpickr) inp.focus();
|
777
|
+
});
|
778
|
+
inp.addEventListener(inp.type === \"checkbox\" ? \"change\" : \"input\", function (e) {
|
779
|
+
if(inp.className.split(\" \").indexOf(\"check-validity\") > 0) {
|
780
|
+
if (inp.checkValidity()) {
|
781
|
+
prevVal = getInpVal();
|
782
|
+
} else {
|
783
|
+
inp.value = prevVal;
|
784
|
+
}
|
785
|
+
} else {
|
786
|
+
// If this is the result of changing an hour or minute, keep the calendar open.
|
787
|
+
// And if it was the result of selecting a date, the calendar can now close.
|
788
|
+
if (inp._flatpickr &&
|
789
|
+
// Test only for changes in the date portion of a date or datetime
|
790
|
+
((giv = getInpVal()) && giv.split(' ')[0]) !== (prevVal && prevVal.split(' ')[0])
|
791
|
+
)
|
792
|
+
inp._flatpickr.close();
|
793
|
+
prevVal = getInpVal();
|
794
|
+
}
|
795
|
+
// Show or hide the revert button
|
796
|
+
if (revert) revert.style.display = getInpVal() === origVal ? \"none\" : \"inline-block\";
|
797
|
+
});
|
798
|
+
function getInpVal() {
|
799
|
+
return inp.type === \"checkbox\" ? inp.checked : inp.value;
|
800
|
+
}
|
801
|
+
});
|
802
|
+
</script>"
|
667
803
|
# As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
|
668
804
|
keys = options.has_key?(:locals) ? options[:locals].keys : []
|
669
805
|
handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
if Object.const_defined?('::Rake::TaskManager')
|
4
|
+
namespace :brick do
|
5
|
+
desc 'Find any seemingly-orphaned records'
|
6
|
+
task orphans: :environment do
|
7
|
+
schema_list = ((multi = ::Brick.config.schema_behavior[:multitenant]) && ::Brick.db_schemas.keys.sort) || []
|
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
|
15
|
+
ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema) if schema
|
16
|
+
orphans = ::Brick.find_orphans(schema)
|
17
|
+
puts "Orphans in #{schema}:\n#{'=' * (schema.length + 12)}" if schema
|
18
|
+
if orphans.empty?
|
19
|
+
puts "No orphans!"
|
20
|
+
else
|
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
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
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
|
@@ -481,6 +487,11 @@ require 'active_record'
|
|
481
487
|
require 'active_record/relation'
|
482
488
|
require 'active_record/relation/query_methods' if is_add_left_outer_join
|
483
489
|
|
490
|
+
# Rake tasks
|
491
|
+
class Railtie < Rails::Railtie
|
492
|
+
Dir.glob("#{File.expand_path(__dir__)}/brick/tasks/**/*.rake").each { |task| load task }
|
493
|
+
end
|
494
|
+
|
484
495
|
# Major compatibility fixes for ActiveRecord < 4.2
|
485
496
|
# ================================================
|
486
497
|
ActiveSupport.on_load(:active_record) do
|
@@ -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.
|
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.42
|
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-
|
11
|
+
date: 2022-07-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -30,6 +30,20 @@ dependencies:
|
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: '7.2'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: fancy_gets
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
33
47
|
- !ruby/object:Gem::Dependency
|
34
48
|
name: appraisal
|
35
49
|
requirement: !ruby/object:Gem::Requirement
|
@@ -225,6 +239,7 @@ files:
|
|
225
239
|
- lib/brick/join_array.rb
|
226
240
|
- lib/brick/serializers/json.rb
|
227
241
|
- lib/brick/serializers/yaml.rb
|
242
|
+
- lib/brick/tasks/orphans.rake
|
228
243
|
- lib/brick/util.rb
|
229
244
|
- lib/brick/version_number.rb
|
230
245
|
- lib/generators/brick/USAGE
|