dbwatcher 1.0.0 → 1.1.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.
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 +603 -0
  68. data/lib/dbwatcher/services/diagram_data/attribute.rb +154 -0
  69. data/lib/dbwatcher/services/diagram_data/dataset.rb +280 -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 +140 -0
  85. data/lib/dbwatcher/services/mermaid_syntax/class_diagram_helper.rb +48 -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 +118 -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,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Services
5
+ module DiagramStrategies
6
+ # Abstract base class for diagram generation strategies
7
+ #
8
+ # Defines the common interface and shared behavior for all diagram generation
9
+ # strategies. Subclasses must implement the render_diagram method.
10
+ class BaseDiagramStrategy
11
+ attr_reader :syntax_builder, :logger
12
+
13
+ # Initialize strategy with dependencies
14
+ #
15
+ # @param dependencies [Hash] injected dependencies
16
+ # @option dependencies [Object] :syntax_builder Mermaid syntax builder
17
+ # @option dependencies [Logger] :logger logger instance
18
+ def initialize(dependencies = {})
19
+ @syntax_builder = dependencies[:syntax_builder] || create_default_syntax_builder
20
+ @logger = dependencies[:logger] || Rails.logger || Logger.new($stdout)
21
+ end
22
+
23
+ # Generate diagram from standardized dataset
24
+ #
25
+ # @param dataset [Dataset] standardized dataset
26
+ # @return [Hash] diagram generation result
27
+ def generate_from_dataset(dataset)
28
+ @logger.info("Generating diagram from dataset with #{dataset.entities.size} entities and " \
29
+ "#{dataset.relationships.size} relationships")
30
+ start_time = Time.current
31
+
32
+ begin
33
+ # Generate diagram using dataset
34
+ result = render_diagram(dataset)
35
+
36
+ log_operation_completion("diagram generation", Time.current - start_time, {
37
+ entities_count: dataset.entities.size,
38
+ relationships_count: dataset.relationships.size
39
+ })
40
+
41
+ result
42
+ rescue StandardError => e
43
+ @logger.error("Diagram generation failed: #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}")
44
+ error_response("Diagram generation failed: #{e.message}")
45
+ end
46
+ end
47
+
48
+ # Get strategy metadata (abstract method)
49
+ #
50
+ # @return [Hash] strategy metadata including name, description
51
+ # @raise [NotImplementedError] if not implemented by subclass
52
+ def metadata
53
+ {
54
+ name: strategy_name,
55
+ description: strategy_description,
56
+ mermaid_type: mermaid_diagram_type
57
+ }
58
+ end
59
+
60
+ protected
61
+
62
+ # Render diagram from dataset (abstract method)
63
+ #
64
+ # @param dataset [Dataset] standardized dataset
65
+ # @return [Hash] diagram generation result
66
+ # @raise [NotImplementedError] if not implemented by subclass
67
+ def render_diagram(dataset)
68
+ raise NotImplementedError, "Subclasses must implement render_diagram method"
69
+ end
70
+
71
+ # Build empty diagram with message
72
+ #
73
+ # @param message [String] message to display in empty diagram
74
+ # @param diagram_type [String] type of empty diagram to create
75
+ # @return [Hash] empty diagram result
76
+ def build_empty_diagram(message, diagram_type = nil)
77
+ diagram_type ||= mermaid_diagram_type
78
+ content = @syntax_builder.build_empty_diagram(message, diagram_type)
79
+ success_response(content, diagram_type)
80
+ end
81
+
82
+ # Create success response
83
+ #
84
+ # @param content [String] diagram content
85
+ # @param type [String] diagram type
86
+ # @return [Hash] success response
87
+ def success_response(content, type)
88
+ {
89
+ success: true,
90
+ content: content,
91
+ type: type,
92
+ generated_at: Time.current,
93
+ metadata: {
94
+ strategy: self.class.name
95
+ }
96
+ }
97
+ end
98
+
99
+ # Create error response
100
+ #
101
+ # @param message [String] error message
102
+ # @return [Hash] error response
103
+ def error_response(message)
104
+ {
105
+ success: false,
106
+ error: message,
107
+ content: nil,
108
+ type: nil,
109
+ generated_at: Time.current,
110
+ metadata: {
111
+ strategy: self.class.name
112
+ }
113
+ }
114
+ end
115
+
116
+ # Log operation completion
117
+ #
118
+ # @param operation [String] operation name
119
+ # @param duration [Float] operation duration in seconds
120
+ # @param context [Hash] additional context
121
+ def log_operation_completion(operation, duration, _context = {})
122
+ @logger.info("Strategy operation completed: #{operation} by #{self.class.name} " \
123
+ "in #{(duration * 1000).round(2)}ms")
124
+ end
125
+
126
+ # Create default syntax builder
127
+ #
128
+ # @return [MermaidSyntaxBuilder] syntax builder instance
129
+ def create_default_syntax_builder
130
+ Dbwatcher::Services::MermaidSyntaxBuilder.new
131
+ end
132
+
133
+ # Strategy metadata methods (abstract)
134
+
135
+ def strategy_name
136
+ raise NotImplementedError, "Subclasses must implement strategy_name"
137
+ end
138
+
139
+ def strategy_description
140
+ raise NotImplementedError, "Subclasses must implement strategy_description"
141
+ end
142
+
143
+ def mermaid_diagram_type
144
+ raise NotImplementedError, "Subclasses must implement mermaid_diagram_type"
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Services
5
+ module DiagramStrategies
6
+ # Strategy for generating class diagrams from model associations
7
+ #
8
+ # Handles class diagram generation by converting dataset entities and relationships
9
+ # to Mermaid class diagram syntax.
10
+ class ClassDiagramStrategy < BaseDiagramStrategy
11
+ protected
12
+
13
+ # Render class diagram from standardized dataset
14
+ #
15
+ # @param dataset [Dataset] standardized dataset
16
+ # @return [Hash] diagram generation result
17
+ def render_diagram(dataset)
18
+ @logger.debug "Rendering class diagram from dataset with #{dataset.entities.size} entities and " \
19
+ "#{dataset.relationships.size} relationships"
20
+
21
+ # Generate diagram content directly from dataset
22
+ content = if dataset.relationships.empty? && dataset.entities.empty?
23
+ @syntax_builder.build_empty_class_diagram("No model associations or entities found")
24
+ else
25
+ @syntax_builder.build_class_diagram_from_dataset(dataset)
26
+ end
27
+
28
+ success_response(content, "classDiagram")
29
+ end
30
+
31
+ private
32
+
33
+ # Strategy metadata methods
34
+
35
+ def strategy_name
36
+ "Model Associations (Class Diagram)"
37
+ end
38
+
39
+ def strategy_description
40
+ "Class diagram showing ActiveRecord model relationships and methods"
41
+ end
42
+
43
+ def mermaid_diagram_type
44
+ "classDiagram"
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Services
5
+ module DiagramStrategies
6
+ # Strategy for generating Entity Relationship Diagrams (ERD)
7
+ #
8
+ # Handles ERD diagram generation by converting dataset entities and relationships
9
+ # to Mermaid ERD syntax.
10
+ class ErdDiagramStrategy < BaseDiagramStrategy
11
+ protected
12
+
13
+ # Render ERD diagram from standardized dataset
14
+ #
15
+ # @param dataset [Dataset] standardized dataset
16
+ # @return [Hash] diagram generation result
17
+ def render_diagram(dataset)
18
+ @logger.debug "Rendering ERD diagram from dataset with #{dataset.entities.size} entities and " \
19
+ "#{dataset.relationships.size} relationships"
20
+
21
+ # Generate diagram content directly from dataset
22
+ content = if dataset.relationships.empty? && dataset.entities.empty?
23
+ @syntax_builder.build_empty_erd("No database relationships or tables found")
24
+ elsif dataset.relationships.empty?
25
+ # Show isolated tables if no relationships but entities exist
26
+ @syntax_builder.build_erd_diagram_with_tables(dataset.entities.values)
27
+ else
28
+ @syntax_builder.build_erd_diagram_from_dataset(dataset)
29
+ end
30
+
31
+ success_response(content, "erDiagram")
32
+ end
33
+
34
+ private
35
+
36
+ # Strategy metadata methods
37
+
38
+ def strategy_name
39
+ "Database Schema (ERD)"
40
+ end
41
+
42
+ def strategy_description
43
+ "Entity relationship diagram showing database tables and foreign key relationships"
44
+ end
45
+
46
+ def mermaid_diagram_type
47
+ "erDiagram"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Services
5
+ module DiagramStrategies
6
+ # Strategy for generating flowchart diagrams from model associations
7
+ #
8
+ # Handles flowchart diagram generation by converting dataset entities and relationships
9
+ # to Mermaid flowchart syntax.
10
+ class FlowchartDiagramStrategy < BaseDiagramStrategy
11
+ protected
12
+
13
+ # Render flowchart diagram from standardized dataset
14
+ #
15
+ # @param dataset [Dataset] standardized dataset
16
+ # @return [Hash] diagram generation result
17
+ def render_diagram(dataset)
18
+ @logger.debug "Rendering flowchart diagram from dataset with #{dataset.entities.size} entities and " \
19
+ "#{dataset.relationships.size} relationships"
20
+
21
+ # Generate diagram content directly from dataset
22
+ content = if dataset.relationships.empty? && dataset.entities.empty?
23
+ @syntax_builder.build_empty_flowchart("No model associations or entities found")
24
+ elsif dataset.relationships.empty?
25
+ # Show isolated nodes if no relationships but entities exist
26
+ @syntax_builder.build_flowchart_with_nodes(dataset.entities.values)
27
+ else
28
+ @syntax_builder.build_flowchart_diagram_from_dataset(dataset)
29
+ end
30
+
31
+ success_response(content, "flowchart")
32
+ end
33
+
34
+ private
35
+
36
+ # Strategy metadata methods
37
+
38
+ def strategy_name
39
+ "Model Associations"
40
+ end
41
+
42
+ def strategy_description
43
+ "Flowchart diagram showing ActiveRecord model relationships and associations"
44
+ end
45
+
46
+ def mermaid_diagram_type
47
+ "flowchart"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Diagram System Components
4
+ # This file centralizes all diagram-related requires for better organization
5
+
6
+ # Core diagram data structures
7
+ require_relative "diagram_data"
8
+
9
+ # Error handling
10
+ require_relative "diagram_error_handler"
11
+
12
+ # Registry and type management
13
+ require_relative "diagram_type_registry"
14
+
15
+ # Mermaid syntax generation
16
+ require_relative "mermaid_syntax_builder"
17
+ require_relative "mermaid_syntax/base_builder"
18
+ require_relative "mermaid_syntax/sanitizer"
19
+ require_relative "mermaid_syntax/cardinality_mapper"
20
+ require_relative "mermaid_syntax/erd_builder"
21
+ require_relative "mermaid_syntax/class_diagram_builder"
22
+ require_relative "mermaid_syntax/flowchart_builder"
23
+
24
+ # Diagram analyzers
25
+ require_relative "diagram_analyzers/base_analyzer"
26
+ require_relative "diagram_analyzers/foreign_key_analyzer"
27
+ require_relative "diagram_analyzers/inferred_relationship_analyzer"
28
+ require_relative "diagram_analyzers/model_association_analyzer"
29
+
30
+ # Diagram strategies
31
+ require_relative "diagram_strategies/base_diagram_strategy"
32
+ require_relative "diagram_strategies/erd_diagram_strategy"
33
+ require_relative "diagram_strategies/class_diagram_strategy"
34
+ require_relative "diagram_strategies/flowchart_diagram_strategy"
35
+
36
+ # Diagram generator (must be loaded after all components)
37
+ require_relative "diagram_generator"
38
+
39
+ module Dbwatcher
40
+ module Services
41
+ # Diagram System Module
42
+ # Provides centralized access to diagram generation capabilities
43
+ module DiagramSystem
44
+ # Get available diagram types
45
+ #
46
+ # @return [Array<String>] available diagram type names
47
+ def self.available_types
48
+ DiagramTypeRegistry.new.available_types
49
+ end
50
+
51
+ # Generate diagram for session
52
+ #
53
+ # @param session_id [String] session identifier
54
+ # @param diagram_type [String] type of diagram to generate
55
+ # @return [Hash] diagram generation result
56
+ def self.generate(session_id, diagram_type = "database_tables")
57
+ DiagramGenerator.new(session_id, diagram_type).call
58
+ end
59
+
60
+ # Check if diagram type is supported
61
+ #
62
+ # @param diagram_type [String] diagram type to check
63
+ # @return [Boolean] true if supported
64
+ def self.supports?(diagram_type)
65
+ DiagramTypeRegistry.new.type_exists?(diagram_type)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Services
5
+ # Registry for managing diagram types and creating strategy instances
6
+ #
7
+ # Provides a central registry for all available diagram types with metadata,
8
+ # and factory methods for creating strategy instances.
9
+ class DiagramTypeRegistry
10
+ # Error classes for registry operations
11
+ class UnknownTypeError < StandardError; end
12
+
13
+ # Built-in diagram types with essential metadata
14
+ DIAGRAM_TYPES = {
15
+ "database_tables" => {
16
+ strategy_class: "Dbwatcher::Services::DiagramStrategies::ErdDiagramStrategy",
17
+ analyzer_class: "Dbwatcher::Services::DiagramAnalyzers::ForeignKeyAnalyzer",
18
+ display_name: "Database Schema",
19
+ description: "Entity relationship diagram showing database tables and foreign key relationships",
20
+ mermaid_type: "erDiagram"
21
+ },
22
+ "database_tables_inferred" => {
23
+ strategy_class: "Dbwatcher::Services::DiagramStrategies::ErdDiagramStrategy",
24
+ analyzer_class: "Dbwatcher::Services::DiagramAnalyzers::InferredRelationshipAnalyzer",
25
+ display_name: "Database Schema (Inferred)",
26
+ description: "Entity relationship diagram with inferred relationships from naming patterns",
27
+ mermaid_type: "erDiagram"
28
+ },
29
+ "model_associations" => {
30
+ strategy_class: "Dbwatcher::Services::DiagramStrategies::ClassDiagramStrategy",
31
+ analyzer_class: "Dbwatcher::Services::DiagramAnalyzers::ModelAssociationAnalyzer",
32
+ display_name: "Model Associations",
33
+ description: "Class diagram showing ActiveRecord models with attributes and methods",
34
+ mermaid_type: "classDiagram"
35
+ },
36
+ "model_associations_flowchart" => {
37
+ strategy_class: "Dbwatcher::Services::DiagramStrategies::FlowchartDiagramStrategy",
38
+ analyzer_class: "Dbwatcher::Services::DiagramAnalyzers::ModelAssociationAnalyzer",
39
+ display_name: "Model Associations (Flowchart)",
40
+ description: "Flowchart diagram showing model relationships",
41
+ mermaid_type: "flowchart"
42
+ }
43
+ }.freeze
44
+
45
+ # Initialize registry
46
+ def initialize
47
+ @logger = if defined?(Rails) && Rails.respond_to?(:logger)
48
+ Rails.logger
49
+ else
50
+ require "logger"
51
+ Logger.new($stdout)
52
+ end
53
+ end
54
+
55
+ # Get list of available diagram type keys
56
+ #
57
+ # @return [Array<String>] diagram type keys
58
+ def available_types
59
+ DIAGRAM_TYPES.keys
60
+ end
61
+
62
+ # Get available types with metadata
63
+ #
64
+ # @return [Hash] diagram types with metadata
65
+ def available_types_with_metadata
66
+ DIAGRAM_TYPES.transform_values do |type_config|
67
+ type_config.except(:strategy_class, :analyzer_class)
68
+ end
69
+ end
70
+
71
+ # Create strategy instance for given type
72
+ #
73
+ # @param type [String] diagram type key
74
+ # @param dependencies [Hash] optional dependencies to inject
75
+ # @return [Object] strategy instance
76
+ # @raise [UnknownTypeError] if type is unknown
77
+ def create_strategy(type, dependencies = {})
78
+ type_config = find_type_config(type)
79
+ strategy_class = resolve_strategy_class(type_config[:strategy_class])
80
+
81
+ @logger.debug("Creating strategy for type #{type}: #{strategy_class.name}")
82
+ strategy_class.new(dependencies)
83
+ rescue StandardError => e
84
+ raise UnknownTypeError, "Cannot create strategy for type '#{type}': #{e.message}"
85
+ end
86
+
87
+ # Create analyzer instance for given type
88
+ #
89
+ # @param type [String] diagram type key
90
+ # @param session [Object] session object
91
+ # @return [Object] analyzer instance
92
+ # @raise [UnknownTypeError] if type is unknown
93
+ def create_analyzer(type, session)
94
+ type_config = find_type_config(type)
95
+ analyzer_class = resolve_analyzer_class(type_config[:analyzer_class])
96
+
97
+ @logger.debug("Creating analyzer for type #{type}: #{analyzer_class.name}")
98
+ analyzer_class.new(session)
99
+ rescue StandardError => e
100
+ raise UnknownTypeError, "Cannot create analyzer for type '#{type}': #{e.message}"
101
+ end
102
+
103
+ # Check if a diagram type exists
104
+ #
105
+ # @param type [String] diagram type key
106
+ # @return [Boolean] true if type exists
107
+ def type_exists?(type)
108
+ DIAGRAM_TYPES.key?(type)
109
+ end
110
+
111
+ # Get metadata for a specific diagram type
112
+ #
113
+ # @param type [String] diagram type key
114
+ # @return [Hash] type metadata
115
+ # @raise [UnknownTypeError] if type is unknown
116
+ def type_metadata(type)
117
+ type_config = find_type_config(type)
118
+ type_config.except(:strategy_class, :analyzer_class)
119
+ end
120
+
121
+ private
122
+
123
+ # Find type configuration by key
124
+ #
125
+ # @param type [String] diagram type key
126
+ # @return [Hash] type configuration
127
+ # @raise [UnknownTypeError] if type not found
128
+ def find_type_config(type)
129
+ type_config = DIAGRAM_TYPES[type]
130
+ raise UnknownTypeError, "Unknown diagram type: #{type}" unless type_config
131
+
132
+ type_config
133
+ end
134
+
135
+ # Resolve strategy class from string or class
136
+ #
137
+ # @param strategy_class [String, Class] strategy class reference
138
+ # @return [Class] resolved strategy class
139
+ def resolve_strategy_class(strategy_class)
140
+ if strategy_class.is_a?(String)
141
+ strategy_class.constantize
142
+ else
143
+ strategy_class
144
+ end
145
+ rescue NameError => e
146
+ raise UnknownTypeError, "Cannot resolve strategy class: #{e.message}"
147
+ end
148
+
149
+ # Resolve analyzer class from string or class
150
+ #
151
+ # @param analyzer_class [String, Class] analyzer class reference
152
+ # @return [Class] resolved analyzer class
153
+ def resolve_analyzer_class(analyzer_class)
154
+ if analyzer_class.is_a?(String)
155
+ analyzer_class.constantize
156
+ else
157
+ analyzer_class
158
+ end
159
+ rescue NameError => e
160
+ raise UnknownTypeError, "Cannot resolve analyzer class: #{e.message}"
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Services
5
+ module MermaidSyntax
6
+ # Base class for Mermaid syntax builders
7
+ #
8
+ # Provides common functionality and configuration options for all Mermaid
9
+ # diagram builders. Subclasses should implement the build_from_dataset method.
10
+ #
11
+ # @example
12
+ # class MyDiagramBuilder < BaseBuilder
13
+ # def build_from_dataset(dataset)
14
+ # # Implementation
15
+ # end
16
+ # end
17
+ class BaseBuilder
18
+ # Initialize a new builder with configuration
19
+ #
20
+ # @param config [Hash] configuration options
21
+ def initialize(config = {})
22
+ @config = default_config.merge(config)
23
+ end
24
+
25
+ # Build diagram content from dataset
26
+ #
27
+ # @param dataset [DiagramData::Dataset] dataset to render
28
+ # @return [String] Mermaid diagram content
29
+ def build_from_dataset(dataset)
30
+ raise NotImplementedError, "Subclasses must implement build_from_dataset"
31
+ end
32
+
33
+ # Build empty diagram with message
34
+ #
35
+ # @param message [String] message to display
36
+ # @return [String] Mermaid diagram content
37
+ def build_empty(message)
38
+ raise NotImplementedError, "Subclasses must implement build_empty"
39
+ end
40
+
41
+ protected
42
+
43
+ # Check if attributes should be shown
44
+ #
45
+ # @return [Boolean] true if attributes should be shown
46
+ def show_attributes?
47
+ @config[:show_attributes] == true
48
+ end
49
+
50
+ # Check if methods should be shown
51
+ #
52
+ # @return [Boolean] true if methods should be shown
53
+ def show_methods?
54
+ @config[:show_methods] == true
55
+ end
56
+
57
+ # Check if cardinality should be shown
58
+ #
59
+ # @return [Boolean] true if cardinality should be shown
60
+ def show_cardinality?
61
+ @config[:show_cardinality] != false
62
+ end
63
+
64
+ # Get maximum number of attributes to show
65
+ #
66
+ # @return [Integer] maximum number of attributes
67
+ def max_attributes
68
+ @config[:max_attributes] || 10
69
+ end
70
+
71
+ # Get maximum number of methods to show
72
+ #
73
+ # @return [Integer] maximum number of methods
74
+ def max_methods
75
+ @config[:max_methods] || 5
76
+ end
77
+
78
+ # Get diagram direction
79
+ #
80
+ # @return [String] diagram direction (TD, LR, etc.)
81
+ def diagram_direction
82
+ @config[:direction] || "LR"
83
+ end
84
+
85
+ # Check if table case should be preserved
86
+ #
87
+ # @return [Boolean] true if table case should be preserved
88
+ def preserve_table_case?
89
+ @config[:preserve_table_case] != false
90
+ end
91
+
92
+ # Get cardinality format
93
+ #
94
+ # @return [Symbol] cardinality format (:standard or :simple)
95
+ def cardinality_format
96
+ @config[:cardinality_format] || :simple
97
+ end
98
+
99
+ # Sanitize text for Mermaid
100
+ #
101
+ # @param text [String] text to sanitize
102
+ # @return [String] sanitized text
103
+ def sanitize_text(text)
104
+ return "" unless text
105
+
106
+ text.to_s.gsub(/["\n\r]/, " ").strip
107
+ end
108
+
109
+ # Default configuration options
110
+ #
111
+ # @return [Hash] default configuration
112
+ def default_config
113
+ {
114
+ show_attributes: true,
115
+ show_methods: false,
116
+ show_cardinality: true,
117
+ max_attributes: 10,
118
+ max_methods: 5,
119
+ direction: "LR",
120
+ preserve_table_case: true,
121
+ cardinality_format: :simple
122
+ }
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end