Module: Brick

Defined in:
lib/brick/util.rb,
lib/brick.rb,
lib/brick/config.rb,
lib/brick/join_array.rb,
lib/brick/version_number.rb,
lib/brick/serializers/json.rb,
lib/brick/serializers/yaml.rb,
lib/brick/frameworks/cucumber.rb,
lib/brick/frameworks/rails/engine.rb,
lib/generators/brick/seeds_generator.rb,
lib/generators/brick/models_generator.rb,
lib/generators/brick/install_generator.rb,
lib/generators/brick/migration_builder.rb,
lib/generators/brick/salesforce_schema.rb,
lib/generators/brick/migrations_generator.rb,
lib/generators/brick/salesforce_migrations_generator.rb,
lib/brick/extensions.rb

Overview

:nodoc:

Defined Under Namespace

Modules: Cucumber, Extensions, MigrationBuilder, Rails, RouteMapper, RouteSet, Serializers, Util, VERSION Classes: Config, InstallGenerator, JoinArray, JoinHash, MigrationsGenerator, ModelsGenerator, SalesforceMigrationsGenerator, SalesforceSchema, SeedsGenerator

Constant Summary collapse

ALL_API_ACTIONS =
[:index, :show, :create, :update, :destroy]
CURRENCY_SYMBOLS =
'$£¢₵€₠ƒ¥₿₩₪₹₫₴₱₲₳₸₺₼₽៛₡₢₣₤₥₦₧₨₭₮₯₰₶₷₻₾'
ADD_CONST_MISSING =
lambda do
  return if @_brick_const_missing_done

  @_brick_const_missing_done = true
  alias _brick_const_missing const_missing
  def const_missing(*args)
    requested = args.first.to_s
    is_controller = requested.end_with?('Controller')
    # self.name is nil when a model name is requested in an .erb file
    if self.name && ::Brick.config.path_prefix
      split_self_name.shift if (split_self_name = self.name.split('::')).first.blank?
      # Asking for the prefix module?
      camelize_prefix = ::Brick.config.path_prefix.camelize
      if self == Object && requested == camelize_prefix
        Object.const_set(args.first, (built_module = Module.new))
        puts "module #{camelize_prefix}; end\n"
        return built_module
      elsif module_parent == Object && self.name == camelize_prefix ||
            module_parent.name == camelize_prefix && module_parent.module_parent == Object
        split_self_name.shift # Remove the identified path prefix from the split name
        is_brick_prefix = true
        if is_controller
          brick_root = split_self_name.empty? ? self : camelize_prefix.constantize
        end
      end
    end
    base_module = if self < ActiveRecord::Migration || !self.name
                    brick_root || Object
                  elsif split_self_name&.length&.> 1 # Classic mode
                    begin
                      base = self
                      unless (base_goal = requested.split('::')[0..-2].join('::')).empty?
                        base = base.parent while base.name != base_goal && base != Object
                      end
                      return base._brick_const_missing(*args)

                    rescue NameError # %%% Avoid the error "____ cannot be autoloaded from an anonymous class or module"
                      return self.const_get(args.first) if self.const_defined?(args.first)

                      # unless self == (prnt = (respond_to?(:parent) ? parent : module_parent))
                      unless self == Object
                        begin
                          return Object._brick_const_missing(*args)

                        rescue NameError
                          return Object.const_get(args.first) if Object.const_defined?(args.first)

                        end
                      end
                    end
                    Object
                  else
                    sti_base = (::Brick.config.sti_namespace_prefixes&.fetch("::#{name}::#{requested}", nil) ||
                                ::Brick.config.sti_namespace_prefixes&.fetch("::#{name}::", nil))&.constantize
                    self
                  end
    # puts "#{self.name} - #{args.first}"
    # Unless it's a Brick prefix looking for a TNP that should create a module ...
    relations = ::Brick.relations
    unless (is_tnp_module = (is_brick_prefix && !is_controller && ::Brick.config.table_name_prefixes.values.include?(requested)))
      # ... first look around for an existing module or class.
      desired_classname = (self == Object || !name) ? requested : "#{name}::#{requested}"
      if ((is_defined = self.const_defined?(args.first)) && (possible = self.const_get(args.first)) &&
          # Reset `possible` if it's a controller request that's not a perfect match
          # Was:  (possible = nil)  but changed to #local_variable_set in order to suppress the "= should be ==" warning
          (possible&.name == desired_classname || (is_controller && binding.local_variable_set(:possible, nil)))) ||
         # Try to require the respective Ruby file
         ((filename = ActiveSupport::Dependencies.search_for_file(desired_classname.underscore) ||
                      (self != Object && ActiveSupport::Dependencies.search_for_file((desired_classname = requested).underscore))
          ) && (require_dependency(filename) || true) &&
          ((possible = self.const_get(args.first)) && possible.name == desired_classname)
         ) ||
         # If any class has turned up so far (and we're not in the middle of eager loading)
         # then return what we've found.
         (is_defined && !::Brick.is_eager_loading) # Used to also have:   && possible != self
        if ((!brick_root && (filename || possible.instance_of?(Class))) ||
            (possible.instance_of?(Module) && possible&.module_parent == self) ||
            (possible.instance_of?(Class) && possible == self)) && # Are we simply searching for ourselves?
           # Skip when what we found as `possible` is not related to the base class of an STI model
           (!sti_base || possible.is_a?(sti_base))
          # if possible.is_a?(ActiveRecord::Base) && !possible.abstract_class? && (pk = possible.primary_key) &&
          #    !(relation = relations.fetch(possible.table_name, nil))&.fetch(:pks, nil)
          #   binding.pry
          #   x = 5
          # end
          return possible
        end
      end
    end
    class_name = ::Brick.namify(requested)
    #        CONTROLLER
    result = if ::Brick.enable_controllers? &&
                is_controller && (plural_class_name = class_name[0..-11]).length.positive?
               # Otherwise now it's up to us to fill in the gaps
               controller_class_name = +''
               full_class_name = +''
               unless self == Object
                 controller_class_name << ((split_self_name&.first && split_self_name.join('::')) || self.name)
                 full_class_name << "::#{controller_class_name}"
                 controller_class_name << '::'
               end
               # (Go over to underscores for a moment so that if we have something come in like VABCsController then the model name ends up as
               # Vabc instead of VABC)
               singular_class_name = ::Brick.namify(plural_class_name, :underscore).singularize.camelize
               full_class_name << "::#{singular_class_name}"
               skip_controller = nil
               if plural_class_name == 'BrickOpenapi' ||
                  (
                    (::Brick.config.add_status || ::Brick.config.add_orphans) &&
                    plural_class_name == 'BrickGem'
                  ) ||
                 begin
                   model = self.const_get(full_class_name)

                   # In the very rare case that we've picked up a MODULE which has the same name as what would be the
                   # resource's MODEL name, just build out an appropriate auto-model on-the-fly. (RailsDevs code has this in PayCustomer.)
                   # %%% We don't yet display the code for this new model
                   if model && !model.is_a?(Class)
                     model, _code = Object.send(:build_model, relations, model.module_parent, model.module_parent.name, singular_class_name)
                   end
                 rescue NameError # If the const_get for the model has failed...
                   skip_controller = true
                   # ... then just fall through and allow it to fail when trying to loading the ____Controller class normally.
                 end
               end
               unless skip_controller
                 Object.send(:build_controller, self, class_name, plural_class_name, model, relations)
               end

             # MODULE
             elsif (::Brick.enable_models? || ::Brick.enable_controllers?) && # Schema match?
                   # %%% This works for Person::Person -- but also limits us to not being able to allow more than one level of namespacing
                   (base_module == Object || (camelize_prefix && base_module == Object.const_get(camelize_prefix))) &&
                   (schema_name = [(singular_table_name = class_name.underscore),
                                   (table_name = singular_table_name.pluralize),
                                   ::Brick.is_oracle ? class_name.upcase : class_name,
                                   (plural_class_name = class_name.pluralize)].find { |s| Brick.db_schemas&.include?(s) }&.camelize ||
                                  (::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}::") && class_name) ||
                                  (::Brick.config.table_name_prefixes&.values&.include?(class_name) && class_name))
               return self.const_get(schema_name) if self.const_defined?(schema_name) &&
                                                     (!is_tnp_module || self.const_get(schema_name).is_a?(Class))

               # Build out a module for the schema if it's namespaced
               # schema_name = schema_name.camelize
               base_module.const_set(schema_name.to_sym, (built_module = Module.new))
               [built_module, "module #{schema_name}; end\n"]
               # %%% Perhaps an option to use the first module just as schema, and additional modules as namespace with a table name prefix applied

             # MODULE (overrides from "treat_as_module")
             elsif (::Brick.enable_models? || ::Brick.enable_controllers?) &&
                   (possible_module = (base_module == Object ? '' : "#{base_module.name}::") + class_name) &&
                   ::Brick.config.treat_as_module.include?(possible_module)
               base_module.const_set(class_name.to_sym, (built_module = Module.new))
               [built_module, "module #{possible_module}; end\n"]

             # AVO Resource
             elsif base_module == Object && Object.const_defined?('Avo') && ::Avo.respond_to?(:railtie_namespace) && requested.end_with?('Resource') &&
                   # Expect that anything called MotorResource or SpinaResource could be from those administrative gems
                   requested.length > 8 && ['MotorResource', 'SpinaResource'].exclude?(requested)
               if (model = Object.const_get(requested[0..-9])) && model < ActiveRecord::Base
                 require 'generators/avo/resource_generator'
                 field_generator = Generators::Avo::ResourceGenerator.new([''])
                 field_generator.instance_variable_set(:@model, model)
                 fields = field_generator.send(:generate_fields)&.split("\n")
                                         &.each_with_object([]) do |f, s|
                                           if (f = f.strip).start_with?('field ')
                                             f = f[6..-1].split(',')
                                             s << [f.first[1..-1].to_sym, [f[1][1..-1].split(': :').map(&:to_sym)].to_h]
                                           end
                                         end || []
                 built_resource = Class.new(Avo::BaseResource) do |new_resource_class|
                   self.model_class = model
                   self.title = :brick_descrip
                   self.includes = []
                   if (!model.is_view? && mod_pk = model.primary_key)
                     field((mod_pk.is_a?(Array) ? mod_pk.first : mod_pk).to_sym, { as: :id })
                   end
                   # Create a call such as:  field :name, as: :text
                   fields.each do |f|
                     # Add proper types if this is a polymorphic belongs_to
                     if f.last == { as: :belongs_to } &&
                        (fk = ::Brick.relations[model.table_name][:fks].find { |k, v| v[:assoc_name] == f.first.to_s }) &&
                        fk.last.fetch(:polymorphic, nil)
                       poly_types = fk.last.fetch(:inverse_table, nil)&.each_with_object([]) do |poly_table, s|
                         s << Object.const_get(::Brick.relations[poly_table][:class_name])
                       end
                       if poly_types.present?
                         f.last[:polymorphic_as] = f.first
                         f.last[:types] = poly_types
                       end
                     end
                     self.send(:field, *f)
                   end
                 end
                 Object.const_set(requested.to_sym, built_resource)
                 [built_resource, nil]
               end

             # MODEL
             elsif ::Brick.enable_models?
               # Custom inheritable Brick base model?
               class_name = (inheritable_name = class_name)[5..-1] if class_name.start_with?('Brick')
               Object.send(:build_model, relations, base_module, name, class_name, inheritable_name)
             end
    if result
      built_class, code = result
      puts "\n#{code}\n"
      built_class
    elsif !schema_name && ::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}")
