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/models_generator.rb,
lib/generators/brick/install_generator.rb,
lib/generators/brick/migrations_generator.rb,
lib/brick/extensions.rb

Overview

:nodoc:

Defined Under Namespace

Modules: Cucumber, Extensions, Rails, RouteSet, Serializers, Util, VERSION Classes: Config, InstallGenerator, JoinArray, JoinHash, MigrationsGenerator, ModelsGenerator

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.auto_modelsObject

Returns the value of attribute auto_models.



136
137
138
# File 'lib/brick.rb', line 136

def auto_models
  @auto_models
end

.db_schemasObject

Returns the value of attribute db_schemas.



136
137
138
# File 'lib/brick.rb', line 136

def db_schemas
  @db_schemas
end

.default_schemaObject

Returns the value of attribute default_schema.



136
137
138
# File 'lib/brick.rb', line 136

def default_schema
  @default_schema
end

.is_eager_loadingObject

Returns the value of attribute is_eager_loading.



136
137
138
# File 'lib/brick.rb', line 136

def is_eager_loading
  @is_eager_loading
end

.is_oracleObject

Returns the value of attribute is_oracle.



136
137
138
# File 'lib/brick.rb', line 136

def is_oracle
  @is_oracle
end

.routes_doneObject

Returns the value of attribute routes_done.



136
137
138
# File 'lib/brick.rb', line 136

def routes_done
  @routes_done
end

Class Method Details

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



2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
# File 'lib/brick/extensions.rb', line 2266

def _add_bt_and_hm(fk, relations, is_polymorphic = false, is_optional = false)
  bt_assoc_name = ::Brick.namify(fk[2], :downcase)
  unless is_polymorphic
    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])
  apartment = Object.const_defined?('Apartment') && Apartment
  fk[0] = ::Brick.apartment_default_tenant if apartment && apartment.excluded_models.include?(fk_namified.singularize.camelize)
  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 apartment && apartment&.excluded_models.include?(fk[4].singularize.camelize)
                                    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}", 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.fetch(:isView, nil) }.keys.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]) || (is_polymorphic && 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 is_polymorphic
      # Assuming same fk (don't yet support composite keys for polymorphics)
      assoc_bt[:inverse_table] << fk[4]
    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 is_polymorphic
    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] = true if is_polymorphic
  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] && 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 is_polymorphic
    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

._class_pk(dotted_name, multitenant) ⇒ Object



2525
2526
2527
# File 'lib/brick/extensions.rb', line 2525

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

.additional_references=(ars) ⇒ Object

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



376
377
378
379
380
381
382
383
# File 'lib/brick.rb', line 376

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

.apartment_default_tenantObject



167
168
169
# File 'lib/brick.rb', line 167

def apartment_default_tenant
  Apartment.default_tenant || 'public'
end

.apartment_multitenantObject



160
161
162
163
164
165
# File 'lib/brick.rb', line 160

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

.api_rootObject



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

def api_root
  Brick.config.api_root
end

.api_root=(path) ⇒ Object



335
336
337
# File 'lib/brick.rb', line 335

def api_root=(path)
  Brick.config.api_root = path
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:



553
554
555
556
557
# File 'lib/brick.rb', line 553

def config
  @config ||= Brick::Config.instance
  yield @config if block_given?
  @config
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



592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
# File 'lib/brick.rb', line 592

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|
      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('/')
  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[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[possible_c_path] || res_names[possible_c_path_singular.join('.')]
             ) &&
             (
               klass ||
               ((rel = ::Brick.relations.fetch(res_name, nil)) &&
               (klass ||= rel[:class_name]&.constantize))
             )
    c_path_parts.shift
  end
  [klass, sti_type]
end

.custom_columns=(cust_cols) ⇒ Object

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



387
388
389
390
391
392
# File 'lib/brick.rb', line 387

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



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

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

.display_classes(prefix, rels, max_length) ⇒ Object



583
584
585
586
587
588
589
# File 'lib/brick.rb', line 583

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.last}"
  end
  puts "\n"
end

.eager_load_classes(do_ar_abstract_bases = false) ⇒ Object



564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
# File 'lib/brick.rb', line 564

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
    Zeitwerk::Loader.eager_load_all
  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



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

def enable_api
  Brick.config.enable_api
end

.enable_api=(path) ⇒ Object



325
326
327
# File 'lib/brick.rb', line 325

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

.enable_controllers=(value) ⇒ Object

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



287
288
289
# File 'lib/brick.rb', line 287

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)


294
295
296
# File 'lib/brick.rb', line 294

def enable_controllers?
  !!Brick.config.enable_controllers
end

.enable_models=(value) ⇒ Object

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



274
275
276
# File 'lib/brick.rb', line 274

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)


281
282
283
# File 'lib/brick.rb', line 281

def enable_models?
  !!Brick.config.enable_models
end

.enable_routes=(value) ⇒ Object

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



313
314
315
# File 'lib/brick.rb', line 313

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)


320
321
322
# File 'lib/brick.rb', line 320

def enable_routes?
  !!Brick.config.enable_routes
end

.enable_views=(value) ⇒ Object

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



300
301
302
# File 'lib/brick.rb', line 300

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)


307
308
309
# File 'lib/brick.rb', line 307

def enable_views?
  !!Brick.config.enable_views
end

.ensure_unique(name, *sources) ⇒ Object



2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
# File 'lib/brick/extensions.rb', line 2440

def ensure_unique(name, *sources)
  base = name
  if (added_num = name.slice!(/_(\d+)$/))
    added_num = added_num[1..-1].to_i
  else
    added_num = 1
  end
  while (
    name = "#{base}_#{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



252
253
254
255
# File 'lib/brick.rb', line 252

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)



402
403
404
405
406
407
408
409
# File 'lib/brick.rb', line 402

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



350
351
352
# File 'lib/brick.rb', line 350

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

.existing_stisObject



132
133
134
# File 'lib/brick.rb', line 132

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_orphans(multi_schema) ⇒ Object

Locate orphaned records



