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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +81 -210
  3. data/app/assets/config/dbwatcher_manifest.js +15 -0
  4. data/app/assets/javascripts/dbwatcher/alpine_registrations.js +39 -0
  5. data/app/assets/javascripts/dbwatcher/auto_init.js +23 -0
  6. data/app/assets/javascripts/dbwatcher/components/base.js +141 -0
  7. data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +1008 -0
  8. data/app/assets/javascripts/dbwatcher/components/diagrams.js +449 -0
  9. data/app/assets/javascripts/dbwatcher/components/summary.js +234 -0
  10. data/app/assets/javascripts/dbwatcher/core/alpine_store.js +138 -0
  11. data/app/assets/javascripts/dbwatcher/core/api_client.js +162 -0
  12. data/app/assets/javascripts/dbwatcher/core/component_loader.js +70 -0
  13. data/app/assets/javascripts/dbwatcher/core/component_registry.js +94 -0
  14. data/app/assets/javascripts/dbwatcher/dbwatcher.js +120 -0
  15. data/app/assets/javascripts/dbwatcher/services/mermaid.js +315 -0
  16. data/app/assets/javascripts/dbwatcher/services/mermaid_service.js +199 -0
  17. data/app/assets/javascripts/dbwatcher/vendor/date-fns-browser.js +99 -0
  18. data/app/assets/javascripts/dbwatcher/vendor/lodash.min.js +140 -0
  19. data/app/assets/javascripts/dbwatcher/vendor/tabulator.min.js +3 -0
  20. data/app/assets/stylesheets/dbwatcher/application.css +423 -0
  21. data/app/assets/stylesheets/dbwatcher/application.scss +15 -0
  22. data/app/assets/stylesheets/dbwatcher/components/_badges.scss +38 -0
  23. data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +162 -0
  24. data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +51 -0
  25. data/app/assets/stylesheets/dbwatcher/components/_forms.scss +27 -0
  26. data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +55 -0
  27. data/app/assets/stylesheets/dbwatcher/core/_base.scss +34 -0
  28. data/app/assets/stylesheets/dbwatcher/core/_variables.scss +47 -0
  29. data/app/assets/stylesheets/dbwatcher/vendor/tabulator.min.css +2 -0
  30. data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +64 -0
  31. data/app/controllers/dbwatcher/base_controller.rb +8 -2
  32. data/app/controllers/dbwatcher/dashboard_controller.rb +8 -0
  33. data/app/controllers/dbwatcher/sessions_controller.rb +25 -10
  34. data/app/helpers/dbwatcher/component_helper.rb +29 -0
  35. data/app/helpers/dbwatcher/diagram_helper.rb +110 -0
  36. data/app/helpers/dbwatcher/session_helper.rb +3 -2
  37. data/app/views/dbwatcher/sessions/_changes_tab.html.erb +265 -0
  38. data/app/views/dbwatcher/sessions/_diagrams_tab.html.erb +166 -0
  39. data/app/views/dbwatcher/sessions/_session_header.html.erb +11 -0
  40. data/app/views/dbwatcher/sessions/_summary_tab.html.erb +88 -0
  41. data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +12 -0
  42. data/app/views/dbwatcher/sessions/changes.html.erb +21 -0
  43. data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +44 -0
  44. data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +96 -0
  45. data/app/views/dbwatcher/sessions/diagrams.html.erb +21 -0
  46. data/app/views/dbwatcher/sessions/index.html.erb +14 -10
  47. data/app/views/dbwatcher/sessions/shared/_layout.html.erb +8 -0
  48. data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +35 -0
  49. data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +25 -0
  50. data/app/views/dbwatcher/sessions/show.html.erb +3 -346
  51. data/app/views/dbwatcher/sessions/summary.html.erb +21 -0
  52. data/app/views/layouts/dbwatcher/application.html.erb +125 -247
  53. data/bin/compile_scss +49 -0
  54. data/config/routes.rb +26 -0
  55. data/lib/dbwatcher/configuration.rb +102 -8
  56. data/lib/dbwatcher/engine.rb +17 -7
  57. data/lib/dbwatcher/services/analyzers/session_data_processor.rb +98 -0
  58. data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +202 -0
  59. data/lib/dbwatcher/services/api/base_api_service.rb +100 -0
  60. data/lib/dbwatcher/services/api/changes_data_service.rb +112 -0
  61. data/lib/dbwatcher/services/api/diagram_data_service.rb +145 -0
  62. data/lib/dbwatcher/services/api/summary_data_service.rb +158 -0
  63. data/lib/dbwatcher/services/base_service.rb +64 -0
  64. data/lib/dbwatcher/services/diagram_analyzers/base_analyzer.rb +162 -0
  65. data/lib/dbwatcher/services/diagram_analyzers/foreign_key_analyzer.rb +354 -0
  66. data/lib/dbwatcher/services/diagram_analyzers/inferred_relationship_analyzer.rb +502 -0
  67. data/lib/dbwatcher/services/diagram_analyzers/model_association_analyzer.rb +564 -0
  68. data/lib/dbwatcher/services/diagram_data/attribute.rb +154 -0
  69. data/lib/dbwatcher/services/diagram_data/dataset.rb +278 -0
  70. data/lib/dbwatcher/services/diagram_data/entity.rb +180 -0
  71. data/lib/dbwatcher/services/diagram_data/relationship.rb +188 -0
  72. data/lib/dbwatcher/services/diagram_data/relationship_params.rb +55 -0
  73. data/lib/dbwatcher/services/diagram_data.rb +65 -0
  74. data/lib/dbwatcher/services/diagram_error_handler.rb +239 -0
  75. data/lib/dbwatcher/services/diagram_generator.rb +154 -0
  76. data/lib/dbwatcher/services/diagram_strategies/base_diagram_strategy.rb +149 -0
  77. data/lib/dbwatcher/services/diagram_strategies/class_diagram_strategy.rb +49 -0
  78. data/lib/dbwatcher/services/diagram_strategies/erd_diagram_strategy.rb +52 -0
  79. data/lib/dbwatcher/services/diagram_strategies/flowchart_diagram_strategy.rb +52 -0
  80. data/lib/dbwatcher/services/diagram_system.rb +69 -0
  81. data/lib/dbwatcher/services/diagram_type_registry.rb +164 -0
  82. data/lib/dbwatcher/services/mermaid_syntax/base_builder.rb +127 -0
  83. data/lib/dbwatcher/services/mermaid_syntax/cardinality_mapper.rb +90 -0
  84. data/lib/dbwatcher/services/mermaid_syntax/class_diagram_builder.rb +136 -0
  85. data/lib/dbwatcher/services/mermaid_syntax/class_diagram_helper.rb +46 -0
  86. data/lib/dbwatcher/services/mermaid_syntax/erd_builder.rb +116 -0
  87. data/lib/dbwatcher/services/mermaid_syntax/flowchart_builder.rb +109 -0
  88. data/lib/dbwatcher/services/mermaid_syntax/sanitizer.rb +102 -0
  89. data/lib/dbwatcher/services/mermaid_syntax_builder.rb +155 -0
  90. data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +15 -128
  91. data/lib/dbwatcher/storage/api/session_api.rb +47 -0
  92. data/lib/dbwatcher/storage/base_storage.rb +7 -0
  93. data/lib/dbwatcher/version.rb +1 -1
  94. data/lib/dbwatcher.rb +58 -1
  95. 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