dbviewer 0.7.10 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +60 -0
  3. data/app/assets/images/dbviewer/emoji-favicon.txt +1 -0
  4. data/app/assets/images/dbviewer/favicon.ico +4 -0
  5. data/app/assets/images/dbviewer/favicon.png +4 -0
  6. data/app/assets/images/dbviewer/favicon.svg +10 -0
  7. data/app/assets/javascripts/dbviewer/entity_relationship_diagram.js +38 -42
  8. data/app/assets/javascripts/dbviewer/error_handler.js +58 -0
  9. data/app/assets/javascripts/dbviewer/home.js +25 -34
  10. data/app/assets/javascripts/dbviewer/layout.js +100 -129
  11. data/app/assets/javascripts/dbviewer/query.js +309 -246
  12. data/app/assets/javascripts/dbviewer/sidebar.js +170 -183
  13. data/app/assets/javascripts/dbviewer/utility.js +124 -0
  14. data/app/assets/stylesheets/dbviewer/application.css +8 -146
  15. data/app/assets/stylesheets/dbviewer/entity_relationship_diagram.css +0 -34
  16. data/app/assets/stylesheets/dbviewer/logs.css +0 -11
  17. data/app/assets/stylesheets/dbviewer/query.css +21 -9
  18. data/app/assets/stylesheets/dbviewer/table.css +49 -131
  19. data/app/controllers/concerns/dbviewer/database_operations/connection_management.rb +90 -0
  20. data/app/controllers/concerns/dbviewer/database_operations/data_export.rb +31 -0
  21. data/app/controllers/concerns/dbviewer/database_operations/database_information.rb +54 -0
  22. data/app/controllers/concerns/dbviewer/database_operations/datatable_operations.rb +37 -0
  23. data/app/controllers/concerns/dbviewer/database_operations/query_operations.rb +37 -0
  24. data/app/controllers/concerns/dbviewer/database_operations/relationship_management.rb +175 -0
  25. data/app/controllers/concerns/dbviewer/database_operations/table_operations.rb +46 -0
  26. data/app/controllers/concerns/dbviewer/database_operations.rb +4 -9
  27. data/app/controllers/dbviewer/api/tables_controller.rb +12 -0
  28. data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +0 -15
  29. data/app/controllers/dbviewer/tables_controller.rb +4 -33
  30. data/app/helpers/dbviewer/datatable_ui_table_helper.rb +10 -9
  31. data/app/helpers/dbviewer/formatting_helper.rb +6 -1
  32. data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +1 -1
  33. data/app/views/dbviewer/tables/query.html.erb +24 -8
  34. data/app/views/dbviewer/tables/show.html.erb +3 -3
  35. data/app/views/layouts/dbviewer/application.html.erb +12 -3
  36. data/config/routes.rb +2 -2
  37. data/lib/dbviewer/configuration.rb +21 -0
  38. data/lib/dbviewer/data_privacy/pii_masker.rb +125 -0
  39. data/lib/dbviewer/database/manager.rb +2 -2
  40. data/lib/dbviewer/datatable/query_operations.rb +1 -17
  41. data/lib/dbviewer/engine.rb +29 -0
  42. data/lib/dbviewer/version.rb +1 -1
  43. data/lib/dbviewer.rb +45 -0
  44. data/lib/generators/dbviewer/install_generator.rb +6 -0
  45. data/lib/generators/dbviewer/templates/pii_configuration_example.rb +99 -0
  46. metadata +17 -10
  47. data/app/controllers/concerns/dbviewer/connection_management.rb +0 -88
  48. data/app/controllers/concerns/dbviewer/data_export.rb +0 -32
  49. data/app/controllers/concerns/dbviewer/database_information.rb +0 -62
  50. data/app/controllers/concerns/dbviewer/datatable_support.rb +0 -47
  51. data/app/controllers/concerns/dbviewer/pagination_concern.rb +0 -34
  52. data/app/controllers/concerns/dbviewer/query_operations.rb +0 -28
  53. data/app/controllers/concerns/dbviewer/relationship_management.rb +0 -173
  54. data/app/controllers/concerns/dbviewer/table_operations.rb +0 -56
