dbwatcher 1.0.0 → 1.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 +4 -4
- data/README.md +81 -210
- data/app/assets/config/dbwatcher_manifest.js +15 -0
- data/app/assets/javascripts/dbwatcher/alpine_registrations.js +39 -0
- data/app/assets/javascripts/dbwatcher/auto_init.js +23 -0
- data/app/assets/javascripts/dbwatcher/components/base.js +141 -0
- data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +1008 -0
- data/app/assets/javascripts/dbwatcher/components/diagrams.js +449 -0
- data/app/assets/javascripts/dbwatcher/components/summary.js +234 -0
- data/app/assets/javascripts/dbwatcher/core/alpine_store.js +138 -0
- data/app/assets/javascripts/dbwatcher/core/api_client.js +162 -0
- data/app/assets/javascripts/dbwatcher/core/component_loader.js +70 -0
- data/app/assets/javascripts/dbwatcher/core/component_registry.js +94 -0
- data/app/assets/javascripts/dbwatcher/dbwatcher.js +120 -0
- data/app/assets/javascripts/dbwatcher/services/mermaid.js +315 -0
- data/app/assets/javascripts/dbwatcher/services/mermaid_service.js +199 -0
- data/app/assets/javascripts/dbwatcher/vendor/date-fns-browser.js +99 -0
- data/app/assets/javascripts/dbwatcher/vendor/lodash.min.js +140 -0
- data/app/assets/javascripts/dbwatcher/vendor/tabulator.min.js +3 -0
- data/app/assets/stylesheets/dbwatcher/application.css +423 -0
- data/app/assets/stylesheets/dbwatcher/application.scss +15 -0
- data/app/assets/stylesheets/dbwatcher/components/_badges.scss +38 -0
- data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +162 -0
- data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +51 -0
- data/app/assets/stylesheets/dbwatcher/components/_forms.scss +27 -0
- data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +55 -0
- data/app/assets/stylesheets/dbwatcher/core/_base.scss +34 -0
- data/app/assets/stylesheets/dbwatcher/core/_variables.scss +47 -0
- data/app/assets/stylesheets/dbwatcher/vendor/tabulator.min.css +2 -0
- data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +64 -0
- data/app/controllers/dbwatcher/base_controller.rb +8 -2
- data/app/controllers/dbwatcher/dashboard_controller.rb +8 -0
- data/app/controllers/dbwatcher/sessions_controller.rb +25 -10
- data/app/helpers/dbwatcher/component_helper.rb +29 -0
- data/app/helpers/dbwatcher/diagram_helper.rb +110 -0
- data/app/helpers/dbwatcher/session_helper.rb +3 -2
- data/app/views/dbwatcher/sessions/_changes_tab.html.erb +265 -0
- data/app/views/dbwatcher/sessions/_diagrams_tab.html.erb +166 -0
- data/app/views/dbwatcher/sessions/_session_header.html.erb +11 -0
- data/app/views/dbwatcher/sessions/_summary_tab.html.erb +88 -0
- data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +12 -0
- data/app/views/dbwatcher/sessions/changes.html.erb +21 -0
- data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +44 -0
- data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +96 -0
- data/app/views/dbwatcher/sessions/diagrams.html.erb +21 -0
- data/app/views/dbwatcher/sessions/index.html.erb +14 -10
- data/app/views/dbwatcher/sessions/shared/_layout.html.erb +8 -0
- data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +35 -0
- data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +25 -0
- data/app/views/dbwatcher/sessions/show.html.erb +3 -346
- data/app/views/dbwatcher/sessions/summary.html.erb +21 -0
- data/app/views/layouts/dbwatcher/application.html.erb +125 -247
- data/bin/compile_scss +49 -0
- data/config/routes.rb +26 -0
- data/lib/dbwatcher/configuration.rb +102 -8
- data/lib/dbwatcher/engine.rb +17 -7
- data/lib/dbwatcher/services/analyzers/session_data_processor.rb +98 -0
- data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +202 -0
- data/lib/dbwatcher/services/api/base_api_service.rb +100 -0
- data/lib/dbwatcher/services/api/changes_data_service.rb +112 -0
- data/lib/dbwatcher/services/api/diagram_data_service.rb +145 -0
- data/lib/dbwatcher/services/api/summary_data_service.rb +158 -0
- data/lib/dbwatcher/services/base_service.rb +64 -0
- data/lib/dbwatcher/services/diagram_analyzers/base_analyzer.rb +162 -0
- data/lib/dbwatcher/services/diagram_analyzers/foreign_key_analyzer.rb +354 -0
- data/lib/dbwatcher/services/diagram_analyzers/inferred_relationship_analyzer.rb +502 -0
- data/lib/dbwatcher/services/diagram_analyzers/model_association_analyzer.rb +564 -0
- data/lib/dbwatcher/services/diagram_data/attribute.rb +154 -0
- data/lib/dbwatcher/services/diagram_data/dataset.rb +278 -0
- data/lib/dbwatcher/services/diagram_data/entity.rb +180 -0
- data/lib/dbwatcher/services/diagram_data/relationship.rb +188 -0
- data/lib/dbwatcher/services/diagram_data/relationship_params.rb +55 -0
- data/lib/dbwatcher/services/diagram_data.rb +65 -0
- data/lib/dbwatcher/services/diagram_error_handler.rb +239 -0
- data/lib/dbwatcher/services/diagram_generator.rb +154 -0
- data/lib/dbwatcher/services/diagram_strategies/base_diagram_strategy.rb +149 -0
- data/lib/dbwatcher/services/diagram_strategies/class_diagram_strategy.rb +49 -0
- data/lib/dbwatcher/services/diagram_strategies/erd_diagram_strategy.rb +52 -0
- data/lib/dbwatcher/services/diagram_strategies/flowchart_diagram_strategy.rb +52 -0
- data/lib/dbwatcher/services/diagram_system.rb +69 -0
- data/lib/dbwatcher/services/diagram_type_registry.rb +164 -0
- data/lib/dbwatcher/services/mermaid_syntax/base_builder.rb +127 -0
- data/lib/dbwatcher/services/mermaid_syntax/cardinality_mapper.rb +90 -0
- data/lib/dbwatcher/services/mermaid_syntax/class_diagram_builder.rb +136 -0
- data/lib/dbwatcher/services/mermaid_syntax/class_diagram_helper.rb +46 -0
- data/lib/dbwatcher/services/mermaid_syntax/erd_builder.rb +116 -0
- data/lib/dbwatcher/services/mermaid_syntax/flowchart_builder.rb +109 -0
- data/lib/dbwatcher/services/mermaid_syntax/sanitizer.rb +102 -0
- data/lib/dbwatcher/services/mermaid_syntax_builder.rb +155 -0
- data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +15 -128
- data/lib/dbwatcher/storage/api/session_api.rb +47 -0
- data/lib/dbwatcher/storage/base_storage.rb +7 -0
- data/lib/dbwatcher/version.rb +1 -1
- data/lib/dbwatcher.rb +58 -1
- metadata +94 -2
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Services
|
5
|
+
module MermaidSyntax
|
6
|
+
# Maps relationship cardinality to Mermaid syntax
|
7
|
+
#
|
8
|
+
# Provides mapping from internal cardinality representation to
|
9
|
+
# specific Mermaid diagram syntax for different diagram types.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# CardinalityMapper.to_erd("one_to_many") # => "||--o{"
|
13
|
+
# CardinalityMapper.to_class("one_to_many") # => "1..*"
|
14
|
+
# CardinalityMapper.to_simple("one_to_many") # => "1:N"
|
15
|
+
class CardinalityMapper
|
16
|
+
class << self
|
17
|
+
# Default cardinality notation
|
18
|
+
DEFAULT_ERD = "||--o{" # Default to one-to-many
|
19
|
+
DEFAULT_CLASS = "1..*" # Default to one-to-many
|
20
|
+
DEFAULT_SIMPLE = "1:N" # Default to one-to-many
|
21
|
+
|
22
|
+
# Lookup tables for different cardinality notations
|
23
|
+
ERD_NOTATIONS = {
|
24
|
+
"one_to_many" => "||--o{",
|
25
|
+
"one_to_zero_or_many" => "||--o{",
|
26
|
+
"many_to_one" => "}o--||",
|
27
|
+
"one_to_one" => "||--||",
|
28
|
+
"many_to_many" => "}o--o{",
|
29
|
+
"zero_or_one_to_many" => "|o--o{",
|
30
|
+
"zero_or_one_to_one" => "|o--||",
|
31
|
+
"one_to_zero_or_one" => "||--|o"
|
32
|
+
}.freeze
|
33
|
+
|
34
|
+
CLASS_NOTATIONS = {
|
35
|
+
"one_to_many" => "1..*",
|
36
|
+
"many_to_one" => "*..*",
|
37
|
+
"many_to_many" => "*..*",
|
38
|
+
"one_to_one" => "1..1",
|
39
|
+
"zero_or_one_to_many" => "0..1..*",
|
40
|
+
"one_to_zero_or_many" => "1..0..*",
|
41
|
+
"zero_or_one_to_one" => "0..1..1",
|
42
|
+
"one_to_zero_or_one" => "1..0..1"
|
43
|
+
}.freeze
|
44
|
+
|
45
|
+
SIMPLE_NOTATIONS = {
|
46
|
+
"one_to_many" => "1:N",
|
47
|
+
"many_to_one" => "N:1",
|
48
|
+
"one_to_one" => "1:1",
|
49
|
+
"many_to_many" => "N:N",
|
50
|
+
"zero_or_one_to_many" => "0,1:N",
|
51
|
+
"one_to_zero_or_many" => "1:0,N",
|
52
|
+
"zero_or_one_to_one" => "0,1:1",
|
53
|
+
"one_to_zero_or_one" => "1:0,1"
|
54
|
+
}.freeze
|
55
|
+
|
56
|
+
# Map cardinality to ERD diagram syntax
|
57
|
+
#
|
58
|
+
# @param cardinality [String, Symbol] internal cardinality representation
|
59
|
+
# @param _format [Symbol] cardinality format (not used for ERD)
|
60
|
+
# @return [String] ERD cardinality notation
|
61
|
+
def to_erd(cardinality, _format = :standard)
|
62
|
+
# ERD format doesn't change based on format parameter
|
63
|
+
# It always uses the standard Mermaid ERD notation
|
64
|
+
ERD_NOTATIONS[cardinality.to_s] || DEFAULT_ERD
|
65
|
+
end
|
66
|
+
|
67
|
+
# Map cardinality to class diagram syntax
|
68
|
+
#
|
69
|
+
# @param cardinality [String, Symbol] internal cardinality representation
|
70
|
+
# @param format [Symbol] cardinality format (:standard or :simple)
|
71
|
+
# @return [String] class diagram cardinality notation
|
72
|
+
def to_class(cardinality, format = :standard)
|
73
|
+
return to_simple(cardinality) if format == :simple
|
74
|
+
|
75
|
+
CLASS_NOTATIONS[cardinality.to_s] || DEFAULT_CLASS
|
76
|
+
end
|
77
|
+
|
78
|
+
# Map cardinality to simple text format
|
79
|
+
#
|
80
|
+
# @param cardinality [String, Symbol] internal cardinality representation
|
81
|
+
# @param _format [Symbol] cardinality format (not used for simple format)
|
82
|
+
# @return [String] simple cardinality notation
|
83
|
+
def to_simple(cardinality, _format = :standard)
|
84
|
+
SIMPLE_NOTATIONS[cardinality.to_s] || DEFAULT_SIMPLE
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "class_diagram_helper"
|
4
|
+
|
5
|
+
module Dbwatcher
|
6
|
+
module Services
|
7
|
+
module MermaidSyntax
|
8
|
+
# Builder for Class Diagrams in Mermaid syntax
|
9
|
+
#
|
10
|
+
# Generates Mermaid class diagram syntax from a standardized dataset with
|
11
|
+
# support for attributes, methods, and relationships with labels.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# builder = ClassDiagramBuilder.new(show_methods: true)
|
15
|
+
# content = builder.build_from_dataset(dataset)
|
16
|
+
# # => "classDiagram
|
17
|
+
# # class User {
|
18
|
+
# # +string name
|
19
|
+
# # +orders()
|
20
|
+
# # }
|
21
|
+
# # User --> Order : orders"
|
22
|
+
class ClassDiagramBuilder < BaseBuilder
|
23
|
+
include ClassDiagramHelper
|
24
|
+
|
25
|
+
# Build class diagram content from dataset
|
26
|
+
#
|
27
|
+
# @param dataset [DiagramData::Dataset] dataset to render
|
28
|
+
# @return [String] Mermaid class diagram content
|
29
|
+
def build_from_dataset(dataset)
|
30
|
+
lines = ["classDiagram"]
|
31
|
+
lines << " direction #{diagram_direction}"
|
32
|
+
|
33
|
+
# Add class definitions with attributes and methods
|
34
|
+
dataset.entities.each_value do |entity|
|
35
|
+
lines += build_class_definition(entity)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Add relationships
|
39
|
+
unless dataset.relationships.empty?
|
40
|
+
lines << ""
|
41
|
+
lines << " %% Relationships"
|
42
|
+
dataset.relationships.each do |relationship|
|
43
|
+
lines << build_class_relationship(relationship, dataset)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
lines.join("\n")
|
48
|
+
end
|
49
|
+
|
50
|
+
# Build empty class diagram with message
|
51
|
+
#
|
52
|
+
# @param message [String] message to display
|
53
|
+
# @return [String] Mermaid class diagram content
|
54
|
+
def build_empty(message)
|
55
|
+
sanitized_message = sanitize_text(message)
|
56
|
+
[
|
57
|
+
"classDiagram",
|
58
|
+
" direction #{diagram_direction}",
|
59
|
+
" class EmptyState {",
|
60
|
+
" +string message",
|
61
|
+
" }",
|
62
|
+
" note for EmptyState \"#{sanitized_message}\""
|
63
|
+
].join("\n")
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
# Build attributes section for class definition
|
69
|
+
def build_attributes_section(entity)
|
70
|
+
return [] unless show_attributes? && entity.attributes.any?
|
71
|
+
|
72
|
+
lines = [" %% Attributes"]
|
73
|
+
entity.attributes.first(max_attributes).each do |attr|
|
74
|
+
lines << format_attribute_line(attr)
|
75
|
+
end
|
76
|
+
add_attributes_overflow_message(lines, entity)
|
77
|
+
add_section_divider(lines, entity)
|
78
|
+
lines
|
79
|
+
end
|
80
|
+
|
81
|
+
# Build methods section for class definition
|
82
|
+
def build_methods_section(entity)
|
83
|
+
return [] unless show_methods? && entity.metadata[:methods]&.any?
|
84
|
+
|
85
|
+
lines = [" %% Methods"]
|
86
|
+
entity.metadata[:methods].first(max_methods).each do |method|
|
87
|
+
lines << format_method_line(method)
|
88
|
+
end
|
89
|
+
if entity.metadata[:methods].size > max_methods
|
90
|
+
lines << " %% ... #{entity.metadata[:methods].size - max_methods} more methods"
|
91
|
+
end
|
92
|
+
lines << " %% ----------------------"
|
93
|
+
lines
|
94
|
+
end
|
95
|
+
|
96
|
+
# Build statistics section for class definition
|
97
|
+
def build_statistics_section(entity)
|
98
|
+
return [] unless entity.attributes.any? || entity.metadata[:methods]&.any?
|
99
|
+
|
100
|
+
lines = [" %% Statistics"]
|
101
|
+
lines << " +Stats: #{entity.attributes.size} attributes" if entity.attributes.any?
|
102
|
+
lines << " +Stats: #{entity.metadata[:methods].size} methods" if entity.metadata[:methods]&.any?
|
103
|
+
lines
|
104
|
+
end
|
105
|
+
|
106
|
+
# Build class definition
|
107
|
+
def build_class_definition(entity)
|
108
|
+
class_name = Sanitizer.class_name(entity.name)
|
109
|
+
lines = [" class #{class_name} {"]
|
110
|
+
lines += build_attributes_section(entity)
|
111
|
+
lines += build_methods_section(entity)
|
112
|
+
lines += build_statistics_section(entity)
|
113
|
+
lines << " }"
|
114
|
+
lines << ""
|
115
|
+
lines
|
116
|
+
end
|
117
|
+
|
118
|
+
# Build class relationship
|
119
|
+
def build_class_relationship(relationship, dataset)
|
120
|
+
source = format_class_name(relationship.source_id, dataset)
|
121
|
+
target = format_class_name(relationship.target_id, dataset)
|
122
|
+
label = Sanitizer.label(relationship.label)
|
123
|
+
|
124
|
+
relationship_line = " #{source}"
|
125
|
+
if show_cardinality? && relationship.cardinality
|
126
|
+
cardinality = CardinalityMapper.to_class(relationship.cardinality, cardinality_format)
|
127
|
+
relationship_line += " \"#{cardinality}\""
|
128
|
+
end
|
129
|
+
relationship_line += " --> #{target}"
|
130
|
+
relationship_line += " : #{label}" if label && !label.empty?
|
131
|
+
relationship_line
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Services
|
5
|
+
module MermaidSyntax
|
6
|
+
# Helper module for ClassDiagramBuilder
|
7
|
+
#
|
8
|
+
# Contains formatting methods for class diagram elements
|
9
|
+
module ClassDiagramHelper
|
10
|
+
# Format an attribute for class diagram
|
11
|
+
def format_attribute_line(attr)
|
12
|
+
visibility = attr.metadata[:visibility] || "+"
|
13
|
+
type = attr.type.to_s.empty? ? "any" : attr.type
|
14
|
+
" #{visibility}#{type} #{attr.name}"
|
15
|
+
end
|
16
|
+
|
17
|
+
# Add overflow message for attributes if needed
|
18
|
+
def add_attributes_overflow_message(lines, entity)
|
19
|
+
return unless entity.attributes.size > max_attributes
|
20
|
+
|
21
|
+
lines << " %% ... #{entity.attributes.size - max_attributes} more attributes"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Add section divider if methods will follow
|
25
|
+
def add_section_divider(lines, entity)
|
26
|
+
return unless show_methods? && entity.metadata[:methods]&.any?
|
27
|
+
|
28
|
+
lines << " %% ----------------------"
|
29
|
+
end
|
30
|
+
|
31
|
+
# Format a method for class diagram
|
32
|
+
def format_method_line(method)
|
33
|
+
visibility = method[:visibility] || "+"
|
34
|
+
method_name = Sanitizer.method_name(method[:name])
|
35
|
+
" #{visibility}#{method_name}"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Format class name for diagram
|
39
|
+
def format_class_name(entity_id, dataset)
|
40
|
+
entity_name = dataset.get_entity(entity_id)&.name || entity_id
|
41
|
+
Sanitizer.class_name(entity_name)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Services
|
5
|
+
module MermaidSyntax
|
6
|
+
# Builder for Entity Relationship Diagrams in Mermaid syntax
|
7
|
+
#
|
8
|
+
# Generates Mermaid ERD syntax from a standardized dataset with
|
9
|
+
# support for attributes and relationships with cardinality.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# builder = ErdBuilder.new(show_attributes: true)
|
13
|
+
# content = builder.build_from_dataset(dataset)
|
14
|
+
# # => "erDiagram\n USER {\n int id PK\n }\n USER ||--o{ ORDER : \"has_many\""
|
15
|
+
class ErdBuilder < BaseBuilder
|
16
|
+
# Build ERD content from dataset
|
17
|
+
#
|
18
|
+
# @param dataset [DiagramData::Dataset] dataset to render
|
19
|
+
# @return [String] Mermaid ERD content
|
20
|
+
def build_from_dataset(dataset)
|
21
|
+
lines = ["erDiagram"]
|
22
|
+
|
23
|
+
# Add entity definitions with attributes
|
24
|
+
dataset.entities.each_value do |entity|
|
25
|
+
lines += build_erd_entity(entity)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Add relationships
|
29
|
+
unless dataset.relationships.empty?
|
30
|
+
lines << ""
|
31
|
+
dataset.relationships.each do |relationship|
|
32
|
+
lines << build_erd_relationship(relationship, dataset)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
lines.join("\n")
|
37
|
+
end
|
38
|
+
|
39
|
+
# Build empty ERD with message
|
40
|
+
#
|
41
|
+
# @param message [String] message to display
|
42
|
+
# @return [String] Mermaid ERD content
|
43
|
+
def build_empty(message)
|
44
|
+
sanitized_message = sanitize_text(message)
|
45
|
+
[
|
46
|
+
"erDiagram",
|
47
|
+
" EMPTY_STATE {",
|
48
|
+
" string message \"#{sanitized_message}\"",
|
49
|
+
" }"
|
50
|
+
].join("\n")
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# Format attribute definition for ERD
|
56
|
+
#
|
57
|
+
# @param attr [DiagramData::Attribute] attribute to format
|
58
|
+
# @return [String] formatted attribute line
|
59
|
+
def format_attribute_line(attr)
|
60
|
+
type = attr.type.to_s.empty? ? "any" : attr.type
|
61
|
+
key_suffix = ""
|
62
|
+
key_suffix += " PK" if attr.primary_key?
|
63
|
+
key_suffix += " FK" if attr.foreign_key? && !attr.primary_key?
|
64
|
+
|
65
|
+
" #{type} #{attr.name}#{key_suffix}"
|
66
|
+
end
|
67
|
+
|
68
|
+
# Build entity definition
|
69
|
+
#
|
70
|
+
# @param entity [DiagramData::Entity] entity to render
|
71
|
+
# @return [Array<String>] entity definition lines
|
72
|
+
def build_erd_entity(entity)
|
73
|
+
table_name = Sanitizer.table_name(entity.name, preserve_table_case?)
|
74
|
+
lines = [" #{table_name} {"]
|
75
|
+
|
76
|
+
# Add attributes if enabled and available
|
77
|
+
if show_attributes? && entity.attributes.any?
|
78
|
+
entity.attributes.first(max_attributes).each do |attr|
|
79
|
+
lines << format_attribute_line(attr)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
lines << " }"
|
84
|
+
lines << ""
|
85
|
+
lines
|
86
|
+
end
|
87
|
+
|
88
|
+
# Format entity name for ERD
|
89
|
+
#
|
90
|
+
# @param entity_id [String] entity ID
|
91
|
+
# @param dataset [DiagramData::Dataset] dataset for entity lookup
|
92
|
+
# @return [String] formatted entity name
|
93
|
+
def format_entity_name(entity_id, dataset)
|
94
|
+
entity_name = dataset.get_entity(entity_id)&.name || entity_id
|
95
|
+
Sanitizer.table_name(entity_name, preserve_table_case?)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Build relationship definition
|
99
|
+
#
|
100
|
+
# @param relationship [DiagramData::Relationship] relationship to render
|
101
|
+
# @param dataset [DiagramData::Dataset] full dataset for context
|
102
|
+
# @return [String] relationship definition line
|
103
|
+
def build_erd_relationship(relationship, dataset)
|
104
|
+
source = format_entity_name(relationship.source_id, dataset)
|
105
|
+
target = format_entity_name(relationship.target_id, dataset)
|
106
|
+
|
107
|
+
label = Sanitizer.label(relationship.label)
|
108
|
+
cardinality = CardinalityMapper.to_erd(relationship.cardinality, cardinality_format)
|
109
|
+
|
110
|
+
# In Mermaid ERD syntax, relationship labels must be quoted
|
111
|
+
" #{source} #{cardinality} #{target} : \"#{label}\""
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Services
|
5
|
+
module MermaidSyntax
|
6
|
+
# Builder for Flowchart Diagrams in Mermaid syntax
|
7
|
+
#
|
8
|
+
# Generates Mermaid flowchart syntax from a standardized dataset with
|
9
|
+
# support for nodes with attributes and edges with labels.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# builder = FlowchartBuilder.new(show_attributes: true)
|
13
|
+
# content = builder.build_from_dataset(dataset)
|
14
|
+
# # => "flowchart LR\n User[User<br/>name, email]\n User -->|has_many| Comment"
|
15
|
+
class FlowchartBuilder < BaseBuilder
|
16
|
+
# Build flowchart content from dataset
|
17
|
+
#
|
18
|
+
# @param dataset [DiagramData::Dataset] dataset to render
|
19
|
+
# @return [String] Mermaid flowchart content
|
20
|
+
def build_from_dataset(dataset)
|
21
|
+
lines = ["flowchart #{diagram_direction}"]
|
22
|
+
|
23
|
+
# Add node definitions
|
24
|
+
dataset.entities.each_value do |entity|
|
25
|
+
lines << build_flowchart_node(entity)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Add relationships
|
29
|
+
unless dataset.relationships.empty?
|
30
|
+
lines << ""
|
31
|
+
dataset.relationships.each do |relationship|
|
32
|
+
lines << build_flowchart_relationship(relationship, dataset)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
lines.join("\n")
|
37
|
+
end
|
38
|
+
|
39
|
+
# Build empty flowchart with message
|
40
|
+
#
|
41
|
+
# @param message [String] message to display
|
42
|
+
# @return [String] Mermaid flowchart content
|
43
|
+
def build_empty(message)
|
44
|
+
sanitized_message = sanitize_text(message)
|
45
|
+
[
|
46
|
+
"flowchart #{diagram_direction}",
|
47
|
+
" EmptyState[\"#{sanitized_message}\"]"
|
48
|
+
].join("\n")
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# Build flowchart node
|
54
|
+
#
|
55
|
+
# @param entity [DiagramData::Entity] entity to render
|
56
|
+
# @return [String] node definition line
|
57
|
+
def build_flowchart_node(entity)
|
58
|
+
node_id = Sanitizer.node_id(entity.name)
|
59
|
+
node_content = entity.name
|
60
|
+
|
61
|
+
" #{node_id}[\"#{node_content}\"]"
|
62
|
+
end
|
63
|
+
|
64
|
+
# Format entity name for node ID
|
65
|
+
#
|
66
|
+
# @param entity_id [String] entity ID
|
67
|
+
# @param dataset [DiagramData::Dataset] dataset for entity lookup
|
68
|
+
# @return [String] formatted node ID
|
69
|
+
def format_node_id(entity_id, dataset)
|
70
|
+
entity_name = dataset.get_entity(entity_id)&.name || entity_id
|
71
|
+
Sanitizer.node_id(entity_name)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Format relationship label with optional cardinality
|
75
|
+
#
|
76
|
+
# @param relationship [DiagramData::Relationship] relationship
|
77
|
+
# @return [String] formatted label
|
78
|
+
def format_relationship_label(relationship)
|
79
|
+
label = Sanitizer.label(relationship.label)
|
80
|
+
|
81
|
+
# Add cardinality to label if enabled
|
82
|
+
if show_cardinality? && relationship.cardinality
|
83
|
+
cardinality = CardinalityMapper.to_simple(relationship.cardinality, cardinality_format)
|
84
|
+
label = label.empty? ? cardinality : "#{label} (#{cardinality})"
|
85
|
+
end
|
86
|
+
|
87
|
+
label
|
88
|
+
end
|
89
|
+
|
90
|
+
# Build flowchart relationship
|
91
|
+
#
|
92
|
+
# @param relationship [DiagramData::Relationship] relationship to render
|
93
|
+
# @param dataset [DiagramData::Dataset] full dataset for context
|
94
|
+
# @return [String] relationship definition line
|
95
|
+
def build_flowchart_relationship(relationship, dataset)
|
96
|
+
source = format_node_id(relationship.source_id, dataset)
|
97
|
+
target = format_node_id(relationship.target_id, dataset)
|
98
|
+
label = format_relationship_label(relationship)
|
99
|
+
|
100
|
+
if label && !label.empty?
|
101
|
+
" #{source} -->|\"#{label}\"| #{target}"
|
102
|
+
else
|
103
|
+
" #{source} --> #{target}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Services
|
5
|
+
module MermaidSyntax
|
6
|
+
# Utility class for sanitizing names and labels for Mermaid syntax
|
7
|
+
#
|
8
|
+
# Provides methods to sanitize various types of identifiers for use
|
9
|
+
# in Mermaid diagrams, ensuring valid syntax.
|
10
|
+
class Sanitizer
|
11
|
+
class << self
|
12
|
+
# Sanitize class name for Mermaid class diagrams
|
13
|
+
#
|
14
|
+
# @param name [String] raw class name
|
15
|
+
# @return [String] sanitized class name
|
16
|
+
def class_name(name)
|
17
|
+
return "UnknownClass" unless name.is_a?(String) && !name.empty?
|
18
|
+
|
19
|
+
name.to_s.gsub(/[^a-zA-Z0-9_]/, "_").capitalize
|
20
|
+
end
|
21
|
+
|
22
|
+
# Sanitize node name for Mermaid flowcharts
|
23
|
+
#
|
24
|
+
# @param name [String] raw node name
|
25
|
+
# @return [String] sanitized node name
|
26
|
+
def node_name(name)
|
27
|
+
return "unknown_node" unless name.is_a?(String) && !name.empty?
|
28
|
+
|
29
|
+
name.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
|
30
|
+
end
|
31
|
+
|
32
|
+
# Sanitize node ID for Mermaid flowcharts
|
33
|
+
#
|
34
|
+
# @param name [String] raw node name
|
35
|
+
# @return [String] sanitized node ID
|
36
|
+
def node_id(name)
|
37
|
+
return "unknown_node" unless name.is_a?(String) && !name.empty?
|
38
|
+
|
39
|
+
# Node IDs in flowcharts must be valid identifiers
|
40
|
+
name.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
|
41
|
+
end
|
42
|
+
|
43
|
+
# Sanitize table name for Mermaid ERD
|
44
|
+
#
|
45
|
+
# @param name [String] raw table name
|
46
|
+
# @param preserve_case [Boolean] whether to preserve original case
|
47
|
+
# @return [String] sanitized table name
|
48
|
+
def table_name(name, preserve_case = nil)
|
49
|
+
return "UNKNOWN_TABLE" unless name.is_a?(String) && !name.empty?
|
50
|
+
|
51
|
+
preserve = if preserve_case.nil?
|
52
|
+
Dbwatcher.configuration.diagram_preserve_table_case
|
53
|
+
else
|
54
|
+
preserve_case
|
55
|
+
end
|
56
|
+
|
57
|
+
if preserve
|
58
|
+
name.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
|
59
|
+
else
|
60
|
+
name.to_s.upcase.gsub(/[^A-Z0-9_]/, "_")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Sanitize method name for Mermaid class diagrams
|
65
|
+
#
|
66
|
+
# @param name [String] raw method name
|
67
|
+
# @return [String] sanitized method name
|
68
|
+
def method_name(name)
|
69
|
+
return "unknown_method()" unless name.is_a?(String) && !name.empty?
|
70
|
+
|
71
|
+
# Ensure method name ends with parentheses
|
72
|
+
method = name.to_s.gsub(/[^a-zA-Z0-9_()]/, "_")
|
73
|
+
method += "()" unless method.include?("(")
|
74
|
+
method
|
75
|
+
end
|
76
|
+
|
77
|
+
# Sanitize relationship label for Mermaid diagrams
|
78
|
+
#
|
79
|
+
# @param label [String] raw label
|
80
|
+
# @return [String] sanitized label
|
81
|
+
def label(label)
|
82
|
+
return "" unless label.is_a?(String) && !label.empty?
|
83
|
+
|
84
|
+
# For Mermaid, we need to escape quotes but not remove them completely
|
85
|
+
# We'll escape backslashes and double quotes, and replace newlines with spaces
|
86
|
+
label.to_s.gsub("\\", "\\\\").gsub('"', '\\"').gsub(/[\n\r]/, " ").strip
|
87
|
+
end
|
88
|
+
|
89
|
+
# Sanitize attribute type for Mermaid ERD
|
90
|
+
#
|
91
|
+
# @param type [String] raw attribute type
|
92
|
+
# @return [String] sanitized attribute type
|
93
|
+
def attribute_type(type)
|
94
|
+
return "string" unless type.is_a?(String) && !type.empty?
|
95
|
+
|
96
|
+
type.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|