brick 1.0.75 → 1.0.77

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: acd5fdf68935981a46106c879f5ac1fb44ac790c269c3eb2c5429cd67880f916
4
- data.tar.gz: 260ce60eeaf2e4b17ca5b5e76e54960c4d2685c9cb9d8549a35a16ce49b5a36c
3
+ metadata.gz: 7e113470d473585f716cfa5b835f503448a0d160b60de20f884bf3ce2cc14c08
4
+ data.tar.gz: 40064f980dd6893cb4e7e47861e00b184983d1a4bbc11b7cbd16fd6caddfab87
5
5
  SHA512:
6
- metadata.gz: 00f45ff020fcd729c2115f6d9f6ab0ab588247b2d727de7e1bd6acf53fa685ef76eeecce181a7a0c4659d8156971f88466915749cfad44e993993179435acdbd
7
- data.tar.gz: 74f0b40c44bae3729624ca0ba6a378f9067e4b0aa98460a03aa193e292432576327991d8c2a107317d8ce51ce223eee780a0d632d5805ca14b91a1b55388259f
6
+ metadata.gz: 4219dfee478a845a949b1ac9b03d0a135504723a892b571ca068af70c2b58ca52025f5e1f2fedb6a97a5b2dcd97280c5f54132b854e285e6daf7bc95d0c2d357
7
+ data.tar.gz: 67ae65b0de3801bbb2a1817d16b4023116ee7d41491b6d45927bd8197a8f800151f8ce79de34732a55f3dfcabbe301dc69b276899cfd544604d6a64e61220c99
data/lib/brick/config.rb CHANGED
@@ -20,6 +20,15 @@ module Brick
20
20
  @serializer = Brick::Serializers::YAML
21
21
  end
22
22
 
23
+ # Any path prefixing to apply to all auto-generated Brick routes
24
+ def path_prefix
25
+ @mutex.synchronize { @path_prefix }
26
+ end
27
+
28
+ def path_prefix=(path)
29
+ @mutex.synchronize { @path_prefix = path }
30
+ end
31
+
23
32
  # Indicates whether Brick models are on or off. Default: true.
24
33
  def enable_models
25
34
  @mutex.synchronize { !!@enable_models }
@@ -90,6 +99,15 @@ module Brick
90
99
  @mutex.synchronize { @additional_references = references }
91
100
  end
92
101
 
102
+ # Custom columns to add to a table, minimally defined with a name and DSL string
103
+ def custom_columns
104
+ @mutex.synchronize { @custom_columns }
105
+ end
106
+
107
+ def custom_columns=(cust_cols)
108
+ @mutex.synchronize { @custom_columns = cust_cols }
109
+ end
110
+
93
111
  # Skip creating a has_many association for these
94
112
  def exclude_hms
95
113
  @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 = +''
@@ -242,12 +256,13 @@ module ActiveRecord
242
256
  table_name == assoc_name ? link : "#{assoc_name}-#{link}".html_safe
243
257
  end
244
258
 
245
- def self._brick_index
246
- tbl_parts = table_name.split('.')
259
+ def self._brick_index(mode = nil)
260
+ tbl_parts = ((mode == :singular) ? table_name.singularize : table_name).split('.')
247
261
  tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.first == Apartment.default_schema
248
- if (index = tbl_parts.map(&:underscore).join('_')) == index.singularize
249
- index << '_index' # Rails applies an _index suffix to that route when the resource name is singular
250
- end
262
+ tbl_parts.unshift(::Brick.config.path_prefix) if ::Brick.config.path_prefix
263
+ index = tbl_parts.map(&:underscore).join('_')
264
+ # Rails applies an _index suffix to that route when the resource name is singular
265
+ index << '_index' if mode != :singular && index == index.singularize
251
266
  index
252
267
  end
253
268
 
@@ -272,18 +287,28 @@ module ActiveRecord
272
287
  def _br_associatives
273
288
  @_br_associatives ||= {}
274
289
  end
290
+ # Custom columns
291
+ def _br_cust_cols
292
+ @_br_cust_cols ||= {}
293
+ end
275
294
  end
276
295
 
277
- # Search for BT, HM, and HMT DSL stuff
296
+ # Search for custom column, BT, HM, and HMT DSL stuff
278
297
  def self._brick_calculate_bts_hms(translations, join_array)
298
+ # Add any custom columns
299
+ ::Brick.config.custom_columns&.fetch(table_name, nil)&.each do |k, cc|
300
+ # false = not polymorphic, and true = yes -- please emit_dsl
301
+ pieces, my_dsl = brick_parse_dsl(join_array, [], translations, false, cc, true)
302
+ _br_cust_cols[k] = [pieces, my_dsl]
303
+ end
279
304
  bts, hms, associatives = ::Brick.get_bts_and_hms(self)
280
305
  bts.each do |_k, bt|
281
306
  next if bt[2] # Polymorphic?
282
307
 
283
308
  # join_array will receive this relation name when calling #brick_parse_dsl
284
309
  _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) }
310
+ # Last params here: "true" is for yes, we are polymorphic
311
+ bt[1].each_with_object({}) { |bt_class, s| s[bt_class] = bt_class.brick_parse_dsl(join_array, bt.first, translations, true) }
287
312
  else
288
313
  { bt.last => bt[1].brick_parse_dsl(join_array, bt.first, translations) }
289
314
  end
@@ -314,7 +339,7 @@ module ActiveRecord
314
339
  else # Expecting only Symbol
315
340
  if _br_hm_counts.key?(ord_part)
316
341
  ord_part = "\"b_r_#{ord_part}_ct\""
317
- elsif !_br_bt_descrip.key?(ord_part) && !column_names.include?(ord_part.to_s)
342
+ elsif !_br_bt_descrip.key?(ord_part) && !_br_cust_cols.key?(ord_part) && !column_names.include?(ord_part.to_s)
318
343
  # Disallow ordering by a bogus column
319
344
  # %%% Note this bogus entry so that Javascript can remove any bogus _brick_order
320
345
  # parameter from the querystring, pushing it into the browser history.
@@ -331,6 +356,11 @@ module ActiveRecord
331
356
  [order_by, order_by_txt]
332
357
  end
333
358
 
359
+ def self.brick_select(params = {}, selects = [], *args)
360
+ (relation = all).brick_select(params, selects, *args)
361
+ relation.select(selects)
362
+ end
363
+
334
364
  private
335
365
 
336
366
  def self._brick_get_fks
@@ -421,7 +451,13 @@ module ActiveRecord
421
451
  end
422
452
  end
423
453
 
424
- def brick_select(params, selects = nil, order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new)
454
+ def brick_select(params, selects = [], order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new)
455
+ is_add_bts = is_add_hms = true
456
+
457
+ # Build out cust_cols, bt_descrip and hm_counts now so that they are available on the
458
+ # model early in case the user wants to do an ORDER BY based on any of that.
459
+ model._brick_calculate_bts_hms(translations, join_array) if is_add_bts || is_add_hms
460
+
425
461
  is_postgres = ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
426
462
  is_mysql = ActiveRecord::Base.connection.adapter_name == 'Mysql2'
427
463
  is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer'
@@ -446,7 +482,7 @@ module ActiveRecord
446
482
  end
447
483
 
448
484
  # %%% Skip the metadata columns
449
- if selects&.empty? # Default to all columns
485
+ if selects.empty? # Default to all columns
450
486
  tbl_no_schema = table.name.split('.').last
451
487
  # %%% Have once gotten this error with MSSQL referring to http://localhost:3000/warehouse/cold_room_temperatures__archive
