brick 1.0.48 → 1.0.51

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: c9c3989f395bc81df36b3f295c72029e92911bd24f8d146db5627fb29c1875f4
4
- data.tar.gz: 97ae12a1562f9f9bae5f401016e219a5ad58218068f80d7fc0e732d61c4f3f90
3
+ metadata.gz: 9d0c2c69187f4d9b8eaa4a22777d3e517546bf355316012bf3e333064f8939b7
4
+ data.tar.gz: 33fcb2f6ca4373eba2db3a2891de392d9ef71ea693687328f72d23837cabdc46
5
5
  SHA512:
6
- metadata.gz: c64d02f90b992640698523190a920467204a2fbb40fb9e21a3e7872ae469db3506bdf7fb535e54cd33157feb93ed43c2d1c11b2bea952bc9f034ac7e7dfb6b06
7
- data.tar.gz: 6c01156b2b35f387e531ed6835a7517f5249b72ec53bb23d5d965c4b298a443d9a62e5694f9c1b4587f6ab04ef30590d7d9077e13c66b651ee8ed12b76282932
6
+ metadata.gz: 7136cb61d19325c1916e9d2e8e6cd7ed6c1d6838c77a216c5275bf4cc0d3540eeabf5caae152b49b9f5cf0d5ea7307bebd98f2fdbc03e651310c1cfc6fbec436
7
+ data.tar.gz: 81007bc391e9871ab0bbe80fc5aaa440da2685e1abe16d65d09419e31e93529232757db173cf97ba78ad3e719af760cac2d83b57cb7d97fa488399d93d3d2f76
data/lib/brick/config.rb CHANGED
@@ -190,6 +190,30 @@ module Brick
190
190
  @mutex.synchronize { @table_name_prefixes = value }
191
191
  end
192
192
 
193
+ def order
194
+ @mutex.synchronize { @order || {} }
195
+ end
196
+
197
+ # Get something like:
198
+ # Override how code sorts with:
199
+ # { 'on_call_list' => { code: "ORDER BY STRING_TO_ARRAY(code, '.')::int[]" } }
200
+ # Specify default thing to order_by with:
201
+ # { 'on_call_list' => { _brick_default: [:last_name, :first_name] } }
202
+ # { 'on_call_list' => { _brick_default: :sequence } }
203
+ def order=(orders)
204
+ @mutex.synchronize do
205
+ case (brick_default = orders.fetch(:_brick_default, nil))
206
+ when NilClass
207
+ orders[:_brick_default] = orders.keys.reject { |k| k == :_brick_default }.first
208
+ when String
209
+ orders[:_brick_default] = [brick_default.to_sym]
210
+ when Symbol
211
+ orders[:_brick_default] = [brick_default]
212
+ end
213
+ @order = orders
214
+ end
215
+ end
216
+
193
217
  def metadata_columns
194
218
  @mutex.synchronize { @metadata_columns }
195
219
  end
@@ -221,6 +221,68 @@ module ActiveRecord
221
221
  template
222
222
  end
223
223
 
224
+ class << self
225
+ # belongs_to DSL descriptions
226
+ def _br_bt_descrip
227
+ @_br_bt_descrip ||= {}
228
+ end
229
+ # has_many count definitions
230
+ def _br_hm_counts
231
+ @_br_hm_counts ||= {}
232
+ end
233
+ # has_many :through associative tables
234
+ def _br_associatives
235
+ @_br_associatives ||= {}
236
+ end
237
+ end
238
+
239
+ # Search for BT, HM, and HMT DSL stuff
240
+ def self._brick_calculate_bts_hms(translations, join_array)
241
+ bts, hms, associatives = ::Brick.get_bts_and_hms(self)
242
+ bts.each do |_k, bt|
243
+ next if bt[2] # Polymorphic?
244
+
245
+ # join_array will receive this relation name when calling #brick_parse_dsl
246
+ _br_bt_descrip[bt.first] = if bt[1].is_a?(Array)
247
+ bt[1].each_with_object({}) { |bt_class, s| s[bt_class] = bt_class.brick_parse_dsl(join_array, bt.first, translations, true) }
248
+ else
249
+ { bt.last => bt[1].brick_parse_dsl(join_array, bt.first, translations) }
250
+ end
251
+ end
252
+ skip_klass_hms = ::Brick.config.skip_index_hms[self.name] || {}
253
+ hms.each do |k, hm|
254
+ next if skip_klass_hms.key?(k)
255
+
256
+ if hm.macro == :has_one
257
+ # For our purposes a :has_one is similar enough to a :belongs_to that we can just join forces
258
+ _br_bt_descrip[k] = { hm.klass => hm.klass.brick_parse_dsl(join_array, k, translations) }
259
+ else # Standard :has_many
260
+ _br_hm_counts[k] = hm
261
+ end
262
+ end
263
+ end
264
+
265
+ def self._brick_calculate_ordering(ordering, is_do_txt = true)
266
+ quoted_table_name = table_name.split('.').map { |x| "\"#{x}\"" }.join('.')
267
+ order_by_txt = [] if is_do_txt
268
+ ordering = [ordering] if ordering && !ordering.is_a?(Array)
269
+ order_by = ordering&.map do |ord_part| # %%% If a term is also used as an eqi-condition in the WHERE clause, it can be omitted from ORDER BY
270
+ case ord_part
271
+ when String
272
+ ord_expr = ord_part.gsub('^^^', quoted_table_name)
273
+ order_by_txt&.<<("Arel.sql(#{ord_expr})")
274
+ Arel.sql(ord_expr)
275
+ else # Expecting only Symbol
276
+ ord_part = "_br_#{ord_part}_ct" if _br_hm_counts.key?(ord_part)
277
+ # Retain any reference to a bt_descrip as being a symbol
278
+ # Was: "#{quoted_table_name}.\"#{ord_part}\""
279
+ order_by_txt&.<<(_br_bt_descrip.key?(ord_part) ? ord_part : ord_part.inspect)
280
+ ord_part
281
+ end
282
+ end
283
+ [order_by, order_by_txt]
284
+ end
285
+
224
286
  private
