brick 1.0.58 → 1.0.61

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: 508d41fa8b75089e0467ece56ab53fc78b651f3951eabe62d108a62fa8b0f35e
4
- data.tar.gz: dcd419560d850fb2014aab3076a6a80f5a555320ac2c2183e3381a916d94dbc8
3
+ metadata.gz: 67de1c0cde3f28379388dfe0c2aa488197c5cfaad1a108408b506ba66ab64b38
4
+ data.tar.gz: 7d3e40960043e5e09e483dee791dd5f7c1d0f54f692a39ec2ef85b1b5cba1073
5
5
  SHA512:
6
- metadata.gz: 49c90c7fbb1845e151f91b9df7f3b639d587e6239dae5e7f31306807a2b4dc7ac83436d78d46839accfea7c614fd2a94f2460443ea3daae424d280fae17cac77
7
- data.tar.gz: 7dd02fd9ab902cfe6359b6be95101ec3a89c47d8b7bc71692ab4537f5ffeb3a24a4ee73063fc10b3d4197ce59fd5c94380f7534f66647d8c03dd512c6ef0c334
6
+ metadata.gz: 90ae5f021e562d3188c4bde03e9b71a744417c5223f5736cff97cce90646b8b06860b245dcb718414fdce00df2c772bcbe1ad0d102310d7f3ff030641591387d
7
+ data.tar.gz: 704f2dde1c0043a141d96ff272346ac583e7f784df494bb4ce39f4bf5e2cd06ffadfa37783a1bfb0671c5a5fd120819c9f286e5c9bd0608764aa10338840d881
data/lib/brick/config.rb CHANGED
@@ -230,6 +230,11 @@ module Brick
230
230
  @mutex.synchronize { @not_nullables = columns }
231
231
  end
232
232
 
233
+ # Add status page showing all resources and what files have been built out for them
234
+ def add_status
235
+ true
236
+ end
237
+
233
238
  # Add a special page to show references to non-existent records ("orphans")
234
239
  def add_orphans
235
240
  true
@@ -373,6 +373,7 @@ module ActiveRecord
373
373
  end
374
374
 
375
375
  def brick_select(params, selects = nil, order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new)
376
+ is_mysql = ActiveRecord::Base.connection.adapter_name == 'Mysql2'
376
377
  is_distinct = nil
377
378
  wheres = {}
378
379
  params.each do |k, v|
@@ -397,12 +398,14 @@ module ActiveRecord
397
398
  if selects&.empty? # Default to all columns
398
399
  tbl_no_schema = table.name.split('.').last
399
400
  columns.each do |col|
400
- if (col_name = col.name) == 'class'
401
- col_alias = ' AS _class'
402
- end
403
- # Postgres can not use DISTINCT with any columns that are XML, so for any of those just convert to text
404
- cast_as_text = '::text' if is_distinct && Brick.relations[klass.table_name]&.[](:cols)&.[](col.name)&.first&.start_with?('xml')
405
- selects << "\"#{tbl_no_schema}\".\"#{col_name}\"#{cast_as_text}#{col_alias}"
401
+ col_alias = ' AS _class' if (col_name = col.name) == 'class'
402
+ selects << if is_mysql
403
+ "`#{tbl_no_schema}`.`#{col_name}`#{col_alias}"
404
+ else
405
+ # Postgres can not use DISTINCT with any columns that are XML, so for any of those just convert to text
406
+ cast_as_text = '::text' if is_distinct && Brick.relations[klass.table_name]&.[](:cols)&.[](col.name)&.first&.start_with?('xml')
407
+ "\"#{tbl_no_schema}\".\"#{col_name}\"#{cast_as_text}#{col_alias}"
408
+ end
406
409
  end
407
410
  end
408
411
 
@@ -430,7 +433,11 @@ module ActiveRecord
430
433
  if used_col_aliases.key?(col_alias = "_brfk_#{v.first}__#{sel_col.last}")
431
434
  col_alias = "_brfk_#{v.first}__#{v1[idx][-2..-1].map(&:to_s).join('__')}"
432
435
  end
433
- selects << "\"#{field_tbl_name}\".\"#{sel_col.last}\"#{'::text' if is_xml} AS \"#{col_alias}\""
436
+ selects << if is_mysql
437
+ "`#{field_tbl_name}`.`#{sel_col.last}` AS `#{col_alias}`"
438
+ else
439
+ "\"#{field_tbl_name}\".\"#{sel_col.last}\"#{'::text' if is_xml} AS \"#{col_alias}\""
440
+ end
434
441
  used_col_aliases[col_alias] = nil
435
442
  v1[idx] << col_alias
436
443
  end
@@ -439,7 +446,11 @@ module ActiveRecord
439
446
  # Accommodate composite primary key by allowing id_col to come in as an array
440
447
  ((id_col = k1.primary_key).is_a?(Array) ? id_col : [id_col]).each do |id_part|
441
448
  id_for_tables[v.first] << if id_part
442
- selects << "#{"\"#{tbl_name}\".\"#{id_part}\""} AS \"#{(id_alias = "_brfk_#{v.first}__#{id_part}")}\""
449
+ selects << if is_mysql
450
+ "#{"`#{tbl_name}`.`#{id_part}`"} AS `#{(id_alias = "_brfk_#{v.first}__#{id_part}")}`"
451
+ else
452
+ "#{"\"#{tbl_name}\".\"#{id_part}\""} AS \"#{(id_alias = "_brfk_#{v.first}__#{id_part}")}\""
453
+ end
443
454
  id_alias
444
455
  end
445
456
  end
@@ -646,7 +657,7 @@ Module.class_exec do
646
657
  full_class_name << "::#{self.name}" unless self == Object
647
658
  full_class_name << "::#{plural_class_name.underscore.singularize.camelize}"
648
659
  if (plural_class_name == 'BrickSwagger' ||
649
- (::Brick.config.add_orphans && plural_class_name == 'BrickGem') ||
660
+ ((::Brick.config.add_status || ::Brick.config.add_orphans) && plural_class_name == 'BrickGem') ||
650
661
  model = self.const_get(full_class_name))
651
662
  # if it's a controller and no match or a model doesn't really use the same table name, eager load all models and try to find a model class of the right name.
652
663
  Object.send(:build_controller, self, class_name, plural_class_name, model, relations)
@@ -667,45 +678,11 @@ Module.class_exec do
667
678
  elsif ::Brick.enable_models?
668
679
  # Custom inheritable Brick base model?
669
680
  class_name = (inheritable_name = class_name)[5..-1] if class_name.start_with?('Brick')
670
- # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
671
- # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
672
-
673
- if (base_model = ::Brick.config.sti_namespace_prefixes&.fetch("::#{base_module.name}::", nil)&.constantize) || # Are we part of an auto-STI namespace? ...
674
- base_module != Object # ... or otherwise already in some namespace?
675
- schema_name = [(singular_schema_name = name.underscore),
676
- (schema_name = singular_schema_name.pluralize),
677
- name,
678
- name.pluralize].find { |s| Brick.db_schemas.include?(s) }
679
- end
680
- plural_class_name = ActiveSupport::Inflector.pluralize(model_name = class_name)
681
- # If it's namespaced then we turn the first part into what would be a schema name
682
- singular_table_name = ActiveSupport::Inflector.underscore(model_name).gsub('/', '.')
683
-
684
- if base_model
685
- schema_name = name.underscore # For the auto-STI namespace models
686
- table_name = base_model.table_name
687
- Object.send(:build_model, base_module, inheritable_name, model_name, singular_table_name, table_name, relations, table_name)
688
- else
689
- # Adjust for STI if we know of a base model for the requested model name
690
- # %%% Does not yet work with namespaced model names. Perhaps prefix with plural_class_name when doing the lookups here.
691
- table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil) || ::Brick.existing_stis[model_name]&.constantize)
692
- base_model.table_name
693
- else
694
- ActiveSupport::Inflector.pluralize(singular_table_name)
695
- end
696
- if ::Brick.apartment_multitenant &&
697
- Apartment.excluded_models.include?(table_name.singularize.camelize)
698
- schema_name = Apartment.default_schema
699
- end
700
- # Maybe, just maybe there's a database table that will satisfy this need
701
- if (matching = [table_name, singular_table_name, plural_class_name, model_name].find { |m| relations.key?(schema_name ? "#{schema_name}.#{m}" : m) })
702
- Object.send(:build_model, schema_name, inheritable_name, model_name, singular_table_name, table_name, relations, matching)
703
- end
704
- end
681
+ Object.send(:build_model, relations, base_module, name, class_name, inheritable_name)
705
682
  end
706
683
  if result
707
684
  built_class, code = result
708
- puts "\n#{code}"
685
+ puts "\n#{code}\n"
709
686
  built_class
710
687
  elsif ::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}") && !schema_name
711
688
  # module_prefixes = type_name.split('::')
@@ -727,7 +704,42 @@ class Object
727
704
 
728
705
  private
729
706
 
730
- def build_model(schema_name, inheritable_name, model_name, singular_table_name, table_name, relations, matching)
707
+ def build_model(relations, base_module, base_name, class_name, inheritable_name = nil)
708
+ if (base_model = ::Brick.config.sti_namespace_prefixes&.fetch("::#{base_module.name}::", nil)&.constantize) || # Are we part of an auto-STI namespace? ...
709
+ base_module != Object # ... or otherwise already in some namespace?
710
+ schema_name = [(singular_schema_name = base_name.underscore),
711
+ (schema_name = singular_schema_name.pluralize),
712
+ base_name,
713
+ base_name.pluralize].find { |s| Brick.db_schemas.include?(s) }
714
+ end
715
+ plural_class_name = ActiveSupport::Inflector.pluralize(model_name = class_name)
716
+ # If it's namespaced then we turn the first part into what would be a schema name
717
+ singular_table_name = ActiveSupport::Inflector.underscore(model_name).gsub('/', '.')
718
+
719
+ if base_model
720
+ schema_name = base_name.underscore # For the auto-STI namespace models
721
+ table_name = base_model.table_name
722
+ build_model_worker(base_module, inheritable_name, model_name, singular_table_name, table_name, relations, table_name)
723
+ else
724
+ # Adjust for STI if we know of a base model for the requested model name
725
+ # %%% Does not yet work with namespaced model names. Perhaps prefix with plural_class_name when doing the lookups here.
726
+ table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil) || ::Brick.existing_stis[model_name]&.constantize)
727
+ base_model.table_name
728
+ else
729
+ ActiveSupport::Inflector.pluralize(singular_table_name)
730
+ end
731
+ if ::Brick.apartment_multitenant &&
732
+ Apartment.excluded_models.include?(table_name.singularize.camelize)
733
+ schema_name = Apartment.default_schema
734
+ end
735
+ # Maybe, just maybe there's a database table that will satisfy this need
736
+ if (matching = [table_name, singular_table_name, plural_class_name, model_name].find { |m| relations.key?(schema_name ? "#{schema_name}.#{m}" : m) })
737
+ build_model_worker(schema_name, inheritable_name, model_name, singular_table_name, table_name, relations, matching)
738
+ end
739
+ end
740
+ end
741
+
742
+ def build_model_worker(schema_name, inheritable_name, model_name, singular_table_name, table_name, relations, matching)
731
743
  if ::Brick.apartment_multitenant &&
732
744
  schema_name == Apartment.default_schema
733
745
  relation = relations["#{schema_name}.#{matching}"]
@@ -893,7 +905,7 @@ class Object
893
905
  end
894
906
  end
895
907
  end
896
- code << "end # model #{full_name}\n\n"
908
+ code << "end # model #{full_name}\n"
897
909
  [built_model, code]
898
910
  end
899
911
 
@@ -989,6 +1001,17 @@ class Object
989
1001
  self.send(macro, assoc_name, **options)
990
1002
  end
991
1003
 
1004
+ def default_ordering(table_name, pk)
1005
+ case (order_tbl = ::Brick.config.order[table_name]) && (order_default = order_tbl[:_brick_default])
1006
+ when Array
1007
+ order_default.map { |od_part| order_tbl[od_part] || od_part }
1008
+ when Symbol
1009
+ order_tbl[order_default] || order_default
1010
+ else
1011
+ pk.map(&:to_sym) # If it's not a custom ORDER BY, just use the key
1012
+ end
1013
+ end
1014
+
992
1015
  def build_controller(namespace, class_name, plural_class_name, model, relations)
993
1016
  table_name = ActiveSupport::Inflector.underscore(plural_class_name)
994
1017
  singular_table_name = ActiveSupport::Inflector.singularize(table_name)
@@ -1002,6 +1025,9 @@ class Object
1002
1025
  # Brick-specific pages
1003
1026
  case plural_class_name
1004
1027
  when 'BrickGem'
1028
+ self.define_method :status do
1029
+ instance_variable_set(:@resources, ::Brick.get_status_of_resources)
1030
+ end
1005
1031
  self.define_method :orphans do
1006
1032
  instance_variable_set(:@orphans, ::Brick.find_orphans(::Brick.set_db_schema(params)))
1007
1033
  end
@@ -1073,27 +1099,9 @@ class Object
1073
1099
  # Normal (non-swagger) request
1074
1100
 
1075
1101
  # %%% Allow params to define which columns to use for order_by
1076
- ordering = if (order_tbl = ::Brick.config.order[table_name])
1077
- case (order_default = order_tbl[:_brick_default])
1078
- when Array
1079
- order_default.map { |od_part| order_tbl[od_part] || od_part }
1080
- when Symbol
1081
- order_tbl[order_default] || order_default
1082
- else
1083
- pk
1084
- end
1085
- else
1086
- pk # If it's not a custom ORDER BY, just use the key
1087
- end
1088
- order_by, order_by_txt = model._brick_calculate_ordering(ordering)
1089
- if (order_params = params['_brick_order']&.split(',')&.map(&:to_sym)) # Overriding the default by providing a querystring param?
1090
- order_by, _ = model._brick_calculate_ordering(order_params, true) # Don't do the txt part
1091
- end
1092
-
1093
- code << " def index\n"
1094
- code << " @#{table_name} = #{model.name}#{pk&.present? ? ".order(#{order_by_txt.join(', ')})" : '.all'}\n"
1095
- code << " @#{table_name}.brick_select(params)\n"
1096
- code << " end\n"
1102
+ # Overriding the default by providing a querystring param?
1103
+ ordering = params['_brick_order']&.split(',')&.map(&:to_sym) || Object.send(:default_ordering, table_name, pk)
1104
+ order_by, _ = model._brick_calculate_ordering(ordering, true) # Don't do the txt part
1097
1105
 
1098
1106
  ::Brick.set_db_schema(params)
1099
1107
  if request.format == :csv # Asking for a template?
@@ -1126,6 +1134,12 @@ class Object
1126
1134
  @_brick_join_array = join_array
1127
1135
  end
1128
1136
 
1137
+ _, order_by_txt = model._brick_calculate_ordering(default_ordering(table_name, pk))
1138
+ code << " def index\n"
1139
+ code << " @#{table_name} = #{model.name}#{pk&.present? ? ".order(#{order_by_txt.join(', ')})" : '.all'}\n"
1140
+ code << " @#{table_name}.brick_select(params)\n"
1141
+ code << " end\n"
1142
+
1129
1143
  is_pk_string = nil
1130
1144
  if (pk_col = model&.primary_key)
1131
1145
  code << " def show\n"
@@ -1255,7 +1269,7 @@ class Object
1255
1269
  # Get column names for params from relations[model.table_name][:cols].keys
1256
1270
  end
1257
1271
  # end
1258
- code << "end # #{namespace_name}#{class_name}\n\n"
1272
+ code << "end # #{namespace_name}#{class_name}\n"
1259
1273
  end # class definition
1260
1274
  [built_controller, code]
1261
1275
  end
@@ -1312,7 +1326,7 @@ module ActiveRecord::ConnectionHandling
1312
1326
  load apartment_initializer
1313
1327
  apartment_excluded = Apartment.excluded_models
1314
1328
  end
1315
- # Only for Postgres? (Doesn't work in sqlite3)
1329
+ # Only for Postgres (Doesn't work in sqlite3 or MySQL)
1316
1330
  # puts ActiveRecord::Base.execute_sql("SELECT current_setting('SEARCH_PATH')").to_a.inspect
1317
1331
 
1318
1332
  is_postgres = nil
@@ -1347,7 +1361,7 @@ module ActiveRecord::ConnectionHandling
1347
1361
  puts "Unfamiliar with connection adapter #{ActiveRecord::Base.connection.adapter_name}"
1348
1362
  end
1349
1363
 
1350
- ::Brick.db_schemas ||= []
1364
+ ::Brick.db_schemas ||= {}
1351
1365
 
1352
1366
  if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1353
1367
  if (possible_schema = ::Brick.config.schema_behavior&.[](:multitenant)&.[](:schema_to_analyse))
@@ -1363,41 +1377,11 @@ module ActiveRecord::ConnectionHandling
1363
1377
  # %%% Retrieve internal ActiveRecord table names like this:
1364
1378
  # ActiveRecord::Base.internal_metadata_table_name, ActiveRecord::Base.schema_migrations_table_name
1365
1379
  # For if it's not SQLite -- so this is the Postgres and MySQL version
1366
- sql ||= "SELECT t.table_schema AS schema, t.table_name AS relation_name, t.table_type,#{"
1367
- pg_catalog.obj_description((t.table_schema || '.' || t.table_name)::regclass, 'pg_class') AS table_description," if is_postgres}
1368
- c.column_name, c.data_type,
1369
- COALESCE(c.character_maximum_length, c.numeric_precision) AS max_length,
1370
- tc.constraint_type AS const, kcu.constraint_name AS \"key\",
1371
- c.is_nullable
1372
- FROM INFORMATION_SCHEMA.tables AS t
1373
- LEFT OUTER JOIN INFORMATION_SCHEMA.columns AS c ON t.table_schema = c.table_schema
1374
- AND t.table_name = c.table_name
1375
- LEFT OUTER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu ON
1376
- -- ON kcu.CONSTRAINT_CATALOG = t.table_catalog AND
1377
- kcu.CONSTRAINT_SCHEMA = c.table_schema
1378
- AND kcu.TABLE_NAME = c.table_name
1379
- AND kcu.position_in_unique_constraint IS NULL
1380
- AND kcu.ordinal_position = c.ordinal_position
1381
- LEFT OUTER JOIN INFORMATION_SCHEMA.table_constraints AS tc
1382
- ON kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
1383
- AND kcu.TABLE_NAME = tc.TABLE_NAME
1384
- AND kcu.CONSTRAINT_NAME = tc.constraint_name
1385
- WHERE t.table_schema NOT IN ('information_schema', 'pg_catalog')#{"
1386
- AND t.table_schema = COALESCE(current_setting('SEARCH_PATH'), 'public')" if schema }
1387
- -- AND t.table_type IN ('VIEW') -- 'BASE TABLE', 'FOREIGN TABLE'
1388
- AND t.table_name NOT IN ('pg_stat_statements', ?, ?)
1389
- ORDER BY 1, t.table_type DESC, c.ordinal_position"
1390
1380
  measures = []
1391
1381
  case ActiveRecord::Base.connection.adapter_name
1392
1382
  when 'PostgreSQL', 'SQLite' # These bring back a hash for each row because the query uses column aliases
1393
1383
  # schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1394
- ar_smtn = if ActiveRecord::Base.respond_to?(:schema_migrations_table_name)
1395
- ActiveRecord::Base.schema_migrations_table_name
1396
- else
1397
- 'schema_migrations'
1398
- end
1399
- ar_imtn = ActiveRecord.version >= ::Gem::Version.new('5.0') ? ActiveRecord::Base.internal_metadata_table_name : ''
1400
- ActiveRecord::Base.execute_sql(sql, ar_smtn, ar_imtn).each do |r|
1384
+ ActiveRecord::Base.retrieve_schema_and_tables(sql, is_postgres, schema).each do |r|
1401
1385
  # If Apartment gem lists the table as being associated with a non-tenanted model then use whatever it thinks
1402
1386
  # is the default schema, usually 'public'.
1403
1387
  schema_name = if ::Brick.config.schema_behavior[:multitenant]
@@ -1424,23 +1408,23 @@ module ActiveRecord::ConnectionHandling
1424
1408
  # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
1425
1409
  end
1426
1410
  else # MySQL2 acts a little differently, bringing back an array for each row
1427
- ActiveRecord::Base.execute_sql(sql).each do |r|
1428
- relation = relations[(relation_name = r[0])] # here relation represents a table or view from the database
1429
- relation[:isView] = true if r[1] == 'VIEW' # table_type
1430
- col_name = r[2]
1431
- key = case r[5] # constraint type
1411
+ ActiveRecord::Base.retrieve_schema_and_tables(sql).each do |r|
1412
+ relation = relations[(relation_name = r[1])] # here relation represents a table or view from the database
1413
+ relation[:isView] = true if r[2] == 'VIEW' # table_type
1414
+ col_name = r[3]
1415
+ key = case r[6] # constraint type
1432
1416
  when 'PRIMARY KEY'
1433
1417
  # key
1434
- relation[:pkey][r[6] || relation_name] ||= []
1418
+ relation[:pkey][r[7] || relation_name] ||= []
1435
1419
  when 'UNIQUE'
1436
- relation[:ukeys][r[6] || "#{relation_name}.#{col_name}"] ||= []
1420
+ relation[:ukeys][r[7] || "#{relation_name}.#{col_name}"] ||= []
1437
1421
  # key = (relation[:ukeys] = Hash.new { |h, k| h[k] = [] }) if key.is_a?(Array)
1438
1422
  # key[r['key']]
1439
1423
  end
1440
1424
  key << col_name if key
1441
1425
  cols = relation[:cols] # relation.fetch(:cols) { relation[:cols] = [] }
1442
1426
  # 'data_type', 'max_length'
1443
- cols[col_name] = [r[3], r[4], measures&.include?(col_name)]
1427
+ cols[col_name] = [r[4], r[5], measures&.include?(col_name)]
1444
1428
  # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
1445
1429
  end
1446
1430
  end
@@ -1476,9 +1460,11 @@ module ActiveRecord::ConnectionHandling
1476
1460
  INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu2
1477
1461
  ON kcu2.CONSTRAINT_CATALOG = rc.UNIQUE_CONSTRAINT_CATALOG
1478
1462
  AND kcu2.CONSTRAINT_SCHEMA = rc.UNIQUE_CONSTRAINT_SCHEMA
1479
- AND kcu2.CONSTRAINT_NAME = rc.UNIQUE_CONSTRAINT_NAME
1463
+ AND kcu2.CONSTRAINT_NAME = rc.UNIQUE_CONSTRAINT_NAME#{"
1464
+ AND kcu2.TABLE_NAME = kcu1.REFERENCED_TABLE_NAME
1465
+ AND kcu2.COLUMN_NAME = kcu1.REFERENCED_COLUMN_NAME" unless is_postgres }
1480
1466
  AND kcu2.ORDINAL_POSITION = kcu1.ORDINAL_POSITION#{"
1481
- WHERE kcu1.CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if schema }"
1467
+ WHERE kcu1.CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if is_postgres && schema }"
1482
1468
  # AND kcu2.TABLE_NAME = ?;", Apartment::Tenant.current, table_name
1483
1469
  when 'SQLite'
1484
1470
  sql = "SELECT m.name, fkl.\"from\", fkl.\"table\", m.name || '_' || fkl.\"from\" AS constraint_name
@@ -1494,12 +1480,14 @@ module ActiveRecord::ConnectionHandling
1494
1480
  # Multitenancy makes things a little more general overall, except for non-tenanted tables
1495
1481
  if apartment_excluded&.include?(fk[1].singularize.camelize)
1496
1482
  fk[0] = Apartment.default_schema
1497
- elsif fk[0] == 'public' || (is_multitenant && fk[0] == schema)
1483
+ elsif is_postgres && (fk[0] == 'public' || (is_multitenant && fk[0] == schema)) ||
1484
+ !is_postgres && ['mysql', 'performance_schema', 'sys'].exclude?(fk[0])
1498
1485
  fk[0] = nil
1499
1486
  end
1500
1487
  if apartment_excluded&.include?(fk[4].singularize.camelize)
1501
1488
  fk[3] = Apartment.default_schema
1502
- elsif fk[3] == 'public' || (is_multitenant && fk[3] == schema)
1489
+ elsif is_postgres && (fk[3] == 'public' || (is_multitenant && fk[3] == schema)) ||
1490
+ !is_postgres && ['mysql', 'performance_schema', 'sys'].exclude?(fk[3])
1503
1491
  fk[3] = nil
1504
1492
  end
1505
1493
  ::Brick._add_bt_and_hm(fk, relations)
@@ -1533,6 +1521,41 @@ module ActiveRecord::ConnectionHandling
1533
1521
 
1534
1522
  ::Brick.load_additional_references if initializer_loaded
1535
1523
  end
1524
+
1525
+ def retrieve_schema_and_tables(sql = nil, is_postgres = nil, schema = nil)
1526
+ sql ||= "SELECT t.table_schema AS \"schema\", t.table_name AS relation_name, t.table_type,#{"
1527
+ pg_catalog.obj_description((t.table_schema || '.' || t.table_name)::regclass, 'pg_class') AS table_description," if is_postgres}
1528
+ c.column_name, c.data_type,
1529
+ COALESCE(c.character_maximum_length, c.numeric_precision) AS max_length,
1530
+ tc.constraint_type AS const, kcu.constraint_name AS \"key\",
1531
+ c.is_nullable
1532
+ FROM INFORMATION_SCHEMA.tables AS t
1533
+ LEFT OUTER JOIN INFORMATION_SCHEMA.columns AS c ON t.table_schema = c.table_schema
1534
+ AND t.table_name = c.table_name
1535
+ LEFT OUTER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu ON
1536
+ -- ON kcu.CONSTRAINT_CATALOG = t.table_catalog AND
1537
+ kcu.CONSTRAINT_SCHEMA = c.table_schema
1538
+ AND kcu.TABLE_NAME = c.table_name
1539
+ AND kcu.position_in_unique_constraint IS NULL
1540
+ AND kcu.ordinal_position = c.ordinal_position
1541
+ LEFT OUTER JOIN INFORMATION_SCHEMA.table_constraints AS tc
1542
+ ON kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
1543
+ AND kcu.TABLE_NAME = tc.TABLE_NAME
1544
+ AND kcu.CONSTRAINT_NAME = tc.constraint_name
1545
+ WHERE t.table_schema #{
1546
+ is_postgres ? "NOT IN ('information_schema', 'pg_catalog')" : "= '#{ActiveRecord::Base.connection.current_database.tr("'", "''")}'"}#{"
1547
+ AND t.table_schema = COALESCE(current_setting('SEARCH_PATH'), 'public')" if is_postgres && schema }
1548
+ -- AND t.table_type IN ('VIEW') -- 'BASE TABLE', 'FOREIGN TABLE'
1549
+ AND t.table_name NOT IN ('pg_stat_statements', ?, ?)
1550
+ ORDER BY 1, t.table_type DESC, 2, c.ordinal_position"
1551
+ ar_smtn = if ActiveRecord::Base.respond_to?(:schema_migrations_table_name)
1552
+ ActiveRecord::Base.schema_migrations_table_name
1553
+ else
1554
+ 'schema_migrations'
1555
+ end
1556
+ ar_imtn = ActiveRecord.version >= ::Gem::Version.new('5.0') ? ActiveRecord::Base.internal_metadata_table_name : ''
1557
+ ActiveRecord::Base.execute_sql(sql, ar_smtn, ar_imtn)
1558
+ end
1536
1559
  end
1537
1560
 
1538
1561
  # ==========================================
@@ -1680,6 +1703,59 @@ module Brick
1680
1703
  assoc_bt[:inverse] = assoc_hm
1681
1704
  end
1682
1705
 
1706
+ # Identify built out routes, migrations, models,
1707
+ # (and also soon controllers and views!)
1708
+ # for each resource
1709
+ def get_status_of_resources
1710
+ rails_root = ::Rails.root.to_s
1711
+ migrations = if Dir.exist?(mig_path = ActiveRecord::Migrator.migrations_paths.first || "#{rails_root}/db/migrate")
1712
+ Dir["#{mig_path}/**/*.rb"].each_with_object(Hash.new { |h, k| h[k] = [] }) do |v, s|
1713
+ File.read(v).split("\n").each do |line|
1714
+ # For all non-commented lines, look for any that have "create_table", "alter_table", or "drop_table"
1715
+ if !line.lstrip.start_with?('#') &&
1716
+ (idx = (line.index('create_table ') || line.index('create_table('))&.+(13)) ||
1717
+ (idx = (line.index('alter_table ') || line.index('alter_table('))&.+(12)) ||
1718
+ (idx = (line.index('drop_table ') || line.index('drop_table('))&.+(11))
1719
+ tbl = line[idx..-1].match(/([:'"\w\.]+)/)&.captures&.first
1720
+ if tbl
1721
+ s[tbl.tr(':\'"', '').pluralize] << v
1722
+ break
1723
+ end
1724
+ end
1725
+ end
1726
+ end
1727
+ end
1728
+ if ::ActiveSupport.version < ::Gem::Version.new('6') ||
1729
+ ::Rails.configuration.instance_variable_get(:@autoloader) == :classic
1730
+ Rails.configuration.eager_load_namespaces.select { |ns| ns < Rails::Application }.each(&:eager_load!)
1731
+ else
1732
+ Zeitwerk::Loader.eager_load_all
1733
+ end
1734
+ abstract_activerecord_bases = ActiveRecord::Base.descendants.select { |ar| ar.abstract_class? }.map(&:name)
1735
+ # abstract_activerecord_bases << ActiveRecord::Base
1736
+ models = if Dir.exist?(model_path = "#{rails_root}/app/models")
1737
+ Dir["#{model_path}/**/*.rb"].each_with_object({}) do |v, s|
1738
+ File.read(v).split("\n").each do |line|
1739
+ # For all non-commented lines, look for any that start with "class " and also "< ApplicationRecord"
1740
+ if line.lstrip.start_with?('class') && (idx = line.index('class'))
1741
+ model = line[idx + 5..-1].match(/[\s:]+([\w:]+)/)&.captures&.first
1742
+ if model && abstract_activerecord_bases.exclude?(model)
1743
+ klass = begin
1744
+ model.constantize
1745
+ rescue
1746
+ end
1747
+ s[model.underscore.tr('/', '.').pluralize] = [
1748
+ v.start_with?(rails_root) ? v[rails_root.length + 1..-1] : v,
1749
+ klass
1750
+ ]
1751
+ end
1752
+ end
1753
+ end
1754
+ end
1755
+ end
1756
+ ::Brick.relations.keys.map { |v| [(r = v.pluralize), (model = models[r])&.last&.table_name || v, migrations&.fetch(r, nil), model&.first] }
1757
+ end
1758
+
1683
1759
  # Locate orphaned records
1684
1760
  def find_orphans(multi_schema)
1685
1761
  is_default_schema = multi_schema&.==(Apartment.default_schema)
@@ -56,6 +56,7 @@ module Brick
56
56
  # Used by Rails 5.0 and above
57
57
  alias :_brick_template_exists? :template_exists?
58
58
  def template_exists?(*args, **options)
59
+ (::Brick.config.add_status && args.first == 'status') ||
59
60
  (::Brick.config.add_orphans && args.first == 'orphans') ||
60
61
  _brick_template_exists?(*args, **options) ||
61
62
  # Do not auto-create a template when it's searching for an application.html.erb, which comes in like: ["edit", ["games", "application"]]
@@ -96,11 +97,12 @@ module Brick
96
97
  @_brick_model ||
97
98
  (ActionView.version < ::Gem::Version.new('5.0') && args[1].is_a?(Array) ? set_brick_model(args) : nil)
98
99
  )&.name) ||
100
+ (is_status = ::Brick.config.add_status && args[0..1] == ['status', ['brick_gem']]) ||
99
101
  (is_orphans = ::Brick.config.add_orphans && args[0..1] == ['orphans', ['brick_gem']])
100
102
  return _brick_find_template(*args, **options)
101
103
  end
102
104
 
103
- unless is_orphans
105
+ unless is_status || is_orphans
104
106
  pk = @_brick_model._brick_primary_key(::Brick.relations.fetch(model_name, nil))
105
107
  obj_name = model_name.split('::').last.underscore
106
108
  path_obj_name = model_name.underscore.tr('/', '_')
@@ -148,7 +150,6 @@ module Brick
148
150
  ", nil, #{path_keys(hm_assoc, hm_fk_name, obj_name, pk)}"
149
151
  end
150
152
  hm_entry << ']'
151
- puts hm_entry
152
153
  hms_columns << hm_entry
153
154
  when 'show', 'update'
154
155
  hm_stuff << if hm_fk_name
@@ -613,8 +614,9 @@ if (headerTop) {
613
614
  });
614
615
  });
615
616
  </script>
616
- <% end %>
617
- <table id=\"headerTop\">
617
+ <% end
618
+
619
+ %><table id=\"headerTop\">
618
620
  <table id=\"#{table_name}\">
619
621
  <thead><tr>#{"<th x-order=\"#{pk.join(',')}\"></th>" if pk.present?}<%=
620
622
  # Consider getting the name from the association -- hm.first.name -- if a more \"friendly\" alias should be used for a screwy table name
@@ -708,6 +710,53 @@ if (headerTop) {
708
710
 
709
711
  #{"<hr><%= link_to \"New #{obj_name}\", new_#{path_obj_name}_path %>" unless @_brick_model.is_view?}
710
712
  #{script}"
713
+
714
+ when 'status'
715
+ # Status page - list of all resources and 5 things they do or don't have present, and what is turned on and off
716
+ # Must load all models, and then find what table names are represented
717
+ # Easily could be multiple files involved (STI for instance)
718
+ +"#{css}
719
+ <p style=\"color: green\"><%= notice %></p>#{"
720
+ <select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
721
+ <select id=\"tbl\">#{table_options}</select>
722
+ <h1>Status</h1>
723
+ <table id=\"status\"><thead><tr>
724
+ <th>Resource</th>
725
+ <th>Table</th>
726
+ <th>Migration</th>
727
+ <th>Model</th>
728
+ <th>Route</th>
729
+ <th>Controller</th>
730
+ <th>Views</th>
731
+ </tr></thead>
732
+ <tbody>
733
+ <% # (listing in schema.rb)
734
+ # Solid colour if file or route entry is present
735
+ @resources.each do |r|
736
+ %>
737
+ <tr>
738
+ <td><%= link_to(r[0], \"/#\{r[0].tr('.', '/')}\") %></td>
739
+ <td<%= if r[1]
740
+ ' class=\"orphan\"' unless ::Brick.relations.key?(r[1])
741
+ else
742
+ ' class=\"dimmed\"'
743
+ end&.html_safe %>><%= # Table
744
+ r[1] %></td>
745
+ <td<%= ' class=\"dimmed\"'.html_safe unless r[2] %>><%= # Migration
746
+ r[2]&.join('<br>')&.html_safe %></td>
747
+ <td<%= ' class=\"dimmed\"'.html_safe unless r[3] %>><%= # Model
748
+ r[3] %></td>
749
+ <td<%= ' class=\"dimmed\"'.html_safe unless r[4] %>><%= # Route
750
+ %></td>
751
+ <td<%= ' class=\"dimmed\"'.html_safe unless r[5] %>><%= # Controller
752
+ %></td>
753
+ <td<%= ' class=\"dimmed\"'.html_safe unless r[6] %>><%= # Views
754
+ %></td>
755
+ <tr>
756
+ <% end %>
757
+ </tbody><table>
758
+ #{script}"
759
+
711
760
  when 'orphans'
712
761
  if is_orphans
713
762
  +"#{css}
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 58
8
+ TINY = 61
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
@@ -484,6 +484,9 @@ In config/initializers/brick.rb appropriate entries would look something like:
484
484
  send(:resources, controller_name.to_sym, **options)
485
485
  end
486
486
  end
487
+ if ::Brick.config.add_status && instance_variable_get(:@set).named_routes.names.exclude?(:brick_status)
488
+ get('/brick_status', to: 'brick_gem#status', as: 'brick_status')
489
+ end
487
490
  if ::Brick.config.add_orphans && instance_variable_get(:@set).named_routes.names.exclude?(:brick_orphans)
488
491
  get('/brick_orphans', to: 'brick_gem#orphans', as: 'brick_orphans')
489
492
  end
@@ -85,10 +85,9 @@ module Brick
85
85
  # Generate a list of tables that can be chosen
86
86
  chosen = gets_list(list: tables, chosen: tables.dup)
87
87
  schemas = chosen.each_with_object({}) do |v, s|
88
- schema = if v.split('.').length > 1
89
- v.first
90
- end
91
- s[schema] = nil if schema && [::Brick.default_schema, 'public'].exclude?(schema)
88
+ if (v_parts = v.split('.')).length > 1
89
+ s[v_parts.first] = nil unless [::Brick.default_schema, 'public'].include?(v_parts.first)
90
+ end
92
91
  end
93
92
  # Start the timestamps back the same number of minutes from now as expected number of migrations to create
94
93
  current_mig_time = Time.now - (schemas.length + chosen.length).minutes
@@ -130,7 +129,7 @@ module Brick
130
129
  tbl_parts.first
131
130
  end
132
131
  end
133
- unless built_schemas.key?(schema)
132
+ unless schema.blank? || built_schemas.key?(schema)
134
133
  mig = +" def change\n create_schema(:#{schema}) unless schema_exists?(:#{schema})\n end\n"
135
134
  migration_file_write(mig_path, "create_db_schema_#{schema}", current_mig_time += 1.minute, ar_version, mig)
136
135
  built_schemas[schema] = nil
@@ -6,63 +6,72 @@ require 'fancy_gets'
6
6
 
7
7
  module Brick
8
8
  # Auto-generates models, controllers, or views
9
- class ModelGenerator < ::Rails::Generators::Base
9
+ class ModelsGenerator < ::Rails::Generators::Base
10
10
  include FancyGets
11
11
  # include ::Rails::Generators::Migration
12
12
 
13
- # # source_root File.expand_path('templates', __dir__)
14
- # class_option(
15
- # :with_changes,
16
- # type: :boolean,
17
- # default: false,
18
- # desc: 'Add IMPORT_TEMPLATE to model'
19
- # )
20
-
21
13
  desc 'Auto-generates models, controllers, or views.'
22
14
 
23
- def brick_model
24
- # %%% If Apartment is active, ask which schema they want
15
+ def brick_models
16
+ # %%% If Apartment is active and there's no schema_to_analyse, ask which schema they want
25
17
 
26
18
  # Load all models
27
- Rails.configuration.eager_load_namespaces.select { |ns| ns < Rails::Application }.each(&:eager_load!)
19
+ if ::ActiveSupport.version < ::Gem::Version.new('6') ||
20
+ ::Rails.configuration.instance_variable_get(:@autoloader) == :classic
21
+ Rails.configuration.eager_load_namespaces.select { |ns| ns < Rails::Application }.each(&:eager_load!)
22
+ else
23
+ Zeitwerk::Loader.eager_load_all
24
+ end
28
25
 
29
26
  # Generate a list of viable models that can be chosen
30
27
  longest_length = 0
31
28
  model_info = Hash.new { |h, k| h[k] = {} }
32
29
  tableless = Hash.new { |h, k| h[k] = [] }
33
- models = ActiveRecord::Base.descendants.reject do |m|
34
- trouble = if m.abstract_class?
35
- true
36
- elsif !m.table_exists?
37
- tableless[m.table_name] << m.name
38
- ' (No Table)'
39
- else
40
- this_f_keys = (model_info[m][:f_keys] = m.reflect_on_all_associations.select { |a| a.macro == :belongs_to }) || []
41
- column_names = (model_info[m][:column_names] = m.columns.map(&:name) - [m.primary_key, 'created_at', 'updated_at', 'deleted_at'] - this_f_keys.map(&:foreign_key))
42
- if column_names.empty? && this_f_keys && !this_f_keys.empty?
43
- fk_message = ", although #{this_f_keys.length} foreign keys"
44
- " (No columns#{fk_message})"
45
- end
46
- end
47
- # puts "#{m.name}#{trouble}" if trouble&.is_a?(String)
48
- trouble
30
+ existing_models = ActiveRecord::Base.descendants.reject do |m|
31
+ m.abstract_class? || !m.table_exists? || ::Brick.relations.key?(m.table_name)
49
32
  end
33
+ models = ::Brick.relations.keys.map do |tbl|
34
+ tbl_parts = tbl.split('.')
35
+ tbl_parts.shift if [::Brick.default_schema, 'public'].include?(tbl_parts.first)
36
+ tbl_parts[-1] = tbl_parts[-1].singularize
37
+ tbl_parts.join('/').camelize
38
+ end - existing_models.map(&:name)
50
39
  models.sort! do |a, b| # Sort first to separate namespaced stuff from the rest, then alphabetically
51
- is_a_namespaced = a.name.include?('::')
52
- is_b_namespaced = b.name.include?('::')
40
+ is_a_namespaced = a.include?('::')
41
+ is_b_namespaced = b.include?('::')
53
42
  if is_a_namespaced && !is_b_namespaced
54
43
  1
55
44
  elsif !is_a_namespaced && is_b_namespaced
56
45
  -1
57
46
  else
58
- a.name <=> b.name
47
+ a <=> b
59
48
  end
60
49
  end
61
50
  models.each do |m| # Find longest name in the list for future use to show lists on the right side of the screen
62
- if longest_length < (len = m.name.length)
51
+ if longest_length < (len = m.length)
63
52
  longest_length = len
64
53
  end
65
54
  end
55
+ chosen = gets_list(list: models, chosen: models.dup)
56
+ relations = ::Brick.relations
57
+ chosen.each do |model_name|
58
+ # %%% If we're in a schema then make sure the module file exists
59
+ base_module = if (model_parts = model_name.split('::')).length > 1
60
+ "::#{model_parts.first}".constantize
61
+ else
62
+ Object
63
+ end
64
+ _built_model, code = Object.send(:build_model, relations, base_module, base_module.name, model_parts.last)
65
+ path = ['models']
66
+ path.concat(model_parts.map(&:underscore))
67
+ dir = +"#{::Rails.root}/app"
68
+ path[0..-2].each do |path_part|
69
+ dir << "/#{path_part}"
70
+ Dir.mkdir(dir) unless Dir.exists?(dir)
71
+ end
72
+ File.open("#{dir}/#{path.last}.rb", 'w') { |f| f.write code } unless code.blank?
73
+ end
74
+ puts "\n*** Created #{chosen.length} model files under app/models ***"
66
75
  end
67
76
 
68
77
  private
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.58
4
+ version: 1.0.61
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-08-12 00:00:00.000000000 Z
11
+ date: 2022-08-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -239,7 +239,7 @@ files:
239
239
  - lib/generators/brick/USAGE
240
240
  - lib/generators/brick/install_generator.rb
241
241
  - lib/generators/brick/migrations_generator.rb
242
- - lib/generators/brick/model_generator.rb
242
+ - lib/generators/brick/models_generator.rb
243
243
  - lib/generators/brick/templates/add_object_changes_to_versions.rb.erb
244
244
  - lib/generators/brick/templates/create_versions.rb.erb
245
245
  homepage: https://github.com/lorint/brick