brick 1.0.56 → 1.0.59

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: b696c53c9d9d359d49de147b06b5342820ca230451c860d097f44bcae3b23bb0
4
- data.tar.gz: 7b0f6ff365f918a97d64305eabf70fcfe78cf764514ca22b3731b780da04b22c
3
+ metadata.gz: db41c2288f8bba8f3cb9c5d2d87bacd4806d1f2401ca9709f85de1886b0db377
4
+ data.tar.gz: 42b0a7b285a3328ce922daa442e15bf994a382ad0bfc5ed8fe4370ae6b779fbb
5
5
  SHA512:
6
- metadata.gz: 43ab6db28554fb7502d0f03267d2f5bab27d310fb988701d371232f6604090cd679767a12097a7c217401b05e938c787844ae11138a2c8143c1e675b93599656
7
- data.tar.gz: 8d2e592ba27bba30e100ea23a2bf951d6f3861c422ef355bad7cd38e146c20c2cf53380aa7ee440f40aa2d111c7f33eadc8dc09f73d1689432fff071e04228c4
6
+ metadata.gz: b8baacad02f8ba5c0d9bfe11182d8297c817ba46bef250e172f55a0ff949591abcadda790aecc2614515468a4da468415a8a0e62fd2a60232177dcd3cd63ca03
7
+ data.tar.gz: e84d2573ec0f95b01dfebf681cec409ec66a1e6052abbb59ebd3130728cad71015c51087f484c55e8e30f3b9a3736d3d9262b7d8cdebf23e5ab702035c0dcbfa
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
@@ -646,7 +646,7 @@ Module.class_exec do
646
646
  full_class_name << "::#{self.name}" unless self == Object
647
647
  full_class_name << "::#{plural_class_name.underscore.singularize.camelize}"
648
648
  if (plural_class_name == 'BrickSwagger' ||
649
- (::Brick.config.add_orphans && plural_class_name == 'BrickGem') ||
649
+ ((::Brick.config.add_status || ::Brick.config.add_orphans) && plural_class_name == 'BrickGem') ||
650
650
  model = self.const_get(full_class_name))
651
651
  # 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
652
  Object.send(:build_controller, self, class_name, plural_class_name, model, relations)
@@ -667,45 +667,11 @@ Module.class_exec do
667
667
  elsif ::Brick.enable_models?
668
668
  # Custom inheritable Brick base model?
669
669
  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
670
+ Object.send(:build_model, relations, base_module, name, class_name, inheritable_name)
705
671
  end
706
672
  if result
707
673
  built_class, code = result
708
- puts "\n#{code}"
674
+ puts "\n#{code}\n"
709
675
  built_class
710
676
  elsif ::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}") && !schema_name
711
677
  # module_prefixes = type_name.split('::')
@@ -727,7 +693,42 @@ class Object
727
693
 
728
694
  private
729
695
 
730
- def build_model(schema_name, inheritable_name, model_name, singular_table_name, table_name, relations, matching)
696
+ def build_model(relations, base_module, base_name, class_name, inheritable_name = nil)
697
+ if (base_model = ::Brick.config.sti_namespace_prefixes&.fetch("::#{base_module.name}::", nil)&.constantize) || # Are we part of an auto-STI namespace? ...
698
+ base_module != Object # ... or otherwise already in some namespace?
699
+ schema_name = [(singular_schema_name = base_name.underscore),
700
+ (schema_name = singular_schema_name.pluralize),
701
+ base_name,
702
+ base_name.pluralize].find { |s| Brick.db_schemas.include?(s) }
703
+ end
704
+ plural_class_name = ActiveSupport::Inflector.pluralize(model_name = class_name)
705
+ # If it's namespaced then we turn the first part into what would be a schema name
706
+ singular_table_name = ActiveSupport::Inflector.underscore(model_name).gsub('/', '.')
707
+
708
+ if base_model
709
+ schema_name = base_name.underscore # For the auto-STI namespace models
710
+ table_name = base_model.table_name
711
+ build_model_worker(base_module, inheritable_name, model_name, singular_table_name, table_name, relations, table_name)
712
+ else
713
+ # Adjust for STI if we know of a base model for the requested model name
714
+ # %%% Does not yet work with namespaced model names. Perhaps prefix with plural_class_name when doing the lookups here.
715
+ table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil) || ::Brick.existing_stis[model_name]&.constantize)
716
+ base_model.table_name
717
+ else
718
+ ActiveSupport::Inflector.pluralize(singular_table_name)
719
+ end
720
+ if ::Brick.apartment_multitenant &&
721
+ Apartment.excluded_models.include?(table_name.singularize.camelize)
722
+ schema_name = Apartment.default_schema
723
+ end
724
+ # Maybe, just maybe there's a database table that will satisfy this need
725
+ if (matching = [table_name, singular_table_name, plural_class_name, model_name].find { |m| relations.key?(schema_name ? "#{schema_name}.#{m}" : m) })
726
+ build_model_worker(schema_name, inheritable_name, model_name, singular_table_name, table_name, relations, matching)
727
+ end
728
+ end
729
+ end
730
+
731
+ def build_model_worker(schema_name, inheritable_name, model_name, singular_table_name, table_name, relations, matching)
731
732
  if ::Brick.apartment_multitenant &&
732
733
  schema_name == Apartment.default_schema
733
734
  relation = relations["#{schema_name}.#{matching}"]
@@ -893,7 +894,7 @@ class Object
893
894
  end
894
895
  end
895
896
  end
896
- code << "end # model #{full_name}\n\n"
897
+ code << "end # model #{full_name}\n"
897
898
  [built_model, code]
898
899
  end
899
900
 
@@ -1002,6 +1003,9 @@ class Object
1002
1003
  # Brick-specific pages
1003
1004
  case plural_class_name
1004
1005
  when 'BrickGem'
1006
+ self.define_method :status do
1007
+ instance_variable_set(:@resources, ::Brick.get_status_of_resources)
1008
+ end
1005
1009
  self.define_method :orphans do
1006
1010
  instance_variable_set(:@orphans, ::Brick.find_orphans(::Brick.set_db_schema(params)))
1007
1011
  end
@@ -1255,7 +1259,7 @@ class Object
1255
1259
  # Get column names for params from relations[model.table_name][:cols].keys
1256
1260
  end
1257
1261
  # end
1258
- code << "end # #{namespace_name}#{class_name}\n\n"
1262
+ code << "end # #{namespace_name}#{class_name}\n"
1259
1263
  end # class definition
1260
1264
  [built_controller, code]
1261
1265
  end
@@ -1680,6 +1684,59 @@ module Brick
1680
1684
  assoc_bt[:inverse] = assoc_hm
1681
1685
  end
1682
1686
 
1687
+ # Identify built out routes, migrations, models,
1688
+ # (and also soon controllers and views!)
1689
+ # for each resource
1690
+ def get_status_of_resources
1691
+ rails_root = ::Rails.root.to_s
1692
+ migrations = if Dir.exist?(mig_path = ActiveRecord::Migrator.migrations_paths.first || "#{rails_root}/db/migrate")
1693
+ Dir["#{mig_path}/**/*.rb"].each_with_object(Hash.new { |h, k| h[k] = [] }) do |v, s|
1694
+ File.read(v).split("\n").each do |line|
1695
+ # For all non-commented lines, look for any that have "create_table", "alter_table", or "drop_table"
1696
+ if !line.lstrip.start_with?('#') &&
1697
+ (idx = (line.index('create_table ') || line.index('create_table('))&.+(13)) ||
1698
+ (idx = (line.index('alter_table ') || line.index('alter_table('))&.+(12)) ||
1699
+ (idx = (line.index('drop_table ') || line.index('drop_table('))&.+(11))
1700
+ tbl = line[idx..-1].match(/([:'"\w\.]+)/)&.captures&.first
1701
+ if tbl
1702
+ s[tbl.tr(':\'"', '').pluralize] << v
1703
+ break
1704
+ end
1705
+ end
1706
+ end
1707
+ end
1708
+ end
1709
+ if ::ActiveSupport.version < ::Gem::Version.new('6') ||
1710
+ ::Rails.configuration.instance_variable_get(:@autoloader) == :classic
1711
+ Rails.configuration.eager_load_namespaces.select { |ns| ns < Rails::Application }.each(&:eager_load!)
1712
+ else
1713
+ Zeitwerk::Loader.eager_load_all
1714
+ end
1715
+ abstract_activerecord_bases = ActiveRecord::Base.descendants.select { |ar| ar.abstract_class? }.map(&:name)
1716
+ # abstract_activerecord_bases << ActiveRecord::Base
1717
+ models = if Dir.exist?(model_path = "#{rails_root}/app/models")
1718
+ Dir["#{model_path}/**/*.rb"].each_with_object({}) do |v, s|
1719
+ File.read(v).split("\n").each do |line|
1720
+ # For all non-commented lines, look for any that start with "class " and also "< ApplicationRecord"
1721
+ if line.lstrip.start_with?('class') && (idx = line.index('class'))
1722
+ model = line[idx + 5..-1].match(/[\s:]+([\w:]+)/)&.captures&.first
1723
+ if model && abstract_activerecord_bases.exclude?(model)
1724
+ klass = begin
1725
+ model.constantize
1726
+ rescue
1727
+ end
1728
+ s[model.underscore.tr('/', '.').pluralize] = [
1729
+ v.start_with?(rails_root) ? v[rails_root.length + 1..-1] : v,
1730
+ klass
1731
+ ]
1732
+ end
1733
+ end
1734
+ end
1735
+ end
1736
+ end
1737
+ ::Brick.relations.keys.map { |v| [(r = v.pluralize), (model = models[r])&.last&.table_name || v, migrations&.fetch(r, nil), model&.first] }
1738
+ end
1739
+
1683
1740
  # Locate orphaned records
1684
1741
  def find_orphans(multi_schema)
1685
1742
  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('/', '_')
@@ -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 = 56
8
+ TINY = 59
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
@@ -5,12 +5,14 @@ require 'rails/generators/active_record'
5
5
  require 'fancy_gets'
6
6
 
7
7
  module Brick
8
- # Auto-generates migrations
8
+ # Auto-generates migration files
9
9
  class MigrationsGenerator < ::Rails::Generators::Base
10
10
  include FancyGets
11
11
  # include ::Rails::Generators::Migration
12
12
 
13
- # SQL types that are the same as their migration data type name: text, integer, bigint, date, boolean, decimal, float
13
+ # Many SQL types are the same as their migration data type name:
14
+ # text, integer, bigint, date, boolean, decimal, float
15
+ # These however are not:
14
16
  SQL_TYPES = { 'character varying' => 'string',
15
17
  'character' => 'string', # %%% Need to put in "limit: 1"
16
18
  'xml' => 'text',
@@ -23,15 +25,7 @@ module Brick
23
25
  'smallint' => 'integer' } # %%% Need to put in "limit: 2"
24
26
  # (Still need to find what "inet" and "json" data types map to.)
25
27
 
26
- # # source_root File.expand_path('templates', __dir__)
27
- # class_option(
28
- # :with_changes,
29
- # type: :boolean,
30
- # default: false,
31
- # desc: 'Add IMPORT_TEMPLATE to model'
32
- # )
33
-
34
- desc 'Auto-generates migrations for an existing database.'
28
+ desc 'Auto-generates migration files for an existing database.'
35
29
 
36
30
  def brick_migrations
37
31
  # If Apartment is active, see if a default schema to analyse is indicated
@@ -47,11 +41,14 @@ module Brick
47
41
  key_type = (ActiveRecord.version < ::Gem::Version.new('5.1') ? 'integer' : 'bigint')
48
42
  is_4x_rails = ActiveRecord.version < ::Gem::Version.new('5.0')
49
43
  ar_version = "[#{ActiveRecord.version.segments[0..1].join('.')}]" unless is_4x_rails
50
- default_mig_path = (mig_path = ActiveRecord::Migrator.migrations_paths.first || "#{::Rails.root}/db/migrate")
51
- if Dir.exist?(mig_path)
44
+ is_insert_versions = true
45
+ is_delete_versions = false
46
+ versions_to_delete_or_append = nil
47
+ if Dir.exist?(mig_path = ActiveRecord::Migrator.migrations_paths.first || "#{::Rails.root}/db/migrate")
52
48
  if Dir["#{mig_path}/**/*.rb"].present?
53
49
  puts "WARNING: migrations folder #{mig_path} appears to already have ruby files present."
54
50
  mig_path2 = "#{::Rails.root}/tmp/brick_migrations"
51
+ is_insert_versions = false unless mig_path == mig_path2
55
52
  if Dir.exist?(mig_path2)
56
53
  if Dir["#{mig_path2}/**/*.rb"].present?
57
54
  puts "As well, temporary folder #{mig_path2} also has ruby files present."
@@ -59,10 +56,15 @@ module Brick
59
56
  mig_path2 = gets_list(list: ['Cancel operation!', "Append migration files into #{mig_path} anyway", mig_path, mig_path2])
60
57
  return if mig_path2.start_with?('Cancel')
61
58
 
59
+ existing_mig_files = Dir["#{mig_path2}/**/*.rb"]
60
+ if (is_insert_versions = mig_path == mig_path2)
61
+ versions_to_delete_or_append = existing_mig_files.map { |ver| ver.split('/').last.split('_').first }
62
+ end
62
63
  if mig_path2.start_with?('Append migration files into ')
63
64
  mig_path2 = mig_path
64
65
  else
65
- Dir["#{mig_path2}/**/*.rb"].each { |rb| File.delete(rb) }
66
+ is_delete_versions = true
67
+ existing_mig_files.each { |rb| File.delete(rb) }
66
68
  end
67
69
  else
68
70
  puts "Using temporary folder #{mig_path2} for created migration files.\n\n"
@@ -82,13 +84,20 @@ module Brick
82
84
 
83
85
  # Generate a list of tables that can be chosen
84
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
85
92
  # Start the timestamps back the same number of minutes from now as expected number of migrations to create
86
- current_mig_time = Time.now - chosen.length.minutes
93
+ current_mig_time = Time.now - (schemas.length + chosen.length).minutes
87
94
  done = []
88
95
  fks = {}
89
96
  stuck = {}
90
97
  indexes = {} # Track index names to make sure things are unique
91
- versions = [] # Resulting versions to be used when updating the schema_migrations table
98
+ built_schemas = {} # Track all built schemas so we can place an appropriate drop_schema command only in the first
99
+ # migration in which that schema is referenced, thereby allowing rollbacks to function properly.
100
+ versions_to_create = [] # Resulting versions to be used when updating the schema_migrations table
92
101
  # Start by making migrations for fringe tables (those with no foreign keys).
93
102
  # Continue layer by layer, creating migrations for tables that reference ones already done, until
94
103
  # no more migrations can be created. (At that point hopefully all tables are accounted for.)
@@ -113,20 +122,28 @@ module Brick
113
122
  end
114
123
  end
115
124
  schema = if (tbl_parts = tbl.split('.')).length > 1
116
- if tbl_parts.first == 'public'
125
+ if tbl_parts.first == (::Brick.default_schema || 'public')
117
126
  tbl_parts.shift
118
127
  nil
119
128
  else
120
129
  tbl_parts.first
121
130
  end
122
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
+
123
138
  # %%% For the moment we're skipping polymorphics
124
139
  fkey_cols = relation[:fks].values.select { |assoc| assoc[:is_bt] && !assoc[:polymorphic] }
125
- mig = +"class Create#{(full_table_name = tbl_parts.join('_')).camelize} < ActiveRecord::Migration#{ar_version}\n"
140
+ # If the primary key is also used as a foreign key, will need to do id: false and then build out
141
+ # a column definition which includes :primary_key -- %%% also using a data type of bigserial or serial
142
+ # if this one has come in as bigint or integer.
143
+ pk_is_also_fk = fkey_cols.any? { |assoc| pkey_cols&.first == assoc[:fk] } ? pkey_cols&.first : nil
126
144
  # Support missing primary key (by adding: ,id: false)
127
- # also integer / uuid / other non-standard data types for primary key
128
- id_option = unless (pkey_col_first = relation[:cols][pkey_cols&.first]&.first) == key_type
129
- unless pkey_cols&.present?
145
+ id_option = if pk_is_also_fk || (pkey_col_first = relation[:cols][pkey_cols&.first]&.first) != key_type
146
+ if pk_is_also_fk || !pkey_cols&.present?
130
147
  ', id: false'
131
148
  else
132
149
  case pkey_col_first
@@ -156,22 +173,12 @@ module Brick
156
173
  end
157
174
  # Refer to this table name as a symbol or dotted string as appropriate
158
175
  tbl_code = tbl_parts.length == 1 ? ":#{tbl_parts.first}" : "'#{tbl}'"
159
- mig << " def change\n return unless reverting? || !table_exists?(#{tbl_code})\n\n"
160
- mig << " create_schema :#{schema} unless schema_exists?(:#{schema})\n" if schema
176
+ mig = +" def change\n return unless reverting? || !table_exists?(#{tbl_code})\n\n"
161
177
  mig << " create_table #{tbl_code}#{id_option} do |t|\n"
162
178
  possible_ts = [] # Track possible generic timestamps
163
179
  add_fks = [] # Track foreign keys to add after table creation
164
180
  relation[:cols].each do |col, col_type|
165
- next if !id_option&.end_with?('id: false') && pkey_cols.include?(col)
166
-
167
- # See if there are generic timestamps
168
- if (sql_type = SQL_TYPES[col_type.first]) == 'timestamp' &&
169
- ['created_at','updated_at'].include?(col)
170
- possible_ts << [col, !col_type[3]]
171
- next
172
- end
173
-
174
- sql_type ||= col_type.first
181
+ sql_type = SQL_TYPES[col_type.first] || col_type.first
175
182
  suffix = col_type[3] ? +', null: false' : +''
176
183
  if !is_4x_rails && klass && (comment = klass.columns_hash.fetch(col, nil)&.comment)&.present?
177
184
  suffix << ", comment: #{comment.inspect}"
@@ -201,7 +208,14 @@ module Brick
201
208
  mig << " t.references :#{fk[:assoc_name]}#{suffix}, foreign_key: { to_table: #{to_table} }\n"
202
209
  end
203
210
  else
204
- mig << emit_column(sql_type, col, suffix)
211
+ next if !id_option&.end_with?('id: false') && pkey_cols.include?(col)
212
+
213
+ # See if there are generic timestamps
214
+ if sql_type == 'timestamp' && ['created_at','updated_at'].include?(col)
215
+ possible_ts << [col, !col_type[3]]
216
+ else
217
+ mig << emit_column(sql_type, col, suffix)
218
+ end
205
219
  end
206
220
  end
207
221
  if possible_ts.length == 2 && # Both created_at and updated_at
@@ -212,6 +226,11 @@ module Brick
212
226
  possible_ts.each { |ts| emit_column('timestamp', ts.first, nil) }
213
227
  end
214
228
  mig << " end\n"
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
215
234
  add_fks.each do |add_fk|
216
235
  is_commented = false
217
236
  # add_fk[2] holds the inverse relation
@@ -221,17 +240,16 @@ module Brick
221
240
  # No official PK, but if coincidentally there's a column of the same name, take a chance on it
222
241
  pk = (add_fk[2][:cols].key?(add_fk[1]) && add_fk[1]) || '???'
223
242
  end
224
- # to_table column
243
+ # to_table column
225
244
  mig << " #{'# ' if is_commented}add_foreign_key #{tbl_code}, #{add_fk[0]}, column: :#{add_fk[1]}, primary_key: :#{pk}\n"
226
245
  end
227
- mig << " end\nend\n"
228
- current_mig_time += 1.minute
229
- versions << (version = current_mig_time.strftime('%Y%m%d%H%M00'))
230
- 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)
231
248
  end
232
249
  done.concat(fringe)
233
250
  chosen -= done
234
251
  end
252
+
235
253
  stuck_counts = Hash.new { |h, k| h[k] = 0 }
236
254
  chosen.each do |leftover|
237
255
  puts "Can't do #{leftover} because:\n #{stuck[leftover].map do |snag|
@@ -250,9 +268,23 @@ module Brick
250
268
  }:"
251
269
  pp stuck_sorted[0..4]
252
270
  else # Successful, and now we can update the schema_migrations table accordingly
253
- ActiveRecord::Base.execute_sql("INSERT INTO #{ActiveRecord::Base.schema_migrations_table_name} (version) VALUES #{
254
- versions.map { |version| "('#{version}')" }.join(', ')
255
- }")
271
+ unless ActiveRecord::Migration.table_exists?(ActiveRecord::Base.schema_migrations_table_name)
272
+ ActiveRecord::SchemaMigration.create_table
273
+ end
274
+ # Remove to_delete - to_create
275
+ if ((versions_to_delete_or_append ||= []) - versions_to_create).present? && is_delete_versions
276
+ ActiveRecord::Base.execute_sql("DELETE FROM #{
277
+ ActiveRecord::Base.schema_migrations_table_name} WHERE version IN (#{
278
+ (versions_to_delete_or_append - versions_to_create).map { |vtd| "'#{vtd}'" }.join(', ')}
279
+ )")
280
+ end
281
+ # Add to_create - to_delete
282
+ if is_insert_versions && ((versions_to_create ||= []) - versions_to_delete_or_append).present?
283
+ ActiveRecord::Base.execute_sql("INSERT INTO #{
284
+ ActiveRecord::Base.schema_migrations_table_name} (version) VALUES #{
285
+ (versions_to_create - versions_to_delete_or_append).map { |vtc| "('#{vtc}')" }.join(', ')
286
+ }")
287
+ end
256
288
  end
257
289
  end
258
290
 
@@ -261,5 +293,14 @@ module Brick
261
293
  def emit_column(type, name, suffix)
262
294
  " t.#{type.start_with?('numeric') ? 'decimal' : type} :#{name}#{suffix}\n"
263
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
264
305
  end
265
306
  end
@@ -6,63 +6,71 @@ 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[-1] = tbl_parts[-1].singularize
36
+ tbl_parts.join('/').camelize
37
+ end - existing_models.map(&:name)
50
38
  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?('::')
39
+ is_a_namespaced = a.include?('::')
40
+ is_b_namespaced = b.include?('::')
53
41
  if is_a_namespaced && !is_b_namespaced
54
42
  1
55
43
  elsif !is_a_namespaced && is_b_namespaced
56
44
  -1
57
45
  else
58
- a.name <=> b.name
46
+ a <=> b
59
47
  end
60
48
  end
61
49
  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)
50
+ if longest_length < (len = m.length)
63
51
  longest_length = len
64
52
  end
65
53
  end
54
+ chosen = gets_list(list: models, chosen: models.dup)
55
+ relations = ::Brick.relations
56
+ chosen.each do |model_name|
57
+ # If we're in a schema then make sure the module file exists
58
+ base_module = if (model_parts = model_name.split('::')).length > 1
59
+ "::#{model_parts.first}".constantize
60
+ else
61
+ Object
62
+ end
63
+ _built_model, code = Object.send(:build_model, relations, base_module, base_module.name, model_parts.last)
64
+ path = ['models']
65
+ path.concat(model_parts.map(&:underscore))
66
+ dir = +"#{::Rails.root}/app"
67
+ path[0..-2].each do |path_part|
68
+ dir << "/#{path_part}"
69
+ Dir.mkdir(dir) unless Dir.exists?(dir)
70
+ end
71
+ File.open("#{dir}/#{path.last}.rb", 'w') { |f| f.write code }
72
+ end
73
+ puts "\n*** Created #{chosen.length} model files under app/models ***"
66
74
  end
67
75
 
68
76
  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.56
4
+ version: 1.0.59
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-09 00:00:00.000000000 Z
11
+ date: 2022-08-14 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