brick 1.0.31 → 1.0.34

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: f529097f82af103af54ae472e226244ce8932b64cf974b836d1d23be6cedfbce
4
- data.tar.gz: 1561bea9cdcdd366d2ef9798cff36f3a74f58768f1f14be5c2bfbf4f40feac8d
3
+ metadata.gz: 9d1e2f8b6ff7ca3fa7fcd129c33de3dfe6e674b6da154b9a64e094c65a3a152a
4
+ data.tar.gz: 30e39f62458a46854f56b2a6decbfe5ea42a4bb5325e81ed76f21400aae8e252
5
5
  SHA512:
6
- metadata.gz: a5d812af4ba7eadae06e28bb9686ea60b79f9df721a51cbddbb0ce87d6abfe78e0675b45446768bd2460f00c1f02bba5276d21d9ec96345dcffa8079d87f2ec0
7
- data.tar.gz: b7ff59c6229dac732eb06a01c59d4a2913675b02a0a8a54106c2dfdcf965ea58cff5dc6d8dfb1c54fb8d9569fa31f2d8217ed281593bdbc167e89d42178225dd
6
+ metadata.gz: 14c6bae09e34f53126e2003cbc7256c4b32f4a4219d42f1f7e94ca83211debf3704fa6c170cd6231d89e26eb168f282d1dea7eff7150ef1a013364aeb4eb65c3
7
+ data.tar.gz: 7f33aa5c89be2e8401f4cda0ea6cecb6e01639934a2df1e7b00bd88d065bf3cc9b748bebb1691d51c1b04114fba3b1739a7820c7614f4eb84d52494bd4cc769e
@@ -86,7 +86,11 @@ module ActiveRecord
86
86
  descrip_col = (columns.map(&:name) - _brick_get_fks -
87
87
  (::Brick.config.metadata_columns || []) -
88
88
  [primary_key]).first
89
- dsl = ::Brick.config.model_descrips[name] = "[#{descrip_col}]" if descrip_col
89
+ dsl = ::Brick.config.model_descrips[name] = if descrip_col
90
+ "[#{descrip_col}]"
91
+ elsif (pk_parts = self.primary_key.is_a?(Array) ? self.primary_key : [self.primary_key])
92
+ "#{name} ##{pk_parts.map { |pk_part| "[#{pk_part}]" }.join(', ')}"
93
+ end
90
94
  end
91
95
  dsl
92
96
  end
@@ -104,7 +108,8 @@ module ActiveRecord
104
108
  if ch == ']' # Time to process a bracketed thing?
105
109
  parts = bracket_name.split('.')
106
110
  first_parts = parts[0..-2].map do |part|
107
- klass = klass.reflect_on_association(part_sym = part.to_sym).klass
111
+ klass = (orig_class = klass).reflect_on_association(part_sym = part.to_sym)&.klass
112
+ puts "Couldn't reference #{orig_class.name}##{part} that's part of the DSL \"#{dsl}\"." if klass.nil?
108
113
  part_sym
109
114
  end
110
115
  parts = prefix + first_parts + [parts[-1]]
@@ -128,7 +133,7 @@ module ActiveRecord
128
133
  end
129
134
  else # With no DSL available, still put this prefix into the JoinArray so we can get primary key (ID) info from this table
130
135
  x = prefix.each_with_object(build_array) { |v, s| s[v.to_sym] }
131
- x[prefix[-1]] = nil unless prefix.empty? # Using []= will "hydrate" any missing part(s) in our whole series
136
+ x[prefix.last] = nil unless prefix.empty? # Using []= will "hydrate" any missing part(s) in our whole series
132
137
  end
133
138
  members
134
139
  end
@@ -158,6 +163,7 @@ module ActiveRecord
158
163
  bracket_name.split('.').each do |part|
159
164
  obj_name += ".#{part}"
160
165
  this_obj = caches.fetch(obj_name) { caches[obj_name] = this_obj&.send(part.to_sym) }
166
+ break if this_obj.nil?
161
167
  end
162
168
  this_obj&.to_s || ''
163
169
  end
@@ -251,13 +257,12 @@ module ActiveRecord
251
257
  names << [piece.right._arel_table_type, (piece.right.table_alias || piece.right.name)]
252
258
  else # "Normal" setup, fed from a JoinSource which has an array of JOINs
253
259
  # The left side is the "JOIN" table
254
- names += _recurse_arel(piece.left)
260
+ names += _recurse_arel(table = piece.left)
255
261
  # The expression on the right side is the "ON" clause
256
262
  # on = piece.right.expr
257
263
  # # Find the table which is not ourselves, and thus must be the "path" that led us here
258
264
  # parent = piece.left == on.left.relation ? on.right.relation : on.left.relation
259
265
  # binding.pry if piece.left.is_a?(Arel::Nodes::TableAlias)
260
- table = piece.left
261
266
  if table.is_a?(Arel::Nodes::TableAlias)
262
267
  alias_name = table.right
263
268
  table = table.left
@@ -276,7 +281,8 @@ module ActiveRecord
276
281
  @_brick_chains = {}
277
282
  # The left side is the "FROM" table
278
283
  # names += _recurse_arel(piece.left)
279
- names << [piece.left._arel_table_type, (piece.left.table_alias || piece.left.name)]
284
+ names << (this_name = [piece.left._arel_table_type, (piece.left.table_alias || piece.left.name)])
285
+ (_brick_chains[this_name.first] ||= []) << this_name.last
280
286
  # The right side is an array of all JOINs
281
287
  piece.right.each { |join| names << _recurse_arel(join) }
282
288
  end
@@ -303,6 +309,23 @@ module ActiveRecord
303
309
  # , is_add_bts, is_add_hms
304
310
  )
305
311
  is_add_bts = is_add_hms = true
312
+ is_distinct = nil
313
+ wheres = {}
314
+ params.each do |k, v|
315
+ case (ks = k.split('.')).length
316
+ when 1
317
+ next unless klass._brick_get_fks.include?(k)
318
+ when 2
319
+ assoc_name = ks.first.to_sym
320
+ # Make sure it's a good association name and that the model has that column name
321
+ next unless klass.reflect_on_association(assoc_name)&.klass&.column_names&.any?(ks.last)
322
+
323
+ join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
324
+ is_distinct = true
325
+ distinct!
326
+ end
327
+ wheres[k] = v.split(',')
328
+ end
306
329
 
307
330
  # %%% Skip the metadata columns
308
331
  if selects&.empty? # Default to all columns
@@ -311,7 +334,9 @@ module ActiveRecord
311
334
  if (col_name = col.name) == 'class'
312
335
  col_alias = ' AS _class'
313
336
  end
314
- selects << "\"#{tbl_no_schema}\".\"#{col_name}\"#{col_alias}"
337
+ # Postgres can not use DISTINCT with any columns that are XML, so for any of those just convert to text
338
+ cast_as_text = '::text' if is_distinct && Brick.relations[klass.table_name]&.[](:cols)&.[](col.name)&.first&.start_with?('xml')
339
+ selects << "\"#{tbl_no_schema}\".\"#{col_name}\"#{cast_as_text}#{col_alias}"
315
340
  end
316
341
  end
317
342
 
@@ -337,21 +362,6 @@ module ActiveRecord
337
362
  end
338
363
  end
339
364
 
340
- wheres = {}
341
- params.each do |k, v|
342
- case (ks = k.split('.')).length
343
- when 1
344
- next unless klass._brick_get_fks.include?(k)
345
- when 2
346
- assoc_name = ks.first.to_sym
347
- # Make sure it's a good association name and that the model has that column name
348
- next unless klass.reflect_on_association(assoc_name)&.klass&.column_names&.any?(ks.last)
349
-
350
- join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
351
- end
352
- wheres[k] = v.split(',')
353
- end
354
-
355
365
  if join_array.present?
356
366
  left_outer_joins!(join_array)
357
367
  # Without working from a duplicate, touching the AREL ast tree sets the @arel instance variable, which causes the relation to be immutable.
@@ -369,7 +379,9 @@ module ActiveRecord
369
379
  v1.map { |x| [translations[x[0..-2].map(&:to_s).join('.')], x.last] }.each_with_index do |sel_col, idx|
370
380
  field_tbl_name = (field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])).split('.').last
371
381
 
