dbwatcher 1.1.4 → 1.1.6
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 +80 -26
- data/app/assets/config/dbwatcher_manifest.js +9 -0
- data/app/assets/images/dbwatcher/README.md +24 -0
- data/app/assets/images/dbwatcher/apple-touch-icon.png +0 -0
- data/app/assets/images/dbwatcher/dbwatcher-tranparent_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/site.webmanifest +21 -0
- data/app/assets/images/dbwatcher/unused-assets.zip +0 -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/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 +49 -83
- data/lib/dbwatcher/logging.rb +2 -2
- 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/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/mermaid_syntax/erd_builder.rb +2 -2
- data/lib/dbwatcher/services/mermaid_syntax/sanitizer.rb +4 -14
- 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 +17 -2
@@ -0,0 +1,224 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Services
|
5
|
+
module DiagramAnalyzers
|
6
|
+
module ModelAnalysis
|
7
|
+
# Service responsible for analyzing ActiveRecord model associations
|
8
|
+
#
|
9
|
+
# This service handles the extraction and analysis of model associations,
|
10
|
+
# converting them into standardized relationship data for diagram generation.
|
11
|
+
class AssociationExtractor
|
12
|
+
attr_reader :session_tables
|
13
|
+
|
14
|
+
# Initialize with optional session tables for scope filtering
|
15
|
+
#
|
16
|
+
# @param session_tables [Array<String>] table names from session (empty for global analysis)
|
17
|
+
def initialize(session_tables = [])
|
18
|
+
@session_tables = session_tables || []
|
19
|
+
end
|
20
|
+
|
21
|
+
# Extract associations from all provided models
|
22
|
+
#
|
23
|
+
# @param models [Array<Class>] ActiveRecord model classes to analyze
|
24
|
+
# @return [Array<Hash>] associations array
|
25
|
+
def extract_all(models)
|
26
|
+
associations = []
|
27
|
+
|
28
|
+
models.each do |model|
|
29
|
+
model_associations = get_model_associations(model)
|
30
|
+
|
31
|
+
model_associations.each do |association|
|
32
|
+
# Skip polymorphic associations for now
|
33
|
+
next if association.options[:polymorphic]
|
34
|
+
|
35
|
+
# Skip if target model is not in scope
|
36
|
+
next unless target_model_in_scope?(association)
|
37
|
+
|
38
|
+
# Build relationship based on association type
|
39
|
+
relationship = build_association_relationship(model, association)
|
40
|
+
associations << relationship if relationship
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
associations
|
45
|
+
end
|
46
|
+
|
47
|
+
# Generate placeholder associations for models without associations
|
48
|
+
#
|
49
|
+
# @param models [Array<Class>] models to create placeholders for
|
50
|
+
# @return [Array<Hash>] placeholder associations
|
51
|
+
def generate_placeholder_associations(models)
|
52
|
+
result = models.map do |model|
|
53
|
+
{
|
54
|
+
type: "node_only",
|
55
|
+
source_model: model.name,
|
56
|
+
source_table: model.table_name,
|
57
|
+
target_model: nil,
|
58
|
+
target_table: nil,
|
59
|
+
association_name: nil
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
Rails.logger.info "AssociationExtractor: Generated #{result.size} placeholder nodes"
|
64
|
+
result
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# Get associations for a model
|
70
|
+
#
|
71
|
+
# @param model [Class] ActiveRecord model class
|
72
|
+
# @return [Array] association objects
|
73
|
+
def get_model_associations(model)
|
74
|
+
model.reflect_on_all_associations
|
75
|
+
rescue StandardError => e
|
76
|
+
Rails.logger.warn "AssociationExtractor: Could not get associations for #{model.name}: #{e.message}"
|
77
|
+
[]
|
78
|
+
end
|
79
|
+
|
80
|
+
# Check if target model is in analysis scope
|
81
|
+
#
|
82
|
+
# @param association [Object] association object
|
83
|
+
# @return [Boolean] true if target model should be included
|
84
|
+
def target_model_in_scope?(association)
|
85
|
+
target_table = get_association_table_name(association)
|
86
|
+
|
87
|
+
# If analyzing session, both tables must be in session
|
88
|
+
# If analyzing globally, include all
|
89
|
+
return true if session_tables.empty?
|
90
|
+
|
91
|
+
# Skip if target table is not in session
|
92
|
+
return false if target_table && !session_tables.include?(target_table)
|
93
|
+
|
94
|
+
true
|
95
|
+
end
|
96
|
+
|
97
|
+
# Build relationship hash based on association type
|
98
|
+
#
|
99
|
+
# @param model [Class] source model class
|
100
|
+
# @param association [Object] association object
|
101
|
+
# @return [Hash] relationship data
|
102
|
+
def build_association_relationship(model, association)
|
103
|
+
case association.macro
|
104
|
+
when :belongs_to
|
105
|
+
build_belongs_to_relationship(model, association)
|
106
|
+
when :has_one
|
107
|
+
build_has_one_relationship(model, association)
|
108
|
+
when :has_many
|
109
|
+
if association.options[:through]
|
110
|
+
build_has_many_through_relationship(model, association)
|
111
|
+
else
|
112
|
+
build_has_many_relationship(model, association)
|
113
|
+
end
|
114
|
+
when :has_and_belongs_to_many
|
115
|
+
build_habtm_relationship(model, association)
|
116
|
+
else
|
117
|
+
# Handle special cases like has_one_attached from Active Storage
|
118
|
+
if association.name.to_s.end_with?("_attachment") || association.name.to_s.end_with?("_attachments")
|
119
|
+
build_active_storage_relationship(model, association)
|
120
|
+
else
|
121
|
+
Rails.logger.warn "AssociationExtractor: Unknown association type: " \
|
122
|
+
"#{association.macro} for #{model.name}.#{association.name}"
|
123
|
+
nil
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Build a standardized relationship hash
|
129
|
+
#
|
130
|
+
# @param model [Class] source model class
|
131
|
+
# @param association [Object] association object
|
132
|
+
# @param type [String] relationship type
|
133
|
+
# @return [Hash] relationship data
|
134
|
+
def build_relationship_hash(model, association, type)
|
135
|
+
return nil unless association&.class_name
|
136
|
+
|
137
|
+
{
|
138
|
+
source_model: model.name,
|
139
|
+
source_table: model.table_name,
|
140
|
+
target_model: association.class_name,
|
141
|
+
target_table: get_association_table_name(association),
|
142
|
+
type: type,
|
143
|
+
association_name: association.name.to_s
|
144
|
+
}
|
145
|
+
end
|
146
|
+
|
147
|
+
# Build belongs_to relationship
|
148
|
+
#
|
149
|
+
# @param model [Class] source model class
|
150
|
+
# @param association [Object] association object
|
151
|
+
# @return [Hash] relationship data
|
152
|
+
def build_belongs_to_relationship(model, association)
|
153
|
+
build_relationship_hash(model, association, "belongs_to")
|
154
|
+
end
|
155
|
+
|
156
|
+
# Build has_one relationship
|
157
|
+
#
|
158
|
+
# @param model [Class] source model class
|
159
|
+
# @param association [Object] association object
|
160
|
+
# @return [Hash] relationship data
|
161
|
+
def build_has_one_relationship(model, association)
|
162
|
+
build_relationship_hash(model, association, "has_one")
|
163
|
+
end
|
164
|
+
|
165
|
+
# Build has_many relationship
|
166
|
+
#
|
167
|
+
# @param model [Class] source model class
|
168
|
+
# @param association [Object] association object
|
169
|
+
# @return [Hash] relationship data
|
170
|
+
def build_has_many_relationship(model, association)
|
171
|
+
build_relationship_hash(model, association, "has_many")
|
172
|
+
end
|
173
|
+
|
174
|
+
# Build has_many :through relationship
|
175
|
+
#
|
176
|
+
# @param model [Class] source model class
|
177
|
+
# @param association [Object] association object
|
178
|
+
# @return [Hash] relationship data
|
179
|
+
def build_has_many_through_relationship(model, association)
|
180
|
+
relationship = build_relationship_hash(model, association, "has_many_through")
|
181
|
+
relationship[:association_name] = "#{association.name} (through #{association.options[:through]})"
|
182
|
+
relationship
|
183
|
+
end
|
184
|
+
|
185
|
+
# Build has_and_belongs_to_many relationship
|
186
|
+
#
|
187
|
+
# @param model [Class] source model class
|
188
|
+
# @param association [Object] association object
|
189
|
+
# @return [Hash] relationship data
|
190
|
+
def build_habtm_relationship(model, association)
|
191
|
+
build_relationship_hash(model, association, "has_and_belongs_to_many")
|
192
|
+
end
|
193
|
+
|
194
|
+
# Build Active Storage relationship
|
195
|
+
#
|
196
|
+
# @param model [Class] source model class
|
197
|
+
# @param association [Object] association object
|
198
|
+
# @return [Hash] relationship data
|
199
|
+
def build_active_storage_relationship(model, association)
|
200
|
+
{
|
201
|
+
source_model: model.name,
|
202
|
+
source_table: model.table_name,
|
203
|
+
target_model: "ActiveStorage::Attachment",
|
204
|
+
target_table: "active_storage_attachments",
|
205
|
+
type: "has_one",
|
206
|
+
association_name: association.name.to_s
|
207
|
+
}
|
208
|
+
end
|
209
|
+
|
210
|
+
# Get table name for association target
|
211
|
+
#
|
212
|
+
# @param association [Object] association object
|
213
|
+
# @return [String, nil] table name
|
214
|
+
def get_association_table_name(association)
|
215
|
+
association.table_name
|
216
|
+
rescue StandardError => e
|
217
|
+
Rails.logger.warn "AssociationExtractor: Could not get table name for #{association.name}: #{e.message}"
|
218
|
+
nil
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Services
|
5
|
+
module DiagramAnalyzers
|
6
|
+
module ModelAnalysis
|
7
|
+
# Service responsible for building datasets from model associations
|
8
|
+
#
|
9
|
+
# This service handles the transformation of model association data into
|
10
|
+
# standardized Dataset format with entities and relationships.
|
11
|
+
class DatasetBuilder
|
12
|
+
# Create entities from discovered model classes
|
13
|
+
#
|
14
|
+
# @param dataset [DiagramData::Dataset] dataset to add entities to
|
15
|
+
# @param models [Array<Class>] ActiveRecord model classes
|
16
|
+
# @return [void]
|
17
|
+
def create_entities_from_models(dataset, models)
|
18
|
+
models.each do |model_class|
|
19
|
+
attributes = extract_model_attributes(model_class)
|
20
|
+
methods = extract_model_methods(model_class)
|
21
|
+
|
22
|
+
entity = create_entity(
|
23
|
+
id: model_class.table_name,
|
24
|
+
name: model_class.name,
|
25
|
+
type: "model",
|
26
|
+
attributes: attributes,
|
27
|
+
metadata: {
|
28
|
+
table_name: model_class.table_name,
|
29
|
+
model_class: model_class.name,
|
30
|
+
methods: methods
|
31
|
+
}
|
32
|
+
)
|
33
|
+
dataset.add_entity(entity)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Create relationships from association data
|
38
|
+
#
|
39
|
+
# @param dataset [DiagramData::Dataset] dataset to add relationships to
|
40
|
+
# @param raw_data [Array<Hash>] raw association data
|
41
|
+
# @return [void]
|
42
|
+
def create_relationships_from_associations(dataset, raw_data)
|
43
|
+
raw_data.each do |association|
|
44
|
+
next if association[:type] == "node_only" || !association[:target_model]
|
45
|
+
|
46
|
+
source_id = association[:source_table]
|
47
|
+
target_id = association[:target_table]
|
48
|
+
|
49
|
+
# Skip if we don't have valid table IDs
|
50
|
+
next unless source_id && target_id
|
51
|
+
|
52
|
+
# Determine cardinality based on relationship type
|
53
|
+
cardinality = determine_cardinality(association[:type])
|
54
|
+
|
55
|
+
relationship = create_relationship({
|
56
|
+
source_id: source_id,
|
57
|
+
target_id: target_id,
|
58
|
+
type: association[:type],
|
59
|
+
label: association[:association_name],
|
60
|
+
cardinality: cardinality,
|
61
|
+
metadata: {
|
62
|
+
association_name: association[:association_name],
|
63
|
+
source_model: association[:source_model],
|
64
|
+
target_model: association[:target_model],
|
65
|
+
original_type: association[:type],
|
66
|
+
self_referential: source_id == target_id
|
67
|
+
}
|
68
|
+
})
|
69
|
+
|
70
|
+
dataset.add_relationship(relationship)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Build complete dataset from associations and models
|
75
|
+
#
|
76
|
+
# @param raw_associations [Array<Hash>] raw association data
|
77
|
+
# @param models [Array<Class>] ActiveRecord model classes
|
78
|
+
# @return [DiagramData::Dataset] standardized dataset
|
79
|
+
def build_from_associations(raw_associations, models)
|
80
|
+
dataset = create_empty_dataset
|
81
|
+
dataset.metadata.merge!({
|
82
|
+
total_relationships: raw_associations.count { |a| a[:target_model] },
|
83
|
+
total_models: models.length,
|
84
|
+
model_names: models.map(&:name)
|
85
|
+
})
|
86
|
+
|
87
|
+
# Create entities from actual discovered models (no inference)
|
88
|
+
create_entities_from_models(dataset, models)
|
89
|
+
|
90
|
+
# Create relationships from association data
|
91
|
+
create_relationships_from_associations(dataset, raw_associations)
|
92
|
+
|
93
|
+
dataset
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
# Extract attributes from model class
|
99
|
+
#
|
100
|
+
# @param model_class [Class, nil] ActiveRecord model class
|
101
|
+
# @return [Array<Attribute>] model attributes
|
102
|
+
def extract_model_attributes(model_class)
|
103
|
+
return [] unless model_class.respond_to?(:columns)
|
104
|
+
|
105
|
+
begin
|
106
|
+
model_class.columns.map do |column|
|
107
|
+
create_attribute(
|
108
|
+
name: column.name,
|
109
|
+
type: column.type.to_s,
|
110
|
+
nullable: column.null,
|
111
|
+
default: column.default,
|
112
|
+
metadata: {
|
113
|
+
primary_key: column.name == model_class.primary_key,
|
114
|
+
foreign_key: column.name.end_with?("_id"),
|
115
|
+
visibility: "+" # Public visibility for all columns
|
116
|
+
}
|
117
|
+
)
|
118
|
+
end
|
119
|
+
rescue StandardError => e
|
120
|
+
Rails.logger.warn "DatasetBuilder: Could not extract attributes for " \
|
121
|
+
"#{model_class.name}: #{e.message}"
|
122
|
+
[]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Extract methods from model class
|
127
|
+
#
|
128
|
+
# @param model_class [Class, nil] ActiveRecord model class
|
129
|
+
# @return [Array<Hash>] model methods
|
130
|
+
def extract_model_methods(model_class)
|
131
|
+
return [] unless model_class && Dbwatcher.configuration.diagram_show_methods
|
132
|
+
|
133
|
+
methods = []
|
134
|
+
|
135
|
+
begin
|
136
|
+
# Get instance methods defined in the model (not inherited from ActiveRecord::Base)
|
137
|
+
model_methods = model_class.instance_methods - ActiveRecord::Base.instance_methods
|
138
|
+
model_methods.each do |method_name|
|
139
|
+
# Skip association methods and attribute methods
|
140
|
+
next if method_name.to_s.end_with?("=") || model_class.column_names.include?(method_name.to_s)
|
141
|
+
|
142
|
+
methods << {
|
143
|
+
name: method_name.to_s,
|
144
|
+
visibility: "+" # Public visibility for all methods
|
145
|
+
}
|
146
|
+
end
|
147
|
+
rescue StandardError => e
|
148
|
+
Rails.logger.warn "DatasetBuilder: Could not extract methods for " \
|
149
|
+
"#{model_class.name}: #{e.message}"
|
150
|
+
end
|
151
|
+
|
152
|
+
methods
|
153
|
+
end
|
154
|
+
|
155
|
+
# Determine cardinality based on relationship type
|
156
|
+
#
|
157
|
+
# @param relationship_type [String] relationship type
|
158
|
+
# @return [String, nil] cardinality type
|
159
|
+
def determine_cardinality(relationship_type)
|
160
|
+
case relationship_type
|
161
|
+
when "has_many"
|
162
|
+
"one_to_many"
|
163
|
+
when "belongs_to"
|
164
|
+
"many_to_one"
|
165
|
+
when "has_one"
|
166
|
+
"one_to_one"
|
167
|
+
when "has_and_belongs_to_many", "has_many_through"
|
168
|
+
"many_to_many"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Helper method to create entities (delegated to base analyzer)
|
173
|
+
#
|
174
|
+
# @param params [Hash] entity parameters
|
175
|
+
# @return [DiagramData::Entity] new entity
|
176
|
+
def create_entity(**params)
|
177
|
+
Dbwatcher::Services::DiagramData::Entity.new(
|
178
|
+
id: params[:id],
|
179
|
+
name: params[:name],
|
180
|
+
type: params[:type] || "default",
|
181
|
+
attributes: params[:attributes] || [],
|
182
|
+
metadata: params[:metadata] || {}
|
183
|
+
)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Helper method to create relationships (delegated to base analyzer)
|
187
|
+
#
|
188
|
+
# @param params [Hash] relationship parameters
|
189
|
+
# @return [DiagramData::Relationship] new relationship
|
190
|
+
def create_relationship(params)
|
191
|
+
params_obj = Dbwatcher::Services::DiagramData::RelationshipParams.new(params)
|
192
|
+
Dbwatcher::Services::DiagramData::Relationship.new(params_obj)
|
193
|
+
end
|
194
|
+
|
195
|
+
# Helper method to create attributes (delegated to base analyzer)
|
196
|
+
#
|
197
|
+
# @param params [Hash] attribute parameters
|
198
|
+
# @return [DiagramData::Attribute] new attribute
|
199
|
+
def create_attribute(**params)
|
200
|
+
Dbwatcher::Services::DiagramData::Attribute.new(
|
201
|
+
name: params[:name],
|
202
|
+
type: params[:type],
|
203
|
+
nullable: params[:nullable] || true,
|
204
|
+
default: params[:default],
|
205
|
+
metadata: params[:metadata] || {}
|
206
|
+
)
|
207
|
+
end
|
208
|
+
|
209
|
+
# Helper method to create empty dataset (delegated to base analyzer)
|
210
|
+
#
|
211
|
+
# @return [DiagramData::Dataset] empty dataset
|
212
|
+
def create_empty_dataset
|
213
|
+
Dbwatcher::Services::DiagramData::Dataset.new(
|
214
|
+
metadata: {
|
215
|
+
analyzer: "ModelAssociationAnalyzer",
|
216
|
+
analyzer_type: "model_association",
|
217
|
+
empty_reason: "No data found or analysis failed",
|
218
|
+
generated_at: Time.current.iso8601
|
219
|
+
}
|
220
|
+
)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Services
|
5
|
+
module DiagramAnalyzers
|
6
|
+
module ModelAnalysis
|
7
|
+
# Service responsible for discovering and filtering ActiveRecord models
|
8
|
+
#
|
9
|
+
# This service handles the complex logic of finding ActiveRecord models
|
10
|
+
# that are relevant for diagram analysis, including models from gems,
|
11
|
+
# namespaced models, and models with custom table names.
|
12
|
+
class ModelDiscovery
|
13
|
+
attr_reader :session_tables, :discovered_models
|
14
|
+
|
15
|
+
# Initialize with optional session tables for filtering
|
16
|
+
#
|
17
|
+
# @param session_tables [Array<String>] table names from session (empty for global analysis)
|
18
|
+
def initialize(session_tables = [])
|
19
|
+
@session_tables = session_tables || []
|
20
|
+
@discovered_models = []
|
21
|
+
end
|
22
|
+
|
23
|
+
# Discover models that correspond to session tables or all models if no session
|
24
|
+
#
|
25
|
+
# @return [Array<Class>] ActiveRecord model classes
|
26
|
+
def discover
|
27
|
+
return [] unless activerecord_available?
|
28
|
+
|
29
|
+
begin
|
30
|
+
all_models = load_all_models
|
31
|
+
@discovered_models = filter_models_by_session(all_models)
|
32
|
+
|
33
|
+
log_discovery_results
|
34
|
+
@discovered_models
|
35
|
+
rescue StandardError => e
|
36
|
+
Rails.logger.error "ModelDiscovery: Error discovering models: #{e.message}"
|
37
|
+
[]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Load all available ActiveRecord models including from gems
|
42
|
+
#
|
43
|
+
# @return [Array<Class>] ActiveRecord model classes
|
44
|
+
def load_all_models
|
45
|
+
eager_load_models
|
46
|
+
|
47
|
+
# Get all model classes directly from ActiveRecord descendants
|
48
|
+
all_models = ActiveRecord::Base.descendants
|
49
|
+
.select { |model| valid_model_class?(model) }
|
50
|
+
.uniq
|
51
|
+
|
52
|
+
Rails.logger.debug "ModelDiscovery: Found #{all_models.size} total ActiveRecord models"
|
53
|
+
all_models
|
54
|
+
end
|
55
|
+
|
56
|
+
# Check if a model class is valid for analysis
|
57
|
+
#
|
58
|
+
# @param model [Class] ActiveRecord model class
|
59
|
+
# @return [Boolean] true if model is valid
|
60
|
+
def valid_model_class?(model)
|
61
|
+
# Must be a proper class with a name (not anonymous)
|
62
|
+
return false unless model.name
|
63
|
+
|
64
|
+
# Must have a table that exists
|
65
|
+
return false unless model_has_table?(model)
|
66
|
+
|
67
|
+
# Skip abstract models
|
68
|
+
return false if model.abstract_class?
|
69
|
+
|
70
|
+
true
|
71
|
+
rescue StandardError => e
|
72
|
+
Rails.logger.debug "ModelDiscovery: Error validating model #{model}: #{e.message}"
|
73
|
+
false
|
74
|
+
end
|
75
|
+
|
76
|
+
# Check if model has a database table
|
77
|
+
#
|
78
|
+
# @param model [Class] ActiveRecord model class
|
79
|
+
# @return [Boolean] true if model has a table
|
80
|
+
def model_has_table?(model)
|
81
|
+
model.table_exists?
|
82
|
+
rescue StandardError
|
83
|
+
false
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
# Check if ActiveRecord is available
|
89
|
+
#
|
90
|
+
# @return [Boolean]
|
91
|
+
def activerecord_available?
|
92
|
+
defined?(ActiveRecord::Base)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Eagerly load all models including those from gems
|
96
|
+
#
|
97
|
+
# @return [void]
|
98
|
+
def eager_load_models
|
99
|
+
return unless defined?(Rails) && Rails.respond_to?(:application)
|
100
|
+
|
101
|
+
begin
|
102
|
+
# Force eager loading of application models
|
103
|
+
Rails.application.eager_load!
|
104
|
+
|
105
|
+
# Also load models from engines/gems if any are configured
|
106
|
+
Rails::Engine.descendants.each do |engine|
|
107
|
+
engine.eager_load! if engine.respond_to?(:eager_load!)
|
108
|
+
rescue StandardError => e
|
109
|
+
error_message = "ModelDiscovery: Could not eager load engine #{engine.class.name}: #{e.message}"
|
110
|
+
Rails.logger.debug error_message
|
111
|
+
end
|
112
|
+
rescue StandardError => e
|
113
|
+
Rails.logger.debug "ModelDiscovery: Could not eager load models: #{e.message}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Filter models based on session tables
|
118
|
+
#
|
119
|
+
# @param all_models [Array<Class>] all available models
|
120
|
+
# @return [Array<Class>] filtered models
|
121
|
+
def filter_models_by_session(all_models)
|
122
|
+
return all_models if session_tables.empty?
|
123
|
+
|
124
|
+
# Build a hash of table_name -> model for efficient lookup
|
125
|
+
table_to_models = {}
|
126
|
+
all_models.each do |model|
|
127
|
+
table_name = model.table_name
|
128
|
+
table_to_models[table_name] ||= []
|
129
|
+
table_to_models[table_name] << model
|
130
|
+
rescue StandardError => e
|
131
|
+
Rails.logger.warn "ModelDiscovery: Error checking table_name for " \
|
132
|
+
"#{model.name}: #{e.message}"
|
133
|
+
end
|
134
|
+
|
135
|
+
# Select models whose tables are in the session
|
136
|
+
filtered_models = []
|
137
|
+
session_tables.each do |table_name|
|
138
|
+
models_for_table = table_to_models[table_name]
|
139
|
+
filtered_models.concat(models_for_table) if models_for_table
|
140
|
+
end
|
141
|
+
|
142
|
+
filtered_models
|
143
|
+
end
|
144
|
+
|
145
|
+
# Log discovery results for debugging
|
146
|
+
#
|
147
|
+
# @return [void]
|
148
|
+
def log_discovery_results
|
149
|
+
if discovered_models.any?
|
150
|
+
model_table_info = discovered_models.map { |m| "#{m.name} (#{m.table_name})" }
|
151
|
+
Rails.logger.debug "ModelDiscovery: Discovered models: #{model_table_info.join(", ")}"
|
152
|
+
end
|
153
|
+
|
154
|
+
Rails.logger.debug "ModelDiscovery: Filtered to #{discovered_models.size} " \
|
155
|
+
"models matching session tables (from #{session_tables.size} tables)"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|