dbwatcher 0.1.5 → 1.0.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 +2 -2
- data/app/controllers/dbwatcher/base_controller.rb +95 -0
- data/app/controllers/dbwatcher/dashboard_controller.rb +12 -0
- data/app/controllers/dbwatcher/queries_controller.rb +24 -0
- data/app/controllers/dbwatcher/sessions_controller.rb +15 -20
- data/app/controllers/dbwatcher/tables_controller.rb +38 -0
- data/app/helpers/dbwatcher/application_helper.rb +103 -0
- data/app/helpers/dbwatcher/formatting_helper.rb +108 -0
- data/app/helpers/dbwatcher/session_helper.rb +27 -0
- data/app/views/dbwatcher/dashboard/index.html.erb +177 -0
- data/app/views/dbwatcher/queries/index.html.erb +240 -0
- data/app/views/dbwatcher/sessions/index.html.erb +120 -27
- data/app/views/dbwatcher/sessions/show.html.erb +326 -129
- data/app/views/dbwatcher/shared/_badge.html.erb +4 -0
- data/app/views/dbwatcher/shared/_data_table.html.erb +20 -0
- data/app/views/dbwatcher/shared/_header.html.erb +7 -0
- data/app/views/dbwatcher/shared/_page_layout.html.erb +20 -0
- data/app/views/dbwatcher/shared/_section_panel.html.erb +9 -0
- data/app/views/dbwatcher/shared/_stats_card.html.erb +11 -0
- data/app/views/dbwatcher/shared/_tab_bar.html.erb +6 -0
- data/app/views/dbwatcher/tables/changes.html.erb +225 -0
- data/app/views/dbwatcher/tables/index.html.erb +123 -0
- data/app/views/dbwatcher/tables/show.html.erb +86 -0
- data/app/views/layouts/dbwatcher/application.html.erb +375 -26
- data/config/routes.rb +17 -3
- data/lib/dbwatcher/configuration.rb +9 -1
- data/lib/dbwatcher/engine.rb +12 -7
- data/lib/dbwatcher/logging.rb +72 -0
- data/lib/dbwatcher/services/dashboard_data_aggregator.rb +121 -0
- data/lib/dbwatcher/services/query_filter_processor.rb +114 -0
- data/lib/dbwatcher/services/table_statistics_collector.rb +119 -0
- data/lib/dbwatcher/sql_logger.rb +107 -0
- data/lib/dbwatcher/storage/api/base_api.rb +134 -0
- data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +172 -0
- data/lib/dbwatcher/storage/api/query_api.rb +95 -0
- data/lib/dbwatcher/storage/api/session_api.rb +134 -0
- data/lib/dbwatcher/storage/api/table_api.rb +86 -0
- data/lib/dbwatcher/storage/base_storage.rb +113 -0
- data/lib/dbwatcher/storage/change_processor.rb +65 -0
- data/lib/dbwatcher/storage/concerns/data_normalizer.rb +134 -0
- data/lib/dbwatcher/storage/concerns/error_handler.rb +75 -0
- data/lib/dbwatcher/storage/concerns/timestampable.rb +74 -0
- data/lib/dbwatcher/storage/concerns/validatable.rb +117 -0
- data/lib/dbwatcher/storage/date_helper.rb +21 -0
- data/lib/dbwatcher/storage/errors.rb +86 -0
- data/lib/dbwatcher/storage/file_manager.rb +122 -0
- data/lib/dbwatcher/storage/null_session.rb +39 -0
- data/lib/dbwatcher/storage/query_storage.rb +338 -0
- data/lib/dbwatcher/storage/query_validator.rb +24 -0
- data/lib/dbwatcher/storage/session.rb +58 -0
- data/lib/dbwatcher/storage/session_operations.rb +37 -0
- data/lib/dbwatcher/storage/session_query.rb +71 -0
- data/lib/dbwatcher/storage/session_storage.rb +322 -0
- data/lib/dbwatcher/storage/table_storage.rb +237 -0
- data/lib/dbwatcher/storage.rb +112 -85
- data/lib/dbwatcher/tracker.rb +4 -55
- data/lib/dbwatcher/version.rb +1 -1
- data/lib/dbwatcher.rb +12 -2
- metadata +47 -1
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Storage
|
5
|
+
module Concerns
|
6
|
+
# Provides timestamping capabilities for storage objects
|
7
|
+
#
|
8
|
+
# This concern adds created_at and updated_at functionality to storage
|
9
|
+
# objects, following Rails conventions for timestamp management.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# class SessionStorage < BaseStorage
|
13
|
+
# include Concerns::Timestampable
|
14
|
+
# end
|
15
|
+
module Timestampable
|
16
|
+
def self.included(base)
|
17
|
+
base.attr_reader :created_at, :updated_at
|
18
|
+
end
|
19
|
+
|
20
|
+
# Sets initial timestamps on creation
|
21
|
+
#
|
22
|
+
# @return [void]
|
23
|
+
def initialize_timestamps
|
24
|
+
now = current_time
|
25
|
+
@created_at = now
|
26
|
+
@updated_at = now
|
27
|
+
end
|
28
|
+
|
29
|
+
# Updates the updated_at timestamp
|
30
|
+
#
|
31
|
+
# @return [Time] the new updated_at timestamp
|
32
|
+
def touch_updated_at
|
33
|
+
@updated_at = current_time
|
34
|
+
end
|
35
|
+
|
36
|
+
# Calculates age since creation
|
37
|
+
#
|
38
|
+
# @return [Float] age in seconds since creation
|
39
|
+
def age
|
40
|
+
current_time - created_at
|
41
|
+
end
|
42
|
+
|
43
|
+
# Checks if the object was recently created
|
44
|
+
#
|
45
|
+
# @param threshold [Integer] threshold in seconds (default: 1 hour)
|
46
|
+
# @return [Boolean] true if created within threshold
|
47
|
+
def recently_created?(threshold = 3600)
|
48
|
+
age < threshold
|
49
|
+
end
|
50
|
+
|
51
|
+
# Checks if the object was recently updated
|
52
|
+
#
|
53
|
+
# @param threshold [Integer] threshold in seconds (default: 1 hour)
|
54
|
+
# @return [Boolean] true if updated within threshold
|
55
|
+
def recently_updated?(threshold = 3600)
|
56
|
+
(current_time - updated_at) < threshold
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# Returns current time (compatible with and without Rails)
|
62
|
+
#
|
63
|
+
# @return [Time] current time
|
64
|
+
def current_time
|
65
|
+
if defined?(Time.current)
|
66
|
+
Time.current
|
67
|
+
else
|
68
|
+
Time.now
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Storage
|
5
|
+
module Concerns
|
6
|
+
# Provides validation capabilities for storage classes
|
7
|
+
#
|
8
|
+
# This concern adds common validation methods and patterns used
|
9
|
+
# across different storage implementations.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# class MyStorage < BaseStorage
|
13
|
+
# include Concerns::Validatable
|
14
|
+
#
|
15
|
+
# def save(data)
|
16
|
+
# validate_presence!(data, :id, :name)
|
17
|
+
# # save logic
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
module Validatable
|
21
|
+
def self.included(base)
|
22
|
+
base.extend(ClassMethods)
|
23
|
+
end
|
24
|
+
|
25
|
+
module ClassMethods
|
26
|
+
# Defines required attributes for validation
|
27
|
+
#
|
28
|
+
# @param attributes [Array<Symbol>] list of required attributes
|
29
|
+
# @return [void]
|
30
|
+
def validates_presence_of(*attributes)
|
31
|
+
@required_attributes = attributes
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns list of required attributes
|
35
|
+
#
|
36
|
+
# @return [Array<Symbol>] required attributes
|
37
|
+
def required_attributes
|
38
|
+
@required_attributes || []
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Validates presence of required attributes
|
43
|
+
#
|
44
|
+
# @param data [Hash] data to validate
|
45
|
+
# @param attributes [Array<Symbol>] specific attributes to check
|
46
|
+
# @raise [ValidationError] if any required attribute is missing
|
47
|
+
# @return [void]
|
48
|
+
def validate_presence!(data, *attributes)
|
49
|
+
attrs_to_check = attributes.any? ? attributes : self.class.required_attributes
|
50
|
+
|
51
|
+
attrs_to_check.each do |attr|
|
52
|
+
next if data.key?(attr) && !blank_value?(data[attr])
|
53
|
+
|
54
|
+
raise ValidationError, "#{attr} is required but was #{data[attr].inspect}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Validates that an ID is present and valid
|
59
|
+
#
|
60
|
+
# @param id [String, Integer, nil] ID to validate
|
61
|
+
# @raise [ValidationError] if ID is invalid
|
62
|
+
# @return [void]
|
63
|
+
def validate_id!(id)
|
64
|
+
return unless id.nil? || id.to_s.strip.empty?
|
65
|
+
|
66
|
+
raise ValidationError, "ID cannot be nil or empty"
|
67
|
+
end
|
68
|
+
|
69
|
+
# Validates that a name is present and valid
|
70
|
+
#
|
71
|
+
# @param name [String, nil] name to validate
|
72
|
+
# @raise [ValidationError] if name is invalid
|
73
|
+
# @return [void]
|
74
|
+
def validate_name!(name)
|
75
|
+
return unless name.nil? || name.to_s.strip.empty?
|
76
|
+
|
77
|
+
raise ValidationError, "Name cannot be nil or empty"
|
78
|
+
end
|
79
|
+
|
80
|
+
# Checks if an ID is valid
|
81
|
+
#
|
82
|
+
# @param id [String, Integer, nil] ID to check
|
83
|
+
# @return [Boolean] true if ID is valid
|
84
|
+
def valid_id?(id)
|
85
|
+
!id.nil? && !id.to_s.strip.empty?
|
86
|
+
end
|
87
|
+
|
88
|
+
# Checks if a name is valid
|
89
|
+
#
|
90
|
+
# @param name [String, nil] name to check
|
91
|
+
# @return [Boolean] true if name is valid
|
92
|
+
def valid_name?(name)
|
93
|
+
!name.nil? && !name.to_s.strip.empty?
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
# Checks if a value should be considered blank
|
99
|
+
#
|
100
|
+
# @param value [Object] value to check
|
101
|
+
# @return [Boolean] true if value is blank
|
102
|
+
def blank_value?(value)
|
103
|
+
case value
|
104
|
+
when String
|
105
|
+
value.strip.empty?
|
106
|
+
when Array, Hash
|
107
|
+
value.empty?
|
108
|
+
when NilClass
|
109
|
+
true
|
110
|
+
else
|
111
|
+
false
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Storage
|
5
|
+
module DateHelper
|
6
|
+
DEFAULT_CLEANUP_DAYS = 30
|
7
|
+
|
8
|
+
def format_date(timestamp)
|
9
|
+
timestamp.strftime("%Y-%m-%d")
|
10
|
+
end
|
11
|
+
|
12
|
+
def cleanup_cutoff_date(days_to_keep = DEFAULT_CLEANUP_DAYS)
|
13
|
+
Time.now - (days_to_keep * 24 * 60 * 60)
|
14
|
+
end
|
15
|
+
|
16
|
+
def date_file_path(base_path, date)
|
17
|
+
File.join(base_path, "#{date}.json")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Storage
|
5
|
+
# Base storage error class
|
6
|
+
#
|
7
|
+
# All storage-related errors inherit from this class to provide
|
8
|
+
# consistent error handling across the storage module.
|
9
|
+
class StorageError < StandardError; end
|
10
|
+
|
11
|
+
# Raised when validation fails on storage operations
|
12
|
+
class ValidationError < StorageError; end
|
13
|
+
|
14
|
+
# Raised when a requested session cannot be found
|
15
|
+
class SessionNotFoundError < StorageError; end
|
16
|
+
|
17
|
+
# Raised when a requested query cannot be found
|
18
|
+
class QueryNotFoundError < StorageError; end
|
19
|
+
|
20
|
+
# Raised when a requested table cannot be found
|
21
|
+
class TableNotFoundError < StorageError; end
|
22
|
+
|
23
|
+
# Raised when storage data becomes corrupted
|
24
|
+
class CorruptedDataError < StorageError; end
|
25
|
+
|
26
|
+
# Raised when storage permissions are insufficient
|
27
|
+
class PermissionError < StorageError; end
|
28
|
+
|
29
|
+
# Provides error handling capabilities for storage operations
|
30
|
+
#
|
31
|
+
# This module can be included in storage classes to provide
|
32
|
+
# standardized error handling and logging capabilities.
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
# class MyStorage
|
36
|
+
# include ErrorHandler
|
37
|
+
#
|
38
|
+
# def risky_operation
|
39
|
+
# safe_operation("my operation") do
|
40
|
+
# # potentially failing code
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
module ErrorHandler
|
45
|
+
# Executes a block with error handling and optional default return value
|
46
|
+
#
|
47
|
+
# @param operation_name [String] description of the operation for logging
|
48
|
+
# @param default_value [Object] value to return if operation fails
|
49
|
+
# @yield [] the block to execute safely
|
50
|
+
# @return [Object] the result of the block or default_value on error
|
51
|
+
def safe_operation(operation_name, default_value = nil, &block)
|
52
|
+
block.call
|
53
|
+
rescue JSON::ParserError => e
|
54
|
+
log_error("JSON parsing failed in #{operation_name}", e)
|
55
|
+
default_value
|
56
|
+
rescue StandardError => e
|
57
|
+
log_error("#{operation_name} failed", e)
|
58
|
+
default_value
|
59
|
+
end
|
60
|
+
|
61
|
+
# Executes a block with error handling that raises StorageError on failure
|
62
|
+
#
|
63
|
+
# @param operation [String] description of the operation
|
64
|
+
# @yield [] the block to execute
|
65
|
+
# @return [Object] the result of the block
|
66
|
+
# @raise [StorageError] if the operation fails
|
67
|
+
def with_error_handling(operation, &block)
|
68
|
+
block.call
|
69
|
+
rescue StandardError => e
|
70
|
+
warn "Storage #{operation} failed: #{e.message}"
|
71
|
+
raise StorageError, "#{operation} failed: #{e.message}"
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
# Logs an error message with exception details
|
77
|
+
#
|
78
|
+
# @param message [String] the error message
|
79
|
+
# @param error [Exception] the exception that occurred
|
80
|
+
# @return [void]
|
81
|
+
def log_error(message, error)
|
82
|
+
warn "#{message}: #{error.message}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Storage
|
5
|
+
# Manages file system operations for storage classes
|
6
|
+
#
|
7
|
+
# This class provides a centralized interface for file operations
|
8
|
+
# including JSON serialization, directory management, and file
|
9
|
+
# system utilities used throughout the storage module.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# manager = FileManager.new("/path/to/storage")
|
13
|
+
# manager.write_json("data.json", { key: "value" })
|
14
|
+
# data = manager.read_json("data.json")
|
15
|
+
class FileManager
|
16
|
+
# @return [String] the base storage path
|
17
|
+
attr_reader :storage_path
|
18
|
+
|
19
|
+
# Initializes file manager with storage path
|
20
|
+
#
|
21
|
+
# Creates the base storage directory if it doesn't exist.
|
22
|
+
#
|
23
|
+
# @param storage_path [String] base path for file operations
|
24
|
+
def initialize(storage_path)
|
25
|
+
@storage_path = storage_path
|
26
|
+
ensure_directories
|
27
|
+
end
|
28
|
+
|
29
|
+
# Writes data to a JSON file
|
30
|
+
#
|
31
|
+
# Serializes the provided data as pretty-printed JSON and writes
|
32
|
+
# it to the specified file path.
|
33
|
+
#
|
34
|
+
# @param file_path [String] path to write the JSON file
|
35
|
+
# @param data [Object] data to serialize as JSON
|
36
|
+
# @return [Integer] number of bytes written
|
37
|
+
# @raise [JSON::GeneratorError] if data cannot be serialized
|
38
|
+
def write_json(file_path, data)
|
39
|
+
# Ensure parent directory exists
|
40
|
+
ensure_directory(File.dirname(file_path))
|
41
|
+
File.write(file_path, JSON.pretty_generate(data))
|
42
|
+
end
|
43
|
+
|
44
|
+
# Reads and parses a JSON file
|
45
|
+
#
|
46
|
+
# Reads the specified file and parses it as JSON with symbolized keys.
|
47
|
+
# Returns empty array if file doesn't exist.
|
48
|
+
#
|
49
|
+
# @param file_path [String] path to the JSON file
|
50
|
+
# @return [Object] parsed JSON data with symbolized keys
|
51
|
+
# @raise [JSON::ParserError] if file contains invalid JSON
|
52
|
+
def read_json(file_path)
|
53
|
+
return default_empty_result unless File.exist?(file_path)
|
54
|
+
|
55
|
+
JSON.parse(File.read(file_path), symbolize_names: true)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Checks if a file exists
|
59
|
+
#
|
60
|
+
# @param file_path [String] path to check
|
61
|
+
# @return [Boolean] true if file exists
|
62
|
+
def file_exists?(file_path)
|
63
|
+
File.exist?(file_path)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Deletes a file
|
67
|
+
#
|
68
|
+
# @param file_path [String] path to file to delete
|
69
|
+
# @return [Integer] number of files deleted (1 or 0)
|
70
|
+
def delete_file(file_path)
|
71
|
+
File.delete(file_path)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Deletes a directory and its contents
|
75
|
+
#
|
76
|
+
# @param dir_path [String] path to directory to delete
|
77
|
+
# @return [Boolean] true if directory was deleted, false if it didn't exist
|
78
|
+
def delete_directory(dir_path)
|
79
|
+
return false unless Dir.exist?(dir_path)
|
80
|
+
|
81
|
+
FileUtils.rm_rf(dir_path)
|
82
|
+
true
|
83
|
+
rescue Errno::ENOENT
|
84
|
+
false
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns files matching a glob pattern
|
88
|
+
#
|
89
|
+
# @param pattern [String] glob pattern to match
|
90
|
+
# @return [Array<String>] array of matching file paths
|
91
|
+
def glob_files(pattern)
|
92
|
+
Dir.glob(pattern)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Ensures a directory exists
|
96
|
+
#
|
97
|
+
# Creates the directory and any necessary parent directories.
|
98
|
+
#
|
99
|
+
# @param path [String] directory path to create
|
100
|
+
# @return [Array, nil] array of created directories or nil if already exists
|
101
|
+
def ensure_directory(path)
|
102
|
+
FileUtils.mkdir_p(path)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
# Creates the base storage directory
|
108
|
+
#
|
109
|
+
# @return [Array, nil] array of created directories or nil if already exists
|
110
|
+
def ensure_directories
|
111
|
+
FileUtils.mkdir_p(@storage_path)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Default return value for empty JSON files
|
115
|
+
#
|
116
|
+
# @return [Array] empty array as default
|
117
|
+
def default_empty_result
|
118
|
+
[]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Storage
|
5
|
+
class NullSession
|
6
|
+
def self.instance
|
7
|
+
@instance ||= new
|
8
|
+
end
|
9
|
+
|
10
|
+
def id
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def name
|
15
|
+
"Unknown Session"
|
16
|
+
end
|
17
|
+
|
18
|
+
def changes
|
19
|
+
[]
|
20
|
+
end
|
21
|
+
|
22
|
+
def started_at
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def ended_at
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def present?
|
31
|
+
false
|
32
|
+
end
|
33
|
+
|
34
|
+
def nil?
|
35
|
+
true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|