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,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
|