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