452
488
  # ActiveRecord::StatementInvalid (TinyTds::Error: DBPROCESS is dead or not enabled)
@@ -481,7 +517,36 @@ module ActiveRecord
481
517
  id_for_tables = Hash.new { |h, k| h[k] = [] }
482
518
  field_tbl_names = Hash.new { |h, k| h[k] = {} }
483
519
  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|
520
+
521
+ # CUSTOM COLUMNS
522
+ # ==============
523
+ klass._br_cust_cols.each do |k, cc|
524
+ if respond_to?(k) # Name already taken?
525
+ # %%% Use ensure_unique here in this kind of fashion:
526
+ # cnstr_name = ensure_unique(+"(brick) #{for_tbl}_#{pri_tbl}", bts, hms)
527
+ # binding.pry
528
+ next
529
+ end
530
+
531
+ cc.first.each do |cc_part|
532
+ dest_klass = cc_part[0..-2].inject(klass) { |kl, cc_part_term| kl.reflect_on_association(cc_part_term).klass }
533
+ tbl_name = (field_tbl_names[k][cc_part.last] ||= shift_or_first(chains[dest_klass])).split('.').last
534
+ # Deal with the conflict if there are two parts in the custom column named the same,
535
+ # "category.name" and "product.name" for instance will end up with aliases of "name"
536
+ # and "product__name".
537
+ cc_part_idx = cc_part.length - 1
538
+ while cc_part_idx > 0 &&
539
+ (col_alias = "br_cc_#{k}__#{cc_part[cc_part_idx..-1].map(&:to_s).join('__')}") &&
540
+ used_col_aliases.key?(col_alias)
541
+ cc_part_idx -= 1
542
+ end
543
+ selects << "#{tbl_name}.#{cc_part.last} AS #{col_alias}"
544
+ cc_part << col_alias
545
+ used_col_aliases[col_alias] = nil
546
+ end
547
+ end
548
+
549
+ klass._br_bt_descrip.each do |v|
485
550
  v.last.each do |k1, v1| # k1 is class, v1 is array of columns to snag
486
551
  next if chains[k1].nil?
487
552
 
@@ -544,7 +609,13 @@ module ActiveRecord
544
609
  klass._br_hm_counts.each do |k, hm|
545
610
  associative = nil
546
611
  count_column = if hm.options[:through]
547
- hm.foreign_key if (fk_col = (associative = klass._br_associatives&.[](hm.name))&.foreign_key)
612
+ if (fk_col = (associative = klass._br_associatives&.[](hm.name))&.foreign_key)
613
+ if hm.source_reflection.macro == :belongs_to # Traditional HMT using an associative table
614
+ hm.foreign_key
615
+ else # A HMT that goes HM -> HM, something like Categories -> Products -> LineItems
616
+ hm.source_reflection.active_record.primary_key
617
+ end
618
+ end
548
619
  else
549
620
  fk_col = hm.foreign_key
550
621
  poly_type = hm.inverse_of.foreign_type if hm.options.key?(:as)
