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,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Services
5
+ module Analyzers
6
+ # Handles session data processing and change iteration
7
+ #
8
+ # This service extracts and processes individual changes from session data,
9
+ # providing a clean interface for iterating over table changes.
10
+ #
11
+ # @example
12
+ # processor = SessionDataProcessor.new(session)
13
+ # processor.process_changes do |table_name, change, tables|
14
+ # # Process each change
15
+ # end
16
+ class SessionDataProcessor < BaseService
17
+ # Initialize with session
18
+ #
19
+ # @param session [Session] session to process
20
+ def initialize(session)
21
+ @session = session
22
+ super()
23
+ end
24
+
25
+ # Process all changes in the session
26
+ #
27
+ # @return [Hash] tables hash with processed data
28
+ def call
29
+ log_service_start "Processing session changes", session_context
30
+ start_time = Time.current
31
+
32
+ tables = {}
33
+
34
+ process_changes do |table_name, change, tables_ref|
35
+ yield(table_name, change, tables_ref) if block_given?
36
+ end
37
+
38
+ log_service_completion(start_time, { tables_found: tables.keys.length })
39
+ tables
40
+ end
41
+
42
+ # Process changes with block
43
+ #
44
+ # @yield [table_name, change, tables] for each change
45
+ # @return [Hash] tables hash
46
+ def process_changes
47
+ return {} unless session&.changes.respond_to?(:each)
48
+
49
+ tables = {}
50
+
51
+ session.changes.each do |change|
52
+ table_name = extract_table_name(change)
53
+ next unless table_name
54
+
55
+ yield(table_name, change, tables) if block_given?
56
+ end
57
+
58
+ tables
59
+ end
60
+
61
+ # Extract table name from change data
62
+ #
63
+ # @param change [Hash] change data
64
+ # @return [String, nil] table name or nil
65
+ def extract_table_name(change)
66
+ return nil unless change.is_a?(Hash)
67
+
68
+ change[:table_name]
69
+ end
70
+
71
+ # Extract session tables that were modified
72
+ #
73
+ # @return [Array<String>] unique table names
74
+ def extract_session_tables
75
+ return [] unless session&.changes
76
+
77
+ session.changes.map do |change|
78
+ extract_table_name(change)
79
+ end.compact.uniq
80
+ end
81
+
82
+ private
83
+
84
+ attr_reader :session
85
+
86
+ # Build context for logging
87
+ #
88
+ # @return [Hash] session context
89
+ def session_context
90
+ {
91
+ session_id: session&.id,
92
+ changes_count: session&.changes&.count || 0
93
+ }
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Dbwatcher
6
+ module Services
7
+ module Analyzers
8
+ # Builds comprehensive table summaries from session data
9
+ #
10
+ # This service aggregates table operations, captures sample records,
11
+ # and builds complete table summaries for analysis and visualization.
12
+ #
13
+ # @example
14
+ # builder = TableSummaryBuilder.new(session)
15
+ # summary = builder.call
16
+ # # => { "users" => { columns: Set, sample_record: {}, operations: {}, changes: [] } }
17
+ class TableSummaryBuilder < BaseService
18
+ # Initialize with session
19
+ #
20
+ # @param session [Session] session to analyze
21
+ def initialize(session)
22
+ super()
23
+ @session = session
24
+ @processor = SessionDataProcessor.new(session)
25
+ end
26
+
27
+ # Build table summaries from session data
28
+ #
29
+ # @return [Hash] table summaries keyed by table name
30
+ def call
31
+ log_service_start("session_id=#{session.id} changes_count=#{session.changes.length}")
32
+
33
+ start_time = Time.now
34
+ tables = build_tables_data
35
+ log_service_completion(start_time, result_context(tables))
36
+ tables
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :session, :processor
42
+
43
+ # Initialize table data structure
44
+ #
45
+ # @param tables [Hash] tables collection
46
+ # @param table_name [String] table name
47
+ def initialize_table_data(tables, table_name)
48
+ tables[table_name] ||= {
49
+ name: table_name,
50
+ columns: Set.new,
51
+ sample_record: nil,
52
+ total_operations: 0,
53
+ operations: { insert: 0, update: 0, delete: 0 },
54
+ changes: []
55
+ }
56
+ end
57
+
58
+ # Update table data with change information
59
+ #
60
+ # @param table_data [Hash] table data structure
61
+ # @param change [Hash] change data
62
+ def update_table_data(table_data, change)
63
+ # Extract and normalize operation
64
+ operation = extract_operation(change)
65
+
66
+ # Update counters
67
+ table_data[:total_operations] += 1
68
+ table_data[:operations][operation] = table_data[:operations].fetch(operation, 0) + 1
69
+
70
+ # Add the actual change data for view display
71
+ table_data[:changes] << change
72
+ end
73
+
74
+ # Update sample record to capture all columns
75
+ #
76
+ # @param table_data [Hash] table data structure
77
+ # @param change [Hash] change data
78
+ def update_sample_record(table_data, change)
79
+ # Try multiple possible data sources for record snapshot
80
+ sample_data = extract_record_data(change)
81
+ return unless sample_data.is_a?(Hash)
82
+
83
+ # Initialize or merge sample record
84
+ if table_data[:sample_record].nil?
85
+ table_data[:sample_record] = sample_data.dup
86
+ else
87
+ # Merge to capture all possible columns from different records
88
+ table_data[:sample_record].merge!(sample_data)
89
+ end
90
+
91
+ # Track all columns seen in this table
92
+ sample_data.each_key { |key| table_data[:columns].add(key.to_s) }
93
+ end
94
+
95
+ # Extract operation type from change data
96
+ #
97
+ # @param change [Hash] change data
98
+ # @return [Symbol] operation type (:insert, :update, :delete)
99
+ def extract_operation(change)
100
+ operation_str = change[:operation]&.to_s&.downcase
101
+
102
+ case operation_str
103
+ when "insert" then :insert
104
+ when "update" then :update
105
+ when "delete" then :delete
106
+ else
107
+ log_warning("Unknown operation type: #{operation_str || "nil"}")
108
+ :unknown
109
+ end
110
+ end
111
+
112
+ # Log a warning message
113
+ #
114
+ # @param message [String] warning message
115
+ def log_warning(message)
116
+ puts "[WARNING] #{service_name}: #{message}"
117
+ end
118
+
119
+ # Extract record data from various change formats
120
+ #
121
+ # @param change [Hash] change data
122
+ # @return [Hash, nil] record data or nil
123
+ def extract_record_data(change)
124
+ # Try record_snapshot first (most reliable)
125
+ return change[:record_snapshot] if change[:record_snapshot].is_a?(Hash)
126
+
127
+ # Fallback to building from changes array
128
+ build_record_from_changes(change[:changes]) if change[:changes].is_a?(Array)
129
+ end
130
+
131
+ # Build record data from changes array
132
+ #
133
+ # @param changes [Array] array of column changes
134
+ # @return [Hash] constructed record data
135
+ def build_record_from_changes(changes)
136
+ record = {}
137
+
138
+ changes.each do |column_change|
139
+ next unless column_change.is_a?(Hash) && column_change[:column]
140
+
141
+ # Use new_value if available, otherwise old_value
142
+ value = column_change[:new_value] || column_change[:old_value]
143
+ record[column_change[:column]] = value
144
+ end
145
+
146
+ record
147
+ end
148
+
149
+ # Get service name for logging
150
+ #
151
+ # @return [String] service name
152
+ def service_name
153
+ "TableSummaryBuilder"
154
+ end
155
+
156
+ # Build tables data by processing all changes
157
+ def build_tables_data
158
+ tables = {}
159
+
160
+ processor.process_changes do |table_name, change, _tables|
161
+ initialize_table_data(tables, table_name)
162
+ update_table_data(tables[table_name], change)
163
+ update_sample_record(tables[table_name], change)
164
+ end
165
+
166
+ filter_and_format_tables(tables)
167
+ end
168
+
169
+ # Filter out empty tables and format operations for view compatibility
170
+ def filter_and_format_tables(tables)
171
+ # Filter out tables with no operations
172
+ tables.reject! { |_, data| data[:total_operations].zero? }
173
+
174
+ # Convert operation counts to string keys for view compatibility
175
+ tables.each_value do |data|
176
+ data[:operations] = format_operations(data[:operations])
177
+ end
178
+
179
+ tables
180
+ end
181
+
182
+ # Format operations hash with string keys and remove zero counts
183
+ def format_operations(operations)
184
+ formatted = {
185
+ "INSERT" => operations[:insert] || 0,
186
+ "UPDATE" => operations[:update] || 0,
187
+ "DELETE" => operations[:delete] || 0
188
+ }
189
+ formatted.reject { |_, count| count.zero? }
190
+ end
191
+
192
+ # Build result context for logging
193
+ def result_context(tables)
194
+ {
195
+ tables_analyzed: tables.keys.length,
196
+ total_operations: tables.values.sum { |t| t[:total_operations] }
197
+ }
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Services
5
+ module Api
6
+ # Base class for API data services
7
+ #
8
+ # Provides common functionality for API endpoints including
9
+ # session handling, caching, and error handling.
10
+ class BaseApiService < BaseService
11
+ attr_reader :session, :params
12
+
13
+ def initialize(session, params = {})
14
+ @session = session
15
+ @params = params
16
+ super()
17
+ end
18
+
19
+ protected
20
+
21
+ # Build cache key for session-based data
22
+ #
23
+ # @param suffix [String] additional cache key suffix
24
+ # @return [String] cache key
25
+ def cache_key(suffix = nil)
26
+ key = "api_#{service_name}_#{session.id}"
27
+ key += "_#{suffix}" if suffix
28
+ key
29
+ end
30
+
31
+ # Get service name for logging and caching
32
+ #
33
+ # @return [String] service name
34
+ def service_name
35
+ self.class.name.demodulize.underscore.gsub("_service", "")
36
+ end
37
+
38
+ # Execute with caching
39
+ #
40
+ # @param cache_suffix [String] optional cache suffix
41
+ # @param expires_in [ActiveSupport::Duration] cache expiration
42
+ # @yield block to execute if cache miss
43
+ # @return [Object] cached or fresh result
44
+ def with_cache(cache_suffix = nil, expires_in: 1.hour)
45
+ key = cache_key(cache_suffix)
46
+
47
+ # Check if caching is enabled
48
+ if defined?(Rails.cache) && Rails.application.config.action_controller.perform_caching
49
+ Rails.cache.fetch(key, expires_in: expires_in) do
50
+ log_service_start("Cache miss, generating fresh data")
51
+ yield
52
+ end
53
+ else
54
+ log_service_start("Caching disabled, generating fresh data")
55
+ yield
56
+ end
57
+ end
58
+
59
+ # Handle service errors consistently
60
+ #
61
+ # @param error [StandardError] the error to handle
62
+ # @return [Hash] error response
63
+ def handle_error(error)
64
+ log_error "Error in #{service_name}: #{error.message}", error: error
65
+ { error: error.message }
66
+ end
67
+
68
+ # Validate session exists
69
+ #
70
+ # @return [Hash, nil] error hash if session invalid, nil if valid
71
+ def validate_session
72
+ return nil if session
73
+
74
+ error_msg = "Session not found"
75
+ log_error error_msg
76
+ { error: error_msg }
77
+ end
78
+
79
+ # Previously had pagination parameters method here
80
+ # Now removed to show all data without pagination
81
+
82
+ # Parse filter parameters (override in subclasses)
83
+ #
84
+ # @return [Hash] filter parameters
85
+ def filter_params
86
+ # Expect params to be a Hash from the controller
87
+ return {} if params.nil?
88
+
89
+ # Extract only the filter-related keys
90
+ # Make sure we handle the case when params doesn't respond to slice
91
+ if params.respond_to?(:slice)
92
+ params.slice(:table, :operation).compact
93
+ else
94
+ {}
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Services
5
+ module Api
6
+ # Service for handling filtered changes data
7
+ #
8
+ # Provides changes data for the sessions changes view and API endpoints
9
+ # with filtering and caching support.
10
+ class ChangesDataService < BaseApiService
11
+ def call
12
+ start_time = Time.now
13
+
14
+ # Check for nil session first
15
+ return { error: "Session not found" } unless session
16
+
17
+ log_service_start("Getting changes data for session #{session.id}")
18
+
19
+ validation_error = validate_session
20
+ return validation_error if validation_error
21
+
22
+ begin
23
+ result = with_cache(cache_suffix) do
24
+ build_changes_response
25
+ end
26
+
27
+ log_service_completion(start_time, session_id: session.id, filters: filter_params)
28
+ result
29
+ rescue StandardError => e
30
+ handle_error(e)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def build_changes_response
37
+ {
38
+ tables_summary: build_filtered_summary,
39
+ filters: filter_params || {},
40
+ session_id: session.id,
41
+ metadata: build_metadata
42
+ }
43
+ end
44
+
45
+ def build_filtered_summary
46
+ summary = Storage.sessions.build_tables_summary(session)
47
+
48
+ # Handle nil filter_params
49
+ filter_params_hash = filter_params || {}
50
+
51
+ # Apply filters only if they exist
52
+ summary = filter_by_table(summary, filter_params_hash) if filter_params_hash[:table]
53
+
54
+ summary = filter_by_operation(summary, filter_params_hash) if filter_params_hash[:operation]
55
+
56
+ summary
57
+ end
58
+
59
+ def filter_by_table(summary, filter_hash)
60
+ summary.select { |table_name, _| table_name == filter_hash[:table] }
61
+ end
62
+
63
+ def filter_by_operation(summary, filter_hash)
64
+ operation = filter_hash[:operation].upcase
65
+
66
+ summary.each_value do |data|
67
+ data[:changes] = data[:changes].select do |change|
68
+ change[:operation] == operation
69
+ end
70
+ end
71
+
72
+ summary
73
+ end
74
+
75
+ def build_metadata
76
+ # Make sure filter_params returns a hash even with nil params
77
+ has_filters = filter_params&.any?
78
+
79
+ {
80
+ generated_at: Time.current,
81
+ has_filters: has_filters || false,
82
+ available_tables: available_tables,
83
+ available_operations: %w[INSERT UPDATE DELETE],
84
+ total_count: calculate_total_count
85
+ }
86
+ end
87
+
88
+ def calculate_total_count
89
+ summary = Storage.sessions.build_tables_summary(session)
90
+ summary.values.sum { |data| data[:changes]&.length || 0 }
91
+ end
92
+
93
+ def available_tables
94
+ summary = Storage.sessions.build_tables_summary(session)
95
+ summary.keys
96
+ end
97
+
98
+ def cache_suffix
99
+ filter_parts = []
100
+
101
+ # Handle nil params safely
102
+ if params
103
+ filter_parts << "table_#{params[:table]}" if params[:table]
104
+ filter_parts << "op_#{params[:operation]}" if params[:operation]
105
+ end
106
+
107
+ filter_parts.any? ? filter_parts.join("_") : nil
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Services
5
+ module Api
6
+ # Service for handling diagram generation and data
7
+ #
8
+ # Provides diagram data for the sessions diagrams view and API endpoints
9
+ # with caching, type validation, and comprehensive error handling.
10
+ class DiagramDataService < BaseApiService
11
+ DEFAULT_DIAGRAM_TYPE = "database_tables"
12
+
13
+ attr_reader :diagram_type, :diagram_registry
14
+
15
+ def initialize(session, diagram_type = nil, params = {})
16
+ super(session, params)
17
+ @diagram_registry = Dbwatcher::Services::DiagramTypeRegistry.new
18
+ @diagram_type = normalize_diagram_type(diagram_type)
19
+ end
20
+
21
+ def call
22
+ start_time = Time.now
23
+ log_service_start("Generating #{diagram_type} diagram for session #{session.id}")
24
+
25
+ validation_error = validate_session
26
+ return validation_error if validation_error
27
+
28
+ type_validation_error = validate_diagram_type
29
+ return type_validation_error if type_validation_error
30
+
31
+ begin
32
+ result = with_cache(diagram_type, expires_in: cache_duration) do
33
+ generate_diagram_data
34
+ end
35
+
36
+ log_service_completion(start_time, session_id: session.id, diagram_type: diagram_type)
37
+ result
38
+ rescue StandardError => e
39
+ handle_error(e)
40
+ end
41
+ end
42
+
43
+ # Get available diagram types
44
+ #
45
+ # @return [Array<String>] available diagram types
46
+ def self.available_types
47
+ Dbwatcher::Services::DiagramTypeRegistry.new.available_types
48
+ end
49
+
50
+ # Get available diagram types with metadata
51
+ #
52
+ # @return [Hash] available diagram types with metadata
53
+ def self.available_types_with_metadata
54
+ Dbwatcher::Services::DiagramTypeRegistry.new.available_types_with_metadata
55
+ end
56
+
57
+ private
58
+
59
+ def generate_diagram_data
60
+ log_service_start("Generating fresh #{diagram_type} diagram")
61
+
62
+ result = Storage.sessions.diagram_data(session.id, diagram_type)
63
+
64
+ if result[:error]
65
+ log_error "Diagram generation failed: #{result[:error]}"
66
+ return result
67
+ end
68
+
69
+ enhance_diagram_result(result)
70
+ end
71
+
72
+ def enhance_diagram_result(base_result)
73
+ base_result.merge(
74
+ diagram_type: diagram_type,
75
+ session_id: session.id,
76
+ metadata: build_diagram_metadata,
77
+ cache_info: build_cache_info
78
+ )
79
+ end
80
+
81
+ def build_diagram_metadata
82
+ {
83
+ generated_at: Time.current,
84
+ diagram_type: diagram_type,
85
+ available_types: self.class.available_types,
86
+ cache_duration: cache_duration,
87
+ supports_refresh: true
88
+ }
89
+ end
90
+
91
+ def build_cache_info
92
+ {
93
+ cache_key: cache_key(diagram_type),
94
+ expires_in: cache_duration,
95
+ can_refresh: params[:refresh] != "true"
96
+ }
97
+ end
98
+
99
+ def normalize_diagram_type(type)
100
+ normalized = type.to_s.strip.downcase
101
+ diagram_registry.type_exists?(normalized) ? normalized : DEFAULT_DIAGRAM_TYPE
102
+ end
103
+
104
+ def validate_diagram_type
105
+ return nil if diagram_registry.type_exists?(diagram_type)
106
+
107
+ available_types = diagram_registry.available_types
108
+ error_msg = "Invalid diagram type '#{diagram_type}'. Valid types: #{available_types.join(", ")}"
109
+ log_error error_msg
110
+ { error: error_msg }
111
+ end
112
+
113
+ def cache_duration
114
+ # Longer cache for complex diagrams based on type
115
+ case diagram_type
116
+ when "model_associations"
117
+ 2.hours
118
+ when "database_tables", "database_tables_inferred"
119
+ 1.hour
120
+ else
121
+ 30.minutes
122
+ end
123
+ end
124
+
125
+ # Override cache key to include refresh parameter
126
+ def cache_key(suffix = nil)
127
+ key = super
128
+ key += "_refresh" if params[:refresh] == "true"
129
+ key
130
+ end
131
+
132
+ # Clear cache if refresh requested
133
+ def with_cache(cache_suffix = nil, expires_in: 1.hour, &block)
134
+ if params[:refresh] == "true"
135
+ key = cache_key(cache_suffix)
136
+ Rails.cache.delete(key)
137
+ log_service_start("Cache cleared due to refresh request")
138
+ end
139
+
140
+ super
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end