372
- selects << "#{"\"#{field_tbl_name}\".\"#{sel_col.last}\""} AS \"#{(col_alias = "_brfk_#{v.first}__#{sel_col.last}")}\""
382
+ # Postgres can not use DISTINCT with any columns that are XML, so for any of those just convert to text
383
+ is_xml = is_distinct && Brick.relations[sel_col.first.table_name]&.[](:cols)&.[](sel_col.last)&.first&.start_with?('xml')
384
+ selects << "\"#{field_tbl_name}\".\"#{sel_col.last}\"#{'::text' if is_xml} AS \"#{(col_alias = "_brfk_#{v.first}__#{sel_col.last}")}\""
373
385
  v1[idx] << col_alias
374
386
  end
375
387
 
@@ -397,6 +409,7 @@ module ActiveRecord
397
409
  hm_counts.each do |k, hm|
398
410
  associative = nil
399
411
  count_column = if hm.options[:through]
412
+ # binding.pry if associatives[hm.name].nil?
400
413
  fk_col = (associative = associatives[hm.name]).foreign_key
401
414
  hm.foreign_key
402
415
  else
@@ -420,11 +433,13 @@ module ActiveRecord
420
433
  on_clause << "#{tbl_alias}.#{poly_type} = '#{name}'"
421
434
  end
422
435
  join_clause = "LEFT OUTER
423
- JOIN (SELECT #{selects.join(', ')}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.table_name || hm.klass.table_name} GROUP BY #{(1..selects.length).to_a.join(', ')}) AS #{tbl_alias}"
436
+ JOIN (SELECT #{selects.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}#{count_column
437
+ }) AS _ct_ FROM #{associative&.table_name || hm.klass.table_name
438
+ } GROUP BY #{(1..selects.length).to_a.join(', ')}) AS #{tbl_alias}"
424
439
  joins!("#{join_clause} ON #{on_clause.join(' AND ')}")
425
440
  end
426
441
  where!(wheres) unless wheres.empty?
427
- limit(1000) # Don't want to get too carried away just yet
442
+ limit!(1000) # Don't want to get too carried away just yet
428
443
  wheres unless wheres.empty? # Return the specific parameters that we did use
429
444
  end
430
445
 
@@ -485,7 +500,9 @@ if ActiveSupport::Dependencies.respond_to?(:autoload_module!) # %%% Only works w
485
500
  alias _brick_autoload_module! autoload_module!
486
501
  def autoload_module!(*args)
487
502
  into, const_name, qualified_name, path_suffix = args
488
- if (base_class = ::Brick.config.sti_namespace_prefixes&.fetch("::#{into.name}::", nil)&.constantize)
503
+ base_class_name = ::Brick.config.sti_namespace_prefixes&.fetch("::#{into.name}::", nil)
504
+ base_class_name = "::#{base_class_name}" unless base_class_name.start_with?('::')
505
+ if (base_class = base_class_name&.constantize)
489
506
  ::Brick.sti_models[qualified_name] = { base: base_class }
490
507
  # Build subclass and place it into the specially STI-namespaced module
491
508
  into.const_set(const_name.to_sym, klass = Class.new(base_class))
@@ -506,8 +523,13 @@ end
506
523
  Module.class_exec do
507
524
  alias _brick_const_missing const_missing
508
525
  def const_missing(*args)
509
- if (self.const_defined?(args.first) && (possible = self.const_get(args.first)) != self) ||
510
- (self != Object && Object.const_defined?(args.first) && (possible = Object.const_get(args.first)) != self)
526
+ if (self.const_defined?(args.first) && (possible = self.const_get(args.first)) && possible != self) ||
527
+ (self != Object && Object.const_defined?(args.first) &&
528
+ (
529
+ (possible = Object.const_get(args.first)) &&
530
+ (possible != self || (possible == self && possible.is_a?(Class)))
531
+ )
532
+ )
511
533
  return possible
512
534
  end
513
535
  class_name = args.first.to_s
@@ -533,51 +555,64 @@ Module.class_exec do
533
555
  result = if ::Brick.enable_controllers? && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
534
556
  # Otherwise now it's up to us to fill in the gaps
535
557
  # (Go over to underscores for a moment so that if we have something come in like VABCsController then the model name ends up as
536
- # VAbc instead of VABC)
558
+ # Vabc instead of VABC)
537
559
  full_class_name = +''
538
560
  full_class_name << "::#{self.name}" unless self == Object
539
561
  full_class_name << "::#{plural_class_name.underscore.singularize.camelize}"
540
- if (model = self.const_get(full_class_name))
562
+ if (plural_class_name == 'BrickSwagger' || model = self.const_get(full_class_name))
541
563
  # if it's a controller and no match or a model doesn't really use the same table name, eager load all models and try to find a model class of the right name.
542
564
  Object.send(:build_controller, self, class_name, plural_class_name, model, relations)
543
565
  end
544
566
  elsif (::Brick.enable_models? || ::Brick.enable_controllers?) && # Schema match?
545
567
  self == Object && # %%% This works for Person::Person -- but also limits us to not being able to allow more than one level of namespacing
546
- schema_name = [(singular_table_name = class_name.underscore),
547
- (table_name = singular_table_name.pluralize),
548
- class_name,
549
- (plural_class_name = class_name.pluralize)].find { |s| Brick.db_schemas.include?(s) }
568
+ (schema_name = [(singular_table_name = class_name.underscore),
569
+ (table_name = singular_table_name.pluralize),
570
+ class_name,
571
+ (plural_class_name = class_name.pluralize)].find { |s| Brick.db_schemas.include?(s) }&.camelize ||
572
+ (::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}::") && class_name))
550
573
  # Build out a module for the schema if it's namespaced
551
- schema_name = schema_name.camelize
574
+ # schema_name = schema_name.camelize
552
575
  self.const_set(schema_name.to_sym, (built_module = Module.new))
553
576
 
554
577
  [built_module, "module #{schema_name}; end\n"]
555
578
  # # %%% Perhaps an option to use the first module just as schema, and additional modules as namespace with a table name prefix applied
556
579
  elsif ::Brick.enable_models?
580
+ # Custom inheritable Brick base model?
581
+ class_name = (inheritable_name = class_name)[5..-1] if class_name.start_with?('Brick')
557
582
  # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
558
583
  # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
559
584
 
560
- unless self == Object # Are we in some namespace?
585
+ if (base_model = ::Brick.config.sti_namespace_prefixes&.fetch("::#{name}::", nil)&.constantize) || # Are we part of an auto-STI namespace? ...
586
+ self != Object # ... or otherwise already in some namespace?
561
587
  schema_name = [(singular_schema_name = name.underscore),
562
588
  (schema_name = singular_schema_name.pluralize),
563
589
  name,
564
590
  name.pluralize].find { |s| Brick.db_schemas.include?(s) }
565
591
  end
566
-
567
592
  plural_class_name = ActiveSupport::Inflector.pluralize(model_name = class_name)
568
593
  # If it's namespaced then we turn the first part into what would be a schema name
569
- singular_table_name = ActiveSupport::Inflector.underscore(model_name).gsub('::', '.')
570
-
571
- # Adjust for STI if we know of a base model for the requested model name
572
- table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil) || ::Brick.existing_stis[model_name]&.constantize)
573
- base_model.table_name
574
- else
575
- ActiveSupport::Inflector.pluralize(singular_table_name)
576
- end
577
-
578
- # Maybe, just maybe there's a database table that will satisfy this need
579
- if (matching = [table_name, singular_table_name, plural_class_name, model_name].find { |m| relations.key?(schema_name ? "#{schema_name}.#{m}" : m) })
580
- Object.send(:build_model, schema_name, model_name, singular_table_name, table_name, relations, matching)
594
+ singular_table_name = ActiveSupport::Inflector.underscore(model_name).gsub('/', '.')
595
+
596
+ if base_model
597
+ schema_name = name.underscore # For the auto-STI namespace models
598
+ table_name = base_model.table_name
599
+ Object.send(:build_model, self, inheritable_name, model_name, singular_table_name, table_name, relations, table_name)
600
+ else
601
+ # Adjust for STI if we know of a base model for the requested model name
602
+ # %%% Does not yet work with namespaced model names. Perhaps prefix with plural_class_name when doing the lookups here.
603
+ table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil) || ::Brick.existing_stis[model_name]&.constantize)
604
+ base_model.table_name
605
+ else
606
+ ActiveSupport::Inflector.pluralize(singular_table_name)
607
+ end
608
+ if ::Brick.config.schema_behavior[:multitenant] && Object.const_defined?('Apartment') &&
609
+ Apartment.excluded_models.include?(table_name.singularize.camelize)
610
+ schema_name = Apartment.default_schema
611
+ end
612
+ # Maybe, just maybe there's a database table that will satisfy this need
613
+ if (matching = [table_name, singular_table_name, plural_class_name, model_name].find { |m| relations.key?(schema_name ? "#{schema_name}.#{m}" : m) })
614
+ Object.send(:build_model, schema_name, inheritable_name, model_name, singular_table_name, table_name, relations, matching)
615
+ end
581
616
  end
