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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7f077a36e2fd62546cdebfde2b9651807ceeaad9571008460423d9e994df44d
4
- data.tar.gz: 368ca31e726fa377325b7ff7838e38cd6b79aa222bcec73cdd6cebc645908d28
3
+ metadata.gz: 615640b22db113a3959644ee9c2d08ff3a2b37aa10f12dc92d68effef091c228
4
+ data.tar.gz: b0d3e616b0f44584cf6960f8b8b191fc449b9afc426a6486da11c2deadece7a6
5
5
  SHA512:
6
- metadata.gz: a4154837ca3926cb1cd8d4803d62a789f1aa6449625560c63c06708508543e218765288d13a763aab238ea946dfdc33391454793b02805a8cdf46f729cf436da
7
- data.tar.gz: 599cd989ef47a99cb0b6b4cb1a1c53da892d5758ccd208ecb4bf1383a2f29c1bc4600458c4de357e5496f90f664984c10634f469a40f80bcc89a9e5592735899
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 }
@@ -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 = {}, emit_dsl = false, is_polymorphic = false)
104
- build_array = ::Brick::JoinArray.new.tap { |ary| ary.replace([build_array]) } if build_array.is_a?(::Brick::JoinHash)
105
- build_array = ::Brick::JoinArray.new unless build_array.nil? || build_array.is_a?(Array)
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
- prefix = [prefix] unless prefix.is_a?(Array)
109
- if (dsl = ::Brick.config.model_descrips[name] || brick_get_dsl)
110
- dsl2 = +'' # To replace our own DSL definition in case it needs to be expanded
111
- dsl3 = +'' # To return expanded DSL that is nested from another model
112
- klass = nil
113
- dsl.each_char do |ch|
114
- if bracket_name
115
- if ch == ']' # Time to process a bracketed thing?
116
- parts = bracket_name.split('.')
117
- first_parts = parts[0..-2].map do |part|
118
- klass = (orig_class = klass).reflect_on_association(part_sym = part.to_sym)&.klass
119
- puts "Couldn't reference #{orig_class.name}##{part} that's part of the DSL \"#{dsl}\"." if klass.nil?
120
- part_sym
121
- end
122
- parts = prefix + first_parts + [parts[-1]]
123
- if parts.length > 1
124
- unless is_polymorphic
125
- s = build_array
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
- if klass.column_names.exclude?(parts.last) &&
132
- (klass = (orig_class = klass).reflect_on_association(possible_dsl = parts.pop.to_sym)&.klass)
133
- # Expand this entry which refers to an association name
134
- members2, dsl2a = klass.brick_parse_dsl(build_array, prefix + [possible_dsl], translations, true)
135
- members += members2
136
- dsl2 << dsl2a
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
- bracket_name = nil
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
- bracket_name << ch
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
- elsif ch == '['
150
- bracket_name = +''
151
- klass = self
157
+ bracket_name = nil
152
158
  else
153
- dsl2 << ch
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
- emit_dsl ? [members, dsl3] : members
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
- if (dsl = ::Brick.config.model_descrips[(klass = self).name] || klass.brick_get_dsl)
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 two params here: "false" is for don't emit DSL, and "true" is for yes, we are polymorphic
286
- bt[1].each_with_object({}) { |bt_class, s| s[bt_class] = bt_class.brick_parse_dsl(join_array, bt.first, translations, false, true) }
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 = nil, order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new)
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&.empty? # Default to all columns
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
- bt_columns = klass._br_bt_descrip.each_with_object([]) do |v, s|
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
- hm.foreign_key if (fk_col = (associative = klass._br_associatives&.[](hm.name))&.foreign_key)
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 naming for each.
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
- if (self.const_defined?(args.first) && (possible = self.const_get(args.first)) && possible != self) ||
714
- (self != Object && Object.const_defined?(args.first) &&
715
- (
716
- (possible = Object.const_get(args.first)) &&
717
- (possible != self || (possible == self && possible.is_a?(Class)))
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.include?(s) }&.camelize ||
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.include?(s) }
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
- @_brick_params = (ar_relation = model.all).brick_select(params, (selects = []), order_by, translations, join_array)
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
- cnstr_base = cnstr_name = "(brick) #{for_tbl}_#{pri_tbl}"
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
- if ::ActiveSupport.version < ::Gem::Version.new('6') ||
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
- # Used to also have: ActionView.version < ::Gem::Version.new('5.0') &&
105
- (model_name = (args[1].is_a?(Array) ? set_brick_model(args) : nil)&.name)
106
- return _brick_find_template(*args, **options)
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: #18B090;
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
- @_brick_sequence = col_keys + #{(hms_keys).inspect}.reject { |assoc_name| @_brick_incl&.exclude?(assoc_name) }
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
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 74
8
+ TINY = 76
9
9
 
10
10
  # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
11
  PRE = nil
data/lib/brick.rb CHANGED
@@ -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
- # Rails.configuration.eager_load_namespaces.select { |ns| ns < Rails::Application }.each(&:eager_load!)
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
- if ::ActiveSupport.version < ::Gem::Version.new('6') ||
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.74
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-09-27 00:00:00.000000000 Z
11
+ date: 2022-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord