dbviewer 0.6.2 → 0.6.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7607c650d549f8b7b0e7fb6ec8aa4c710028a7f9ca40f1524b41594c4e22dc68
4
- data.tar.gz: 00c9b8b1063ecd3584b5d53d0471b87130a63900760cb089b8b5537b4cea80b0
3
+ metadata.gz: b378480a03f4c61f60ee8e6b918b81bfabb09fcb3f7256010d6ee7d59795feb3
4
+ data.tar.gz: 0ed1b725b27aa7c1290f767ce900ee8d5be1e2dd54df5edcaecef71f8945658f
5
5
  SHA512:
6
- metadata.gz: 2d07a11c5481917f3b80c893ac7b0e3a4ceefa5e502ccd843d08881ab1d0cdd62989e8a6a7d2eda9e225a27e87187511c698dde1cfbddca33215d3dd2560721a
7
- data.tar.gz: 84830ff63c74c5792adff5d3d0656c45a4c0f11805fb4b21899b979812530223f29f3045d8be359f355846190b2933fea845eb59a308a3ddeac500e7e5ce7288
6
+ metadata.gz: d7e70b3cb0c7ad44b8d5dee1fc476d85738a3169b1aaccac9e9aea242f0e1cf2801836341f4c391f9dd7d184985606376ca09366dada9944cd8058111a407105
7
+ data.tar.gz: 9914b89d8c4678cf949edad75d713d74cdc9aa9ece9f67cabe967afd42d411730d8d34839ee9c006393ec0f35bc2a3cfe88d8195b49caaec99d6aab09f5c9d03
data/README.md CHANGED
@@ -373,7 +373,7 @@ graph TB
373
373
  subgraph "DBViewer Engine"
374
374
  Engine[Engine<br/>Rails::Engine]
375
375
  Config[Configuration<br/>Settings & Defaults]
376
- SqlValidator[SqlValidator<br/>Query Validation]
376
+ Validator::Sql[Validator::Sql<br/>Query Validation]
377
377
  end
378
378
 
379
379
  subgraph "Controllers Layer"
@@ -239,31 +239,33 @@ module Dbviewer
239
239
 
240
240
  # Fetch relationships between tables for ERD visualization
241
241
  def fetch_table_relationships
242
- relationships = []
243
-
244
- @tables.each do |table|
245
- table_name = table[:name]
246
-
247
- # Get foreign keys defined in this table pointing to others
248
- begin
249
- metadata = database_manager.table_metadata(table_name)
250
- if metadata && metadata[:foreign_keys].present?
251
- metadata[:foreign_keys].each do |fk|
252
- relationships << {
253
- from_table: table_name,
254
- to_table: fk[:to_table],
255
- from_column: fk[:column],
256
- to_column: fk[:primary_key],
257
- name: fk[:name]
258
- }
259
- end
260
- end
261
- rescue => e
262
- Rails.logger.error("Error fetching relationships for #{table_name}: #{e.message}")
263
- end
242
+ # Use functional approach: flat_map to extract all relationships from all tables
243
+ @tables.flat_map do |table|
244
+ extract_table_relationships_from_metadata(table[:name])
264
245
  end
246
+ end
265
247
 
266
- relationships
248
+ private
249
+
250
+ # Extract relationships for a single table from its metadata
251
+ # @param table_name [String] The name of the table to process
252
+ # @return [Array<Hash>] Array of relationship hashes for this table
253
+ def extract_table_relationships_from_metadata(table_name)
254
+ metadata = database_manager.table_metadata(table_name)
255
+ return [] unless metadata&.dig(:foreign_keys)&.present?
256
+
257
+ metadata[:foreign_keys].map do |fk|
258
+ {
259
+ from_table: table_name,
260
+ to_table: fk[:to_table],
261
+ from_column: fk[:column],
262
+ to_column: fk[:primary_key],
263
+ name: fk[:name]
264
+ }
265
+ end
266
+ rescue => e
267
+ Rails.logger.error("Error fetching relationships for #{table_name}: #{e.message}")
268
+ [] # Return empty array to continue processing other tables
267
269
  end
268
270
 
269
271
  # Get mini ERD data for a specific table and its relationships
@@ -399,7 +401,7 @@ module Dbviewer
399
401
  @query = params[:query].present? ? params[:query].to_s : default_query
400
402
 
401
403
  # Validate query for security