582
617
  end
583
618
  if result
@@ -593,7 +628,7 @@ Module.class_exec do
593
628
  elsif self != Object
594
629
  module_parent.const_missing(*args)
595
630
  else
596
- puts "MISSING! mod #{self.name} #{args.inspect} #{table_name}"
631
+ puts "MISSING! #{self.name} #{args.inspect} #{table_name}"
597
632
  self._brick_const_missing(*args)
598
633
  end
599
634
  end
@@ -601,123 +636,39 @@ end
601
636
 
602
637
  class Object
603
638
  class << self
604
- # alias _brick_const_missing const_missing
605
- # def const_missing(*args)
606
- # # return self.const_get(args.first) if self.const_defined?(args.first)
607
- # # return Object.const_get(args.first) if Object.const_defined?(args.first) unless self == Object
608
- # if self.const_defined?(args.first) && (possible = self.const_get(args.first)) != self
609
- # return possible
610
- # end
611
- # if self != Object && Object.const_defined?(args.first) && (possible = Object.const_get(args.first)) != self
612
- # return possible
613
- # end
614
-
615
- # class_name = args.first.to_s
616
- # # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
617
- # # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
618
- # # that is, checking #qualified_name_for with: from_mod, const_name
619
- # # If we want to support namespacing in the future, might have to utilise something like this:
620
- # # path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
621
- # # return self._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
622
- # # If the file really exists, go and snag it:
623
- # if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = (self.name || class_name)&.split('::'))
624
- # filepath = (filepath[0..-2] + [class_name]).join('/').underscore + '.rb'
625
- # end
626
- # if is_found
627
- # return self._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
- # end
632
- # relations = ::Brick.instance_variable_get(:@relations)[ActiveRecord::Base.connection_pool.object_id] || {}
633
- # result = if ::Brick.enable_controllers? && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
634
- # # Otherwise now it's up to us to fill in the gaps
635
- # # (Go over to underscores for a moment so that if we have something come in like VABCsController then the model name ends up as
636
- # # VAbc instead of VABC)
637
- # if (model = Object.const_get(plural_class_name.underscore.singularize.camelize))
638
- # # if it's a controller and no match or a model doesn't really use the same table name, eager load all models and try to find a model class of the right name.
639
- # build_controller(nil, class_name, plural_class_name, model, relations)
640
- # end
641
- # elsif (::Brick.enable_models? || ::Brick.enable_controllers?) && # Schema match?
642
- # db_schema_name = [(singular_table_name = class_name.underscore),
643
- # (table_name = singular_table_name.pluralize),
644
- # class_name,
645
- # (plural_class_name = class_name.pluralize)].find { |s| Brick.db_schemas.include?(s) }
646
- # # Build out a module for the schema if it's namespaced
647
- # schema_name = db_schema_name.camelize
648
- # unless Object.const_defined?(schema_name.to_sym)
649
- # Object.const_set(schema_name.to_sym, (built_module = Module.new))
650
- # Brick.db_schemas[db_schema_name] = built_module
651
- # [built_module, "module #{schema_name}; end\n"]
652
- # end
653
- # # # %%% Perhaps an option to use the first module just as schema, and additional modules as namespace with a table name prefix applied
654
- # # schema_name, model_name =
655
- # # code = +''
656
- # # mod_tree = +''
657
- # # model_name.split('::')[0..-2].each do |mod_name|
658
- # # mod_tree << "::#{mod_name}"
659
- # # Module.new(mod_tree)
660
- # # code << "module #{mod_tree}; end\n"
661
- # # end
662
- # elsif ::Brick.enable_models?
663
- # # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
664
- # # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
665
- # plural_class_name = ActiveSupport::Inflector.pluralize(model_name = class_name)
666
- # singular_table_name = ActiveSupport::Inflector.underscore(model_name)
667
-
668
- # # Adjust for STI if we know of a base model for the requested model name
669
- # table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil) || ::Brick.existing_stis[model_name]&.constantize)
670
- # base_model.table_name
671
- # else
672
- # ActiveSupport::Inflector.pluralize(singular_table_name)
673
- # end
674
-
675
- # # Maybe, just maybe there's a database table that will satisfy this need
676
- # if (matching = [table_name, singular_table_name, plural_class_name, model_name].find { |m| relations.key?(m) })
677
- # build_model(nil, model_name, singular_table_name, table_name, relations, matching)
678
- # end
679
- # end
680
- # if result
681
- # built_class, code = result
682
- # puts "\n#{code}"
683
- # built_class
684
- # elsif ::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}") && !schema_name
685
- # # module_prefixes = type_name.split('::')
686
- # # path = self.name.split('::')[0..-2] + []
687
- # # module_prefixes.unshift('') unless module_prefixes.first.blank?
688
- # # candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb')
689
- # self._brick_const_missing(*args)
690
- # elsif self != Object
691
- # module_parent.const_missing(*args)
692
- # else
693
- # puts "MISSING! obj #{self.name}/#{schema_name} #{args.inspect} #{table_name}"
694
- # self._brick_const_missing(*args)
695
- # end
696
- # end
697
639
 
698
640
  private
699
641
 
700
- def build_model(schema_name, model_name, singular_table_name, table_name, relations, matching)
701
- full_name = if schema_name.blank?
702
- model_name
642
+ def build_model(schema_name, inheritable_name, model_name, singular_table_name, table_name, relations, matching)
643
+ full_name = if (::Brick.config.schema_behavior[:multitenant] && Object.const_defined?('Apartment') && schema_name == Apartment.default_schema)
644
+ relation = relations["#{schema_name}.#{matching}"]
645
+ inheritable_name || model_name
646
+ elsif schema_name.blank?
647
+ inheritable_name || model_name
703
648
  else # Prefix the schema to the table name + prefix the schema namespace to the class name
704
- schema_module = (Brick.db_schemas[schema_name] ||= self.const_get(schema_name.singularize.camelize))
705
- matching = "#{schema_name}.#{matching}"
706
- "#{schema_module&.name}::#{model_name}"
649
+ schema_module = if schema_name.instance_of?(Module) # from an auto-STI namespace?
650
+ schema_name
651
+ else
652
+ matching = "#{schema_name}.#{matching}"
653
+ (Brick.db_schemas[schema_name] ||= self.const_get(schema_name.singularize.camelize))
654
+ end
655
+ "#{schema_module&.name}::#{inheritable_name || model_name}"
707
656
  end
708
657
 
709
- return if ((is_view = (relation = relations[matching]).key?(:isView)) && ::Brick.config.skip_database_views) ||
658
+ return if ((is_view = (relation ||= relations[matching]).key?(:isView)) && ::Brick.config.skip_database_views) ||
710
659
  ::Brick.config.exclude_tables.include?(matching)
711
660
 
712
661
  # Are they trying to use a pluralised class name such as "Employees" instead of "Employee"?
713
662
  if table_name == singular_table_name && !ActiveSupport::Inflector.inflections.uncountable.include?(table_name)
714
- unless ::Brick.config.sti_namespace_prefixes&.key?("::#{singular_table_name.titleize}::")
715
- puts "Warning: Class name for a model that references table \"#{matching}\" should be \"#{ActiveSupport::Inflector.singularize(model_name)}\"."
663
+ unless ::Brick.config.sti_namespace_prefixes&.key?("::#{singular_table_name.camelize}::")
664
+ puts "Warning: Class name for a model that references table \"#{matching
665
+ }\" should be \"#{ActiveSupport::Inflector.singularize(inheritable_name || model_name)}\"."
716
666
  end
717
667
  return
718
668
  end
719
669
 
720
- if (base_model = ::Brick.sti_models[full_name]&.fetch(:base, nil) || ::Brick.existing_stis[full_name]&.constantize)
670
+ full_model_name = full_name.split('::').tap { |fn| fn[-1] = model_name }.join('::')
671
+ if (base_model = ::Brick.sti_models[full_model_name]&.fetch(:base, nil) || ::Brick.existing_stis[full_model_name]&.constantize)
721
672
  is_sti = true
722
673
  else
723
674
  base_model = ::Brick.config.models_inherit_from || ActiveRecord::Base
@@ -725,9 +676,21 @@ class Object
725
676
  hmts = nil
726
677
  code = +"class #{full_name} < #{base_model.name}\n"
727
678
  built_model = Class.new(base_model) do |new_model_class|