@@ -596,7 +667,8 @@ JOIN (SELECT #{selects.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}
596
667
  end
597
668
  where!(wheres) unless wheres.empty?
598
669
  # 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.
670
+ # or custom columns as they must be expanded to find the corresponding b_r_model__column
671
+ # or br_cc_column naming for each.
600
672
  if order_by.present?
601
673
  final_order_by = *order_by.each_with_object([]) do |v, s|
602
674
  if v.is_a?(Symbol)
@@ -605,6 +677,8 @@ JOIN (SELECT #{selects.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}
605
677
  bt_cols.values.each do |v1|
606
678
  v1.each { |v2| s << "\"#{v2.last}\"" if v2.length > 1 }
607
679
  end
680
+ elsif (cc_cols = klass._br_cust_cols[v])
681
+ cc_cols.first.each { |v1| s << "\"#{v1.last}\"" if v1.length > 1 }
608
682
  else
609
683
  s << v
610
684
  end
@@ -710,50 +784,60 @@ end
710
784
  Module.class_exec do
711
785
  alias _brick_const_missing const_missing
712
786
  def const_missing(*args)
713
- desired_classname = (self == Object) ? args.first.to_s : "#{name}::#{args.first}"
787
+ requested = args.first.to_s
788
+ is_controller = requested.end_with?('Controller')
789
+ # self.name is nil when a model name is requested in an .erb file
790
+ if self.name && ::Brick.config.path_prefix
791
+ camelize_prefix = ::Brick.config.path_prefix.camelize
792
+ # Asking for the prefix module?
793
+ if self == Object && requested == camelize_prefix
794
+ Object.const_set(args.first, (built_module = Module.new))
795
+ puts "module #{camelize_prefix}; end\n"
796
+ return built_module
797
+ end
798
+ split_self_name.shift if (split_self_name = self.name.split('::')).first.blank?
799
+ if split_self_name.first == camelize_prefix
800
+ split_self_name.shift # Remove the identified path prefix from the split name
801
+ if is_controller
802
+ brick_root = split_self_name.empty? ? self : camelize_prefix.constantize
803
+ end
804
+ end
805
+ end
806
+ base_module = if self < ActiveRecord::Migration || !self.name
807
+ brick_root || Object
808
+ elsif (split_self_name || self.name.split('::')).length > 1
809
+ return self._brick_const_missing(*args)
810
+ else
811
+ self
812
+ end
813
+ desired_classname = (self == Object) ? requested : "#{name}::#{requested}"
714
814
  if ((is_defined = self.const_defined?(args.first)) && (possible = self.const_get(args.first)) && possible.name == desired_classname) ||
715
815
  # Try to require the respective Ruby file
716
816
  ((filename = ActiveSupport::Dependencies.search_for_file(desired_classname.underscore) ||
717
- (self != Object && ActiveSupport::Dependencies.search_for_file((desired_classname = args.first.to_s).underscore))
817
+ (self != Object && ActiveSupport::Dependencies.search_for_file((desired_classname = requested).underscore))
718
818
  ) && (require_dependency(filename) || true) &&
719
819
  ((possible = self.const_get(args.first)) && possible.name == desired_classname)
720
820
  ) ||
721
821
  # If any class has turned up so far (and we're not in the middle of eager loading)
722
822
  # then return what we've found.
723
- (is_defined && !::Brick.is_eager_loading)
724
- return possible
725
- end
726
- class_name = ::Brick.namify(args.first.to_s)
727
- # self.name is nil when a model name is requested in an .erb file
728
- base_module = (self < ActiveRecord::Migration || !self.name) ? Object : self
729
- # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
730
- # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
731
- # that is, checking #qualified_name_for with: from_mod, const_name
732
- # If we want to support namespacing in the future, might have to utilise something like this:
733
- # path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
734
- # return self._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
735
- # If the file really exists, go and snag it:
736
- if ActiveSupport::Dependencies.search_for_file(class_name.underscore)
737
- return base_module._brick_const_missing(*args)
738
- # elsif ActiveSupport::Dependencies.search_for_file(filepath) # Last-ditch effort to pick this thing up before we fill in the gaps on our own
739
- # my_const = parent.const_missing(class_name) # ends up having: MyModule::MyClass
740
- # return my_const
741
- else
742
- filepath = base_module.name&.split('::')&.[](0..-2) unless base_module == Object
743
- filepath = ((filepath || []) + [class_name]).join('/').underscore + '.rb'
744
- if ActiveSupport::Dependencies.search_for_file(filepath) # Last-ditch effort to pick this thing up before we fill in the gaps on our own
745
- return base_module._brick_const_missing(*args)
823
+ (is_defined && !::Brick.is_eager_loading) # Used to also have: && possible != self
824
+ if (!brick_root && (filename || possible.instance_of?(Class))) ||
825
+ (possible.instance_of?(Module) &&
826
+ ((possible.respond_to?(:module_parent) ? possible.module_parent : possible.parent) == self)
827
+ ) ||
828
+ (possible.instance_of?(Class) && possible == self) # Are we simply searching for ourselves?
829
+ return possible
746
830
  end
747
831
  end
748
-
832
+ class_name = ::Brick.namify(requested)
749
833
  relations = ::Brick.relations
750
- # puts "ON OBJECT: #{args.inspect}" if self.module_parent == Object
751
- result = if ::Brick.enable_controllers? && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
834
+ result = if ::Brick.enable_controllers? &&
835
+ is_controller && (plural_class_name = class_name[0..-11]).length.positive?
752
836
  # Otherwise now it's up to us to fill in the gaps
837
+ full_class_name = +''
838
+ full_class_name << "::#{(split_self_name&.first && split_self_name.join('::')) || self.name}" unless self == Object
753
839
  # (Go over to underscores for a moment so that if we have something come in like VABCsController then the model name ends up as
754
840
  # Vabc instead of VABC)
755
- full_class_name = +''
756
- full_class_name << "::#{self.name}" unless self == Object
757
841
  singular_class_name = ::Brick.namify(plural_class_name, :underscore).singularize.camelize
758
842
  full_class_name << "::#{singular_class_name}"
759
843
  if plural_class_name == 'BrickSwagger' ||
@@ -856,7 +940,9 @@ class Object
856
940
  schema_name
857
941
  else
858
942
  matching = "#{schema_name}.#{matching}"
859
- (Brick.db_schemas[schema_name] ||= self.const_get(schema_name.camelize))
943
+ # %%% Coming up with integers when tables are in schemas
944
+ # ::Brick.db_schemas[schema_name] ||= self.const_get(schema_name.camelize.to_sym)
945
+ self.const_get(schema_name.camelize)
860
946
  end
861
947
  "#{schema_module&.name}::#{inheritable_name || model_name}"
862
948
  end
@@ -1124,8 +1210,7 @@ class Object
1124
1210
  is_postgres = ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1125
1211
  is_mysql = ActiveRecord::Base.connection.adapter_name == 'Mysql2'
1126
1212
 
1127
- namespace_name = "#{namespace.name}::" if namespace
1128
- code = +"class #{namespace_name}#{class_name} < ApplicationController\n"
1213
+ code = +"class #{class_name} < ApplicationController\n"
1129
1214
  built_controller = Class.new(ActionController::Base) do |new_controller_class|
1130
1215
  (namespace || Object).const_set(class_name.to_sym, new_controller_class)
1131
1216
 
@@ -1204,22 +1289,6 @@ class Object
1204
1289
  return
1205
1290
  end
1206
1291
 
1207
- # Normal (non-swagger) request
1208
-
1209
- # We do all of this now so that bt_descrip and hm_counts are available on the model early in case the user
1210
- # wants to do an ORDER BY based on any of that
1211
- translations = {}
1212
- join_array = ::Brick::JoinArray.new
1213
- is_add_bts = is_add_hms = true
1214
- # This builds out bt_descrip and hm_counts on the model
1215
- model._brick_calculate_bts_hms(translations, join_array) if is_add_bts || is_add_hms
1216
-
1217
- # %%% Allow params to define which columns to use for order_by
1218
- # Overriding the default by providing a querystring param?
1219
- ordering = params['_brick_order']&.split(',')&.map(&:to_sym) || Object.send(:default_ordering, table_name, pk)
1220
- order_by, _ = model._brick_calculate_ordering(ordering, true) # Don't do the txt part
1221
-
1222
- ::Brick.set_db_schema(params)
1223
1292
  if request.format == :csv # Asking for a template?
1224
1293
  require 'csv'
1225
1294
  exported_csv = CSV.generate(force_quotes: false) do |csv_out|
@@ -1233,7 +1302,16 @@ class Object
1233
1302
  return
1234
1303
  end
1235
1304
 
1236
- @_brick_params = (ar_relation = model.all).brick_select(params, (selects = []), order_by, translations, join_array)
1305
+ # Normal (not swagger or CSV) request
1306
+
1307
+ # %%% Allow params to define which columns to use for order_by
1308
+ # Overriding the default by providing a querystring param?
1309
+ ordering = params['_brick_order']&.split(',')&.map(&:to_sym) || Object.send(:default_ordering, table_name, pk)
1310
+ order_by, _ = model._brick_calculate_ordering(ordering, true) # Don't do the txt part
1311
+
1312
+ @_brick_params = (ar_relation = model.all).brick_select(params, (selects = []), nil,
1313
+ translations = {},
1314
+ join_array = ::Brick::JoinArray.new)
1237
1315
  # %%% Add custom HM count columns
1238
1316
  # %%% What happens when the PK is composite?
1239
1317
  counts = model._br_hm_counts.each_with_object([]) do |v, s|
@@ -1275,13 +1353,6 @@ class Object
1275
1353
  code << " end\n"
1276
1354
  self.define_method :show do
1277
1355
  ::Brick.set_db_schema(params)
1278
- id = if model.columns_hash[pk.first]&.type == :string
1279
- is_pk_string = true
1280
- params[:id]
1281
- else
1282
- params[:id]&.split(/[\/,_]/)
1283
- end
1284
- id = id.first if id.is_a?(Array) && id.length == 1
1285
1356
  instance_variable_set("@#{singular_table_name}".to_sym, find_obj)
1286
1357
  end
1287
1358
  end
@@ -1376,7 +1447,14 @@ class Object
1376
1447
  @#{singular_table_name} = #{model.name}.find(id.is_a?(Array) && id.length == 1 ? id.first : id)
1377
1448
  end\n"
1378
1449
  self.define_method :find_obj do
1379
- id = is_pk_string ? params[:id] : params[:id]&.split(/[\/,_]/)
1450
+ id = if model.columns_hash[pk.first]&.type == :string
1451
+ is_pk_string = true
1452
+ params[:id].gsub('^^sl^^', '/')
1453
+ else
1454
+ params[:id]&.split(/[\/,_]/).map do |val_part|
1455
+ val_part.gsub('^^sl^^', '/')
1456
+ end
1457
+ end
1380
1458
  model.find(id.is_a?(Array) && id.length == 1 ? id.first : id)
1381
1459
  end
1382
1460
  end
@@ -1399,33 +1477,55 @@ class Object
1399
1477
  # Get column names for params from relations[model.table_name][:cols].keys
1400
1478
  end
1401
1479
  end # unless is_swagger
1402
- code << "end # #{namespace_name}#{class_name}\n"
1480
+ code << "end # #{class_name}\n"
1403
1481
  end # class definition
1404
1482
  [built_controller, code]
1405
1483
  end
1406
1484
 
1407
1485
  def _brick_get_hm_assoc_name(relation, hm_assoc, source = nil)
1408
- if (relation[:hm_counts][hm_assoc[:inverse_table]]&.> 1) &&
1409
- hm_assoc[:alternate_name] != (source || name.underscore)
1410
- plural = "#{hm_assoc[:assoc_name]}_#{ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name])}"
1411
- new_alt_name = (hm_assoc[:alternate_name] == name.underscore) ? "#{hm_assoc[:assoc_name].singularize}_#{plural}" : plural
1412
- # uniq = 1
1413
- # while same_name = relation[:fks].find { |x| x.last[:assoc_name] == hm_assoc[:assoc_name] && x.last != hm_assoc }
1414
- # hm_assoc[:assoc_name] = "#{hm_assoc_name}_#{uniq += 1}"
1415
- # end
1416
- # puts new_alt_name
1417
- # hm_assoc[:assoc_name] = new_alt_name
1418
- [new_alt_name, true]
1419
- else
1420
- assoc_name = ::Brick.namify(hm_assoc[:inverse_table]).pluralize
1421
- if (needs_class = assoc_name.include?('.')) # If there is a schema name present, use a downcased version for the :has_many
1422
- assoc_parts = assoc_name.split('.')
1423
- assoc_parts[0].downcase! if assoc_parts[0] =~ /^[A-Z0-9_]+$/
1424
- assoc_name = assoc_parts.join('.')
1486
+ assoc_name, needs_class = if (relation[:hm_counts][hm_assoc[:inverse_table]]&.> 1) &&
1487
+ hm_assoc[:alternate_name] != (source || name.underscore)
1488
+ plural = "#{hm_assoc[:assoc_name]}_#{ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name])}"
1489
+ new_alt_name = (hm_assoc[:alternate_name] == name.underscore) ? "#{hm_assoc[:assoc_name].singularize}_#{plural}" : plural
1490
+ # uniq = 1
1491
+ # while same_name = relation[:fks].find { |x| x.last[:assoc_name] == hm_assoc[:assoc_name] && x.last != hm_assoc }
1492
+ # hm_assoc[:assoc_name] = "#{hm_assoc_name}_#{uniq += 1}"
1493
+ # end
1494
+ # puts new_alt_name
1495
+ # hm_assoc[:assoc_name] = new_alt_name
1496
+ [new_alt_name, true]
1497
+ else
1498
+ assoc_name = ::Brick.namify(hm_assoc[:inverse_table]).pluralize
1499
+ if (needs_class = assoc_name.include?('.')) # If there is a schema name present, use a downcased version for the :has_many
1500
+ assoc_parts = assoc_name.split('.')
1501
+ assoc_parts[0].downcase! if assoc_parts[0] =~ /^[A-Z0-9_]+$/
1502
+ assoc_name = assoc_parts.join('.')
1503
+ end
1504
+ # hm_assoc[:assoc_name] = assoc_name
1505
+ [assoc_name, needs_class]
1506
+ end
1507
+ # Already have the HM class around?
1508
+ begin
1509
+ if (hm_class = Object._brick_const_missing(hm_class_name = relation[:class_name].to_sym))
1510
+ existing_hm_assocs = hm_class.reflect_on_all_associations.select do |assoc|
1511
+ assoc.macro != :belongs_to && assoc.klass == self && assoc.foreign_key == hm_assoc[:fk]
1512
+ end
1513
+ # Missing a has_many in an existing class?
1514
+ if existing_hm_assocs.empty?
1515
+ options = { inverse_of: hm_assoc[:inverse][:assoc_name].to_sym }
1516
+ # Add class_name and foreign_key where necessary
1517
+ unless hm_assoc[:alternate_name] == (source || name.underscore)
1518
+ options[:class_name] = self.name
1519
+ options[:foreign_key] = hm_assoc[:fk].to_sym
1520
+ end
1521
+ hm_class.send(:has_many, assoc_name.to_sym, options)
1522
+ puts "# ** Adding a missing has_many to #{hm_class.name}:\nclass #{hm_class.name} < #{hm_class.superclass.name}"
1523
+ puts " has_many :#{assoc_name}, #{options.inspect}\nend\n"
1524
+ end
1425
1525
  end
1426
- # hm_assoc[:assoc_name] = assoc_name
1427
- [assoc_name, needs_class]
1526
+ rescue NameError
1428
1527
  end
1528
+ [assoc_name, needs_class]
1429
1529
  end
1430
1530
  end
1431
1531
  end
@@ -1438,6 +1538,7 @@ module ActiveRecord::ConnectionHandling
1438
1538
  alias _brick_establish_connection establish_connection
1439
1539
  def establish_connection(*args)
1440
1540
  conn = _brick_establish_connection(*args)
1541
+ begin
1441
1542
  # Overwrite SQLite's #begin_db_transaction so it opens in IMMEDIATE mode instead of
1442
1543
  # the default DEFERRED mode.
1443
1544
  # https://discuss.rubyonrails.org/t/failed-write-transaction-upgrades-in-sqlite3/81480/2
@@ -1459,9 +1560,10 @@ module ActiveRecord::ConnectionHandling
1459
1560
  end
1460
1561
  end
1461
1562
  end
1462
- begin
1563
+ # ::Brick.is_db_present = true
1463
1564
  _brick_reflect_tables
1464
1565
  rescue ActiveRecord::NoDatabaseError
1566
+ # ::Brick.is_db_present = false
1465
1567
  end
1466
1568
  conn
1467
1569
  end
@@ -1469,6 +1571,8 @@ module ActiveRecord::ConnectionHandling
1469
1571
  # This is done separately so that during testing it can be called right after a migration
1470
1572
  # in order to make sure everything is good.
1471
1573
  def _brick_reflect_tables
1574
+ # return if ActiveRecord::Base.connection.current_database == 'postgres'
1575
+
1472
1576
  initializer_loaded = false
1473
1577
  if (relations = ::Brick.relations).empty?
1474
1578
  # If there's schema things configured then we only expect our initializer to be named exactly this
@@ -1477,8 +1581,8 @@ module ActiveRecord::ConnectionHandling
1477
1581
  end
1478
1582
  # Load the initializer for the Apartment gem a little early so that if .excluded_models and
1479
1583
  # .default_schema are specified then we can work with non-tenanted models more appropriately
1480
- apartment = Object.const_defined?('Apartment')
1481
- if apartment && File.exist?(apartment_initializer = ::Rails.root.join('config/initializers/apartment.rb'))
1584
+ if (apartment = Object.const_defined?('Apartment')) &&
1585
+ File.exist?(apartment_initializer = ::Rails.root.join('config/initializers/apartment.rb'))
1482
1586
  load apartment_initializer
1483
1587
  apartment_excluded = Apartment.excluded_models
1484
1588
  end
@@ -1490,19 +1594,21 @@ module ActiveRecord::ConnectionHandling
1490
1594
  case ActiveRecord::Base.connection.adapter_name
1491
1595
  when 'PostgreSQL', 'SQLServer'
1492
1596
  is_postgres = !is_mssql
1493
- db_schemas = ActiveRecord::Base.execute_sql('SELECT DISTINCT table_schema FROM INFORMATION_SCHEMA.tables;')
1597
+ db_schemas = if is_postgres
1598
+ ActiveRecord::Base.execute_sql('SELECT nspname AS table_schema, MAX(oid) AS dt FROM pg_namespace GROUP BY 1 ORDER BY 1;')
1599
+ else
1600
+ ActiveRecord::Base.execute_sql('SELECT DISTINCT table_schema, NULL AS dt FROM INFORMATION_SCHEMA.tables;')
1601
+ end
1494
1602
  ::Brick.db_schemas = db_schemas.each_with_object({}) do |row, s|
1495
1603
  row = case row
1496
- when String
1497
- row
1498
1604
  when Array
1499
- row.first
1605
+ row
1500
1606
  else
1501
- row['table_schema']
1607
+ [row['table_schema'], row['dt']]
1502
1608
  end
1503
1609
  # Remove any system schemas
1504
- s[row] = nil unless ['information_schema', 'pg_catalog',
1505
- 'INFORMATION_SCHEMA', 'sys'].include?(row)
1610
+ s[row.first] = row.last unless ['information_schema', 'pg_catalog', 'pg_toast',
1611
+ 'INFORMATION_SCHEMA', 'sys'].include?(row.first)
1506
1612
  end
1507
1613
  if (is_multitenant = (multitenancy = ::Brick.config.schema_behavior[:multitenant]) &&
1508
1614
  (sta = multitenancy[:schema_to_analyse]) != 'public') &&
@@ -1536,6 +1642,10 @@ module ActiveRecord::ConnectionHandling
1536
1642
  if ::Brick.db_schemas.key?(possible_schema)
1537
1643
  ::Brick.default_schema = schema = possible_schema
1538
1644
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1645
+ elsif Rails.env == 'test' # When testing, just find the most recently-created schema
1646
+ ::Brick.default_schema = schema = ::Brick.db_schemas.to_a.sort { |a, b| b.last <=> a.last }.first.first
1647
+ puts "While running tests, had noticed that in the brick.rb initializer the line \"::Brick.schema_behavior = ...\" refers to a schema called \"#{possible_schema}\" which does not exist. Reverting to instead use the most recently-created schema, #{schema}."
1648
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1539
1649
  else
1540
1650
  puts "*** In the brick.rb initializer the line \"::Brick.schema_behavior = ...\" refers to a schema called \"#{possible_schema}\". This schema does not exist. ***"
1541
1651
  end
@@ -1548,7 +1658,7 @@ module ActiveRecord::ConnectionHandling
1548
1658
  measures = []
1549
1659
  ::Brick.is_oracle = true if ActiveRecord::Base.connection.adapter_name == 'OracleEnhanced'
1550
1660
  case ActiveRecord::Base.connection.adapter_name
1551
- when 'PostgreSQL', 'SQLite' # These bring back a hash for each row because the query uses column aliases
1661
+ when 'PostgreSQL', 'SQLite', 'SQLServer' # These bring back a hash for each row because the query uses column aliases
1552
1662
  # schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1553
1663
  ActiveRecord::Base.retrieve_schema_and_tables(sql, is_postgres, is_mssql, schema).each do |r|
1554
1664
  # If Apartment gem lists the table as being associated with a non-tenanted model then use whatever it thinks
@@ -1757,7 +1867,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
1757
1867
  LEFT OUTER JOIN INFORMATION_SCHEMA.columns AS c ON t.table_schema = c.table_schema
1758
1868
  AND t.table_name = c.table_name
1759
1869
  LEFT OUTER JOIN
1760
- (SELECT kcu1.constraint_schema, kcu1.table_name, kcu1.ordinal_position,
1870
+ (SELECT kcu1.constraint_schema, kcu1.table_name, kcu1.column_name, kcu1.ordinal_position,
1761
1871
  tc.constraint_type, kcu1.constraint_name
1762
1872
  FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu1
1763
1873
  INNER JOIN INFORMATION_SCHEMA.table_constraints AS tc
@@ -1768,9 +1878,9 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
1768
1878
  ) AS kcu ON
1769
1879
  -- kcu.CONSTRAINT_CATALOG = t.table_catalog AND
1770
1880
  kcu.CONSTRAINT_SCHEMA = c.table_schema
1771
- AND kcu.TABLE_NAME = c.table_name#{"
1881
+ AND kcu.TABLE_NAME = c.table_name
1882
+ AND kcu.column_name = c.column_name#{"
1772
1883
  -- AND kcu.position_in_unique_constraint IS NULL" unless is_mssql}
1773
- AND kcu.ordinal_position = c.ordinal_position
1774
1884
  WHERE t.table_schema #{is_postgres || is_mssql ?
1775
1885
  "NOT IN ('information_schema', 'pg_catalog',
1776
1886
  'INFORMATION_SCHEMA', 'sys')"
@@ -1779,7 +1889,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
1779
1889
  AND t.table_schema = COALESCE(current_setting('SEARCH_PATH'), 'public')" if is_postgres && schema }
1780
1890
  -- AND t.table_type IN ('VIEW') -- 'BASE TABLE', 'FOREIGN TABLE'
1781
1891
  AND t.table_name NOT IN ('pg_stat_statements', ?, ?)
1782
- ORDER BY 1, t.table_type DESC, 2, c.ordinal_position"
1892
+ ORDER BY 1, t.table_type DESC, 2, kcu.ordinal_position"
1783
1893
  ActiveRecord::Base.execute_sql(sql, *ar_tables)
1784
1894
  end
1785
1895
 
@@ -1902,9 +2012,7 @@ module Brick
1902
2012
  # For any appended references (those that come from config), arrive upon a definitely unique constraint name
1903
2013
  pri_tbl = is_class ? fk[4][:class].underscore : pri_tbl
1904
2014
  pri_tbl = "#{bt_assoc_name}_#{pri_tbl}" if pri_tbl&.singularize != bt_assoc_name
1905
- cnstr_base = cnstr_name = "(brick) #{for_tbl}_#{pri_tbl}"
1906
- cnstr_added_num = 1
1907
- cnstr_name = "#{cnstr_base}_#{cnstr_added_num += 1}" while bts&.key?(cnstr_name) || hms&.key?(cnstr_name)
2015
+ cnstr_name = ensure_unique(+"(brick) #{for_tbl}_#{pri_tbl}", bts, hms)
1908
2016
  missing = []
1909
2017
  missing << fk[1] unless relations.key?(fk[1])
1910
2018
  missing << primary_table unless is_class || relations.key?(primary_table)
@@ -2026,7 +2134,29 @@ module Brick
2026
2134
  end
2027
2135
  end
2028
2136
  end
2029
- ::Brick.relations.keys.map { |v| [(r = v.pluralize), (model = models[r])&.last&.table_name || v, migrations&.fetch(r, nil), model&.first] }
2137
+ ::Brick.relations.keys.map { |v| [(model = models[v])&.last, model&.last&.table_name, migrations&.fetch(v, nil), model&.first] }
2138
+ end
2139
+
2140
+ def ensure_unique(name, *sources)
2141
+ base = name
2142
+ if (added_num = name.slice!(/_(\d+)$/))
2143
+ added_num = added_num[1..-1].to_i
2144
+ else
2145
+ added_num = 1
2146
+ end
2147
+ while (
2148
+ name = "#{base}_#{added_num += 1}"
2149
+ sources.each_with_object(nil) do |v, s|
2150
+ s || case v
2151
+ when Hash
2152
+ v.key?(name)
2153
+ when Array
2154
+ v.include?(name)
2155
+ end
2156
+ end
2157
+ )
2158
+ end
2159
+ name
2030
2160
  end
2031
2161
 
2032
2162
  # Locate orphaned records
@@ -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
 
@@ -111,7 +114,7 @@ module Brick
111
114
  if @_brick_model
112
115
  pk = @_brick_model._brick_primary_key(::Brick.relations.fetch(@_brick_model&.table_name, nil))
113
116
  obj_name = model_name.split('::').last.underscore
114
- path_obj_name = model_name.underscore.tr('/', '_')
117
+ path_obj_name = @_brick_model._brick_index(:singular)
115
118
  table_name = obj_name.pluralize
116
119
  template_link = nil
117
120
  bts, hms = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
@@ -122,9 +125,11 @@ module Brick
122
125
  "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]}",
123
126
  (assoc_name = hm.first)]
124
127
  hm_fk_name = if (through = hm_assoc.options[:through])
125
- next unless @_brick_model.instance_methods.include?(through)
128
+ # %%% How to deal with weird self_ref type has_many -> has_one polymorphic stuff?
129
+ # (or perhaps we don't need to!)
130
+ next unless @_brick_model.instance_methods.include?(through) &&
131
+ (associative = @_brick_model._br_associatives.fetch(hm.first, nil))
126
132
 
127
- associative = @_brick_model._br_associatives[hm.first]
128
133
  tbl_nm = if hm_assoc.options[:source]
129
134
  associative.klass.reflect_on_association(hm_assoc.options[:source]).inverse_of&.name
130
135
  else
@@ -174,6 +179,7 @@ module Brick
174
179
  # %%% If we are not auto-creating controllers (or routes) then omit by default, and if enabled anyway, such as in a development
175
180
  # environment or whatever, then get either the controllers or routes list instead
176
181
  apartment_default_schema = ::Brick.apartment_multitenant && Apartment.default_schema
182
+ prefix = "#{::Brick.config.path_prefix}/" if ::Brick.config.path_prefix
177
183
  table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables).each_with_object({}) do |tbl, s|
178
184
  binding.pry if tbl.is_a?(Symbol)
179
185
  if (tbl_parts = tbl.split('.')).first == apartment_default_schema
@@ -181,7 +187,7 @@ module Brick
181
187
  end
182
188
  s[tbl] = nil
183
189
  end.keys.sort.each_with_object(+'') do |v, s|
184
- s << "<option value=\"#{v.underscore.gsub('.', '/')}\">#{v}</option>"
190
+ s << "<option value=\"#{prefix}#{v.underscore.gsub('.', '/')}\">#{v}</option>"
185
191
  end.html_safe
186
192
  table_options << '<option value="brick_status">(Status)</option>'.html_safe if ::Brick.config.add_status
187
193
  table_options << '<option value="brick_orphans">(Orphans)</option>'.html_safe if is_orphans
@@ -249,8 +255,8 @@ tr th {
249
255
  right: 0;
250
256
  cursor: pointer;
251
257
  }
252
- #headerTop tr th:hover {
253
- background-color: #18B090;
258
+ #headerTop tr th:hover, #headerTop tr th.highlight {
259
+ background-color: #28B898;
254
260
  }
255
261
  #exclusions {
256
262
  font-size: 0.7em;
@@ -273,6 +279,10 @@ tr th, tr td {
273
279
  padding: 0.2em 0.5em;
274
280
  }
