brick 1.0.74 → 1.0.76
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 +9 -0
- data/lib/brick/extensions.rb +213 -109
- data/lib/brick/frameworks/rails/engine.rb +53 -9
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +27 -3
- data/lib/generators/brick/install_generator.rb +6 -0
- data/lib/generators/brick/migrations_generator.rb +1 -1
- data/lib/generators/brick/models_generator.rb +1 -6
- 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: 615640b22db113a3959644ee9c2d08ff3a2b37aa10f12dc92d68effef091c228
|
4
|
+
data.tar.gz: b0d3e616b0f44584cf6960f8b8b191fc449b9afc426a6486da11c2deadece7a6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 02a9f1c74af24e1e23df8b64972d7af8d74bbb870f1510b69d0814f362959e2527f2a7ee9ff7ca65187eb97be4fad1136d64bea7e01a8327a57608924a8f12d8
|
7
|
+
data.tar.gz: '0265977b746d0955c76bef269067d14363cda4373fc5f726f19fb284baf31d0df3bdaae6ffad7301b71cd630a2745db4d30f1da79ec43ef4ae1a92bc986058e9'
|
data/lib/brick/config.rb
CHANGED
@@ -90,6 +90,15 @@ module Brick
|
|
90
90
|
@mutex.synchronize { @additional_references = references }
|
91
91
|
end
|
92
92
|
|
93
|
+
# Custom columns to add to a table, minimally defined with a name and DSL string
|
94
|
+
def custom_columns
|
95
|
+
@mutex.synchronize { @custom_columns }
|
96
|
+
end
|
97
|
+
|
98
|
+
def custom_columns=(cust_cols)
|
99
|
+
@mutex.synchronize { @custom_columns = cust_cols }
|
100
|
+
end
|
101
|
+
|
93
102
|
# Skip creating a has_many association for these
|
94
103
|
def exclude_hms
|
95
104
|
@mutex.synchronize { @exclude_hms }
|
data/lib/brick/extensions.rb
CHANGED
@@ -100,73 +100,86 @@ module ActiveRecord
|
|
100
100
|
dsl
|
101
101
|
end
|
102
102
|
|
103
|
-
def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {},
|
104
|
-
|
105
|
-
|
103
|
+
def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {}, is_polymorphic = false, dsl = nil, emit_dsl = false)
|
104
|
+
unless build_array.is_a?(::Brick::JoinArray)
|
105
|
+
build_array = ::Brick::JoinArray.new.tap { |ary| ary.replace([build_array]) } if build_array.is_a?(::Brick::JoinHash)
|
106
|
+
build_array = ::Brick::JoinArray.new unless build_array.nil? || build_array.is_a?(Array)
|
107
|
+
end
|
108
|
+
prefix = [prefix] unless prefix.is_a?(Array)
|
106
109
|
members = []
|
110
|
+
unless dsl || (dsl = ::Brick.config.model_descrips[name] || brick_get_dsl)
|
111
|
+
# With no DSL available, still put this prefix into the JoinArray so we can get primary key (ID) info from this table
|
112
|
+
x = prefix.each_with_object(build_array) { |v, s| s[v.to_sym] }
|
113
|
+
x[prefix.last] = nil unless prefix.empty? # Using []= will "hydrate" any missing part(s) in our whole series
|
114
|
+
return members
|
115
|
+
end
|
116
|
+
|
117
|
+
# Do the actual dirty work of recursing through nested DSL
|
107
118
|
bracket_name = nil
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
parts[0..-3].each { |v| s = s[v.to_sym] }
|
127
|
-
s[parts[-2]] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
|
128
|
-
end
|
129
|
-
translations[parts[0..-2].join('.')] = klass
|
119
|
+
dsl2 = +'' # To replace our own DSL definition in case it needs to be expanded
|
120
|
+
dsl3 = +'' # To return expanded DSL that is nested from another model
|
121
|
+
klass = nil
|
122
|
+
dsl.each_char do |ch|
|
123
|
+
if bracket_name
|
124
|
+
if ch == ']' # Time to process a bracketed thing?
|
125
|
+
parts = bracket_name.split('.')
|
126
|
+
first_parts = parts[0..-2].map do |part|
|
127
|
+
klass = (orig_class = klass).reflect_on_association(part_sym = part.to_sym)&.klass
|
128
|
+
puts "Couldn't reference #{orig_class.name}##{part} that's part of the DSL \"#{dsl}\"." if klass.nil?
|
129
|
+
part_sym
|
130
|
+
end
|
131
|
+
parts = prefix + first_parts + [parts[-1]]
|
132
|
+
if parts.length > 1
|
133
|
+
unless is_polymorphic
|
134
|
+
s = build_array
|
135
|
+
parts[0..-3].each { |v| s = s[v.to_sym] }
|
136
|
+
s[parts[-2]] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
|
130
137
|
end
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
dsl3 << dsl2a
|
138
|
-
else
|
139
|
-
dsl2 << "[#{bracket_name}]"
|
140
|
-
if emit_dsl
|
141
|
-
dsl3 << "[#{prefix[1..-1].map { |p| "#{p.to_s}." }.join if prefix.length > 1}#{bracket_name}]"
|
142
|
-
end
|
143
|
-
members << parts
|
138
|
+
translations[parts[0..-2].join('.')] = klass
|
139
|
+
end
|
140
|
+
if klass.column_names.exclude?(parts.last) &&
|
141
|
+
(klass = (orig_class = klass).reflect_on_association(possible_dsl = parts.pop.to_sym)&.klass)
|
142
|
+
if prefix.empty? # Custom columns start with an empty prefix
|
143
|
+
prefix << parts.shift until parts.empty?
|
144
144
|
end
|
145
|
-
|
145
|
+
# Expand this entry which refers to an association name
|
146
|
+
members2, dsl2a = klass.brick_parse_dsl(build_array, prefix + [possible_dsl], translations, is_polymorphic, nil, true)
|
147
|
+
members += members2
|
148
|
+
dsl2 << dsl2a
|
149
|
+
dsl3 << dsl2a
|
146
150
|
else
|
147
|
-
|
151
|
+
dsl2 << "[#{bracket_name}]"
|
152
|
+
if emit_dsl
|
153
|
+
dsl3 << "[#{prefix[1..-1].map { |p| "#{p.to_s}." }.join if prefix.length > 1}#{bracket_name}]"
|
154
|
+
end
|
155
|
+
members << parts
|
148
156
|
end
|
149
|
-
|
150
|
-
bracket_name = +''
|
151
|
-
klass = self
|
157
|
+
bracket_name = nil
|
152
158
|
else
|
153
|
-
|
154
|
-
dsl3 << ch
|
159
|
+
bracket_name << ch
|
155
160
|
end
|
161
|
+
elsif ch == '['
|
162
|
+
bracket_name = +''
|
163
|
+
klass = self
|
164
|
+
else
|
165
|
+
dsl2 << ch
|
166
|
+
dsl3 << ch
|
156
167
|
end
|
157
|
-
# Rewrite the DSL in case it's now different from having to expand it
|
158
|
-
# if ::Brick.config.model_descrips[name] != dsl2
|
159
|
-
# puts ::Brick.config.model_descrips[name]
|
160
|
-
# puts dsl2.inspect
|
161
|
-
# puts dsl3.inspect
|
162
|
-
# binding.pry
|
163
|
-
# end
|
164
|
-
::Brick.config.model_descrips[name] = dsl2 unless emit_dsl
|
165
|
-
else # With no DSL available, still put this prefix into the JoinArray so we can get primary key (ID) info from this table
|
166
|
-
x = prefix.each_with_object(build_array) { |v, s| s[v.to_sym] }
|
167
|
-
x[prefix.last] = nil unless prefix.empty? # Using []= will "hydrate" any missing part(s) in our whole series
|
168
168
|
end
|
169
|
-
|
169
|
+
# Rewrite the DSL in case it's now different from having to expand it
|
170
|
+
# if ::Brick.config.model_descrips[name] != dsl2
|
171
|
+
# puts ::Brick.config.model_descrips[name]
|
172
|
+
# puts dsl2.inspect
|
173
|
+
# puts dsl3.inspect
|
174
|
+
# binding.pry
|
175
|
+
# end
|
176
|
+
if emit_dsl
|
177
|
+
# Had been: [members, dsl2, dsl3]
|
178
|
+
[members, dsl3]
|
179
|
+
else
|
180
|
+
::Brick.config.model_descrips[name] = dsl2
|
181
|
+
members
|
182
|
+
end
|
170
183
|
end
|
171
184
|
|
172
185
|
# If available, parse simple DSL attached to a model in order to provide a friendlier name.
|
@@ -177,7 +190,8 @@ module ActiveRecord
|
|
177
190
|
end
|
178
191
|
|
179
192
|
def self.brick_descrip(obj, data = nil, pk_alias = nil)
|
180
|
-
|
193
|
+
dsl = obj if obj.is_a?(String)
|
194
|
+
if (dsl ||= ::Brick.config.model_descrips[(klass = self).name] || klass.brick_get_dsl)
|
181
195
|
idx = -1
|
182
196
|
caches = {}
|
183
197
|
output = +''
|
@@ -235,7 +249,7 @@ module ActiveRecord
|
|
235
249
|
|
236
250
|
def self.bt_link(assoc_name)
|
237
251
|
assoc_name = CGI.escapeHTML(assoc_name.to_s)
|
238
|
-
model_path = Rails.application.routes.url_helpers.send("#{_brick_index}_path".to_sym)
|
252
|
+
model_path = ::Rails.application.routes.url_helpers.send("#{_brick_index}_path".to_sym)
|
239
253
|
av_class = Class.new.extend(ActionView::Helpers::UrlHelper)
|
240
254
|
av_class.extend(ActionView::Helpers::TagHelper) if ActionView.version < ::Gem::Version.new('7')
|
241
255
|
link = av_class.link_to(name, model_path)
|
@@ -272,18 +286,28 @@ module ActiveRecord
|
|
272
286
|
def _br_associatives
|
273
287
|
@_br_associatives ||= {}
|
274
288
|
end
|
289
|
+
# Custom columns
|
290
|
+
def _br_cust_cols
|
291
|
+
@_br_cust_cols ||= {}
|
292
|
+
end
|
275
293
|
end
|
276
294
|
|
277
|
-
# Search for BT, HM, and HMT DSL stuff
|
295
|
+
# Search for custom column, BT, HM, and HMT DSL stuff
|
278
296
|
def self._brick_calculate_bts_hms(translations, join_array)
|
297
|
+
# Add any custom columns
|
298
|
+
::Brick.config.custom_columns&.fetch(table_name, nil)&.each do |k, cc|
|
299
|
+
# false = not polymorphic, and true = yes -- please emit_dsl
|
300
|
+
pieces, my_dsl = brick_parse_dsl(join_array, [], translations, false, cc, true)
|
301
|
+
_br_cust_cols[k] = [pieces, my_dsl]
|
302
|
+
end
|
279
303
|
bts, hms, associatives = ::Brick.get_bts_and_hms(self)
|
280
304
|
bts.each do |_k, bt|
|
281
305
|
next if bt[2] # Polymorphic?
|
282
306
|
|
283
307
|
# join_array will receive this relation name when calling #brick_parse_dsl
|
284
308
|
_br_bt_descrip[bt.first] = if bt[1].is_a?(Array)
|
285
|
-
# Last
|
286
|
-
bt[1].each_with_object({}) { |bt_class, s| s[bt_class] = bt_class.brick_parse_dsl(join_array, bt.first, translations,
|
309
|
+
# Last params here: "true" is for yes, we are polymorphic
|
310
|
+
bt[1].each_with_object({}) { |bt_class, s| s[bt_class] = bt_class.brick_parse_dsl(join_array, bt.first, translations, true) }
|
287
311
|
else
|
288
312
|
{ bt.last => bt[1].brick_parse_dsl(join_array, bt.first, translations) }
|
289
313
|
end
|
@@ -314,7 +338,7 @@ module ActiveRecord
|
|
314
338
|
else # Expecting only Symbol
|
315
339
|
if _br_hm_counts.key?(ord_part)
|
316
340
|
ord_part = "\"b_r_#{ord_part}_ct\""
|
317
|
-
elsif !_br_bt_descrip.key?(ord_part) && !column_names.include?(ord_part.to_s)
|
341
|
+
elsif !_br_bt_descrip.key?(ord_part) && !_br_cust_cols.key?(ord_part) && !column_names.include?(ord_part.to_s)
|
318
342
|
# Disallow ordering by a bogus column
|
319
343
|
# %%% Note this bogus entry so that Javascript can remove any bogus _brick_order
|
320
344
|
# parameter from the querystring, pushing it into the browser history.
|
@@ -331,6 +355,11 @@ module ActiveRecord
|
|
331
355
|
[order_by, order_by_txt]
|
332
356
|
end
|
333
357
|
|
358
|
+
def self.brick_select(params = {}, selects = [], *args)
|
359
|
+
(relation = all).brick_select(params, selects, *args)
|
360
|
+
relation.select(selects)
|
361
|
+
end
|
362
|
+
|
334
363
|
private
|
335
364
|
|
336
365
|
def self._brick_get_fks
|
@@ -421,7 +450,13 @@ module ActiveRecord
|
|
421
450
|
end
|
422
451
|
end
|
423
452
|
|
424
|
-
def brick_select(params, selects =
|
453
|
+
def brick_select(params, selects = [], order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new)
|
454
|
+
is_add_bts = is_add_hms = true
|
455
|
+
|
456
|
+
# Build out cust_cols, bt_descrip and hm_counts now so that they are available on the
|
457
|
+
# model early in case the user wants to do an ORDER BY based on any of that.
|
458
|
+
model._brick_calculate_bts_hms(translations, join_array) if is_add_bts || is_add_hms
|
459
|
+
|
425
460
|
is_postgres = ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
|
426
461
|
is_mysql = ActiveRecord::Base.connection.adapter_name == 'Mysql2'
|
427
462
|
is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer'
|
@@ -446,7 +481,7 @@ module ActiveRecord
|
|
446
481
|
end
|
447
482
|
|
448
483
|
# %%% Skip the metadata columns
|
449
|
-
if selects
|
484
|
+
if selects.empty? # Default to all columns
|
450
485
|
tbl_no_schema = table.name.split('.').last
|
451
486
|
# %%% Have once gotten this error with MSSQL referring to http://localhost:3000/warehouse/cold_room_temperatures__archive
|
452
487
|
# ActiveRecord::StatementInvalid (TinyTds::Error: DBPROCESS is dead or not enabled)
|
@@ -481,7 +516,36 @@ module ActiveRecord
|
|
481
516
|
id_for_tables = Hash.new { |h, k| h[k] = [] }
|
482
517
|
field_tbl_names = Hash.new { |h, k| h[k] = {} }
|
483
518
|
used_col_aliases = {} # Used to make sure there is not a name clash
|
484
|
-
|
519
|
+
|
520
|
+
# CUSTOM COLUMNS
|
521
|
+
# ==============
|
522
|
+
klass._br_cust_cols.each do |k, cc|
|
523
|
+
if respond_to?(k) # Name already taken?
|
524
|
+
# %%% Use ensure_unique here in this kind of fashion:
|
525
|
+
# cnstr_name = ensure_unique(+"(brick) #{for_tbl}_#{pri_tbl}", bts, hms)
|
526
|
+
# binding.pry
|
527
|
+
next
|
528
|
+
end
|
529
|
+
|
530
|
+
cc.first.each do |cc_part|
|
531
|
+
dest_klass = cc_part[0..-2].inject(klass) { |kl, cc_part_term| kl.reflect_on_association(cc_part_term).klass }
|
532
|
+
tbl_name = (field_tbl_names[k][cc_part.last] ||= shift_or_first(chains[dest_klass])).split('.').last
|
533
|
+
# Deal with the conflict if there are two parts in the custom column named the same,
|
534
|
+
# "category.name" and "product.name" for instance will end up with aliases of "name"
|
535
|
+
# and "product__name".
|
536
|
+
cc_part_idx = cc_part.length - 1
|
537
|
+
while cc_part_idx > 0 &&
|
538
|
+
(col_alias = "br_cc_#{k}__#{cc_part[cc_part_idx..-1].map(&:to_s).join('__')}") &&
|
539
|
+
used_col_aliases.key?(col_alias)
|
540
|
+
cc_part_idx -= 1
|
541
|
+
end
|
542
|
+
selects << "#{tbl_name}.#{cc_part.last} AS #{col_alias}"
|
543
|
+
cc_part << col_alias
|
544
|
+
used_col_aliases[col_alias] = nil
|
545
|
+
end
|
546
|
+
end
|
547
|
+
|
548
|
+
klass._br_bt_descrip.each do |v|
|
485
549
|
v.last.each do |k1, v1| # k1 is class, v1 is array of columns to snag
|
486
550
|
next if chains[k1].nil?
|
487
551
|
|
@@ -544,7 +608,13 @@ module ActiveRecord
|
|
544
608
|
klass._br_hm_counts.each do |k, hm|
|
545
609
|
associative = nil
|
546
610
|
count_column = if hm.options[:through]
|
547
|
-
|
611
|
+
if (fk_col = (associative = klass._br_associatives&.[](hm.name))&.foreign_key)
|
612
|
+
if hm.source_reflection.macro == :belongs_to # Traditional HMT using an associative table
|
613
|
+
hm.foreign_key
|
614
|
+
else # A HMT that goes HM -> HM, something like Categories -> Products -> LineItems
|
615
|
+
hm.source_reflection.active_record.primary_key
|
616
|
+
end
|
617
|
+
end
|
548
618
|
else
|
549
619
|
fk_col = hm.foreign_key
|
550
620
|
poly_type = hm.inverse_of.foreign_type if hm.options.key?(:as)
|
@@ -596,7 +666,8 @@ JOIN (SELECT #{selects.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}
|
|
596
666
|
end
|
597
667
|
where!(wheres) unless wheres.empty?
|
598
668
|
# Must parse the order_by and see if there are any symbols which refer to BT associations
|
599
|
-
# as they must be expanded to find the corresponding b_r_model__column
|
669
|
+
# or custom columns as they must be expanded to find the corresponding b_r_model__column
|
670
|
+
# or br_cc_column naming for each.
|
600
671
|
if order_by.present?
|
601
672
|
final_order_by = *order_by.each_with_object([]) do |v, s|
|
602
673
|
if v.is_a?(Symbol)
|
@@ -605,6 +676,8 @@ JOIN (SELECT #{selects.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}
|
|
605
676
|
bt_cols.values.each do |v1|
|
606
677
|
v1.each { |v2| s << "\"#{v2.last}\"" if v2.length > 1 }
|
607
678
|
end
|
679
|
+
elsif (cc_cols = klass._br_cust_cols[v])
|
680
|
+
cc_cols.first.each { |v1| s << "\"#{v1.last}\"" if v1.length > 1 }
|
608
681
|
else
|
609
682
|
s << v
|
610
683
|
end
|
@@ -641,7 +714,7 @@ JOIN (SELECT #{selects.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}
|
|
641
714
|
module_prefixes.unshift('') unless module_prefixes.first.blank?
|
642
715
|
module_name = module_prefixes[0..-2].join('::')
|
643
716
|
if (snp = ::Brick.config.sti_namespace_prefixes)&.key?("::#{module_name}::") || snp&.key?("#{module_name}::") ||
|
644
|
-
File.exist?(candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb'))
|
717
|
+
File.exist?(candidate_file = ::Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb'))
|
645
718
|
_brick_find_sti_class(type_name) # Find this STI class normally
|
646
719
|
else
|
647
720
|
# Build missing prefix modules if they don't yet exist
|
@@ -710,13 +783,17 @@ end
|
|
710
783
|
Module.class_exec do
|
711
784
|
alias _brick_const_missing const_missing
|
712
785
|
def const_missing(*args)
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
786
|
+
desired_classname = (self == Object) ? args.first.to_s : "#{name}::#{args.first}"
|
787
|
+
if ((is_defined = self.const_defined?(args.first)) && (possible = self.const_get(args.first)) && possible.name == desired_classname) ||
|
788
|
+
# Try to require the respective Ruby file
|
789
|
+
((filename = ActiveSupport::Dependencies.search_for_file(desired_classname.underscore) ||
|
790
|
+
(self != Object && ActiveSupport::Dependencies.search_for_file((desired_classname = args.first.to_s).underscore))
|
791
|
+
) && (require_dependency(filename) || true) &&
|
792
|
+
((possible = self.const_get(args.first)) && possible.name == desired_classname)
|
793
|
+
) ||
|
794
|
+
# If any class has turned up so far (and we're not in the middle of eager loading)
|
795
|
+
# then return what we've found.
|
796
|
+
(is_defined && !::Brick.is_eager_loading)
|
720
797
|
return possible
|
721
798
|
end
|
722
799
|
class_name = ::Brick.namify(args.first.to_s)
|
@@ -766,7 +843,7 @@ Module.class_exec do
|
|
766
843
|
(schema_name = [(singular_table_name = class_name.underscore),
|
767
844
|
(table_name = singular_table_name.pluralize),
|
768
845
|
::Brick.is_oracle ? class_name.upcase : class_name,
|
769
|
-
(plural_class_name = class_name.pluralize)].find { |s| Brick.db_schemas
|
846
|
+
(plural_class_name = class_name.pluralize)].find { |s| Brick.db_schemas&.include?(s) }&.camelize ||
|
770
847
|
(::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}::") && class_name))
|
771
848
|
return self.const_get(schema_name) if self.const_defined?(schema_name)
|
772
849
|
|
@@ -789,7 +866,7 @@ Module.class_exec do
|
|
789
866
|
# module_prefixes = type_name.split('::')
|
790
867
|
# path = base_module.name.split('::')[0..-2] + []
|
791
868
|
# module_prefixes.unshift('') unless module_prefixes.first.blank?
|
792
|
-
# candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb')
|
869
|
+
# candidate_file = ::Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb')
|
793
870
|
base_module._brick_const_missing(*args)
|
794
871
|
# elsif base_module != Object
|
795
872
|
# module_parent.const_missing(*args)
|
@@ -811,7 +888,7 @@ class Object
|
|
811
888
|
schema_name = [(singular_schema_name = base_name.underscore),
|
812
889
|
(schema_name = singular_schema_name.pluralize),
|
813
890
|
base_name,
|
814
|
-
base_name.pluralize].find { |s| Brick.db_schemas
|
891
|
+
base_name.pluralize].find { |s| Brick.db_schemas&.include?(s) }
|
815
892
|
end
|
816
893
|
plural_class_name = ActiveSupport::Inflector.pluralize(model_name = class_name)
|
817
894
|
# If it's namespaced then we turn the first part into what would be a schema name
|
@@ -1200,22 +1277,6 @@ class Object
|
|
1200
1277
|
return
|
1201
1278
|
end
|
1202
1279
|
|
1203
|
-
# Normal (non-swagger) request
|
1204
|
-
|
1205
|
-
# We do all of this now so that bt_descrip and hm_counts are available on the model early in case the user
|
1206
|
-
# wants to do an ORDER BY based on any of that
|
1207
|
-
translations = {}
|
1208
|
-
join_array = ::Brick::JoinArray.new
|
1209
|
-
is_add_bts = is_add_hms = true
|
1210
|
-
# This builds out bt_descrip and hm_counts on the model
|
1211
|
-
model._brick_calculate_bts_hms(translations, join_array) if is_add_bts || is_add_hms
|
1212
|
-
|
1213
|
-
# %%% Allow params to define which columns to use for order_by
|
1214
|
-
# Overriding the default by providing a querystring param?
|
1215
|
-
ordering = params['_brick_order']&.split(',')&.map(&:to_sym) || Object.send(:default_ordering, table_name, pk)
|
1216
|
-
order_by, _ = model._brick_calculate_ordering(ordering, true) # Don't do the txt part
|
1217
|
-
|
1218
|
-
::Brick.set_db_schema(params)
|
1219
1280
|
if request.format == :csv # Asking for a template?
|
1220
1281
|
require 'csv'
|
1221
1282
|
exported_csv = CSV.generate(force_quotes: false) do |csv_out|
|
@@ -1229,7 +1290,16 @@ class Object
|
|
1229
1290
|
return
|
1230
1291
|
end
|
1231
1292
|
|
1232
|
-
|
1293
|
+
# Normal (not swagger or CSV) request
|
1294
|
+
|
1295
|
+
# %%% Allow params to define which columns to use for order_by
|
1296
|
+
# Overriding the default by providing a querystring param?
|
1297
|
+
ordering = params['_brick_order']&.split(',')&.map(&:to_sym) || Object.send(:default_ordering, table_name, pk)
|
1298
|
+
order_by, _ = model._brick_calculate_ordering(ordering, true) # Don't do the txt part
|
1299
|
+
|
1300
|
+
@_brick_params = (ar_relation = model.all).brick_select(params, (selects = []), nil,
|
1301
|
+
translations = {},
|
1302
|
+
join_array = ::Brick::JoinArray.new)
|
1233
1303
|
# %%% Add custom HM count columns
|
1234
1304
|
# %%% What happens when the PK is composite?
|
1235
1305
|
counts = model._br_hm_counts.each_with_object([]) do |v, s|
|
@@ -1434,6 +1504,27 @@ module ActiveRecord::ConnectionHandling
|
|
1434
1504
|
alias _brick_establish_connection establish_connection
|
1435
1505
|
def establish_connection(*args)
|
1436
1506
|
conn = _brick_establish_connection(*args)
|
1507
|
+
# Overwrite SQLite's #begin_db_transaction so it opens in IMMEDIATE mode instead of
|
1508
|
+
# the default DEFERRED mode.
|
1509
|
+
# https://discuss.rubyonrails.org/t/failed-write-transaction-upgrades-in-sqlite3/81480/2
|
1510
|
+
if ActiveRecord::Base.connection.adapter_name == 'SQLite'
|
1511
|
+
arca = ::ActiveRecord::ConnectionAdapters
|
1512
|
+
db_statements = arca::SQLite3::DatabaseStatements
|
1513
|
+
# Rails 7.1 and later
|
1514
|
+
if arca::AbstractAdapter.private_instance_methods.include?(:with_raw_connection)
|
1515
|
+
db_statements.define_method(:begin_db_transaction) do
|
1516
|
+
log("begin immediate transaction", "TRANSACTION") do
|
1517
|
+
with_raw_connection(allow_retry: true, uses_transaction: false) do |conn|
|
1518
|
+
conn.transaction(:immediate)
|
1519
|
+
end
|
1520
|
+
end
|
1521
|
+
end
|
1522
|
+
else # Rails < 7.1
|
1523
|
+
db_statements.define_method(:begin_db_transaction) do
|
1524
|
+
log('begin immediate transaction', 'TRANSACTION') { @connection.transaction(:immediate) }
|
1525
|
+
end
|
1526
|
+
end
|
1527
|
+
end
|
1437
1528
|
begin
|
1438
1529
|
_brick_reflect_tables
|
1439
1530
|
rescue ActiveRecord::NoDatabaseError
|
@@ -1447,13 +1538,13 @@ module ActiveRecord::ConnectionHandling
|
|
1447
1538
|
initializer_loaded = false
|
1448
1539
|
if (relations = ::Brick.relations).empty?
|
1449
1540
|
# If there's schema things configured then we only expect our initializer to be named exactly this
|
1450
|
-
if File.exist?(brick_initializer = Rails.root.join('config/initializers/brick.rb'))
|
1541
|
+
if File.exist?(brick_initializer = ::Rails.root.join('config/initializers/brick.rb'))
|
1451
1542
|
initializer_loaded = load brick_initializer
|
1452
1543
|
end
|
1453
1544
|
# Load the initializer for the Apartment gem a little early so that if .excluded_models and
|
1454
1545
|
# .default_schema are specified then we can work with non-tenanted models more appropriately
|
1455
1546
|
apartment = Object.const_defined?('Apartment')
|
1456
|
-
if apartment && File.exist?(apartment_initializer = Rails.root.join('config/initializers/apartment.rb'))
|
1547
|
+
if apartment && File.exist?(apartment_initializer = ::Rails.root.join('config/initializers/apartment.rb'))
|
1457
1548
|
load apartment_initializer
|
1458
1549
|
apartment_excluded = Apartment.excluded_models
|
1459
1550
|
end
|
@@ -1877,9 +1968,7 @@ module Brick
|
|
1877
1968
|
# For any appended references (those that come from config), arrive upon a definitely unique constraint name
|
1878
1969
|
pri_tbl = is_class ? fk[4][:class].underscore : pri_tbl
|
1879
1970
|
pri_tbl = "#{bt_assoc_name}_#{pri_tbl}" if pri_tbl&.singularize != bt_assoc_name
|
1880
|
-
|
1881
|
-
cnstr_added_num = 1
|
1882
|
-
cnstr_name = "#{cnstr_base}_#{cnstr_added_num += 1}" while bts&.key?(cnstr_name) || hms&.key?(cnstr_name)
|
1971
|
+
cnstr_name = ensure_unique(+"(brick) #{for_tbl}_#{pri_tbl}", bts, hms)
|
1883
1972
|
missing = []
|
1884
1973
|
missing << fk[1] unless relations.key?(fk[1])
|
1885
1974
|
missing << primary_table unless is_class || relations.key?(primary_table)
|
@@ -1980,14 +2069,7 @@ module Brick
|
|
1980
2069
|
end
|
1981
2070
|
end
|
1982
2071
|
end
|
1983
|
-
|
1984
|
-
::Rails.configuration.instance_variable_get(:@autoloader) == :classic
|
1985
|
-
Rails.configuration.eager_load_namespaces.select { |ns| ns < Rails::Application }.each(&:eager_load!)
|
1986
|
-
else
|
1987
|
-
Zeitwerk::Loader.eager_load_all
|
1988
|
-
end
|
1989
|
-
abstract_activerecord_bases = ActiveRecord::Base.descendants.select { |ar| ar.abstract_class? }.map(&:name)
|
1990
|
-
# abstract_activerecord_bases << ActiveRecord::Base
|
2072
|
+
abstract_activerecord_bases = ::Brick.eager_load_classes(true)
|
1991
2073
|
models = if Dir.exist?(model_path = "#{rails_root}/app/models")
|
1992
2074
|
Dir["#{model_path}/**/*.rb"].each_with_object({}) do |v, s|
|
1993
2075
|
File.read(v).split("\n").each do |line|
|
@@ -2011,6 +2093,28 @@ module Brick
|
|
2011
2093
|
::Brick.relations.keys.map { |v| [(r = v.pluralize), (model = models[r])&.last&.table_name || v, migrations&.fetch(r, nil), model&.first] }
|
2012
2094
|
end
|
2013
2095
|
|
2096
|
+
def ensure_unique(name, *sources)
|
2097
|
+
base = name
|
2098
|
+
if (added_num = name.slice!(/_(\d+)$/))
|
2099
|
+
added_num = added_num[1..-1].to_i
|
2100
|
+
else
|
2101
|
+
added_num = 1
|
2102
|
+
end
|
2103
|
+
while (
|
2104
|
+
name = "#{base}_#{added_num += 1}"
|
2105
|
+
sources.each_with_object(nil) do |v, s|
|
2106
|
+
s || case v
|
2107
|
+
when Hash
|
2108
|
+
v.key?(name)
|
2109
|
+
when Array
|
2110
|
+
v.include?(name)
|
2111
|
+
end
|
2112
|
+
end
|
2113
|
+
)
|
2114
|
+
end
|
2115
|
+
name
|
2116
|
+
end
|
2117
|
+
|
2014
2118
|
# Locate orphaned records
|
2015
2119
|
def find_orphans(multi_schema)
|
2016
2120
|
is_default_schema = multi_schema&.==(Apartment.default_schema)
|
@@ -33,6 +33,9 @@ module Brick
|
|
33
33
|
# Additional references (virtual foreign keys)
|
34
34
|
::Brick.additional_references = app.config.brick.fetch(:additional_references, nil)
|
35
35
|
|
36
|
+
# Custom columns to add to a table, minimally defined with a name and DSL string
|
37
|
+
::Brick.custom_columns = app.config.brick.fetch(:custom_columns, nil)
|
38
|
+
|
36
39
|
# When table names have specific prefixes, automatically place them in their own module with a table_name_prefix.
|
37
40
|
::Brick.order = app.config.brick.fetch(:order, {})
|
38
41
|
|
@@ -72,7 +75,6 @@ module Brick
|
|
72
75
|
def set_brick_model(find_args)
|
73
76
|
# Need to return true if we can fill in the blanks for a missing one
|
74
77
|
# args will be something like: ["index", ["categories"]]
|
75
|
-
find_args[1] = find_args[1].each_with_object([]) { |a, s| s.concat(a.split('/')) }
|
76
78
|
if (class_name = find_args[1].last&.singularize)
|
77
79
|
find_args[1][find_args[1].length - 1] = class_name # Make sure the last item, defining the class name, is singular
|
78
80
|
if (model = find_args[1].map(&:camelize).join('::').constantize) && (
|
@@ -100,10 +102,13 @@ module Brick
|
|
100
102
|
def find_template(*args, **options)
|
101
103
|
unless (model_name = @_brick_model&.name) ||
|
102
104
|
(is_status = ::Brick.config.add_status && args[0..1] == ['status', ['brick_gem']]) ||
|
103
|
-
(is_orphans = ::Brick.config.add_orphans && args[0..1] == ['orphans', ['brick_gem']])
|
104
|
-
|
105
|
-
|
106
|
-
|
105
|
+
(is_orphans = ::Brick.config.add_orphans && args[0..1] == ['orphans', ['brick_gem']])
|
106
|
+
if (possible_template = _brick_find_template(*args, **options))
|
107
|
+
return possible_template
|
108
|
+
else
|
109
|
+
# Used to also have: ActionView.version < ::Gem::Version.new('5.0') &&
|
110
|
+
model_name = (args[1].is_a?(Array) ? set_brick_model(args) : nil)&.name
|
111
|
+
end
|
107
112
|
end
|
108
113
|
|
109
114
|
if @_brick_model
|
@@ -247,8 +252,8 @@ tr th {
|
|
247
252
|
right: 0;
|
248
253
|
cursor: pointer;
|
249
254
|
}
|
250
|
-
#headerTop tr th:hover {
|
251
|
-
background-color: #
|
255
|
+
#headerTop tr th:hover, #headerTop tr th.highlight {
|
256
|
+
background-color: #28B898;
|
252
257
|
}
|
253
258
|
#exclusions {
|
254
259
|
font-size: 0.7em;
|
@@ -271,6 +276,10 @@ tr th, tr td {
|
|
271
276
|
padding: 0.2em 0.5em;
|
272
277
|
}
|
273
278
|
|
279
|
+
tr td.highlight {
|
280
|
+
background-color: #B0B0FF;
|
281
|
+
}
|
282
|
+
|
274
283
|
.show-field {
|
275
284
|
background-color: #004998;
|
276
285
|
}
|
@@ -498,6 +507,34 @@ function changeout(href, param, value, trimAfter) {
|
|
498
507
|
var grid = document.getElementById(\"#{table_name}\");
|
499
508
|
#{table_name}HtColumns = grid && [grid.getElementsByTagName(\"TR\")[0]];
|
500
509
|
var headerTop = document.getElementById(\"headerTop\");
|
510
|
+
var headerCols;
|
511
|
+
if (grid) {
|
512
|
+
// COLUMN HEADER AND TABLE CELL HIGHLIGHTING
|
513
|
+
var gridHighHeader = null,
|
514
|
+
gridHighCell = null;
|
515
|
+
grid.addEventListener(\"mouseenter\", gridMove);
|
516
|
+
grid.addEventListener(\"mousemove\", gridMove);
|
517
|
+
grid.addEventListener(\"mouseleave\", function (evt) {
|
518
|
+
if (gridHighCell) gridHighCell.classList.remove(\"highlight\");
|
519
|
+
gridHighCell = null;
|
520
|
+
if (gridHighHeader) gridHighHeader.classList.remove(\"highlight\");
|
521
|
+
gridHighHeader = null;
|
522
|
+
});
|
523
|
+
function gridMove(evt) {
|
524
|
+
var lastHighCell = gridHighCell;
|
525
|
+
gridHighCell = document.elementFromPoint(evt.x, evt.y);
|
526
|
+
if (lastHighCell !== gridHighCell) {
|
527
|
+
gridHighCell.classList.add(\"highlight\");
|
528
|
+
if (lastHighCell) lastHighCell.classList.remove(\"highlight\");
|
529
|
+
}
|
530
|
+
var lastHighHeader = gridHighHeader;
|
531
|
+
gridHighHeader = headerCols[gridHighCell.cellIndex];
|
532
|
+
if (lastHighHeader !== gridHighHeader) {
|
533
|
+
if (gridHighHeader) gridHighHeader.classList.add(\"highlight\");
|
534
|
+
if (lastHighHeader) lastHighHeader.classList.remove(\"highlight\");
|
535
|
+
}
|
536
|
+
}
|
537
|
+
}
|
501
538
|
function setHeaderSizes() {
|
502
539
|
// console.log(\"start\");
|
503
540
|
// See if the headerTop is already populated
|
@@ -529,6 +566,7 @@ function setHeaderSizes() {
|
|
529
566
|
}
|
530
567
|
}
|
531
568
|
}
|
569
|
+
headerCols = tr.childNodes;
|
532
570
|
if (isEmpty) headerTop.appendChild(tr);
|
533
571
|
}
|
534
572
|
grid.style.marginTop = \"-\" + getComputedStyle(headerTop).height;
|
@@ -777,7 +815,8 @@ erDiagram
|
|
777
815
|
cols[col_name] = col
|
778
816
|
end
|
779
817
|
unless @_brick_sequence # If no sequence is defined, start with all inclusions
|
780
|
-
|
818
|
+
cust_cols = #{model_name}._br_cust_cols
|
819
|
+
@_brick_sequence = col_keys + cust_cols.keys + #{(hms_keys).inspect}.reject { |assoc_name| @_brick_incl&.exclude?(assoc_name) }
|
781
820
|
end
|
782
821
|
@_brick_sequence.reject! { |nm| @_brick_excl.include?(nm) } if @_brick_excl # Reject exclusions
|
783
822
|
@_brick_sequence.each_with_object(+'') do |col_name, s|
|
@@ -794,6 +833,8 @@ erDiagram
|
|
794
833
|
elsif col # HM column
|
795
834
|
s << \"<th#\{' x-order=\"' + col_name + '\"' if true}>#\{col[2]} \"
|
796
835
|
s << (col.first ? \"#\{col[3]}\" : \"#\{link_to(col[3], send(\"#\{col[1]._brick_index}_path\"))}\")
|
836
|
+
elsif (cc = cust_cols.key?(col_name)) # Custom column
|
837
|
+
s << \"<th x-order=\\\"#\{col_name}\\\">#\{col_name}\"
|
797
838
|
else # Bad column name!
|
798
839
|
s << \"<th title=\\\"<< Unknown column >>\\\">#\{col_name}\"
|
799
840
|
end
|
@@ -811,7 +852,7 @@ erDiagram
|
|
811
852
|
<td><%= link_to '⇛', #{path_obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
|
812
853
|
<% @_brick_sequence.each do |col_name|
|
813
854
|
val = #{obj_name}.attributes[col_name] %>
|
814
|
-
<td<%= ' class=\"dimmed\"'.html_safe unless cols.key?(col_name)%>><%
|
855
|
+
<td<%= ' class=\"dimmed\"'.html_safe unless cols.key?(col_name) || (cust_col = cust_cols[col_name])%>><%
|
815
856
|
if (bt = bts[col_name])
|
816
857
|
if bt[2] # Polymorphic?
|
817
858
|
bt_class = #{obj_name}.send(\"#\{bt.first\}_type\")
|
@@ -845,6 +886,9 @@ erDiagram
|
|
845
886
|
elsif (col = cols[col_name])
|
846
887
|
col_type = col&.sql_type == 'geography' ? col.sql_type : col&.type
|
847
888
|
%><%= display_value(col_type || col&.sql_type, val) %><%
|
889
|
+
elsif cust_col
|
890
|
+
data = cust_col.first.map { |cc_part| #{obj_name}.send(cc_part.last) }
|
891
|
+
%><%= #{model_name}.brick_descrip(cust_col.last, data) %><%
|
848
892
|
else # Bad column name!
|
849
893
|
%>?<%
|
850
894
|
end
|
data/lib/brick/version_number.rb
CHANGED
data/lib/brick.rb
CHANGED
@@ -123,7 +123,7 @@ module Brick
|
|
123
123
|
end
|
124
124
|
|
125
125
|
class << self
|
126
|
-
attr_accessor :default_schema, :db_schemas, :routes_done, :is_oracle
|
126
|
+
attr_accessor :default_schema, :db_schemas, :routes_done, :is_oracle, :is_eager_loading
|
127
127
|
|
128
128
|
def set_db_schema(params = nil)
|
129
129
|
schema = (params ? params['_brick_schema'] : ::Brick.default_schema) || 'public'
|
@@ -329,6 +329,15 @@ module Brick
|
|
329
329
|
end
|
330
330
|
end
|
331
331
|
|
332
|
+
# Custom columns to add to a table, minimally defined with a name and DSL string.
|
333
|
+
# @api public
|
334
|
+
def custom_columns=(cust_cols)
|
335
|
+
if cust_cols
|
336
|
+
cust_cols = cust_cols.call if cust_cols.is_a?(Proc)
|
337
|
+
Brick.config.custom_columns = cust_cols
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
332
341
|
# @api public
|
333
342
|
def order=(value)
|
334
343
|
Brick.config.order = value
|
@@ -495,6 +504,21 @@ In config/initializers/brick.rb appropriate entries would look something like:
|
|
495
504
|
VERSION::STRING
|
496
505
|
end
|
497
506
|
|
507
|
+
def eager_load_classes(do_ar_abstract_bases = false)
|
508
|
+
::Brick.is_eager_loading = true
|
509
|
+
if ::ActiveSupport.version < ::Gem::Version.new('6') ||
|
510
|
+
::Rails.configuration.instance_variable_get(:@autoloader) == :classic
|
511
|
+
::Rails.configuration.eager_load_namespaces.select { |ns| ns < ::Rails::Application }.each(&:eager_load!)
|
512
|
+
else
|
513
|
+
Zeitwerk::Loader.eager_load_all
|
514
|
+
end
|
515
|
+
abstract_ar_bases = if do_ar_abstract_bases
|
516
|
+
ActiveRecord::Base.descendants.select { |ar| ar.abstract_class? }.map(&:name)
|
517
|
+
end
|
518
|
+
::Brick.is_eager_loading = false
|
519
|
+
abstract_ar_bases
|
520
|
+
end
|
521
|
+
|
498
522
|
def display_classes(rels, max_length)
|
499
523
|
rels.sort.each do |rel|
|
500
524
|
puts "#{rel.first}#{' ' * (max_length - rel.first.length)} /#{rel.last}"
|
@@ -593,12 +617,12 @@ require 'active_record/relation/query_methods' if ActiveRecord.version < ::Gem::
|
|
593
617
|
require 'rails/railtie' if ActiveRecord.version < ::Gem::Version.new('4.2')
|
594
618
|
|
595
619
|
# Rake tasks
|
596
|
-
class Railtie < Rails::Railtie
|
620
|
+
class Railtie < ::Rails::Railtie
|
597
621
|
Dir.glob("#{File.expand_path(__dir__)}/brick/tasks/**/*.rake").each { |task| load task }
|
598
622
|
end
|
599
623
|
|
600
624
|
# Rails < 4.2 does not have env
|
601
|
-
module Rails
|
625
|
+
module ::Rails
|
602
626
|
unless respond_to?(:env)
|
603
627
|
def self.env
|
604
628
|
@_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development")
|
@@ -207,6 +207,12 @@ module Brick
|
|
207
207
|
# # to be the primary key.)
|
208
208
|
#{bar}
|
209
209
|
|
210
|
+
# # Custom columns to add to a table, minimally defined with a name and DSL string.
|
211
|
+
# Brick.custom_columns = { 'users' => { messages: ['[COUNT(messages)] messages', 'messages'] },
|
212
|
+
# 'orders' => { salesperson: '[salesperson.first] [salesperson.last]',
|
213
|
+
# products: ['[COUNT(order_items.product)] products', 'order_items.product' ] }
|
214
|
+
# }
|
215
|
+
|
210
216
|
# # Skip creating a has_many association for these (only retain the belongs_to built from this additional_reference).
|
211
217
|
# # (Uses the same exact three-part format as would define an additional_reference)
|
212
218
|
# # Say for instance that we didn't care to display the favourite colours that users have:
|
@@ -55,7 +55,7 @@ module Brick
|
|
55
55
|
# If Apartment is active, see if a default schema to analyse is indicated
|
56
56
|
|
57
57
|
# # Load all models
|
58
|
-
#
|
58
|
+
# ::Brick.eager_load_classes
|
59
59
|
|
60
60
|
if (tables = ::Brick.relations.reject { |k, v| v.key?(:isView) && v[:isView] == true }.map(&:first).sort).empty?
|
61
61
|
puts "No tables found in database #{ActiveRecord::Base.connection.current_database}."
|
@@ -16,12 +16,7 @@ module Brick
|
|
16
16
|
# %%% If Apartment is active and there's no schema_to_analyse, ask which schema they want
|
17
17
|
|
18
18
|
# Load all models
|
19
|
-
|
20
|
-
::Rails.configuration.instance_variable_get(:@autoloader) == :classic
|
21
|
-
Rails.configuration.eager_load_namespaces.select { |ns| ns < Rails::Application }.each(&:eager_load!)
|
22
|
-
else
|
23
|
-
Zeitwerk::Loader.eager_load_all
|
24
|
-
end
|
19
|
+
::Brick.eager_load_classes
|
25
20
|
|
26
21
|
# Generate a list of viable models that can be chosen
|
27
22
|
longest_length = 0
|
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.76
|
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-10-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|