225
287
 
226
288
  def self._brick_get_fks
@@ -310,16 +372,15 @@ module ActiveRecord
310
372
  end
311
373
  end
312
374
 
313
- def brick_select(params, selects = nil, bt_descrip = {}, hm_counts = {}, join_array = ::Brick::JoinArray.new
314
- # , is_add_bts, is_add_hms
315
- )
316
- is_add_bts = is_add_hms = true
375
+ def brick_select(params, selects = nil, order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new)
317
376
  is_distinct = nil
318
377
  wheres = {}
319
378
  params.each do |k, v|
379
+ next if ['_brick_schema', '_brick_order'].include?(k)
380
+
320
381
  case (ks = k.split('.')).length
321
382
  when 1
322
- next unless klass._brick_get_fks.include?(k)
383
+ next unless klass.column_names.any?(k) || klass._brick_get_fks.include?(k)
323
384
  when 2
324
385
  assoc_name = ks.first.to_sym
325
386
  # Make sure it's a good association name and that the model has that column name
@@ -345,33 +406,6 @@ module ActiveRecord
345
406
  end
346
407
  end
347
408
 
348
- # Search for BT, HM, and HMT DSL stuff
349
- translations = {}
350
- if is_add_bts || is_add_hms
351
- bts, hms, associatives = ::Brick.get_bts_and_hms(klass)
352
- bts.each do |_k, bt|
353
- next if bt[2] # Polymorphic?
354
-
355
- # join_array will receive this relation name when calling #brick_parse_dsl
356
- bt_descrip[bt.first] = if bt[1].is_a?(Array)
357
- bt[1].each_with_object({}) { |bt_class, s| s[bt_class] = bt_class.brick_parse_dsl(join_array, bt.first, translations, true) }
358
- else
359
- { bt.last => bt[1].brick_parse_dsl(join_array, bt.first, translations) }
360
- end
361
- end
362
- skip_klass_hms = ::Brick.config.skip_index_hms[klass.name] || {}
363
- hms.each do |k, hm|
364
- next if skip_klass_hms.key?(k)
365
-
366
- if hm.macro == :has_one
367
- # For our purposes a :has_one is similar enough to a :belongs_to that we can just join forces
368
- bt_descrip[k] = { hm.klass => hm.klass.brick_parse_dsl(join_array, k, translations) }
369
- else # Standard :has_many
370
- hm_counts[k] = hm
371
- end
372
- end
373
- end
374
-
375
409
  if join_array.present?
376
410
  left_outer_joins!(join_array)
377
411
  # Without working from a duplicate, touching the AREL ast tree sets the @arel instance variable, which causes the relation to be immutable.
@@ -380,7 +414,8 @@ module ActiveRecord
380
414
  chains = rel_dupe._brick_chains
381
415
  id_for_tables = Hash.new { |h, k| h[k] = [] }
382
416
  field_tbl_names = Hash.new { |h, k| h[k] = {} }
383
- bt_columns = bt_descrip.each_with_object([]) do |v, s|
417
+ used_col_aliases = {} # Used to make sure there is not a name clash
418
+ bt_columns = klass._br_bt_descrip.each_with_object([]) do |v, s|
384
419
  v.last.each do |k1, v1| # k1 is class, v1 is array of columns to snag
385
420
  next if chains[k1].nil?
386
421
 
@@ -391,7 +426,12 @@ module ActiveRecord
391
426
 
392
427
  # Postgres can not use DISTINCT with any columns that are XML, so for any of those just convert to text
393
428
  is_xml = is_distinct && Brick.relations[sel_col.first.table_name]&.[](:cols)&.[](sel_col.last)&.first&.start_with?('xml')
394
- selects << "\"#{field_tbl_name}\".\"#{sel_col.last}\"#{'::text' if is_xml} AS \"#{(col_alias = "_brfk_#{v.first}__#{sel_col.last}")}\""
429
+ # If it's not unique then also include the belongs_to association name before the column name
430
+ if used_col_aliases.key?(col_alias = "_brfk_#{v.first}__#{sel_col.last}")
431
+ col_alias = "_brfk_#{v.first}__#{v1[idx][-2..-1].map(&:to_s).join('__')}"
432
+ end
433
+ selects << "\"#{field_tbl_name}\".\"#{sel_col.last}\"#{'::text' if is_xml} AS \"#{col_alias}\""
434
+ used_col_aliases[col_alias] = nil
395
435
  v1[idx] << col_alias
396
436
  end
397
437
 
@@ -416,11 +456,10 @@ module ActiveRecord
416
456
  end
417
457
  end
418
458
  # Add derived table JOIN for the has_many counts
419
- hm_counts.each do |k, hm|
459
+ klass._br_hm_counts.each do |k, hm|
420
460
  associative = nil
421
461
  count_column = if hm.options[:through]
422
- fk_col = (associative = associatives[hm.name])&.foreign_key
423
- hm.foreign_key if fk_col
462
+ hm.foreign_key if (fk_col = (associative = klass._br_associatives&.[](hm.name))&.foreign_key)
424
463
  else
425
464
  fk_col = hm.foreign_key