#         module_prefixes = type_name.split('::')
#         path = base_module.name.split('::')[0..-2] + []
#         module_prefixes.unshift('') unless module_prefixes.first.blank?
#         candidate_file = ::Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb')
      base_module._brick_const_missing(*args)
    # elsif base_module != Object
    #   module_parent.const_missing(*args)
    elsif Rails.respond_to?(:autoloaders) && # After finding nothing else, if Zeitwerk is enabled ...
          (Rails::Autoloaders.respond_to?(:zeitwerk_enabled?) ? Rails::Autoloaders.zeitwerk_enabled? : true)
      self._brick_const_missing(*args) # ... rely solely on Zeitwerk.
    else # Classic mode
      unless (found = base_module._brick_const_missing(*args))
        puts "MISSING! #{base_module.name} #{args.inspect} #{table_name}"
      end
      found
    end
  end

  # Support Rails < 6.0 which adds #parent instead of #module_parent
  unless respond_to?(:module_parent)
    def module_parent # Weirdly for Grape::API does NOT come in with the proper class, but some anonymous Class thing
      parent
    end
  end
end

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.auto_modelsObject

Returns the value of attribute auto_models.



109
110
111
# File 'lib/brick.rb', line 109

def auto_models
  @auto_models
end

.db_schemasObject

Returns the value of attribute db_schemas.



109
110
111
# File 'lib/brick.rb', line 109

def db_schemas
  @db_schemas
end

.default_schemaObject

Returns the value of attribute default_schema.



109
110
111
# File 'lib/brick.rb', line 109

def default_schema
  @default_schema
end

.established_drfObject

Returns the value of attribute established_drf.



109
110
111
# File 'lib/brick.rb', line 109

def established_drf
  @established_drf
end

.initializer_loadedObject

Returns the value of attribute initializer_loaded.



109
110
111
# File 'lib/brick.rb', line 109

def initializer_loaded
  @initializer_loaded
end

.is_eager_loadingObject

Returns the value of attribute is_eager_loading.



109
110
111
# File 'lib/brick.rb', line 109

def is_eager_loading
  @is_eager_loading
end

.is_oracleObject

Returns the value of attribute is_oracle.



109
110
111
# File 'lib/brick.rb', line 109

def is_oracle
  @is_oracle
end

.routes_doneObject

Returns the value of attribute routes_done.



109
110
111
# File 'lib/brick.rb', line 109

def routes_done
  @routes_done
end

.table_name_lookupObject

Returns the value of attribute table_name_lookup.



109
110
111
# File 'lib/brick.rb', line 109

def table_name_lookup
  @table_name_lookup
end

.test_schemaObject

Returns the value of attribute test_schema.



109
110
111
# File 'lib/brick.rb', line 109

def test_schema
  @test_schema
end

Class Method Details

._add_bt_and_hm(fk, relations, polymorphic_class = nil, is_optional = false) ⇒ Object



3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
# File 'lib/brick/extensions.rb', line 3160