728
- (schema_module || Object).const_set(model_name.to_sym, new_model_class)
679
+ (schema_module || Object).const_set((inheritable_name || model_name).to_sym, new_model_class)
680
+ if inheritable_name
681
+ new_model_class.define_singleton_method :inherited do |subclass|
682
+ super(subclass)
683
+ if subclass.name == model_name
684
+ puts "#{full_model_name} properly extends from #{full_name}"
685
+ else
686
+ puts "should be \"class #{model_name} < #{inheritable_name}\"\n (not \"#{subclass.name} < #{inheritable_name}\")"
687
+ end
688
+ end
689
+ self.abstract_class = true
690
+ code << " self.abstract_class = true\n"
691
+ end
729
692
  # Accommodate singular or camel-cased table names such as "order_detail" or "OrderDetails"
730
- code << " self.table_name = '#{self.table_name = matching}'\n" unless table_name == matching
693
+ code << " self.table_name = '#{self.table_name = matching}'\n" if inheritable_name || table_name != matching
731
694
 
732
695
  # Override models backed by a view so they return true for #is_view?
733
696
  # (Dynamically-created controllers and view templates for such models will then act in a read-only way)
@@ -755,6 +718,9 @@ class Object
755
718
  code << " self.primary_key = #{pk_sym.inspect}\n"
756
719
  end
757
720
  _brick_primary_key(relation) # Set the newly-found PK in the instance variable
721
+ elsif (possible_pk = ActiveRecord::Base.get_primary_key(base_class.name)) && relation[:cols][possible_pk]
722
+ new_model_class.primary_key = (possible_pk = possible_pk.to_sym)
723
+ code << " self.primary_key = #{possible_pk.inspect}\n"
758
724
  else
759
725
  code << " # Could not identify any column(s) to use as a primary key\n" unless is_view
760
726
  end
@@ -792,10 +758,11 @@ class Object
792
758
  end # class definition
793
759
  # Having this separate -- will this now work out better?
794
760
  built_model.class_exec do
795
- hmts.each do |hmt_fk, fks|
761
+ hmts&.each do |hmt_fk, fks|
796
762
  hmt_fk = hmt_fk.tr('.', '_')
797
763
  fks.each do |fk|
798
- through = fk.first[:assoc_name]
764
+ # %%% Will not work with custom has_many name
765
+ through = ::Brick.config.schema_behavior[:multitenant] ? fk.first[:assoc_name] : fk.first[:inverse_table].tr('.', '_').pluralize
799
766
  hmt_name = if fks.length > 1
800
767
  if fks[0].first[:inverse][:assoc_name] == fks[1].first[:inverse][:assoc_name] # Same BT names pointing back to us? (Most common scenario)
801
768
  "#{hmt_fk}_through_#{fk.first[:assoc_name]}"
@@ -807,10 +774,15 @@ class Object
807
774
  else
808
775
  hmt_fk
809
776
  end
810
- source = fk.last unless hmt_name.singularize == fk.last
811
- code << " has_many :#{hmt_name}, through: #{(assoc_name = through.to_sym).to_sym.inspect}#{", source: :#{source}" if source}\n"
812
- options = { through: assoc_name }
813
- options[:source] = source.to_sym if source
777
+ options = { through: through.to_sym }
778
+ if relation[:fks].any? { |k, v| v[:assoc_name] == hmt_name }
779
+ hmt_name = "#{hmt_name.singularize}_#{fk.first[:assoc_name]}"
780
+ # binding.pry if relation[:fks].any? { |k, v| v[:assoc_name] == hmt_name }
781
+ options[:class_name] = fk.first[:inverse_table].singularize.camelize
782
+ options[:foreign_key] = fk.first[:fk].to_sym
783
+ end
784
+ options[:source] = fk.last.to_sym unless hmt_name.singularize == fk.last
785
+ code << " has_many :#{hmt_name}#{options.map { |opt| ", #{opt.first}: #{opt.last.inspect}" }.join}\n"
814
786
  self.send(:has_many, hmt_name.to_sym, **options)
815
787
  end
816
788
  end
@@ -832,6 +804,7 @@ class Object
832
804
  else
833
805
  assoc[:assoc_name]
834
806
  end
807
+ options[:optional] = true if assoc.key?(:optional)
835
808
  if assoc.key?(:polymorphic)
836
809
  options[:polymorphic] = true
837
810
  else
@@ -875,7 +848,11 @@ class Object
875
848
  end
876
849
  # Figure out if we need to specially call out the class_name and/or foreign key
877
850
  # (and if either of those then definitely also a specific inverse_of)
878
- options[:class_name] = "::#{assoc[:primary_class]&.name || singular_table_name.split('.').map(&:camelize).join('::')}" if need_class_name
851
+ if (singular_table_parts = singular_table_name.split('.')).length > 1 &&
852
+ ::Brick.config.schema_behavior[:multitenant] && singular_table_parts.first == 'public'
853
+ singular_table_parts.shift
854
+ end
855
+ options[:class_name] = "::#{assoc[:primary_class]&.name || singular_table_parts.map(&:camelize).join('::')}" if need_class_name
879
856
  # Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
880
857
  if need_fk # Funky foreign key?
881
858
  options[:foreign_key] = if assoc[:fk].is_a?(Array)
@@ -889,7 +866,6 @@ class Object
889
866
 
890
867
  # Prepare a list of entries for "has_many :through"
891
868
  if macro == :has_many
892
- puts [inverse_table, relations[inverse_table].length].inspect
893
869
  relations[inverse_table][:hmt_fks].each do |k, hmt_fk|
894
870
  next if k == assoc[:fk]
895
871
 
@@ -905,19 +881,71 @@ class Object
905
881
  def build_controller(namespace, class_name, plural_class_name, model, relations)
906
882
  table_name = ActiveSupport::Inflector.underscore(plural_class_name)
907
883
  singular_table_name = ActiveSupport::Inflector.singularize(table_name)
908
- pk = model._brick_primary_key(relations.fetch(table_name, nil))
884
+ pk = model&._brick_primary_key(relations.fetch(table_name, nil))
909
885
 
910
886
  namespace_name = "#{namespace.name}::" if namespace
911
887
  code = +"class #{namespace_name}#{class_name} < ApplicationController\n"
912
888
  built_controller = Class.new(ActionController::Base) do |new_controller_class|
913
889
  (namespace || Object).const_set(class_name.to_sym, new_controller_class)
914
890
 
