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,188 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "relationship_params"
|
4
|
+
|
5
|
+
module Dbwatcher
|
6
|
+
module Services
|
7
|
+
module DiagramData
|
8
|
+
# Standard relationship between entities
|
9
|
+
#
|
10
|
+
# This class provides a standardized representation for all diagram relationships
|
11
|
+
# (edges, connections, associations, foreign keys, etc.) with consistent validation.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# relationship = Relationship.new(
|
15
|
+
# source_id: "users",
|
16
|
+
# target_id: "orders",
|
17
|
+
# type: "has_many",
|
18
|
+
# label: "orders",
|
19
|
+
# cardinality: "one_to_many",
|
20
|
+
# metadata: { association_type: "has_many" }
|
21
|
+
# )
|
22
|
+
# relationship.valid? # => true
|
23
|
+
# relationship.to_h # => { source_id: "users", target_id: "orders", ... }
|
24
|
+
class Relationship
|
25
|
+
attr_accessor :source_id, :target_id, :type, :label, :cardinality, :metadata
|
26
|
+
|
27
|
+
# Valid cardinality types
|
28
|
+
VALID_CARDINALITIES = [
|
29
|
+
"one_to_one",
|
30
|
+
"one_to_many",
|
31
|
+
"many_to_one",
|
32
|
+
"many_to_many",
|
33
|
+
nil
|
34
|
+
].freeze
|
35
|
+
|
36
|
+
# Cardinality mapping for relationship types
|
37
|
+
CARDINALITY_MAPPING = {
|
38
|
+
"has_many" => "one_to_many",
|
39
|
+
"belongs_to" => "many_to_one",
|
40
|
+
"has_one" => "one_to_one",
|
41
|
+
"has_and_belongs_to_many" => "many_to_many"
|
42
|
+
}.freeze
|
43
|
+
|
44
|
+
# ERD cardinality notations
|
45
|
+
ERD_NOTATIONS = {
|
46
|
+
"one_to_many" => "||--o{",
|
47
|
+
"many_to_one" => "}o--||",
|
48
|
+
"one_to_one" => "||--||",
|
49
|
+
"many_to_many" => "}|--|{"
|
50
|
+
}.freeze
|
51
|
+
|
52
|
+
# Default ERD notation
|
53
|
+
DEFAULT_ERD_NOTATION = "||--o{" # Default to one-to-many
|
54
|
+
|
55
|
+
# Initialize relationship
|
56
|
+
#
|
57
|
+
# @param params [RelationshipParams, Hash] relationship parameters
|
58
|
+
# @return [Relationship] new relationship instance
|
59
|
+
def initialize(params)
|
60
|
+
params = RelationshipParams.new(params) if params.is_a?(Hash)
|
61
|
+
|
62
|
+
@source_id = params.source_id.to_s
|
63
|
+
@target_id = params.target_id.to_s
|
64
|
+
@type = params.type.to_s
|
65
|
+
@label = params.label&.to_s
|
66
|
+
@cardinality = params.cardinality&.to_s
|
67
|
+
@metadata = params.metadata.is_a?(Hash) ? params.metadata : {}
|
68
|
+
end
|
69
|
+
|
70
|
+
# Check if relationship is valid
|
71
|
+
#
|
72
|
+
# @return [Boolean] true if relationship has required fields
|
73
|
+
def valid?
|
74
|
+
validation_errors.empty?
|
75
|
+
end
|
76
|
+
|
77
|
+
# Get validation errors
|
78
|
+
#
|
79
|
+
# @return [Array<String>] array of validation error messages
|
80
|
+
def validation_errors
|
81
|
+
errors = []
|
82
|
+
errors << "Source ID cannot be blank" if source_id.nil? || source_id.to_s.strip.empty?
|
83
|
+
errors << "Target ID cannot be blank" if target_id.nil? || target_id.to_s.strip.empty?
|
84
|
+
errors << "Type cannot be blank" if type.nil? || type.to_s.strip.empty?
|
85
|
+
|
86
|
+
# Allow self-referential relationships when explicitly marked as such
|
87
|
+
errors << "Source and target cannot be the same" if !metadata[:self_referential] && (source_id == target_id)
|
88
|
+
|
89
|
+
errors << "Invalid cardinality: #{cardinality}" if cardinality && !VALID_CARDINALITIES.include?(cardinality)
|
90
|
+
errors << "Metadata must be a Hash" unless metadata.is_a?(Hash)
|
91
|
+
errors
|
92
|
+
end
|
93
|
+
|
94
|
+
# Infer cardinality from relationship type if not explicitly set
|
95
|
+
#
|
96
|
+
# @return [String, nil] inferred cardinality or nil if can't be determined
|
97
|
+
def infer_cardinality
|
98
|
+
return cardinality if cardinality
|
99
|
+
|
100
|
+
CARDINALITY_MAPPING[type]
|
101
|
+
end
|
102
|
+
|
103
|
+
# Get cardinality for ERD notation
|
104
|
+
#
|
105
|
+
# @return [String] ERD cardinality notation
|
106
|
+
def erd_cardinality_notation
|
107
|
+
# Default to one-to-many if not recognized
|
108
|
+
ERD_NOTATIONS[infer_cardinality] || DEFAULT_ERD_NOTATION
|
109
|
+
end
|
110
|
+
|
111
|
+
# Serialize relationship to hash
|
112
|
+
#
|
113
|
+
# @return [Hash] serialized relationship data
|
114
|
+
def to_h
|
115
|
+
{
|
116
|
+
source_id: source_id,
|
117
|
+
target_id: target_id,
|
118
|
+
type: type,
|
119
|
+
label: label,
|
120
|
+
cardinality: cardinality,
|
121
|
+
metadata: metadata
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
# Serialize relationship to JSON
|
126
|
+
#
|
127
|
+
# @return [String] JSON representation
|
128
|
+
def to_json(*args)
|
129
|
+
to_h.to_json(*args)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Create relationship from hash
|
133
|
+
#
|
134
|
+
# @param hash [Hash] relationship data
|
135
|
+
# @return [Relationship] new relationship instance
|
136
|
+
def self.from_h(hash)
|
137
|
+
hash = hash.transform_keys(&:to_sym) if hash.keys.first.is_a?(String)
|
138
|
+
new(hash)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Create relationship from JSON
|
142
|
+
#
|
143
|
+
# @param json [String] JSON string
|
144
|
+
# @return [Relationship] new relationship instance
|
145
|
+
def self.from_json(json)
|
146
|
+
from_h(JSON.parse(json))
|
147
|
+
end
|
148
|
+
|
149
|
+
# Check equality with another relationship
|
150
|
+
#
|
151
|
+
# @param other [Relationship] other relationship to compare
|
152
|
+
# @return [Boolean] true if relationships are equal
|
153
|
+
def ==(other)
|
154
|
+
return false unless other.is_a?(Relationship)
|
155
|
+
|
156
|
+
source_id == other.source_id &&
|
157
|
+
target_id == other.target_id &&
|
158
|
+
type == other.type &&
|
159
|
+
label == other.label &&
|
160
|
+
cardinality == other.cardinality &&
|
161
|
+
metadata == other.metadata
|
162
|
+
end
|
163
|
+
|
164
|
+
# Generate hash code for relationship
|
165
|
+
#
|
166
|
+
# @return [Integer] hash code
|
167
|
+
def hash
|
168
|
+
[source_id, target_id, type, label, cardinality, metadata].hash
|
169
|
+
end
|
170
|
+
|
171
|
+
# String representation of relationship
|
172
|
+
#
|
173
|
+
# @return [String] string representation
|
174
|
+
def to_s
|
175
|
+
"#{self.class.name}(source: #{source_id}, target: #{target_id}, type: #{type})"
|
176
|
+
end
|
177
|
+
|
178
|
+
# Detailed string representation
|
179
|
+
#
|
180
|
+
# @return [String] detailed string representation
|
181
|
+
def inspect
|
182
|
+
"#{self.class.name}(source: #{source_id.inspect}, target: #{target_id.inspect}, " \
|
183
|
+
"type: #{type.inspect}, label: #{label.inspect}, cardinality: #{cardinality.inspect})"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Services
|
5
|
+
module DiagramData
|
6
|
+
# Parameter object for relationship creation
|
7
|
+
#
|
8
|
+
# This class encapsulates parameters used for creating relationships
|
9
|
+
# to avoid long parameter lists.
|
10
|
+
class RelationshipParams
|
11
|
+
attr_accessor :source_id, :target_id, :type, :label, :cardinality, :metadata
|
12
|
+
|
13
|
+
# Initialize relationship parameters
|
14
|
+
#
|
15
|
+
# @param params [Hash] hash containing all parameters
|
16
|
+
# @option params [String] :source_id ID of the source entity
|
17
|
+
# @option params [String] :target_id ID of the target entity
|
18
|
+
# @option params [String] :type relationship type
|
19
|
+
# @option params [String] :label optional display label
|
20
|
+
# @option params [String] :cardinality optional cardinality type
|
21
|
+
# @option params [Hash] :metadata additional information
|
22
|
+
def initialize(params = {})
|
23
|
+
@source_id = params[:source_id]
|
24
|
+
@target_id = params[:target_id]
|
25
|
+
@type = params[:type]
|
26
|
+
@label = params[:label]
|
27
|
+
@cardinality = params[:cardinality]
|
28
|
+
@metadata = params[:metadata] || {}
|
29
|
+
end
|
30
|
+
|
31
|
+
# Create from individual parameters
|
32
|
+
#
|
33
|
+
# @param params [Hash] parameters hash
|
34
|
+
# @return [RelationshipParams] new instance
|
35
|
+
def self.create(params)
|
36
|
+
new(params)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Convert to hash
|
40
|
+
#
|
41
|
+
# @return [Hash] hash representation of parameters
|
42
|
+
def to_h
|
43
|
+
{
|
44
|
+
source_id: source_id,
|
45
|
+
target_id: target_id,
|
46
|
+
type: type,
|
47
|
+
label: label,
|
48
|
+
cardinality: cardinality,
|
49
|
+
metadata: metadata
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "diagram_data/entity"
|
4
|
+
require_relative "diagram_data/relationship"
|
5
|
+
require_relative "diagram_data/dataset"
|
6
|
+
|
7
|
+
module Dbwatcher
|
8
|
+
module Services
|
9
|
+
# DiagramData module provides standardized data models for diagram generation
|
10
|
+
#
|
11
|
+
# This module contains the core data structures used to represent diagram
|
12
|
+
# entities and relationships in a consistent, validated format that can be
|
13
|
+
# consumed by any diagram strategy.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# # Create entities
|
17
|
+
# user_entity = Dbwatcher::Services::DiagramData::Entity.new(
|
18
|
+
# id: "users", name: "User", type: "table"
|
19
|
+
# )
|
20
|
+
# order_entity = Dbwatcher::Services::DiagramData::Entity.new(
|
21
|
+
# id: "orders", name: "Order", type: "table"
|
22
|
+
# )
|
23
|
+
#
|
24
|
+
# # Create relationship
|
25
|
+
# relationship = Dbwatcher::Services::DiagramData::Relationship.new(
|
26
|
+
# source_id: "users", target_id: "orders", type: "has_many"
|
27
|
+
# )
|
28
|
+
#
|
29
|
+
# # Create dataset
|
30
|
+
# dataset = Dbwatcher::Services::DiagramData::Dataset.new
|
31
|
+
# dataset.add_entity(user_entity)
|
32
|
+
# dataset.add_entity(order_entity)
|
33
|
+
# dataset.add_relationship(relationship)
|
34
|
+
#
|
35
|
+
# # Validate and use
|
36
|
+
# if dataset.valid?
|
37
|
+
# puts dataset.stats
|
38
|
+
# end
|
39
|
+
module DiagramData
|
40
|
+
# Convenience method to create a new Entity
|
41
|
+
#
|
42
|
+
# @param args [Hash] entity arguments
|
43
|
+
# @return [Entity] new entity instance
|
44
|
+
def self.entity(**args)
|
45
|
+
Entity.new(**args)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Convenience method to create a new Relationship
|
49
|
+
#
|
50
|
+
# @param args [Hash] relationship arguments
|
51
|
+
# @return [Relationship] new relationship instance
|
52
|
+
def self.relationship(**args)
|
53
|
+
Relationship.new(**args)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Convenience method to create a new Dataset
|
57
|
+
#
|
58
|
+
# @param args [Hash] dataset arguments
|
59
|
+
# @return [Dataset] new dataset instance
|
60
|
+
def self.dataset(**args)
|
61
|
+
Dataset.new(**args)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Services
|
5
|
+
# Centralized error handling for diagram generation
|
6
|
+
#
|
7
|
+
# Provides consistent error categorization, logging, and response formatting
|
8
|
+
# for all diagram generation failures with recovery strategies.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# handler = DiagramErrorHandler.new
|
12
|
+
# response = handler.handle_generation_error(error, context)
|
13
|
+
# # => { success: false, error_code: 'DIAGRAM_001', message: '...', recoverable: false }
|
14
|
+
class DiagramErrorHandler
|
15
|
+
# Custom error class for diagram generation failures
|
16
|
+
class DiagramGenerationError < StandardError
|
17
|
+
attr_reader :error_code, :context, :original_error
|
18
|
+
|
19
|
+
def initialize(message, error_code: nil, context: {}, original_error: nil)
|
20
|
+
super(message)
|
21
|
+
@error_code = error_code
|
22
|
+
@context = context
|
23
|
+
@original_error = original_error
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Error code mapping for consistent error identification
|
28
|
+
ERROR_CODES = {
|
29
|
+
session_not_found: "DIAGRAM_001",
|
30
|
+
invalid_diagram_type: "DIAGRAM_002",
|
31
|
+
syntax_validation_failed: "DIAGRAM_003",
|
32
|
+
generation_timeout: "DIAGRAM_004",
|
33
|
+
insufficient_data: "DIAGRAM_005",
|
34
|
+
analyzer_error: "DIAGRAM_006",
|
35
|
+
cache_error: "DIAGRAM_007",
|
36
|
+
system_error: "DIAGRAM_099"
|
37
|
+
}.freeze
|
38
|
+
|
39
|
+
# Initialize error handler with configuration
|
40
|
+
#
|
41
|
+
# @param config [Hash] error handler configuration
|
42
|
+
# @option config [Logger] :logger logger instance
|
43
|
+
# @option config [Boolean] :include_backtrace include backtrace in logs
|
44
|
+
# @option config [Integer] :backtrace_lines number of backtrace lines to log
|
45
|
+
def initialize(config = {})
|
46
|
+
@config = default_config.merge(config)
|
47
|
+
@logger = @config[:logger] || default_logger
|
48
|
+
end
|
49
|
+
|
50
|
+
# Handle diagram generation error with categorization and logging
|
51
|
+
#
|
52
|
+
# @param error [Exception] the original error
|
53
|
+
# @param context [Hash] additional context information
|
54
|
+
# @return [Hash] formatted error response
|
55
|
+
def handle_generation_error(error, context = {})
|
56
|
+
error_info = categorize_error(error, context)
|
57
|
+
log_error(error_info)
|
58
|
+
|
59
|
+
# Return user-friendly error response
|
60
|
+
create_error_response(error_info)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Check if an error type is recoverable
|
64
|
+
#
|
65
|
+
# @param error_code [String] error code
|
66
|
+
# @return [Boolean] true if error is recoverable
|
67
|
+
def recoverable_error?(error_code)
|
68
|
+
recoverable_codes = [
|
69
|
+
ERROR_CODES[:syntax_validation_failed],
|
70
|
+
ERROR_CODES[:generation_timeout],
|
71
|
+
ERROR_CODES[:insufficient_data],
|
72
|
+
ERROR_CODES[:cache_error]
|
73
|
+
]
|
74
|
+
recoverable_codes.include?(error_code)
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Default configuration
|
80
|
+
#
|
81
|
+
# @return [Hash] default configuration
|
82
|
+
def default_config
|
83
|
+
{
|
84
|
+
include_backtrace: true,
|
85
|
+
backtrace_lines: 5
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
# Default logger when no logger is provided
|
90
|
+
#
|
91
|
+
# @return [Logger] default logger instance
|
92
|
+
def default_logger
|
93
|
+
# Use Rails logger if available, otherwise create a simple logger
|
94
|
+
if defined?(Rails) && Rails.respond_to?(:logger)
|
95
|
+
Rails.logger
|
96
|
+
else
|
97
|
+
require "logger"
|
98
|
+
Logger.new($stdout)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Categorize error based on type and context
|
103
|
+
#
|
104
|
+
# @param error [Exception] the original error
|
105
|
+
# @param context [Hash] error context
|
106
|
+
# @return [Hash] categorized error information
|
107
|
+
def categorize_error(error, context)
|
108
|
+
# Check for session not found errors first
|
109
|
+
if error.message.include?("Session") && error.message.include?("not found")
|
110
|
+
return {
|
111
|
+
type: :session_not_found,
|
112
|
+
code: ERROR_CODES[:session_not_found],
|
113
|
+
message: "Session not found: #{context[:session_id]}",
|
114
|
+
recoverable: false,
|
115
|
+
original_error: error,
|
116
|
+
user_message: "The requested session could not be found. Please verify the session ID."
|
117
|
+
}
|
118
|
+
end
|
119
|
+
|
120
|
+
case error
|
121
|
+
when ArgumentError
|
122
|
+
if error.message.include?("Unknown diagram type") || error.message.include?("Invalid diagram type")
|
123
|
+
{
|
124
|
+
type: :invalid_diagram_type,
|
125
|
+
code: ERROR_CODES[:invalid_diagram_type],
|
126
|
+
message: "Invalid diagram type: #{context[:diagram_type]}",
|
127
|
+
recoverable: false,
|
128
|
+
original_error: error,
|
129
|
+
user_message: "The requested diagram type is not supported."
|
130
|
+
}
|
131
|
+
else
|
132
|
+
categorize_generic_error(error, context)
|
133
|
+
end
|
134
|
+
when StandardError
|
135
|
+
if error.message.include?("syntax") || error.message.include?("Mermaid")
|
136
|
+
{
|
137
|
+
type: :syntax_validation_failed,
|
138
|
+
code: ERROR_CODES[:syntax_validation_failed],
|
139
|
+
message: "Diagram syntax validation failed: #{error.message}",
|
140
|
+
recoverable: true,
|
141
|
+
original_error: error,
|
142
|
+
user_message: "There was an issue generating the diagram syntax. Please try again."
|
143
|
+
}
|
144
|
+
elsif error.message.include?("timeout") || error.is_a?(Timeout::Error)
|
145
|
+
{
|
146
|
+
type: :generation_timeout,
|
147
|
+
code: ERROR_CODES[:generation_timeout],
|
148
|
+
message: "Diagram generation timed out",
|
149
|
+
recoverable: true,
|
150
|
+
original_error: error,
|
151
|
+
user_message: "Diagram generation took too long. Please try again with a smaller dataset."
|
152
|
+
}
|
153
|
+
elsif error.message.include?("No") && error.message.include?("found")
|
154
|
+
{
|
155
|
+
type: :insufficient_data,
|
156
|
+
code: ERROR_CODES[:insufficient_data],
|
157
|
+
message: "Insufficient data for diagram generation: #{error.message}",
|
158
|
+
recoverable: true,
|
159
|
+
original_error: error,
|
160
|
+
user_message: "Not enough data available to generate the diagram."
|
161
|
+
}
|
162
|
+
elsif error.message.include?("cache") || error.message.include?("Cache")
|
163
|
+
{
|
164
|
+
type: :cache_error,
|
165
|
+
code: ERROR_CODES[:cache_error],
|
166
|
+
message: "Cache operation failed: #{error.message}",
|
167
|
+
recoverable: true,
|
168
|
+
original_error: error,
|
169
|
+
user_message: "A temporary caching issue occurred. The diagram was still generated."
|
170
|
+
}
|
171
|
+
else
|
172
|
+
categorize_generic_error(error, context)
|
173
|
+
end
|
174
|
+
else
|
175
|
+
categorize_generic_error(error, context)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Categorize generic/unknown errors
|
180
|
+
#
|
181
|
+
# @param error [Exception] the original error
|
182
|
+
# @param context [Hash] error context
|
183
|
+
# @return [Hash] categorized error information
|
184
|
+
def categorize_generic_error(error, _context)
|
185
|
+
{
|
186
|
+
type: :system_error,
|
187
|
+
code: ERROR_CODES[:system_error],
|
188
|
+
message: "Unexpected error during diagram generation: #{error.class.name}",
|
189
|
+
recoverable: false,
|
190
|
+
original_error: error,
|
191
|
+
user_message: "An unexpected error occurred. Please try again or contact support."
|
192
|
+
}
|
193
|
+
end
|
194
|
+
|
195
|
+
# Log error with appropriate level and detail
|
196
|
+
#
|
197
|
+
# @param error_info [Hash] categorized error information
|
198
|
+
def log_error(error_info)
|
199
|
+
log_data = {
|
200
|
+
error_code: error_info[:code],
|
201
|
+
error_type: error_info[:type],
|
202
|
+
message: error_info[:message],
|
203
|
+
recoverable: error_info[:recoverable],
|
204
|
+
original_error_class: error_info[:original_error]&.class&.name
|
205
|
+
}
|
206
|
+
|
207
|
+
# Add backtrace if configured and available
|
208
|
+
if @config[:include_backtrace] && error_info[:original_error]&.backtrace
|
209
|
+
log_data[:backtrace] = error_info[:original_error].backtrace.first(@config[:backtrace_lines])
|
210
|
+
end
|
211
|
+
|
212
|
+
# Log at appropriate level based on recoverability
|
213
|
+
if error_info[:recoverable]
|
214
|
+
@logger.warn "Recoverable diagram generation error: #{error_info[:message]} (#{error_info[:code]})"
|
215
|
+
else
|
216
|
+
@logger.error "Non-recoverable diagram generation error: #{error_info[:message]} (#{error_info[:code]})"
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Create formatted error response
|
221
|
+
#
|
222
|
+
# @param error_info [Hash] categorized error information
|
223
|
+
# @return [Hash] formatted error response
|
224
|
+
def create_error_response(error_info)
|
225
|
+
{
|
226
|
+
success: false,
|
227
|
+
error: error_info[:user_message] || error_info[:message],
|
228
|
+
error_code: error_info[:code],
|
229
|
+
error_type: error_info[:type],
|
230
|
+
message: error_info[:user_message] || error_info[:message],
|
231
|
+
recoverable: error_info[:recoverable],
|
232
|
+
timestamp: Time.now.iso8601,
|
233
|
+
content: nil,
|
234
|
+
type: nil
|
235
|
+
}
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "diagram_error_handler"
|
4
|
+
require_relative "diagram_type_registry"
|
5
|
+
|
6
|
+
module Dbwatcher
|
7
|
+
module Services
|
8
|
+
# Orchestrator for diagram generation using strategy pattern
|
9
|
+
#
|
10
|
+
# This service coordinates diagram generation by delegating to appropriate
|
11
|
+
# analyzer and strategy classes with clean error handling.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# generator = DiagramGenerator.new(session_id, 'database_tables')
|
15
|
+
# result = generator.call
|
16
|
+
# # => { content: "erDiagram\n USERS ||--o{ ORDERS : user_id", type: 'erDiagram' }
|
17
|
+
class DiagramGenerator < BaseService
|
18
|
+
attr_reader :session_id, :diagram_type, :registry, :error_handler, :logger
|
19
|
+
|
20
|
+
# Initialize with session id and diagram type
|
21
|
+
#
|
22
|
+
# @param session_id [String] session identifier
|
23
|
+
# @param diagram_type [String] type of diagram to generate
|
24
|
+
# @param dependencies [Hash] optional dependency injection
|
25
|
+
# @option dependencies [DiagramTypeRegistry] :registry type registry
|
26
|
+
# @option dependencies [DiagramErrorHandler] :error_handler error handler
|
27
|
+
# @option dependencies [Logger] :logger logger instance
|
28
|
+
def initialize(session_id, diagram_type = "database_tables", dependencies = {})
|
29
|
+
@session_id = session_id
|
30
|
+
@diagram_type = diagram_type
|
31
|
+
@registry = dependencies[:registry] || DiagramTypeRegistry.new
|
32
|
+
@error_handler = dependencies[:error_handler] || DiagramErrorHandler.new
|
33
|
+
@logger = dependencies[:logger] || default_logger
|
34
|
+
super()
|
35
|
+
end
|
36
|
+
|
37
|
+
# Generate diagram for session
|
38
|
+
#
|
39
|
+
# @return [Hash] diagram data with content and type
|
40
|
+
def call
|
41
|
+
@logger.info("Generating diagram for session #{@session_id} with type #{@diagram_type}")
|
42
|
+
start_time = Time.now
|
43
|
+
|
44
|
+
begin
|
45
|
+
result = generate_diagram
|
46
|
+
log_completion(start_time, result)
|
47
|
+
result
|
48
|
+
rescue StandardError => e
|
49
|
+
@error_handler.handle_generation_error(e, error_context)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Get available diagram types with metadata
|
54
|
+
#
|
55
|
+
# @return [Hash] diagram types with metadata
|
56
|
+
def available_types
|
57
|
+
@registry.available_types_with_metadata
|
58
|
+
end
|
59
|
+
|
60
|
+
# Get available diagram types (class method for backward compatibility)
|
61
|
+
#
|
62
|
+
# @return [Hash] diagram types with metadata
|
63
|
+
def self.available_types
|
64
|
+
DiagramTypeRegistry.new.available_types_with_metadata
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# Default logger when no logger is provided
|
70
|
+
#
|
71
|
+
# @return [Logger] default logger instance
|
72
|
+
def default_logger
|
73
|
+
# Use Rails logger if available, otherwise create a simple logger
|
74
|
+
if defined?(Rails) && Rails.respond_to?(:logger)
|
75
|
+
Rails.logger
|
76
|
+
else
|
77
|
+
require "logger"
|
78
|
+
Logger.new($stdout)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Generate diagram using standardized analyzer-to-strategy flow
|
83
|
+
#
|
84
|
+
# @return [Hash] diagram generation result
|
85
|
+
def generate_diagram
|
86
|
+
# Validate diagram type
|
87
|
+
unless @registry.type_exists?(@diagram_type)
|
88
|
+
raise DiagramTypeRegistry::UnknownTypeError, "Invalid diagram type: #{@diagram_type}"
|
89
|
+
end
|
90
|
+
|
91
|
+
# Load session
|
92
|
+
session = load_session
|
93
|
+
return error_result("Session not found") unless session
|
94
|
+
|
95
|
+
# Create analyzer and generate dataset
|
96
|
+
analyzer = @registry.create_analyzer(@diagram_type, session)
|
97
|
+
dataset = analyzer.call
|
98
|
+
|
99
|
+
@logger.debug("Generated dataset with #{dataset.entities.size} entities and " \
|
100
|
+
"#{dataset.relationships.size} relationships")
|
101
|
+
|
102
|
+
# Create strategy and generate diagram from dataset
|
103
|
+
strategy = @registry.create_strategy(@diagram_type)
|
104
|
+
strategy.generate_from_dataset(dataset)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Load session for analysis
|
108
|
+
#
|
109
|
+
# @return [Object] session object
|
110
|
+
def load_session
|
111
|
+
Dbwatcher::Storage.sessions.find(@session_id)
|
112
|
+
rescue StandardError => e
|
113
|
+
@logger.warn("Could not load session #{@session_id}: #{e.message}")
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
|
117
|
+
# Build error context for error handler
|
118
|
+
#
|
119
|
+
# @return [Hash] error context
|
120
|
+
def error_context
|
121
|
+
{
|
122
|
+
session_id: @session_id,
|
123
|
+
diagram_type: @diagram_type,
|
124
|
+
timestamp: Time.now
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
# Create error result
|
129
|
+
#
|
130
|
+
# @param message [String] error message
|
131
|
+
# @return [Hash] error result
|
132
|
+
def error_result(message)
|
133
|
+
{
|
134
|
+
success: false,
|
135
|
+
error: message,
|
136
|
+
content: nil,
|
137
|
+
type: nil,
|
138
|
+
generated_at: Time.now.iso8601
|
139
|
+
}
|
140
|
+
end
|
141
|
+
|
142
|
+
# Log generation completion
|
143
|
+
#
|
144
|
+
# @param start_time [Time] operation start time
|
145
|
+
# @param result [Hash] generation result
|
146
|
+
def log_completion(start_time, result)
|
147
|
+
duration = Time.now - start_time
|
148
|
+
success = result[:success] || false
|
149
|
+
@logger.info("Diagram generation completed for session #{@session_id} type #{@diagram_type} " \
|
150
|
+
"in #{(duration * 1000).round(2)}ms - Success: #{success}")
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|