dbwatcher 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +81 -210
- data/app/assets/config/dbwatcher_manifest.js +15 -0
- data/app/assets/javascripts/dbwatcher/alpine_registrations.js +39 -0
- data/app/assets/javascripts/dbwatcher/auto_init.js +23 -0
- data/app/assets/javascripts/dbwatcher/components/base.js +141 -0
- data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +1008 -0
- data/app/assets/javascripts/dbwatcher/components/diagrams.js +449 -0
- data/app/assets/javascripts/dbwatcher/components/summary.js +234 -0
- data/app/assets/javascripts/dbwatcher/core/alpine_store.js +138 -0
- data/app/assets/javascripts/dbwatcher/core/api_client.js +162 -0
- data/app/assets/javascripts/dbwatcher/core/component_loader.js +70 -0
- data/app/assets/javascripts/dbwatcher/core/component_registry.js +94 -0
- data/app/assets/javascripts/dbwatcher/dbwatcher.js +120 -0
- data/app/assets/javascripts/dbwatcher/services/mermaid.js +315 -0
- data/app/assets/javascripts/dbwatcher/services/mermaid_service.js +199 -0
- data/app/assets/javascripts/dbwatcher/vendor/date-fns-browser.js +99 -0
- data/app/assets/javascripts/dbwatcher/vendor/lodash.min.js +140 -0
- data/app/assets/javascripts/dbwatcher/vendor/tabulator.min.js +3 -0
- data/app/assets/stylesheets/dbwatcher/application.css +423 -0
- data/app/assets/stylesheets/dbwatcher/application.scss +15 -0
- data/app/assets/stylesheets/dbwatcher/components/_badges.scss +38 -0
- data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +162 -0
- data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +51 -0
- data/app/assets/stylesheets/dbwatcher/components/_forms.scss +27 -0
- data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +55 -0
- data/app/assets/stylesheets/dbwatcher/core/_base.scss +34 -0
- data/app/assets/stylesheets/dbwatcher/core/_variables.scss +47 -0
- data/app/assets/stylesheets/dbwatcher/vendor/tabulator.min.css +2 -0
- data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +64 -0
- data/app/controllers/dbwatcher/base_controller.rb +8 -2
- data/app/controllers/dbwatcher/dashboard_controller.rb +8 -0
- data/app/controllers/dbwatcher/sessions_controller.rb +25 -10
- data/app/helpers/dbwatcher/component_helper.rb +29 -0
- data/app/helpers/dbwatcher/diagram_helper.rb +110 -0
- data/app/helpers/dbwatcher/session_helper.rb +3 -2
- data/app/views/dbwatcher/sessions/_changes_tab.html.erb +265 -0
- data/app/views/dbwatcher/sessions/_diagrams_tab.html.erb +166 -0
- data/app/views/dbwatcher/sessions/_session_header.html.erb +11 -0
- data/app/views/dbwatcher/sessions/_summary_tab.html.erb +88 -0
- data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +12 -0
- data/app/views/dbwatcher/sessions/changes.html.erb +21 -0
- data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +44 -0
- data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +96 -0
- data/app/views/dbwatcher/sessions/diagrams.html.erb +21 -0
- data/app/views/dbwatcher/sessions/index.html.erb +14 -10
- data/app/views/dbwatcher/sessions/shared/_layout.html.erb +8 -0
- data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +35 -0
- data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +25 -0
- data/app/views/dbwatcher/sessions/show.html.erb +3 -346
- data/app/views/dbwatcher/sessions/summary.html.erb +21 -0
- data/app/views/layouts/dbwatcher/application.html.erb +125 -247
- data/bin/compile_scss +49 -0
- data/config/routes.rb +26 -0
- data/lib/dbwatcher/configuration.rb +102 -8
- data/lib/dbwatcher/engine.rb +17 -7
- data/lib/dbwatcher/services/analyzers/session_data_processor.rb +98 -0
- data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +202 -0
- data/lib/dbwatcher/services/api/base_api_service.rb +100 -0
- data/lib/dbwatcher/services/api/changes_data_service.rb +112 -0
- data/lib/dbwatcher/services/api/diagram_data_service.rb +145 -0
- data/lib/dbwatcher/services/api/summary_data_service.rb +158 -0
- data/lib/dbwatcher/services/base_service.rb +64 -0
- data/lib/dbwatcher/services/diagram_analyzers/base_analyzer.rb +162 -0
- data/lib/dbwatcher/services/diagram_analyzers/foreign_key_analyzer.rb +354 -0
- data/lib/dbwatcher/services/diagram_analyzers/inferred_relationship_analyzer.rb +502 -0
- data/lib/dbwatcher/services/diagram_analyzers/model_association_analyzer.rb +564 -0
- data/lib/dbwatcher/services/diagram_data/attribute.rb +154 -0
- data/lib/dbwatcher/services/diagram_data/dataset.rb +278 -0
- data/lib/dbwatcher/services/diagram_data/entity.rb +180 -0
- data/lib/dbwatcher/services/diagram_data/relationship.rb +188 -0
- data/lib/dbwatcher/services/diagram_data/relationship_params.rb +55 -0
- data/lib/dbwatcher/services/diagram_data.rb +65 -0
- data/lib/dbwatcher/services/diagram_error_handler.rb +239 -0
- data/lib/dbwatcher/services/diagram_generator.rb +154 -0
- data/lib/dbwatcher/services/diagram_strategies/base_diagram_strategy.rb +149 -0
- data/lib/dbwatcher/services/diagram_strategies/class_diagram_strategy.rb +49 -0
- data/lib/dbwatcher/services/diagram_strategies/erd_diagram_strategy.rb +52 -0
- data/lib/dbwatcher/services/diagram_strategies/flowchart_diagram_strategy.rb +52 -0
- data/lib/dbwatcher/services/diagram_system.rb +69 -0
- data/lib/dbwatcher/services/diagram_type_registry.rb +164 -0
- data/lib/dbwatcher/services/mermaid_syntax/base_builder.rb +127 -0
- data/lib/dbwatcher/services/mermaid_syntax/cardinality_mapper.rb +90 -0
- data/lib/dbwatcher/services/mermaid_syntax/class_diagram_builder.rb +136 -0
- data/lib/dbwatcher/services/mermaid_syntax/class_diagram_helper.rb +46 -0
- data/lib/dbwatcher/services/mermaid_syntax/erd_builder.rb +116 -0
- data/lib/dbwatcher/services/mermaid_syntax/flowchart_builder.rb +109 -0
- data/lib/dbwatcher/services/mermaid_syntax/sanitizer.rb +102 -0
- data/lib/dbwatcher/services/mermaid_syntax_builder.rb +155 -0
- data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +15 -128
- data/lib/dbwatcher/storage/api/session_api.rb +47 -0
- data/lib/dbwatcher/storage/base_storage.rb +7 -0
- data/lib/dbwatcher/version.rb +1 -1
- data/lib/dbwatcher.rb +58 -1
- metadata +94 -2
@@ -0,0 +1,154 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Services
|
5
|
+
module DiagramData
|
6
|
+
# Attribute representing a property of an entity
|
7
|
+
#
|
8
|
+
# This class provides a standardized representation for entity attributes
|
9
|
+
# (columns, fields, properties) with consistent validation and serialization.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# attribute = Attribute.new(
|
13
|
+
# name: "email",
|
14
|
+
# type: "string",
|
15
|
+
# nullable: false,
|
16
|
+
# default: nil,
|
17
|
+
# metadata: { unique: true }
|
18
|
+
# )
|
19
|
+
# attribute.valid? # => true
|
20
|
+
# attribute.to_h # => { name: "email", type: "string", ... }
|
21
|
+
class Attribute
|
22
|
+
attr_accessor :name, :type, :nullable, :default, :metadata
|
23
|
+
|
24
|
+
# Initialize attribute
|
25
|
+
#
|
26
|
+
# @param name [String] attribute name
|
27
|
+
# @param type [String] attribute data type
|
28
|
+
# @param nullable [Boolean] whether attribute can be null
|
29
|
+
# @param default [Object] default value
|
30
|
+
# @param metadata [Hash] additional type-specific information
|
31
|
+
def initialize(name:, type: nil, nullable: true, default: nil, metadata: {})
|
32
|
+
@name = name.to_s
|
33
|
+
@type = type.to_s
|
34
|
+
@nullable = nullable == true
|
35
|
+
@default = default
|
36
|
+
@metadata = metadata.is_a?(Hash) ? metadata : {}
|
37
|
+
end
|
38
|
+
|
39
|
+
# Check if attribute is valid
|
40
|
+
#
|
41
|
+
# @return [Boolean] true if attribute has required fields
|
42
|
+
def valid?
|
43
|
+
validation_errors.empty?
|
44
|
+
end
|
45
|
+
|
46
|
+
# Get validation errors
|
47
|
+
#
|
48
|
+
# @return [Array<String>] array of validation error messages
|
49
|
+
def validation_errors
|
50
|
+
errors = []
|
51
|
+
errors << "Name cannot be blank" if name.nil? || name.to_s.strip.empty?
|
52
|
+
errors << "Metadata must be a Hash" unless metadata.is_a?(Hash)
|
53
|
+
errors
|
54
|
+
end
|
55
|
+
|
56
|
+
# Check if attribute is a primary key
|
57
|
+
#
|
58
|
+
# @return [Boolean] true if attribute is a primary key
|
59
|
+
def primary_key?
|
60
|
+
metadata[:primary_key] == true
|
61
|
+
end
|
62
|
+
|
63
|
+
# Check if attribute is a foreign key
|
64
|
+
#
|
65
|
+
# @return [Boolean] true if attribute is a foreign key
|
66
|
+
def foreign_key?
|
67
|
+
metadata[:foreign_key] == true || name.to_s.end_with?("_id")
|
68
|
+
end
|
69
|
+
|
70
|
+
# Serialize attribute to hash
|
71
|
+
#
|
72
|
+
# @return [Hash] serialized attribute data
|
73
|
+
def to_h
|
74
|
+
{
|
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
|
+
name: hash[:name],
|
101
|
+
type: hash[:type],
|
102
|
+
nullable: hash.key?(:nullable) ? hash[:nullable] : true,
|
103
|
+
default: hash[:default],
|
104
|
+
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})"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,278 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Services
|
5
|
+
module DiagramData
|
6
|
+
# Complete dataset for diagram generation
|
7
|
+
#
|
8
|
+
# This class serves as a container for all diagram data including entities
|
9
|
+
# and relationships, with validation and management capabilities.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# dataset = Dataset.new
|
13
|
+
# dataset.add_entity(Entity.new(id: "users", name: "User", type: "table"))
|
14
|
+
# dataset.add_entity(Entity.new(id: "orders", name: "Order", type: "table"))
|
15
|
+
# dataset.add_relationship(Relationship.new(
|
16
|
+
# source_id: "users", target_id: "orders", type: "has_many"
|
17
|
+
# ))
|
18
|
+
# dataset.valid? # => true
|
19
|
+
# dataset.to_h # => complete dataset hash
|
20
|
+
class Dataset
|
21
|
+
attr_reader :entities, :relationships, :metadata
|
22
|
+
|
23
|
+
# Initialize empty dataset
|
24
|
+
#
|
25
|
+
# @param metadata [Hash] optional dataset-level metadata
|
26
|
+
def initialize(metadata: {})
|
27
|
+
@entities = {}
|
28
|
+
@relationships = []
|
29
|
+
@metadata = metadata.is_a?(Hash) ? metadata : {}
|
30
|
+
end
|
31
|
+
|
32
|
+
# Add entity to dataset
|
33
|
+
#
|
34
|
+
# @param entity [Entity] entity to add
|
35
|
+
# @return [Entity] the added entity
|
36
|
+
# @raise [ArgumentError] if entity is invalid
|
37
|
+
def add_entity(entity)
|
38
|
+
raise ArgumentError, "Entity must be an Entity instance" unless entity.is_a?(Entity)
|
39
|
+
|
40
|
+
raise ArgumentError, "Entity is invalid: #{entity.validation_errors.join(", ")}" unless entity.valid?
|
41
|
+
|
42
|
+
@entities[entity.id] = entity
|
43
|
+
entity
|
44
|
+
end
|
45
|
+
|
46
|
+
# Add relationship to dataset
|
47
|
+
#
|
48
|
+
# @param relationship [Relationship] relationship to add
|
49
|
+
# @return [Relationship] the added relationship
|
50
|
+
# @raise [ArgumentError] if relationship is invalid
|
51
|
+
def add_relationship(relationship)
|
52
|
+
raise ArgumentError, "Relationship must be a Relationship instance" unless relationship.is_a?(Relationship)
|
53
|
+
|
54
|
+
unless relationship.valid?
|
55
|
+
raise ArgumentError, "Relationship is invalid: #{relationship.validation_errors.join(", ")}"
|
56
|
+
end
|
57
|
+
|
58
|
+
@relationships << relationship
|
59
|
+
relationship
|
60
|
+
end
|
61
|
+
|
62
|
+
# Get entity by ID
|
63
|
+
#
|
64
|
+
# @param id [String] entity ID
|
65
|
+
# @return [Entity, nil] entity or nil if not found
|
66
|
+
def get_entity(id)
|
67
|
+
@entities[id.to_s]
|
68
|
+
end
|
69
|
+
|
70
|
+
# Check if entity exists
|
71
|
+
#
|
72
|
+
# @param id [String] entity ID
|
73
|
+
# @return [Boolean] true if entity exists
|
74
|
+
def entity?(id)
|
75
|
+
@entities.key?(id.to_s)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Remove entity by ID
|
79
|
+
#
|
80
|
+
# @param id [String] entity ID
|
81
|
+
# @return [Entity, nil] removed entity or nil if not found
|
82
|
+
def remove_entity(id)
|
83
|
+
entity = @entities.delete(id.to_s)
|
84
|
+
|
85
|
+
# Remove relationships involving this entity
|
86
|
+
if entity
|
87
|
+
@relationships.reject! do |rel|
|
88
|
+
rel.source_id == id.to_s || rel.target_id == id.to_s
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
entity
|
93
|
+
end
|
94
|
+
|
95
|
+
# Remove relationship
|
96
|
+
#
|
97
|
+
# @param relationship [Relationship] relationship to remove
|
98
|
+
# @return [Boolean] true if relationship was removed
|
99
|
+
def remove_relationship(relationship)
|
100
|
+
!@relationships.delete(relationship).nil?
|
101
|
+
end
|
102
|
+
|
103
|
+
# Get relationships for an entity
|
104
|
+
#
|
105
|
+
# @param entity_id [String] entity ID
|
106
|
+
# @param direction [Symbol] :outgoing, :incoming, or :all
|
107
|
+
# @return [Array<Relationship>] filtered relationships
|
108
|
+
def relationships_for(entity_id, direction: :all)
|
109
|
+
id = entity_id.to_s
|
110
|
+
|
111
|
+
case direction
|
112
|
+
when :outgoing
|
113
|
+
@relationships.select { |rel| rel.source_id == id }
|
114
|
+
when :incoming
|
115
|
+
@relationships.select { |rel| rel.target_id == id }
|
116
|
+
when :all
|
117
|
+
@relationships.select { |rel| rel.source_id == id || rel.target_id == id }
|
118
|
+
else
|
119
|
+
raise ArgumentError, "Direction must be :outgoing, :incoming, or :all"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Check if dataset is valid
|
124
|
+
#
|
125
|
+
# @return [Boolean] true if dataset is valid
|
126
|
+
def valid?
|
127
|
+
validation_errors.empty?
|
128
|
+
end
|
129
|
+
|
130
|
+
# Get validation errors
|
131
|
+
#
|
132
|
+
# @return [Array<String>] array of validation error messages
|
133
|
+
def validation_errors
|
134
|
+
errors = []
|
135
|
+
|
136
|
+
# Validate all entities
|
137
|
+
@entities.each do |id, entity|
|
138
|
+
errors << "Entity #{id} is invalid: #{entity.validation_errors.join(", ")}" unless entity.valid?
|
139
|
+
end
|
140
|
+
|
141
|
+
# Validate all relationships
|
142
|
+
@relationships.each_with_index do |relationship, index|
|
143
|
+
unless relationship.valid?
|
144
|
+
errors << "Relationship #{index} is invalid: #{relationship.validation_errors.join(", ")}"
|
145
|
+
end
|
146
|
+
|
147
|
+
# Check that referenced entities exist
|
148
|
+
unless entity?(relationship.source_id)
|
149
|
+
errors << "Relationship #{index} references non-existent source entity: #{relationship.source_id}"
|
150
|
+
end
|
151
|
+
|
152
|
+
unless entity?(relationship.target_id)
|
153
|
+
errors << "Relationship #{index} references non-existent target entity: #{relationship.target_id}"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Validate metadata
|
158
|
+
errors << "Metadata must be a Hash" unless @metadata.is_a?(Hash)
|
159
|
+
|
160
|
+
errors
|
161
|
+
end
|
162
|
+
|
163
|
+
# Get dataset statistics
|
164
|
+
#
|
165
|
+
# @return [Hash] statistics about the dataset
|
166
|
+
def stats
|
167
|
+
{
|
168
|
+
entity_count: @entities.size,
|
169
|
+
relationship_count: @relationships.size,
|
170
|
+
entity_types: @entities.values.map(&:type).uniq.sort,
|
171
|
+
relationship_types: @relationships.map(&:type).uniq.sort,
|
172
|
+
isolated_entities: isolated_entities.map(&:id),
|
173
|
+
connected_entities: connected_entities.map(&:id)
|
174
|
+
}
|
175
|
+
end
|
176
|
+
|
177
|
+
# Get entities with no relationships
|
178
|
+
#
|
179
|
+
# @return [Array<Entity>] isolated entities
|
180
|
+
def isolated_entities
|
181
|
+
connected_ids = (@relationships.map(&:source_id) + @relationships.map(&:target_id)).uniq
|
182
|
+
@entities.values.reject { |entity| connected_ids.include?(entity.id) }
|
183
|
+
end
|
184
|
+
|
185
|
+
# Get entities with at least one relationship
|
186
|
+
#
|
187
|
+
# @return [Array<Entity>] connected entities
|
188
|
+
def connected_entities
|
189
|
+
connected_ids = (@relationships.map(&:source_id) + @relationships.map(&:target_id)).uniq
|
190
|
+
@entities.values.select { |entity| connected_ids.include?(entity.id) }
|
191
|
+
end
|
192
|
+
|
193
|
+
# Check if dataset is empty
|
194
|
+
#
|
195
|
+
# @return [Boolean] true if no entities or relationships
|
196
|
+
def empty?
|
197
|
+
@entities.empty? && @relationships.empty?
|
198
|
+
end
|
199
|
+
|
200
|
+
# Clear all data from dataset
|
201
|
+
#
|
202
|
+
# @return [self] for method chaining
|
203
|
+
def clear
|
204
|
+
@entities.clear
|
205
|
+
@relationships.clear
|
206
|
+
@metadata.clear
|
207
|
+
self
|
208
|
+
end
|
209
|
+
|
210
|
+
# Serialize dataset to hash
|
211
|
+
#
|
212
|
+
# @return [Hash] serialized dataset
|
213
|
+
def to_h
|
214
|
+
{
|
215
|
+
entities: @entities.transform_values(&:to_h),
|
216
|
+
relationships: @relationships.map(&:to_h),
|
217
|
+
metadata: @metadata,
|
218
|
+
stats: stats
|
219
|
+
}
|
220
|
+
end
|
221
|
+
|
222
|
+
# Serialize dataset to JSON
|
223
|
+
#
|
224
|
+
# @return [String] JSON representation
|
225
|
+
def to_json(*args)
|
226
|
+
to_h.to_json(*args)
|
227
|
+
end
|
228
|
+
|
229
|
+
# Create dataset from hash
|
230
|
+
#
|
231
|
+
# @param hash [Hash] dataset data
|
232
|
+
# @return [Dataset] new dataset instance
|
233
|
+
def self.from_h(hash)
|
234
|
+
dataset = new(metadata: hash[:metadata] || hash["metadata"] || {})
|
235
|
+
|
236
|
+
# Load entities
|
237
|
+
entities_data = hash[:entities] || hash["entities"] || {}
|
238
|
+
entities_data.each_value do |entity_data|
|
239
|
+
entity = Entity.from_h(entity_data)
|
240
|
+
dataset.add_entity(entity)
|
241
|
+
end
|
242
|
+
|
243
|
+
# Load relationships
|
244
|
+
relationships_data = hash[:relationships] || hash["relationships"] || []
|
245
|
+
relationships_data.each do |relationship_data|
|
246
|
+
relationship = Relationship.from_h(relationship_data)
|
247
|
+
dataset.add_relationship(relationship)
|
248
|
+
end
|
249
|
+
|
250
|
+
dataset
|
251
|
+
end
|
252
|
+
|
253
|
+
# Create dataset from JSON
|
254
|
+
#
|
255
|
+
# @param json [String] JSON string
|
256
|
+
# @return [Dataset] new dataset instance
|
257
|
+
def self.from_json(json)
|
258
|
+
from_h(JSON.parse(json))
|
259
|
+
end
|
260
|
+
|
261
|
+
# String representation of dataset
|
262
|
+
#
|
263
|
+
# @return [String] string representation
|
264
|
+
def to_s
|
265
|
+
"#{self.class.name}(entities: #{@entities.size}, relationships: #{@relationships.size})"
|
266
|
+
end
|
267
|
+
|
268
|
+
# Detailed string representation
|
269
|
+
#
|
270
|
+
# @return [String] detailed string representation
|
271
|
+
def inspect
|
272
|
+
"#{self.class.name}(entities: #{@entities.size}, relationships: #{@relationships.size}, " \
|
273
|
+
"metadata: #{@metadata.inspect})"
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Services
|
5
|
+
module DiagramData
|
6
|
+
# Entity representing a node in any diagram
|
7
|
+
#
|
8
|
+
# This class provides a standardized representation for all diagram entities
|
9
|
+
# (nodes, tables, models, etc.) with consistent validation and serialization.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# entity = Entity.new(
|
13
|
+
# id: "users",
|
14
|
+
# name: "User",
|
15
|
+
# type: "table",
|
16
|
+
# attributes: [
|
17
|
+
# Attribute.new(name: "id", type: "integer", nullable: false, metadata: { primary_key: true })
|
18
|
+
# ],
|
19
|
+
# metadata: { columns: ["id", "name", "email"] }
|
20
|
+
# )
|
21
|
+
# entity.valid? # => true
|
22
|
+
# entity.to_h # => { id: "users", name: "User", ... }
|
23
|
+
class Entity
|
24
|
+
attr_accessor :id, :name, :type, :attributes, :metadata
|
25
|
+
|
26
|
+
# Initialize entity
|
27
|
+
#
|
28
|
+
# @param id [String] unique identifier for the entity
|
29
|
+
# @param name [String] display name for the entity
|
30
|
+
# @param type [String] entity type (table, model, etc.)
|
31
|
+
# @param attributes [Array<Attribute>] entity attributes/properties
|
32
|
+
# @param metadata [Hash] additional type-specific information
|
33
|
+
def initialize(id:, name:, type: "default", attributes: [], metadata: {})
|
34
|
+
@id = id.to_s
|
35
|
+
@name = name.to_s
|
36
|
+
@type = type.to_s
|
37
|
+
@attributes = attributes.is_a?(Array) ? attributes : []
|
38
|
+
@metadata = metadata.is_a?(Hash) ? metadata : {}
|
39
|
+
end
|
40
|
+
|
41
|
+
# Check if entity is valid
|
42
|
+
#
|
43
|
+
# @return [Boolean] true if entity has required fields
|
44
|
+
def valid?
|
45
|
+
validation_errors.empty?
|
46
|
+
end
|
47
|
+
|
48
|
+
# Get validation errors
|
49
|
+
#
|
50
|
+
# @return [Array<String>] array of validation error messages
|
51
|
+
def validation_errors
|
52
|
+
errors = []
|
53
|
+
errors << "ID cannot be blank" if id.nil? || id.to_s.strip.empty?
|
54
|
+
errors << "Name cannot be blank" if name.nil? || name.to_s.strip.empty?
|
55
|
+
errors << "Type cannot be blank" if type.nil? || type.to_s.strip.empty?
|
56
|
+
errors << "Attributes must be an Array" unless attributes.is_a?(Array)
|
57
|
+
errors << "Metadata must be a Hash" unless metadata.is_a?(Hash)
|
58
|
+
|
59
|
+
# Validate all attributes
|
60
|
+
attributes.each_with_index do |attribute, index|
|
61
|
+
errors << "Attribute at index #{index} is invalid" unless attribute.is_a?(Attribute) && attribute.valid?
|
62
|
+
end
|
63
|
+
|
64
|
+
errors
|
65
|
+
end
|
66
|
+
|
67
|
+
# Add an attribute to the entity
|
68
|
+
#
|
69
|
+
# @param attribute [Attribute] attribute to add
|
70
|
+
# @return [Attribute] the added attribute
|
71
|
+
# @raise [ArgumentError] if attribute is invalid
|
72
|
+
def add_attribute(attribute)
|
73
|
+
raise ArgumentError, "Attribute must be an Attribute instance" unless attribute.is_a?(Attribute)
|
74
|
+
raise ArgumentError, "Attribute is invalid: #{attribute.validation_errors.join(", ")}" unless attribute.valid?
|
75
|
+
|
76
|
+
@attributes << attribute
|
77
|
+
attribute
|
78
|
+
end
|
79
|
+
|
80
|
+
# Get primary key attributes
|
81
|
+
#
|
82
|
+
# @return [Array<Attribute>] primary key attributes
|
83
|
+
def primary_key_attributes
|
84
|
+
attributes.select(&:primary_key?)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Get foreign key attributes
|
88
|
+
#
|
89
|
+
# @return [Array<Attribute>] foreign key attributes
|
90
|
+
def foreign_key_attributes
|
91
|
+
attributes.select(&:foreign_key?)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Serialize entity to hash
|
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)
|
119
|
+
attrs = []
|
120
|
+
if hash[:attributes] || hash["attributes"]
|
121
|
+
attr_data = hash[:attributes] || hash["attributes"]
|
122
|
+
attrs = attr_data.map { |attr| Attribute.from_h(attr) }
|
123
|
+
end
|
124
|
+
|
125
|
+
new(
|
126
|
+
id: hash[:id] || hash["id"],
|
127
|
+
name: hash[:name] || hash["name"],
|
128
|
+
type: hash[:type] || hash["type"] || "default",
|
129
|
+
attributes: attrs,
|
130
|
+
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})"
|
168
|
+
end
|
169
|
+
|
170
|
+
# Detailed string representation
|
171
|
+
#
|
172
|
+
# @return [String] detailed string representation
|
173
|
+
def inspect
|
174
|
+
"#{self.class.name}(id: #{id.inspect}, name: #{name.inspect}, " \
|
175
|
+
"type: #{type.inspect}, attributes: #{attributes.length}, metadata: #{metadata.inspect})"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|