mermaid_rails_erd 1.0.1
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 +190 -0
- data/Rakefile +12 -0
- data/bin/console +10 -0
- data/bin/release +152 -0
- data/bin/setup +8 -0
- data/lib/mermaid_rails_erd/association_resolver.rb +32 -0
- data/lib/mermaid_rails_erd/column_info.rb +15 -0
- data/lib/mermaid_rails_erd/generator.rb +71 -0
- data/lib/mermaid_rails_erd/mermaid_emitter.rb +34 -0
- data/lib/mermaid_rails_erd/model_data_collector.rb +157 -0
- data/lib/mermaid_rails_erd/model_loader.rb +12 -0
- data/lib/mermaid_rails_erd/parsed_data.rb +51 -0
- data/lib/mermaid_rails_erd/polymorphic_targets_resolver.rb +37 -0
- data/lib/mermaid_rails_erd/railtie.rb +9 -0
- data/lib/mermaid_rails_erd/relationship.rb +41 -0
- data/lib/mermaid_rails_erd/relationship_builders/base_relationship_builder.rb +91 -0
- data/lib/mermaid_rails_erd/relationship_builders/belongs_to_relationship_builder.rb +39 -0
- data/lib/mermaid_rails_erd/relationship_builders/habtm_relationship_builder.rb +81 -0
- data/lib/mermaid_rails_erd/relationship_builders/has_many_relationship_builder.rb +36 -0
- data/lib/mermaid_rails_erd/relationship_builders/has_one_relationship_builder.rb +39 -0
- data/lib/mermaid_rails_erd/relationship_registry.rb +85 -0
- data/lib/mermaid_rails_erd/relationship_symbol_mapper.rb +13 -0
- data/lib/mermaid_rails_erd/version.rb +5 -0
- data/lib/mermaid_rails_erd.rb +35 -0
- data/lib/tasks/mermaid_rails_erd.rake +36 -0
- data/mermaid_rails_erd.gemspec +46 -0
- metadata +205 -0
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "column_info"
|
4
|
+
|
5
|
+
module MermaidRailsErd
|
6
|
+
class ModelDataCollector
|
7
|
+
attr_reader :models_data, :tables, :models_no_tables, :models, :invalid_associations, :polymorphic_associations, :regular_associations
|
8
|
+
|
9
|
+
def initialize(model_loader)
|
10
|
+
@models_data = {}
|
11
|
+
@polymorphic_associations = []
|
12
|
+
@regular_associations = []
|
13
|
+
@invalid_associations = []
|
14
|
+
@polymorphic_targets = Hash.new { |h, k| h[k] = [] }
|
15
|
+
@tables = {}
|
16
|
+
@models_no_tables = []
|
17
|
+
@models = model_loader.load
|
18
|
+
end
|
19
|
+
|
20
|
+
def collect
|
21
|
+
(base_models & models_with_tables).each do |model|
|
22
|
+
model_data = { model: model, associations: [] }
|
23
|
+
|
24
|
+
# Register polymorphic interfaces that this model implements
|
25
|
+
model.reflect_on_all_associations.each do |assoc|
|
26
|
+
if (interface_name = assoc.options[:as])
|
27
|
+
register_polymorphic_target(interface_name, model)
|
28
|
+
end
|
29
|
+
|
30
|
+
if assoc.options[:polymorphic]
|
31
|
+
@polymorphic_associations << { model: model, association: assoc }
|
32
|
+
else
|
33
|
+
@regular_associations << { model: model, association: assoc }
|
34
|
+
end
|
35
|
+
|
36
|
+
model_data[:associations] << assoc
|
37
|
+
end
|
38
|
+
|
39
|
+
@models_data[model.name] = model_data
|
40
|
+
|
41
|
+
collect_table_for_model(model)
|
42
|
+
end
|
43
|
+
|
44
|
+
(base_models & models_without_tables).each do |model|
|
45
|
+
@models_no_tables << model
|
46
|
+
end
|
47
|
+
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_model_data(model_name)
|
52
|
+
@models_data[model_name]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Register a model as implementing a polymorphic interface
|
56
|
+
# @param interface_name [String, Symbol] Name of the polymorphic interface
|
57
|
+
# @param model [Class] Model that implements the interface
|
58
|
+
def register_polymorphic_target(interface_name, model)
|
59
|
+
@polymorphic_targets[interface_name.to_s] << model
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns all models that implement the given polymorphic interface
|
63
|
+
# @param polymorphic_name [String, Symbol] Name of the polymorphic interface
|
64
|
+
# @return [Array<Class>] Array of models that implement the interface
|
65
|
+
def polymorphic_targets_for(polymorphic_name)
|
66
|
+
@polymorphic_targets[polymorphic_name.to_s] || []
|
67
|
+
end
|
68
|
+
|
69
|
+
# Reset all registered polymorphic targets (useful for testing)
|
70
|
+
def reset_polymorphic_targets
|
71
|
+
@polymorphic_targets.clear
|
72
|
+
end
|
73
|
+
|
74
|
+
# Collect table information from a model
|
75
|
+
# @param model [Class] ActiveRecord model to collect table information from
|
76
|
+
def collect_table_for_model(model)
|
77
|
+
# Skip if we've already collected this table
|
78
|
+
return if @tables.key?(model.table_name)
|
79
|
+
|
80
|
+
# Collect basic table structure with columns
|
81
|
+
@tables[model.table_name] = model.columns.map do |col|
|
82
|
+
annotations = []
|
83
|
+
annotations << "PK" if col.name == model.primary_key
|
84
|
+
|
85
|
+
ColumnInfo.new(col.name, annotations, col.sql_type, col.type, col.null)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Update table definitions with foreign key annotations
|
90
|
+
# @param relationships [Array<Relationship>] List of relationships to extract FKs from
|
91
|
+
# @return [Hash] Updated tables hash
|
92
|
+
def update_foreign_keys(relationships)
|
93
|
+
fk_mapping = extract_foreign_keys(relationships)
|
94
|
+
|
95
|
+
@tables.each do |table_name, columns|
|
96
|
+
next unless fk_mapping[table_name]
|
97
|
+
|
98
|
+
begin
|
99
|
+
columns.each do |col|
|
100
|
+
col.annotations << "FK" if fk_mapping[table_name].include?(col.name) && !col.annotations.include?("FK")
|
101
|
+
end
|
102
|
+
rescue StandardError => e
|
103
|
+
puts "Error updating FK annotations for #{table_name}: #{e.message}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
@tables
|
108
|
+
end
|
109
|
+
|
110
|
+
# Register an invalid association that couldn't be resolved
|
111
|
+
# @param model [Class] Model containing the invalid association
|
112
|
+
# @param assoc [ActiveRecord::Reflection::Association] Invalid association
|
113
|
+
# @param reason [String] Reason why the association is invalid
|
114
|
+
def register_invalid_association(model, assoc, reason = nil)
|
115
|
+
@invalid_associations << {
|
116
|
+
model: model,
|
117
|
+
association: assoc,
|
118
|
+
reason: reason,
|
119
|
+
label: "#{model.name}##{assoc.name}",
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def base_models
|
126
|
+
models.reject { |model| (model < model.base_class) }
|
127
|
+
end
|
128
|
+
|
129
|
+
def models_with_tables
|
130
|
+
models.select(&:table_exists?)
|
131
|
+
end
|
132
|
+
|
133
|
+
def models_without_tables
|
134
|
+
models.reject(&:table_exists?)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Extract foreign key information from relationships
|
138
|
+
# @param relationships [Array<Relationship>] List of relationships to extract FKs from
|
139
|
+
# @return [Hash] Mapping of table names to arrays of FK column names
|
140
|
+
def extract_foreign_keys(relationships)
|
141
|
+
fk_mapping = {}
|
142
|
+
|
143
|
+
relationships.each do |rel|
|
144
|
+
# Use the relationship attributes directly instead of parsing the label
|
145
|
+
table_name = rel.fk_table
|
146
|
+
fk_column = rel.fk_column
|
147
|
+
|
148
|
+
fk_mapping[table_name] ||= []
|
149
|
+
fk_mapping[table_name] << fk_column unless fk_mapping[table_name].include?(fk_column)
|
150
|
+
rescue StandardError => e
|
151
|
+
puts "Error extracting FK from relationship: #{e.message}"
|
152
|
+
end
|
153
|
+
|
154
|
+
fk_mapping
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MermaidRailsErd
|
4
|
+
class ModelLoader
|
5
|
+
def load
|
6
|
+
raise MermaidRailsErd::Error, "Rails is not loaded" unless defined?(Rails)
|
7
|
+
|
8
|
+
Rails.application.eager_load! unless Rails.configuration.cache_classes
|
9
|
+
ActiveRecord::Base.descendants.reject(&:abstract_class?)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MermaidRailsErd
|
4
|
+
# Class to hold parsed ERD data with delegation to model_data_collector
|
5
|
+
# Provides structured access to relationships, tables, and model data
|
6
|
+
class ParsedData
|
7
|
+
attr_reader :relationships, :tables, :model_data_collector
|
8
|
+
|
9
|
+
# Initialize with collected ERD data
|
10
|
+
# @param relationships [Array] Array of relationship objects
|
11
|
+
# @param tables [Hash] Hash of table definitions keyed by table name
|
12
|
+
# @param model_data_collector [ModelDataCollector] Collector instance with model data
|
13
|
+
def initialize(relationships, tables, model_data_collector)
|
14
|
+
@relationships = relationships
|
15
|
+
@tables = tables
|
16
|
+
@model_data_collector = model_data_collector
|
17
|
+
end
|
18
|
+
|
19
|
+
# Delegated methods from model_data_collector for better IDE support
|
20
|
+
|
21
|
+
# @return [Hash] Hash of model data keyed by model name
|
22
|
+
def models_data
|
23
|
+
model_data_collector.models_data
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Array] Array of models without tables
|
27
|
+
def models_no_tables
|
28
|
+
model_data_collector.models_no_tables
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Array] Array of all loaded models
|
32
|
+
def models
|
33
|
+
model_data_collector.models
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Array<Hash>] Array of invalid associations with details
|
37
|
+
def invalid_associations
|
38
|
+
model_data_collector.invalid_associations
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Array<Hash>] Array of polymorphic associations
|
42
|
+
def polymorphic_associations
|
43
|
+
model_data_collector.polymorphic_associations
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Array<Hash>] Array of regular (non-polymorphic) associations
|
47
|
+
def regular_associations
|
48
|
+
model_data_collector.regular_associations
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "relationship"
|
4
|
+
|
5
|
+
module MermaidRailsErd
|
6
|
+
class PolymorphicTargetsResolver
|
7
|
+
attr_reader :model_data_collector
|
8
|
+
|
9
|
+
def initialize(model_data_collector)
|
10
|
+
@model_data_collector = model_data_collector
|
11
|
+
end
|
12
|
+
|
13
|
+
def resolve(name, from_table, rel_type)
|
14
|
+
# Get all models that implement the polymorphic interface
|
15
|
+
target_models = model_data_collector.polymorphic_targets_for(name)
|
16
|
+
|
17
|
+
# Create relationships for each target model
|
18
|
+
target_models.map do |target|
|
19
|
+
fk_column = "#{name}_id"
|
20
|
+
Relationship.new(
|
21
|
+
from_table,
|
22
|
+
target.table_name,
|
23
|
+
fk_column,
|
24
|
+
rel_type,
|
25
|
+
nil, # Let the Relationship generate the label
|
26
|
+
from_table, # fk_table
|
27
|
+
fk_column, # fk_column
|
28
|
+
target.table_name, # pk_table
|
29
|
+
"id", # pk_column
|
30
|
+
# Add (polymorphic) to the label
|
31
|
+
true, # is_polymorphic
|
32
|
+
"polymorphic", # extra_label
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MermaidRailsErd
|
4
|
+
class Relationship
|
5
|
+
attr_reader :from_table, :to_table, :foreign_key, :relationship_type, :label, :fk_table, :fk_column, :pk_table, :pk_column, :is_polymorphic, :extra_label
|
6
|
+
|
7
|
+
# Provide aliases for backward compatibility
|
8
|
+
alias from from_table
|
9
|
+
alias to to_table
|
10
|
+
alias symbol relationship_type
|
11
|
+
|
12
|
+
def initialize(from, to, foreign_key, relationship_type, label = nil,
|
13
|
+
fk_table = nil, fk_column = nil, pk_table = nil, pk_column = nil,
|
14
|
+
is_polymorphic = false, extra_label = nil)
|
15
|
+
@from_table = from
|
16
|
+
@to_table = to
|
17
|
+
@foreign_key = foreign_key
|
18
|
+
@relationship_type = relationship_type
|
19
|
+
@is_polymorphic = is_polymorphic
|
20
|
+
@extra_label = extra_label
|
21
|
+
|
22
|
+
# Store FK/PK information
|
23
|
+
@fk_table = fk_table || from
|
24
|
+
@fk_column = fk_column || foreign_key
|
25
|
+
@pk_table = pk_table || to
|
26
|
+
@pk_column = pk_column || "id"
|
27
|
+
|
28
|
+
# Generate label if not provided
|
29
|
+
@label = label || generate_label
|
30
|
+
end
|
31
|
+
|
32
|
+
def key
|
33
|
+
[from_table, to_table, foreign_key].sort.join("::")
|
34
|
+
end
|
35
|
+
|
36
|
+
def generate_label
|
37
|
+
base_label = "#{@fk_table}.#{@fk_column} FK → #{@pk_table}.#{@pk_column} PK"
|
38
|
+
@is_polymorphic ? "#{base_label} (#{@extra_label})" : base_label
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
require_relative "../relationship"
|
5
|
+
|
6
|
+
module MermaidRailsErd
|
7
|
+
module RelationshipBuilders
|
8
|
+
class BaseRelationshipBuilder
|
9
|
+
attr_reader :symbol_mapper, :association_resolver, :model_data_collector
|
10
|
+
|
11
|
+
# Class variable to track one-to-one relationships across builder instances
|
12
|
+
@@one_to_one_keys = Set.new
|
13
|
+
|
14
|
+
def initialize(symbol_mapper:, association_resolver:, model_data_collector: nil)
|
15
|
+
@symbol_mapper = symbol_mapper
|
16
|
+
@association_resolver = association_resolver
|
17
|
+
@model_data_collector = model_data_collector
|
18
|
+
end
|
19
|
+
|
20
|
+
def build(model, assoc)
|
21
|
+
raise NotImplementedError, "Subclasses must implement #build"
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def resolve_association_model(model, assoc)
|
27
|
+
# Let the association_resolver handle all the resolution logic
|
28
|
+
# including dynamic model creation when needed
|
29
|
+
association_resolver.resolve(assoc)
|
30
|
+
rescue StandardError => e
|
31
|
+
puts " ERROR resolving association class for #{model.name}##{assoc.name}: #{e.class} - #{e.message}"
|
32
|
+
puts e.backtrace.join("\n")
|
33
|
+
# Register invalid association if model_data_collector is available
|
34
|
+
register_invalid_association(model, assoc, "#{e.class} - #{e.message}")
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def safe_foreign_key(model, assoc)
|
39
|
+
# Skip through associations entirely
|
40
|
+
return nil if assoc.options[:through]
|
41
|
+
|
42
|
+
# Try to get the foreign key
|
43
|
+
begin
|
44
|
+
assoc.foreign_key
|
45
|
+
rescue StandardError => e
|
46
|
+
puts " WARNING: Cannot determine foreign key for #{model.name}##{assoc.name} - skipping"
|
47
|
+
register_invalid_association(model, assoc, "Cannot determine foreign key: #{e.class} - #{e.message}")
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def skip_duplicate_one_to_one?(model, assoc, to_table_info)
|
53
|
+
return false unless %i[has_one belongs_to].include?(assoc.macro)
|
54
|
+
|
55
|
+
# Skip the check for polymorphic associations
|
56
|
+
return false if assoc.options[:polymorphic]
|
57
|
+
|
58
|
+
return false unless to_table_info
|
59
|
+
|
60
|
+
# Create a unique key for this one-to-one relationship
|
61
|
+
rel_key = [model.table_name, to_table_info[:table_name], "1:1"].sort.join("::")
|
62
|
+
|
63
|
+
# Check if we've already processed this relationship
|
64
|
+
return true if @@one_to_one_keys.include?(rel_key)
|
65
|
+
|
66
|
+
# Mark this relationship as processed
|
67
|
+
@@one_to_one_keys << rel_key
|
68
|
+
false
|
69
|
+
end
|
70
|
+
|
71
|
+
def log_missing_table_warning(model, assoc, reason = "table does not exist")
|
72
|
+
if assoc.options[:class_name]
|
73
|
+
assoc.options[:class_name].to_s.tableize
|
74
|
+
else
|
75
|
+
assoc.name.to_s.tableize
|
76
|
+
end
|
77
|
+
puts " WARNING: Could not create relationship for #{model.name}##{assoc.name} - #{reason}"
|
78
|
+
register_invalid_association(model, assoc, reason)
|
79
|
+
[]
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def register_invalid_association(model, assoc, reason)
|
85
|
+
return unless @model_data_collector
|
86
|
+
|
87
|
+
@model_data_collector.register_invalid_association(model, assoc, reason)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_relationship_builder"
|
4
|
+
|
5
|
+
module MermaidRailsErd
|
6
|
+
module RelationshipBuilders
|
7
|
+
class BelongsToRelationshipBuilder < BaseRelationshipBuilder
|
8
|
+
def build(model, assoc)
|
9
|
+
from_table = model.table_name
|
10
|
+
fk = safe_foreign_key(model, assoc)
|
11
|
+
|
12
|
+
# Skip if we couldn't determine the foreign key
|
13
|
+
return [] unless fk
|
14
|
+
|
15
|
+
to_table_info = resolve_association_model(model, assoc)
|
16
|
+
|
17
|
+
# Skip if this is a duplicate one-to-one relationship
|
18
|
+
return [] if skip_duplicate_one_to_one?(model, assoc, to_table_info)
|
19
|
+
|
20
|
+
if to_table_info
|
21
|
+
# FK is on source table for belongs_to
|
22
|
+
[Relationship.new(
|
23
|
+
from_table,
|
24
|
+
to_table_info[:table_name],
|
25
|
+
fk,
|
26
|
+
"}o--||",
|
27
|
+
nil, # Let the Relationship generate the label
|
28
|
+
from_table, # fk_table
|
29
|
+
fk, # fk_column
|
30
|
+
to_table_info[:table_name], # pk_table
|
31
|
+
to_table_info[:primary_key], # pk_column
|
32
|
+
)]
|
33
|
+
else
|
34
|
+
log_missing_table_warning(model, assoc)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_relationship_builder"
|
4
|
+
require_relative "../relationship"
|
5
|
+
|
6
|
+
module MermaidRailsErd
|
7
|
+
module RelationshipBuilders
|
8
|
+
class HasAndBelongsToManyRelationshipBuilder < BaseRelationshipBuilder
|
9
|
+
def initialize(symbol_mapper:, association_resolver:, printed_tables: Set.new, model_data_collector: nil)
|
10
|
+
super(symbol_mapper: symbol_mapper, association_resolver: association_resolver, model_data_collector: model_data_collector)
|
11
|
+
@printed_tables = printed_tables
|
12
|
+
end
|
13
|
+
|
14
|
+
def build(model, assoc)
|
15
|
+
from_table = model.table_name
|
16
|
+
to_table_info = resolve_association_model(model, assoc)
|
17
|
+
|
18
|
+
return log_missing_table_warning(model, assoc, "target model does not exist") unless to_table_info
|
19
|
+
|
20
|
+
join_table = assoc.join_table.to_s
|
21
|
+
|
22
|
+
# Check if we need to verify the table existence
|
23
|
+
unless @printed_tables.include?(join_table)
|
24
|
+
begin
|
25
|
+
ActiveRecord::Base.connection.columns(join_table)
|
26
|
+
@printed_tables << join_table
|
27
|
+
rescue StandardError => e
|
28
|
+
return log_missing_table_warning(model, assoc, "join table #{join_table} is missing: #{e.message}")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Try to get the foreign keys
|
33
|
+
source_fk = nil
|
34
|
+
target_fk = nil
|
35
|
+
|
36
|
+
begin
|
37
|
+
source_fk = assoc.foreign_key
|
38
|
+
rescue StandardError => e
|
39
|
+
puts " WARNING: Could not determine foreign key for #{model.name} in HABTM: #{e.message}"
|
40
|
+
register_invalid_association(model, assoc, "Could not determine foreign key: #{e.message}")
|
41
|
+
end
|
42
|
+
|
43
|
+
begin
|
44
|
+
target_fk = assoc.association_foreign_key
|
45
|
+
rescue StandardError => e
|
46
|
+
puts " WARNING: Could not determine association foreign key for #{model.name}##{assoc.name}: #{e.message}"
|
47
|
+
register_invalid_association(model, assoc, "Could not determine association foreign key: #{e.message}")
|
48
|
+
end
|
49
|
+
|
50
|
+
# Skip if we couldn't determine both foreign keys
|
51
|
+
return log_missing_table_warning(model, assoc, "could not determine foreign keys") if !source_fk || !target_fk
|
52
|
+
|
53
|
+
# If we reach here, the join table exists, so create relationships
|
54
|
+
[
|
55
|
+
Relationship.new(
|
56
|
+
join_table,
|
57
|
+
from_table,
|
58
|
+
source_fk,
|
59
|
+
"}o--||",
|
60
|
+
nil, # Let the Relationship generate the label
|
61
|
+
join_table, # fk_table
|
62
|
+
source_fk, # fk_column
|
63
|
+
from_table, # pk_table
|
64
|
+
model.primary_key, # pk_column
|
65
|
+
),
|
66
|
+
Relationship.new(
|
67
|
+
join_table,
|
68
|
+
to_table_info[:table_name],
|
69
|
+
target_fk,
|
70
|
+
"}o--||",
|
71
|
+
nil, # Let the Relationship generate the label
|
72
|
+
join_table, # fk_table
|
73
|
+
target_fk, # fk_column
|
74
|
+
to_table_info[:table_name], # pk_table
|
75
|
+
to_table_info[:primary_key], # pk_column
|
76
|
+
),
|
77
|
+
]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_relationship_builder"
|
4
|
+
|
5
|
+
module MermaidRailsErd
|
6
|
+
module RelationshipBuilders
|
7
|
+
class HasManyRelationshipBuilder < BaseRelationshipBuilder
|
8
|
+
def build(model, assoc)
|
9
|
+
from_table = model.table_name
|
10
|
+
fk = safe_foreign_key(model, assoc)
|
11
|
+
|
12
|
+
# Skip if we couldn't determine the foreign key
|
13
|
+
return [] unless fk
|
14
|
+
|
15
|
+
to_table_info = resolve_association_model(model, assoc)
|
16
|
+
|
17
|
+
if to_table_info
|
18
|
+
# FK is on target table for has_many
|
19
|
+
[Relationship.new(
|
20
|
+
to_table_info[:table_name],
|
21
|
+
from_table,
|
22
|
+
fk,
|
23
|
+
"}o--||",
|
24
|
+
nil, # Let the Relationship generate the label
|
25
|
+
to_table_info[:table_name], # fk_table
|
26
|
+
fk, # fk_column
|
27
|
+
from_table, # pk_table
|
28
|
+
model.primary_key, # pk_column
|
29
|
+
)]
|
30
|
+
else
|
31
|
+
log_missing_table_warning(model, assoc)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_relationship_builder"
|
4
|
+
|
5
|
+
module MermaidRailsErd
|
6
|
+
module RelationshipBuilders
|
7
|
+
class HasOneRelationshipBuilder < BaseRelationshipBuilder
|
8
|
+
def build(model, assoc)
|
9
|
+
from_table = model.table_name
|
10
|
+
rel_type = symbol_mapper.map(assoc.macro)
|
11
|
+
fk = safe_foreign_key(model, assoc)
|
12
|
+
|
13
|
+
# Skip if we couldn't determine the foreign key
|
14
|
+
return [] unless fk
|
15
|
+
|
16
|
+
to_table_info = resolve_association_model(model, assoc)
|
17
|
+
|
18
|
+
# Skip if this is a duplicate one-to-one relationship
|
19
|
+
return [] if skip_duplicate_one_to_one?(model, assoc, to_table_info)
|
20
|
+
|
21
|
+
if to_table_info
|
22
|
+
[Relationship.new(
|
23
|
+
from_table,
|
24
|
+
to_table_info[:table_name],
|
25
|
+
fk,
|
26
|
+
rel_type,
|
27
|
+
nil, # Let the Relationship generate the label
|
28
|
+
to_table_info[:table_name], # fk_table
|
29
|
+
fk, # fk_column
|
30
|
+
from_table, # pk_table
|
31
|
+
model.primary_key, # pk_column
|
32
|
+
)]
|
33
|
+
else
|
34
|
+
log_missing_table_warning(model, assoc)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "relationship_builders/belongs_to_relationship_builder"
|
4
|
+
require_relative "relationship_builders/has_many_relationship_builder"
|
5
|
+
require_relative "relationship_builders/has_one_relationship_builder"
|
6
|
+
require_relative "relationship_builders/habtm_relationship_builder"
|
7
|
+
require_relative "model_data_collector"
|
8
|
+
|
9
|
+
module MermaidRailsErd
|
10
|
+
class RelationshipRegistry
|
11
|
+
attr_reader :builders, :polymorphic_resolver, :model_data_collector
|
12
|
+
|
13
|
+
def initialize(
|
14
|
+
symbol_mapper:,
|
15
|
+
association_resolver:,
|
16
|
+
polymorphic_resolver:,
|
17
|
+
model_data_collector:,
|
18
|
+
printed_tables: Set.new
|
19
|
+
)
|
20
|
+
@polymorphic_resolver = polymorphic_resolver
|
21
|
+
@model_data_collector = model_data_collector
|
22
|
+
@printed_tables = printed_tables
|
23
|
+
|
24
|
+
@builders = {
|
25
|
+
belongs_to: RelationshipBuilders::BelongsToRelationshipBuilder.new(
|
26
|
+
symbol_mapper: symbol_mapper,
|
27
|
+
association_resolver: association_resolver,
|
28
|
+
model_data_collector: model_data_collector,
|
29
|
+
),
|
30
|
+
has_many: RelationshipBuilders::HasManyRelationshipBuilder.new(
|
31
|
+
symbol_mapper: symbol_mapper,
|
32
|
+
association_resolver: association_resolver,
|
33
|
+
model_data_collector: model_data_collector,
|
34
|
+
),
|
35
|
+
has_one: RelationshipBuilders::HasOneRelationshipBuilder.new(
|
36
|
+
symbol_mapper: symbol_mapper,
|
37
|
+
association_resolver: association_resolver,
|
38
|
+
model_data_collector: model_data_collector,
|
39
|
+
),
|
40
|
+
has_and_belongs_to_many: RelationshipBuilders::HasAndBelongsToManyRelationshipBuilder.new(
|
41
|
+
symbol_mapper: symbol_mapper,
|
42
|
+
association_resolver: association_resolver,
|
43
|
+
printed_tables: printed_tables,
|
44
|
+
model_data_collector: model_data_collector,
|
45
|
+
),
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def build_relationships(model, assoc)
|
50
|
+
# Check for polymorphic association first
|
51
|
+
if assoc.options[:polymorphic]
|
52
|
+
from_table = model.table_name
|
53
|
+
rel_type = builders[assoc.macro].symbol_mapper.map(assoc.macro)
|
54
|
+
return polymorphic_resolver.resolve(assoc.name.to_s, from_table, rel_type)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Delegate to the appropriate builder
|
58
|
+
builder = builders[assoc.macro]
|
59
|
+
return builder.build(model, assoc) if builder
|
60
|
+
|
61
|
+
# If no builder exists for this macro type, return an empty array
|
62
|
+
[]
|
63
|
+
end
|
64
|
+
|
65
|
+
def build_all_relationships
|
66
|
+
relationships = []
|
67
|
+
|
68
|
+
# Process polymorphic associations first
|
69
|
+
@model_data_collector.polymorphic_associations.each do |data|
|
70
|
+
model = data[:model]
|
71
|
+
assoc = data[:association]
|
72
|
+
relationships.concat(build_relationships(model, assoc))
|
73
|
+
end
|
74
|
+
|
75
|
+
# Then process regular associations
|
76
|
+
@model_data_collector.regular_associations.each do |data|
|
77
|
+
model = data[:model]
|
78
|
+
assoc = data[:association]
|
79
|
+
relationships.concat(build_relationships(model, assoc))
|
80
|
+
end
|
81
|
+
|
82
|
+
relationships
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|