blast_radius 0.1.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/CHANGELOG.md +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +259 -0
- data/lib/blast_radius/active_record_extension.rb +62 -0
- data/lib/blast_radius/analyzer.rb +60 -0
- data/lib/blast_radius/configuration.rb +31 -0
- data/lib/blast_radius/dependency_tree.rb +87 -0
- data/lib/blast_radius/formatters/base.rb +28 -0
- data/lib/blast_radius/formatters/dot_formatter.rb +61 -0
- data/lib/blast_radius/formatters/html_formatter.rb +144 -0
- data/lib/blast_radius/formatters/json_formatter.rb +36 -0
- data/lib/blast_radius/formatters/mermaid_formatter.rb +49 -0
- data/lib/blast_radius/formatters/text_formatter.rb +43 -0
- data/lib/blast_radius/html/graph.js +852 -0
- data/lib/blast_radius/html/styles.css +475 -0
- data/lib/blast_radius/html/template.html.erb +112 -0
- data/lib/blast_radius/impact_calculator.rb +129 -0
- data/lib/blast_radius/node.rb +65 -0
- data/lib/blast_radius/railtie.rb +21 -0
- data/lib/blast_radius/version.rb +5 -0
- data/lib/blast_radius.rb +47 -0
- data/lib/tasks/blast_radius.rake +102 -0
- metadata +164 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BlastRadius
|
|
4
|
+
class Node
|
|
5
|
+
attr_reader :model_class, :association_name, :dependent_type,
|
|
6
|
+
:association_type, :children, :parent, :depth,
|
|
7
|
+
:through, :polymorphic
|
|
8
|
+
|
|
9
|
+
def initialize(model_class, options = {})
|
|
10
|
+
@model_class = model_class
|
|
11
|
+
@association_name = options[:association_name]
|
|
12
|
+
@dependent_type = options[:dependent_type]
|
|
13
|
+
@association_type = options[:association_type]
|
|
14
|
+
@through = options[:through]
|
|
15
|
+
@polymorphic = options[:polymorphic] || false
|
|
16
|
+
@parent = options[:parent]
|
|
17
|
+
@depth = options[:depth] || 0
|
|
18
|
+
@children = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def add_child(child_node)
|
|
22
|
+
@children << child_node
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Check if this node creates a circular reference
|
|
26
|
+
# @return [Boolean] true if circular reference detected
|
|
27
|
+
def circular_reference?
|
|
28
|
+
ancestors.any? { |ancestor| ancestor.model_class == @model_class }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Get all ancestor nodes
|
|
32
|
+
# @return [Array<Node>] array of ancestor nodes
|
|
33
|
+
def ancestors
|
|
34
|
+
return [] unless @parent
|
|
35
|
+
|
|
36
|
+
[@parent] + @parent.ancestors
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get model name as string
|
|
40
|
+
# @return [String] model class name
|
|
41
|
+
def model_name
|
|
42
|
+
@model_class.name
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Check if this is the root node
|
|
46
|
+
# @return [Boolean] true if root node
|
|
47
|
+
def root?
|
|
48
|
+
@parent.nil?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Check if this is a leaf node
|
|
52
|
+
# @return [Boolean] true if leaf node
|
|
53
|
+
def leaf?
|
|
54
|
+
@children.empty?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get the path from root to this node
|
|
58
|
+
# @return [Array<String>] array of model names
|
|
59
|
+
def path
|
|
60
|
+
return [model_name] if root?
|
|
61
|
+
|
|
62
|
+
@parent.path + [model_name]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/railtie'
|
|
4
|
+
|
|
5
|
+
module BlastRadius
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
railtie_name :blast_radius
|
|
8
|
+
|
|
9
|
+
rake_tasks do
|
|
10
|
+
load 'tasks/blast_radius.rake'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Add ActiveRecord extensions
|
|
14
|
+
initializer 'blast_radius.active_record' do
|
|
15
|
+
ActiveSupport.on_load(:active_record) do
|
|
16
|
+
extend BlastRadius::ActiveRecordExtension::ClassMethods
|
|
17
|
+
include BlastRadius::ActiveRecordExtension::InstanceMethods
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/blast_radius.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'blast_radius/version'
|
|
4
|
+
require_relative 'blast_radius/configuration'
|
|
5
|
+
require_relative 'blast_radius/analyzer'
|
|
6
|
+
require_relative 'blast_radius/node'
|
|
7
|
+
require_relative 'blast_radius/dependency_tree'
|
|
8
|
+
require_relative 'blast_radius/impact_calculator'
|
|
9
|
+
require_relative 'blast_radius/formatters/base'
|
|
10
|
+
require_relative 'blast_radius/formatters/text_formatter'
|
|
11
|
+
require_relative 'blast_radius/formatters/mermaid_formatter'
|
|
12
|
+
require_relative 'blast_radius/formatters/json_formatter'
|
|
13
|
+
require_relative 'blast_radius/formatters/dot_formatter'
|
|
14
|
+
require_relative 'blast_radius/formatters/html_formatter'
|
|
15
|
+
require_relative 'blast_radius/active_record_extension'
|
|
16
|
+
|
|
17
|
+
# Load Railtie if Rails is available
|
|
18
|
+
require_relative 'blast_radius/railtie' if defined?(Rails::Railtie)
|
|
19
|
+
|
|
20
|
+
module BlastRadius
|
|
21
|
+
class Error < StandardError; end
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
attr_writer :configuration
|
|
25
|
+
|
|
26
|
+
def configuration
|
|
27
|
+
@configuration ||= Configuration.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def configure
|
|
31
|
+
yield(configuration)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def reset_configuration
|
|
35
|
+
@configuration = Configuration.new
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Analyze a model class and return dependency tree
|
|
39
|
+
# @param model_class [Class] ActiveRecord model class
|
|
40
|
+
# @param options [Hash] options for analysis
|
|
41
|
+
# @return [Node] root node of dependency tree
|
|
42
|
+
def analyze(model_class, options = {})
|
|
43
|
+
tree = DependencyTree.new(configuration)
|
|
44
|
+
tree.build(model_class, options)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :blast_radius do
|
|
4
|
+
desc 'Visualize dependency tree for all models or a specific model'
|
|
5
|
+
task :visualize, %i[model_name format] => :environment do |_t, args|
|
|
6
|
+
require 'blast_radius'
|
|
7
|
+
|
|
8
|
+
format = (args[:format] || ENV['FORMAT'] || 'text').to_sym
|
|
9
|
+
|
|
10
|
+
if args[:model_name]
|
|
11
|
+
# Visualize specific model
|
|
12
|
+
begin
|
|
13
|
+
model_class = args[:model_name].constantize
|
|
14
|
+
output = model_class.blast_radius(format: format)
|
|
15
|
+
puts output
|
|
16
|
+
rescue NameError
|
|
17
|
+
puts "Error: Model '#{args[:model_name]}' not found"
|
|
18
|
+
exit 1
|
|
19
|
+
end
|
|
20
|
+
else
|
|
21
|
+
# Visualize all models
|
|
22
|
+
analyzer = BlastRadius::Analyzer.new
|
|
23
|
+
models = analyzer.all_models
|
|
24
|
+
|
|
25
|
+
puts "Found #{models.size} models:"
|
|
26
|
+
puts
|
|
27
|
+
|
|
28
|
+
models.each do |model|
|
|
29
|
+
puts "=== #{model.name} ==="
|
|
30
|
+
output = model.blast_radius(format: format)
|
|
31
|
+
puts output
|
|
32
|
+
puts
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc 'Calculate the impact of deleting a specific record'
|
|
38
|
+
task :impact, %i[model_name record_id] => :environment do |_t, args|
|
|
39
|
+
require 'blast_radius'
|
|
40
|
+
|
|
41
|
+
unless args[:model_name] && args[:record_id]
|
|
42
|
+
puts 'Usage: rake blast_radius:impact[ModelName,record_id]'
|
|
43
|
+
exit 1
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
begin
|
|
47
|
+
model_class = args[:model_name].constantize
|
|
48
|
+
record = model_class.find(args[:record_id])
|
|
49
|
+
|
|
50
|
+
calculator = BlastRadius::ImpactCalculator.new
|
|
51
|
+
impact = calculator.dry_run(record)
|
|
52
|
+
|
|
53
|
+
puts "Impact of deleting #{model_class.name}##{record.id}:"
|
|
54
|
+
puts
|
|
55
|
+
|
|
56
|
+
if impact.empty?
|
|
57
|
+
puts 'No dependent records would be affected.'
|
|
58
|
+
else
|
|
59
|
+
total_count = impact.values.sum { |v| v[:count] }
|
|
60
|
+
puts "Total records affected: #{total_count}"
|
|
61
|
+
puts
|
|
62
|
+
|
|
63
|
+
impact.each do |model_name, info|
|
|
64
|
+
puts "#{model_name}:"
|
|
65
|
+
puts " Count: #{info[:count]}"
|
|
66
|
+
puts " Dependent type: #{info[:dependent_type]}"
|
|
67
|
+
puts " Sample IDs: #{info[:sample_ids].join(', ')}"
|
|
68
|
+
puts
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
rescue NameError
|
|
72
|
+
puts "Error: Model '#{args[:model_name]}' not found"
|
|
73
|
+
exit 1
|
|
74
|
+
rescue ActiveRecord::RecordNotFound
|
|
75
|
+
puts "Error: #{args[:model_name]}##{args[:record_id]} not found"
|
|
76
|
+
exit 1
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
desc 'Generate HTML visualization and save to file'
|
|
81
|
+
task :html, %i[model_name output_file] => :environment do |_t, args|
|
|
82
|
+
require 'blast_radius'
|
|
83
|
+
|
|
84
|
+
unless args[:model_name]
|
|
85
|
+
puts 'Usage: rake blast_radius:html[ModelName,output_file]'
|
|
86
|
+
exit 1
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
output_file = args[:output_file] || 'blast_radius.html'
|
|
90
|
+
|
|
91
|
+
begin
|
|
92
|
+
model_class = args[:model_name].constantize
|
|
93
|
+
html = model_class.blast_radius(format: :html)
|
|
94
|
+
|
|
95
|
+
File.write(output_file, html)
|
|
96
|
+
puts "HTML visualization saved to: #{output_file}"
|
|
97
|
+
rescue NameError
|
|
98
|
+
puts "Error: Model '#{args[:model_name]}' not found"
|
|
99
|
+
exit 1
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: blast_radius
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activerecord
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '6.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '6.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activesupport
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '6.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '6.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rails
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '6.1'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '6.1'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rspec
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rubocop
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '1.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rubocop-rspec
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '2.0'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '2.0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: sqlite3
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '1.7'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '1.7'
|
|
110
|
+
description: 'A gem to visualize which records will be deleted due to dependent: :destroy
|
|
111
|
+
and similar options in ActiveRecord associations'
|
|
112
|
+
email:
|
|
113
|
+
- t.yudai92@gmail.com
|
|
114
|
+
executables: []
|
|
115
|
+
extensions: []
|
|
116
|
+
extra_rdoc_files: []
|
|
117
|
+
files:
|
|
118
|
+
- CHANGELOG.md
|
|
119
|
+
- LICENSE.txt
|
|
120
|
+
- README.md
|
|
121
|
+
- lib/blast_radius.rb
|
|
122
|
+
- lib/blast_radius/active_record_extension.rb
|
|
123
|
+
- lib/blast_radius/analyzer.rb
|
|
124
|
+
- lib/blast_radius/configuration.rb
|
|
125
|
+
- lib/blast_radius/dependency_tree.rb
|
|
126
|
+
- lib/blast_radius/formatters/base.rb
|
|
127
|
+
- lib/blast_radius/formatters/dot_formatter.rb
|
|
128
|
+
- lib/blast_radius/formatters/html_formatter.rb
|
|
129
|
+
- lib/blast_radius/formatters/json_formatter.rb
|
|
130
|
+
- lib/blast_radius/formatters/mermaid_formatter.rb
|
|
131
|
+
- lib/blast_radius/formatters/text_formatter.rb
|
|
132
|
+
- lib/blast_radius/html/graph.js
|
|
133
|
+
- lib/blast_radius/html/styles.css
|
|
134
|
+
- lib/blast_radius/html/template.html.erb
|
|
135
|
+
- lib/blast_radius/impact_calculator.rb
|
|
136
|
+
- lib/blast_radius/node.rb
|
|
137
|
+
- lib/blast_radius/railtie.rb
|
|
138
|
+
- lib/blast_radius/version.rb
|
|
139
|
+
- lib/tasks/blast_radius.rake
|
|
140
|
+
homepage: https://github.com/ydah/blast_radius
|
|
141
|
+
licenses:
|
|
142
|
+
- MIT
|
|
143
|
+
metadata:
|
|
144
|
+
homepage_uri: https://github.com/ydah/blast_radius
|
|
145
|
+
source_code_uri: https://github.com/ydah/blast_radius
|
|
146
|
+
changelog_uri: https://github.com/ydah/blast_radius/blob/main/CHANGELOG.md
|
|
147
|
+
rdoc_options: []
|
|
148
|
+
require_paths:
|
|
149
|
+
- lib
|
|
150
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
151
|
+
requirements:
|
|
152
|
+
- - ">="
|
|
153
|
+
- !ruby/object:Gem::Version
|
|
154
|
+
version: 3.0.0
|
|
155
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
156
|
+
requirements:
|
|
157
|
+
- - ">="
|
|
158
|
+
- !ruby/object:Gem::Version
|
|
159
|
+
version: '0'
|
|
160
|
+
requirements: []
|
|
161
|
+
rubygems_version: 3.6.9
|
|
162
|
+
specification_version: 4
|
|
163
|
+
summary: Visualize the cascade deletion impact in Rails applications
|
|
164
|
+
test_files: []
|