915
- code << " def index\n"
916
- code << " @#{table_name} = #{model.name}#{pk&.present? ? ".order(#{pk.inspect})" : '.all'}\n"
917
- code << " @#{table_name}.brick_select(params)\n"
918
- code << " end\n"
891
+ unless (is_swagger = plural_class_name == 'BrickSwagger') # && request.format == :json)
892
+ code << " def index\n"
893
+ code << " @#{table_name} = #{model.name}#{pk&.present? ? ".order(#{pk.inspect})" : '.all'}\n"
894
+ code << " @#{table_name}.brick_select(params)\n"
895
+ code << " end\n"
896
+ end
919
897
  self.protect_from_forgery unless: -> { self.request.format.js? }
920
898
  self.define_method :index do
899
+ if is_swagger
900
+ json = { 'openapi': '3.0.1', 'info': { 'title': 'API V1', 'version': 'v1' },
901
+ 'servers': [
902
+ { 'url': 'https://{defaultHost}', 'variables': { 'defaultHost': { 'default': 'www.example.com' } } }
903
+ ]
904
+ }
905
+ json['paths'] = relations.inject({}) do |s, v|
906
+ s["/api/v1/#{v.first}"] = {
907
+ 'get': {
908
+ 'summary': "list #{v.first}",
909
+ 'parameters': v.last[:cols].map { |k, v| { 'name' => k, 'schema': { 'type': v.first } } },
910
+ 'responses': { '200': { 'description': 'successful' } }
911
+ }
912
+ }
913
+ # next if v.last[:isView]
914
+
915
+ s["/api/v1/#{v.first}/{id}"] = {
916
+ 'patch': {
917
+ 'summary': "update a #{v.first.singularize}",
918
+ 'parameters': v.last[:cols].reject { |k, v| Brick.config.metadata_columns.include?(k) }.map do |k, v|
919
+ { 'name' => k, 'schema': { 'type': v.first } }
920
+ end,
921
+ 'responses': { '200': { 'description': 'successful' } }
922
+ }
923
+ # "/api/v1/books/{id}": {
924
+ # "parameters": [
925
+ # {
926
+ # "name": "id",
927
+ # "in": "path",
928
+ # "description": "id",
929
+ # "required": true,
930
+ # "schema": {
931
+ # "type": "string"
932
+ # }
933
+ # },
934
+ # {
935
+ # "name": "Authorization",
936
+ # "in": "header",
937
+ # "schema": {
938
+ # "type": "string"
939
+ # }
940
+ # }
941
+ # ],
942
+ }
943
+ s
944
+ end
945
+ # binding.pry
946
+ render inline: json.to_json, content_type: request.format
947
+ return
948
+ end
921
949
  ::Brick.set_db_schema(params)
922
950
  if request.format == :csv # Asking for a template?
923
951
  require 'csv'
@@ -946,7 +974,7 @@ class Object
946
974
  @_brick_join_array = join_array
947
975
  end
948
976
 
949
- if model.primary_key
977
+ if model&.primary_key
950
978
  code << " def show\n"
951
979
  code << (find_by_id = " id = params[:id]&.split(/[\\/,_]/)
952
980
  id = id.first if id.is_a?(Array) && id.length == 1
@@ -961,7 +989,7 @@ class Object
961
989
  end
962
990
 
963
991
  # By default, views get marked as read-only
964
- unless false # model.readonly # (relation = relations[model.table_name]).key?(:isView)
992
+ unless is_swagger # model.readonly # (relation = relations[model.table_name]).key?(:isView)
965
993
  code << " # (Define :new, :create)\n"
966
994
 
967
995
  if model.primary_key
@@ -993,7 +1021,9 @@ class Object
993
1021
  # return
994
1022
  end
995
1023
 
996
- instance_variable_set("@#{singular_table_name}".to_sym, (obj = model.find(params[:id].split(','))))
1024
+ id = params[:id]&.split(/[\/,_]/)
1025
+ id = id.first if id.is_a?(Array) && id.length == 1
1026
+ instance_variable_set("@#{singular_table_name}".to_sym, (obj = model.find(id)))
997
1027
  obj = obj.first if obj.is_a?(Array)
998
1028
  obj.send(:update, send(params_name = params_name.to_sym))
999
1029
  end
@@ -1018,10 +1048,23 @@ class Object
1018
1048
 
1019
1049
  def _brick_get_hm_assoc_name(relation, hm_assoc)
1020
1050
  if relation[:hm_counts][hm_assoc[:assoc_name]]&.> 1
1051
+ # binding.pry if (same_name = relation[:fks].find { |x| x.last[:assoc_name] == hm_assoc[:assoc_name] && x.last != hm_assoc }) #&&
1052
+ # x.last[:alternate_name] == hm_assoc[:alternate_name] })
1053
+ # relation[:fks].any? { |k, v| v[:assoc_name] == new_alt_name }
1021
1054
  plural = ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name])
1022
- [hm_assoc[:alternate_name] == name.underscore ? "#{hm_assoc[:assoc_name].singularize}_#{plural}" : plural, true]
1055
+ # binding.pry if hm_assoc[:assoc_name] == 'issue_issue_duplicates'
1056
+ new_alt_name = (hm_assoc[:alternate_name] == name.underscore) ? "#{hm_assoc[:assoc_name].singularize}_#{plural}" : plural
1057
+ # uniq = 1
1058
+ # while same_name = relation[:fks].find { |x| x.last[:assoc_name] == hm_assoc[:assoc_name] && x.last != hm_assoc }
1059
+ # hm_assoc[:assoc_name] = "#{hm_assoc_name}_#{uniq += 1}"
1060
+ # end
1061
+ # puts new_alt_name
1062
+ # binding.pry if new_alt_name == 'issue_duplicates'
1063
+ # hm_assoc[:assoc_name] = new_alt_name
1064
+ [new_alt_name, true]
1023
1065
  else
1024
1066
  assoc_name = hm_assoc[:inverse_table].pluralize
1067
+ # hm_assoc[:assoc_name] = assoc_name
1025
1068
  [assoc_name, assoc_name.include?('.')]
1026
1069
  end
1027
1070
  end
@@ -1040,11 +1083,20 @@ module ActiveRecord::ConnectionHandling
1040
1083
  conn
1041
1084
  end
1042
1085
 
1086
+ # This is done separately so that during testing it can be called right after a migration
1087
+ # in order to make sure everything is good.
1043
1088
  def _brick_reflect_tables
1089
+ initializer_loaded = false
1044
1090
  if (relations = ::Brick.relations).empty?
1045
- # Hopefully our initializer is named exactly this!
1091
+ # If there's schema things configured then we only expect our initializer to be named exactly this
1046
1092
  if File.exist?(brick_initializer = Rails.root.join('config/initializers/brick.rb'))
1047
- load brick_initializer
1093
+ initializer_loaded = load brick_initializer
1094
+ end
1095
+ # Load the initializer for the Apartment gem a little early so that if .excluded_models and
1096
+ # .default_schema are specified then we can work with non-tenanted models more appropriately
1097
+ if Object.const_defined?('Apartment') && File.exist?(apartment_initializer = Rails.root.join('config/initializers/apartment.rb'))
1098
+ load apartment_initializer
1099
+ apartment_excluded = Apartment.excluded_models
1048
1100
  end
1049
1101
  # Only for Postgres? (Doesn't work in sqlite3)
1050
1102
  # puts ActiveRecord::Base.execute_sql("SELECT current_setting('SEARCH_PATH')").to_a.inspect
@@ -1052,24 +1104,53 @@ module ActiveRecord::ConnectionHandling
1052
1104
  schema_sql = 'SELECT NULL AS table_schema;'
1053
1105
  case ActiveRecord::Base.connection.adapter_name
1054
1106
  when 'PostgreSQL'
1055
- if (::Brick.default_schema = schema = ::Brick.config.schema_behavior&.[](:multitenant)&.[](:schema_to_analyse))
1107
+ if (is_multitenant = (multitenancy = ::Brick.config.schema_behavior[:multitenant]) &&
1108
+ (sta = multitenancy[:schema_to_analyse]) != 'public')
1109
+ ::Brick.default_schema = schema = sta
1056
1110
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1057
1111
  end
1058
1112
  schema_sql = 'SELECT DISTINCT table_schema FROM INFORMATION_SCHEMA.tables;'
1059
1113
  when 'Mysql2'
1060
1114
  ::Brick.default_schema = schema = ActiveRecord::Base.connection.current_database
1061
1115
  when 'SQLite'
1116
+ # %%% Retrieve internal ActiveRecord table names like this:
1117
+ # ActiveRecord::Base.internal_metadata_table_name, ActiveRecord::Base.schema_migrations_table_name
1062
1118
  sql = "SELECT m.name AS relation_name, UPPER(m.type) AS table_type,
1063
1119
  p.name AS column_name, p.type AS data_type,
1064
1120
  CASE p.pk WHEN 1 THEN 'PRIMARY KEY' END AS const
1065
1121
  FROM sqlite_master AS m
1066
1122
  INNER JOIN pragma_table_info(m.name) AS p
1067
- WHERE m.name NOT IN ('ar_internal_metadata', 'schema_migrations')
1123
+ WHERE m.name NOT IN (?, ?)
1068
1124
  ORDER BY m.name, p.cid"
1069
1125
  else
1070
1126
  puts "Unfamiliar with connection adapter #{ActiveRecord::Base.connection.adapter_name}"
1071
1127
  end
1072
1128
 
1129
+ unless (db_schemas = ActiveRecord::Base.execute_sql(schema_sql)).is_a?(Array)
1130
+ db_schemas = db_schemas.to_a
1131
+ end
1132
+ unless db_schemas.empty?
1133
+ ::Brick.db_schemas = db_schemas.each_with_object({}) do |row, s|
1134
+ row = row.is_a?(String) ? row : row['table_schema']
1135
+ # Remove any system schemas
1136
+ s[row] = nil unless ['information_schema', 'pg_catalog'].include?(row)
1137
+ end
1138
+ end
1139
+
1140
+ if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1141
+ if (possible_schema = ::Brick.config.schema_behavior&.[](:multitenant)&.[](:schema_to_analyse))
1142
+ if ::Brick.db_schemas.key?(possible_schema)
1143
+ ::Brick.default_schema = schema = possible_schema
1144
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1145
+ else
1146
+ puts "*** In the brick.rb initializer the line \"::Brick.schema_behavior = ...\" refers to a schema called \"#{possible_schema}\". This schema does not exist. ***"
1147
+ end
1148
+ end
1149
+ end
1150
+
1151
+ # %%% Retrieve internal ActiveRecord table names like this:
1152
+ # ActiveRecord::Base.internal_metadata_table_name, ActiveRecord::Base.schema_migrations_table_name
1153
+ # For if it's not SQLite -- so this is the Postgres and MySQL version
1073
1154
  sql ||= "SELECT t.table_schema AS schema, t.table_name AS relation_name, t.table_type,
1074
1155
  c.column_name, c.data_type,
1075
1156
  COALESCE(c.character_maximum_length, c.numeric_precision) AS max_length,
@@ -1091,19 +1172,21 @@ module ActiveRecord::ConnectionHandling
1091
1172
  WHERE t.table_schema NOT IN ('information_schema', 'pg_catalog')#{"
1092
1173
  AND t.table_schema = COALESCE(current_setting('SEARCH_PATH'), 'public')" if schema }
1093
1174
  -- AND t.table_type IN ('VIEW') -- 'BASE TABLE', 'FOREIGN TABLE'
1094
- AND t.table_name NOT IN ('pg_stat_statements', 'ar_internal_metadata', 'schema_migrations')
1175
+ AND t.table_name NOT IN ('pg_stat_statements', ?, ?)
1095
1176
  ORDER BY 1, t.table_type DESC, c.ordinal_position"
1096
1177
  measures = []
1097
1178
  case ActiveRecord::Base.connection.adapter_name
1098
1179
  when 'PostgreSQL', 'SQLite' # These bring back a hash for each row because the query uses column aliases
1099
- schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1100
- ActiveRecord::Base.execute_sql(sql).each do |r|
1101
- # next if internal_views.include?(r['relation_name']) # Skip internal views such as v_all_assessments
1102
- relation_name = if r['schema'] != schema
1103
- "#{schema_name = r['schema']}.#{r['relation_name']}"
1104
- else
1105
- r['relation_name']
1106
- end
1180
+ # schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1181
+ ActiveRecord::Base.execute_sql(sql, ActiveRecord::Base.internal_metadata_table_name, ActiveRecord::Base.schema_migrations_table_name).each do |r|
1182
+ # If Apartment gem lists the table as being associated with a non-tenanted model then use whatever it thinks
1183
+ # is the default schema, usually 'public'.
1184
+ schema_name = if ::Brick.config.schema_behavior[:multitenant]
1185
+ Apartment.default_schema if apartment_excluded&.include?(r['relation_name'].singularize.camelize)
1186
+ elsif ![schema, 'public'].include?(r['schema'])
1187
+ r['schema']
1188
+ end
1189
+ relation_name = schema_name ? "#{schema_name}.#{r['relation_name']}" : r['relation_name']
1107
1190
  relation = relations[relation_name]
1108
1191
  relation[:isView] = true if r['table_type'] == 'VIEW'
1109
1192
  col_name = r['column_name']
@@ -1122,7 +1205,6 @@ module ActiveRecord::ConnectionHandling
1122
1205
  end
1123
1206
  else # MySQL2 acts a little differently, bringing back an array for each row
1124
1207
  ActiveRecord::Base.execute_sql(sql).each do |r|
1125
- # next if internal_views.include?(r['relation_name']) # Skip internal views such as v_all_assessments
1126
1208
  relation = relations[(relation_name = r[0])] # here relation represents a table or view from the database
1127
1209
  relation[:isView] = true if r[1] == 'VIEW' # table_type
1128
1210
  col_name = r[2]
@@ -1161,7 +1243,7 @@ module ActiveRecord::ConnectionHandling
1161
1243
  # end
1162
1244
  # end
1163
1245
  # end
1164
- schema = ::Brick.default_schema # Reset back for this next round of fun
1246
+ # schema = ::Brick.default_schema # Reset back for this next round of fun
1165
1247
  case ActiveRecord::Base.connection.adapter_name
1166
1248
  when 'PostgreSQL', 'Mysql2'
1167
1249
  sql = "SELECT kcu1.CONSTRAINT_SCHEMA, kcu1.TABLE_NAME, kcu1.COLUMN_NAME,
@@ -1176,7 +1258,7 @@ module ActiveRecord::ConnectionHandling
1176
1258
  AND kcu2.CONSTRAINT_SCHEMA = rc.UNIQUE_CONSTRAINT_SCHEMA
1177
1259
  AND kcu2.CONSTRAINT_NAME = rc.UNIQUE_CONSTRAINT_NAME
1178
1260
  AND kcu2.ORDINAL_POSITION = kcu1.ORDINAL_POSITION#{"
1179
- WHERE kcu1.CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if schema }"
1261
+ WHERE kcu1.CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if schema }"
1180
1262
  # AND kcu2.TABLE_NAME = ?;", Apartment::Tenant.current, table_name
