dbwatcher 1.1.3 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +79 -26
- data/app/assets/images/dbwatcher/apple-touch-icon.png +0 -0
- data/app/assets/images/dbwatcher/dbwatcher-social-preview.png +0 -0
- data/app/assets/images/dbwatcher/dbwatcher-tranparent_512x512.png +0 -0
- data/app/assets/images/dbwatcher/dbwatcher_512x512.png +0 -0
- data/app/assets/images/dbwatcher/favicon-96x96.png +0 -0
- data/app/assets/images/dbwatcher/favicon.ico +0 -0
- data/app/assets/images/dbwatcher/favicon.svg +3 -0
- data/app/assets/images/dbwatcher/site.webmanifest +21 -0
- data/app/assets/images/dbwatcher/web-app-manifest-192x192.png +0 -0
- data/app/assets/images/dbwatcher/web-app-manifest-512x512.png +0 -0
- data/app/assets/stylesheets/dbwatcher/application.css +38 -4
- data/app/assets/stylesheets/dbwatcher/components/_tabulator.scss +57 -13
- data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +14 -18
- data/app/controllers/dbwatcher/api/v1/system_info_controller.rb +1 -1
- data/app/controllers/dbwatcher/dashboard_controller.rb +1 -1
- data/app/views/dbwatcher/dashboard/_overview.html.erb +8 -7
- data/app/views/dbwatcher/sessions/index.html.erb +42 -59
- data/app/views/layouts/dbwatcher/application.html.erb +22 -6
- data/lib/dbwatcher/configuration.rb +51 -74
- data/lib/dbwatcher/logging.rb +23 -1
- data/lib/dbwatcher/services/diagram_analyzers/concerns/activerecord_introspection.rb +60 -0
- data/lib/dbwatcher/services/diagram_analyzers/concerns/association_scope_filtering.rb +60 -0
- data/lib/dbwatcher/services/diagram_analyzers/inferred_relationship_analyzer.rb +62 -36
- data/lib/dbwatcher/services/diagram_analyzers/model_analysis/association_extractor.rb +224 -0
- data/lib/dbwatcher/services/diagram_analyzers/model_analysis/dataset_builder.rb +226 -0
- data/lib/dbwatcher/services/diagram_analyzers/model_analysis/model_discovery.rb +161 -0
- data/lib/dbwatcher/services/diagram_analyzers/model_association_analyzer.rb +27 -514
- data/lib/dbwatcher/services/diagram_data/attribute.rb +22 -83
- data/lib/dbwatcher/services/diagram_data/base.rb +129 -0
- data/lib/dbwatcher/services/diagram_data/entity.rb +23 -72
- data/lib/dbwatcher/services/diagram_data/relationship.rb +15 -66
- data/lib/dbwatcher/services/diagram_generator.rb +35 -69
- data/lib/dbwatcher/services/diagram_strategies/base_diagram_strategy.rb +23 -9
- data/lib/dbwatcher/services/diagram_strategies/class_diagram_strategy.rb +16 -22
- data/lib/dbwatcher/services/diagram_strategies/diagram_strategy_helpers.rb +33 -0
- data/lib/dbwatcher/services/diagram_strategies/erd_diagram_strategy.rb +20 -25
- data/lib/dbwatcher/services/diagram_strategies/flowchart_diagram_strategy.rb +20 -25
- data/lib/dbwatcher/services/diagram_strategies/standard_diagram_strategy.rb +80 -0
- data/lib/dbwatcher/services/diagram_system.rb +14 -1
- data/lib/dbwatcher/services/mermaid_syntax/base_builder.rb +2 -0
- data/lib/dbwatcher/services/mermaid_syntax/erd_builder.rb +2 -2
- data/lib/dbwatcher/services/mermaid_syntax/sanitizer.rb +4 -14
- data/lib/dbwatcher/services/mermaid_syntax_builder.rb +10 -8
- data/lib/dbwatcher/services/system_info/runtime_info_collector.rb +7 -7
- data/lib/dbwatcher/services/system_info/system_info_collector.rb +3 -3
- data/lib/dbwatcher/services/timeline_data_service/entry_builder.rb +23 -1
- data/lib/dbwatcher/storage/session_storage.rb +2 -2
- data/lib/dbwatcher/storage.rb +1 -1
- data/lib/dbwatcher/version.rb +1 -1
- metadata +20 -2
data/lib/dbwatcher/logging.rb
CHANGED
@@ -14,9 +14,12 @@ module Dbwatcher
|
|
14
14
|
end
|
15
15
|
|
16
16
|
# Log a debug message with optional context
|
17
|
+
# Only logs if debug mode is enabled
|
17
18
|
# @param message [String] the log message
|
18
19
|
# @param context [Hash] additional context data
|
19
20
|
def log_debug(message, context = {})
|
21
|
+
return unless debug_enabled?
|
22
|
+
|
20
23
|
log_with_level(:debug, message, context)
|
21
24
|
end
|
22
25
|
|
@@ -34,6 +37,16 @@ module Dbwatcher
|
|
34
37
|
log_with_level(:error, message, context)
|
35
38
|
end
|
36
39
|
|
40
|
+
# Check if debug logging is enabled
|
41
|
+
# @return [Boolean] true if debug logging is enabled
|
42
|
+
def debug_enabled?
|
43
|
+
return Dbwatcher.configuration.debug_mode if defined?(Dbwatcher.configuration) &&
|
44
|
+
Dbwatcher.configuration.respond_to?(:debug_mode)
|
45
|
+
return Rails.env.development? if defined?(Rails)
|
46
|
+
|
47
|
+
false
|
48
|
+
end
|
49
|
+
|
37
50
|
private
|
38
51
|
|
39
52
|
def log_with_level(level, message, context)
|
@@ -51,7 +64,16 @@ module Dbwatcher
|
|
51
64
|
end
|
52
65
|
|
53
66
|
def component_name
|
54
|
-
|
67
|
+
if is_a?(Module) && !is_a?(Class)
|
68
|
+
# For modules
|
69
|
+
name.to_s.split("::").last
|
70
|
+
elsif self.class.name
|
71
|
+
# For classes
|
72
|
+
self.class.name.split("::").last
|
73
|
+
else
|
74
|
+
# Fallback
|
75
|
+
"Logger"
|
76
|
+
end
|
55
77
|
end
|
56
78
|
|
57
79
|
def rails_logger
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Services
|
5
|
+
module DiagramAnalyzers
|
6
|
+
module Concerns
|
7
|
+
# Concern for ActiveRecord introspection utilities
|
8
|
+
#
|
9
|
+
# This module provides common methods for checking ActiveRecord availability
|
10
|
+
# and performing model introspection operations.
|
11
|
+
module ActiverecordIntrospection
|
12
|
+
extend ActiveSupport::Concern if defined?(ActiveSupport)
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
# Check if ActiveRecord is available
|
17
|
+
#
|
18
|
+
# @return [Boolean]
|
19
|
+
def activerecord_available?
|
20
|
+
defined?(ActiveRecord::Base)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Check if models analysis is available
|
24
|
+
#
|
25
|
+
# @return [Boolean] true if models can be analyzed
|
26
|
+
def models_available?
|
27
|
+
unless activerecord_available?
|
28
|
+
Rails.logger.warn "#{self.class.name}: ActiveRecord not available"
|
29
|
+
return false
|
30
|
+
end
|
31
|
+
|
32
|
+
true
|
33
|
+
end
|
34
|
+
|
35
|
+
# Eagerly load all models including those from gems
|
36
|
+
#
|
37
|
+
# @return [void]
|
38
|
+
def eager_load_models
|
39
|
+
return unless defined?(Rails) && Rails.respond_to?(:application)
|
40
|
+
|
41
|
+
begin
|
42
|
+
# Force eager loading of application models
|
43
|
+
Rails.application.eager_load!
|
44
|
+
|
45
|
+
# Also load models from engines/gems if any are configured
|
46
|
+
Rails::Engine.descendants.each do |engine|
|
47
|
+
engine.eager_load! if engine.respond_to?(:eager_load!)
|
48
|
+
rescue StandardError => e
|
49
|
+
error_message = "#{self.class.name}: Could not eager load engine #{engine.class.name}: #{e.message}"
|
50
|
+
Rails.logger.debug error_message
|
51
|
+
end
|
52
|
+
rescue StandardError => e
|
53
|
+
Rails.logger.debug "#{self.class.name}: Could not eager load models: #{e.message}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Services
|
5
|
+
module DiagramAnalyzers
|
6
|
+
module Concerns
|
7
|
+
# Concern for filtering associations based on scope
|
8
|
+
#
|
9
|
+
# This module provides methods for determining which associations
|
10
|
+
# should be included in the analysis based on session context.
|
11
|
+
module AssociationScopeFiltering
|
12
|
+
extend ActiveSupport::Concern if defined?(ActiveSupport)
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
# Check if target model is in analysis scope
|
17
|
+
#
|
18
|
+
# @param association [Object] association object
|
19
|
+
# @param session_tables [Array<String>] tables from session context
|
20
|
+
# @return [Boolean] true if target model should be included
|
21
|
+
def target_model_in_scope?(association, session_tables = [])
|
22
|
+
target_table = get_association_table_name(association)
|
23
|
+
|
24
|
+
# If analyzing session, both tables must be in session
|
25
|
+
# If analyzing globally, include all
|
26
|
+
return true if session_tables.empty?
|
27
|
+
|
28
|
+
# Skip if target table is not in session
|
29
|
+
return false if target_table && !session_tables.include?(target_table)
|
30
|
+
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
# Get table name for association target
|
35
|
+
#
|
36
|
+
# @param association [Object] association object
|
37
|
+
# @return [String, nil] table name
|
38
|
+
def get_association_table_name(association)
|
39
|
+
association.table_name
|
40
|
+
rescue StandardError => e
|
41
|
+
Rails.logger.warn "#{self.class.name}: Could not get table name for #{association.name}: #{e.message}"
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
|
45
|
+
# Extract tables that were involved in the session
|
46
|
+
#
|
47
|
+
# @param session [Object] session object with changes
|
48
|
+
# @return [Array<String>] unique table names
|
49
|
+
def extract_session_tables(session)
|
50
|
+
return [] unless session&.changes
|
51
|
+
|
52
|
+
session.changes.map do |change|
|
53
|
+
change[:table_name] || change["table_name"]
|
54
|
+
end.compact.uniq
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -232,6 +232,37 @@ module Dbwatcher
|
|
232
232
|
# @param primary_key [String, nil] optional primary key for testing
|
233
233
|
# @return [Boolean] true if likely self-referential
|
234
234
|
def self_referential_column?(column_name, table_name, primary_key = nil)
|
235
|
+
# Get the singular form of the table name
|
236
|
+
base_name = singularize(table_name)
|
237
|
+
|
238
|
+
# Special case for post_id in posts table - not a self-reference
|
239
|
+
return false if column_name == "#{base_name}_id" && table_name == "posts" && base_name == "post"
|
240
|
+
|
241
|
+
# Check primary key if this is a table-specific reference
|
242
|
+
if column_name == "#{base_name}_id"
|
243
|
+
if primary_key.nil?
|
244
|
+
begin
|
245
|
+
primary_key = connection.primary_key(table_name)
|
246
|
+
rescue StandardError
|
247
|
+
return false
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
return column_name != primary_key
|
252
|
+
end
|
253
|
+
|
254
|
+
# Use pattern matching to check various self-referential patterns
|
255
|
+
common_pattern?(column_name) ||
|
256
|
+
hierarchy_pattern?(column_name, base_name) ||
|
257
|
+
relationship_pattern?(column_name) ||
|
258
|
+
directional_pattern?(column_name)
|
259
|
+
end
|
260
|
+
|
261
|
+
# Check if column matches common self-referential patterns
|
262
|
+
#
|
263
|
+
# @param column_name [String] column name to check
|
264
|
+
# @return [Boolean] true if matches common patterns
|
265
|
+
def common_pattern?(column_name)
|
235
266
|
# Common self-referential patterns
|
236
267
|
self_ref_patterns = %w[
|
237
268
|
parent_id
|
@@ -257,52 +288,47 @@ module Dbwatcher
|
|
257
288
|
replied_to_id
|
258
289
|
]
|
259
290
|
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
# Get the singular form of the table name
|
264
|
-
base_name = singularize(table_name)
|
265
|
-
|
266
|
-
# Special case for post_id in posts table - not a self-reference
|
267
|
-
return false if column_name == "#{base_name}_id" && table_name == "posts" && base_name == "post"
|
268
|
-
|
269
|
-
# Check for table-specific self-references (e.g., comment_id in comments table)
|
270
|
-
if column_name == "#{base_name}_id"
|
271
|
-
# Check if this is not the primary key column
|
272
|
-
if primary_key.nil?
|
273
|
-
begin
|
274
|
-
primary_key = connection.primary_key(table_name)
|
275
|
-
rescue StandardError
|
276
|
-
return false
|
277
|
-
end
|
278
|
-
end
|
279
|
-
|
280
|
-
return column_name != primary_key
|
281
|
-
end
|
291
|
+
self_ref_patterns.include?(column_name)
|
292
|
+
end
|
282
293
|
|
283
|
-
|
294
|
+
# Check for hierarchy patterns with table name
|
295
|
+
#
|
296
|
+
# @param column_name [String] column name to check
|
297
|
+
# @param base_name [String] singular form of table name
|
298
|
+
# @return [Boolean] true if matches hierarchy patterns
|
299
|
+
def hierarchy_pattern?(column_name, base_name)
|
284
300
|
hierarchy_prefixes = %w[parent child ancestor descendant superior subordinate manager supervisor]
|
285
|
-
hierarchy_prefixes.each do |prefix|
|
286
|
-
# Check for patterns like parent_comment_id in comments table
|
287
|
-
return true if column_name.start_with?("#{prefix}_#{base_name}_id")
|
288
301
|
|
289
|
-
|
290
|
-
|
302
|
+
hierarchy_prefixes.any? do |prefix|
|
303
|
+
# Check for patterns like parent_comment_id in comments table
|
304
|
+
column_name.start_with?("#{prefix}_#{base_name}_id") ||
|
305
|
+
# Check for patterns like parent_of_id in any table
|
306
|
+
column_name.start_with?("#{prefix}_of_id")
|
291
307
|
end
|
308
|
+
end
|
292
309
|
|
293
|
-
|
310
|
+
# Check for relationship patterns
|
311
|
+
#
|
312
|
+
# @param column_name [String] column name to check
|
313
|
+
# @return [Boolean] true if matches relationship patterns
|
314
|
+
def relationship_pattern?(column_name)
|
294
315
|
relationship_patterns = %w[related linked connected associated referenced]
|
295
|
-
|
296
|
-
|
316
|
+
|
317
|
+
relationship_patterns.any? do |pattern|
|
318
|
+
column_name.start_with?("#{pattern}_")
|
297
319
|
end
|
320
|
+
end
|
298
321
|
|
299
|
-
|
322
|
+
# Check for directional patterns
|
323
|
+
#
|
324
|
+
# @param column_name [String] column name to check
|
325
|
+
# @return [Boolean] true if matches directional patterns
|
326
|
+
def directional_pattern?(column_name)
|
300
327
|
directional_patterns = %w[previous next original copy source target]
|
301
|
-
directional_patterns.each do |pattern|
|
302
|
-
return true if column_name.start_with?("#{pattern}_")
|
303
|
-
end
|
304
328
|
|
305
|
-
|
329
|
+
directional_patterns.any? do |pattern|
|
330
|
+
column_name.start_with?("#{pattern}_")
|
331
|
+
end
|
306
332
|
end
|
307
333
|
|
308
334
|
# Analyze junction tables (many-to-many relationships)
|
@@ -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
|