402
- unless ::Dbviewer::SqlValidator.safe_query?(@query)
404
+ unless ::Dbviewer::Validator::Sql.safe_query?(@query)
403
405
  @query = default_query
404
406
  flash.now[:warning] = "Only SELECT queries are allowed. Your query contained potentially unsafe operations. Using default query instead."
405
407
  end
@@ -439,8 +441,71 @@ module Dbviewer
439
441
  csv_data
440
442
  end
441
443
 
444
+ # Consolidated method to fetch all datatable-related data in one call
445
+ # Returns a hash containing all necessary datatable information
446
+ def fetch_datatable_data(table_name, query_params)
447
+ # Fetch all required data using functional programming patterns
448
+ columns = fetch_table_columns(table_name)
449
+
450
+ # Handle case where table has no columns (should rarely happen)
451
+ return default_datatable_structure(table_name) if columns.empty?
452
+
453
+ # Fetch records with error handling for null cases
454
+ records = begin
455
+ fetch_table_records(table_name, query_params)
456
+ rescue => e
457
+ Rails.logger.error("Error fetching table records for #{table_name}: #{e.message}")
458
+ nil
459
+ end
460
+
461
+ # Calculate total count - use filtered count if filters are present
462
+ total_count = begin
463
+ if query_params.column_filters.empty?
464
+ fetch_table_record_count(table_name)
465
+ else
466
+ fetch_filtered_record_count(table_name, query_params.column_filters)
467
+ end
468
+ rescue => e
469
+ Rails.logger.error("Error fetching record count for #{table_name}: #{e.message}")
470
+ 0
471
+ end
472
+
473
+ # Calculate pagination data functionally with safety check
474
+ total_pages = total_count > 0 ? (total_count.to_f / query_params.per_page).ceil : 0
475
+
476
+ # Get metadata with error handling
477
+ metadata = fetch_table_metadata(table_name)
478
+
479
+ {
480
+ columns: columns,
481
+ records: records,
482
+ total_count: total_count,
483
+ total_pages: total_pages,
484
+ metadata: metadata,
485
+ current_page: query_params.page,
486
+ per_page: query_params.per_page,
487
+ order_by: query_params.order_by,
488
+ direction: query_params.direction
489
+ }
490
+ end
491
+
442
492
  private
443
493
 
494
+ # Default structure for tables with no data/columns
495
+ def default_datatable_structure(table_name)
496
+ {
497
+ columns: [],
498
+ records: [],
499
+ total_count: 0,
500
+ total_pages: 0,
501
+ metadata: {},
502
+ current_page: 1,
503
+ per_page: 25,
504
+ order_by: nil,
505
+ direction: "ASC"
506
+ }
507
+ end
508
+
444
509
  # Format cell values for CSV export to handle nil values and special characters
445
510
  def format_csv_value(value)
446
511
  return "" if value.nil?
@@ -452,81 +517,5 @@ module Dbviewer
452
517
  columns = fetch_table_columns(table_name)
453
518
  columns.any? { |col| col[:name] == "created_at" && [ :datetime, :timestamp ].include?(col[:type]) }
454
519
  end
455
-
456
- # Fetch timestamp data for visualization (hourly, daily, weekly)
457
- def fetch_timestamp_data(table_name, grouping = "daily")
458
- return nil unless has_timestamp_column?(table_name)
459
-
460
- quoted_table = safe_quote_table_name(table_name)
461
- adapter = database_manager.connection.adapter_name.downcase
462
-
463
- sql_query = case grouping
464
- when "hourly"
465
- case adapter
466
- when /mysql/
467
- "SELECT DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00') as time_group, COUNT(*) as count FROM #{quoted_table} GROUP BY time_group ORDER BY time_group DESC LIMIT 48"
468
- when /postgres/
469
- "SELECT date_trunc('hour', created_at) as time_group, COUNT(*) as count FROM #{quoted_table} GROUP BY time_group ORDER BY time_group DESC LIMIT 48"
470
- else # SQLite and others
471
- "SELECT strftime('%Y-%m-%d %H:00:00', created_at) as time_group, COUNT(*) as count FROM #{quoted_table} GROUP BY time_group ORDER BY time_group DESC LIMIT 48"
472
- end
473
- when "daily"
474
- case adapter
475
- when /mysql/
476
- "SELECT DATE_FORMAT(created_at, '%Y-%m-%d') as time_group, COUNT(*) as count FROM #{quoted_table} GROUP BY time_group ORDER BY time_group DESC LIMIT 30"
477
- when /postgres/
478
- "SELECT date_trunc('day', created_at) as time_group, COUNT(*) as count FROM #{quoted_table} GROUP BY time_group ORDER BY time_group DESC LIMIT 30"
479
- else # SQLite and others
480
- "SELECT strftime('%Y-%m-%d', created_at) as time_group, COUNT(*) as count FROM #{quoted_table} GROUP BY time_group ORDER BY time_group DESC LIMIT 30"
481
- end
482
- when "weekly"
483
- case adapter
484
- when /mysql/
485
- "SELECT DATE_FORMAT(created_at, '%Y-%u') as time_group, YEARWEEK(created_at) as sort_key, COUNT(*) as count FROM #{quoted_table} GROUP BY time_group ORDER BY sort_key DESC LIMIT 26"
486
- when /postgres/
487
- "SELECT date_trunc('week', created_at) as time_group, COUNT(*) as count FROM #{quoted_table} GROUP BY time_group ORDER BY time_group DESC LIMIT 26"
488
- else # SQLite and others
489
- "SELECT strftime('%Y-%W', created_at) as time_group, COUNT(*) as count FROM #{quoted_table} GROUP BY time_group ORDER BY time_group DESC LIMIT 26"
490
- end
491
- else
492
- return nil
493
- end
494
-
495
- begin
496
- result = database_manager.execute_query(sql_query)
497
-
498
- # Format the data for the chart
499
- result.map do |row|
500
- time_str = row["time_group"].to_s
501
- count = row["count"].to_i
502
-
503
- # Format the label based on grouping type
504
- label = case grouping
505
- when "hourly"
506
- # For hourly, show "May 10, 2PM"
507
- time = time_str.is_a?(Time) ? time_str : Time.parse(time_str)
508
- time.strftime("%b %d, %l%p")
509
- when "daily"
510
- # For daily, show "May 10"
511
- time = time_str.is_a?(Time) ? time_str : (time_str.include?("-") ? Time.parse(time_str) : Time.now)
512
- time.strftime("%b %d")
513
- when "weekly"
514
- # For weekly, show "Week 19" or the week's start date
515
- if time_str.include?("-")
516
- week_num = time_str.split("-").last.to_i
517
- "Week #{week_num}"
518
- else
519
- time = time_str.is_a?(Time) ? time_str : Time.parse(time_str)
520
- "Week #{time.strftime('%W')}"
521
- end
522
- end
523
-
524
- { label: label, value: count, raw_date: time_str }
525
- end
526
- rescue => e
527
- Rails.logger.error("[DBViewer] Error fetching timestamp data: #{e.message}")
528
- nil
529
- end
530
- end
531
520
  end
532
521
  end
@@ -13,29 +13,10 @@ module Dbviewer
13
13
 
14
14
  def table_relationships
15
15
  table_names = params[:tables]&.split(",") || []
16
- relationships = []
17
-
18
- table_names.each do |table_name|
19
- next unless @tables.any? { |t| t[:name] == table_name }
20
-
21
- begin
22
- metadata = fetch_table_metadata(table_name)
23
- if metadata && metadata[:foreign_keys].present?
24
- metadata[:foreign_keys].each do |fk|
25
- relationships << {
26
- from_table: table_name,
27
- to_table: fk[:to_table],
28
- from_column: fk[:column],
29
- to_column: fk[:primary_key],
30
- name: fk[:name]
31
- }
32
- end
33
- end
34
- rescue => e
35
- Rails.logger.error("[DBViewer] Error fetching relationships for #{table_name}: #{e.message}")
36
- # Continue with other tables even if one fails
37
- end
38
- end
16
+
17
+ relationships = table_names
18
+ .filter { |table_name| @tables.any? { |t| t[:name] == table_name } }
19
+ .flat_map { |table_name| extract_table_relationships(table_name) }
39
20
 