1181
1263
  when 'SQLite'
1182
1264
  sql = "SELECT m.name, fkl.\"from\", fkl.\"table\", m.name || '_' || fkl.\"from\" AS constraint_name
@@ -1186,36 +1268,47 @@ module ActiveRecord::ConnectionHandling
1186
1268
  else
1187
1269
  end
1188
1270
  if sql
1189
- ::Brick.default_schema ||= schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1190
- unless (db_schemas = ActiveRecord::Base.execute_sql(schema_sql)).is_a?(Array)
1191
- db_schemas = db_schemas.to_a
1192
- end
1193
- unless db_schemas.empty?
1194
- ::Brick.db_schemas = db_schemas.each_with_object({}) do |row, s|
1195
- row = row.is_a?(String) ? row : row['table_schema']
1196
- # Remove whatever default schema we're using and other system schemas
1197
- s[row] = nil unless ['information_schema', 'pg_catalog', schema].include?(row)
1198
- end
1199
- end
1271
+ # ::Brick.default_schema ||= schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1200
1272
  ActiveRecord::Base.execute_sql(sql).each do |fk|
1201
1273
  fk = fk.values unless fk.is_a?(Array)
1274
+ # Multitenancy makes things a little more general overall, except for non-tenanted tables
1275
+ if apartment_excluded&.include?(fk[1].singularize.camelize)
1276
+ fk[0] = Apartment.default_schema
1277
+ elsif fk[0] == 'public' || (is_multitenant && fk[0] == schema)
1278
+ fk[0] = nil
1279
+ end
1280
+ if apartment_excluded&.include?(fk[4].singularize.camelize)
1281
+ fk[3] = Apartment.default_schema
1282
+ elsif fk[3] == 'public' || (is_multitenant && fk[3] == schema)
1283
+ fk[3] = nil
1284
+ end
1202
1285
  ::Brick._add_bt_and_hm(fk, relations)
1203
1286
  end
1204
1287
  end
1205
1288
  end
1206
1289
 
1207
- puts "\nClasses that can be built from tables:"
1208
- relations.select { |_k, v| !v.key?(:isView) }.keys.each { |k| puts ActiveSupport::Inflector.singularize(k).camelize }
1209
- unless (views = relations.select { |_k, v| v.key?(:isView) }).empty?
1290
+ apartment = Object.const_defined?('Apartment') && Apartment
1291
+ tables = []
1292
+ views = []
1293
+ relations.each do |k, v|
1294
+ name_parts = k.split('.')
1295
+ if v.key?(:isView)
1296
+ views
1297
+ else
1298
+ name_parts.shift if apartment && name_parts.length > 1 && name_parts.first == Apartment.default_schema
1299
+ tables
1300
+ end << name_parts.map { |x| x.singularize.camelize }.join('::')
1301
+ end
1302
+ unless tables.empty?
1303
+ puts "\nClasses that can be built from tables:"
1304
+ tables.sort.each { |x| puts x }
1305
+ end
1306
+ unless views.empty?
1210
1307
  puts "\nClasses that can be built from views:"
1211
- views.keys.each { |k| puts ActiveSupport::Inflector.singularize(k).camelize }
1308
+ views.sort.each { |x| puts x }
1212
1309
  end
1213
1310
 
1214
- # Try to load the initializer pretty danged early
1215
- if File.exist?(brick_initialiser = Rails.root.join('config/initializers/brick.rb'))
1216
- load brick_initialiser
1217
- ::Brick.load_additional_references
1218
- end
1311
+ ::Brick.load_additional_references if initializer_loaded
1219
1312
  end
1220
1313
  end
1221
1314
 
@@ -1242,31 +1335,52 @@ module Brick
1242
1335
  # rubocop:enable Style/CommentedKeyword
1243
1336
 
1244
1337
  class << self