@@ -1,88 +0,0 @@
1
- module Dbviewer
2
- module ConnectionManagement
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- helper_method :current_connection_key, :available_connections if respond_to?(:helper_method)
7
- end
8
-
9
- # Get the current active connection key
10
- def current_connection_key
11
- key = session[:dbviewer_connection] || Dbviewer.configuration.current_connection
12
- return key.to_sym if key && Dbviewer.configuration.database_connections.key?(key.to_sym)
13
-
14
- first_key = Dbviewer.configuration.database_connections.keys.first
15
- if first_key
16
- session[:dbviewer_connection] = first_key
17
- return first_key
18
- end
19
-
20
- :default
21
- end
22
-
23
- # Set the current connection to use
24
- def switch_connection(connection_key)
25
- connection_key = connection_key.to_sym if connection_key.respond_to?(:to_sym)
26
-
27
- if connection_key && Dbviewer.configuration.database_connections.key?(connection_key)
28
- session[:dbviewer_connection] = connection_key
29
- # Clear the database manager to force it to be recreated with the new connection
30
- @database_manager = nil
31
- return true
32
- else
33
- # If the connection key doesn't exist, reset to default connection
34
- if Dbviewer.configuration.database_connections.key?(Dbviewer.configuration.current_connection)
35
- session[:dbviewer_connection] = Dbviewer.configuration.current_connection
36
- @database_manager = nil
37
- return true
38
- else
39
- # If even the default connection isn't valid, try the first available connection
40
- first_key = Dbviewer.configuration.database_connections.keys.first
41
- if first_key
42
- session[:dbviewer_connection] = first_key
43
- @database_manager = nil
44
- return true
45
- end
46
- end
47
- end
48
-
49
- false # Return false if we couldn't set a valid connection
50
- end
51
-
52
- # Get list of available connections
53
- def available_connections
54
- connections = Dbviewer.configuration.database_connections.map do |key, config|
55
- # Try to determine the adapter name if it's not already stored
56
- adapter_name = nil
57
- if config[:adapter_name].present?
58
- adapter_name = config[:adapter_name]
59
- elsif config[:connection].present?
60
- begin
61
- adapter_name = config[:connection].connection.adapter_name
62
- rescue => e
63
- Rails.logger.error("Error getting adapter name: #{e.message}")
64
- end
65
- end
66
-
67
- {
68
- key: key,
69
- name: config[:name] || key.to_s.humanize,
70
- adapter_name: adapter_name,
71
- current: key.to_sym == current_connection_key.to_sym
72
- }
73
- end
74
-
75
- # Ensure at least one connection is marked as current
76
- unless connections.any? { |c| c[:current] }
77
- # If no connection is current, mark the first one as current
78
- if connections.any?
79
- connections.first[:current] = true
80
- # Also update the session
81
- session[:dbviewer_connection] = connections.first[:key]
82
- end
83
- end
84
-
85
- connections
86
- end
87
- end
88
- end
@@ -1,32 +0,0 @@
1
- require "csv"
2
-
3
- module Dbviewer
4
- module DataExport
5
- extend ActiveSupport::Concern
6
-
7
- # Export table data to CSV
8
- def export_table_to_csv(table_name, query_params = nil, include_headers = true)
9
- records = database_manager.table_query_operations.table_records(table_name, query_params)
10
-
11
- csv_data = CSV.generate do |csv|
12
- # Add headers if requested
13
- csv << records.columns if include_headers
14
-
15
- # Add rows
16
- records.rows.each do |row|
17
- csv << row.map { |cell| format_csv_value(cell) }
18
- end
19
- end
20
-
21
- csv_data
22
- end
23
-
24
- private
25
-
26
- # Format cell values for CSV export to handle nil values and special characters
27
- def format_csv_value(value)
28
- return "" if value.nil?
29
- value.to_s
30
- end
31
- end
32
- end
@@ -1,62 +0,0 @@
1
- module Dbviewer
2
- module DatabaseInformation
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- helper_method :get_database_name, :get_adapter_name if respond_to?(:helper_method)
7
- end
8
-
9
- # Get the name of the current database
10
- def get_database_name
11
- # First check if this connection has a name in the configuration
12
- current_conn_config = Dbviewer.configuration.database_connections[current_connection_key]
13
- return current_conn_config[:name] if current_conn_config && current_conn_config[:name].present?
14
-
15
- adapter = database_manager.connection.adapter_name.downcase
16
-
17
- case adapter
18
- when /mysql/
19
- query = "SELECT DATABASE() as db_name"
20
- result = database_manager.execute_query(query).first
21
- result ? result["db_name"] : "Database"
22
- when /postgres/
23
- query = "SELECT current_database() as db_name"
24
- result = database_manager.execute_query(query).first
25
- result ? result["db_name"] : "Database"
26
- when /sqlite/
27
- # For SQLite, extract the database name from the connection_config
28
- database_path = database_manager.connection.pool.spec.config[:database] || ""
29
- File.basename(database_path, ".*") || "SQLite Database"
30
- else
31
- "Database" # Default fallback
32
- end
33
- end
34
-
35
- # Get the name of the current database adapter
36
- def get_adapter_name
37
- adapter_name = database_manager.connection.adapter_name.downcase
38
- adapter_mappings = {
39
- /mysql/i => "MySQL",
40
- /postgres/i => "PostgreSQL",
41
- /sqlite/i => "SQLite",
42
- /oracle/i => "Oracle",
43
- /sqlserver|mssql/i => "SQL Server"
44
- }
45
- adapter_mappings.find { |pattern, _| adapter_name =~ pattern }&.last || adapter_name.titleize
46
- rescue
47
- "Unknown"
48
- end
49
-
50
- # Gather database analytics information
51
- def fetch_database_analytics
52
- tables = fetch_tables(include_record_counts: true)
53
-
54
- {
55
- total_tables: tables.size,
56
- total_records: tables.sum { |t| t[:record_count] },
57
- largest_tables: tables.sort_by { |t| -t[:record_count] }.first(10),
58
- empty_tables: tables.select { |t| t[:record_count] == 0 }
59
- }
60
- end
61
- end
62
- end
@@ -1,47 +0,0 @@
1
- module Dbviewer
2
- module DatatableSupport
3
- extend ActiveSupport::Concern
4
-
5
- # Consolidated method to fetch all datatable-related data in one call
6
- # Returns a hash containing all necessary datatable information
7
- def fetch_datatable_data(table_name, query_params)
8
- columns = fetch_table_columns(table_name)
9
- return default_datatable_structure(table_name) if columns.empty?
10
-
11
- if query_params.column_filters.empty?
12
- total_count = fetch_table_record_count(table_name)
13
- else
14
- total_count = fetch_filtered_record_count(table_name, query_params.column_filters)
15
- end
16
-
17
- {
18
- columns: columns,
19
- records: fetch_table_records(table_name, query_params),
20
- total_count: total_count,
21
- total_pages: total_count > 0 ? (total_count.to_f / query_params.per_page).ceil : 0,
22
- metadata: fetch_table_metadata(table_name),
23
- current_page: query_params.page,
24
- per_page: query_params.per_page,
25
- order_by: query_params.order_by,
26
- direction: query_params.direction
27
- }
28
- end
29
-
30
- private
31
-
32
- # Default structure for tables with no data/columns
33
- def default_datatable_structure(table_name)
34
- {
35
- columns: [],
36
- records: [],
37
- total_count: 0,
38
- total_pages: 0,
39
- metadata: {},
40
- current_page: 1,
41
- per_page: 25,
42
- order_by: nil,
43
- direction: "ASC"
44
- }
45
- end
46
- end
47
- end
@@ -1,34 +0,0 @@
1
- module Dbviewer
2
- module PaginationConcern
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- # Sort direction validation
7
- const_set(:VALID_SORT_DIRECTIONS, %w[ASC DESC].freeze) unless const_defined?(:VALID_SORT_DIRECTIONS)
8
- end
9
-
10
- module ClassMethods
11
- # Use configuration values from Dbviewer module
12
- def per_page_options
13
- Dbviewer.configuration.per_page_options
14
- end
15
-
16
- def default_per_page
17
- Dbviewer.configuration.default_per_page
18
- end
19
- end
20
-
21
- def fetch_total_count(table_name, query_params)
22
- if query_params.column_filters.present? && query_params.column_filters.values.any?(&:present?)
23
- fetch_filtered_record_count(table_name, query_params.column_filters)
24
- else
25
- fetch_table_record_count(table_name)
26
- end
27
- end
28
-
29
- # Calculate the total number of pages
30
- def calculate_total_pages(total_count, per_page)
31
- (total_count.to_f / per_page).ceil
32
- end
33
- end
34
- end
@@ -1,28 +0,0 @@
1
- module Dbviewer
2
- module QueryOperations
3
- extend ActiveSupport::Concern
4
-
5
- # Prepare the SQL query - either from params or default
6
- def prepare_query(table_name, query)
7
- query = query.present? ? query.to_s : default_query(table_name)
8
-
9
- # Validate query for security
10
- unless ::Dbviewer::Validator::Sql.safe_query?(query)
11
- query = default_query(table_name)
12
- flash.now[:warning] = "Only SELECT queries are allowed. Your query contained potentially unsafe operations. Using default query instead."
13
- end
14
-
15
- query
16
- end
17
-
18
- # Execute the prepared SQL query
19
- def execute_query(query)
20
- database_manager.execute_query(@query)
21
- end
22
-
23
- def default_query(table_name)
24
- quoted_table = safe_quote_table_name(table_name)
25
- "SELECT * FROM #{quoted_table} LIMIT 100"
26
- end
27
- end
28
- end
@@ -1,173 +0,0 @@
1
- module Dbviewer
2
- module RelationshipManagement
3
- extend ActiveSupport::Concern
4
-
5
- # Fetch relationships between tables for ERD visualization
6
- def fetch_table_relationships(tables)
7
- tables.flat_map { |table| extract_table_relationships_from_metadata(table[:name]) }
8
- end
9
-
10
- # Get mini ERD data for a specific table and its relationships
11
- def fetch_mini_erd_for_table(table_name)
12
- outgoing_data = collect_outgoing_relationships(table_name)
13
- incoming_data = collect_incoming_relationships(table_name)
14
-
15
- initial_tables = [ { name: table_name } ]
16
- all_relationships = outgoing_data[:relationships] + incoming_data[:relationships]
17
- all_tables = (initial_tables + outgoing_data[:tables] + incoming_data[:tables]).uniq { |t| t[:name] }
18
-
19
- {
20
- tables: all_tables,
21
- relationships: all_relationships,
22
- timestamp: Time.now.to_i
23
- }
24
- end
25
-
26
- private
27
-
28
- # Extract relationships for a single table from its metadata
29
- # @param table_name [String] The name of the table to process
30
- # @return [Array<Hash>] Array of relationship hashes for this table
31
- def extract_table_relationships_from_metadata(table_name)
32
- metadata = database_manager.table_metadata(table_name)
33
- return [] unless metadata&.dig(:foreign_keys)&.present?
34
-
35
- metadata[:foreign_keys].map do |fk|
36
- {
37
- from_table: table_name,
38
- to_table: fk[:to_table],
39
- from_column: fk[:column],
40
- to_column: fk[:primary_key],
41
- name: fk[:name]
42
- }
43
- end
44
- end
45
-
46
- # Collect outgoing relationships from the specified table to other tables
47
- # @param table_name [String] The source table name
48
- # @return [Hash] Hash containing :tables and :relationships arrays
49
- def collect_outgoing_relationships(table_name)
50
- tables = []
51
- relationships = []
52
-
53
- metadata = fetch_table_metadata(table_name)
54
- return { tables: tables, relationships: relationships } unless metadata&.dig(:foreign_keys)&.present?
55
-
56
- metadata[:foreign_keys].each do |fk|
57
- result = process_outgoing_foreign_key(table_name, fk)
58
- if result
59
- relationships << result[:relationship]
60
- tables << result[:table]
61
- end
62
- end
63
-
64
- {
65
- tables: tables,
66
- relationships: relationships
67
- }
68
- end
69
-
70
- # Process a single outgoing foreign key relationship
71
- # @param table_name [String] The source table name
72
- # @param fk [Hash] Foreign key metadata
73
- # @return [Hash, nil] Hash containing :relationship and :table, or nil if invalid
74
- def process_outgoing_foreign_key(table_name, fk)
75
- return nil unless fk[:to_table].present? && fk[:column].present?
76
-
77
- relationship = build_relationship_hash(
78
- from_table: table_name.to_s,
79
- to_table: fk[:to_table].to_s,
80
- from_column: fk[:column].to_s,
81
- to_column: fk[:primary_key].to_s.presence || "id",
82
- name: fk[:name].to_s.presence || "#{table_name}_to_#{fk[:to_table]}",
83
- direction: "outgoing"
84
- )
85
-
86
- {
87
- relationship: relationship,
88
- table: {
89
- name: fk[:to_table].to_s
90
- }
91
- }
92
- end
93
-
94
- # Collect incoming relationships from other tables to the specified table
95
- # @param table_name [String] The target table name
96
- # @return [Hash] Hash containing :tables and :relationships arrays
97
- def collect_incoming_relationships(table_name)
98
- results = database_manager.tables
99
- .reject { |other_table_name| other_table_name == table_name }
100
- .map { |other_table_name| process_table_for_incoming_relationships(table_name, other_table_name) }
101
- .compact
102
-
103
- {
104
- tables: results.flat_map { |result| result[:tables] },
105
- relationships: results.flat_map { |result| result[:relationships] }
106
- }
107
- end
108
-
109
- # Process a single table to find incoming relationships to the target table
110
- # @param target_table [String] The target table name
111
- # @param source_table [String] The source table name to check
112
- # @return [Hash, nil] Hash containing :tables and :relationships arrays, or nil if no relationships
113
- def process_table_for_incoming_relationships(target_table, source_table)
114
- other_metadata = fetch_table_metadata(source_table)
115
- return nil unless other_metadata&.dig(:foreign_keys)&.present?
116
-
117
- results = other_metadata[:foreign_keys]
118
- .map { |fk| process_incoming_foreign_key(target_table, source_table, fk) }
119
- .compact
120
-
121
- return nil if results.empty?
122
-
123
- {
124
- tables: results.map { |result| result[:table] },
125
- relationships: results.map { |result| result[:relationship] }
126
- }
127
- end
128
-
129
- # Process a single incoming foreign key relationship
130
- # @param target_table [String] The target table name
131
- # @param source_table [String] The source table name
132
- # @param fk [Hash] Foreign key metadata
133
- # @return [Hash, nil] Hash containing :relationship and :table, or nil if invalid
134
- def process_incoming_foreign_key(target_table, source_table, fk)
135
- return nil unless fk[:to_table] == target_table && fk[:column].present?
136
-
137
- relationship = build_relationship_hash(
138
- from_table: source_table.to_s,
139
- to_table: target_table.to_s,
140
- from_column: fk[:column].to_s,
141
- to_column: fk[:primary_key].to_s.presence || "id",
142
- name: fk[:name].to_s.presence || "#{source_table}_to_#{target_table}",
143
- direction: "incoming"
144
- )
145
-
146
- {
147
- relationship: relationship,
148
- table: {
149
- name: source_table.to_s
150
- }
151
- }
152
- end
153
-
154
- # Build a standardized relationship hash
155
- # @param from_table [String] Source table name
156
- # @param to_table [String] Target table name
157
- # @param from_column [String] Source column name
158
- # @param to_column [String] Target column name
159
- # @param name [String] Relationship name
160
- # @param direction [String] Relationship direction
161
- # @return [Hash] Standardized relationship hash
162
- def build_relationship_hash(from_table:, to_table:, from_column:, to_column:, name:, direction:)
163
- {
164
- from_table: from_table,
165
- to_table: to_table,
166
- from_column: from_column,
167
- to_column: to_column,
168
- name: name,
169
- direction: direction
170
- }
171
- end
172
- end
173
- end
@@ -1,56 +0,0 @@
1
- module Dbviewer
2
- module TableOperations
3
- extend ActiveSupport::Concern
4
-
5
- # Fetch all tables with their stats
6
- # By default, don't include record counts for better performance on sidebar
7
- def fetch_tables(include_record_counts = false)
8
- database_manager.tables.map do |table_name|
9
- table_stats = {
10
- name: table_name
11
- }
12
- table_stats[:record_count] = database_manager.record_count(table_name) if include_record_counts
13
- table_stats
14
- end
15
- end
16
-
17
- # Get column information for a specific table
18
- def fetch_table_columns(table_name)
19
- database_manager.table_columns(table_name)
20
- end
21
-
22
- # Get the total number of records in a table
23
- def fetch_table_record_count(table_name)
24
- database_manager.table_count(table_name)
25
- end
26
-
27
- # Fetch records for a table with pagination and sorting
28
- def fetch_table_records(table_name, query_params)
29
- database_manager.table_records(table_name, query_params)
30
- end
31
-
32
- # Get filtered record count for a table
33
- def fetch_filtered_record_count(table_name, column_filters)
34
- database_manager.filtered_record_count(table_name, column_filters)
35
- end
36
-
37
- # Safely quote a table name, with fallback
38
- def safe_quote_table_name(table_name)
39
- database_manager.connection.quote_table_name(table_name) rescue table_name.to_s
40
- end
41
-
42
- # Get table metadata for display (e.g., primary key, foreign keys, indexes)
43
- def fetch_table_metadata(table_name)
44
- database_manager.table_metadata(table_name)
45
- end
46
-
47
- private
48
-
49
- # Check if a table has a created_at column for timestamp visualization
50
- def has_timestamp_column?(table_name)
51
- fetch_table_columns(table_name).any? do |column|
52
- column[:name] == "created_at" && [ :datetime, :timestamp ].include?(column[:type])
53
- end
54
- end
55
- end
56
- end