40
21
  render_success({
41
22
  relationships: relationships,
@@ -49,6 +30,27 @@ module Dbviewer
49
30
  def set_tables
50
31
  @tables = fetch_tables
51
32
  end
33
+
34
+ # Extract relationships for a single table, handling errors gracefully
35
+ # @param table_name [String] The name of the table to process
36
+ # @return [Array<Hash>] Array of relationship hashes for this table
37
+ def extract_table_relationships(table_name)
38
+ metadata = fetch_table_metadata(table_name)
39
+ return [] unless metadata&.dig(:foreign_keys)&.present?
40
+
41
+ metadata[:foreign_keys].map do |fk|
42
+ {
43
+ from_table: table_name,
44
+ to_table: fk[:to_table],
45
+ from_column: fk[:column],
46
+ to_column: fk[:primary_key],
47
+ name: fk[:name]
48
+ }
49
+ end
50
+ rescue => e
51
+ Rails.logger.error("[DBViewer] Error fetching relationships for #{table_name}: #{e.message}")
52
+ [] # Return empty array to continue processing other tables
53
+ end
52
54
  end
53
55
  end
54
56
  end
@@ -18,16 +18,16 @@ module Dbviewer
18
18
  direction: @order_direction,
19
19
  column_filters: @column_filters.reject { |_, v| v.blank? }
20
20
  )
21
- @total_count = fetch_total_count(@table_name, query_params)
22
- @records = fetch_table_records(@table_name, query_params)
23
- @total_pages = calculate_total_pages(@total_count, @per_page)
24
- @columns = fetch_table_columns(@table_name)
25
- @metadata = fetch_table_metadata(@table_name)
26
21
 
27
- if @records.nil?
28
- column_names = @columns.map { |column| column[:name] }
29
- @records = ActiveRecord::Result.new(column_names, [])
30
- end
22
+ # Get all datatable data in one method call
23
+ datatable_data = fetch_datatable_data(@table_name, query_params)
24
+
25
+ # Assign to instance variables for view access
26
+ @total_count = datatable_data[:total_count]
27
+ @records = datatable_data[:records]
28
+ @total_pages = datatable_data[:total_pages]
29
+ @columns = datatable_data[:columns]
30
+ @metadata = datatable_data[:metadata]
31
31
 
32
32
  respond_to do |format|
33
33
  format.html # Default HTML response
@@ -505,19 +505,39 @@ module Dbviewer
505
505
  end
506
506
 
507
507
  content_tag(:td, class: "text-center action-column") do
508
- button_tag(
509
- type: "button",
510
- class: "btn btn-sm btn-primary view-record-btn",
511
- title: "View Record Details",
512
- data: {
513
- bs_toggle: "modal",
514
- bs_target: "#recordDetailModal",
515
- record_data: data_attributes.to_json,
516
- foreign_keys: metadata && metadata[:foreign_keys] ? metadata[:foreign_keys].to_json : "[]",
517
- reverse_foreign_keys: metadata && metadata[:reverse_foreign_keys] ? metadata[:reverse_foreign_keys].to_json : "[]"
518
- }
519
- ) do
520
- content_tag(:i, "", class: "bi bi-eye")
508
+ content_tag(:div, class: "d-flex gap-1 justify-content-center") do
509
+ # View Record button (existing)
510
+ view_button = button_tag(
511
+ type: "button",
512
+ class: "btn btn-sm btn-primary view-record-btn",
513
+ title: "View Record Details",
514
+ data: {
515
+ bs_toggle: "modal",
516
+ bs_target: "#recordDetailModal",
517
+ record_data: data_attributes.to_json,
518
+ foreign_keys: metadata && metadata[:foreign_keys] ? metadata[:foreign_keys].to_json : "[]",
519
+ reverse_foreign_keys: metadata && metadata[:reverse_foreign_keys] ? metadata[:reverse_foreign_keys].to_json : "[]"
520
+ }
521
+ ) do
522
+ content_tag(:i, "", class: "bi bi-eye")
523
+ end
524
+
525
+ # Copy FactoryBot button (new)
526
+ copy_factory_button = button_tag(
527
+ type: "button",
528
+ class: "btn btn-sm btn-outline-secondary copy-factory-btn",
529
+ title: "Copy to JSON",
530
+ data: {
531
+ record_data: data_attributes.to_json,
532
+ table_name: @table_name
533
+ },
534
+ onclick: "copyToJson(this)"
535
+ ) do
536
+ content_tag(:i, "", class: "bi bi-clipboard")
537
+ end
538
+
539
+ # Concatenate both buttons
540
+ view_button + copy_factory_button
521
541
  end
522
542
  end
523
543
  end