brick 1.0.76 → 1.0.77

Sign up to get free protection for your applications and to get access to all the features.
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