426
465
  poly_type = hm.inverse_of.foreign_type if hm.options.key?(:as)
@@ -450,6 +489,25 @@ JOIN (SELECT #{selects.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}
450
489
  joins!("#{join_clause} ON #{on_clause.join(' AND ')}")
451
490
  end
452
491
  where!(wheres) unless wheres.empty?
492
+ # Must parse the order_by and see if there are any symbols which refer to BT associations
493
+ # as they must be expanded to find the corresponding _br_model__column naming for each.
494
+ if order_by.present?
495
+ final_order_by = *order_by.each_with_object([]) do |v, s|
496
+ if v.is_a?(Symbol)
497
+ # Add the ordered series of columns derived from the BT based on its DSL
498
+ if (bt_cols = klass._br_bt_descrip[v])
499
+ bt_cols.values.each do |v1|
500
+ v1.each { |v2| s << v2.last if v2.length > 1 }
501
+ end
502
+ else
503
+ s << v
504
+ end
505
+ else # String stuff just comes straight through
506
+ s << v
507
+ end
508
+ end
509
+ order!(*final_order_by)
510
+ end
453
511
  limit!(1000) # Don't want to get too carried away just yet
454
512
  wheres unless wheres.empty? # Return the specific parameters that we did use
455
513
  end
@@ -489,12 +547,23 @@ JOIN (SELECT #{selects.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}
489
547
  this_module.const_set(module_name.to_sym, Module.new)
490
548
  end
491
549
  end
492
- if this_module.const_defined?(class_name = module_prefixes.last.to_sym)
493
- this_module.const_get(class_name)
494
- else
495
- # Build STI subclass and place it into the namespace module
496
- this_module.const_set(class_name, klass = Class.new(self))
497
- klass
550
+ begin
551
+ if this_module.const_defined?(class_name = module_prefixes.last.to_sym)
552
+ this_module.const_get(class_name)
553
+ else
554
+ # Build STI subclass and place it into the namespace module
555
+ this_module.const_set(class_name, klass = Class.new(self))
556
+ klass
557
+ end
558
+ rescue NameError => err
559
+ if column_names.include?(inheritance_column)
560
+ puts "Table #{table_name} has column #{inheritance_column} which ActiveRecord expects to use as its special inheritance column."
561
+ puts "Unfortunately the value \"#{type_name}\" does not seem to refer to a valid type name, greatly confusing matters. If that column is intended to be used for data and not STI, consider putting this line into your Brick initializer so that only for this table that column will not clash with ActiveRecord:"
562
+ puts " Brick.sti_type_column = { 'rails_#{inheritance_column}' => ['#{table_name}'] }"
563
+ self
564
+ else
565
+ raise
566
+ end
498
567
  end
499
568
  end
500
569
  end
@@ -545,6 +614,8 @@ Module.class_exec do
545
614
  return possible
546
615
  end
547
616
  class_name = args.first.to_s
617
+ # self.name is nil when a model name is requested in an .erb file
618
+ base_module = (self < ActiveRecord::Migration || !self.name) ? Object : self
548
619
  # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
549
620
  # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
550
621
  # that is, checking #qualified_name_for with: from_mod, const_name
@@ -552,14 +623,17 @@ Module.class_exec do
552
623
  # path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
553
624
  # return self._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
554
625
  # If the file really exists, go and snag it:
555
- if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = (self.name || class_name)&.split('::'))
556
- filepath = (filepath[0..-2] + [class_name]).join('/').underscore + '.rb'
557
- end
558
- if is_found
559
- return self._brick_const_missing(*args)
560
- # elsif ActiveSupport::Dependencies.search_for_file(filepath) # Last-ditch effort to pick this thing up before we fill in the gaps on our own
561
- # my_const = parent.const_missing(class_name) # ends up having: MyModule::MyClass
562
- # return my_const
626
+ if ActiveSupport::Dependencies.search_for_file(class_name.underscore)
627
+ return base_module._brick_const_missing(*args)
628
+ # elsif ActiveSupport::Dependencies.search_for_file(filepath) # Last-ditch effort to pick this thing up before we fill in the gaps on our own
629
+ # my_const = parent.const_missing(class_name) # ends up having: MyModule::MyClass
630
+ # return my_const
631
+ else
632
+ filepath = base_module.name&.split('::')&.[](0..-2) unless base_module == Object
633
+ filepath = ((filepath || []) + [class_name]).join('/').underscore + '.rb'
634
+ if ActiveSupport::Dependencies.search_for_file(filepath) # Last-ditch effort to pick this thing up before we fill in the gaps on our own
635
+ return base_module._brick_const_missing(*args)
636
+ end
563
637
  end
564
638
 
565
639
  relations = ::Brick.relations
@@ -578,7 +652,7 @@ Module.class_exec do
578
652
  Object.send(:build_controller, self, class_name, plural_class_name, model, relations)
579
653
  end
580
654
  elsif (::Brick.enable_models? || ::Brick.enable_controllers?) && # Schema match?
581
- self == Object && # %%% This works for Person::Person -- but also limits us to not being able to allow more than one level of namespacing
655
+ base_module == Object && # %%% This works for Person::Person -- but also limits us to not being able to allow more than one level of namespacing
582
656
  (schema_name = [(singular_table_name = class_name.underscore),
583
657
  (table_name = singular_table_name.pluralize),
584
658
  class_name,
@@ -586,7 +660,7 @@ Module.class_exec do
586
660
  (::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}::") && class_name))
587
661
  # Build out a module for the schema if it's namespaced
588
662
  # schema_name = schema_name.camelize
589
- self.const_set(schema_name.to_sym, (built_module = Module.new))
663
+ base_module.const_set(schema_name.to_sym, (built_module = Module.new))
590
664
 
591
665
  [built_module, "module #{schema_name}; end\n"]
592
666
  # # %%% Perhaps an option to use the first module just as schema, and additional modules as namespace with a table name prefix applied
@@ -596,8 +670,8 @@ Module.class_exec do
596
670
  # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
597
671
  # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
598
672
 
599
- if (base_model = ::Brick.config.sti_namespace_prefixes&.fetch("::#{self.name}::", nil)&.constantize) || # Are we part of an auto-STI namespace? ...
600
- self != Object # ... or otherwise already in some namespace?
673
+ if (base_model = ::Brick.config.sti_namespace_prefixes&.fetch("::#{base_module.name}::", nil)&.constantize) || # Are we part of an auto-STI namespace? ...
674
+ base_module != Object # ... or otherwise already in some namespace?
601
675
  schema_name = [(singular_schema_name = name.underscore),
602
676
  (schema_name = singular_schema_name.pluralize),
603
677
  name,
@@ -610,7 +684,7 @@ Module.class_exec do
610
684
  if base_model
611
685
  schema_name = name.underscore # For the auto-STI namespace models
612
686
  table_name = base_model.table_name
613
- Object.send(:build_model, self, inheritable_name, model_name, singular_table_name, table_name, relations, table_name)
687
+ Object.send(:build_model, base_module, inheritable_name, model_name, singular_table_name, table_name, relations, table_name)
614
688
  else
615
689
  # Adjust for STI if we know of a base model for the requested model name
616
690
  # %%% Does not yet work with namespaced model names. Perhaps prefix with plural_class_name when doing the lookups here.
@@ -635,15 +709,15 @@ Module.class_exec do
635
709
  built_class
636
710
  elsif ::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}") && !schema_name
637
711
  # module_prefixes = type_name.split('::')
638
- # path = self.name.split('::')[0..-2] + []
712
+ # path = base_module.name.split('::')[0..-2] + []
639
713
  # module_prefixes.unshift('') unless module_prefixes.first.blank?
640
714
  # candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb')
641
- self._brick_const_missing(*args)
642
- # elsif self != Object
715
+ base_module._brick_const_missing(*args)
716
+ # elsif base_module != Object
643
717
  # module_parent.const_missing(*args)
644
718
  else
645
- puts "MISSING! #{self.name} #{args.inspect} #{table_name}"
646
- self._brick_const_missing(*args)
719
+ puts "MISSING! #{base_module.name} #{args.inspect} #{table_name}"
720
+ base_module._brick_const_missing(*args)
647
721
  end
648
722
  end
649
723
  end
@@ -784,15 +858,15 @@ class Object
784
858
  "#{hmt_fk}_through_#{hm.first[:assoc_name]}"
785
859
  else # Use BT names to provide uniqueness
786
860
  if self.name.underscore.singularize == hm.first[:alternate_name]
787
- # Has previously been:
788
- # # If it folds back on itself then look at the other side
789
- # # (At this point just infer the source be the inverse of the first has_many that
790
- # # we find that is not ourselves. If there are more than two then uh oh, can't
791
- # # yet handle that rare circumstance!)
792
- # other = hms.find { |hm1| hm1 != hm } # .first[:fk]
793
- # options[:source] = other.first[:inverse][:assoc_name].to_sym
794
- # And also has been:
795
- # hm.first[:inverse][:assoc_name].to_sym
861
+ # Has previously been:
862
+ # # If it folds back on itself then look at the other side
863
+ # # (At this point just infer the source be the inverse of the first has_many that
864
+ # # we find that is not ourselves. If there are more than two then uh oh, can't
865
+ # # yet handle that rare circumstance!)
866
+ # other = hms.find { |hm1| hm1 != hm } # .first[:fk]
867
+ # options[:source] = other.first[:inverse][:assoc_name].to_sym
868
+ # And also has been:
869
+ # hm.first[:inverse][:assoc_name].to_sym
796
870
  options[:source] = hm.last.to_sym
797
871
  else
798
872
  through = hm.first[:alternate_name].pluralize
@@ -926,21 +1000,26 @@ class Object
926
1000
  (namespace || Object).const_set(class_name.to_sym, new_controller_class)
927
1001
 
928
1002
  # Brick-specific pages
929
- if plural_class_name == 'BrickGem'
1003
+ case plural_class_name
1004
+ when 'BrickGem'
930
1005
  self.define_method :orphans do
931
1006
  instance_variable_set(:@orphans, ::Brick.find_orphans(::Brick.set_db_schema(params)))
932
1007
  end
933
1008
  return [new_controller_class, code + ' # BrickGem controller']
1009
+ when 'BrickSwagger'
1010
+ is_swagger = true # if request.format == :json)
934
1011
  end
935
1012
 
936
- unless (is_swagger = plural_class_name == 'BrickSwagger') # && request.format == :json)
937
- code << " def index\n"
938
- code << " @#{table_name} = #{model.name}#{pk&.present? ? ".order(#{pk.inspect})" : '.all'}\n"
939
- code << " @#{table_name}.brick_select(params)\n"
940
- code << " end\n"
941
- end
942
1013
  self.protect_from_forgery unless: -> { self.request.format.js? }
943
1014
  self.define_method :index do
1015
+ # We do all of this now so that bt_descrip and hm_counts are available on the model early in case the user
1016
+ # wants to do an ORDER BY based on any of that
1017
+ translations = {}
1018
+ join_array = ::Brick::JoinArray.new
1019
+ is_add_bts = is_add_hms = true
1020
+ # This builds out bt_descrip and hm_counts on the model
1021
+ model._brick_calculate_bts_hms(translations, join_array) if is_add_bts || is_add_hms
1022
+
944
1023
  if is_swagger
945
1024
  json = { 'openapi': '3.0.1', 'info': { 'title': 'API V1', 'version': 'v1' },
946
1025
  'servers': [
@@ -990,6 +1069,32 @@ class Object
990
1069
  render inline: json.to_json, content_type: request.format
991
1070
  return
992
1071
  end
1072
+
1073
+ # Normal (non-swagger) request
1074
+
1075
+ # %%% Allow params to define which columns to use for order_by
1076
+ ordering = if (order_tbl = ::Brick.config.order[table_name])
1077
+ case (order_default = order_tbl[:_brick_default])
1078
+ when Array
1079
+ order_default.map { |od_part| order_tbl[od_part] || od_part }
1080
+ when Symbol
1081
+ order_tbl[order_default] || order_default
1082
+ else
1083
+ pk
1084
+ end
1085
+ else
1086
+ pk # If it's not a custom ORDER BY, just use the key
1087
+ end
1088
+ order_by, order_by_txt = model._brick_calculate_ordering(ordering)
1089
+ if (order_params = params['_brick_order']&.split(',')&.map(&:to_sym)) # Overriding the default by providing a querystring param?
1090
+ order_by, _ = model._brick_calculate_ordering(order_params, true) # Don't do the txt part
1091
+ end
1092
+
1093
+ code << " def index\n"
1094
+ code << " @#{table_name} = #{model.name}#{pk&.present? ? ".order(#{order_by_txt.join(', ')})" : '.all'}\n"
1095
+ code << " @#{table_name}.brick_select(params)\n"
1096
+ code << " end\n"
1097
+
993
1098
  ::Brick.set_db_schema(params)
994
1099
  if request.format == :csv # Asking for a template?
995
1100
  require 'csv'
@@ -1003,19 +1108,16 @@ class Object
1003
1108
  return
1004
1109
  end
1005
1110
 
1006
- quoted_table_name = model.table_name.split('.').map { |x| "\"#{x}\"" }.join('.')
1007
- order = pk.each_with_object([]) { |pk_part, s| s << "#{quoted_table_name}.\"#{pk_part}\"" }
1008
- ar_relation = order.present? ? model.order("#{order.join(', ')}") : model.all
1009
- @_brick_params = ar_relation.brick_select(params, (selects = []), (bt_descrip = {}), (hm_counts = {}), (join_array = ::Brick::JoinArray.new))
1111
+ @_brick_params = (ar_relation = model.all).brick_select(params, (selects = []), order_by, translations, join_array)
1010
1112
  # %%% Add custom HM count columns
1011
1113
  # %%% What happens when the PK is composite?
1012
- counts = hm_counts.each_with_object([]) { |v, s| s << "_br_#{v.first}._ct_ AS _br_#{v.first}_ct" }
1114
+ counts = model._br_hm_counts.each_with_object([]) { |v, s| s << "_br_#{v.first}._ct_ AS _br_#{v.first}_ct" }
1013
1115
  instance_variable_set("@#{table_name}".to_sym, ar_relation.dup._select!(*selects, *counts))
1014
1116
  if namespace && (idx = lookup_context.prefixes.index(table_name))
1015
1117
  lookup_context.prefixes[idx] = "#{namespace.name.underscore}/#{lookup_context.prefixes[idx]}"
1016
1118
  end
1017
- @_brick_bt_descrip = bt_descrip
1018
- @_brick_hm_counts = hm_counts
1119
+ @_brick_bt_descrip = model._br_bt_descrip
1120
+ @_brick_hm_counts = model._br_hm_counts
1019
1121
  @_brick_join_array = join_array
1020
1122
  end
1021
1123
 
@@ -1049,7 +1151,17 @@ class Object
1049
1151
  # end
1050
1152
 
1051
1153
  is_need_params = true
1052
- # code << " # (Define :edit, and :destroy)\n"
1154
+ # code << " # (Define :destroy)\n"
1155
+ code << " def edit\n"
1156
+ code << find_by_id
1157
+ code << " end\n"
1158
+ self.define_method :edit do
1159
+ ::Brick.set_db_schema(params)
1160
+ id = is_pk_string ? params[:id] : params[:id]&.split(/[\/,_]/)
1161
+ id = id.first if id.is_a?(Array) && id.length == 1
1162
+ instance_variable_set("@#{singular_table_name}".to_sym, model.find(id))
1163
+ end
1164
+
1053
1165
  code << " def update\n"
1054
1166
  code << find_by_id
1055
1167
  params_name = "#{singular_table_name}_params"
@@ -1083,8 +1195,12 @@ class Object
1083
1195
  if is_need_params
1084
1196
  code << "private\n"
1085
1197
  code << " def #{params_name}\n"
1198
+ permits = model.columns_hash.keys.map { |c| c.to_sym.inspect } +
1199
+ model.reflect_on_all_associations.each_with_object([]) do |assoc, s|
1200
+ s << "#{assoc.name.to_s.singularize}_ids: []" if assoc.macro == :has_many && assoc.options[:through]
1201
+ end
1086
1202
  code << " params.require(:#{require_name = model.name.underscore.tr('/', '_')
1087
- }).permit(#{model.columns_hash.keys.map { |c| c.to_sym.inspect }.join(', ')})\n"
1203
+ }).permit(#{permits.join(', ')})\n"
1088
1204
  code << " end\n"
1089
1205
  self.define_method(params_name) do
1090
1206
  params.require(require_name.to_sym).permit(model.columns_hash.keys)
@@ -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
+ # When table names have specific prefixes, automatically place them in their own module with a table_name_prefix.
37
+ ::Brick.order = app.config.brick.fetch(:order, {})
38
+
36
39
  # Skip creating a has_many association for these
37
40
  ::Brick.exclude_hms = app.config.brick.fetch(:exclude_hms, nil)
38
41
 
@@ -55,7 +58,9 @@ module Brick
55
58
  def template_exists?(*args, **options)
56
59
  (::Brick.config.add_orphans && args.first == 'orphans') ||
57
60
  _brick_template_exists?(*args, **options) ||
58
- set_brick_model(args)
61
+ # Do not auto-create a template when it's searching for an application.html.erb, which comes in like: ["edit", ["games", "application"]]
62
+ ((args[1].length == 1 || args[1][-1] != 'application') &&
63
+ set_brick_model(args))
59
64
  end
60
65
 
61
66
  def set_brick_model(find_args)
@@ -101,7 +106,7 @@ module Brick
101
106
  path_obj_name = model_name.underscore.tr('/', '_')
102
107
  table_name = obj_name.pluralize
103
108
  template_link = nil
104
- bts, hms, associatives = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
109
+ bts, hms = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
105
110
  hms_columns = [] # Used for 'index'
106
111
  skip_klass_hms = ::Brick.config.skip_index_hms[model_name] || {}
107
112
  hms_headers = hms.each_with_object([]) do |hm, s|
@@ -109,7 +114,7 @@ module Brick
109
114
  "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]}",
110
115
  (assoc_name = hm.first)]
111
116
  hm_fk_name = if hm_assoc.options[:through]
112
- associative = associatives[hm.first]
117
+ associative = @_brick_model._br_associatives[hm.first]
113
118
  tbl_nm = if hm_assoc.options[:source]
114
119
  associative.klass.reflect_on_association(hm_assoc.options[:source]).inverse_of&.name
115
120
  else
@@ -163,12 +168,12 @@ module Brick
163
168
  # %%% If we are not auto-creating controllers (or routes) then omit by default, and if enabled anyway, such as in a development
164
169
  # environment or whatever, then get either the controllers or routes list instead
165
170
  apartment_default_schema = ::Brick.apartment_multitenant && Apartment.default_schema
166
- table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables).map do |tbl|
171
+ table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables).each_with_object({}) do |tbl, s|
167
172
  if (tbl_parts = tbl.split('.')).first == apartment_default_schema
168
173
  tbl = tbl_parts.last
169
174
  end
170
- tbl
171
- end.sort.each_with_object(+'') do |v, s|
175
+ s[tbl] = nil
176
+ end.keys.sort.each_with_object(+'') do |v, s|
172
177
  s << "<option value=\"#{v.underscore.gsub('.', '/').pluralize}\">#{v}</option>"
173
178
  end.html_safe
174
179
  table_options << '<option value="brick_orphans">(Orphans)</option>'.html_safe if is_orphans
@@ -199,6 +204,9 @@ table thead tr th, table tr th {
199
204
  color: #fff;
200
205
  text-align: left;
201
206
  }
207
+ #headerTop th:hover, #headerTop th:hover {
208
+ background-color: #18B090;
209
+ }
202
210
  table thead tr th a, table tr th a {
203
211
  color: #80FFB8;
204
212
  }
@@ -547,30 +555,33 @@ if (headerTop) {
547
555
  <br>
548
556
  <table id=\"headerTop\">
549
557
  <table id=\"#{table_name}\">
550
- <thead><tr>#{'<th></th>' if pk.present?}<%
558
+ <thead><tr>#{"<th x-order=\"#{pk.join(',')}\"></th>" if pk.present?}<%
551
559
  col_order = []
552
560
  @#{table_name}.columns.each do |col|
553
561
  next if (#{(pk || []).inspect}.include?(col_name = col.name) && col.type == :integer && !bts.key?(col_name)) ||
554
562
  ::Brick.config.metadata_columns.include?(col_name) || poly_cols.include?(col_name)
555
563
 
556
564
  col_order << col_name
557
- %><th<%= \" title = \\\"#\{col.comment}\\\"\".html_safe if col.respond_to?(:comment) && !col.comment.blank? %>><%
565
+ %><th<%= \" title=\\\"#\{col.comment}\\\"\".html_safe if col.respond_to?(:comment) && !col.comment.blank? %><%
558
566
  if (bt = bts[col_name]) %>
559
- BT <%
567
+ <%= \" x-order=\\\"#\{bt.first}\\\"\".html_safe unless bt[2] # Allow sorting any BT except polymorphics
568
+ %>>BT <%
560
569
  bt[1].each do |bt_pair| %><%=
561
570
  bt_pair.first.bt_link(bt.first) %> <%
562
571
  end %><%
563
- else %><%=
564
- col_name %><%
572
+ else %><%= \" x-order=\\\"#\{col_name}\\\"\".html_safe if true # Currently we always allow click to sort
573
+ %>><%= col_name %><%
565
574
  end
566
575
  %></th><%
567
576
  end
568
577
  # Consider getting the name from the association -- h.first.name -- if a more \"friendly\" alias should be used for a screwy table name
569
578
  %>#{hms_headers.map do |h|
579
+ # Currently we always allow click to sort
580
+ "<th#{" x-order=\"#{h.first.name}\"" if true}>" +
570
581
  if h.first.options[:through] && !h.first.through_reflection
571
- "<th>#{h[1]} #{h[2]} %></th>" # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
582
+ "#{h[1]} #{h[2]} %></th>" # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
572
583
  else
573
- "<th>#{h[1]} <%= link_to('#{h[2]}', #{h.first.klass.name.underscore.tr('/', '_').pluralize}_path) %></th>"
584
+ "#{h[1]} <%= link_to('#{h[2]}', #{h.first.klass.name.underscore.tr('/', '_').pluralize}_path) %></th>"
574
585
  end
575
586
  end.join
576
587
  }</tr></thead>
@@ -658,7 +669,7 @@ end
658
669
  <tr>
659
670
  <% next if (#{(pk || []).inspect}.include?(k) && !bts.key?(k)) ||
660
671
  ::Brick.config.metadata_columns.include?(k) %>
661
- <th class=\"show-field\"<%= \" title = \\\"#\{col.comment}\\\"\".html_safe if col.respond_to?(:comment) && !col.comment.blank? %>>
672
+ <th class=\"show-field\"<%= \" title=\\\"#\{col.comment}\\\"\".html_safe if col.respond_to?(:comment) && !col.comment.blank? %>>
662
673
  <% has_fields = true
663
674
  if (bt = bts[k])
664
675
  # Add a final member in this array with descriptive options to be used in <select> drop-downs
@@ -729,9 +740,14 @@ end
729
740
  <% when :uuid %>
730
741
  <%=
731
742
  # Postgres naturally uses the +uuid_generate_v4()+ function from the uuid-ossp extension
732
- # If it's not yet enabled then: enable_extension 'uuid-ossp'
743
+ # If it's not yet enabled then: create extension \"uuid-ossp\";
733
744
  # ActiveUUID gem created a new :uuid type
734
745
  val %>
746
+ <% when :ltree %>
747
+ <%=
748
+ # In Postgres labels of data stored in a hierarchical tree-like structure
749
+ # If it's not yet enabled then: create extension ltree;
750
+ val %>
735
751
  <% when :binary, :primary_key %>
736
752
  <% end %>
737
753
  <% end %>
@@ -789,6 +805,15 @@ flatpickr(\".timepicker\", {enableTime: true, noCalendar: true});
789
805
  </script>
790
806
  <% end %>
791
807
  <script>
808
+ <% # Make column headers sort when clicked
809
+ # %%% Create a smart javascript routine which can do this client-side %>
810
+ [... document.getElementsByTagName(\"TH\")].forEach(function (th) {
811
+ th.addEventListener(\"click\", function (e) {
812
+ var xOrder;
813
+ if (xOrder = this.getAttribute(\"x-order\"))
814
+ location.href = changeout(location.href, \"_brick_order\", xOrder);
815
+ });
816
+ });
792
817
  document.querySelectorAll(\"input, select\").forEach(function (inp) {
793
818
  var origVal = getInpVal(),
794
819
  prevVal = origVal;
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 48
8
+ TINY = 51
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
@@ -172,14 +172,14 @@ module Brick
172
172
  # Mark has_manys that go to an associative ("join") table so that they are skipped in the UI,
173
173
  # as well as any possible polymorphic associations
174
174
  skip_hms = {}
175
- associatives = hms.each_with_object({}) do |hmt, s|
175
+ hms.each do |hmt|
176
176
  if (through = hmt.last.options[:through])
177
177
  skip_hms[through] = nil # if hms[through]
178
178
  # binding.pry if !hms[through]
179
179
  # End up with a hash of HMT names pointing to join-table associations
180
180
  # Last part was: hmt.last.name
181
181
  # Changed up because looking for: hms[:issue_issue_duplicates]
182
- s[hmt.first] = hms[through] # || hms["#{(opt = hmt.last.options)[:through].to_s.singularize}_#{opt[:source].to_s.pluralize}".to_sym]
182
+ model._br_associatives[hmt.first] = hms[through] # || hms["#{(opt = hmt.last.options)[:through].to_s.singularize}_#{opt[:source].to_s.pluralize}".to_sym]
183
183
  elsif hmt.last.inverse_of.nil?
184
184
  puts "SKIPPING #{hmt.last.name.inspect}"
185
185
  # %%% If we don't do this then below associative.name will find that associative is nil
@@ -187,7 +187,7 @@ module Brick
187
187
  end
188
188
  end
189
189
  skip_hms.each { |k, _v| hms.delete(k) }
190
- [bts, hms, associatives]
190
+ [bts, hms]
191
191
  end
192
192
 
193
193
  # Switches Brick auto-models on or off, for all threads
@@ -283,6 +283,11 @@ module Brick
283
283
  end
284
284
  end
285
285
 
286
+ # @api public
287
+ def order=(value)
288
+ Brick.config.order = value
289
+ end
290
+
286
291
  # Skip creating a has_many association for these
287
292
  # (Uses the same exact three-part format as would define an additional_reference)
288
293
  # @api public
@@ -159,6 +159,41 @@ module Brick
159
159
  # # When table names have specific prefixes automatically place them in their own module with a table_name_prefix.
160
160
  # Brick.table_name_prefixes = { 'nav_' => 'Navigation' }
161
161
 
162
+ # # COLUMN SEQUENCING AND INCLUSION / EXCLUSION
163
+
164
+ # # By default if there is a primary key present then rows in an index view are ordered by this primary key. To
165
+ # # use a different rule for doing ORDER BY, you can override this default ordering done by The Brick, for instance
166
+ # # to have the rows in a contact list sorted by email:
167
+ # Brick.order = { 'contacts' => { _brick_default: :email } }
168
+ # # or by last name then first name:
169
+ # Brick.order = { 'contacts' => { _brick_default: [:lastname, :firstname] } }
170
+ # # Totally legitimate to have the default order be the name of a belongs_to or has_many association instead of an
171
+ # # actual column name, in which case for has_many it just orders by the count of how many records are associated,
172
+ # # and for belongs_to it's based on the primary table's DSL if any is defined (since that is what is used to
173
+ # # calculate what is shown when a foreign table lists out related records). If contacts relates to addresses,
174
+ # # then this is perfectly fine:
175
+ # Brick.order = { 'contacts' => { _brick_default: :address } }
176
+ # # You can even have a specific custom clause used in the ORDER BY. In this case it is recommended to include a
177
+ # # special placeholder for the table name with the sequence \"^^^\". Here is an example of having the default
178
+ # # ordering happening on the \"code\" column, and also defining custom sorting to be done, in this case proper
179
+ # # ordering if that code is stored as a dotted numeric value:
180
+ # Brick.order = { 'document_trees' => { _brick_default: :code,
181
+ # code: \"ORDER BY STRING_TO_ARRAY(^^^.code, '.')::int[]\" } }
182
+
183
+ # # Sequence of columns for each model. This also allows you to add read-only calculated columns in the same
184
+ # # kind of way that they can be added in the include: portion of include/exclude columns, below.
185
+ # # Designated by { <table name> => [<column name>, <column name>] }
186
+ # Brick.column_sequence = { 'users' => ['email', 'profile.firstname', 'profile.lastname'] }
187
+
188
+ # # Specific columns to include or exclude for each model. If there are only inclusions then only those
189
+ # # columns show. If there are any exclusions then all non-excluded columns are attempted to be shown,
190
+ # # which negates the usefulness of inclusions except to add calculated column detail built from DSL.
191
+ # # Designated by <table name>.<column name>
192
+ # Brick.column_sequence = { 'users' => { include: ['email', 'profile.firstname', 'profile.lastname'] },
193
+ # 'profile' => { exclude: ['birthdate'] } }
194
+
195
+ # # EXTRA FOREIGN KEYS AND OTHER HAS_MANY SETTINGS
196
+
162
197
  # # Additional table references which are used to create has_many / belongs_to associations inside auto-created
163
198
  # # models. (You can consider these to be \"virtual foreign keys\" if you wish)... You only have to add these
164
199
  # # in cases where your database for some reason does not have foreign key constraints defined. Sometimes for
@@ -176,8 +211,9 @@ module Brick
176
211
  # Brick.exclude_hms = [['users', 'favourite_colour_id', 'colours']]
177
212
 
178
213
  # # Skip showing counts for these specific has_many associations when building auto-generated #index views.
179
- # # When there are related tables with a significant number of records, this can lessen the load on the database
180
- # # considerably, sometimes fixing what might appear to be an index page that just \"hangs\" for no apparent reason.
214
+ # # When there are related tables with a significant number of records (generally 100,000 or more), this can lessen
215
+ # # the load on the database considerably, sometimes fixing what might appear to be an index page that just \"hangs\"
216
+ # # for no apparent reason.
181
217
  # Brick.skip_index_hms = ['User.litany_of_woes']
182
218
 
183
219
  # # By default primary tables involved in a foreign key relationship will indicate a \"has_many\" relationship pointing
@@ -199,6 +235,8 @@ module Brick
199
235
  # # Designated by <table name>.<column name>
200
236
  # Brick.not_nullables = ['users.name']
201
237
 
238
+ # # FRIENDLY DSL
239
+
202
240
  # # A simple DSL is available to allow more user-friendly display of objects. Normally a user object might be shown
203
241
  # # as its first non-metadata column, or if that is not available then something like \"User #45\" where 45 is that
204
242
  # # object's ID. If there is no primary key then even that is not possible, so the object's .to_s method is called.
@@ -206,6 +244,8 @@ module Brick
206
244
  # # user, then you can use model_descrips like this, putting expressions with property references in square brackets:
207
245
  # Brick.model_descrips = { 'User' => '[profile.firstname] [profile.lastname]' }
208
246
 
247
+ # # SINGLE TABLE INHERITANCE
248
+
209
249
  # # Specify STI subclasses either directly by name or as a general module prefix that should always relate to a specific
210
250
  # # parent STI class. The prefixed :: here for these examples is mandatory. Also having a suffixed :: means instead of
211
251
  # # a class reference, this is for a general namespace reference. So in this case requests for, say, either of the
@@ -221,6 +261,8 @@ module Brick
221
261
  # Brick.sti_type_column = 'sti_type'
222
262
  # Brick.sti_type_column = { 'rails_type' => ['sales.specialoffer'] }
223
263
 
264
+ # # POLYMORPHIC ASSOCIATIONS
265
+
224
266
  # # Database schema to use when analysing existing data, such as deriving a list of polymorphic classes in the case that
225
267
  # # it wasn't originally specified.
226
268
  # Brick.schema_behavior = :namespaced
@@ -231,6 +273,8 @@ module Brick
231
273
 
232
274
  # # Polymorphic associations are set up by providing a model name and polymorphic association name#{poly}
233
275
 
276
+ # # DEFAULT ROOT ROUTE
277
+
234
278
  # # If a default route is not supplied, Brick attempts to find the most \"central\" table and wires up the default
235
279
  # # route to go to the :index action for what would be a controller for that table. You can specify any controller
236
280
  # # name and action you wish in order to override this and have that be the default route when none other has been
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.48
4
+ version: 1.0.51
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorin Thwaits
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-20 00:00:00.000000000 Z
11
+ date: 2022-08-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord