brick 1.0.76 → 1.0.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 615640b22db113a3959644ee9c2d08ff3a2b37aa10f12dc92d68effef091c228
4
- data.tar.gz: b0d3e616b0f44584cf6960f8b8b191fc449b9afc426a6486da11c2deadece7a6
3
+ metadata.gz: 7e113470d473585f716cfa5b835f503448a0d160b60de20f884bf3ce2cc14c08
4
+ data.tar.gz: 40064f980dd6893cb4e7e47861e00b184983d1a4bbc11b7cbd16fd6caddfab87
5
5
  SHA512:
6
- metadata.gz: 02a9f1c74af24e1e23df8b64972d7af8d74bbb870f1510b69d0814f362959e2527f2a7ee9ff7ca65187eb97be4fad1136d64bea7e01a8327a57608924a8f12d8
7
- data.tar.gz: '0265977b746d0955c76bef269067d14363cda4373fc5f726f19fb284baf31d0df3bdaae6ffad7301b71cd630a2745db4d30f1da79ec43ef4ae1a92bc986058e9'
6
+ metadata.gz: 4219dfee478a845a949b1ac9b03d0a135504723a892b571ca068af70c2b58ca52025f5e1f2fedb6a97a5b2dcd97280c5f54132b854e285e6daf7bc95d0c2d357
7
+ data.tar.gz: 67ae65b0de3801bbb2a1817d16b4023116ee7d41491b6d45927bd8197a8f800151f8ce79de34732a55f3dfcabbe301dc69b276899cfd544604d6a64e61220c99
data/lib/brick/config.rb CHANGED
@@ -20,6 +20,15 @@ module Brick
20
20
  @serializer = Brick::Serializers::YAML
21
21
  end
22
22
 
23
+ # Any path prefixing to apply to all auto-generated Brick routes
24
+ def path_prefix
25
+ @mutex.synchronize { @path_prefix }
26
+ end
27
+
28
+ def path_prefix=(path)
29
+ @mutex.synchronize { @path_prefix = path }
30
+ end
31
+
23
32
  # Indicates whether Brick models are on or off. Default: true.
24
33
  def enable_models
25
34
  @mutex.synchronize { !!@enable_models }
@@ -256,12 +256,13 @@ module ActiveRecord
256
256
  table_name == assoc_name ? link : "#{assoc_name}-#{link}".html_safe
257
257
  end
258
258
 
259
- def self._brick_index
260
- tbl_parts = table_name.split('.')
259
+ def self._brick_index(mode = nil)
260
+ tbl_parts = ((mode == :singular) ? table_name.singularize : table_name).split('.')
261
261
  tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.first == Apartment.default_schema
262
- if (index = tbl_parts.map(&:underscore).join('_')) == index.singularize
263
- index << '_index' # Rails applies an _index suffix to that route when the resource name is singular
264
- end
262
+ tbl_parts.unshift(::Brick.config.path_prefix) if ::Brick.config.path_prefix
263
+ index = tbl_parts.map(&:underscore).join('_')
264
+ # Rails applies an _index suffix to that route when the resource name is singular
265
+ index << '_index' if mode != :singular && index == index.singularize
265
266
  index
266
267
  end
267
268
 
@@ -783,50 +784,60 @@ end
783
784
  Module.class_exec do
784
785
  alias _brick_const_missing const_missing
785
786
  def const_missing(*args)
786
- desired_classname = (self == Object) ? args.first.to_s : "#{name}::#{args.first}"
787
+ requested = args.first.to_s
788
+ is_controller = requested.end_with?('Controller')
789
+ # self.name is nil when a model name is requested in an .erb file
790
+ if self.name && ::Brick.config.path_prefix
791
+ camelize_prefix = ::Brick.config.path_prefix.camelize
792
+ # Asking for the prefix module?
793
+ if self == Object && requested == camelize_prefix
794
+ Object.const_set(args.first, (built_module = Module.new))
795
+ puts "module #{camelize_prefix}; end\n"
796
+ return built_module
797
+ end
798
+ split_self_name.shift if (split_self_name = self.name.split('::')).first.blank?
799
+ if split_self_name.first == camelize_prefix
800
+ split_self_name.shift # Remove the identified path prefix from the split name
801
+ if is_controller
802
+ brick_root = split_self_name.empty? ? self : camelize_prefix.constantize
803
+ end
804
+ end
805
+ end
806
+ base_module = if self < ActiveRecord::Migration || !self.name
807
+ brick_root || Object
808
+ elsif (split_self_name || self.name.split('::')).length > 1
809
+ return self._brick_const_missing(*args)
810
+ else
811
+ self
812
+ end
813
+ desired_classname = (self == Object) ? requested : "#{name}::#{requested}"
787
814
  if ((is_defined = self.const_defined?(args.first)) && (possible = self.const_get(args.first)) && possible.name == desired_classname) ||
788
815
  # Try to require the respective Ruby file
789
816
  ((filename = ActiveSupport::Dependencies.search_for_file(desired_classname.underscore) ||
790
- (self != Object && ActiveSupport::Dependencies.search_for_file((desired_classname = args.first.to_s).underscore))
817
+ (self != Object && ActiveSupport::Dependencies.search_for_file((desired_classname = requested).underscore))
791
818
  ) && (require_dependency(filename) || true) &&
792
819
  ((possible = self.const_get(args.first)) && possible.name == desired_classname)
793
820
  ) ||
794
821
  # If any class has turned up so far (and we're not in the middle of eager loading)
795
822
  # then return what we've found.
796
- (is_defined && !::Brick.is_eager_loading)
797
- return possible
798
- end
799
- class_name = ::Brick.namify(args.first.to_s)
800
- # self.name is nil when a model name is requested in an .erb file
801
- base_module = (self < ActiveRecord::Migration || !self.name) ? Object : self
802
- # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
803
- # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
804
- # that is, checking #qualified_name_for with: from_mod, const_name
805
- # If we want to support namespacing in the future, might have to utilise something like this:
806
- # path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
807
- # return self._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
808
- # If the file really exists, go and snag it:
809
- if ActiveSupport::Dependencies.search_for_file(class_name.underscore)
810
- return base_module._brick_const_missing(*args)
811
- # elsif ActiveSupport::Dependencies.search_for_file(filepath) # Last-ditch effort to pick this thing up before we fill in the gaps on our own
812
- # my_const = parent.const_missing(class_name) # ends up having: MyModule::MyClass
813
- # return my_const
814
- else
815
- filepath = base_module.name&.split('::')&.[](0..-2) unless base_module == Object
816
- filepath = ((filepath || []) + [class_name]).join('/').underscore + '.rb'
817
- if ActiveSupport::Dependencies.search_for_file(filepath) # Last-ditch effort to pick this thing up before we fill in the gaps on our own
818
- return base_module._brick_const_missing(*args)
823
+ (is_defined && !::Brick.is_eager_loading) # Used to also have: && possible != self
824
+ if (!brick_root && (filename || possible.instance_of?(Class))) ||
825
+ (possible.instance_of?(Module) &&
826
+ ((possible.respond_to?(:module_parent) ? possible.module_parent : possible.parent) == self)
827
+ ) ||
828
+ (possible.instance_of?(Class) && possible == self) # Are we simply searching for ourselves?
829
+ return possible
819
830
  end
820
831
  end
821
-
832
+ class_name = ::Brick.namify(requested)
822
833
  relations = ::Brick.relations
823
- # puts "ON OBJECT: #{args.inspect}" if self.module_parent == Object
824
- result = if ::Brick.enable_controllers? && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
834
+ result = if ::Brick.enable_controllers? &&
835
+ is_controller && (plural_class_name = class_name[0..-11]).length.positive?
825
836
  # Otherwise now it's up to us to fill in the gaps
837
+ full_class_name = +''
838
+ full_class_name << "::#{(split_self_name&.first && split_self_name.join('::')) || self.name}" unless self == Object
826
839
  # (Go over to underscores for a moment so that if we have something come in like VABCsController then the model name ends up as
827
840
  # Vabc instead of VABC)
828
- full_class_name = +''
829
- full_class_name << "::#{self.name}" unless self == Object
830
841
  singular_class_name = ::Brick.namify(plural_class_name, :underscore).singularize.camelize
831
842
  full_class_name << "::#{singular_class_name}"
832
843
  if plural_class_name == 'BrickSwagger' ||
@@ -929,7 +940,9 @@ class Object
929
940
  schema_name
930
941
  else
931
942
  matching = "#{schema_name}.#{matching}"
932
- (Brick.db_schemas[schema_name] ||= self.const_get(schema_name.camelize))
943
+ # %%% Coming up with integers when tables are in schemas
944
+ # ::Brick.db_schemas[schema_name] ||= self.const_get(schema_name.camelize.to_sym)
945
+ self.const_get(schema_name.camelize)
933
946
  end
934
947
  "#{schema_module&.name}::#{inheritable_name || model_name}"
935
948
  end
@@ -1197,8 +1210,7 @@ class Object
1197
1210
  is_postgres = ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1198
1211
  is_mysql = ActiveRecord::Base.connection.adapter_name == 'Mysql2'
1199
1212
 
1200
- namespace_name = "#{namespace.name}::" if namespace
1201
- code = +"class #{namespace_name}#{class_name} < ApplicationController\n"
1213
+ code = +"class #{class_name} < ApplicationController\n"
1202
1214
  built_controller = Class.new(ActionController::Base) do |new_controller_class|
1203
1215
  (namespace || Object).const_set(class_name.to_sym, new_controller_class)
1204
1216
 
@@ -1341,13 +1353,6 @@ class Object
1341
1353
  code << " end\n"
1342
1354
  self.define_method :show do
1343
1355
  ::Brick.set_db_schema(params)
1344
- id = if model.columns_hash[pk.first]&.type == :string
1345
- is_pk_string = true
1346
- params[:id]
1347
- else
1348
- params[:id]&.split(/[\/,_]/)
1349
- end
1350
- id = id.first if id.is_a?(Array) && id.length == 1
1351
1356
  instance_variable_set("@#{singular_table_name}".to_sym, find_obj)
1352
1357
  end
1353
1358
  end
@@ -1442,7 +1447,14 @@ class Object
1442
1447
  @#{singular_table_name} = #{model.name}.find(id.is_a?(Array) && id.length == 1 ? id.first : id)
1443
1448
  end\n"
1444
1449
  self.define_method :find_obj do
1445
- id = is_pk_string ? params[:id] : params[:id]&.split(/[\/,_]/)
1450
+ id = if model.columns_hash[pk.first]&.type == :string
1451
+ is_pk_string = true
1452
+ params[:id].gsub('^^sl^^', '/')
1453
+ else
1454
+ params[:id]&.split(/[\/,_]/).map do |val_part|
1455
+ val_part.gsub('^^sl^^', '/')
1456
+ end
1457
+ end
1446
1458
  model.find(id.is_a?(Array) && id.length == 1 ? id.first : id)
1447
1459
  end
1448
1460
  end
@@ -1465,33 +1477,55 @@ class Object
1465
1477
  # Get column names for params from relations[model.table_name][:cols].keys
1466
1478
  end
1467
1479
  end # unless is_swagger
1468
- code << "end # #{namespace_name}#{class_name}\n"
1480
+ code << "end # #{class_name}\n"
1469
1481
  end # class definition
1470
1482
  [built_controller, code]
1471
1483
  end
1472
1484
 
1473
1485
  def _brick_get_hm_assoc_name(relation, hm_assoc, source = nil)
1474
- if (relation[:hm_counts][hm_assoc[:inverse_table]]&.> 1) &&
1475
- hm_assoc[:alternate_name] != (source || name.underscore)
1476
- plural = "#{hm_assoc[:assoc_name]}_#{ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name])}"
1477
- new_alt_name = (hm_assoc[:alternate_name] == name.underscore) ? "#{hm_assoc[:assoc_name].singularize}_#{plural}" : plural
1478
- # uniq = 1
1479
- # while same_name = relation[:fks].find { |x| x.last[:assoc_name] == hm_assoc[:assoc_name] && x.last != hm_assoc }
1480
- # hm_assoc[:assoc_name] = "#{hm_assoc_name}_#{uniq += 1}"
1481
- # end
1482
- # puts new_alt_name
1483
- # hm_assoc[:assoc_name] = new_alt_name
1484
- [new_alt_name, true]
1485
- else
1486
- assoc_name = ::Brick.namify(hm_assoc[:inverse_table]).pluralize
1487
- if (needs_class = assoc_name.include?('.')) # If there is a schema name present, use a downcased version for the :has_many
1488
- assoc_parts = assoc_name.split('.')
1489
- assoc_parts[0].downcase! if assoc_parts[0] =~ /^[A-Z0-9_]+$/
1490
- assoc_name = assoc_parts.join('.')
1486
+ assoc_name, needs_class = if (relation[:hm_counts][hm_assoc[:inverse_table]]&.> 1) &&
1487
+ hm_assoc[:alternate_name] != (source || name.underscore)
1488
+ plural = "#{hm_assoc[:assoc_name]}_#{ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name])}"
1489
+ new_alt_name = (hm_assoc[:alternate_name] == name.underscore) ? "#{hm_assoc[:assoc_name].singularize}_#{plural}" : plural
1490
+ # uniq = 1
1491
+ # while same_name = relation[:fks].find { |x| x.last[:assoc_name] == hm_assoc[:assoc_name] && x.last != hm_assoc }
1492
+ # hm_assoc[:assoc_name] = "#{hm_assoc_name}_#{uniq += 1}"
1493
+ # end
1494
+ # puts new_alt_name
1495
+ # hm_assoc[:assoc_name] = new_alt_name
1496
+ [new_alt_name, true]
1497
+ else
1498
+ assoc_name = ::Brick.namify(hm_assoc[:inverse_table]).pluralize
1499
+ if (needs_class = assoc_name.include?('.')) # If there is a schema name present, use a downcased version for the :has_many
1500
+ assoc_parts = assoc_name.split('.')
1501
+ assoc_parts[0].downcase! if assoc_parts[0] =~ /^[A-Z0-9_]+$/
1502
+ assoc_name = assoc_parts.join('.')
1503
+ end
1504
+ # hm_assoc[:assoc_name] = assoc_name
1505
+ [assoc_name, needs_class]
1506
+ end
1507
+ # Already have the HM class around?
1508
+ begin
1509
+ if (hm_class = Object._brick_const_missing(hm_class_name = relation[:class_name].to_sym))
1510
+ existing_hm_assocs = hm_class.reflect_on_all_associations.select do |assoc|
1511
+ assoc.macro != :belongs_to && assoc.klass == self && assoc.foreign_key == hm_assoc[:fk]
1512
+ end
1513
+ # Missing a has_many in an existing class?
1514
+ if existing_hm_assocs.empty?
1515
+ options = { inverse_of: hm_assoc[:inverse][:assoc_name].to_sym }
1516
+ # Add class_name and foreign_key where necessary
1517
+ unless hm_assoc[:alternate_name] == (source || name.underscore)
1518
+ options[:class_name] = self.name
1519
+ options[:foreign_key] = hm_assoc[:fk].to_sym
1520
+ end
1521
+ hm_class.send(:has_many, assoc_name.to_sym, options)
1522
+ puts "# ** Adding a missing has_many to #{hm_class.name}:\nclass #{hm_class.name} < #{hm_class.superclass.name}"
1523
+ puts " has_many :#{assoc_name}, #{options.inspect}\nend\n"
1524
+ end
1491
1525
  end
1492
- # hm_assoc[:assoc_name] = assoc_name
1493
- [assoc_name, needs_class]
1526
+ rescue NameError
1494
1527
  end
1528
+ [assoc_name, needs_class]
1495
1529
  end
1496
1530
  end
1497
1531
  end
@@ -1504,6 +1538,7 @@ module ActiveRecord::ConnectionHandling
1504
1538
  alias _brick_establish_connection establish_connection
1505
1539
  def establish_connection(*args)
1506
1540
  conn = _brick_establish_connection(*args)
1541
+ begin
1507
1542
  # Overwrite SQLite's #begin_db_transaction so it opens in IMMEDIATE mode instead of
1508
1543
  # the default DEFERRED mode.
1509
1544
  # https://discuss.rubyonrails.org/t/failed-write-transaction-upgrades-in-sqlite3/81480/2
@@ -1525,9 +1560,10 @@ module ActiveRecord::ConnectionHandling
1525
1560
  end
1526
1561
  end
1527
1562
  end
1528
- begin
1563
+ # ::Brick.is_db_present = true
1529
1564
  _brick_reflect_tables
1530
1565
  rescue ActiveRecord::NoDatabaseError
1566
+ # ::Brick.is_db_present = false
1531
1567
  end
1532
1568
  conn
1533
1569
  end
@@ -1535,6 +1571,8 @@ module ActiveRecord::ConnectionHandling
1535
1571
  # This is done separately so that during testing it can be called right after a migration
1536
1572
  # in order to make sure everything is good.
1537
1573
  def _brick_reflect_tables
1574
+ # return if ActiveRecord::Base.connection.current_database == 'postgres'
1575
+
1538
1576
  initializer_loaded = false
1539
1577
  if (relations = ::Brick.relations).empty?
1540
1578
  # If there's schema things configured then we only expect our initializer to be named exactly this
@@ -1543,8 +1581,8 @@ module ActiveRecord::ConnectionHandling
1543
1581
  end
1544
1582
  # Load the initializer for the Apartment gem a little early so that if .excluded_models and
1545
1583
  # .default_schema are specified then we can work with non-tenanted models more appropriately
1546
- apartment = Object.const_defined?('Apartment')
1547
- if apartment && File.exist?(apartment_initializer = ::Rails.root.join('config/initializers/apartment.rb'))
1584
+ if (apartment = Object.const_defined?('Apartment')) &&
1585
+ File.exist?(apartment_initializer = ::Rails.root.join('config/initializers/apartment.rb'))
1548
1586
  load apartment_initializer
1549
1587
  apartment_excluded = Apartment.excluded_models
1550
1588
  end
@@ -1556,19 +1594,21 @@ module ActiveRecord::ConnectionHandling
1556
1594
  case ActiveRecord::Base.connection.adapter_name
1557
1595
  when 'PostgreSQL', 'SQLServer'
1558
1596
  is_postgres = !is_mssql
1559
- db_schemas = ActiveRecord::Base.execute_sql('SELECT DISTINCT table_schema FROM INFORMATION_SCHEMA.tables;')
1597
+ db_schemas = if is_postgres
1598
+ ActiveRecord::Base.execute_sql('SELECT nspname AS table_schema, MAX(oid) AS dt FROM pg_namespace GROUP BY 1 ORDER BY 1;')
1599
+ else
1600
+ ActiveRecord::Base.execute_sql('SELECT DISTINCT table_schema, NULL AS dt FROM INFORMATION_SCHEMA.tables;')
1601
+ end
1560
1602
  ::Brick.db_schemas = db_schemas.each_with_object({}) do |row, s|
1561
1603
  row = case row
1562
- when String
1563
- row
1564
1604
  when Array
1565
- row.first
1605
+ row
1566
1606
  else
1567
- row['table_schema']
1607
+ [row['table_schema'], row['dt']]
1568
1608
  end
1569
1609
  # Remove any system schemas
1570
- s[row] = nil unless ['information_schema', 'pg_catalog',
1571
- 'INFORMATION_SCHEMA', 'sys'].include?(row)
1610
+ s[row.first] = row.last unless ['information_schema', 'pg_catalog', 'pg_toast',
1611
+ 'INFORMATION_SCHEMA', 'sys'].include?(row.first)
1572
1612
  end
1573
1613
  if (is_multitenant = (multitenancy = ::Brick.config.schema_behavior[:multitenant]) &&
1574
1614
  (sta = multitenancy[:schema_to_analyse]) != 'public') &&
@@ -1602,6 +1642,10 @@ module ActiveRecord::ConnectionHandling
1602
1642
  if ::Brick.db_schemas.key?(possible_schema)
1603
1643
  ::Brick.default_schema = schema = possible_schema
1604
1644
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1645
+ elsif Rails.env == 'test' # When testing, just find the most recently-created schema
1646
+ ::Brick.default_schema = schema = ::Brick.db_schemas.to_a.sort { |a, b| b.last <=> a.last }.first.first
1647
+ puts "While running tests, had noticed that in the brick.rb initializer the line \"::Brick.schema_behavior = ...\" refers to a schema called \"#{possible_schema}\" which does not exist. Reverting to instead use the most recently-created schema, #{schema}."
1648
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1605
1649
  else
1606
1650
  puts "*** In the brick.rb initializer the line \"::Brick.schema_behavior = ...\" refers to a schema called \"#{possible_schema}\". This schema does not exist. ***"
1607
1651
  end
@@ -1614,7 +1658,7 @@ module ActiveRecord::ConnectionHandling
1614
1658
  measures = []
1615
1659
  ::Brick.is_oracle = true if ActiveRecord::Base.connection.adapter_name == 'OracleEnhanced'
1616
1660
  case ActiveRecord::Base.connection.adapter_name
1617
- when 'PostgreSQL', 'SQLite' # These bring back a hash for each row because the query uses column aliases
1661
+ when 'PostgreSQL', 'SQLite', 'SQLServer' # These bring back a hash for each row because the query uses column aliases
1618
1662
  # schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1619
1663
  ActiveRecord::Base.retrieve_schema_and_tables(sql, is_postgres, is_mssql, schema).each do |r|
1620
1664
  # If Apartment gem lists the table as being associated with a non-tenanted model then use whatever it thinks
@@ -1823,7 +1867,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
1823
1867
  LEFT OUTER JOIN INFORMATION_SCHEMA.columns AS c ON t.table_schema = c.table_schema
1824
1868
  AND t.table_name = c.table_name
1825
1869
  LEFT OUTER JOIN
1826
- (SELECT kcu1.constraint_schema, kcu1.table_name, kcu1.ordinal_position,
1870
+ (SELECT kcu1.constraint_schema, kcu1.table_name, kcu1.column_name, kcu1.ordinal_position,
1827
1871
  tc.constraint_type, kcu1.constraint_name
1828
1872
  FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu1
1829
1873
  INNER JOIN INFORMATION_SCHEMA.table_constraints AS tc
@@ -1834,9 +1878,9 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
1834
1878
  ) AS kcu ON
1835
1879
  -- kcu.CONSTRAINT_CATALOG = t.table_catalog AND
1836
1880
  kcu.CONSTRAINT_SCHEMA = c.table_schema
1837
- AND kcu.TABLE_NAME = c.table_name#{"
1881
+ AND kcu.TABLE_NAME = c.table_name
1882
+ AND kcu.column_name = c.column_name#{"
1838
1883
  -- AND kcu.position_in_unique_constraint IS NULL" unless is_mssql}
1839
- AND kcu.ordinal_position = c.ordinal_position
1840
1884
  WHERE t.table_schema #{is_postgres || is_mssql ?
1841
1885
  "NOT IN ('information_schema', 'pg_catalog',
1842
1886
  'INFORMATION_SCHEMA', 'sys')"
@@ -1845,7 +1889,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
1845
1889
  AND t.table_schema = COALESCE(current_setting('SEARCH_PATH'), 'public')" if is_postgres && schema }
1846
1890
  -- AND t.table_type IN ('VIEW') -- 'BASE TABLE', 'FOREIGN TABLE'
1847
1891
  AND t.table_name NOT IN ('pg_stat_statements', ?, ?)
1848
- ORDER BY 1, t.table_type DESC, 2, c.ordinal_position"
1892
+ ORDER BY 1, t.table_type DESC, 2, kcu.ordinal_position"
1849
1893
  ActiveRecord::Base.execute_sql(sql, *ar_tables)
1850
1894
  end
1851
1895
 
@@ -2090,7 +2134,7 @@ module Brick
2090
2134
  end
2091
2135
  end
2092
2136
  end
2093
- ::Brick.relations.keys.map { |v| [(r = v.pluralize), (model = models[r])&.last&.table_name || v, migrations&.fetch(r, nil), model&.first] }
2137
+ ::Brick.relations.keys.map { |v| [(model = models[v])&.last, model&.last&.table_name, migrations&.fetch(v, nil), model&.first] }
2094
2138
  end
2095
2139
 
2096
2140
  def ensure_unique(name, *sources)
@@ -114,7 +114,7 @@ module Brick
114
114
  if @_brick_model
115
115
  pk = @_brick_model._brick_primary_key(::Brick.relations.fetch(@_brick_model&.table_name, nil))
116
116
  obj_name = model_name.split('::').last.underscore
117
- path_obj_name = model_name.underscore.tr('/', '_')
117
+ path_obj_name = @_brick_model._brick_index(:singular)
118
118
  table_name = obj_name.pluralize
119
119
  template_link = nil
120
120
  bts, hms = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
@@ -125,9 +125,11 @@ module Brick
125
125
  "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]}",
126
126
  (assoc_name = hm.first)]
127
127
  hm_fk_name = if (through = hm_assoc.options[:through])
128
- next unless @_brick_model.instance_methods.include?(through)
128
+ # %%% How to deal with weird self_ref type has_many -> has_one polymorphic stuff?
129
+ # (or perhaps we don't need to!)
130
+ next unless @_brick_model.instance_methods.include?(through) &&
131
+ (associative = @_brick_model._br_associatives.fetch(hm.first, nil))
129
132
 
130
- associative = @_brick_model._br_associatives[hm.first]
131
133
  tbl_nm = if hm_assoc.options[:source]
132
134
  associative.klass.reflect_on_association(hm_assoc.options[:source]).inverse_of&.name
133
135
  else
@@ -177,6 +179,7 @@ module Brick
177
179
  # %%% If we are not auto-creating controllers (or routes) then omit by default, and if enabled anyway, such as in a development
178
180
  # environment or whatever, then get either the controllers or routes list instead
179
181
  apartment_default_schema = ::Brick.apartment_multitenant && Apartment.default_schema
182
+ prefix = "#{::Brick.config.path_prefix}/" if ::Brick.config.path_prefix
180
183
  table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables).each_with_object({}) do |tbl, s|
181
184
  binding.pry if tbl.is_a?(Symbol)
182
185
  if (tbl_parts = tbl.split('.')).first == apartment_default_schema
@@ -184,7 +187,7 @@ module Brick
184
187
  end
185
188
  s[tbl] = nil
186
189
  end.keys.sort.each_with_object(+'') do |v, s|
187
- s << "<option value=\"#{v.underscore.gsub('.', '/')}\">#{v}</option>"
190
+ s << "<option value=\"#{prefix}#{v.underscore.gsub('.', '/')}\">#{v}</option>"
188
191
  end.html_safe
189
192
  table_options << '<option value="brick_status">(Status)</option>'.html_safe if ::Brick.config.add_status
190
193
  table_options << '<option value="brick_orphans">(Orphans)</option>'.html_safe if is_orphans
@@ -405,6 +408,11 @@ def display_value(col_type, val)
405
408
  end
406
409
  end
407
410
  end
411
+ # Accommodate composite primary keys that include strings with forward-slash characters
412
+ def slashify(val)
413
+ val = [val] unless val.is_a?(Array)
414
+ val.map { |val_part| val_part.is_a?(String) ? val_part.gsub('/', '^^sl^^') : val_part }
415
+ end
408
416
  callbacks = {} %>"
409
417
 
410
418
  if ['index', 'show', 'new', 'update'].include?(args.first)
@@ -461,7 +469,7 @@ window.addEventListener(\"pageshow\", function() {
461
469
  });
462
470
 
463
471
  if (tblSelect) { // Always present
464
- var i = schemaSelect ? 1 : 0,
472
+ var i = #{::Brick.config.path_prefix ? '0' : 'schemaSelect ? 1 : 0'},
465
473
  changeoutList = changeout(location.href);
466
474
  for (; i < changeoutList.length; ++i) {
467
475
  tblSelect.value = changeoutList[i];
@@ -523,15 +531,19 @@ if (grid) {
523
531
  function gridMove(evt) {
524
532
  var lastHighCell = gridHighCell;
525
533
  gridHighCell = document.elementFromPoint(evt.x, evt.y);
526
- if (lastHighCell !== gridHighCell) {
527
- gridHighCell.classList.add(\"highlight\");
528
- if (lastHighCell) lastHighCell.classList.remove(\"highlight\");
529
- }
530
- var lastHighHeader = gridHighHeader;
531
- gridHighHeader = headerCols[gridHighCell.cellIndex];
532
- if (lastHighHeader !== gridHighHeader) {
533
- if (gridHighHeader) gridHighHeader.classList.add(\"highlight\");
534
- if (lastHighHeader) lastHighHeader.classList.remove(\"highlight\");
534
+ while (gridHighCell && gridHighCell.tagName !== \"TD\" && gridHighCell.tagName !== \"TH\")
535
+ gridHighCell = gridHighCell.parentElement;
536
+ if (gridHighCell) {
537
+ if (lastHighCell !== gridHighCell) {
538
+ gridHighCell.classList.add(\"highlight\");
539
+ if (lastHighCell) lastHighCell.classList.remove(\"highlight\");
540
+ }
541
+ var lastHighHeader = gridHighHeader;
542
+ gridHighHeader = headerCols[gridHighCell.cellIndex];
543
+ if (lastHighHeader !== gridHighHeader) {
544
+ if (gridHighHeader) gridHighHeader.classList.add(\"highlight\");
545
+ if (lastHighHeader) lastHighHeader.classList.remove(\"highlight\");
546
+ }
535
547
  }
536
548
  }
537
549
  }
@@ -849,24 +861,25 @@ erDiagram
849
861
  @#{table_name}.each do |#{obj_name}|
850
862
  hms_cols = {#{hms_columns.join(', ')}} %>
851
863
  <tr>#{"
852
- <td><%= link_to '⇛', #{path_obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
864
+ <td><%= link_to '⇛', #{path_obj_name}_path(slashify(#{obj_pk})), { class: 'big-arrow' } %></td>" if obj_pk}
853
865
  <% @_brick_sequence.each do |col_name|
854
866
  val = #{obj_name}.attributes[col_name] %>
855
867
  <td<%= ' class=\"dimmed\"'.html_safe unless cols.key?(col_name) || (cust_col = cust_cols[col_name])%>><%
856
868
  if (bt = bts[col_name])
857
869
  if bt[2] # Polymorphic?
858
- bt_class = #{obj_name}.send(\"#\{bt.first\}_type\")
859
- base_class = (::Brick.existing_stis[bt_class] || bt_class).constantize.base_class.name.underscore
860
- poly_id = #{obj_name}.send(\"#\{bt.first\}_id\")
861
- %><%= link_to(\"#\{bt_class\} ##\{poly_id\}\", send(\"#\{base_class\}_path\".to_sym, poly_id)) if poly_id %><%
870
+ bt_class = #{obj_name}.send(\"#\{bt.first}_type\")
871
+ base_class_underscored = (::Brick.existing_stis[bt_class] || bt_class).constantize.base_class._brick_index(:singular)
872
+ poly_id = #{obj_name}.send(\"#\{bt.first}_id\")
873
+ %><%= link_to(\"#\{bt_class} ##\{poly_id}\", send(\"#\{base_class_underscored}_path\".to_sym, poly_id)) if poly_id %><%
862
874
  else
875
+ # binding.pry if @_brick_bt_descrip[bt.first][bt[1].first.first].nil?
863
876
  bt_txt = (bt_class = bt[1].first.first).brick_descrip(
864
877
  # 0..62 because Postgres column names are limited to 63 characters
865
878
  #{obj_name}, (descrips = @_brick_bt_descrip[bt.first][bt_class])[0..-2].map { |id| #{obj_name}.send(id.last[0..62]) }, (bt_id_col = descrips.last)
866
879
  )
867
880
  bt_txt ||= \"<span class=\\\"orphan\\\">&lt;&lt; Orphaned ID: #\{val} >></span>\".html_safe if val
868
881
  bt_id = bt_id_col.map { |id_col| #{obj_name}.send(id_col.to_sym) } %>
869
- <%= bt_id&.first ? link_to(bt_txt, send(\"#\{bt_class.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, bt_id)) : bt_txt %>
882
+ <%= bt_id&.first ? link_to(bt_txt, send(\"#\{bt_class.base_class._brick_index(:singular)}_path\".to_sym, bt_id)) : bt_txt %>
870
883
  <% end
871
884
  elsif (hms_col = hms_cols[col_name])
872
885
  if hms_col.length == 1 %>
@@ -877,9 +890,9 @@ erDiagram
877
890
  descrips = @_brick_bt_descrip[col_name.to_sym][klass]
878
891
  ho_txt = klass.brick_descrip(#{obj_name}, descrips[0..-2].map { |id| #{obj_name}.send(id.last[0..62]) }, (ho_id_col = descrips.last))
879
892
  ho_id = ho_id_col.map { |id_col| #{obj_name}.send(id_col.to_sym) }
880
- ho_id&.first ? link_to(ho_txt, send(\"#\{klass.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, ho_id)) : ho_txt
893
+ ho_id&.first ? link_to(ho_txt, send(\"#\{klass.base_class._brick_index(:singular)}_path\".to_sym, ho_id)) : ho_txt
881
894
  else
882
- \"#\{hms_col[1] || 'View'\} #\{hms_col.first}\"
895
+ \"#\{hms_col[1] || 'View'} #\{hms_col.first}\"
883
896
  end %>
884
897
  <%= link_to txt, send(\"#\{klass._brick_index}_path\".to_sym, hms_col[2]) unless hms_col[1]&.zero? %>
885
898
  <% end
@@ -927,7 +940,7 @@ erDiagram
927
940
  @resources.each do |r|
928
941
  %>
929
942
  <tr>
930
- <td><%= link_to(r[0], \"/#\{r[0].underscore.tr('.', '/')}\") %></td>
943
+ <td><%= link_to(r[0], r[0] && send(\"#\{r[0]&._brick_index}_path\".to_sym)) %></td>
931
944
  <td<%= if r[1]
932
945
  ' class=\"orphan\"' unless ::Brick.relations.key?(r[1])
933
946
  else
@@ -984,12 +997,15 @@ if (description = (relation = Brick.relations[#{model_name}.table_name])&.fetch(
984
997
  end
985
998
  %><%= link_to '(See all #{obj_name.pluralize})', #{@_brick_model._brick_index}_path %>
986
999
  #{erd_markup}
987
- <% if obj %>
1000
+ <% if obj
1001
+ # path_options = [obj.#{pk}]
1002
+ # path_options << { '_brick_schema': } if
1003
+ # url = send(:#\{model_name._brick_index(:singular)}_path, obj.#{pk})
1004
+ options = {}
1005
+ options[:url] = send(\"#\{#{model_name}._brick_index(:singular)}_path\".to_sym, obj) if ::Brick.config.path_prefix
1006
+ %>
988
1007
  <br><br>
989
- <%= # path_options = [obj.#{pk}]
990
- # path_options << { '_brick_schema': } if
991
- # url = send(:#{model_name.underscore}_path, obj.#{pk})
992
- form_for(obj.becomes(#{model_name})) do |f| %>
1008
+ <%= form_for(obj.becomes(#{model_name}), options) do |f| %>
993
1009
  <table class=\"shadow\">
994
1010
  <% has_fields = false
995
1011
  @#{obj_name}.attributes.each do |k, val|
@@ -1008,7 +1024,7 @@ end
1008
1024
  bt_pair = nil
1009
1025
  loop do
1010
1026
  bt_pair = bt[1].find { |pair| pair.first.name == poly_class_name }
1011
- # Acxommodate any valid STI by going up the chain of inheritance
1027
+ # Accommodate any valid STI by going up the chain of inheritance
1012
1028
  break unless bt_pair.nil? && poly_class_name = ::Brick.existing_stis[poly_class_name]
1013
1029
  end
1014
1030
  puts \"*** Might be missing an STI class called #\{orig_poly_name\} whose base class should have this:
@@ -1043,7 +1059,7 @@ end
1043
1059
  html_options[:prompt] = \"Select #\{bt_name\}\" %>
1044
1060
  <%= f.select k.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
1045
1061
  <%= if (bt_obj = bt_class&.find_by(bt_pair[1] => val))
1046
- link_to('⇛', send(\"#\{bt_class.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, bt_obj.send(bt_class.primary_key.to_sym)), { class: 'show-arrow' })
1062
+ link_to('⇛', send(\"#\{bt_class.base_class._brick_index(:singular)\}_path\".to_sym, bt_obj.send(bt_class.primary_key.to_sym)), { class: 'show-arrow' })
1047
1063
  elsif val
1048
1064
  \"<span class=\\\"orphan\\\">Orphaned ID: #\{val}</span>\".html_safe
1049
1065
  end %>
@@ -1101,9 +1117,10 @@ end
1101
1117
  <tr><td colspan=\"2\">(No displayable fields)</td></tr>
1102
1118
  <% end %>
1103
1119
  </table>
1104
- <% end %>
1120
+ <% end %>
1105
1121
 
1106
- #{hms_headers.each_with_object(+'') do |hm, s|
1122
+ #{unless args.first == 'new'
1123
+ hms_headers.each_with_object(+'') do |hm, s|
1107
1124
  # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
1108
1125
  next if hm.first.options[:through] && !hm.first.through_reflection
1109
1126
 
@@ -1118,14 +1135,15 @@ end
1118
1135
  <tr><td>(none)</td></tr>
1119
1136
  <% else %>
1120
1137
  <% collection.uniq.each do |#{hm_singular_name}| %>
1121
- <tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.name.underscore.tr('/', '_')}_path([#{obj_pk}])) %></td></tr>
1138
+ <tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass._brick_index(:singular)}_path(slashify(#{obj_pk}))) %></td></tr>
1122
1139
  <% end %>
1123
1140
  <% end %>
1124
1141
  </table>"
1125
1142
  else
1126
1143
  s
1127
1144
  end
1128
- end}
1145
+ end
1146
+ end}
1129
1147
  <% end %>
1130
1148
  #{script}"
1131
1149
 
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 76
8
+ TINY = 77
9
9
 
10
10
  # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
11
  PRE = nil
data/lib/brick.rb CHANGED
@@ -216,6 +216,12 @@ module Brick
216
216
  true
217
217
  end
218
218
 
219
+ # Any path prefixing to apply to all auto-generated Brick routes
220
+ # @api public
221
+ def path_prefix=(path)
222
+ Brick.config.path_prefix = path
223
+ end
224
+
219
225
  # Switches Brick auto-models on or off, for all threads
220
226
  # @api public
221
227
  def enable_models=(value)
@@ -538,23 +544,40 @@ In config/initializers/brick.rb appropriate entries would look something like:
538
544
  view_class_length = 37 # Length of "Classes that can be built from views:"
539
545
  existing_controllers = routes.each_with_object({}) { |r, s| c = r.defaults[:controller]; s[c] = nil if c }
540
546
  ::Rails.application.routes.append do
547
+ brick_routes_create = lambda do |schema_name, controller_name, v, options|
548
+ if schema_name # && !Object.const_defined('Apartment')
549
+ send(:namespace, schema_name) do
550
+ send(:resources, v[:resource].to_sym, **options)
551
+ end
552
+ else
553
+ send(:resources, v[:resource].to_sym, **options)
554
+ end
555
+ end
556
+
541
557
  # %%% TODO: If no auto-controllers then enumerate the controllers folder in order to build matching routes
542
558
  # If auto-controllers and auto-models are both enabled then this makes sense:
543
559
  ::Brick.relations.each do |k, v|
544
560
  unless !(controller_name = v.fetch(:resource, nil)&.pluralize) || existing_controllers.key?(controller_name)
545
561
  options = {}
546
562
  options[:only] = [:index, :show] if v.key?(:isView)
563
+ # First do the API routes
547
564
  full_resource = nil
548
- if (schema_name = v.fetch(:schema, nil)) # && !Object.const_defined('Apartment')
549
- send(:namespace, schema_name) do
550
- send(:resources, v[:resource].to_sym, **options)
551
- end
565
+ controller_prefix = (::Brick.config.path_prefix ? "#{::Brick.config.path_prefix}/" : '')
566
+ if (schema_name = v.fetch(:schema, nil))
552
567
  full_resource = "#{schema_name}/#{v[:resource]}"
553
- send(:get, "#{::Brick.api_root}#{full_resource}", { to: "#{schema_name}/#{controller_name}#index" }) if Object.const_defined?('Rswag::Ui')
568
+ send(:get, "#{::Brick.api_root}#{full_resource}", { to: "#{controller_prefix}#{schema_name}/#{controller_name}#index" }) if Object.const_defined?('Rswag::Ui')
554
569
  else
555
- send(:resources, v[:resource].to_sym, **options)
556
570
  # Normally goes to something like: /api/v1/employees
557
- send(:get, "#{::Brick.api_root}#{v[:resource]}", { to: "#{controller_name}#index" }) if Object.const_defined?('Rswag::Ui')
571
+ send(:get, "#{::Brick.api_root}#{v[:resource]}", { to: "#{controller_prefix}#{controller_name}#index" }) if Object.const_defined?('Rswag::Ui')
572
+ end
573
+ # Now the normal routes
574
+ if ::Brick.config.path_prefix
575
+ # Was: send(:scope, path: ::Brick.config.path_prefix) do
576
+ send(:namespace, ::Brick.config.path_prefix) do
577
+ brick_routes_create.call(schema_name, controller_name, v, options)
578
+ end
579
+ else
580
+ brick_routes_create.call(schema_name, controller_name, v, options)
558
581
  end
559
582
 
560
583
  if (class_name = v.fetch(:class_name, nil))
@@ -139,6 +139,10 @@ module Brick
139
139
  # # Settings for the Brick gem
140
140
  # # (By default this auto-creates models, controllers, views, and routes on-the-fly.)
141
141
 
142
+ # # Custom path prefix to apply to all auto-generated Brick routes. Also causes auto-generated controllers
143
+ # # to be created inside a module with the same name.
144
+ # ::Brick.path_prefix = 'admin'
145
+
142
146
  # # Normally all are enabled in development mode, and for security reasons only models are enabled in production
143
147
  # # and test. This allows you to either (a) turn off models entirely, or (b) enable controllers, views, and routes
144
148
  # # in production.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brick
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.76
4
+ version: 1.0.77
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorin Thwaits
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-05 00:00:00.000000000 Z
11
+ date: 2022-10-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord