dbwatcher 1.1.3 → 1.1.5
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 +79 -26
- data/app/assets/images/dbwatcher/apple-touch-icon.png +0 -0
- data/app/assets/images/dbwatcher/dbwatcher-social-preview.png +0 -0
- data/app/assets/images/dbwatcher/dbwatcher-tranparent_512x512.png +0 -0
- data/app/assets/images/dbwatcher/dbwatcher_512x512.png +0 -0
- data/app/assets/images/dbwatcher/favicon-96x96.png +0 -0
- data/app/assets/images/dbwatcher/favicon.ico +0 -0
- data/app/assets/images/dbwatcher/favicon.svg +3 -0
- data/app/assets/images/dbwatcher/site.webmanifest +21 -0
- data/app/assets/images/dbwatcher/web-app-manifest-192x192.png +0 -0
- data/app/assets/images/dbwatcher/web-app-manifest-512x512.png +0 -0
- data/app/assets/stylesheets/dbwatcher/application.css +38 -4
- data/app/assets/stylesheets/dbwatcher/components/_tabulator.scss +57 -13
- data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +14 -18
- data/app/controllers/dbwatcher/api/v1/system_info_controller.rb +1 -1
- data/app/controllers/dbwatcher/dashboard_controller.rb +1 -1
- data/app/views/dbwatcher/dashboard/_overview.html.erb +8 -7
- data/app/views/dbwatcher/sessions/index.html.erb +42 -59
- data/app/views/layouts/dbwatcher/application.html.erb +22 -6
- data/lib/dbwatcher/configuration.rb +51 -74
- data/lib/dbwatcher/logging.rb +23 -1
- data/lib/dbwatcher/services/diagram_analyzers/concerns/activerecord_introspection.rb +60 -0
- data/lib/dbwatcher/services/diagram_analyzers/concerns/association_scope_filtering.rb +60 -0
- data/lib/dbwatcher/services/diagram_analyzers/inferred_relationship_analyzer.rb +62 -36
- data/lib/dbwatcher/services/diagram_analyzers/model_analysis/association_extractor.rb +224 -0
- data/lib/dbwatcher/services/diagram_analyzers/model_analysis/dataset_builder.rb +226 -0
- data/lib/dbwatcher/services/diagram_analyzers/model_analysis/model_discovery.rb +161 -0
- data/lib/dbwatcher/services/diagram_analyzers/model_association_analyzer.rb +27 -514
- data/lib/dbwatcher/services/diagram_data/attribute.rb +22 -83
- data/lib/dbwatcher/services/diagram_data/base.rb +129 -0
- data/lib/dbwatcher/services/diagram_data/entity.rb +23 -72
- data/lib/dbwatcher/services/diagram_data/relationship.rb +15 -66
- data/lib/dbwatcher/services/diagram_generator.rb +35 -69
- data/lib/dbwatcher/services/diagram_strategies/base_diagram_strategy.rb +23 -9
- data/lib/dbwatcher/services/diagram_strategies/class_diagram_strategy.rb +16 -22
- data/lib/dbwatcher/services/diagram_strategies/diagram_strategy_helpers.rb +33 -0
- data/lib/dbwatcher/services/diagram_strategies/erd_diagram_strategy.rb +20 -25
- data/lib/dbwatcher/services/diagram_strategies/flowchart_diagram_strategy.rb +20 -25
- data/lib/dbwatcher/services/diagram_strategies/standard_diagram_strategy.rb +80 -0
- data/lib/dbwatcher/services/diagram_system.rb +14 -1
- data/lib/dbwatcher/services/mermaid_syntax/base_builder.rb +2 -0
- data/lib/dbwatcher/services/mermaid_syntax/erd_builder.rb +2 -2
- data/lib/dbwatcher/services/mermaid_syntax/sanitizer.rb +4 -14
- data/lib/dbwatcher/services/mermaid_syntax_builder.rb +10 -8
- data/lib/dbwatcher/services/system_info/runtime_info_collector.rb +7 -7
- data/lib/dbwatcher/services/system_info/system_info_collector.rb +3 -3
- data/lib/dbwatcher/services/timeline_data_service/entry_builder.rb +23 -1
- data/lib/dbwatcher/storage/session_storage.rb +2 -2
- data/lib/dbwatcher/storage.rb +1 -1
- data/lib/dbwatcher/version.rb +1 -1
- metadata +20 -2
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "base"
|
4
|
+
|
3
5
|
module Dbwatcher
|
4
6
|
module Services
|
5
7
|
module DiagramData
|
@@ -18,7 +20,7 @@ module Dbwatcher
|
|
18
20
|
# )
|
19
21
|
# attribute.valid? # => true
|
20
22
|
# attribute.to_h # => { name: "email", type: "string", ... }
|
21
|
-
class Attribute
|
23
|
+
class Attribute < Base
|
22
24
|
attr_accessor :name, :type, :nullable, :default, :metadata
|
23
25
|
|
24
26
|
# Initialize attribute
|
@@ -29,6 +31,7 @@ module Dbwatcher
|
|
29
31
|
# @param default [Object] default value
|
30
32
|
# @param metadata [Hash] additional type-specific information
|
31
33
|
def initialize(name:, type: nil, nullable: true, default: nil, metadata: {})
|
34
|
+
super() # Initialize parent class
|
32
35
|
@name = name.to_s
|
33
36
|
@type = type.to_s
|
34
37
|
@nullable = nullable == true
|
@@ -36,16 +39,23 @@ module Dbwatcher
|
|
36
39
|
@metadata = metadata.is_a?(Hash) ? metadata : {}
|
37
40
|
end
|
38
41
|
|
39
|
-
#
|
40
|
-
|
41
|
-
|
42
|
-
def valid?
|
43
|
-
validation_errors.empty?
|
42
|
+
# Implementation for Base class
|
43
|
+
def comparable_attributes
|
44
|
+
[name, type, nullable, default, metadata]
|
44
45
|
end
|
45
46
|
|
46
|
-
#
|
47
|
-
|
48
|
-
|
47
|
+
# Implementation for Base class
|
48
|
+
def serializable_attributes
|
49
|
+
{
|
50
|
+
name: name,
|
51
|
+
type: type,
|
52
|
+
nullable: nullable,
|
53
|
+
default: default,
|
54
|
+
metadata: metadata
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
# Implementation for Base class
|
49
59
|
def validation_errors
|
50
60
|
errors = []
|
51
61
|
errors << "Name cannot be blank" if name.nil? || name.to_s.strip.empty?
|
@@ -67,86 +77,15 @@ module Dbwatcher
|
|
67
77
|
metadata[:foreign_key] == true || name.to_s.end_with?("_id")
|
68
78
|
end
|
69
79
|
|
70
|
-
#
|
71
|
-
|
72
|
-
# @return [Hash] serialized attribute data
|
73
|
-
def to_h
|
80
|
+
# Override base class method to handle nullable default
|
81
|
+
def self.extract_constructor_args(hash)
|
74
82
|
{
|
75
|
-
name: name,
|
76
|
-
type: type,
|
77
|
-
nullable: nullable,
|
78
|
-
default: default,
|
79
|
-
metadata: metadata
|
80
|
-
}
|
81
|
-
end
|
82
|
-
|
83
|
-
# Serialize attribute to JSON
|
84
|
-
#
|
85
|
-
# @return [String] JSON representation
|
86
|
-
def to_json(*args)
|
87
|
-
to_h.to_json(*args)
|
88
|
-
end
|
89
|
-
|
90
|
-
# Create attribute from hash
|
91
|
-
#
|
92
|
-
# @param hash [Hash] attribute data
|
93
|
-
# @return [Attribute] new attribute instance
|
94
|
-
def self.from_h(hash)
|
95
|
-
# Convert string keys to symbols for consistent access
|
96
|
-
hash = hash.transform_keys(&:to_sym) if hash.keys.first.is_a?(String)
|
97
|
-
|
98
|
-
# Use fetch with default values to handle missing fields
|
99
|
-
new(
|
100
83
|
name: hash[:name],
|
101
84
|
type: hash[:type],
|
102
85
|
nullable: hash.key?(:nullable) ? hash[:nullable] : true,
|
103
86
|
default: hash[:default],
|
104
87
|
metadata: hash[:metadata] || {}
|
105
|
-
|
106
|
-
end
|
107
|
-
|
108
|
-
# Create attribute from JSON
|
109
|
-
#
|
110
|
-
# @param json [String] JSON string
|
111
|
-
# @return [Attribute] new attribute instance
|
112
|
-
def self.from_json(json)
|
113
|
-
from_h(JSON.parse(json))
|
114
|
-
end
|
115
|
-
|
116
|
-
# Check equality with another attribute
|
117
|
-
#
|
118
|
-
# @param other [Attribute] other attribute to compare
|
119
|
-
# @return [Boolean] true if attributes are equal
|
120
|
-
def ==(other)
|
121
|
-
return false unless other.is_a?(Attribute)
|
122
|
-
|
123
|
-
name == other.name &&
|
124
|
-
type == other.type &&
|
125
|
-
nullable == other.nullable &&
|
126
|
-
default == other.default &&
|
127
|
-
metadata == other.metadata
|
128
|
-
end
|
129
|
-
|
130
|
-
# Generate hash code for attribute
|
131
|
-
#
|
132
|
-
# @return [Integer] hash code
|
133
|
-
def hash
|
134
|
-
[name, type, nullable, default, metadata].hash
|
135
|
-
end
|
136
|
-
|
137
|
-
# String representation of attribute
|
138
|
-
#
|
139
|
-
# @return [String] string representation
|
140
|
-
def to_s
|
141
|
-
"#{self.class.name}(name: #{name}, type: #{type}, nullable: #{nullable})"
|
142
|
-
end
|
143
|
-
|
144
|
-
# Detailed string representation
|
145
|
-
#
|
146
|
-
# @return [String] detailed string representation
|
147
|
-
def inspect
|
148
|
-
"#{self.class.name}(name: #{name.inspect}, type: #{type.inspect}, " \
|
149
|
-
"nullable: #{nullable.inspect}, default: #{default.inspect}, metadata: #{metadata.inspect})"
|
88
|
+
}
|
150
89
|
end
|
151
90
|
end
|
152
91
|
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Dbwatcher
|
6
|
+
module Services
|
7
|
+
module DiagramData
|
8
|
+
# Base class for diagram data objects
|
9
|
+
#
|
10
|
+
# Provides common functionality for serialization, validation, and comparison
|
11
|
+
# that is shared across Attribute, Entity, and Relationship classes.
|
12
|
+
#
|
13
|
+
# Subclasses must implement:
|
14
|
+
# - comparable_attributes: Array of values used for equality comparison
|
15
|
+
# - serializable_attributes: Hash of attributes for serialization
|
16
|
+
# - validation_errors: Array of validation error strings (optional)
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# class MyClass < Base
|
20
|
+
# def comparable_attributes
|
21
|
+
# [name, type, value]
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# def serializable_attributes
|
25
|
+
# { name: name, type: type, value: value }
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
class Base
|
29
|
+
# Check if object is valid
|
30
|
+
#
|
31
|
+
# @return [Boolean] true if object has no validation errors
|
32
|
+
def valid?
|
33
|
+
validation_errors.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
# Check equality with another object of the same class
|
37
|
+
#
|
38
|
+
# @param other [Object] object to compare with
|
39
|
+
# @return [Boolean] true if objects are equal
|
40
|
+
def ==(other)
|
41
|
+
return false unless other.is_a?(self.class)
|
42
|
+
|
43
|
+
comparable_attributes == other.comparable_attributes
|
44
|
+
end
|
45
|
+
|
46
|
+
# Generate hash code for object
|
47
|
+
#
|
48
|
+
# @return [Integer] hash code
|
49
|
+
def hash
|
50
|
+
comparable_attributes.hash
|
51
|
+
end
|
52
|
+
|
53
|
+
# Serialize object to hash
|
54
|
+
#
|
55
|
+
# @return [Hash] serialized object data
|
56
|
+
def to_h
|
57
|
+
serializable_attributes
|
58
|
+
end
|
59
|
+
|
60
|
+
# Serialize object to JSON
|
61
|
+
#
|
62
|
+
# @return [String] JSON representation
|
63
|
+
def to_json(*args)
|
64
|
+
to_h.to_json(*args)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Create object from hash
|
68
|
+
#
|
69
|
+
# @param hash [Hash] object data
|
70
|
+
# @return [Object] new object instance
|
71
|
+
def self.from_h(hash)
|
72
|
+
# Convert string keys to symbols for consistent access
|
73
|
+
hash = hash.transform_keys(&:to_sym) if hash.respond_to?(:transform_keys) && hash.keys.first.is_a?(String)
|
74
|
+
|
75
|
+
new(**extract_constructor_args(hash))
|
76
|
+
end
|
77
|
+
|
78
|
+
# Create object from JSON
|
79
|
+
#
|
80
|
+
# @param json [String] JSON string
|
81
|
+
# @return [Object] new object instance
|
82
|
+
def self.from_json(json)
|
83
|
+
from_h(JSON.parse(json))
|
84
|
+
end
|
85
|
+
|
86
|
+
# String representation of object
|
87
|
+
#
|
88
|
+
# @return [String] string representation
|
89
|
+
def to_s
|
90
|
+
attrs = serializable_attributes.map { |k, v| "#{k}: #{v}" }.join(", ")
|
91
|
+
"#{self.class.name}(#{attrs})"
|
92
|
+
end
|
93
|
+
|
94
|
+
# Detailed string representation
|
95
|
+
#
|
96
|
+
# @return [String] detailed string representation
|
97
|
+
def inspect
|
98
|
+
attrs = serializable_attributes.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
|
99
|
+
"#{self.class.name}(#{attrs})"
|
100
|
+
end
|
101
|
+
|
102
|
+
# Default implementation - subclasses should override
|
103
|
+
def comparable_attributes
|
104
|
+
raise NotImplementedError, "#{self.class} must implement #comparable_attributes"
|
105
|
+
end
|
106
|
+
|
107
|
+
# Default implementation - subclasses should override
|
108
|
+
def serializable_attributes
|
109
|
+
raise NotImplementedError, "#{self.class} must implement #serializable_attributes"
|
110
|
+
end
|
111
|
+
|
112
|
+
# Default implementation - subclasses should override if validation needed
|
113
|
+
def validation_errors
|
114
|
+
[]
|
115
|
+
end
|
116
|
+
|
117
|
+
# Extract constructor arguments from hash
|
118
|
+
# Subclasses can override this for custom initialization logic
|
119
|
+
#
|
120
|
+
# @param hash [Hash] object data
|
121
|
+
# @return [Hash] constructor arguments
|
122
|
+
def self.extract_constructor_args(hash)
|
123
|
+
hash
|
124
|
+
end
|
125
|
+
private_class_method :extract_constructor_args
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "base"
|
4
|
+
|
3
5
|
module Dbwatcher
|
4
6
|
module Services
|
5
7
|
module DiagramData
|
@@ -20,7 +22,7 @@ module Dbwatcher
|
|
20
22
|
# )
|
21
23
|
# entity.valid? # => true
|
22
24
|
# entity.to_h # => { id: "users", name: "User", ... }
|
23
|
-
class Entity
|
25
|
+
class Entity < Base
|
24
26
|
attr_accessor :id, :name, :type, :attributes, :metadata
|
25
27
|
|
26
28
|
# Initialize entity
|
@@ -31,6 +33,7 @@ module Dbwatcher
|
|
31
33
|
# @param attributes [Array<Attribute>] entity attributes/properties
|
32
34
|
# @param metadata [Hash] additional type-specific information
|
33
35
|
def initialize(id:, name:, type: "default", attributes: [], metadata: {})
|
36
|
+
super() # Initialize parent class
|
34
37
|
@id = id.to_s
|
35
38
|
@name = name.to_s
|
36
39
|
@type = type.to_s
|
@@ -38,16 +41,23 @@ module Dbwatcher
|
|
38
41
|
@metadata = metadata.is_a?(Hash) ? metadata : {}
|
39
42
|
end
|
40
43
|
|
41
|
-
#
|
42
|
-
|
43
|
-
|
44
|
-
def valid?
|
45
|
-
validation_errors.empty?
|
44
|
+
# Implementation for Base class
|
45
|
+
def comparable_attributes
|
46
|
+
[id, name, type, attributes, metadata]
|
46
47
|
end
|
47
48
|
|
48
|
-
#
|
49
|
-
|
50
|
-
|
49
|
+
# Implementation for Base class
|
50
|
+
def serializable_attributes
|
51
|
+
{
|
52
|
+
id: id,
|
53
|
+
name: name,
|
54
|
+
type: type,
|
55
|
+
attributes: attributes.map(&:to_h),
|
56
|
+
metadata: metadata
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
# Implementation for Base class
|
51
61
|
def validation_errors
|
52
62
|
errors = []
|
53
63
|
errors << "ID cannot be blank" if id.nil? || id.to_s.strip.empty?
|
@@ -91,80 +101,21 @@ module Dbwatcher
|
|
91
101
|
attributes.select(&:foreign_key?)
|
92
102
|
end
|
93
103
|
|
94
|
-
#
|
95
|
-
|
96
|
-
# @return [Hash] serialized entity data
|
97
|
-
def to_h
|
98
|
-
{
|
99
|
-
id: id,
|
100
|
-
name: name,
|
101
|
-
type: type,
|
102
|
-
attributes: attributes.map(&:to_h),
|
103
|
-
metadata: metadata
|
104
|
-
}
|
105
|
-
end
|
106
|
-
|
107
|
-
# Serialize entity to JSON
|
108
|
-
#
|
109
|
-
# @return [String] JSON representation
|
110
|
-
def to_json(*args)
|
111
|
-
to_h.to_json(*args)
|
112
|
-
end
|
113
|
-
|
114
|
-
# Create entity from hash
|
115
|
-
#
|
116
|
-
# @param hash [Hash] entity data
|
117
|
-
# @return [Entity] new entity instance
|
118
|
-
def self.from_h(hash)
|
104
|
+
# Override base class method to handle attributes array
|
105
|
+
def self.extract_constructor_args(hash)
|
119
106
|
attrs = []
|
120
107
|
if hash[:attributes] || hash["attributes"]
|
121
108
|
attr_data = hash[:attributes] || hash["attributes"]
|
122
109
|
attrs = attr_data.map { |attr| Attribute.from_h(attr) }
|
123
110
|
end
|
124
111
|
|
125
|
-
|
112
|
+
{
|
126
113
|
id: hash[:id] || hash["id"],
|
127
114
|
name: hash[:name] || hash["name"],
|
128
115
|
type: hash[:type] || hash["type"] || "default",
|
129
116
|
attributes: attrs,
|
130
117
|
metadata: hash[:metadata] || hash["metadata"] || {}
|
131
|
-
|
132
|
-
end
|
133
|
-
|
134
|
-
# Create entity from JSON
|
135
|
-
#
|
136
|
-
# @param json [String] JSON string
|
137
|
-
# @return [Entity] new entity instance
|
138
|
-
def self.from_json(json)
|
139
|
-
from_h(JSON.parse(json))
|
140
|
-
end
|
141
|
-
|
142
|
-
# Check equality with another entity
|
143
|
-
#
|
144
|
-
# @param other [Entity] other entity to compare
|
145
|
-
# @return [Boolean] true if entities are equal
|
146
|
-
def ==(other)
|
147
|
-
return false unless other.is_a?(Entity)
|
148
|
-
|
149
|
-
id == other.id &&
|
150
|
-
name == other.name &&
|
151
|
-
type == other.type &&
|
152
|
-
attributes == other.attributes &&
|
153
|
-
metadata == other.metadata
|
154
|
-
end
|
155
|
-
|
156
|
-
# Generate hash code for entity
|
157
|
-
#
|
158
|
-
# @return [Integer] hash code
|
159
|
-
def hash
|
160
|
-
[id, name, type, attributes, metadata].hash
|
161
|
-
end
|
162
|
-
|
163
|
-
# String representation of entity
|
164
|
-
#
|
165
|
-
# @return [String] string representation
|
166
|
-
def to_s
|
167
|
-
"#{self.class.name}(id: #{id}, name: #{name}, type: #{type})"
|
118
|
+
}
|
168
119
|
end
|
169
120
|
|
170
121
|
# Detailed string representation
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "base"
|
3
4
|
require_relative "relationship_params"
|
4
5
|
|
5
6
|
module Dbwatcher
|
@@ -21,7 +22,7 @@ module Dbwatcher
|
|
21
22
|
# )
|
22
23
|
# relationship.valid? # => true
|
23
24
|
# relationship.to_h # => { source_id: "users", target_id: "orders", ... }
|
24
|
-
class Relationship
|
25
|
+
class Relationship < Base
|
25
26
|
attr_accessor :source_id, :target_id, :type, :label, :cardinality, :metadata
|
26
27
|
|
27
28
|
# Valid cardinality types
|
@@ -57,6 +58,7 @@ module Dbwatcher
|
|
57
58
|
# @param params [RelationshipParams, Hash] relationship parameters
|
58
59
|
# @return [Relationship] new relationship instance
|
59
60
|
def initialize(params)
|
61
|
+
super() # Initialize parent class
|
60
62
|
params = RelationshipParams.new(params) if params.is_a?(Hash)
|
61
63
|
|
62
64
|
@source_id = params.source_id.to_s
|
@@ -108,10 +110,18 @@ module Dbwatcher
|
|
108
110
|
ERD_NOTATIONS[infer_cardinality] || DEFAULT_ERD_NOTATION
|
109
111
|
end
|
110
112
|
|
111
|
-
#
|
112
|
-
|
113
|
-
|
114
|
-
|
113
|
+
# Override base class method to handle simple hash initialization
|
114
|
+
def self.extract_constructor_args(hash)
|
115
|
+
hash
|
116
|
+
end
|
117
|
+
|
118
|
+
# Implementation for Base class
|
119
|
+
def comparable_attributes
|
120
|
+
[source_id, target_id, type, label, cardinality, metadata]
|
121
|
+
end
|
122
|
+
|
123
|
+
# Implementation for Base class
|
124
|
+
def serializable_attributes
|
115
125
|
{
|
116
126
|
source_id: source_id,
|
117
127
|
target_id: target_id,
|
@@ -121,67 +131,6 @@ module Dbwatcher
|
|
121
131
|
metadata: metadata
|
122
132
|
}
|
123
133
|
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
134
|
end
|
186
135
|
end
|
187
136
|
end
|
@@ -5,81 +5,58 @@ require_relative "diagram_type_registry"
|
|
5
5
|
|
6
6
|
module Dbwatcher
|
7
7
|
module Services
|
8
|
-
#
|
8
|
+
# Service for generating diagrams from session data
|
9
9
|
#
|
10
|
-
#
|
11
|
-
#
|
10
|
+
# Coordinates the process of generating diagrams by:
|
11
|
+
# 1. Loading session data
|
12
|
+
# 2. Using appropriate analyzers to extract relationships
|
13
|
+
# 3. Applying diagram generation strategies
|
12
14
|
#
|
13
15
|
# @example
|
14
|
-
# generator = DiagramGenerator.new(session_id,
|
16
|
+
# generator = DiagramGenerator.new(session_id: "abc123", diagram_type: "database_tables")
|
15
17
|
# result = generator.call
|
16
|
-
# # => { content: "erDiagram\n
|
17
|
-
class DiagramGenerator
|
18
|
-
|
18
|
+
# # => { success: true, content: "erDiagram\n...", type: "erDiagram" }
|
19
|
+
class DiagramGenerator
|
20
|
+
include Dbwatcher::Logging
|
19
21
|
|
20
|
-
# Initialize with
|
22
|
+
# Initialize generator with options
|
21
23
|
#
|
22
|
-
# @param session_id [String] session
|
24
|
+
# @param session_id [String] session ID to analyze
|
23
25
|
# @param diagram_type [String] type of diagram to generate
|
24
|
-
# @param
|
25
|
-
|
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 = {})
|
26
|
+
# @param options [Hash] additional options
|
27
|
+
def initialize(session_id:, diagram_type:, options: {})
|
29
28
|
@session_id = session_id
|
30
29
|
@diagram_type = diagram_type
|
31
|
-
@
|
32
|
-
@
|
33
|
-
@logger =
|
34
|
-
super()
|
30
|
+
@options = options
|
31
|
+
@registry = options[:registry] || DiagramTypeRegistry.new
|
32
|
+
@logger = options[:logger] || Rails.logger
|
35
33
|
end
|
36
34
|
|
37
|
-
# Generate diagram
|
35
|
+
# Generate diagram
|
38
36
|
#
|
39
|
-
# @return [Hash] diagram
|
37
|
+
# @return [Hash] diagram generation result
|
40
38
|
def call
|
41
|
-
|
42
|
-
start_time = Time.now
|
39
|
+
log_info("Generating diagram of type #{@diagram_type} for session #{@session_id}")
|
43
40
|
|
44
|
-
|
45
|
-
|
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
|
41
|
+
start_time = Time.now
|
42
|
+
result = generate_diagram
|
52
43
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
44
|
+
duration_ms = ((Time.now - start_time) * 1000).round(2)
|
45
|
+
log_info("Diagram generation completed in #{duration_ms}ms", {
|
46
|
+
session_id: @session_id,
|
47
|
+
diagram_type: @diagram_type,
|
48
|
+
success: result[:success]
|
49
|
+
})
|
59
50
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
DiagramTypeRegistry.new.available_types_with_metadata
|
51
|
+
result
|
52
|
+
rescue StandardError => e
|
53
|
+
log_error("Diagram generation failed: #{e.message}", error_context)
|
54
|
+
error_result("Diagram generation failed: #{e.message}")
|
65
55
|
end
|
66
56
|
|
67
57
|
private
|
68
58
|
|
69
|
-
#
|
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
|
59
|
+
# Generate diagram based on configuration
|
83
60
|
#
|
84
61
|
# @return [Hash] diagram generation result
|
85
62
|
def generate_diagram
|
@@ -96,8 +73,8 @@ module Dbwatcher
|
|
96
73
|
analyzer = @registry.create_analyzer(@diagram_type, session)
|
97
74
|
dataset = analyzer.call
|
98
75
|
|
99
|
-
|
100
|
-
|
76
|
+
log_debug("Generated dataset with #{dataset.entities.size} entities and " \
|
77
|
+
"#{dataset.relationships.size} relationships")
|
101
78
|
|
102
79
|
# Create strategy and generate diagram from dataset
|
103
80
|
strategy = @registry.create_strategy(@diagram_type)
|
@@ -110,7 +87,7 @@ module Dbwatcher
|
|
110
87
|
def load_session
|
111
88
|
Dbwatcher::Storage.sessions.find(@session_id)
|
112
89
|
rescue StandardError => e
|
113
|
-
|
90
|
+
log_warn("Could not load session #{@session_id}: #{e.message}")
|
114
91
|
nil
|
115
92
|
end
|
116
93
|
|
@@ -138,17 +115,6 @@ module Dbwatcher
|
|
138
115
|
generated_at: Time.now.iso8601
|
139
116
|
}
|
140
117
|
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
118
|
end
|
153
119
|
end
|
154
120
|
end
|