2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
# File 'lib/brick/extensions.rb', line 2463

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 (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`.



534
535
536
# File 'lib/brick.rb', line 534

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

.get_bts_and_hms(model) ⇒ Object



193
194
195
196
197
198
199
200
201
202
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
# File 'lib/brick.rb', line 193

def get_bts_and_hms(model)
  bts, hms = model.reflect_on_all_associations.each_with_object([{}, {}]) do |a, s|
    next unless a.polymorphic? || (!a.belongs_to? && (through = a.options[:through])) ||
                (a.klass && ::Brick.config.exclude_tables.exclude?(a.klass.table_name))

    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 = primary_tables&.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.
          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"
        end
      else
        s.first[a.foreign_key.to_s] = [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 "WARNING:  HMT relationship :#{a.name} in model #{model.name} has invalid source :#{a.source_reflection_name}."
          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
      # 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) }
  [bts, hms]
end

.get_status_of_resourcesObject

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



2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
# File 'lib/brick/extensions.rb', line 2391

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)
  models = if Dir.exist?(model_path = "#{rails_root}/app/models")
             Dir["#{model_path}/**/*.rb"].each_with_object({}) do |v, s|
               File.read(v).split("\n").each do |line|
                 # For all non-commented lines, look for any that start with "class " and also "< ApplicationRecord"
                 if line.lstrip.start_with?('class') && (idx = line.index('class'))
                   model = line[idx + 5..-1].match(/[\s:]+([\w:]+)/)&.captures&.first
                   if model && abstract_activerecord_bases.exclude?(model)
                     klass = begin
                               model.constantize
                             rescue
                             end
                     s[model.underscore.tr('/', '.').pluralize] = [
                       v.start_with?(rails_root) ? v[rails_root.length + 1..-1] : v,
                       klass
                     ]
                   end
                 end
               end
             end
           end
  ::Brick.relations.keys.map do |v|
    tbl_parts = v.split('.')
    tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.length > 1 && tbl_parts.first == ::Brick.apartment_default_tenant
    res = tbl_parts.join('.')
    [v, (model = models[res])&.last&.table_name, migrations&.fetch(res, nil), model&.first]
  end
end

.has_ones=(hos) ⇒ Object

Associations to treat as a has_one



419
420
421
422
423
424
425
426
427
428
429
430
# File 'lib/brick.rb', line 419

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

.license=(key) ⇒ Object



469
470
471
# File 'lib/brick.rb', line 469

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



476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
# File 'lib/brick.rb', line 476

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|
          if relations.key?(primary_table = type.underscore.pluralize)
            ::Brick._add_bt_and_hm([nil, table_name, poly, nil, primary_table, "(brick) #{table_name}_#{poly}"], relations, true, is_optional)
          else
            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|
    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

.metadata_columns=(value) ⇒ Object



365
366
367
# File 'lib/brick.rb', line 365

def (value)
  Brick.config. = value
end

.mode=(setting) ⇒ Object



262
263
264
# File 'lib/brick.rb', line 262

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

.model_descrips=(descrips) ⇒ Object

DSL templates for individual models to provide prettier descriptions of objects



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

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

.models_inherit_from=(value) ⇒ Object



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

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



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/brick.rb', line 177

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

.non_tenanted_modelsObject

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



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

def non_tenanted_models
  @pending_models ||= {}
end

.not_nullables=(value) ⇒ Object



370
371
372
# File 'lib/brick.rb', line 370

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

.order=(value) ⇒ Object



395
396
397
# File 'lib/brick.rb', line 395

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

.path_prefix=(path) ⇒ Object

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



268
269
270
# File 'lib/brick.rb', line 268

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

.polymorphics=(polys) ⇒ Object

Polymorphic associations



433
434
435
436
# File 'lib/brick.rb', line 433

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)



155
156
157
158
# File 'lib/brick.rb', line 155

def relations
  # Key our list of relations for this connection off of the connection pool's object_id
  (@relations ||= {})[ActiveRecord::Base.connection_pool.object_id] ||= Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = {} } }
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.



453
454
455
# File 'lib/brick.rb', line 453

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



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

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

.serializerObject

Get the Brick serializer used by all threads.



546
547
548
# File 'lib/brick.rb', line 546

def serializer
  Brick.config.serializer
end

.serializer=(value) ⇒ Object

Set the Brick serializer. This setting affects all threads.



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

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

.set_db_schema(params = nil) ⇒ Object



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/brick.rb', line 138

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.
  chosen = if (is_show_schema_list = (apartment_multitenant && Apartment::Tenant.current == ::Brick.default_schema)) &&
              (schema = (params ? params['_brick_schema'] : ::Brick.default_schema)) &&
              ::Brick.db_schemas&.key?(schema)
             Apartment::Tenant.switch!(schema)
             schema
           elsif ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
             # Just return the current schema
             current_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
             (current_schema - ['pg_catalog', 'pg_toast', 'heroku_ext']).first
           end
  [chosen == ::Brick.default_schema ? nil : chosen, is_show_schema_list]
end

.skip_database_views=(value) ⇒ Object



345
346
347
# File 'lib/brick.rb', line 345

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



413
414
415
# File 'lib/brick.rb', line 413

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

.sti_modelsObject



128
129
130
# File 'lib/brick.rb', line 128

def sti_models
  @sti_models ||= {}
end

.sti_namespace_prefixes=(snp) ⇒ Object

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



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

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

.sti_type_column=(type_col) ⇒ Object



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

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

.table_name_prefixes=(value) ⇒ Object



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

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

.unexclude_column(table, col) ⇒ Object



256
257
258
259
# File 'lib/brick.rb', line 256

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

.versionObject



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

def version
  VERSION::STRING
end