dbwatcher 1.1.0 → 1.1.2
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 +24 -2
- data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +12 -22
- data/app/assets/javascripts/dbwatcher/components/dashboard.js +325 -0
- data/app/assets/stylesheets/dbwatcher/application.css +394 -41
- data/app/assets/stylesheets/dbwatcher/application.scss +4 -0
- data/app/assets/stylesheets/dbwatcher/components/_badges.scss +68 -23
- data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +83 -26
- data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +3 -3
- data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +9 -0
- data/app/assets/stylesheets/dbwatcher/components/_tabulator.scss +248 -0
- data/app/assets/stylesheets/dbwatcher/vendor/_tabulator_overrides.scss +37 -0
- data/app/controllers/dbwatcher/api/v1/system_info_controller.rb +180 -0
- data/app/controllers/dbwatcher/dashboard/system_info_controller.rb +64 -0
- data/app/controllers/dbwatcher/dashboard_controller.rb +17 -0
- data/app/controllers/dbwatcher/sessions_controller.rb +3 -19
- data/app/helpers/dbwatcher/application_helper.rb +43 -11
- data/app/helpers/dbwatcher/diagram_helper.rb +0 -88
- data/app/views/dbwatcher/dashboard/_layout.html.erb +27 -0
- data/app/views/dbwatcher/dashboard/_overview.html.erb +188 -0
- data/app/views/dbwatcher/dashboard/_system_info.html.erb +22 -0
- data/app/views/dbwatcher/dashboard/_system_info_content.html.erb +389 -0
- data/app/views/dbwatcher/dashboard/index.html.erb +8 -177
- data/app/views/dbwatcher/sessions/_changes.html.erb +91 -0
- data/app/views/dbwatcher/sessions/_layout.html.erb +23 -0
- data/app/views/dbwatcher/sessions/index.html.erb +107 -87
- data/app/views/dbwatcher/sessions/show.html.erb +10 -4
- data/app/views/dbwatcher/tables/index.html.erb +32 -40
- data/app/views/layouts/dbwatcher/application.html.erb +100 -48
- data/config/routes.rb +23 -6
- data/lib/dbwatcher/configuration.rb +18 -1
- data/lib/dbwatcher/services/base_service.rb +2 -0
- data/lib/dbwatcher/services/diagram_analyzers/model_association_analyzer.rb +177 -138
- data/lib/dbwatcher/services/diagram_data/dataset.rb +2 -0
- data/lib/dbwatcher/services/mermaid_syntax/class_diagram_builder.rb +13 -9
- data/lib/dbwatcher/services/mermaid_syntax/class_diagram_helper.rb +3 -1
- data/lib/dbwatcher/services/mermaid_syntax/sanitizer.rb +17 -1
- data/lib/dbwatcher/services/system_info/database_info_collector.rb +263 -0
- data/lib/dbwatcher/services/system_info/machine_info_collector.rb +387 -0
- data/lib/dbwatcher/services/system_info/runtime_info_collector.rb +328 -0
- data/lib/dbwatcher/services/system_info/system_info_collector.rb +114 -0
- data/lib/dbwatcher/storage/concerns/error_handler.rb +6 -6
- data/lib/dbwatcher/storage/session.rb +5 -0
- data/lib/dbwatcher/storage/system_info_storage.rb +242 -0
- data/lib/dbwatcher/storage.rb +12 -0
- data/lib/dbwatcher/version.rb +1 -1
- data/lib/dbwatcher.rb +15 -1
- metadata +20 -15
- data/app/helpers/dbwatcher/component_helper.rb +0 -29
- data/app/views/dbwatcher/sessions/_changes_tab.html.erb +0 -265
- data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +0 -12
- data/app/views/dbwatcher/sessions/changes.html.erb +0 -21
- data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +0 -44
- data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +0 -96
- data/app/views/dbwatcher/sessions/diagrams.html.erb +0 -21
- data/app/views/dbwatcher/sessions/shared/_layout.html.erb +0 -8
- data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +0 -35
- data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +0 -25
- data/app/views/dbwatcher/sessions/summary.html.erb +0 -21
- /data/app/views/dbwatcher/sessions/{_diagrams_tab.html.erb → _diagrams.html.erb} +0 -0
- /data/app/views/dbwatcher/sessions/{_summary_tab.html.erb → _summary.html.erb} +0 -0
@@ -6,7 +6,14 @@ module Dbwatcher
|
|
6
6
|
# Analyzes relationships based on ActiveRecord model associations
|
7
7
|
#
|
8
8
|
# This service examines ActiveRecord models to detect associations between
|
9
|
-
# models that were involved in a session.
|
9
|
+
# models that were involved in a session. It uses direct model enumeration
|
10
|
+
# from ActiveRecord::Base.descendants to ensure reliable model discovery.
|
11
|
+
#
|
12
|
+
# Supported model scenarios:
|
13
|
+
# - Regular models with standard table names
|
14
|
+
# - Namespaced models (e.g., Admin::User)
|
15
|
+
# - Models with custom table names (using self.table_name)
|
16
|
+
# - Models from external gems and complex inheritance hierarchies
|
10
17
|
#
|
11
18
|
# @example
|
12
19
|
# analyzer = ModelAssociationAnalyzer.new(session)
|
@@ -31,21 +38,9 @@ module Dbwatcher
|
|
31
38
|
|
32
39
|
Rails.logger.debug "ModelAssociationAnalyzer: Starting analysis with #{models.length} models"
|
33
40
|
associations = extract_model_associations
|
34
|
-
|
35
|
-
# Add placeholder nodes for models without associations
|
36
41
|
associations = generate_placeholder_associations if associations.empty? && models.any?
|
37
42
|
|
38
|
-
|
39
|
-
if associations.any?
|
40
|
-
sample_association = associations.first
|
41
|
-
Rails.logger.debug "ModelAssociationAnalyzer: Sample association - " \
|
42
|
-
"source_model: #{sample_association[:source_model]}, " \
|
43
|
-
"target_model: #{sample_association[:target_model]}, " \
|
44
|
-
"type: #{sample_association[:type]}"
|
45
|
-
else
|
46
|
-
Rails.logger.info "ModelAssociationAnalyzer: No associations found"
|
47
|
-
end
|
48
|
-
|
43
|
+
log_analysis_results(associations)
|
49
44
|
associations
|
50
45
|
end
|
51
46
|
|
@@ -61,55 +56,53 @@ module Dbwatcher
|
|
61
56
|
model_names: models.map(&:name)
|
62
57
|
})
|
63
58
|
|
64
|
-
# Create entities
|
65
|
-
|
59
|
+
# Create entities from actual discovered models (no inference)
|
60
|
+
create_entities_from_models(dataset)
|
66
61
|
|
67
|
-
#
|
68
|
-
|
69
|
-
raw_data.each do |association|
|
70
|
-
models_to_process << association[:source_model] if association[:source_model]
|
71
|
-
models_to_process << association[:target_model] if association[:target_model]
|
72
|
-
end
|
73
|
-
models_to_process.uniq!
|
62
|
+
# Create relationships from association data
|
63
|
+
create_relationships_from_associations(dataset, raw_data)
|
74
64
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
65
|
+
dataset
|
66
|
+
end
|
67
|
+
|
68
|
+
# Create entities from discovered model classes
|
69
|
+
#
|
70
|
+
# @param dataset [DiagramData::Dataset] dataset to add entities to
|
71
|
+
# @return [void]
|
72
|
+
def create_entities_from_models(dataset)
|
73
|
+
models.each do |model_class|
|
79
74
|
attributes = extract_model_attributes(model_class)
|
80
75
|
methods = extract_model_methods(model_class)
|
81
76
|
|
82
|
-
# Get table name if available
|
83
|
-
table_name = model_class.respond_to?(:table_name) ? model_class.table_name : model_name.downcase
|
84
|
-
|
85
77
|
entity = create_entity(
|
86
|
-
id: table_name
|
87
|
-
name:
|
78
|
+
id: model_class.table_name,
|
79
|
+
name: model_class.name,
|
88
80
|
type: "model",
|
89
81
|
attributes: attributes,
|
90
82
|
metadata: {
|
91
|
-
table_name: table_name,
|
92
|
-
model_class:
|
83
|
+
table_name: model_class.table_name,
|
84
|
+
model_class: model_class.name,
|
93
85
|
methods: methods
|
94
86
|
}
|
95
87
|
)
|
96
88
|
dataset.add_entity(entity)
|
97
|
-
model_entities[model_name] = entity
|
98
89
|
end
|
90
|
+
end
|
99
91
|
|
100
|
-
|
92
|
+
# Create relationships from association data
|
93
|
+
#
|
94
|
+
# @param dataset [DiagramData::Dataset] dataset to add relationships to
|
95
|
+
# @param raw_data [Array<Hash>] raw association data
|
96
|
+
# @return [void]
|
97
|
+
def create_relationships_from_associations(dataset, raw_data)
|
101
98
|
raw_data.each do |association|
|
102
99
|
next if association[:type] == "node_only" || !association[:target_model]
|
103
100
|
|
104
|
-
source_id = association[:source_table]
|
105
|
-
target_id = association[:target_table]
|
101
|
+
source_id = association[:source_table]
|
102
|
+
target_id = association[:target_table]
|
106
103
|
|
107
|
-
#
|
108
|
-
|
109
|
-
if source_id == target_id
|
110
|
-
Rails.logger.info "ModelAssociationAnalyzer: Including self-referential relationship for " \
|
111
|
-
"#{source_id} (#{association[:association_name]})"
|
112
|
-
end
|
104
|
+
# Skip if we don't have valid table IDs
|
105
|
+
next unless source_id && target_id
|
113
106
|
|
114
107
|
# Determine cardinality based on relationship type
|
115
108
|
cardinality = determine_cardinality(association[:type])
|
@@ -131,8 +124,6 @@ module Dbwatcher
|
|
131
124
|
|
132
125
|
dataset.add_relationship(relationship)
|
133
126
|
end
|
134
|
-
|
135
|
-
dataset
|
136
127
|
end
|
137
128
|
|
138
129
|
# Get analyzer type
|
@@ -159,6 +150,28 @@ module Dbwatcher
|
|
159
150
|
|
160
151
|
attr_reader :session, :session_tables, :models
|
161
152
|
|
153
|
+
# Eagerly load all models including those from gems
|
154
|
+
#
|
155
|
+
# @return [void]
|
156
|
+
def eager_load_models
|
157
|
+
return unless defined?(Rails) && Rails.respond_to?(:application)
|
158
|
+
|
159
|
+
begin
|
160
|
+
# Force eager loading of application models
|
161
|
+
Rails.application.eager_load!
|
162
|
+
|
163
|
+
# Also load models from engines/gems if any are configured
|
164
|
+
Rails::Engine.descendants.each do |engine|
|
165
|
+
engine.eager_load! if engine.respond_to?(:eager_load!)
|
166
|
+
rescue StandardError => e
|
167
|
+
error_message = "ModelAssociationAnalyzer: Could not eager load engine #{engine.class.name}: #{e.message}"
|
168
|
+
Rails.logger.debug error_message
|
169
|
+
end
|
170
|
+
rescue StandardError => e
|
171
|
+
Rails.logger.debug "ModelAssociationAnalyzer: Could not eager load models: #{e.message}"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
162
175
|
# Generate placeholder associations for models without associations
|
163
176
|
#
|
164
177
|
# @return [Array<Hash>] placeholder associations
|
@@ -183,7 +196,7 @@ module Dbwatcher
|
|
183
196
|
#
|
184
197
|
# @return [Boolean] true if models can be analyzed
|
185
198
|
def models_available?
|
186
|
-
unless
|
199
|
+
unless activerecord_available?
|
187
200
|
Rails.logger.warn "ModelAssociationAnalyzer: ActiveRecord not available"
|
188
201
|
return false
|
189
202
|
end
|
@@ -211,66 +224,85 @@ module Dbwatcher
|
|
211
224
|
#
|
212
225
|
# @return [Array<Class>] ActiveRecord model classes
|
213
226
|
def discover_session_models
|
214
|
-
return [] unless
|
227
|
+
return [] unless activerecord_available?
|
215
228
|
|
216
|
-
# Get all ActiveRecord models
|
217
229
|
begin
|
218
|
-
all_models =
|
219
|
-
|
220
|
-
end
|
230
|
+
all_models = load_all_models
|
231
|
+
discovered_models = filter_models_by_session(all_models)
|
221
232
|
|
222
|
-
#
|
223
|
-
if
|
224
|
-
|
225
|
-
|
226
|
-
all_models = attempt_explicit_model_loading
|
233
|
+
# Log the discovered models and their table names for debugging
|
234
|
+
if discovered_models.any?
|
235
|
+
model_table_info = discovered_models.map { |m| "#{m.name} (#{m.table_name})" }
|
236
|
+
Rails.logger.debug "ModelAssociationAnalyzer: Discovered models: #{model_table_info.join(", ")}"
|
227
237
|
end
|
228
238
|
|
229
|
-
|
230
|
-
|
231
|
-
# Filter to models whose tables are in session (if session provided)
|
232
|
-
if session_tables.any?
|
233
|
-
filtered_models = all_models.select { |model| session_tables.include?(model.table_name) }
|
234
|
-
Rails.logger.debug "ModelAssociationAnalyzer: Filtered to #{filtered_models.size} " \
|
235
|
-
"models matching session tables"
|
236
|
-
filtered_models
|
237
|
-
else
|
238
|
-
all_models
|
239
|
-
end
|
239
|
+
discovered_models
|
240
240
|
rescue StandardError => e
|
241
241
|
Rails.logger.error "ModelAssociationAnalyzer: Error discovering models: #{e.message}"
|
242
242
|
[]
|
243
243
|
end
|
244
244
|
end
|
245
245
|
|
246
|
-
#
|
246
|
+
# Log analysis results for debugging
|
247
247
|
#
|
248
|
-
# @
|
249
|
-
|
250
|
-
|
248
|
+
# @param associations [Array<Hash>] found associations
|
249
|
+
# @return [void]
|
250
|
+
def log_analysis_results(associations)
|
251
|
+
Rails.logger.debug "ModelAssociationAnalyzer: Found #{associations.length} associations"
|
251
252
|
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
model_names.each do |model_name|
|
261
|
-
model_class = model_name.constantize
|
262
|
-
if model_class.respond_to?(:table_name) && model_class.table_name == table_name
|
263
|
-
models << model_class
|
264
|
-
Rails.logger.debug "ModelAssociationAnalyzer: Loaded model #{model_name} for table #{table_name}"
|
265
|
-
break
|
266
|
-
end
|
267
|
-
rescue NameError
|
268
|
-
# Model doesn't exist, try the next one
|
269
|
-
next
|
270
|
-
end
|
253
|
+
if associations.any?
|
254
|
+
sample_association = associations.first
|
255
|
+
Rails.logger.debug "ModelAssociationAnalyzer: Sample association - " \
|
256
|
+
"source_model: #{sample_association[:source_model]}, " \
|
257
|
+
"target_model: #{sample_association[:target_model]}, " \
|
258
|
+
"type: #{sample_association[:type]}"
|
259
|
+
else
|
260
|
+
Rails.logger.info "ModelAssociationAnalyzer: No associations found"
|
271
261
|
end
|
262
|
+
end
|
263
|
+
|
264
|
+
# === Model Discovery Methods ===
|
272
265
|
|
273
|
-
|
266
|
+
# Check if ActiveRecord is available
|
267
|
+
#
|
268
|
+
# @return [Boolean]
|
269
|
+
def activerecord_available?
|
270
|
+
defined?(ActiveRecord::Base)
|
271
|
+
end
|
272
|
+
|
273
|
+
# Load all available ActiveRecord models including from gems
|
274
|
+
#
|
275
|
+
# @return [Array<Class>] ActiveRecord model classes
|
276
|
+
def load_all_models
|
277
|
+
eager_load_models
|
278
|
+
|
279
|
+
# Get all model classes directly from ActiveRecord descendants
|
280
|
+
all_models = ActiveRecord::Base.descendants
|
281
|
+
.select { |model| valid_model_class?(model) }
|
282
|
+
.uniq
|
283
|
+
|
284
|
+
Rails.logger.debug "ModelAssociationAnalyzer: Found #{all_models.size} total ActiveRecord models"
|
285
|
+
all_models
|
286
|
+
end
|
287
|
+
|
288
|
+
# Check if a model class is valid for analysis
|
289
|
+
#
|
290
|
+
# @param model [Class] ActiveRecord model class
|
291
|
+
# @return [Boolean] true if model is valid
|
292
|
+
def valid_model_class?(model)
|
293
|
+
# Must be a proper class with a name (not anonymous)
|
294
|
+
return false unless model.name
|
295
|
+
|
296
|
+
# Must have a table that exists
|
297
|
+
return false unless model_has_table?(model)
|
298
|
+
|
299
|
+
# Skip abstract models
|
300
|
+
return false if model.abstract_class?
|
301
|
+
|
302
|
+
true
|
303
|
+
rescue StandardError => e
|
304
|
+
Rails.logger.debug "ModelAssociationAnalyzer: Error validating model #{model}: #{e.message}"
|
305
|
+
false
|
274
306
|
end
|
275
307
|
|
276
308
|
# Check if model has a database table
|
@@ -283,6 +315,38 @@ module Dbwatcher
|
|
283
315
|
false
|
284
316
|
end
|
285
317
|
|
318
|
+
# Filter models based on session tables
|
319
|
+
#
|
320
|
+
# @param all_models [Array<Class>] all available models
|
321
|
+
# @return [Array<Class>] filtered models
|
322
|
+
def filter_models_by_session(all_models)
|
323
|
+
return all_models if session_tables.empty?
|
324
|
+
|
325
|
+
# Build a hash of table_name -> model for efficient lookup
|
326
|
+
table_to_models = {}
|
327
|
+
all_models.each do |model|
|
328
|
+
table_name = model.table_name
|
329
|
+
table_to_models[table_name] ||= []
|
330
|
+
table_to_models[table_name] << model
|
331
|
+
rescue StandardError => e
|
332
|
+
Rails.logger.warn "ModelAssociationAnalyzer: Error checking table_name for " \
|
333
|
+
"#{model.name}: #{e.message}"
|
334
|
+
end
|
335
|
+
|
336
|
+
# Select models whose tables are in the session
|
337
|
+
filtered_models = []
|
338
|
+
session_tables.each do |table_name|
|
339
|
+
models_for_table = table_to_models[table_name]
|
340
|
+
filtered_models.concat(models_for_table) if models_for_table
|
341
|
+
end
|
342
|
+
|
343
|
+
Rails.logger.debug "ModelAssociationAnalyzer: Filtered to #{filtered_models.size} " \
|
344
|
+
"models matching session tables (from #{session_tables.size} tables)"
|
345
|
+
filtered_models
|
346
|
+
end
|
347
|
+
|
348
|
+
# === Association Analysis Methods ===
|
349
|
+
|
286
350
|
# Extract associations from all models
|
287
351
|
#
|
288
352
|
# @return [Array<Hash>] associations array
|
@@ -367,36 +431,41 @@ module Dbwatcher
|
|
367
431
|
end
|
368
432
|
end
|
369
433
|
|
370
|
-
# Build
|
434
|
+
# Build a standardized relationship hash
|
371
435
|
#
|
372
436
|
# @param model [Class] source model class
|
373
437
|
# @param association [Object] association object
|
438
|
+
# @param type [String] relationship type
|
374
439
|
# @return [Hash] relationship data
|
375
|
-
def
|
440
|
+
def build_relationship_hash(model, association, type)
|
441
|
+
return nil unless association&.class_name
|
442
|
+
|
376
443
|
{
|
377
444
|
source_model: model.name,
|
378
445
|
source_table: model.table_name,
|
379
446
|
target_model: association.class_name,
|
380
447
|
target_table: get_association_table_name(association),
|
381
|
-
type:
|
448
|
+
type: type,
|
382
449
|
association_name: association.name.to_s
|
383
450
|
}
|
384
451
|
end
|
385
452
|
|
453
|
+
# Build belongs_to relationship
|
454
|
+
#
|
455
|
+
# @param model [Class] source model class
|
456
|
+
# @param association [Object] association object
|
457
|
+
# @return [Hash] relationship data
|
458
|
+
def build_belongs_to_relationship(model, association)
|
459
|
+
build_relationship_hash(model, association, "belongs_to")
|
460
|
+
end
|
461
|
+
|
386
462
|
# Build has_one relationship
|
387
463
|
#
|
388
464
|
# @param model [Class] source model class
|
389
465
|
# @param association [Object] association object
|
390
466
|
# @return [Hash] relationship data
|
391
467
|
def build_has_one_relationship(model, association)
|
392
|
-
|
393
|
-
source_model: model.name,
|
394
|
-
source_table: model.table_name,
|
395
|
-
target_model: association.class_name,
|
396
|
-
target_table: get_association_table_name(association),
|
397
|
-
type: "has_one",
|
398
|
-
association_name: association.name.to_s
|
399
|
-
}
|
468
|
+
build_relationship_hash(model, association, "has_one")
|
400
469
|
end
|
401
470
|
|
402
471
|
# Build has_many relationship
|
@@ -405,14 +474,7 @@ module Dbwatcher
|
|
405
474
|
# @param association [Object] association object
|
406
475
|
# @return [Hash] relationship data
|
407
476
|
def build_has_many_relationship(model, association)
|
408
|
-
|
409
|
-
source_model: model.name,
|
410
|
-
source_table: model.table_name,
|
411
|
-
target_model: association.class_name,
|
412
|
-
target_table: get_association_table_name(association),
|
413
|
-
type: "has_many",
|
414
|
-
association_name: association.name.to_s
|
415
|
-
}
|
477
|
+
build_relationship_hash(model, association, "has_many")
|
416
478
|
end
|
417
479
|
|
418
480
|
# Build has_many :through relationship
|
@@ -421,14 +483,9 @@ module Dbwatcher
|
|
421
483
|
# @param association [Object] association object
|
422
484
|
# @return [Hash] relationship data
|
423
485
|
def build_has_many_through_relationship(model, association)
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
target_model: association.class_name,
|
428
|
-
target_table: get_association_table_name(association),
|
429
|
-
type: "has_many_through",
|
430
|
-
association_name: "#{association.name} (through #{association.options[:through]})"
|
431
|
-
}
|
486
|
+
relationship = build_relationship_hash(model, association, "has_many_through")
|
487
|
+
relationship[:association_name] = "#{association.name} (through #{association.options[:through]})"
|
488
|
+
relationship
|
432
489
|
end
|
433
490
|
|
434
491
|
# Build has_and_belongs_to_many relationship
|
@@ -437,14 +494,7 @@ module Dbwatcher
|
|
437
494
|
# @param association [Object] association object
|
438
495
|
# @return [Hash] relationship data
|
439
496
|
def build_habtm_relationship(model, association)
|
440
|
-
|
441
|
-
source_model: model.name,
|
442
|
-
source_table: model.table_name,
|
443
|
-
target_model: association.class_name,
|
444
|
-
target_table: get_association_table_name(association),
|
445
|
-
type: "has_and_belongs_to_many",
|
446
|
-
association_name: association.name.to_s
|
447
|
-
}
|
497
|
+
build_relationship_hash(model, association, "has_and_belongs_to_many")
|
448
498
|
end
|
449
499
|
|
450
500
|
# Build Active Storage relationship
|
@@ -474,17 +524,6 @@ module Dbwatcher
|
|
474
524
|
nil
|
475
525
|
end
|
476
526
|
|
477
|
-
# Find model class by name
|
478
|
-
#
|
479
|
-
# @param model_name [String] model class name
|
480
|
-
# @return [Class, nil] model class
|
481
|
-
def find_model_class(model_name)
|
482
|
-
model_name.constantize
|
483
|
-
rescue StandardError => e
|
484
|
-
Rails.logger.warn "ModelAssociationAnalyzer: Could not find model class #{model_name}: #{e.message}"
|
485
|
-
nil
|
486
|
-
end
|
487
|
-
|
488
527
|
# Extract attributes from model class
|
489
528
|
#
|
490
529
|
# @param model_class [Class, nil] ActiveRecord model class
|
@@ -96,9 +96,11 @@ module Dbwatcher
|
|
96
96
|
#
|
97
97
|
# @param relationship [Relationship] relationship to remove
|
98
98
|
# @return [Boolean] true if relationship was removed
|
99
|
+
# rubocop:disable Naming/PredicateMethod
|
99
100
|
def remove_relationship(relationship)
|
100
101
|
!@relationships.delete(relationship).nil?
|
101
102
|
end
|
103
|
+
# rubocop:enable Naming/PredicateMethod
|
102
104
|
|
103
105
|
# Get relationships for an entity
|
104
106
|
#
|
@@ -69,12 +69,12 @@ module Dbwatcher
|
|
69
69
|
def build_attributes_section(entity)
|
70
70
|
return [] unless show_attributes? && entity.attributes.any?
|
71
71
|
|
72
|
-
lines = [" %% Attributes"]
|
72
|
+
lines = [" %% === Attributes ==="]
|
73
73
|
entity.attributes.first(max_attributes).each do |attr|
|
74
74
|
lines << format_attribute_line(attr)
|
75
75
|
end
|
76
76
|
add_attributes_overflow_message(lines, entity)
|
77
|
-
|
77
|
+
lines << ""
|
78
78
|
lines
|
79
79
|
end
|
80
80
|
|
@@ -82,14 +82,14 @@ module Dbwatcher
|
|
82
82
|
def build_methods_section(entity)
|
83
83
|
return [] unless show_methods? && entity.metadata[:methods]&.any?
|
84
84
|
|
85
|
-
lines = [" %% Methods"]
|
85
|
+
lines = [" %% === Methods ==="]
|
86
86
|
entity.metadata[:methods].first(max_methods).each do |method|
|
87
87
|
lines << format_method_line(method)
|
88
88
|
end
|
89
89
|
if entity.metadata[:methods].size > max_methods
|
90
90
|
lines << " %% ... #{entity.metadata[:methods].size - max_methods} more methods"
|
91
91
|
end
|
92
|
-
lines << "
|
92
|
+
lines << ""
|
93
93
|
lines
|
94
94
|
end
|
95
95
|
|
@@ -97,16 +97,20 @@ module Dbwatcher
|
|
97
97
|
def build_statistics_section(entity)
|
98
98
|
return [] unless entity.attributes.any? || entity.metadata[:methods]&.any?
|
99
99
|
|
100
|
-
lines = [" %% Statistics"]
|
101
|
-
|
102
|
-
|
100
|
+
lines = [" %% === Statistics ==="]
|
101
|
+
stats_parts = []
|
102
|
+
stats_parts << "#{entity.attributes.size} attributes" if entity.attributes.any?
|
103
|
+
stats_parts << "#{entity.metadata[:methods].size} methods" if entity.metadata[:methods]&.any?
|
104
|
+
lines << " %% #{stats_parts.join(" | ")}"
|
103
105
|
lines
|
104
106
|
end
|
105
107
|
|
106
108
|
# Build class definition
|
107
109
|
def build_class_definition(entity)
|
108
|
-
|
109
|
-
|
110
|
+
display_name = Sanitizer.display_name(entity.name)
|
111
|
+
|
112
|
+
# Use display name (with ::) for class definition instead of sanitized version
|
113
|
+
lines = [" class `#{display_name}` {"]
|
110
114
|
lines += build_attributes_section(entity)
|
111
115
|
lines += build_methods_section(entity)
|
112
116
|
lines += build_statistics_section(entity)
|
@@ -38,7 +38,9 @@ module Dbwatcher
|
|
38
38
|
# Format class name for diagram
|
39
39
|
def format_class_name(entity_id, dataset)
|
40
40
|
entity_name = dataset.get_entity(entity_id)&.name || entity_id
|
41
|
-
Sanitizer.
|
41
|
+
display_name = Sanitizer.display_name(entity_name)
|
42
|
+
# Use backticks to allow :: in class names for Mermaid
|
43
|
+
"`#{display_name}`"
|
42
44
|
end
|
43
45
|
end
|
44
46
|
end
|
@@ -16,7 +16,12 @@ module Dbwatcher
|
|
16
16
|
def class_name(name)
|
17
17
|
return "UnknownClass" unless name.is_a?(String) && !name.empty?
|
18
18
|
|
19
|
-
|
19
|
+
# For namespaced models, preserve the namespace structure
|
20
|
+
# Convert :: to underscore for Mermaid syntax while maintaining readability
|
21
|
+
sanitized = name.to_s.gsub("::", "__")
|
22
|
+
|
23
|
+
# Only replace other special characters with underscores
|
24
|
+
sanitized.gsub(/[^a-zA-Z0-9_]/, "_")
|
20
25
|
end
|
21
26
|
|
22
27
|
# Sanitize node name for Mermaid flowcharts
|
@@ -86,6 +91,17 @@ module Dbwatcher
|
|
86
91
|
label.to_s.gsub("\\", "\\\\").gsub('"', '\\"').gsub(/[\n\r]/, " ").strip
|
87
92
|
end
|
88
93
|
|
94
|
+
# Get display name for class (preserves namespace format for labels)
|
95
|
+
#
|
96
|
+
# @param name [String] raw class name
|
97
|
+
# @return [String] display name with proper namespace format
|
98
|
+
def display_name(name)
|
99
|
+
return "UnknownClass" unless name.is_a?(String) && !name.empty?
|
100
|
+
|
101
|
+
# Return the original name for display purposes (preserves :: for namespaces)
|
102
|
+
name.to_s
|
103
|
+
end
|
104
|
+
|
89
105
|
# Sanitize attribute type for Mermaid ERD
|
90
106
|
#
|
91
107
|
# @param type [String] raw attribute type
|