275
281
 
282
+ tr td.highlight {
283
+ background-color: #B0B0FF;
284
+ }
285
+
276
286
  .show-field {
277
287
  background-color: #004998;
278
288
  }
@@ -398,6 +408,11 @@ def display_value(col_type, val)
398
408
  end
399
409
  end
400
410
  end
411
+ # Accommodate composite primary keys that include strings with forward-slash characters
412
+ def slashify(val)
413
+ val = [val] unless val.is_a?(Array)
414
+ val.map { |val_part| val_part.is_a?(String) ? val_part.gsub('/', '^^sl^^') : val_part }
415
+ end
401
416
  callbacks = {} %>"
402
417
 
403
418
  if ['index', 'show', 'new', 'update'].include?(args.first)
@@ -454,7 +469,7 @@ window.addEventListener(\"pageshow\", function() {
454
469
  });
455
470
 
456
471
  if (tblSelect) { // Always present
457
- var i = schemaSelect ? 1 : 0,
472
+ var i = #{::Brick.config.path_prefix ? '0' : 'schemaSelect ? 1 : 0'},
458
473
  changeoutList = changeout(location.href);
459
474
  for (; i < changeoutList.length; ++i) {
460
475
  tblSelect.value = changeoutList[i];
@@ -500,6 +515,38 @@ function changeout(href, param, value, trimAfter) {
500
515
  var grid = document.getElementById(\"#{table_name}\");
501
516
  #{table_name}HtColumns = grid && [grid.getElementsByTagName(\"TR\")[0]];
502
517
  var headerTop = document.getElementById(\"headerTop\");
518
+ var headerCols;
519
+ if (grid) {
520
+ // COLUMN HEADER AND TABLE CELL HIGHLIGHTING
521
+ var gridHighHeader = null,
522
+ gridHighCell = null;
523
+ grid.addEventListener(\"mouseenter\", gridMove);
524
+ grid.addEventListener(\"mousemove\", gridMove);
525
+ grid.addEventListener(\"mouseleave\", function (evt) {
526
+ if (gridHighCell) gridHighCell.classList.remove(\"highlight\");
527
+ gridHighCell = null;
528
+ if (gridHighHeader) gridHighHeader.classList.remove(\"highlight\");
529
+ gridHighHeader = null;
530
+ });
531
+ function gridMove(evt) {
532
+ var lastHighCell = gridHighCell;
533
+ gridHighCell = document.elementFromPoint(evt.x, evt.y);
534
+ while (gridHighCell && gridHighCell.tagName !== \"TD\" && gridHighCell.tagName !== \"TH\")
535
+ gridHighCell = gridHighCell.parentElement;
536
+ if (gridHighCell) {
537
+ if (lastHighCell !== gridHighCell) {
538
+ gridHighCell.classList.add(\"highlight\");
539
+ if (lastHighCell) lastHighCell.classList.remove(\"highlight\");
540
+ }
541
+ var lastHighHeader = gridHighHeader;
542
+ gridHighHeader = headerCols[gridHighCell.cellIndex];
543
+ if (lastHighHeader !== gridHighHeader) {
544
+ if (gridHighHeader) gridHighHeader.classList.add(\"highlight\");
545
+ if (lastHighHeader) lastHighHeader.classList.remove(\"highlight\");
546
+ }
547
+ }
548
+ }
549
+ }
503
550
  function setHeaderSizes() {
504
551
  // console.log(\"start\");
505
552
  // See if the headerTop is already populated
@@ -531,6 +578,7 @@ function setHeaderSizes() {
531
578
  }
532
579
  }
533
580
  }
581
+ headerCols = tr.childNodes;
534
582
  if (isEmpty) headerTop.appendChild(tr);
535
583
  }
536
584
  grid.style.marginTop = \"-\" + getComputedStyle(headerTop).height;
@@ -779,7 +827,8 @@ erDiagram
779
827
  cols[col_name] = col
780
828
  end
781
829
  unless @_brick_sequence # If no sequence is defined, start with all inclusions
782
- @_brick_sequence = col_keys + #{(hms_keys).inspect}.reject { |assoc_name| @_brick_incl&.exclude?(assoc_name) }
830
+ cust_cols = #{model_name}._br_cust_cols
831
+ @_brick_sequence = col_keys + cust_cols.keys + #{(hms_keys).inspect}.reject { |assoc_name| @_brick_incl&.exclude?(assoc_name) }
783
832
  end
784
833
  @_brick_sequence.reject! { |nm| @_brick_excl.include?(nm) } if @_brick_excl # Reject exclusions
785
834
  @_brick_sequence.each_with_object(+'') do |col_name, s|
@@ -796,6 +845,8 @@ erDiagram
796
845
  elsif col # HM column
797
846
  s << \"<th#\{' x-order=\"' + col_name + '\"' if true}>#\{col[2]} \"
798
847
  s << (col.first ? \"#\{col[3]}\" : \"#\{link_to(col[3], send(\"#\{col[1]._brick_index}_path\"))}\")
848
+ elsif (cc = cust_cols.key?(col_name)) # Custom column
849
+ s << \"<th x-order=\\\"#\{col_name}\\\">#\{col_name}\"
799
850
  else # Bad column name!
800
851
  s << \"<th title=\\\"<< Unknown column >>\\\">#\{col_name}\"
801
852
  end
@@ -810,24 +861,25 @@ erDiagram
810
861
  @#{table_name}.each do |#{obj_name}|
811
862
  hms_cols = {#{hms_columns.join(', ')}} %>
812
863
  <tr>#{"
813
- <td><%= link_to '⇛', #{path_obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
864
+ <td><%= link_to '⇛', #{path_obj_name}_path(slashify(#{obj_pk})), { class: 'big-arrow' } %></td>" if obj_pk}
814
865
  <% @_brick_sequence.each do |col_name|
815
866
  val = #{obj_name}.attributes[col_name] %>
816
- <td<%= ' class=\"dimmed\"'.html_safe unless cols.key?(col_name)%>><%
867
+ <td<%= ' class=\"dimmed\"'.html_safe unless cols.key?(col_name) || (cust_col = cust_cols[col_name])%>><%
817
868
  if (bt = bts[col_name])
818
869
  if bt[2] # Polymorphic?
819
- bt_class = #{obj_name}.send(\"#\{bt.first\}_type\")
820
- base_class = (::Brick.existing_stis[bt_class] || bt_class).constantize.base_class.name.underscore
821
- poly_id = #{obj_name}.send(\"#\{bt.first\}_id\")
822
- %><%= link_to(\"#\{bt_class\} ##\{poly_id\}\", send(\"#\{base_class\}_path\".to_sym, poly_id)) if poly_id %><%
870
+ bt_class = #{obj_name}.send(\"#\{bt.first}_type\")
871
+ base_class_underscored = (::Brick.existing_stis[bt_class] || bt_class).constantize.base_class._brick_index(:singular)
872
+ poly_id = #{obj_name}.send(\"#\{bt.first}_id\")
873
+ %><%= link_to(\"#\{bt_class} ##\{poly_id}\", send(\"#\{base_class_underscored}_path\".to_sym, poly_id)) if poly_id %><%
823
874
  else
875
+ # binding.pry if @_brick_bt_descrip[bt.first][bt[1].first.first].nil?
824
876
  bt_txt = (bt_class = bt[1].first.first).brick_descrip(
825
877
  # 0..62 because Postgres column names are limited to 63 characters
826
878
  #{obj_name}, (descrips = @_brick_bt_descrip[bt.first][bt_class])[0..-2].map { |id| #{obj_name}.send(id.last[0..62]) }, (bt_id_col = descrips.last)
827
879
  )
828
880
  bt_txt ||= \"<span class=\\\"orphan\\\">&lt;&lt; Orphaned ID: #\{val} >></span>\".html_safe if val
829
881
  bt_id = bt_id_col.map { |id_col| #{obj_name}.send(id_col.to_sym) } %>
830
- <%= bt_id&.first ? link_to(bt_txt, send(\"#\{bt_class.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, bt_id)) : bt_txt %>
882
+ <%= bt_id&.first ? link_to(bt_txt, send(\"#\{bt_class.base_class._brick_index(:singular)}_path\".to_sym, bt_id)) : bt_txt %>
831
883
  <% end
832
884
  elsif (hms_col = hms_cols[col_name])
833
885
  if hms_col.length == 1 %>
@@ -838,15 +890,18 @@ erDiagram
838
890
  descrips = @_brick_bt_descrip[col_name.to_sym][klass]
839
891
  ho_txt = klass.brick_descrip(#{obj_name}, descrips[0..-2].map { |id| #{obj_name}.send(id.last[0..62]) }, (ho_id_col = descrips.last))
840
892
  ho_id = ho_id_col.map { |id_col| #{obj_name}.send(id_col.to_sym) }
841
- ho_id&.first ? link_to(ho_txt, send(\"#\{klass.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, ho_id)) : ho_txt
893
+ ho_id&.first ? link_to(ho_txt, send(\"#\{klass.base_class._brick_index(:singular)}_path\".to_sym, ho_id)) : ho_txt
842
894
  else
843
- \"#\{hms_col[1] || 'View'\} #\{hms_col.first}\"
895
+ \"#\{hms_col[1] || 'View'} #\{hms_col.first}\"
844
896
  end %>
845
897
  <%= link_to txt, send(\"#\{klass._brick_index}_path\".to_sym, hms_col[2]) unless hms_col[1]&.zero? %>
846
898
  <% end
847
899
  elsif (col = cols[col_name])
848
900
  col_type = col&.sql_type == 'geography' ? col.sql_type : col&.type
849
901
  %><%= display_value(col_type || col&.sql_type, val) %><%
902
+ elsif cust_col
903
+ data = cust_col.first.map { |cc_part| #{obj_name}.send(cc_part.last) }
904
+ %><%= #{model_name}.brick_descrip(cust_col.last, data) %><%
850
905
  else # Bad column name!
851
906
  %>?<%
852
907
  end
@@ -885,7 +940,7 @@ erDiagram
885
940
  @resources.each do |r|
886
941
  %>
887
942
  <tr>
888
- <td><%= link_to(r[0], \"/#\{r[0].underscore.tr('.', '/')}\") %></td>
943
+ <td><%= link_to(r[0], r[0] && send(\"#\{r[0]&._brick_index}_path\".to_sym)) %></td>
889
944
  <td<%= if r[1]
890
945
  ' class=\"orphan\"' unless ::Brick.relations.key?(r[1])
891
946
  else
@@ -942,12 +997,15 @@ if (description = (relation = Brick.relations[#{model_name}.table_name])&.fetch(
942
997
  end
943
998
  %><%= link_to '(See all #{obj_name.pluralize})', #{@_brick_model._brick_index}_path %>
944
999
  #{erd_markup}
945
- <% if obj %>
1000
+ <% if obj
1001
+ # path_options = [obj.#{pk}]
1002
+ # path_options << { '_brick_schema': } if
1003
+ # url = send(:#\{model_name._brick_index(:singular)}_path, obj.#{pk})
1004
+ options = {}
1005
+ options[:url] = send(\"#\{#{model_name}._brick_index(:singular)}_path\".to_sym, obj) if ::Brick.config.path_prefix
1006
+ %>
946
1007
  <br><br>
947
- <%= # path_options = [obj.#{pk}]
948
- # path_options << { '_brick_schema': } if
949
- # url = send(:#{model_name.underscore}_path, obj.#{pk})
950
- form_for(obj.becomes(#{model_name})) do |f| %>
1008
+ <%= form_for(obj.becomes(#{model_name}), options) do |f| %>
951
1009
  <table class=\"shadow\">
952
1010
  <% has_fields = false
953
1011
  @#{obj_name}.attributes.each do |k, val|
@@ -966,7 +1024,7 @@ end
966
1024
  bt_pair = nil
967
1025
  loop do
968
1026
  bt_pair = bt[1].find { |pair| pair.first.name == poly_class_name }
969
- # Acxommodate any valid STI by going up the chain of inheritance
1027
+ # Accommodate any valid STI by going up the chain of inheritance
970
1028
  break unless bt_pair.nil? && poly_class_name = ::Brick.existing_stis[poly_class_name]
971
1029
  end
972
1030
  puts \"*** Might be missing an STI class called #\{orig_poly_name\} whose base class should have this:
@@ -1001,7 +1059,7 @@ end
1001
1059
  html_options[:prompt] = \"Select #\{bt_name\}\" %>
1002
1060
  <%= f.select k.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
1003
1061
  <%= if (bt_obj = bt_class&.find_by(bt_pair[1] => val))
1004
- 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' })
1062
+ link_to('⇛', send(\"#\{bt_class.base_class._brick_index(:singular)\}_path\".to_sym, bt_obj.send(bt_class.primary_key.to_sym)), { class: 'show-arrow' })
1005
1063
  elsif val
1006
1064
  \"<span class=\\\"orphan\\\">Orphaned ID: #\{val}</span>\".html_safe
1007
1065
  end %>
@@ -1059,9 +1117,10 @@ end
1059
1117
  <tr><td colspan=\"2\">(No displayable fields)</td></tr>
1060
1118
  <% end %>
1061
1119
  </table>
1062
- <% end %>
1120
+ <% end %>
1063
1121
 
1064
- #{hms_headers.each_with_object(+'') do |hm, s|
1122
+ #{unless args.first == 'new'
1123
+ hms_headers.each_with_object(+'') do |hm, s|
1065
1124
  # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
1066
1125
  next if hm.first.options[:through] && !hm.first.through_reflection
1067
1126
 
@@ -1076,14 +1135,15 @@ end
1076
1135
  <tr><td>(none)</td></tr>
1077
1136
  <% else %>
1078
1137
  <% collection.uniq.each do |#{hm_singular_name}| %>
1079
- <tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.name.underscore.tr('/', '_')}_path([#{obj_pk}])) %></td></tr>
1138
+ <tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass._brick_index(:singular)}_path(slashify(#{obj_pk}))) %></td></tr>
1080
1139
  <% end %>
1081
1140
  <% end %>
1082
1141
  </table>"
1083
1142
  else
1084
1143
  s
1085
1144
  end
1086
- end}
1145
+ end
1146
+ end}
1087
1147
  <% end %>
1088
1148
  #{script}"
1089
1149
 
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 75
8
+ TINY = 77
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
@@ -216,6 +216,12 @@ module Brick
216
216
  true
217
217
  end
218
218
 
219
+ # Any path prefixing to apply to all auto-generated Brick routes
220
+ # @api public
221
+ def path_prefix=(path)
222
+ Brick.config.path_prefix = path
223
+ end
224
+
219
225
  # Switches Brick auto-models on or off, for all threads
220
226
  # @api public
221
227
  def enable_models=(value)
@@ -329,6 +335,15 @@ module Brick
329
335
  end
330
336
  end
331
337
 
338
+ # Custom columns to add to a table, minimally defined with a name and DSL string.
339
+ # @api public
340
+ def custom_columns=(cust_cols)
341
+ if cust_cols
342
+ cust_cols = cust_cols.call if cust_cols.is_a?(Proc)
343
+ Brick.config.custom_columns = cust_cols
344
+ end
345
+ end
346
+
332
347
  # @api public
333
348
  def order=(value)
334
349
  Brick.config.order = value
@@ -529,23 +544,40 @@ In config/initializers/brick.rb appropriate entries would look something like:
529
544
  view_class_length = 37 # Length of "Classes that can be built from views:"
530
545
  existing_controllers = routes.each_with_object({}) { |r, s| c = r.defaults[:controller]; s[c] = nil if c }
531
546
  ::Rails.application.routes.append do
547
+ brick_routes_create = lambda do |schema_name, controller_name, v, options|
548
+ if schema_name # && !Object.const_defined('Apartment')
549
+ send(:namespace, schema_name) do
550
+ send(:resources, v[:resource].to_sym, **options)
551
+ end
552
+ else
553
+ send(:resources, v[:resource].to_sym, **options)
554
+ end
555
+ end
556
+
532
557
  # %%% TODO: If no auto-controllers then enumerate the controllers folder in order to build matching routes
533
558
  # If auto-controllers and auto-models are both enabled then this makes sense:
534
559
  ::Brick.relations.each do |k, v|
535
560
  unless !(controller_name = v.fetch(:resource, nil)&.pluralize) || existing_controllers.key?(controller_name)
536
561
  options = {}
537
562
  options[:only] = [:index, :show] if v.key?(:isView)
563
+ # First do the API routes
538
564
  full_resource = nil
539
- if (schema_name = v.fetch(:schema, nil)) # && !Object.const_defined('Apartment')
540
- send(:namespace, schema_name) do
541
- send(:resources, v[:resource].to_sym, **options)
542
- end
565
+ controller_prefix = (::Brick.config.path_prefix ? "#{::Brick.config.path_prefix}/" : '')
566
+ if (schema_name = v.fetch(:schema, nil))
543
567
  full_resource = "#{schema_name}/#{v[:resource]}"
544
- send(:get, "#{::Brick.api_root}#{full_resource}", { to: "#{schema_name}/#{controller_name}#index" }) if Object.const_defined?('Rswag::Ui')
568
+ send(:get, "#{::Brick.api_root}#{full_resource}", { to: "#{controller_prefix}#{schema_name}/#{controller_name}#index" }) if Object.const_defined?('Rswag::Ui')
545
569
  else
546
- send(:resources, v[:resource].to_sym, **options)
547
570
  # Normally goes to something like: /api/v1/employees
548
- send(:get, "#{::Brick.api_root}#{v[:resource]}", { to: "#{controller_name}#index" }) if Object.const_defined?('Rswag::Ui')
571
+ send(:get, "#{::Brick.api_root}#{v[:resource]}", { to: "#{controller_prefix}#{controller_name}#index" }) if Object.const_defined?('Rswag::Ui')
572
+ end
573
+ # Now the normal routes
574
+ if ::Brick.config.path_prefix
575
+ # Was: send(:scope, path: ::Brick.config.path_prefix) do
576
+ send(:namespace, ::Brick.config.path_prefix) do
577
+ brick_routes_create.call(schema_name, controller_name, v, options)
578
+ end
579
+ else
580
+ brick_routes_create.call(schema_name, controller_name, v, options)
549
581
  end
550
582
 
551
583
  if (class_name = v.fetch(:class_name, nil))
@@ -139,6 +139,10 @@ module Brick
139
139
  # # Settings for the Brick gem
140
140
  # # (By default this auto-creates models, controllers, views, and routes on-the-fly.)
141
141
 
142
+ # # Custom path prefix to apply to all auto-generated Brick routes. Also causes auto-generated controllers
143
+ # # to be created inside a module with the same name.
144
+ # ::Brick.path_prefix = 'admin'
145
+
142
146
  # # Normally all are enabled in development mode, and for security reasons only models are enabled in production
143
147
  # # and test. This allows you to either (a) turn off models entirely, or (b) enable controllers, views, and routes
144
148
  # # in production.
@@ -207,6 +211,12 @@ module Brick
207
211
  # # to be the primary key.)
208
212
  #{bar}
209
213
 
214
+ # # Custom columns to add to a table, minimally defined with a name and DSL string.
215
+ # Brick.custom_columns = { 'users' => { messages: ['[COUNT(messages)] messages', 'messages'] },
216
+ # 'orders' => { salesperson: '[salesperson.first] [salesperson.last]',
217
+ # products: ['[COUNT(order_items.product)] products', 'order_items.product' ] }
218
+ # }
219
+
210
220
  # # Skip creating a has_many association for these (only retain the belongs_to built from this additional_reference).
211
221
  # # (Uses the same exact three-part format as would define an additional_reference)
212
222
  # # Say for instance that we didn't care to display the favourite colours that users have:
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.75
4
+ version: 1.0.77
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-30 00:00:00.000000000 Z
11
+ date: 2022-10-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord