brick 1.0.57 → 1.0.60

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: 930370b67a72b31db99cc8d50afa80d4c3be0d07e727155eb20ac5a7628bb1bb
4
- data.tar.gz: f2d7964c79172a438800b3c69c63e62b26a4984357ca5d6c7ff9e2ef72e5308f
3
+ metadata.gz: 741384ed503092151ed20f360abd8819fdf3b25fc4042e5aa953dae928acab6d
4
+ data.tar.gz: 11e88b4b7bdc51b9237e095fdaae1297741e1c315737e18d7194c3c84e937797
5
5
  SHA512:
6
- metadata.gz: 03e7ba018c426517060aa6312d14a9f1e0877d82b376782ea46b4b1506433bbe3e9e4d2c3d88aaf3d5fb356c68a129fd975b1718d7b2ac826b241efab3f2cee6
7
- data.tar.gz: 645c6b750dd870180e312ffddff1d199bb84e92cdde0309f253ec8c927f3ba5304b84ae95b066c260d0523048c607b28148d1b577a479ec661b94de05248813a
6
+ metadata.gz: daa2950ea3ff1354c642949797a4ed7e7ed5c14cfcba4e5560572e997e4df43b537ecb12e84dc2d710b3e067870a91c69519ee41735ea7e9ada0d110ae2bd906
7
+ data.tar.gz: 3e74b2d21d72d84ea7286a7b8959b0abb293c50844b5e3ba4fce73cc4f8b5dd68fa7a50dc9753901f0b82b6a3196ab3fd282c4e709d0ae82af37e5a4e68488a9
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
@@ -1478,7 +1462,7 @@ module ActiveRecord::ConnectionHandling
1478
1462
  AND kcu2.CONSTRAINT_SCHEMA = rc.UNIQUE_CONSTRAINT_SCHEMA
1479
1463
  AND kcu2.CONSTRAINT_NAME = rc.UNIQUE_CONSTRAINT_NAME
1480
1464
  AND kcu2.ORDINAL_POSITION = kcu1.ORDINAL_POSITION#{"
1481
- WHERE kcu1.CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if schema }"
1465
+ WHERE kcu1.CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if is_postgres && schema }"
1482
1466
  # AND kcu2.TABLE_NAME = ?;", Apartment::Tenant.current, table_name
1483
1467
  when 'SQLite'
1484
1468
  sql = "SELECT m.name, fkl.\"from\", fkl.\"table\", m.name || '_' || fkl.\"from\" AS constraint_name
@@ -1494,12 +1478,14 @@ module ActiveRecord::ConnectionHandling
1494
1478
  # Multitenancy makes things a little more general overall, except for non-tenanted tables
1495
1479
  if apartment_excluded&.include?(fk[1].singularize.camelize)
1496
1480
  fk[0] = Apartment.default_schema
1497
- elsif fk[0] == 'public' || (is_multitenant && fk[0] == schema)
1481
+ elsif is_postgres && (fk[0] == 'public' || (is_multitenant && fk[0] == schema)) ||
1482
+ !is_postgres && ['mysql', 'performance_schema', 'sys'].exclude?(fk[0])
1498
1483
  fk[0] = nil
1499
1484
  end
1500
1485
  if apartment_excluded&.include?(fk[4].singularize.camelize)
1501
1486
  fk[3] = Apartment.default_schema
1502
- elsif fk[3] == 'public' || (is_multitenant && fk[3] == schema)
1487
+ elsif is_postgres && (fk[3] == 'public' || (is_multitenant && fk[3] == schema)) ||
1488
+ !is_postgres && ['mysql', 'performance_schema', 'sys'].exclude?(fk[3])
1503
1489
  fk[3] = nil
1504
1490
  end
1505
1491
  ::Brick._add_bt_and_hm(fk, relations)
@@ -1533,6 +1519,41 @@ module ActiveRecord::ConnectionHandling
1533
1519
 
1534
1520
  ::Brick.load_additional_references if initializer_loaded
1535
1521
  end
1522
+
1523
+ def retrieve_schema_and_tables(sql = nil, is_postgres = nil, schema = nil)
1524
+ sql ||= "SELECT t.table_schema AS \"schema\", t.table_name AS relation_name, t.table_type,#{"
1525
+ pg_catalog.obj_description((t.table_schema || '.' || t.table_name)::regclass, 'pg_class') AS table_description," if is_postgres}
1526
+ c.column_name, c.data_type,
1527
+ COALESCE(c.character_maximum_length, c.numeric_precision) AS max_length,
1528
+ tc.constraint_type AS const, kcu.constraint_name AS \"key\",
1529
+ c.is_nullable
1530
+ FROM INFORMATION_SCHEMA.tables AS t
1531
+ LEFT OUTER JOIN INFORMATION_SCHEMA.columns AS c ON t.table_schema = c.table_schema
1532
+ AND t.table_name = c.table_name
1533
+ LEFT OUTER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu ON
1534
+ -- ON kcu.CONSTRAINT_CATALOG = t.table_catalog AND
1535
+ kcu.CONSTRAINT_SCHEMA = c.table_schema
1536
+ AND kcu.TABLE_NAME = c.table_name
1537
+ AND kcu.position_in_unique_constraint IS NULL
1538
+ AND kcu.ordinal_position = c.ordinal_position
1539
+ LEFT OUTER JOIN INFORMATION_SCHEMA.table_constraints AS tc
1540
+ ON kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
1541
+ AND kcu.TABLE_NAME = tc.TABLE_NAME
1542
+ AND kcu.CONSTRAINT_NAME = tc.constraint_name
1543
+ WHERE t.table_schema NOT IN ('information_schema', #{
1544
+ is_postgres ? "'pg_catalog'" : "'mysql', 'performance_schema', 'sys'"})#{"
1545
+ AND t.table_schema = COALESCE(current_setting('SEARCH_PATH'), 'public')" if is_postgres && schema }
1546
+ -- AND t.table_type IN ('VIEW') -- 'BASE TABLE', 'FOREIGN TABLE'
1547
+ AND t.table_name NOT IN ('pg_stat_statements', ?, ?)
1548
+ ORDER BY 1, t.table_type DESC, 2, c.ordinal_position"
1549
+ ar_smtn = if ActiveRecord::Base.respond_to?(:schema_migrations_table_name)
1550
+ ActiveRecord::Base.schema_migrations_table_name
1551
+ else
1552
+ 'schema_migrations'
1553
+ end
1554
+ ar_imtn = ActiveRecord.version >= ::Gem::Version.new('5.0') ? ActiveRecord::Base.internal_metadata_table_name : ''
1555
+ ActiveRecord::Base.execute_sql(sql, ar_smtn, ar_imtn)
1556
+ end
1536
1557
  end
1537
1558
 
1538
1559
  # ==========================================
@@ -1680,6 +1701,59 @@ module Brick
1680
1701
  assoc_bt[:inverse] = assoc_hm
1681
1702
  end
1682
1703
 
1704
+ # Identify built out routes, migrations, models,
1705
+ # (and also soon controllers and views!)
1706
+ # for each resource
1707
+ def get_status_of_resources
1708
+ rails_root = ::Rails.root.to_s
1709
+ migrations = if Dir.exist?(mig_path = ActiveRecord::Migrator.migrations_paths.first || "#{rails_root}/db/migrate")
1710
+ Dir["#{mig_path}/**/*.rb"].each_with_object(Hash.new { |h, k| h[k] = [] }) do |v, s|
1711
+ File.read(v).split("\n").each do |line|
1712
+ # For all non-commented lines, look for any that have "create_table", "alter_table", or "drop_table"
1713
+ if !line.lstrip.start_with?('#') &&
1714
+ (idx = (line.index('create_table ') || line.index('create_table('))&.+(13)) ||
1715
+ (idx = (line.index('alter_table ') || line.index('alter_table('))&.+(12)) ||
1716
+ (idx = (line.index('drop_table ') || line.index('drop_table('))&.+(11))
1717
+ tbl = line[idx..-1].match(/([:'"\w\.]+)/)&.captures&.first
1718
+ if tbl
1719
+ s[tbl.tr(':\'"', '').pluralize] << v
1720
+ break
1721
+ end
1722
+ end
1723
+ end
1724
+ end
1725
+ end
1726
+ if ::ActiveSupport.version < ::Gem::Version.new('6') ||
1727
+ ::Rails.configuration.instance_variable_get(:@autoloader) == :classic
1728
+ Rails.configuration.eager_load_namespaces.select { |ns| ns < Rails::Application }.each(&:eager_load!)
1729
+ else
1730
+ Zeitwerk::Loader.eager_load_all
1731
+ end
1732
+ abstract_activerecord_bases = ActiveRecord::Base.descendants.select { |ar| ar.abstract_class? }.map(&:name)
1733
+ # abstract_activerecord_bases << ActiveRecord::Base
1734
+ models = if Dir.exist?(model_path = "#{rails_root}/app/models")
1735
+ Dir["#{model_path}/**/*.rb"].each_with_object({}) do |v, s|
1736
+ File.read(v).split("\n").each do |line|
1737
+ # For all non-commented lines, look for any that start with "class " and also "< ApplicationRecord"
1738
+ if line.lstrip.start_with?('class') && (idx = line.index('class'))
1739
+ model = line[idx + 5..-1].match(/[\s:]+([\w:]+)/)&.captures&.first
1740
+ if model && abstract_activerecord_bases.exclude?(model)
1741
+ klass = begin
1742
+ model.constantize
1743
+ rescue
1744
+ end
1745
+ s[model.underscore.tr('/', '.').pluralize] = [
1746
+ v.start_with?(rails_root) ? v[rails_root.length + 1..-1] : v,
1747
+ klass
1748
+ ]
1749
+ end
1750
+ end
1751
+ end
1752
+ end
1753
+ end
1754
+ ::Brick.relations.keys.map { |v| [(r = v.pluralize), (model = models[r])&.last&.table_name || v, migrations&.fetch(r, nil), model&.first] }
1755
+ end
1756
+
1683
1757
  # Locate orphaned records
1684
1758
  def find_orphans(multi_schema)
1685
1759
  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 = 57
8
+ TINY = 60
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
@@ -84,8 +84,13 @@ module Brick
84
84
 
85
85
  # Generate a list of tables that can be chosen
86
86
  chosen = gets_list(list: tables, chosen: tables.dup)
87
+ schemas = chosen.each_with_object({}) do |v, s|
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
91
+ end
87
92
  # Start the timestamps back the same number of minutes from now as expected number of migrations to create
88
- current_mig_time = Time.now - chosen.length.minutes
93
+ current_mig_time = Time.now - (schemas.length + chosen.length).minutes
89
94
  done = []
90
95
  fks = {}
91
96
  stuck = {}
@@ -124,9 +129,14 @@ module Brick
124
129
  tbl_parts.first
125
130
  end
126
131
  end
132
+ unless schema.blank? || built_schemas.key?(schema)
133
+ mig = +" def change\n create_schema(:#{schema}) unless schema_exists?(:#{schema})\n end\n"
134
+ migration_file_write(mig_path, "create_db_schema_#{schema}", current_mig_time += 1.minute, ar_version, mig)
135
+ built_schemas[schema] = nil
136
+ end
137
+
127
138
  # %%% For the moment we're skipping polymorphics
128
139
  fkey_cols = relation[:fks].values.select { |assoc| assoc[:is_bt] && !assoc[:polymorphic] }
129
- mig = +"class Create#{(full_table_name = tbl_parts.join('_')).camelize} < ActiveRecord::Migration#{ar_version}\n"
130
140
  # If the primary key is also used as a foreign key, will need to do id: false and then build out
131
141
  # a column definition which includes :primary_key -- %%% also using a data type of bigserial or serial
132
142
  # if this one has come in as bigint or integer.
@@ -163,8 +173,7 @@ module Brick
163
173
  end
164
174
  # Refer to this table name as a symbol or dotted string as appropriate
165
175
  tbl_code = tbl_parts.length == 1 ? ":#{tbl_parts.first}" : "'#{tbl}'"
166
- mig << " def change\n return unless reverting? || !table_exists?(#{tbl_code})\n\n"
167
- mig << " create_schema :#{schema} unless reverting? || schema_exists?(:#{schema})\n" if schema
176
+ mig = +" def change\n return unless reverting? || !table_exists?(#{tbl_code})\n\n"
168
177
  mig << " create_table #{tbl_code}#{id_option} do |t|\n"
169
178
  possible_ts = [] # Track possible generic timestamps
170
179
  add_fks = [] # Track foreign keys to add after table creation
@@ -217,7 +226,11 @@ module Brick
217
226
  possible_ts.each { |ts| emit_column('timestamp', ts.first, nil) }
218
227
  end
219
228
  mig << " end\n"
220
- mig << " execute('ALTER TABLE #{tbl} ADD PRIMARY KEY (#{pk_is_also_fk});')\n" if pk_is_also_fk
229
+ if pk_is_also_fk
230
+ mig << " reversible do |dir|\n"
231
+ mig << " dir.up { execute('ALTER TABLE #{tbl} ADD PRIMARY KEY (#{pk_is_also_fk})') }\n"
232
+ mig << " end\n"
233
+ end
221
234
  add_fks.each do |add_fk|
222
235
  is_commented = false
223
236
  # add_fk[2] holds the inverse relation
@@ -227,21 +240,16 @@ module Brick
227
240
  # No official PK, but if coincidentally there's a column of the same name, take a chance on it
228
241
  pk = (add_fk[2][:cols].key?(add_fk[1]) && add_fk[1]) || '???'
229
242
  end
230
- # to_table column
243
+ # to_table column
231
244
  mig << " #{'# ' if is_commented}add_foreign_key #{tbl_code}, #{add_fk[0]}, column: :#{add_fk[1]}, primary_key: :#{pk}\n"
232
245
  end
233
- unless built_schemas.key?(schema)
234
- mig << " drop_schema :#{schema} if reverting? && schema_exists?(:#{schema})\n"
235
- built_schemas[schema] = nil
236
- end
237
- mig << " end\nend\n"
238
- current_mig_time += 1.minute
239
- versions_to_create << (version = current_mig_time.strftime('%Y%m%d%H%M00')).split('_').first
240
- File.open("#{mig_path}/#{version}_create_#{full_table_name}.rb", "w") { |f| f.write mig }
246
+ mig << " end\n"
247
+ versions_to_create << migration_file_write(mig_path, "create_#{tbl_parts.join('_')}", current_mig_time += 1.minute, ar_version, mig)
241
248
  end
242
249
  done.concat(fringe)
243
250
  chosen -= done
244
251
  end
252
+
245
253
  stuck_counts = Hash.new { |h, k| h[k] = 0 }
246
254
  chosen.each do |leftover|
247
255
  puts "Can't do #{leftover} because:\n #{stuck[leftover].map do |snag|
@@ -285,5 +293,14 @@ module Brick
285
293
  def emit_column(type, name, suffix)
286
294
  " t.#{type.start_with?('numeric') ? 'decimal' : type} :#{name}#{suffix}\n"
287
295
  end
296
+
297
+ def migration_file_write(mig_path, name, current_mig_time, ar_version, mig)
298
+ File.open("#{mig_path}/#{version = current_mig_time.strftime('%Y%m%d%H%M00')}_#{name}.rb", "w") do |f|
299
+ f.write "class #{name.camelize} < ActiveRecord::Migration#{ar_version}\n"
300
+ f.write mig
301
+ f.write "end\n"
302
+ end
303
+ version
304
+ end
288
305
  end
289
306
  end
@@ -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.57
4
+ version: 1.0.60
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-11 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