1245
- def _add_bt_and_hm(fk, relations, is_polymorphic = false)
1246
- if (bt_assoc_name = fk[2].underscore).end_with?('_id')
1247
- bt_assoc_name = bt_assoc_name[0..-4]
1248
- elsif bt_assoc_name.end_with?('id') && bt_assoc_name.exclude?('_') # Make the bold assumption that we can just peel off the final ID part
1249
- bt_assoc_name = bt_assoc_name[0..-3]
1250
- else
1251
- bt_assoc_name = "#{bt_assoc_name}_bt"
1338
+ def _add_bt_and_hm(fk, relations, is_polymorphic = false, is_optional = false)
1339
+ bt_assoc_name = fk[2]
1340
+ unless is_polymorphic
1341
+ bt_assoc_name = if bt_assoc_name.underscore.end_with?('_id')
1342
+ bt_assoc_name[0..-4]
1343
+ elsif bt_assoc_name.downcase.end_with?('id') && bt_assoc_name.exclude?('_')
1344
+ bt_assoc_name[0..-3] # Make the bold assumption that we can just peel off any final ID part
1345
+ else
1346
+ "#{bt_assoc_name}_bt"
1347
+ end
1252
1348
  end
1253
1349
  # %%% Temporary schema patch
1254
- fk[1] = "#{fk[0]}.#{for_tbl = fk[1]}" if fk[0] && fk[0] != ::Brick.default_schema
1255
- for_tbl << '_' if for_tbl
1350
+ for_tbl = fk[1]
1351
+ apartment = Object.const_defined?('Apartment') && Apartment
1352
+ fk[0] = Apartment.default_schema if apartment && apartment.excluded_models.include?(for_tbl.singularize.camelize)
1353
+ fk[1] = "#{fk[0]}.#{fk[1]}" if fk[0] # && fk[0] != ::Brick.default_schema
1256
1354
  bts = (relation = relations.fetch(fk[1], nil))&.fetch(:fks) { relation[:fks] = {} }
1355
+
1257
1356
  # %%% Do we miss out on has_many :through or even HM based on constantizing this model early?
1258
1357
  # Maybe it's already gotten this info because we got as far as to say there was a unique class
1259
1358
  primary_table = if (is_class = fk[4].is_a?(Hash) && fk[4].key?(:class))
1260
1359
  pri_tbl = (primary_class = fk[4][:class].constantize).table_name
1360
+ if (pri_tbl_parts = pri_tbl.split('.')).length > 1
1361
+ fk[3] = pri_tbl_parts.first
1362
+ end
1261
1363
  else
1364
+ is_schema = if ::Brick.config.schema_behavior[:multitenant]
1365
+ # If Apartment gem lists the primary table as being associated with a non-tenanted model
1366
+ # then use 'public' schema for the primary table
1367
+ if apartment&.excluded_models.include?(fk[4].singularize.camelize)
1368
+ fk[3] = Apartment.default_schema
1369
+ true
1370
+ end
1371
+ else
1372
+ fk[3] && fk[3] != ::Brick.default_schema && fk[3] != 'public'
1373
+ end
1262
1374
  pri_tbl = fk[4]
1263
- (fk[3] && fk[3] != ::Brick.default_schema) ? "#{fk[3]}.#{pri_tbl}" : pri_tbl
1375
+ is_schema ? "#{fk[3]}.#{pri_tbl}" : pri_tbl
1264
1376
  end
1265
1377
  hms = (relation = relations.fetch(primary_table, nil))&.fetch(:fks) { relation[:fks] = {} } unless is_class
1266
1378
 
1267
1379
  unless (cnstr_name = fk[5])
1268
1380
  # For any appended references (those that come from config), arrive upon a definitely unique constraint name
1269
- cnstr_base = cnstr_name = "(brick) #{for_tbl}#{is_class ? fk[4][:class].underscore : pri_tbl}"
1381
+ pri_tbl = is_class ? fk[4][:class].underscore : pri_tbl
1382
+ pri_tbl = "#{bt_assoc_name}_#{pri_tbl}" if pri_tbl.singularize != bt_assoc_name
1383
+ cnstr_base = cnstr_name = "(brick) #{for_tbl}_#{pri_tbl}"
1270
1384
  cnstr_added_num = 1
1271
1385
  cnstr_name = "#{cnstr_base}_#{cnstr_added_num += 1}" while bts&.key?(cnstr_name) || hms&.key?(cnstr_name)
1272
1386
  missing = []
@@ -1306,6 +1420,7 @@ module Brick
1306
1420
  else
1307
1421
  inverse_table = [primary_table] if is_polymorphic
1308
1422
  assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[2], assoc_name: bt_assoc_name, inverse_table: inverse_table || primary_table }
1423
+ assoc_bt[:optional] = true if is_optional
1309
1424
  assoc_bt[:polymorphic] = true if is_polymorphic
1310
1425
  end
1311
1426
  if is_class
@@ -1326,7 +1441,14 @@ module Brick
1326
1441
  assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
1327
1442
  assoc_hm[:inverse] = assoc_bt
1328
1443
  else
1329
- assoc_hm = hms[hm_cnstr_name] = { is_bt: false, fk: fk[2], assoc_name: fk[1].tr('.', '_').pluralize, alternate_name: bt_assoc_name, inverse_table: fk[1], inverse: assoc_bt }
1444
+ inv_tbl = if ::Brick.config.schema_behavior[:multitenant] && apartment && fk[0] == Apartment.default_schema
1445
+ for_tbl
1446
+ else
1447
+ fk[1]
1448
+ end
1449
+ # binding.pry if inv_tbl == 'issue_issue_duplicates' # inverse_table goofed?
1450
+ assoc_hm = hms[hm_cnstr_name] = { is_bt: false, fk: fk[2], assoc_name: for_tbl.pluralize, alternate_name: bt_assoc_name,
1451
+ inverse_table: inv_tbl, inverse: assoc_bt }
1330
1452
  assoc_hm[:polymorphic] = true if is_polymorphic
1331
1453
  hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
1332
1454
  hm_counts[fk[1]] = hm_counts.fetch(fk[1]) { 0 } + 1
@@ -124,8 +124,15 @@ module Brick
124
124
  schema_options = ::Brick.db_schemas.keys.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
125
125
  # %%% If we are not auto-creating controllers (or routes) then omit by default, and if enabled anyway, such as in a development
126
126
  # environment or whatever, then get either the controllers or routes list instead
127
- table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables).sort
128
- .each_with_object(+'') { |v, s| s << "<option value=\"#{v.underscore.gsub('.', '/').pluralize}\">#{v}</option>" }.html_safe
127
+ apartment_default_schema = ::Brick.config.schema_behavior[:multitenant] && Object.const_defined?('Apartment') && Apartment.default_schema
128
+ table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables).map do |tbl|
129
+ if (tbl_parts = tbl.split('.')).first == apartment_default_schema
130
+ tbl = tbl_parts.last
131
+ end
132
+ tbl
133
+ end.sort.each_with_object(+'') do |v, s|
134
+ s << "<option value=\"#{v.underscore.gsub('.', '/').pluralize}\">#{v}</option>"
135
+ end.html_safe
129
136
  css = +"<style>
130
137
  #dropper {
131
138
  background-color: #eee;
@@ -222,7 +229,7 @@ end %>"
222
229
  if ['index', 'show', 'update'].include?(args.first)
223
230
  poly_cols = []
224
231
  css << "<% bts = { #{
225
- bts.each_with_object([]) do |v, s|
232
+ bt_items = bts.each_with_object([]) do |v, s|
226
233
  foreign_models = if v.last[2] # Polymorphic?
227
234
  poly_cols << @_brick_model.reflect_on_association(v[1].first).foreign_type
228
235
  v.last[1].each_with_object([]) { |x, s| s << "[#{x.name}, #{x.primary_key.inspect}]" }.join(', ')
@@ -230,14 +237,18 @@ end %>"
230
237
  "[#{v.last[1].name}, #{v.last[1].primary_key.inspect}]"
231
238
  end
232
239
  s << "#{v.first.inspect} => [#{v.last.first.inspect}, [#{foreign_models}], #{v.last[2].inspect}]"
233
- end.join(', ')
240
+ end
241
+ # # %%% Need to fix poly going to an STI class
242
+ # binding.pry unless poly_cols.empty?
243
+ bt_items.join(', ')
234
244
  } }
235
245
  poly_cols = #{poly_cols.inspect} %>"
236
246
  end
237
247
 
238
- # %%% When doing schema select, if there's an ID then remove it, or if we're on a new page go to index
248
+ # %%% When doing schema select, if we're on a new page go to index
239
249
  script = "<script>
