railsforge 1.0.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +528 -0
- data/bin/railsforge +8 -0
- data/lib/railsforge/analyzers/base_analyzer.rb +41 -0
- data/lib/railsforge/analyzers/controller_analyzer.rb +83 -0
- data/lib/railsforge/analyzers/database_analyzer.rb +55 -0
- data/lib/railsforge/analyzers/metrics_analyzer.rb +55 -0
- data/lib/railsforge/analyzers/model_analyzer.rb +74 -0
- data/lib/railsforge/analyzers/performance_analyzer.rb +161 -0
- data/lib/railsforge/analyzers/refactor_analyzer.rb +118 -0
- data/lib/railsforge/analyzers/security_analyzer.rb +169 -0
- data/lib/railsforge/analyzers/spec_analyzer.rb +58 -0
- data/lib/railsforge/api_generator.rb +397 -0
- data/lib/railsforge/audit.rb +289 -0
- data/lib/railsforge/cli.rb +671 -0
- data/lib/railsforge/config.rb +181 -0
- data/lib/railsforge/database_analyzer.rb +300 -0
- data/lib/railsforge/doctor.rb +250 -0
- data/lib/railsforge/feature_generator.rb +560 -0
- data/lib/railsforge/generator.rb +313 -0
- data/lib/railsforge/generators/base_generator.rb +70 -0
- data/lib/railsforge/generators/demo_generator.rb +307 -0
- data/lib/railsforge/generators/devops_generator.rb +287 -0
- data/lib/railsforge/generators/monitoring_generator.rb +134 -0
- data/lib/railsforge/generators/service_generator.rb +122 -0
- data/lib/railsforge/generators/stimulus_controller_generator.rb +129 -0
- data/lib/railsforge/generators/test_generator.rb +289 -0
- data/lib/railsforge/generators/view_component_generator.rb +169 -0
- data/lib/railsforge/graph.rb +270 -0
- data/lib/railsforge/loader.rb +56 -0
- data/lib/railsforge/mailer_generator.rb +191 -0
- data/lib/railsforge/plugins/plugin_loader.rb +60 -0
- data/lib/railsforge/plugins.rb +30 -0
- data/lib/railsforge/profiles/admin_app.yml +49 -0
- data/lib/railsforge/profiles/api_only.yml +47 -0
- data/lib/railsforge/profiles/blog.yml +47 -0
- data/lib/railsforge/profiles/standard.yml +44 -0
- data/lib/railsforge/profiles.rb +99 -0
- data/lib/railsforge/refactor_analyzer.rb +401 -0
- data/lib/railsforge/refactor_controller.rb +277 -0
- data/lib/railsforge/refactors/refactor_controller.rb +117 -0
- data/lib/railsforge/template_loader.rb +105 -0
- data/lib/railsforge/templates/v1/form/spec_template.rb +18 -0
- data/lib/railsforge/templates/v1/form/template.rb +28 -0
- data/lib/railsforge/templates/v1/job/spec_template.rb +17 -0
- data/lib/railsforge/templates/v1/job/template.rb +13 -0
- data/lib/railsforge/templates/v1/policy/spec_template.rb +41 -0
- data/lib/railsforge/templates/v1/policy/template.rb +57 -0
- data/lib/railsforge/templates/v1/presenter/spec_template.rb +12 -0
- data/lib/railsforge/templates/v1/presenter/template.rb +13 -0
- data/lib/railsforge/templates/v1/query/spec_template.rb +12 -0
- data/lib/railsforge/templates/v1/query/template.rb +16 -0
- data/lib/railsforge/templates/v1/serializer/spec_template.rb +13 -0
- data/lib/railsforge/templates/v1/serializer/template.rb +11 -0
- data/lib/railsforge/templates/v1/service/spec_template.rb +12 -0
- data/lib/railsforge/templates/v1/service/template.rb +25 -0
- data/lib/railsforge/templates/v1/stimulus_controller/template.rb +35 -0
- data/lib/railsforge/templates/v1/view_component/template.rb +24 -0
- data/lib/railsforge/templates/v2/job/template.rb +49 -0
- data/lib/railsforge/templates/v2/query/template.rb +66 -0
- data/lib/railsforge/templates/v2/service/spec_template.rb +33 -0
- data/lib/railsforge/templates/v2/service/template.rb +71 -0
- data/lib/railsforge/templates/v3/job/template.rb +72 -0
- data/lib/railsforge/templates/v3/query/spec_template.rb +54 -0
- data/lib/railsforge/templates/v3/query/template.rb +115 -0
- data/lib/railsforge/templates/v3/service/spec_template.rb +51 -0
- data/lib/railsforge/templates/v3/service/template.rb +84 -0
- data/lib/railsforge/version.rb +5 -0
- data/lib/railsforge/wizard.rb +265 -0
- data/lib/railsforge/wizard_tui.rb +286 -0
- data/lib/railsforge.rb +13 -0
- metadata +216 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Config module for RailsForge
|
|
2
|
+
# Manages .railsforgerc configuration
|
|
3
|
+
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
|
|
7
|
+
module RailsForge
|
|
8
|
+
# Config class handles RailsForge configuration
|
|
9
|
+
class Config
|
|
10
|
+
# Default config file name
|
|
11
|
+
CONFIG_FILE = ".railsforgerc".freeze
|
|
12
|
+
|
|
13
|
+
# Default configuration
|
|
14
|
+
DEFAULT_CONFIG = {
|
|
15
|
+
"version" => "1.0",
|
|
16
|
+
"profile" => "standard",
|
|
17
|
+
"generators" => {
|
|
18
|
+
"default_template_version" => "v1",
|
|
19
|
+
"include_specs" => true
|
|
20
|
+
},
|
|
21
|
+
"analyzers" => {
|
|
22
|
+
"controller_max_lines" => 150,
|
|
23
|
+
"controller_max_methods" => 10,
|
|
24
|
+
"model_max_lines" => 200,
|
|
25
|
+
"model_max_method_lines" => 15
|
|
26
|
+
},
|
|
27
|
+
"refactor" => {
|
|
28
|
+
"min_method_lines" => 15,
|
|
29
|
+
"auto_extract" => false
|
|
30
|
+
}
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
# Load configuration from file
|
|
34
|
+
# @param base_path [String] Rails app root path
|
|
35
|
+
# @return [Hash] Configuration
|
|
36
|
+
def self.load(base_path = nil)
|
|
37
|
+
base_path ||= find_rails_app_path || Dir.pwd
|
|
38
|
+
|
|
39
|
+
config_file = File.join(base_path, CONFIG_FILE)
|
|
40
|
+
|
|
41
|
+
if File.exist?(config_file)
|
|
42
|
+
YAML.safe_load(File.read(config_file), permitted_classes: [], permitted_symbols: [], aliases: true) || DEFAULT_CONFIG
|
|
43
|
+
else
|
|
44
|
+
DEFAULT_CONFIG.dup
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Save configuration to file
|
|
49
|
+
# @param config [Hash] Configuration to save
|
|
50
|
+
# @param base_path [String] Rails app root path
|
|
51
|
+
def self.save(config, base_path = nil)
|
|
52
|
+
base_path ||= find_rails_app_path || Dir.pwd
|
|
53
|
+
|
|
54
|
+
config_file = File.join(base_path, CONFIG_FILE)
|
|
55
|
+
|
|
56
|
+
File.write(config_file, YAML.dump(config))
|
|
57
|
+
puts "Configuration saved to #{config_file}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get a specific config value
|
|
61
|
+
# @param key [String] Configuration key (dot-notation supported)
|
|
62
|
+
# @param base_path [String] Rails app root path
|
|
63
|
+
# @return [Object] Configuration value
|
|
64
|
+
def self.get(key, base_path = nil)
|
|
65
|
+
config = load(base_path)
|
|
66
|
+
|
|
67
|
+
keys = key.split(".")
|
|
68
|
+
value = config
|
|
69
|
+
|
|
70
|
+
keys.each do |k|
|
|
71
|
+
value = value[k] if value.is_a?(Hash)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
value
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Set a specific config value
|
|
78
|
+
# @param key [String] Configuration key
|
|
79
|
+
# @param value [Object] Value to set
|
|
80
|
+
# @param base_path [String] Rails app root path
|
|
81
|
+
def self.set(key, value, base_path = nil)
|
|
82
|
+
config = load(base_path)
|
|
83
|
+
|
|
84
|
+
keys = key.split(".")
|
|
85
|
+
current = config
|
|
86
|
+
|
|
87
|
+
keys[0...-1].each do |k|
|
|
88
|
+
current[k] ||= {}
|
|
89
|
+
current = current[k]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
current[keys.last] = value
|
|
93
|
+
save(config, base_path)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Initialize default config file
|
|
97
|
+
# @param base_path [String] Rails app root path
|
|
98
|
+
def self.init(base_path = nil)
|
|
99
|
+
base_path ||= find_rails_app_path || Dir.pwd
|
|
100
|
+
|
|
101
|
+
config_file = File.join(base_path, CONFIG_FILE)
|
|
102
|
+
|
|
103
|
+
if File.exist?(config_file)
|
|
104
|
+
puts "Config file already exists"
|
|
105
|
+
return false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
save(DEFAULT_CONFIG.dup, base_path)
|
|
109
|
+
true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Show current configuration
|
|
113
|
+
# @param base_path [String] Rails app root path
|
|
114
|
+
def self.show(base_path = nil)
|
|
115
|
+
config = load(base_path)
|
|
116
|
+
|
|
117
|
+
puts "RailsForge Configuration:"
|
|
118
|
+
puts ""
|
|
119
|
+
|
|
120
|
+
# Version
|
|
121
|
+
puts " Version: #{config['version']}"
|
|
122
|
+
puts " Profile: #{config['profile']}"
|
|
123
|
+
puts ""
|
|
124
|
+
|
|
125
|
+
# Generators
|
|
126
|
+
puts " Generators:"
|
|
127
|
+
gens = config["generators"] || {}
|
|
128
|
+
gens.each do |key, value|
|
|
129
|
+
puts " #{key}: #{value}"
|
|
130
|
+
end
|
|
131
|
+
puts ""
|
|
132
|
+
|
|
133
|
+
# Analyzers
|
|
134
|
+
puts " Analyzers:"
|
|
135
|
+
analyzers = config["analyzers"] || {}
|
|
136
|
+
analyzers.each do |key, value|
|
|
137
|
+
puts " #{key}: #{value}"
|
|
138
|
+
end
|
|
139
|
+
puts ""
|
|
140
|
+
|
|
141
|
+
# Refactor
|
|
142
|
+
puts " Refactor:"
|
|
143
|
+
refactor = config["refactor"] || {}
|
|
144
|
+
refactor.each do |key, value|
|
|
145
|
+
puts " #{key}: #{value}"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Reset configuration to defaults
|
|
150
|
+
# @param base_path [String] Rails app root path
|
|
151
|
+
def self.reset(base_path = nil)
|
|
152
|
+
base_path ||= find_rails_app_path || Dir.pwd
|
|
153
|
+
|
|
154
|
+
config_file = File.join(base_path, CONFIG_FILE)
|
|
155
|
+
|
|
156
|
+
if File.exist?(config_file)
|
|
157
|
+
File.delete(config_file)
|
|
158
|
+
puts "Configuration reset to defaults"
|
|
159
|
+
else
|
|
160
|
+
puts "No configuration file to reset"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
# Find Rails app path
|
|
167
|
+
def self.find_rails_app_path
|
|
168
|
+
path = Dir.pwd
|
|
169
|
+
max_depth = 10
|
|
170
|
+
|
|
171
|
+
max_depth.times do
|
|
172
|
+
return path if File.exist?(File.join(path, "config", "application.rb"))
|
|
173
|
+
parent = File.dirname(path)
|
|
174
|
+
break if parent == path
|
|
175
|
+
path = parent
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
module RailsForge
|
|
2
|
+
# DatabaseAnalyzer module scans models and database schema for issues
|
|
3
|
+
module DatabaseAnalyzer
|
|
4
|
+
# Error class for database analysis issues
|
|
5
|
+
class DatabaseError < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Simple pluralize helper
|
|
8
|
+
def self.pluralize(word)
|
|
9
|
+
return word + 's' unless word.end_with?('s')
|
|
10
|
+
return word + 'es' if word.end_with?('sh') || word.end_with?('ch')
|
|
11
|
+
word + 's'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Simple tableize helper
|
|
15
|
+
def self.tableize(word)
|
|
16
|
+
pluralize(word.downcase)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Analyzes database and models for common issues
|
|
20
|
+
# @param base_path [String] Rails app root path
|
|
21
|
+
# @return [Array<Hash>] Analysis results
|
|
22
|
+
def self.analyze(base_path = nil)
|
|
23
|
+
base_path ||= find_rails_app_path
|
|
24
|
+
raise DatabaseError, "Not in a Rails application directory" unless base_path
|
|
25
|
+
|
|
26
|
+
results = []
|
|
27
|
+
|
|
28
|
+
# Analyze models
|
|
29
|
+
results += analyze_models(base_path)
|
|
30
|
+
|
|
31
|
+
# Analyze schema if available
|
|
32
|
+
results += analyze_schema(base_path)
|
|
33
|
+
|
|
34
|
+
results
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Analyzes models for missing indexes and constraints
|
|
38
|
+
# @param base_path [String] Rails app root path
|
|
39
|
+
# @return [Array<Hash>] Analysis results
|
|
40
|
+
def self.analyze_models(base_path)
|
|
41
|
+
results = []
|
|
42
|
+
models_dir = File.join(base_path, "app", "models")
|
|
43
|
+
|
|
44
|
+
return results unless Dir.exist?(models_dir)
|
|
45
|
+
|
|
46
|
+
Dir.glob(File.join(models_dir, "**", "*.rb")).each do |file|
|
|
47
|
+
next if file.end_with?("_application.rb")
|
|
48
|
+
|
|
49
|
+
model_name = File.basename(file, ".rb")
|
|
50
|
+
content = File.read(file)
|
|
51
|
+
|
|
52
|
+
# Skip abstract classes
|
|
53
|
+
next if content.include?("abstract_class = true")
|
|
54
|
+
|
|
55
|
+
# Check for missing indexes on foreign keys
|
|
56
|
+
results += check_foreign_keys(content, model_name)
|
|
57
|
+
|
|
58
|
+
# Check for uniqueness constraints
|
|
59
|
+
results += check_uniqueness(content, model_name)
|
|
60
|
+
|
|
61
|
+
# Check for missing database indexes
|
|
62
|
+
results += check_indexes(content, model_name)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
results
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Analyzes schema.rb for missing foreign keys and indexes
|
|
69
|
+
# @param base_path [String] Rails app root path
|
|
70
|
+
# @return [Array<Hash>] Analysis results
|
|
71
|
+
def self.analyze_schema(base_path)
|
|
72
|
+
results = []
|
|
73
|
+
schema_file = File.join(base_path, "db", "schema.rb")
|
|
74
|
+
|
|
75
|
+
return results unless File.exist?(schema_file)
|
|
76
|
+
|
|
77
|
+
content = File.read(schema_file)
|
|
78
|
+
|
|
79
|
+
# Parse create_table statements
|
|
80
|
+
tables = content.scan(/create_table\s+"(\w+)"/).flatten
|
|
81
|
+
|
|
82
|
+
tables.each do |table|
|
|
83
|
+
# Check for tables without indexes
|
|
84
|
+
table_section = content[/create_table\s+"#{table}".*?(?=create_table|\z)/m]
|
|
85
|
+
|
|
86
|
+
if table_section
|
|
87
|
+
# Check for missing timestamps indexes
|
|
88
|
+
if table_section.include?("t.datetime")
|
|
89
|
+
results << {
|
|
90
|
+
type: :index,
|
|
91
|
+
table: table,
|
|
92
|
+
issue: "Table #{table} has datetime columns - consider adding indexes",
|
|
93
|
+
suggestion: "add_index :#{table}, :created_at",
|
|
94
|
+
severity: :low
|
|
95
|
+
} unless table_section.include?("index") && table.include?("created_at")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
results
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Checks for missing foreign key indexes
|
|
104
|
+
# @param content [String] Model file content
|
|
105
|
+
# @param model_name [String] Model name
|
|
106
|
+
# @return [Array<Hash>] Analysis results
|
|
107
|
+
def self.check_foreign_keys(content, model_name)
|
|
108
|
+
results = []
|
|
109
|
+
|
|
110
|
+
# Find belongs_to associations
|
|
111
|
+
content.scan(/belongs_to\s+:(\w+)/).each do |assoc|
|
|
112
|
+
assoc_name = assoc[0]
|
|
113
|
+
|
|
114
|
+
# Check if foreign key is indexed
|
|
115
|
+
unless content.include?("index") && content.include?("#{assoc_name}_id")
|
|
116
|
+
results << {
|
|
117
|
+
type: :foreign_key,
|
|
118
|
+
model: model_name,
|
|
119
|
+
assoc: assoc_name,
|
|
120
|
+
issue: "Model #{model_name} has belongs_to :#{assoc_name} but may be missing an index",
|
|
121
|
+
suggestion: "add_index :#{tableize(model_name)}, :#{assoc_name}_id",
|
|
122
|
+
severity: :medium
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
results
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Checks for uniqueness validations without database constraints
|
|
131
|
+
# @param content [String] Model file content
|
|
132
|
+
# @param model_name [String] Model name
|
|
133
|
+
# @return [Array<Hash>] Analysis results
|
|
134
|
+
def self.check_uniqueness(content, model_name)
|
|
135
|
+
results = []
|
|
136
|
+
|
|
137
|
+
# Find uniqueness validations
|
|
138
|
+
content.scan(/validates\s+:(\w+),\s+uniqueness:/).each do |field|
|
|
139
|
+
field_name = field[0]
|
|
140
|
+
|
|
141
|
+
# Check if there's a unique index
|
|
142
|
+
unless content.include?("unique: true")
|
|
143
|
+
results << {
|
|
144
|
+
type: :uniqueness,
|
|
145
|
+
model: model_name,
|
|
146
|
+
field: field_name,
|
|
147
|
+
issue: "Model #{model_name} validates uniqueness of :#{field_name} but has no unique index",
|
|
148
|
+
suggestion: "add_index :#{tableize(model_name)}, :#{field_name}, unique: true",
|
|
149
|
+
severity: :high
|
|
150
|
+
}
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
results
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Checks for commonly indexed fields that may be missing indexes
|
|
158
|
+
# @param content [String] Model file content
|
|
159
|
+
# @param model_name [String] Model name
|
|
160
|
+
# @return [Array<Hash>] Analysis results
|
|
161
|
+
def self.check_indexes(content, model_name)
|
|
162
|
+
results = []
|
|
163
|
+
|
|
164
|
+
# Common fields that should be indexed
|
|
165
|
+
commonly_indexed = %w[email slug token uuid]
|
|
166
|
+
|
|
167
|
+
commonly_indexed.each do |field|
|
|
168
|
+
if content.include?(":#{field}") && !content.include?("add_index")
|
|
169
|
+
results << {
|
|
170
|
+
type: :index,
|
|
171
|
+
model: model_name,
|
|
172
|
+
issue: "Model #{model_name} has :#{field} field but may be missing an index",
|
|
173
|
+
suggestion: "add_index :#{tableize(model_name)}, :#{field}",
|
|
174
|
+
severity: :medium
|
|
175
|
+
}
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Check for has_many :through associations that need indexes
|
|
180
|
+
content.scan(/has_many\s+:(\w+),\s+through:/).each do |assoc|
|
|
181
|
+
results << {
|
|
182
|
+
type: :index,
|
|
183
|
+
model: model_name,
|
|
184
|
+
issue: "Model #{model_name} has has_many :through :#{assoc[0]} - ensure join table has indexes",
|
|
185
|
+
suggestion: "Ensure your join table has composite indexes on both foreign keys",
|
|
186
|
+
severity: :low
|
|
187
|
+
}
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
results
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Generates a migration file for a suggested fix
|
|
194
|
+
# @param suggestion [Hash] Suggestion details
|
|
195
|
+
# @param base_path [String] Rails app root
|
|
196
|
+
# @return [String] Path to created migration
|
|
197
|
+
def self.generate_migration(suggestion, base_path = nil)
|
|
198
|
+
base_path ||= find_rails_app_path
|
|
199
|
+
raise DatabaseError, "Not in a Rails application directory" unless base_path
|
|
200
|
+
|
|
201
|
+
migrate_dir = File.join(base_path, "db", "migrate")
|
|
202
|
+
FileUtils.mkdir_p(migrate_dir)
|
|
203
|
+
|
|
204
|
+
timestamp = Time.now.strftime("%Y%m%d%H%M%S")
|
|
205
|
+
filename = "migration_#{timestamp}.rb"
|
|
206
|
+
filepath = File.join(migrate_dir, filename)
|
|
207
|
+
|
|
208
|
+
table_name = tableize(suggestion[:model])
|
|
209
|
+
|
|
210
|
+
migration_content = case suggestion[:type]
|
|
211
|
+
when :uniqueness
|
|
212
|
+
field = suggestion[:field] || "field"
|
|
213
|
+
<<~RUBY
|
|
214
|
+
class Add#{suggestion[:model]}#{field.capitalize}UniqueIndex < ActiveRecord::Migration[7.0]
|
|
215
|
+
def change
|
|
216
|
+
add_index :#{table_name}, :#{field}, unique: true
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
RUBY
|
|
220
|
+
when :foreign_key
|
|
221
|
+
assoc = suggestion[:assoc] || "association"
|
|
222
|
+
<<~RUBY
|
|
223
|
+
class Add#{suggestion[:model].capitalize}#{assoc.capitalize}Index < ActiveRecord::Migration[7.0]
|
|
224
|
+
def change
|
|
225
|
+
add_index :#{table_name}, :#{assoc}_id
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
RUBY
|
|
229
|
+
when :index
|
|
230
|
+
<<~RUBY
|
|
231
|
+
class Add#{suggestion[:model].capitalize}Index < ActiveRecord::Migration[7.0]
|
|
232
|
+
def change
|
|
233
|
+
#{suggestion[:suggestion]}
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
RUBY
|
|
237
|
+
else
|
|
238
|
+
"# No migration template available for #{suggestion[:type]}"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
File.write(filepath, migration_content)
|
|
242
|
+
puts " Created db/migrate/#{filename}"
|
|
243
|
+
filepath
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Prints analysis report
|
|
247
|
+
# @param results [Array<Hash>] Analysis results
|
|
248
|
+
def self.print_report(results)
|
|
249
|
+
return puts "\n✓ No database issues found!" if results.empty?
|
|
250
|
+
|
|
251
|
+
puts "\n" + "=" * 60
|
|
252
|
+
puts "DATABASE ANALYSIS REPORT"
|
|
253
|
+
puts "=" * 60
|
|
254
|
+
|
|
255
|
+
# Group by severity
|
|
256
|
+
high = results.select { |r| r[:severity] == :high }
|
|
257
|
+
medium = results.select { |r| r[:severity] == :medium }
|
|
258
|
+
low = results.select { |r| r[:severity] == :low }
|
|
259
|
+
|
|
260
|
+
if high.any?
|
|
261
|
+
puts "\n🔴 High Priority (#{high.count}):"
|
|
262
|
+
high.each { |r| print_issue(r) }
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
if medium.any?
|
|
266
|
+
puts "\n🟡 Medium Priority (#{medium.count}):"
|
|
267
|
+
medium.each { |r| print_issue(r) }
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
if low.any?
|
|
271
|
+
puts "\n🟢 Low Priority (#{low.count}):"
|
|
272
|
+
low.each { |r| print_issue(r) }
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
puts "\n" + "=" * 60
|
|
276
|
+
puts "Total issues found: #{results.count}"
|
|
277
|
+
puts "=" * 60
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Prints a single issue
|
|
281
|
+
# @param issue [Hash] Issue details
|
|
282
|
+
def self.print_issue(issue)
|
|
283
|
+
puts "\n #{issue[:type].to_s.upcase}: #{issue[:issue]}"
|
|
284
|
+
puts " → Suggested: #{issue[:suggestion]}"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Finds Rails app root path
|
|
288
|
+
# @return [String, nil] Rails app path
|
|
289
|
+
def self.find_rails_app_path
|
|
290
|
+
path = Dir.pwd
|
|
291
|
+
10.times do
|
|
292
|
+
return path if File.exist?(File.join(path, "config", "application.rb"))
|
|
293
|
+
parent = File.dirname(path)
|
|
294
|
+
break if parent == path
|
|
295
|
+
path = parent
|
|
296
|
+
end
|
|
297
|
+
nil
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|