def _add_bt_and_hm(fk, relations, polymorphic_class = nil, is_optional = false)
  bt_assoc_name = ::Brick.namify(fk[2].dup, :downcase)
  unless polymorphic_class
    bt_assoc_name = if bt_assoc_name.underscore.end_with?('_id')
                      bt_assoc_name[-3] == '_' ? bt_assoc_name[0..-4] : bt_assoc_name[0..-3]
                    elsif bt_assoc_name.downcase.end_with?('id') && bt_assoc_name.exclude?('_')
                      bt_assoc_name[0..-3] # Make the bold assumption that we can just peel off any final ID part
                    else
                      "#{bt_assoc_name}_bt"
                    end
  end
  bt_assoc_name = "#{bt_assoc_name}_" if bt_assoc_name == 'attribute'

  # %%% Temporary schema patch
  for_tbl = fk[1]
  fk_namified = ::Brick.namify(fk[1])
  fk[0] = ::Brick.apartment_default_tenant if ::Brick.is_apartment_excluded_table(fk_namified)
  fk[1] = "#{fk[0]}.#{fk[1]}" if fk[0] # && fk[0] != ::Brick.default_schema
  bts = (relation = relations.fetch(fk[1], nil))&.fetch(:fks) { relation[:fks] = {} }

  # %%% Do we miss out on has_many :through or even HM based on constantizing this model early?
  # Maybe it's already gotten this info because we got as far as to say there was a unique class
  primary_table = if (is_class = fk[4].is_a?(Hash) && fk[4].key?(:class))
                    pri_tbl = (primary_class = fk[4][:class].constantize).table_name
                    if (pri_tbl_parts = pri_tbl.split('.')).length > 1
                      fk[3] = pri_tbl_parts.first
                    end
                  else
                    is_schema = if ::Brick.config.schema_behavior[:multitenant]
                                  # If Apartment gem lists the primary table as being associated with a non-tenanted model
                                  # then use 'public' schema for the primary table
                                  if ::Brick.is_apartment_excluded_table(fk[4])
                                    fk[3] = ::Brick.apartment_default_tenant
                                    true
                                  end
                                else
                                  fk[3] && fk[3] != ::Brick.default_schema && fk[3] != 'public'
                                end
                    pri_tbl = fk[4]
                    is_schema ? "#{fk[3]}.#{pri_tbl}" : pri_tbl
                  end
  hms = (relation = relations.fetch(primary_table, nil))&.fetch(:fks) { relation[:fks] = {} } unless is_class

  unless (cnstr_name = fk[5])
    # For any appended references (those that come from config), arrive upon a definitely unique constraint name
    pri_tbl = is_class ? fk[4][:class].underscore : pri_tbl
    pri_tbl = "#{bt_assoc_name}_#{pri_tbl}" if pri_tbl&.singularize != bt_assoc_name
    cnstr_name = ensure_unique(+"(brick) #{for_tbl}_#{pri_tbl}", nil, bts, hms)
    missing = []
    missing << fk[1] unless relations.key?(fk[1])
    missing << primary_table unless is_class || relations.key?(primary_table)
    unless missing.empty?
      tables = relations.reject { |_k, v| v.is_a?(Hash) && v.fetch(:isView, nil) }.keys
                        .select { |table_name| table_name.is_a?(String) }.sort
      puts "Brick: Additional reference #{fk.inspect} refers to non-existent #{'table'.pluralize(missing.length)} #{missing.join(' and ')}. (Available tables include #{tables.join(', ')}.)"
      return
    end
    unless (cols = relations[fk[1]][:cols]).key?(fk[2]) || (polymorphic_class && cols.key?("#{fk[2]}_id") && cols.key?("#{fk[2]}_type"))
      columns = cols.map { |k, v| "#{k} (#{v.first.split(' ').first})" }
      puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[2]}. (Columns present in #{fk[1]} are #{columns.join(', ')}.)"
      return
    end
    if (redundant = bts.find { |_k, v| v[:inverse]&.fetch(:inverse_table, nil) == fk[1] && v[:fk] == fk[2] && v[:inverse_table] == primary_table })
      if is_class && !redundant.last.key?(:class)
        redundant.last[:primary_class] = primary_class # Round out this BT so it can find the proper :source for a HMT association that references an STI subclass
      else
        puts "Brick: Additional reference #{fk.inspect} is redundant and can be removed.  (Already established by #{redundant.first}.)"
      end
      return
    end
  end
  return unless bts # Rails 5.0 and older can have bts end up being nil

  if (assoc_bt = bts[cnstr_name])
    if polymorphic_class
      # Assuming same fk (don't yet support composite keys for polymorphics)
      assoc_bt[:inverse_table] << fk[4]
      assoc_bt[:polymorphic] << polymorphic_class
    else # Expect we could have a composite key going
      if assoc_bt[:fk].is_a?(String)
        assoc_bt[:fk] = [assoc_bt[:fk], fk[2]] unless fk[2] == assoc_bt[:fk]
      elsif assoc_bt[:fk].exclude?(fk[2])
        assoc_bt[:fk] << fk[2]
      end
      assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[2]}"
    end
  else
    inverse_table = [primary_table] if polymorphic_class
    assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[2], assoc_name: bt_assoc_name, inverse_table: inverse_table || primary_table }
    assoc_bt[:optional] = true if is_optional
    assoc_bt[:polymorphic] = [polymorphic_class] if polymorphic_class
  end
  if is_class
    # For use in finding the proper :source for a HMT association that references an STI subclass
    assoc_bt[:primary_class] = primary_class
    # For use in finding the proper :inverse_of for a BT association that references an STI subclass
    # assoc_bt[:inverse_of] = primary_class.reflect_on_all_associations.find { |a| a.foreign_key == bt[1] }
  end

  return if is_class || ::Brick.config.exclude_hms&.any? { |exclusion| fk[1] == exclusion[0] && fk[2] == exclusion[1] && primary_table == exclusion[2] } || hms.nil?

  if (assoc_hm = hms.fetch((hm_cnstr_name = "hm_#{cnstr_name}"), nil))
    if assoc_hm[:fk].is_a?(String)
      assoc_hm[:fk] = [assoc_hm[:fk], fk[2]] unless fk[2] == assoc_hm[:fk]
    elsif assoc_hm[:fk].exclude?(fk[2])
      assoc_hm[:fk] << fk[2]
    end
    assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
  else
    inv_tbl = if ::Brick.config.schema_behavior[:multitenant] && Object.const_defined?('Apartment') && fk[0] == ::Brick.apartment_default_tenant
                for_tbl
              else
                fk[1]
              end
    assoc_hm = hms[hm_cnstr_name] = { is_bt: false, fk: fk[2], assoc_name: fk_namified.pluralize, alternate_name: bt_assoc_name,
                                      inverse_table: inv_tbl, inverse: assoc_bt }
    assoc_hm[:polymorphic] = true if polymorphic_class
    hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
    this_hm_count = hm_counts[fk[1]] = hm_counts.fetch(fk[1]) { 0 } + 1
  end
  assoc_bt[:inverse] = assoc_hm
end

._brick_index(tbl_name, mode = nil, separator = nil, relation = nil) ⇒ Object



3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
# File 'lib/brick/extensions.rb', line 3429

def _brick_index(tbl_name, mode = nil, separator = nil, relation = nil)
  separator ||= '_'
  res_name = (tbl_name_parts = tbl_name.split('.'))[0..-2].first
  res_name << '.' if res_name
  (res_name ||= +'') << (relation || ::Brick.relations.fetch(tbl_name, nil)&.fetch(:resource, nil) || tbl_name_parts.last)

  res_parts = ((mode == :singular) ? res_name.singularize : res_name).split('.')
  res_parts.shift if ::Brick.apartment_multitenant && res_parts.length > 1 && res_parts.first == ::Brick.apartment_default_tenant
  if (aps = relation&.fetch(:auto_prefixed_schema, nil)) && res_parts.last.start_with?(aps)
    last_part = res_parts.last[aps.length..-1]
    aps = aps[0..-2] if aps[-1] == '_'
    res_parts[-1] = aps
    res_parts << last_part
  end
  path_prefix = []
  if ::Brick.config.path_prefix
    res_parts.unshift(::Brick.config.path_prefix)
    path_prefix << ::Brick.config.path_prefix
  end
  index = res_parts.map(&:underscore).join(separator)
  index = index.tr('_', 'x') if separator == 'x'
  # Rails applies an _index suffix to that route when the resource name isn't something plural
  index << '_index' if mode != :singular && separator == '_' &&
                       index == (path_prefix + [name&.underscore&.tr('/', '_') || '_']).join(separator)
  index
end

._class_pk(dotted_name, multitenant) ⇒ Object



3463
3464
3465
# File 'lib/brick/extensions.rb', line 3463

def _class_pk(dotted_name, multitenant)
  Object.const_get((multitenant ? [dotted_name.split('.').last] : dotted_name.split('.')).map { |nm| "::#{nm.singularize.camelize}" }.join).primary_key
end

._find_csv_separator(stream, likely_separator) ⇒ Object



850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
# File 'lib/brick.rb', line 850

def _find_csv_separator(stream, likely_separator)
  line = nil
  last_char = nil
  in_quotes = false
  ret = +''
  while true
    (line = stream.readline).each_char do |ch|
      likely_separator[ch] += 2 if !in_quotes && last_char == '"' && ch != "\n"
      if ch == '"'
        in_quotes = !in_quotes
        likely_separator[last_char] += 2 if in_quotes && last_char != "\n"
      end
      last_char = ch
    end
    ret << line
    break if !in_quotes || stream.eof?
  end
  likely_separator[line[0]] += 1
  likely_separator[line[-2]] += 1
  ret
end

.additional_references=(ars) ⇒ Object

Additional table associations to use (Think of these as virtual foreign keys perhaps)



488
489
490
491
492
493
494
495
# File 'lib/brick.rb', line 488

def additional_references=(ars)
  if ars
    ars = ars.call if ars.is_a?(Proc)
    ars = ars.to_a unless ars.is_a?(Array)
    ars = [ars] unless ars.empty? || ars.first.is_a?(Array)
    Brick.config.additional_references = ars
  end
end

.always_load_fields=(field_set) ⇒ Object



626
627
628
# File 'lib/brick.rb', line 626

def always_load_fields=(field_set)
  Brick.config.always_load_fields = field_set
end

.apartment_default_tenantObject



177
178
179
# File 'lib/brick.rb', line 177

def apartment_default_tenant
  Apartment.default_tenant || 'public'
end

.apartment_multitenantObject



170
171
172
173
174
175
# File 'lib/brick.rb', line 170

def apartment_multitenant
  if @apartment_multitenant.nil?
    @apartment_multitenant = ::Brick.config.schema_behavior[:multitenant] && Object.const_defined?('Apartment')
  end
  @apartment_multitenant
end

.api_filterObject



441
442
443
# File 'lib/brick.rb', line 441

def api_filter
  Brick.config.api_filter
end

.api_filter=(proc) ⇒ Object



436
437
438
# File 'lib/brick.rb', line 436

def api_filter=(proc)
  Brick.config.api_filter = proc
end

.api_root=(path) ⇒ Object



421
422
423
# File 'lib/brick.rb', line 421

def api_root=(path)
  Brick.config.api_roots = [path]
end

.api_rootsObject



431
432
433
# File 'lib/brick.rb', line 431

def api_roots
  Brick.config.api_roots
end

.api_roots=(paths) ⇒ Object



426
427
428
# File 'lib/brick.rb', line 426

def api_roots=(paths)
  Brick.config.api_roots = paths
end

.apply_double_underscore_patchObject



812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
# File 'lib/brick.rb', line 812

def apply_double_underscore_patch
  unless @double_underscore_applied
    # Same as normal #camelize and #underscore, just that double-underscores turn into a single underscore
    ActiveSupport::Inflector.class_eval do
      def camelize(term, uppercase_first_letter = true)
        strings = term.to_s.split('__').map do |string|
          # String#camelize takes a symbol (:upper or :lower), so here we also support :lower to keep the methods consistent.
          if !uppercase_first_letter || uppercase_first_letter == :lower
            string = string.sub(inflections.acronyms_camelize_regex) { |match| match.downcase! || match }
          else
            string = string.sub(/^[a-z\d]*/) { |match| inflections.acronyms[match] || match.capitalize! || match }
          end
          string.gsub!(/(?:_|(\/))([a-z\d]*)/i) do
            word = $2
            substituted = inflections.acronyms[word] || word.capitalize! || word
            $1 ? "::#{substituted}" : substituted
          end
          string
        end
        strings.join('_')
      end

      def underscore(camel_cased_word)
        return camel_cased_word.to_s unless /[A-Z-]|::/.match?(camel_cased_word)
        regex = inflections.respond_to?(:acronyms_underscore_regex) ? inflections.acronyms_underscore_regex : inflections.acronym_regex
        camel_cased_word.to_s.gsub('::', '/').split('_').map do |word|
          word.gsub!(regex) { "#{$1 && '_' }#{$2.downcase}" }
          word.gsub!(/([A-Z]+)(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) { ($1 || $2) << '_' }
          word.tr!('-', '_')
          word.downcase!
          word
        end.join('__')
      end
    end
    @double_underscore_applied = true
  end
end

.ar_baseObject



3283
3284
3285
# File 'lib/brick/extensions.rb', line 3283

def ar_base
  @ar_base ||= Object.const_defined?(:ApplicationRecord) ? ApplicationRecord : Class.new(ActiveRecord::Base)
end

.config {|@config| ... } ⇒ Object Also known as: configure

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns Brick’s global configuration object, a singleton. These settings affect all threads.

Yields:



717
718
719
720
721
# File 'lib/brick.rb', line 717

def config
  @config ||= Brick::Config.instance
  yield @config if block_given?
  @config
end

.controllers_inherit_from=(value) ⇒ Object



461
462
463
# File 'lib/brick.rb', line 461

def controllers_inherit_from=(value)
  Brick.config.controllers_inherit_from = value
end

.ctrl_to_klass(ctrl_path, res_names = {}) ⇒ Object

Attempt to determine an ActiveRecord::Base class and additional STI information when given a controller’s path



771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
# File 'lib/brick.rb', line 771

def ctrl_to_klass(ctrl_path, res_names = {})
  klass = nil
  sti_type = nil

  if res_names.empty?
    ::Brick.relations.each_with_object({}) do |v, s|
      next if v.first.is_a?(Symbol)

      v_parts = v.first.split('.')
      v_parts.shift if v_parts.first == 'public'
      res_names[v_parts.join('.')] = v.first
    end
  end

  c_path_parts = ctrl_path.split('/')
  found = nil
  while c_path_parts.present?
    possible_c_path = c_path_parts.join('.')
    possible_c_path_singular = c_path_parts[0..-2] + [c_path_parts.last.singularize]
    possible_sti = possible_c_path_singular.join('/').camelize
    break if (
               res_name = res_names[found = possible_c_path] ||
                          ((klass = Brick.config.sti_namespace_prefixes.key?("::#{possible_sti}") && possible_sti.constantize) &&
                           (sti_type = possible_sti)) ||
                          # %%% Used to have the more flexible:  (DidYouMean::SpellChecker.new(dictionary: res_names.keys).correct(possible_c_path)).first
                          res_names[found = possible_c_path] || res_names[found = possible_c_path_singular.join('.')] ||
                          ((::Brick.config.table_name_prefixes.key?(tn_prefix = c_path_parts.first) ||
                            ::Brick.config.table_name_prefixes.key?(tn_prefix = "#{c_path_parts.first}_")) &&
                           res_names[found = tn_prefix + c_path_parts.last]
                          )
             ) &&
             (
               klass ||
               ((rel = ::Brick.relations.fetch(res_name, nil)) &&
               (klass ||= rel[:class_name]&.constantize))
             )
    c_path_parts.shift
  end
  [klass, sti_type, found]
end

.custom_columns=(cust_cols) ⇒ Object

Custom columns to add to a table, minimally defined with a name and DSL string.



514
515
516
517
518
519
# File 'lib/brick.rb', line 514

def custom_columns=(cust_cols)
  if cust_cols
    cust_cols = cust_cols.call if cust_cols.is_a?(Proc)
    Brick.config.custom_columns = cust_cols
  end
end

.default_route_fallback=(resource_name) ⇒ Object



614
615
616
# File 'lib/brick.rb', line 614

def default_route_fallback=(resource_name)
  Brick.config.default_route_fallback = resource_name
end

.defer_references_for_generation=(drfg) ⇒ Object

References to disregard when auto-building migrations, models, and seeds



499
500
501
502
503
504
# File 'lib/brick.rb', line 499

def defer_references_for_generation=(drfg)
  if drfg
    drfg = [drfg] unless drfg.empty? || drfg.first.is_a?(Array)
    Brick.config.defer_references_for_generation = drfg
  end
end

.display_classes(prefix, rels, max_length) ⇒ Object



762
763
764
765
766
767
768
# File 'lib/brick.rb', line 762

def display_classes(prefix, rels, max_length)
  rels.sort.each do |rel|
    ::Brick.auto_models << rel.first
    puts "#{rel.first}#{' ' * (max_length - rel.first.length)}  /#{prefix}#{"#{rel[1]}/" if rel[1]}#{rel.last}"
  end
  puts "\n"
end

.drfgsObject



506
507
508
509
510
# File 'lib/brick.rb', line 506

def drfgs
  (::Brick.config.defer_references_for_generation || []).each_with_object({}) do |drfg, s|
    s[drfg.first] = [drfg[1..-1]]
  end
end

.eager_load_classes(do_ar_abstract_bases = false) ⇒ Object



728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
# File 'lib/brick.rb', line 728

def eager_load_classes(do_ar_abstract_bases = false)
  ::Brick.is_eager_loading = true
  if ::ActiveSupport.version < ::Gem::Version.new('6') ||
     ::Rails.configuration.instance_variable_get(:@autoloader) == :classic
    if ::ActiveSupport.version < ::Gem::Version.new('4')
      ::Rails.application.eager_load!
    else
      ::Rails.configuration.eager_load_namespaces.select { |ns| ns < ::Rails::Application }.each(&:eager_load!)
    end
  else
    # Same as:  Zeitwerk::Loader.eager_load_all -- plus retry when something skips a beat
    Zeitwerk::Registry.loaders.each { |loader| load_with_retry(loader) }
  end
  abstract_ar_bases = if do_ar_abstract_bases
                        ActiveRecord::Base.descendants.select { |ar| ar.abstract_class? }.map(&:name)
                      end
  ::Brick.is_eager_loading = false
  abstract_ar_bases
end

.enable_apiObject



416
417
418
# File 'lib/brick.rb', line 416

def enable_api
  Brick.config.enable_api
end

.enable_api=(path) ⇒ Object



411
412
413
# File 'lib/brick.rb', line 411

def enable_api=(path)
  Brick.config.enable_api = path
end

.enable_controllers=(value) ⇒ Object

Switches Brick auto-controllers on or off, for all threads



373
374
375
# File 'lib/brick.rb', line 373

def enable_controllers=(value)
  Brick.config.enable_controllers = value
end

.enable_controllers?Boolean

Returns ‘true` if Brick controllers are on, `false` otherwise. This affects all threads. Enabled by default.

Returns:

  • (Boolean)


380
381
382
# File 'lib/brick.rb', line 380

def enable_controllers?
  !!Brick.config.enable_controllers
end

.enable_models=(value) ⇒ Object

Switches Brick auto-models on or off, for all threads



360
361
362
# File 'lib/brick.rb', line 360

def enable_models=(value)
  Brick.config.enable_models = value
end

.enable_models?Boolean

Returns ‘true` if Brick models are on, `false` otherwise. This affects all threads. Enabled by default.

Returns:

  • (Boolean)


367
368
369
# File 'lib/brick.rb', line 367

def enable_models?
  !!Brick.config.enable_models
end

.enable_routes=(value) ⇒ Object

Switches Brick auto-routes on or off, for all threads



399
400
401
# File 'lib/brick.rb', line 399

def enable_routes=(value)
  Brick.config.enable_routes = value
end

.enable_routes?Boolean

Returns ‘true` if Brick routes are on, `false` otherwise. This affects all threads. Enabled by default.

Returns:

  • (Boolean)


406
407
408
# File 'lib/brick.rb', line 406

def enable_routes?
  !!Brick.config.enable_routes
end

.enable_views=(value) ⇒ Object

Switches Brick auto-views on or off, for all threads



386
387
388
# File 'lib/brick.rb', line 386

def enable_views=(value)
  Brick.config.enable_views = value
end

.enable_views?Boolean

Returns ‘true` if Brick views are on, `false` otherwise. This affects all threads. Enabled by default.

Returns:

  • (Boolean)


393
394
395
# File 'lib/brick.rb', line 393

def enable_views?
  !!Brick.config.enable_views
end

.ensure_unique(name, delimiter, *sources) ⇒ Object



3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
# File 'lib/brick/extensions.rb', line 3341

def ensure_unique(name, delimiter, *sources)
  base = name
  delimiter ||= '_'
  # By default ends up building this regex:  /_(\d+)$/
  if (added_num = name.slice!(Regexp.new("#{delimiter}(\d+)$")))
    added_num = added_num[1..-1].to_i
  else
    added_num = 1
  end
  while (
    name = "#{base}#{delimiter}#{added_num += 1}"
    sources.each_with_object(nil) do |v, s|
      s || case v
           when Hash
             v.key?(name)
           when Array
             v.include?(name)
           end
    end
  )
  end
  name
end

.exclude_column(table, col) ⇒ Object



328
329
330
331
# File 'lib/brick.rb', line 328

def exclude_column(table, col)
  puts "Excluding #{table}.#{col}"
  true
end

.exclude_hms=(skips) ⇒ Object

Skip creating a has_many association for these (Uses the same exact three-part format as would define an additional_reference)



529
530
531
532
533
534
535
536
# File 'lib/brick.rb', line 529

def exclude_hms=(skips)
  if skips
    skips = skips.call if skips.is_a?(Proc)
    skips = skips.to_a unless skips.is_a?(Array)
    skips = [skips] unless skips.empty? || skips.first.is_a?(Array)
    Brick.config.exclude_hms = skips
  end
end

.exclude_tables=(value) ⇒ Object



451
452
453
# File 'lib/brick.rb', line 451

def exclude_tables=(value)
  Brick.config.exclude_tables = value
end

.existing_stisObject



105
106
107
# File 'lib/brick.rb', line 105

def existing_stis
  @existing_stis ||= Brick.config.sti_namespace_prefixes.each_with_object({}) { |snp, s| s[snp.first[2..-1]] = snp.last unless snp.first.end_with?('::') }
end

.find_col_renaming(api_ver_path, relation_name) ⇒ Object



3456
3457
3458
3459
3460
3461
# File 'lib/brick/extensions.rb', line 3456

def find_col_renaming(api_ver_path, relation_name)
  ::Brick.config.api_column_renaming&.fetch(
    api_ver_path,
    ::Brick.config.api_column_renaming&.fetch(relation_name, nil)
  )
end

.find_orphans(multi_schema) ⇒ Object

Locate orphaned records



3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
# File 'lib/brick/extensions.rb', line 3366

def find_orphans(multi_schema)
  is_default_schema = multi_schema&.==(::Brick.apartment_default_tenant)
  relations.each_with_object([]) do |v, s|
    frn_tbl = v.first
    next if frn_tbl.is_a?(Symbol) || # Skip internal metadata entries
            (relation = v.last).key?(:isView) || config.exclude_tables.include?(frn_tbl) ||
            !(for_pk = (relation[:pkey].values.first&.first))

    is_default_frn_schema = !is_default_schema && multi_schema &&
                            ((frn_parts = frn_tbl.split('.')).length > 1 && frn_parts.first)&.==(::Brick.apartment_default_tenant)
    relation[:fks].select { |_k, assoc| assoc[:is_bt] }.each do |_k, bt|
      begin
        if bt.key?(:polymorphic)
          pri_pk = for_pk
          pri_tables = Brick.config.polymorphics["#{frn_tbl}.#{bt[:fk]}"]
                            .each_with_object(Hash.new { |h, k| h[k] = [] }) do |pri_class, s|
            s[Object.const_get(pri_class).table_name] << pri_class
          end
          fk_id_col = "#{bt[:fk]}_id"
          fk_type_col = "#{bt[:fk]}_type"
          selects = []
          pri_tables.each do |pri_tbl, pri_types|
            # Skip if database is multitenant, we're not focused on "public", and the foreign and primary tables
            # are both in the "public" schema
            next if is_default_frn_schema &&
                    ((pri_parts = pri_tbl&.split('.'))&.length > 1 && pri_parts.first)&.==(::Brick.apartment_default_tenant)

            selects << "SELECT '#{pri_tbl}' AS pri_tbl, frn.#{fk_type_col} AS pri_type, frn.#{fk_id_col} AS pri_id, frn.#{for_pk} AS frn_id
            FROM #{frn_tbl} AS frn
              LEFT OUTER JOIN #{pri_tbl} AS pri ON pri.#{pri_pk} = frn.#{fk_id_col}
            WHERE frn.#{fk_type_col} IN (#{
              pri_types.map { |pri_type| "'#{pri_type}'" }.join(', ')
            }) AND frn.#{bt[:fk]}_id IS NOT NULL AND pri.#{pri_pk} IS NULL\n"
          end
          ActiveRecord::Base.execute_sql(selects.join("UNION ALL\n")).each do |o|
            entry = [frn_tbl, o['frn_id'], o['pri_type'], o['pri_id'], fk_id_col]
            entry << o['pri_tbl'] if (pri_class = Object.const_get(o['pri_type'])) != pri_class.base_class
            s << entry
          end
        else
          # Skip if database is multitenant, we're not focused on "public", and the foreign and primary tables
          # are both in the "public" schema
          pri_tbl = bt.key?(:inverse_table) && bt[:inverse_table]
          next if is_default_frn_schema &&
                  ((pri_parts = pri_tbl&.split('.'))&.length > 1 && pri_parts.first)&.==(::Brick.apartment_default_tenant)

          pri_pk = relations[pri_tbl].fetch(:pkey, nil)&.values&.first&.first ||
                   _class_pk(pri_tbl, multi_schema)
          ActiveRecord::Base.execute_sql(
            "SELECT frn.#{bt[:fk]} AS pri_id, frn.#{for_pk} AS frn_id
            FROM #{frn_tbl} AS frn
              LEFT OUTER JOIN #{pri_tbl} AS pri ON pri.#{pri_pk} = frn.#{bt[:fk]}
            WHERE frn.#{bt[:fk]} IS NOT NULL AND pri.#{pri_pk} IS NULL
            ORDER BY 1, 2"
          ).each { |o| s << [frn_tbl, o['frn_id'], pri_tbl, o['pri_id'], bt[:fk]] }
        end
      rescue StandardError => err
        puts "Strange -- #{err.inspect}"
      end
    end
  end
end

.gem_versionObject

Returns Brick’s ‘::Gem::Version`, convenient for comparisons. This is recommended over `::Brick::VERSION::STRING`.



698
699
700
# File 'lib/brick.rb', line 698

def gem_version
  ::Gem::Version.new(VERSION::STRING)
end

.get_bts_and_hms(model, recalculate = nil) ⇒ Object



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/brick.rb', line 203

def get_bts_and_hms(model, recalculate = nil)
  if !recalculate && (ret = model.instance_variable_get(:@_brick_bts_and_hms))
    return ret
  end

  model_cols = model.columns_hash
  bts, hms = model.reflect_on_all_associations.each_with_object([{}, {}]) do |a, s|
    if a.belongs_to? && !a.polymorphic? && ::Brick.config.polymorphics.fetch(full_assoc_name = "#{model.table_name}.#{a.name}", nil)
      puts "Based on inclusion in ::Brick.polymorphics, marking association #{full_assoc_name} as being polymorphic."
      a.options[:polymorphic] = true
    end
    if (through = a.options[:through]) && !a.through_reflection
      puts "WARNING:  In model #{model.name}, unable to utilise:  has_many #{a.name}, through: #{through.inspect}"
    end
    primary_klass = if a.belongs_to?
                      a.polymorphic? ? nil : a.klass
                    else
                      a.active_record
                    end
    a_pk = a.options.fetch(:primary_key, nil)&.to_s ||
           primary_klass&.primary_key # Was:  a.klass.primary_key
    if !a.polymorphic? && (a.belongs_to? || (through && (thr = a.through_reflection))) &&
       !((kls = thr&.klass || a.klass) && ::Brick.config.exclude_tables.exclude?(kls.table_name) &&
          (!a.belongs_to? ||
            ((fk_type = a.foreign_key.is_a?(Array) ? a.foreign_key.map { |fk_part| model_cols[fk_part.to_s].type } : model_cols[a.foreign_key.to_s].type) &&
             (primary_cols = primary_klass.columns_hash) &&
             (pk_type = a_pk.is_a?(Array) ? a_pk.map { |pk_part| primary_cols[pk_part.to_s].type } : primary_cols[a_pk].type) &&
             (same_type = (pk_type == fk_type))
            )
          )
        )
    # unless a.polymorphic? || (!a.belongs_to? && (through = a.options[:through])) ||
    #        (a.klass && ::Brick.config.exclude_tables.exclude?(a.klass.table_name) &&
    #          (!a.belongs_to? || (same_type = (fk_type = model_cols[a.foreign_key.to_s]&.type) == pk_type))
    #        )
      if same_type == false # We really do want to test specifically for false here, and not nil!
        puts "WARNING:
  Foreign key column #{a.klass.table_name}.#{a.foreign_key} is #{fk_type}, but the primary key it relates to, #{primary_klass.table_name}.#{a_pk}, is #{pk_type}.
  These columns should both be of the same type."
      end
      next
    end

    if a.belongs_to?
      if a.polymorphic?
        rel_poly_bt = relations[model.table_name][:fks].find { |_k, fk| fk[:assoc_name] == a.name.to_s }
        if (primary_tables = rel_poly_bt&.last&.fetch(:inverse_table, [])).is_a?(Array)
          models = rel_poly_bt[1][:polymorphic]&.map { |table| table.singularize.camelize.constantize }
          s.first[a.foreign_key.to_s] = [a.name, models, true]
        else
          # This will come up when using Devise invitable when invited_by_class_name is not
          # specified because in that circumstance it adds a polymorphic :invited_by association,
          # along with appropriate invited_by_type and invited_by_id columns.

          # See if any currently-loaded models have a has_many association over to this polymorphic belongs_to
          hm_models = ActiveRecord::Base.descendants.select do |m|
            m.reflect_on_all_associations.any? { |assoc| !assoc.belongs_to? && assoc.options[:as]&.to_sym == a.name }
          end
          # No need to include models with no table, or subclassed models if their parent is already in the list
          hm_models.reject! { |m| !m.table_exists? || hm_models.any? { |parent| parent != m && m < parent } }
          if hm_models.empty?
            puts "Missing any real indication as to which models \"has_many\" this polymorphic BT in model #{a.active_record.name}:"
            puts "  belongs_to :#{a.name}, polymorphic: true"
          else
            puts "Having analysed all currently-loaded models to infer the various polymorphic has_many associations for #{model.name}, here are the current results:"
            puts "::Brick.polymorphics = { \"#{model.table_name}.#{a.name}\" =>
                     #{hm_models.map(&:name).inspect}
                   }"
            puts 'If you add the above to your brick.rb, it will "cement" these options into place, and avoid this lookup process.'
            s.first[a.foreign_key.to_s] = [a.name, hm_models, true]
          end
        end
      else
        bt_key = a.foreign_key.is_a?(Array) ? a.foreign_key : a.foreign_key.to_s
        s.first[bt_key] = [a.name, a.klass]
      end
    else # This gets all forms of has_many and has_one
      if through # has_many :through or has_one :through
        is_invalid_source = nil
        begin
          if a.through_reflection.macro != :has_many # This HM goes through either a belongs_to or a has_one, so essentially a HOT?
            # Treat it like a belongs_to - just keyed on the association name instead of a foreign_key
            s.first[a.name] = [a.name, a.klass]
            next
          elsif !a.source_reflection # Had considered:  a.active_record.reflect_on_association(a.source_reflection_name).nil?
            is_invalid_source = true
          end
        rescue
          is_invalid_source = true
        end
        if is_invalid_source
          puts "  due to invalid source #{a.source_reflection_name.inspect}."
          next
        end
      else
        this_fks = (this_fk = a.foreign_key).is_a?(Array) ? this_fk.uniq : [this_fk.to_s]
        if !a.options.key?(:as) && (this_fks - a.klass.column_names).length.positive?
          options = ", #{a.options.map { |k, v| "#{k.inspect} => #{v.inspect}" }.join(', ')}" if a.options.present?
          puts "WARNING:  Model #{model.name} has this association:
        has_many :#{a.name}#{options}
      which expects column #{a.foreign_key} to exist in table #{a.klass.table_name}.  This column is missing."
          next

        end
      end
      s.last[a.name] = a
    end
  end
  # Mark has_manys that go to an associative ("join") table so that they are skipped in the UI,
  # as well as any possible polymorphic associations
  skip_hms = {}
  hms.each do |hmt|
    if (through = hmt.last.options[:through])
      # ::Brick.relations[hmt.last.through_reflection.table_name]
      skip_hms[through] = nil if hms[through] && model.is_brick? &&
                                 hmt.last.klass != hmt.last.active_record && # Don't pull HMs for HMTs that point back to the same table
                                 hmt[1].source_reflection.belongs_to? # Don't pull HMs when the HMT is built from HM -> HM
      # End up with a hash of HMT names pointing to join-table associations
      model._br_associatives[hmt.first] = hms[through] # || hms["#{(opt = hmt.last.options)[:through].to_s.singularize}_#{opt[:source].to_s.pluralize}".to_sym]
    end
  end
  skip_hms.each { |k, _v| hms.delete(k) }
  model.instance_variable_set(:@_brick_bts_and_hms, [bts, hms]) # Cache and return this result
end

.get_possible_schemasObject



115
116
117
118
119
120
121
122
# File 'lib/brick.rb', line 115

def get_possible_schemas
  if (possible_schemas = (multitenancy = ::Brick.config.schema_behavior&.[](:multitenant)) &&
                          multitenancy&.[](:schema_to_analyse))
    possible_schemas = [possible_schemas] unless possible_schemas.is_a?(Array)
    possible_schema = possible_schemas.find { |ps| ::Brick.db_schemas.key?(ps) }
  end
  [possible_schema, possible_schemas, multitenancy]
end

.get_status_of_resourcesObject

Identify built out routes, migrations, models, (and also soon controllers and views!) for each resource



3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
# File 'lib/brick/extensions.rb', line 3290

def get_status_of_resources
  rails_root = ::Rails.root.to_s
  migrations = if Dir.exist?(mig_path = ActiveRecord::Migrator.migrations_paths.first || "#{rails_root}/db/migrate")
                 Dir["#{mig_path}/**/*.rb"].each_with_object(Hash.new { |h, k| h[k] = [] }) do |v, s|
                   File.read(v).split("\n").each_with_index do |line, line_idx|
                     # For all non-commented lines, look for any that have "create_table", "alter_table", or "drop_table"
                     if !line.lstrip.start_with?('#') &&
                        (idx = (line.index('create_table ') || line.index('create_table('))&.+(13)) ||
                        (idx = (line.index('alter_table ') || line.index('alter_table('))&.+(12)) ||
                        (idx = (line.index('drop_table ') || line.index('drop_table('))&.+(11))
                       tbl = line[idx..-1].match(/([:'"\w\.]+)/)&.captures&.first
                       if tbl
                         v = v[(rails_root.length)..-1] if v.start_with?(rails_root)
                         v = v[1..-1] if v.start_with?('/')
                         s[tbl.tr(':\'"', '').pluralize] << [v, line_idx + 1]
                       end
                     end
                   end
                 end
               end
  abstract_activerecord_bases = ::Brick.eager_load_classes(true)
  rails_root = ::Rails.root.to_s
  models = ::Brick.relations.each_with_object({}) do |rel, s|
             next if rel.first.is_a?(Symbol)

             begin
               if (model = rel.last[:class_name]&.constantize) &&
                  (inh = ActiveRecord::Base._brick_inheriteds[model]&.join(':'))
                 inh = inh[rails_root.length + 1..-1] if inh.start_with?(rails_root)
                 s[rel.first] = [inh, model]
               end
             rescue
             end
           end
  ::Brick.relations.each_with_object([]) do |v, s|
    next if v.first.is_a?(Symbol) # ||
    #       Brick.config.exclude_tables.include?(v.first)

    tbl_parts = v.first.split('.')
    tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.length > 1 && tbl_parts.first == ::Brick.apartment_default_tenant
    res = tbl_parts.join('.')
    table_name = (model = models[res])&.last&.table_name
    table_name ||= begin
                     v.last[:class_name].constantize.table_name
                   rescue
                   end
    model = model.first if model.is_a?(Array)
    s << [v.first, table_name || v.first, migrations&.fetch(res, nil), model]
  end
end

.has_ones=(hos) ⇒ Object

Associations to treat as a has_one



546
547
548
549
550
551
552
553
554
555
556
557
# File 'lib/brick.rb', line 546

def has_ones=(hos)
  if hos
    hos = hos.call if hos.is_a?(Proc)
    hos = hos.to_a unless hos.is_a?(Array)
    hos = [hos] unless hos.empty? || hos.first.is_a?(Array)
    # Translate to being nested hashes
    Brick.config.has_ones = hos&.each_with_object(Hash.new { |h, k| h[k] = {} }) do |v, s|
      s[v.first][v[1]] = v[2] if v[1]
      s
    end
  end
end

.hmts=(assocs) ⇒ Object

Associations for which to auto-create a has_many _, through: _



567
568
569
# File 'lib/brick.rb', line 567

def hmts=(assocs)
  Brick.config.hmts = assocs
end

.is_apartment_excluded_table(tbl) ⇒ Object



3467
3468
3469
3470
3471
3472
3473
# File 'lib/brick/extensions.rb', line 3467

def is_apartment_excluded_table(tbl)
  if Object.const_defined?('Apartment')
    tbl_klass = (tnp = ::Brick.config.table_name_prefixes&.find { |k, _v| tbl.start_with?(k) }) ? +"#{tnp.last}::" : +''
    tbl_klass << tbl[tnp&.first&.length || 0..-1].singularize.camelize
    Apartment.excluded_models&.include?(tbl_klass)
  end
end

.is_geography?(val) ⇒ Boolean

Returns:

  • (Boolean)


343
344
345
# File 'lib/brick.rb', line 343

def is_geography?(val)
  val.length < 31 && (val.length - 6) % 8 == 0 && val[0..5].bytes == [230, 16, 0, 0, 1, 12]
end

.json_columns=(cols) ⇒ Object



578
579
580
# File 'lib/brick.rb', line 578

def json_columns=(cols)
  Brick.config.json_columns = cols
end

.license=(key) ⇒ Object



618
619
620
# File 'lib/brick.rb', line 618

def license=(key)
  Brick.config.license = key
end

.load_additional_referencesObject

Load additional references (virtual foreign keys) This is attempted early if a brick initialiser file is found, and then again as a failsafe at the end of our engine’s initialisation %%% Maybe look for differences the second time ‘round and just add new stuff instead of entirely deferring



633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
# File 'lib/brick.rb', line 633

def load_additional_references
  return if @_additional_references_loaded || ::Brick.config.mode != :on

  relations = ::Brick.relations
  if (ars = ::Brick.config.additional_references) || ::Brick.config.polymorphics
    is_optional = ActiveRecord.version >= ::Gem::Version.new('5.0')
    if ars
      ars.each do |ar|
        fk = ar.length < 5 ? [nil, +ar[0], ar[1], nil, +ar[2]] : [ar[0], +ar[1], ar[2], ar[3], +ar[4], ar[5]]
        ::Brick._add_bt_and_hm(fk, relations, false, is_optional)
      end
    end
    if (polys = ::Brick.config.polymorphics)
      if (schema = ::Brick.config.schema_behavior[:multitenant]&.fetch(:schema_to_analyse, nil)) && ::Brick.db_schemas&.key?(schema)
        ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
      end
      missing_stis = {}
      polys.each do |k, v|
        table_name, poly = k.split('.')
        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'] }
        v.each do |type|
          # Allow polymorphic BT to relate to an STI subclass
          base_type = ::Brick.config.sti_namespace_prefixes["::#{type}"] ||
                      ::Brick.config.sti_namespace_prefixes.find { |k, _v| k.end_with?('::') && type.start_with?(k[2..-1]) }&.last&.[](2..-1)
          if relations.key?(primary_table = (base_type || type).underscore.pluralize)
            ::Brick._add_bt_and_hm([nil, table_name, poly, nil, primary_table, "(brick) #{table_name}_#{poly}"], relations,
                                   type, # Polymorphic class
                                   is_optional)
          elsif relations.present?
            missing_stis[primary_table] = type unless ::Brick.existing_stis.key?(type)
          end
        end
      end
      unless missing_stis.empty?
        print "
You might be missing an STI namespace prefix entry for these tables:  #{missing_stis.keys.join(', ')}.
In config/initializers/brick.rb appropriate entries would look something like:
  Brick.sti_namespace_prefixes = {"
        puts missing_stis.map { |_k, missing_sti| "\n    '#{missing_sti}' => 'SomeParentModel'" }.join(',')
        puts "  }
(Just trade out SomeParentModel with some more appropriate one.)"
      end
    end
    @_additional_references_loaded = true
  end

  # Find associative tables that can be set up for has_many :through
  ::Brick.relations.each do |key, tbl|
    next if key.is_a?(Symbol)

    tbl_cols = tbl[:cols].keys
    fks = tbl[:fks].each_with_object({}) { |fk, s| s[fk.last[:fk]] = [fk.last[:assoc_name], fk.last[:inverse_table]] if fk.last[:is_bt]; s }
    # Aside from the primary key and the metadata columns created_at, updated_at, and deleted_at, if this table only has
    # foreign keys then it can act as an associative table and thus be used with has_many :through.
    if fks.length > 1 && (tbl_cols - fks.keys - (::Brick.config. || []) - (tbl[:pkey].values.first || [])).length.zero?
      fks.each { |fk| tbl[:hmt_fks][fk.first] = fk.last }
    end
  end
end

.load_with_retry(loader, autoloaded = nil) ⇒ Object

Some classes (like Phlex::Testing::Rails) will successfully auto-load after a retry



749
750
751
752
753
754
755
756
757
758
759
760
# File 'lib/brick.rb', line 749

def load_with_retry(loader, autoloaded = nil)
  autoloaded ||= loader.send(:autoloaded_dirs).dup
  begin
    loader.eager_load
  rescue Zeitwerk::SetupRequired
    # This is fine -- we eager load what can be eager loaded
  rescue Zeitwerk::NameError
    if autoloaded != (new_auto = loader.send(:autoloaded_dirs))
      load_with_retry(loader, new_auto.dup) # Try one more time and it could come together
    end
  end
end

.metadata_columns=(value) ⇒ Object



477
478
479
# File 'lib/brick.rb', line 477

def (value)
  Brick.config. = value
end

.mode=(setting) ⇒ Object



348
349
350
# File 'lib/brick.rb', line 348

def mode=(setting)
  Brick.config.mode = setting
end

.model_descrips=(descrips) ⇒ Object

DSL templates for individual models to provide prettier descriptions of objects



589
590
591
# File 'lib/brick.rb', line 589

def model_descrips=(descrips)
  Brick.config.model_descrips = descrips
end

.models_inherit_from=(value) ⇒ Object



456
457
458
# File 'lib/brick.rb', line 456

def models_inherit_from=(value)
  Brick.config.models_inherit_from = value
end

.namify(name, action = nil) ⇒ Object

Convert spaces to underscores if the second character and onwards is mixed case



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/brick.rb', line 187

def namify(name, action = nil)
  has_uppers = name =~ /[A-Z]+/
  has_lowers = name =~ /[a-z]+/
  name.downcase! if has_uppers && action == :downcase
  if name.include?(' ')
    # All uppers or all lowers?
    if !has_uppers || !has_lowers
      name.titleize.tr(' ', '_')
    else # Mixed uppers and lowers -- just remove existing spaces
      name.tr(' ', '')
    end
  else
    action == :underscore ? name.underscore : name
  end
end

.nested_attributes=(anaf) ⇒ Object

Associations upon which to add #accepts_nested_attributes_for logic



561
562
563
# File 'lib/brick.rb', line 561

def nested_attributes=(anaf)
  Brick.config.nested_attributes = anaf
end

.non_tenanted_modelsObject

If multitenancy is enabled, a list of non-tenanted “global” models



182
183
184
# File 'lib/brick.rb', line 182

def non_tenanted_models
  @pending_models ||= {}
end

.not_nullables=(value) ⇒ Object



482
483
484
# File 'lib/brick.rb', line 482

def not_nullables=(value)
  Brick.config.not_nullables = value
end

.omit_empty_tables_in_dropdown=(setting) ⇒ Object



622
623
624
# File 'lib/brick.rb', line 622

def omit_empty_tables_in_dropdown=(setting)
  Brick.config.omit_empty_tables_in_dropdown = setting
end

.order=(value) ⇒ Object



522
523
524
# File 'lib/brick.rb', line 522

def order=(value)
  Brick.config.order = value
end

.path_prefix=(path) ⇒ Object

Any path prefixing to apply to all auto-generated Brick routes



354
355
356
# File 'lib/brick.rb', line 354

def path_prefix=(path)
  Brick.config.path_prefix = path
end

.polymorphics=(polys) ⇒ Object

Polymorphic associations



572
573
574
575
# File 'lib/brick.rb', line 572

def polymorphics=(polys)
  polys = polys.each_with_object({}) { |poly, s| s[poly] = nil } if polys.is_a?(Array)
  Brick.config.polymorphics = polys || {}
end

.relationsObject

All tables and views (what Postgres calls “relations” including column and foreign key info)



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/brick.rb', line 148

def relations
  return {} if (ch = ::ActiveRecord::Base.connection_handler).respond_to?(:connection_pool_list) &&
               ch.connection_pool_list.blank?

  # Key our list of relations for this connection off of the connection pool's object_id
  cp_objid = ActiveRecord::Base.connection_pool.object_id
  @relations ||= {}

  # Have we already seen this database? (Perhaps we're connecting to a replica?)
  @relations[cp_objid] ||= if (db_name = ActiveRecord::Base.connection&.instance_variable_get(:@config)&.fetch(:database, nil)) &&
                              !@relations.key?(cp_objid) &&
                              (existing = @relations.find { |_k, v| v.fetch(:db_name, nil) == db_name })
                             existing.last
                           else
                             Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = {} } }.tap do |h|
                               h[:db_name] = db_name if db_name
                             end
                           end
rescue ActiveRecord::NoDatabaseError
  {}
end

.schema_behavior=(behavior) ⇒ Object

Database schema to use when analysing existing data, such as deriving a list of polymorphic classes for polymorphics in which it wasn’t originally specified.



602
603
604
# File 'lib/brick.rb', line 602

def schema_behavior=(behavior)
  Brick.config.schema_behavior = (behavior.is_a?(Symbol) ? { behavior => nil } : behavior)
end

.schema_behaviour=(behavior) ⇒ Object

For any Brits out there



606
607
608
# File 'lib/brick.rb', line 606

def schema_behaviour=(behavior)
  Brick.schema_behavior = behavior
end

.serializerObject

Get the Brick serializer used by all threads.



710
711
712
# File 'lib/brick.rb', line 710

def serializer
  Brick.config.serializer
end

.serializer=(value) ⇒ Object

Set the Brick serializer. This setting affects all threads.



704
705
706
# File 'lib/brick.rb', line 704

def serializer=(value)
  Brick.config.serializer = value
end

.set_db_schema(params = nil) ⇒ Object



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/brick.rb', line 124

def set_db_schema(params = nil)
  # If Apartment::Tenant.current is not still the default (usually 'public') then an elevator has brought us into
  # a different tenant.  If so then don't allow schema navigation.
  if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' && apartment_multitenant
    current_schema = (ActiveRecord::Base.execute_sql('SELECT current_schemas(true)')
                                        .first['current_schemas'][1..-2]
                                        .split(',') - ['pg_catalog', 'pg_toast', 'heroku_ext']).first
    is_show_schema_list = current_schema == ::Brick.default_schema
    schema = (is_show_schema_list && params && params['_brick_schema']) || ::Brick.default_schema
    chosen = if is_show_schema_list && ::Brick.db_schemas&.key?(schema)
               Apartment::Tenant.switch!(schema)
               schema
             elsif ::Brick.test_schema
               is_show_schema_list = true
               Apartment::Tenant.switch!(::Brick.test_schema)
               ::Brick.test_schema
             else
               current_schema # Just return the current schema
             end
  end
  [chosen == ::Brick.default_schema ? nil : chosen, is_show_schema_list]
end

.sidescroll=(scroll) ⇒ Object



583
584
585
# File 'lib/brick.rb', line 583

def sidescroll=(scroll)
  Brick.config.sidescroll = scroll
end

.skip_database_views=(value) ⇒ Object



446
447
448
# File 'lib/brick.rb', line 446

def skip_database_views=(value)
  Brick.config.skip_database_views = value
end

.skip_index_hms=(value) ⇒ Object

Skip showing counts for these specific has_many associations when building auto-generated #index views



540
541
542
# File 'lib/brick.rb', line 540

def skip_index_hms=(value)
  Brick.config.skip_index_hms = value
end

.sti_modelsObject



101
102
103
# File 'lib/brick.rb', line 101

def sti_models
  @sti_models ||= {}
end

.sti_namespace_prefixes=(snp) ⇒ Object

Module prefixes to build out and associate with specific base STI models



595
596
597
# File 'lib/brick.rb', line 595

def sti_namespace_prefixes=(snp)
  Brick.config.sti_namespace_prefixes = snp
end

.sti_type_column=(type_col) ⇒ Object



610
611
612
# File 'lib/brick.rb', line 610

def sti_type_column=(type_col)
  Brick.config.sti_type_column = (type_col.is_a?(String) ? { type_col => nil } : type_col)
end

.swagger_endpoint(url, name) ⇒ Object

Add a swagger endpoint in the same kind of way that RSwag UI does



338
339
340
341
# File 'lib/brick.rb', line 338

def swagger_endpoint(url, name)
  @swagger_endpoints ||= []
  @swagger_endpoints << { url: url, name: name }
end

.table_name_prefixes=(value) ⇒ Object



466
467
468
# File 'lib/brick.rb', line 466

def table_name_prefixes=(value)
  Brick.config.table_name_prefixes = value
end

.treat_as_module=(value) ⇒ Object

Causes Decidim to work with this line: Brick.treat_as_module = [‘Decidim::ContentBlocks’]



472
473
474
# File 'lib/brick.rb', line 472

def treat_as_module=(value)
  Brick.config.treat_as_module = value
end

.unexclude_column(table, col) ⇒ Object



332
333
334
335
# File 'lib/brick.rb', line 332

def unexclude_column(table, col)
  puts "Unexcluding #{table}.#{col}"
  true
end

.versionObject



724
725
726
# File 'lib/brick.rb', line 724

def version
  VERSION::STRING
end