240
250
  var schemaSelect = document.getElementById(\"schema\");
251
+ var tblSelect = document.getElementById(\"tbl\");
241
252
  var brickSchema;
242
253
  if (schemaSelect) {
243
254
  brickSchema = changeout(location.href, \"_brick_schema\");
@@ -247,7 +258,8 @@ if (schemaSelect) {
247
258
  schemaSelect.value = brickSchema || \"public\";
248
259
  schemaSelect.focus();
249
260
  schemaSelect.addEventListener(\"change\", function () {
250
- location.href = changeout(location.href, \"_brick_schema\", this.value);
261
+ // If there's an ID then remove it (trim after selected table)
262
+ location.href = changeout(location.href, \"_brick_schema\", this.value, tblSelect.value);
251
263
  });
252
264
  }
253
265
  [... document.getElementsByTagName(\"FORM\")].forEach(function (form) {
@@ -262,7 +274,6 @@ if (schemaSelect) {
262
274
  });
263
275
  });
264
276
 
265
- var tblSelect = document.getElementById(\"tbl\");
266
277
  if (tblSelect) {
267
278
  tblSelect.value = changeout(location.href)[0];
268
279
  if (tblSelect.selectedIndex < 0) tblSelect.value = changeout(location.href)[1];
@@ -274,7 +285,7 @@ if (tblSelect) {
274
285
  });
275
286
  }
276
287
 
277
- function changeout(href, param, value) {
288
+ function changeout(href, param, value, trimAfter) {
278
289
  var hrefParts = href.split(\"?\");
279
290
  if (param === undefined || param === null) {
280
291
  hrefParts = hrefParts[0].split(\"://\");
@@ -285,6 +296,11 @@ function changeout(href, param, value) {
285
296
  else
286
297
  return hrefParts[0] + \"://\" + pathParts[0] + \"/\" + value;
287
298
  }
299
+ if (trimAfter) {
300
+ var pathParts = hrefParts[0].split(\"/\");
301
+ while (pathParts.lastIndexOf(trimAfter) != pathParts.length - 1) pathParts.pop();
302
+ hrefParts[0] = pathParts.join(\"/\");
303
+ }
288
304
  var params = hrefParts.length > 1 ? hrefParts[1].split(\"&\") : [];
289
305
  params = params.reduce(function (s, v) { var parts = v.split(\"=\"); s[parts[0]] = parts[1]; return s; }, {});
290
306
  if (value === undefined) return params[param];
@@ -404,7 +420,7 @@ function changeout(href, param, value) {
404
420
  origin = (key_parts = k.split('.')).length == 1 ? #{model_name} : #{model_name}.reflect_on_association(key_parts.first).klass
405
421
  # binding.pry
406
422
  if (destination_fk = Brick.relations[origin.table_name][:fks].values.find { |fk| puts fk.inspect; fk[:fk] == key_parts.last }) &&
407
- (obj = (destination = origin.reflect_on_association(destination_fk[:assoc_name])&.klass)&.find(id)) %>
423
+ (obj = (destination = origin.reflect_on_association(destination_fk[:assoc_name])&.klass)&.find(id)) %>
408
424
  <h3>for <%= link_to \"#{"#\{obj.brick_descrip\} (#\{destination.name\})\""}, send(\"#\{destination.name.underscore.tr('/', '_')\}_path\".to_sym, id) %></h3><%
409
425
  end
410
426
  end %>
@@ -563,7 +579,7 @@ function changeout(href, param, value) {
563
579
  s << "<table id=\"#{hm_name}\">
564
580
  <tr><th>#{hm[3]}</th></tr>
565
581
  <% collection = @#{obj_name}.#{hm_name}
566
- collection = collection.is_a?(ActiveRecord::Associations::CollectionProxy) ? collection.order(#{pk.inspect}) : [collection]
582
+ collection = collection.is_a?(ActiveRecord::Associations::CollectionProxy) ? collection.order(#{pk.inspect}) : [collection].compact
567
583
  if collection.empty? %>
568
584
  <tr><td>(none)</td></tr>
569
585
  <% else %>
@@ -597,6 +613,7 @@ function changeout(href, param, value) {
597
613
 
598
614
  # Just in case it hadn't been done previously when we tried to load the brick initialiser,
599
615
  # go make sure we've loaded additional references (virtual foreign keys and polymorphic associations).
616
+ # (This should only happen if for whatever reason the initializer file was not exactly config/initializers/brick.rb.)
600
617
  ::Brick.load_additional_references
601
618
  end
602
619
  end
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 31
8
+ TINY = 34
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
@@ -93,16 +93,19 @@ module Brick
93
93
  attr_accessor :default_schema, :db_schemas
94
94
 
95
95
  def set_db_schema(params)
96
- schema = params['_brick_schema']
96
+ schema = params['_brick_schema'] || 'public'
97
97
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema) if schema && ::Brick.db_schemas&.include?(schema)
98
98
  end
99
99
 
100
100
  # All tables and views (what Postgres calls "relations" including column and foreign key info)
101
101
  def relations
102
- connections = Brick.instance_variable_get(:@relations) ||
103
- Brick.instance_variable_set(:@relations, (connections = {}))
104
102
  # Key our list of relations for this connection off of the connection pool's object_id
105
- (connections[ActiveRecord::Base.connection_pool.object_id] ||= Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = {} } })
103
+ (@relations ||= {})[ActiveRecord::Base.connection_pool.object_id] ||= Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = {} } }
104
+ end
105
+
106
+ # If multitenancy is enabled, a list of non-tenanted "global" models
107
+ def non_tenanted_models
108
+ @pending_models ||= {}
106
109
  end
107
110
 
108
111
  def get_bts_and_hms(model)
@@ -129,6 +132,7 @@ module Brick
129
132
  associatives = hms.each_with_object({}) do |hmt, s|
130
133
  if (through = hmt.last.options[:through])
131
134
  skip_hms[through] = nil
135
+ # binding.pry if hmt.first == :issue_issues
132
136
  s[hmt.first] = hms[through] # End up with a hash of HMT names pointing to join-table associations
133
137
  elsif hmt.last.inverse_of.nil?
134
138
  puts "SKIPPING #{hmt.last.name.inspect}"
@@ -314,7 +318,7 @@ module Brick
314
318
  if ars
315
319
  ars.each do |ar|
316
320
  fk = ar.length < 5 ? [nil, +ar[0], ar[1], nil, +ar[2]] : [ar[0], +ar[1], ar[2], ar[3], +ar[4], ar[5]]
317
- ::Brick._add_bt_and_hm(fk, relations)
321
+ ::Brick._add_bt_and_hm(fk, relations, false, true)
318
322
  end
319
323
  end
320
324
  if (polys = ::Brick.config.polymorphics)
@@ -327,7 +331,7 @@ module Brick
327
331
  v ||= ActiveRecord::Base.execute_sql("SELECT DISTINCT #{poly}_type AS typ FROM #{table_name}").each_with_object([]) { |result, s| s << result['typ'] if result['typ'] }
328
332
  v.each do |type|
329
333
  if relations.key?(primary_table = type.underscore.pluralize)
330
- ::Brick._add_bt_and_hm([nil, table_name, poly, nil, primary_table, "(brick) #{table_name}_#{poly}"], relations, true)
334
+ ::Brick._add_bt_and_hm([nil, table_name, poly, nil, primary_table, "(brick) #{table_name}_#{poly}"], relations, true, true)
331
335
  else
332
336
  missing_stis[primary_table] = type unless ::Brick.existing_stis.key?(type)
333
337
  end
@@ -406,6 +410,7 @@ In config/initializers/brick.rb appropriate entries would look something like:
406
410
  ::Brick.relations.each do |rel_name, v|
407
411
  rel_name = rel_name.split('.').map(&:underscore)
408
412
  schema_names = rel_name[0..-2]
413
+ schema_names.shift if ::Brick.config.schema_behavior[:multitenant] && Object.const_defined?('Apartment') && schema_names.first == Apartment.default_schema
409
414
  k = rel_name.last
410
415
  unless existing_controllers.key?(controller_name = k.pluralize)
411
416
  options = {}
@@ -419,6 +424,7 @@ In config/initializers/brick.rb appropriate entries would look something like:
419
424
  end
420
425
  end
421
426
  end
427
+ send(:get, '/api-docs/v1/swagger.json', { to: 'brick_swagger#index' }) if Object.const_defined?('Rswag::Ui')
422
428
  end
423
429
  super
424
430
  end
@@ -28,7 +28,7 @@ module Brick
28
28
  col_down = col.downcase
29
29
 
30
30
  if (is_possible_poly = ['character varying', 'text'].include?(type.first))
31
- if col_down.end_with?('_type') &&
31
+ if col_down.end_with?('_type')
32
32
  poly_type_cut_length = -6
33
33
  col_down = col_down[0..-6]
34
34
  elsif col_down.end_with?('type')
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.31
4
+ version: 1.0.34
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-05-29 00:00:00.000000000 Z
11
+ date: 2022-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord