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.
- 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 +603 -0
- data/lib/dbwatcher/services/diagram_data/attribute.rb +154 -0
- data/lib/dbwatcher/services/diagram_data/dataset.rb +280 -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 +140 -0
- data/lib/dbwatcher/services/mermaid_syntax/class_diagram_helper.rb +48 -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 +118 -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,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Services
|
5
|
+
module Api
|
6
|
+
# Service for handling summary statistics data
|
7
|
+
#
|
8
|
+
# Provides enhanced summary data for the sessions summary view and API endpoints
|
9
|
+
# with caching and comprehensive statistics.
|
10
|
+
class SummaryDataService < BaseApiService
|
11
|
+
def call
|
12
|
+
start_time = Time.now
|
13
|
+
log_service_start("Getting summary data for session #{session.id}")
|
14
|
+
|
15
|
+
validation_error = validate_session
|
16
|
+
return validation_error if validation_error
|
17
|
+
|
18
|
+
begin
|
19
|
+
result = with_cache do
|
20
|
+
build_summary_response
|
21
|
+
end
|
22
|
+
|
23
|
+
log_service_completion(start_time, session_id: session.id)
|
24
|
+
result
|
25
|
+
rescue StandardError => e
|
26
|
+
handle_error(e)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def build_summary_response
|
33
|
+
summary_data = base_summary
|
34
|
+
|
35
|
+
return summary_data if summary_data[:error]
|
36
|
+
|
37
|
+
enhance_summary_data(summary_data)
|
38
|
+
end
|
39
|
+
|
40
|
+
def base_summary
|
41
|
+
result = Storage.sessions.summary(session.id)
|
42
|
+
|
43
|
+
if result[:error]
|
44
|
+
log_error "Base summary error: #{result[:error]}"
|
45
|
+
return { error: result[:error] }
|
46
|
+
end
|
47
|
+
|
48
|
+
result
|
49
|
+
end
|
50
|
+
|
51
|
+
def enhance_summary_data(base_summary)
|
52
|
+
tables_summary = Storage.sessions.build_tables_summary(session)
|
53
|
+
|
54
|
+
base_summary.merge(
|
55
|
+
session_id: session.id,
|
56
|
+
enhanced_stats: build_enhanced_stats(tables_summary),
|
57
|
+
tables_breakdown: build_tables_breakdown(tables_summary),
|
58
|
+
metadata: build_metadata,
|
59
|
+
timing: build_timing_info
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
def build_enhanced_stats(tables_summary)
|
64
|
+
{
|
65
|
+
tables_count: tables_summary.keys.length,
|
66
|
+
total_changes: calculate_total_changes(tables_summary),
|
67
|
+
operations_breakdown: calculate_operations_breakdown(tables_summary),
|
68
|
+
tables_with_changes: tables_summary.keys,
|
69
|
+
most_active_table: find_most_active_table(tables_summary),
|
70
|
+
change_distribution: calculate_change_distribution(tables_summary)
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
def build_tables_breakdown(tables_summary)
|
75
|
+
tables_data = tables_summary.map do |table_name, data|
|
76
|
+
{
|
77
|
+
table_name: table_name,
|
78
|
+
change_count: data[:changes]&.length || 0,
|
79
|
+
operations: data[:operations] || {},
|
80
|
+
sample_columns: data[:sample_record]&.keys || []
|
81
|
+
}
|
82
|
+
end
|
83
|
+
tables_data.sort_by { |table| -table[:change_count] }
|
84
|
+
end
|
85
|
+
|
86
|
+
def build_metadata
|
87
|
+
{
|
88
|
+
generated_at: Time.current,
|
89
|
+
cache_key: cache_key,
|
90
|
+
data_freshness: "cached"
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
def calculate_total_changes(tables_summary)
|
95
|
+
tables_summary.values.sum { |data| data[:changes]&.length || 0 }
|
96
|
+
end
|
97
|
+
|
98
|
+
def calculate_operations_breakdown(tables_summary)
|
99
|
+
operations = { "INSERT" => 0, "UPDATE" => 0, "DELETE" => 0 }
|
100
|
+
|
101
|
+
tables_summary.each_value do |data|
|
102
|
+
data[:operations]&.each do |op, count|
|
103
|
+
normalized_op = op.to_s.upcase
|
104
|
+
operations[normalized_op] += count if operations.key?(normalized_op)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
operations
|
109
|
+
end
|
110
|
+
|
111
|
+
def find_most_active_table(tables_summary)
|
112
|
+
return nil if tables_summary.empty?
|
113
|
+
|
114
|
+
most_active = tables_summary.max_by do |_, data|
|
115
|
+
data[:changes]&.length || 0
|
116
|
+
end
|
117
|
+
|
118
|
+
{
|
119
|
+
table_name: most_active[0],
|
120
|
+
change_count: most_active[1][:changes]&.length || 0
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
def calculate_change_distribution(tables_summary)
|
125
|
+
total_changes = calculate_total_changes(tables_summary)
|
126
|
+
return {} if total_changes.zero?
|
127
|
+
|
128
|
+
distribution = {}
|
129
|
+
tables_summary.each do |table_name, data|
|
130
|
+
change_count = data[:changes]&.length || 0
|
131
|
+
percentage = (change_count.to_f / total_changes * 100).round(2)
|
132
|
+
distribution[table_name] = {
|
133
|
+
count: change_count,
|
134
|
+
percentage: percentage
|
135
|
+
}
|
136
|
+
end
|
137
|
+
|
138
|
+
distribution
|
139
|
+
end
|
140
|
+
|
141
|
+
def build_timing_info
|
142
|
+
{
|
143
|
+
started_at: session.started_at,
|
144
|
+
ended_at: session.ended_at,
|
145
|
+
duration: calculate_duration
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
149
|
+
def calculate_duration
|
150
|
+
return nil unless session.started_at
|
151
|
+
|
152
|
+
end_time = session.ended_at || Time.current
|
153
|
+
((end_time.to_time - session.started_at.to_time) * 1000).round
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Services
|
5
|
+
# Base class for all service objects
|
6
|
+
#
|
7
|
+
# Provides common functionality and patterns for service objects
|
8
|
+
# including logging and consistent call interface.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# class MyService < BaseService
|
12
|
+
# def call
|
13
|
+
# log_info "Starting service"
|
14
|
+
# # service logic
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# result = MyService.call
|
19
|
+
class BaseService
|
20
|
+
include Dbwatcher::Logging
|
21
|
+
|
22
|
+
# Class method to create instance and call
|
23
|
+
#
|
24
|
+
# @param args [Array] arguments to pass to initialize
|
25
|
+
# @return [Object] result of call method
|
26
|
+
def self.call(*args)
|
27
|
+
new(*args).call
|
28
|
+
end
|
29
|
+
|
30
|
+
# Initialize service
|
31
|
+
#
|
32
|
+
# @param args [Array] service arguments
|
33
|
+
def initialize(*args)
|
34
|
+
# Override in subclasses
|
35
|
+
end
|
36
|
+
|
37
|
+
# Perform service operation
|
38
|
+
#
|
39
|
+
# @return [Object] service result
|
40
|
+
def call
|
41
|
+
raise NotImplementedError, "#{self.class} must implement #call"
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Log service start with context
|
47
|
+
#
|
48
|
+
# @param message [String] log message
|
49
|
+
# @param context [Hash] additional context
|
50
|
+
def log_service_start(message, context = {})
|
51
|
+
log_info "#{self.class.name}: #{message}", context
|
52
|
+
end
|
53
|
+
|
54
|
+
# Log service completion with duration
|
55
|
+
#
|
56
|
+
# @param start_time [Time] service start time
|
57
|
+
# @param context [Hash] additional context
|
58
|
+
def log_service_completion(start_time, context = {})
|
59
|
+
duration = Time.now - start_time
|
60
|
+
log_info "#{self.class.name}: Completed in #{duration.round(3)}s", context.merge(duration: duration)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../diagram_data/relationship_params"
|
4
|
+
|
5
|
+
module Dbwatcher
|
6
|
+
module Services
|
7
|
+
module DiagramAnalyzers
|
8
|
+
# Abstract base class for diagram analyzers
|
9
|
+
#
|
10
|
+
# This class provides a standard interface that all diagram analyzers must implement
|
11
|
+
# to ensure consistent data flow and transformation to Dataset format.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# class CustomAnalyzer < BaseAnalyzer
|
15
|
+
# def analyze(context)
|
16
|
+
# # Perform analysis and return raw data
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# def transform_to_dataset(raw_data)
|
20
|
+
# # Transform raw data to Dataset
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# def analyzer_type
|
24
|
+
# "custom"
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
class BaseAnalyzer < BaseService
|
28
|
+
# Standard interface that all analyzers must implement
|
29
|
+
#
|
30
|
+
# @param context [Hash] analysis context (session, options, etc.)
|
31
|
+
# @return [Object] raw analysis data in analyzer-specific format
|
32
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
33
|
+
def analyze(context)
|
34
|
+
raise NotImplementedError, "Subclasses must implement analyze method"
|
35
|
+
end
|
36
|
+
|
37
|
+
# Transform raw data to standard Dataset
|
38
|
+
#
|
39
|
+
# @param raw_data [Object] raw data from analyze method
|
40
|
+
# @return [DiagramData::Dataset] standardized dataset
|
41
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
42
|
+
def transform_to_dataset(raw_data)
|
43
|
+
raise NotImplementedError, "Subclasses must implement transform_to_dataset"
|
44
|
+
end
|
45
|
+
|
46
|
+
# Get analyzer type classification
|
47
|
+
#
|
48
|
+
# @return [String] analyzer type identifier
|
49
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
50
|
+
def analyzer_type
|
51
|
+
raise NotImplementedError, "Subclasses must implement analyzer_type"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Main entry point - analyze and transform
|
55
|
+
#
|
56
|
+
# @return [DiagramData::Dataset] standardized dataset
|
57
|
+
def call
|
58
|
+
log_service_start "Starting #{self.class.name}", analysis_context
|
59
|
+
start_time = Time.current
|
60
|
+
|
61
|
+
begin
|
62
|
+
# Perform analysis
|
63
|
+
raw_data = analyze(analysis_context)
|
64
|
+
|
65
|
+
# Transform to standard format
|
66
|
+
dataset = transform_to_dataset(raw_data)
|
67
|
+
|
68
|
+
# Validate result
|
69
|
+
unless dataset.is_a?(Dbwatcher::Services::DiagramData::Dataset)
|
70
|
+
raise StandardError, "transform_to_dataset must return a Dataset instance"
|
71
|
+
end
|
72
|
+
|
73
|
+
unless dataset.valid?
|
74
|
+
Rails.logger.warn "#{self.class.name}: Generated invalid dataset: #{dataset.validation_errors.join(", ")}"
|
75
|
+
end
|
76
|
+
|
77
|
+
log_service_completion(start_time, {
|
78
|
+
entities_count: dataset.entities.size,
|
79
|
+
relationships_count: dataset.relationships.size,
|
80
|
+
dataset_valid: dataset.valid?
|
81
|
+
})
|
82
|
+
|
83
|
+
dataset
|
84
|
+
rescue StandardError => e
|
85
|
+
Rails.logger.error "#{self.class.name} error: #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
|
86
|
+
# Return empty dataset instead of failing
|
87
|
+
create_empty_dataset
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
protected
|
92
|
+
|
93
|
+
# Build analysis context for this analyzer
|
94
|
+
#
|
95
|
+
# @return [Hash] analysis context
|
96
|
+
def analysis_context
|
97
|
+
# Override in subclasses to provide specific context
|
98
|
+
{}
|
99
|
+
end
|
100
|
+
|
101
|
+
# Create empty dataset with metadata
|
102
|
+
#
|
103
|
+
# @return [DiagramData::Dataset] empty dataset
|
104
|
+
def create_empty_dataset
|
105
|
+
Dbwatcher::Services::DiagramData::Dataset.new(
|
106
|
+
metadata: {
|
107
|
+
analyzer: self.class.name,
|
108
|
+
analyzer_type: analyzer_type,
|
109
|
+
empty_reason: "No data found or analysis failed",
|
110
|
+
generated_at: Time.current.iso8601
|
111
|
+
}
|
112
|
+
)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Helper method to create entities
|
116
|
+
#
|
117
|
+
# @param id [String] entity ID
|
118
|
+
# @param name [String] entity name
|
119
|
+
# @param type [String] entity type
|
120
|
+
# @param attributes [Array<Attribute>] entity attributes/properties
|
121
|
+
# @param metadata [Hash] entity metadata
|
122
|
+
# @return [DiagramData::Entity] new entity
|
123
|
+
def create_entity(id:, name:, type: "default", attributes: [], metadata: {})
|
124
|
+
Dbwatcher::Services::DiagramData::Entity.new(
|
125
|
+
id: id,
|
126
|
+
name: name,
|
127
|
+
type: type,
|
128
|
+
attributes: attributes,
|
129
|
+
metadata: metadata
|
130
|
+
)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Helper method to create relationships
|
134
|
+
#
|
135
|
+
# @param params [Hash] relationship parameters
|
136
|
+
# @return [DiagramData::Relationship] new relationship
|
137
|
+
def create_relationship(params)
|
138
|
+
params_obj = Dbwatcher::Services::DiagramData::RelationshipParams.new(params)
|
139
|
+
Dbwatcher::Services::DiagramData::Relationship.new(params_obj)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Helper method to create attributes
|
143
|
+
#
|
144
|
+
# @param name [String] attribute name
|
145
|
+
# @param type [String] attribute data type
|
146
|
+
# @param nullable [Boolean] whether attribute can be null
|
147
|
+
# @param default [Object] default value
|
148
|
+
# @param metadata [Hash] additional type-specific information
|
149
|
+
# @return [DiagramData::Attribute] new attribute
|
150
|
+
def create_attribute(name:, type: nil, nullable: true, default: nil, metadata: {})
|
151
|
+
Dbwatcher::Services::DiagramData::Attribute.new(
|
152
|
+
name: name,
|
153
|
+
type: type,
|
154
|
+
nullable: nullable,
|
155
|
+
default: default,
|
156
|
+
metadata: metadata
|
157
|
+
)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|