dbviewer 0.7.5 → 0.7.7

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: 27da0a6d3d08f2a6d1a597c1f7e688a54ffec7048e135b30a605ee3ace20370e
4
- data.tar.gz: 2e59f9c1c69b2bb87c7d9a351a045cfaea9709dc49ce7651d8ef24821db43aa7
3
+ metadata.gz: 3b8c2130ace09b45819bd1fa2d739fecb8a32a75a1348e7c094cb8ce0d66bd71
4
+ data.tar.gz: 65ff2d9bf93a401c7b193dca798e8afdfe4bb535d7b0c4ee5ce7729312792d49
5
5
  SHA512:
6
- metadata.gz: 48a192e9bdd118358734a2b5008a87e7219fb224b95c897601b5e448c513027b4d14b816d9abcef3950540cb99cb97af991a3f58325359592179e8dc9e2d0752
7
- data.tar.gz: f3dacb78ba4309d724c7c469e4ff00457921e97d567f808a14cfe48efb38d45fadee7303f31c8f6e326037f8de27783302561acd0b84e4d42487bada7da8fed8
6
+ metadata.gz: fdc6983afc3916e25335e0ce9cb33d9525b16dc7c468d24d4a22794c8fb3509d77527a7bd64d48d2e5fd2637eed1b5cea0a3a92d3011a3486b2869d93ca363f9
7
+ data.tar.gz: e3a401344d1fcf3fc2d2ecedc0ac283295ef6c5c364fe396095b53d284af42430ab3f49dda08b6d2c78e0b6a0cc808977b558a06b0aa00287df52ec5b4bb2125
data/README.md CHANGED
@@ -116,6 +116,9 @@ Dbviewer.configure do |config|
116
116
 
117
117
  # Authentication options
118
118
  # config.admin_credentials = { username: "admin", password: "your_secure_password" } # Basic HTTP auth credentials
119
+
120
+ # Disable DBViewer completely
121
+ # config.disabled = Rails.env.production? # Disable in production
119
122
  end
120
123
  ```
121
124
 
@@ -123,6 +126,34 @@ You can also create this file manually if you prefer.
123
126
 
124
127
  The configuration is accessed through `Dbviewer.configuration` throughout the codebase. You can also access it via `Dbviewer.config` which is an alias for backward compatibility.
125
128
 
129
+ ### Disabling DBViewer Completely
130
+
131
+ You can completely disable DBViewer access by setting the `disabled` configuration option to `true`. When disabled, all DBViewer routes will return 404 (Not Found) responses:
132
+
133
+ ```ruby
134
+ # config/initializers/dbviewer.rb
135
+ Dbviewer.configure do |config|
136
+ # Completely disable DBViewer in production
137
+ config.disabled = Rails.env.production?
138
+
139
+ # Or disable unconditionally
140
+ # config.disabled = true
141
+ end
142
+ ```
143
+
144
+ This is useful for:
145
+
146
+ - **Production environments** where you want to completely disable access to database viewing tools
147
+ - **Security compliance** where database admin tools must be disabled in certain environments
148
+ - **Performance** where you want to eliminate any potential overhead from DBViewer routes
149
+
150
+ When disabled:
151
+
152
+ - All DBViewer routes return 404 (Not Found) responses
153
+ - No database connections are validated
154
+ - No DBViewer middleware or concerns are executed
155
+ - The application behaves as if DBViewer was never mounted
156
+
126
157
  ### Multiple Database Connections
127
158
 
128
159
  DBViewer supports working with multiple database connections in your application. This is useful for applications that connect to multiple databases or use different connection pools.
@@ -149,7 +180,7 @@ Dbviewer.configure do |config|
149
180
  end
150
181
  ```
151
182
 
152
- Each connection needs to reference an ActiveRecord class that establishes a database connection. For more details, see [Multiple Database Connections](docs/multiple_connections.md).
183
+ Each connection needs to reference an ActiveRecord class that establishes a database connection.
153
184
 
154
185
  ## 🪵 Query Logging (Development Only)
155
186
 
@@ -195,6 +226,7 @@ DBViewer includes several security features to protect your database:
195
226
  - **Pattern Detection**: Detection of SQL injection patterns and suspicious constructs
196
227
  - **Error Handling**: Informative error messages without exposing sensitive information
197
228
  - **HTTP Basic Authentication**: Protect access with username and password authentication
229
+ - **Complete Disabling**: Completely disable DBViewer in production or sensitive environments
198
230
 
199
231
  ### Basic Authentication
200
232
 
@@ -212,6 +244,19 @@ end
212
244
  When credentials are provided, all DBViewer routes will be protected by HTTP Basic Authentication.
213
245
  Without valid credentials, users will be prompted for a username and password before they can access any DBViewer page.
214
246
 
247
+ ### Complete Disabling
248
+
249
+ For maximum security in production environments, you can completely disable DBViewer:
250
+
251
+ ```ruby
252
+ Dbviewer.configure do |config|
253
+ # Completely disable DBViewer in production
254
+ config.disabled = Rails.env.production?
255
+ end
256
+ ```
257
+
258
+ When disabled, all DBViewer routes return 404 responses, making it appear as if the tool was never installed. This is the recommended approach for production systems where database admin tools should not be accessible.
259
+
215
260
  ## 📝 Security Note
216
261
 
217
262
  ⚠️ **Warning**: This engine provides direct access to your database contents, which contains sensitive information. Always protect it with HTTP Basic Authentication by configuring strong credentials as shown above.
@@ -281,21 +326,21 @@ If you prefer to set up manually:
281
326
  bundle install
282
327
 
283
328
  # Set up the dummy app database
284
- cd test/dummy
329
+ cd sample/app
285
330
  bin/rails db:prepare
286
331
  bin/rails db:migrate
287
332
  bin/rails db:seed
288
333
  cd ../..
289
334
 
290
335
  # Prepare test environment
291
- cd test/dummy && bin/rails db:test:prepare && cd ../..
336
+ cd sample/app && bin/rails db:test:prepare && cd ../..
292
337
  ```
293
338
 
294
339
  ### Development Commands
295
340
 
296
341
  ```bash
297
342
  # Start the development server
298
- cd test/dummy && bin/rails server
343
+ cd sample/app && bin/rails server
299
344
 
300
345
  # Run tests
301
346
  bundle exec rspec
@@ -312,7 +357,7 @@ gem build dbviewer.gemspec
312
357
 
313
358
  ### Testing Your Changes
314
359
 
315
- 1. Start the dummy Rails application: `cd test/dummy && bin/rails server`
360
+ 1. Start the dummy Rails application: `cd sample/app && bin/rails server`
316
361
  2. Visit `http://localhost:3000/dbviewer` to test your changes
317
362
  3. The dummy app includes sample data across multiple tables to test various DBViewer features
318
363
 
data/Rakefile CHANGED
@@ -1,6 +1,6 @@
1
1
  require "bundler/setup"
2
2
 
3
- APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
3
+ APP_RAKEFILE = File.expand_path("sample/app/Rakefile", __dir__)
4
4
  load "rails/tasks/engine.rake"
5
5
 
6
6
  load "rails/tasks/statistics.rake"
@@ -9,6 +9,15 @@ document.addEventListener("DOMContentLoaded", function () {
9
9
  return;
10
10
  }
11
11
 
12
+ // Helper function to debounce rapid function calls
13
+ function debounce(func, wait) {
14
+ let timeout;
15
+ return function (...args) {
16
+ clearTimeout(timeout);
17
+ timeout = setTimeout(() => func.apply(this, args), wait);
18
+ };
19
+ }
20
+
12
21
  // Initialize mermaid with theme detection like mini ERD
13
22
  mermaid.initialize({
14
23
  startOnLoad: true,
@@ -102,6 +111,7 @@ document.addEventListener("DOMContentLoaded", function () {
102
111
  // Function to fetch relationships asynchronously
103
112
  async function fetchRelationships() {
104
113
  const apiPath = document.getElementById("relationships_api_path").value;
114
+ updateRelationshipsStatus(false, "Requesting relationships data...");
105
115
  try {
106
116
  const response = await fetch(apiPath, {
107
117
  headers: {
@@ -114,15 +124,19 @@ document.addEventListener("DOMContentLoaded", function () {
114
124
  throw new Error(`HTTP error! status: ${response.status}`);
115
125
  }
116
126
 
127
+ updateRelationshipsStatus(false, "Processing relationships data...");
117
128
  const data = await response.json();
118
129
  relationships = data.relationships || [];
119
130
  relationshipsLoaded = true;
120
- updateRelationshipsStatus(true);
131
+ updateRelationshipsStatus(
132
+ true,
133
+ `Loaded ${relationships.length} relationships`
134
+ );
121
135
  return relationships;
122
136
  } catch (error) {
123
137
  console.error("Error fetching relationships:", error);
124
138
  relationshipsLoaded = true; // Mark as loaded even on error to prevent infinite loading
125
- updateRelationshipsStatus(true);
139
+ updateRelationshipsStatus(true, "Failed to load relationships", true);
126
140
  return [];
127
141
  }
128
142
  }
@@ -155,20 +169,31 @@ document.addEventListener("DOMContentLoaded", function () {
155
169
  }
156
170
 
157
171
  // Function to update relationships status
158
- function updateRelationshipsStatus(loaded) {
172
+ function updateRelationshipsStatus(loaded, message, isError = false) {
159
173
  const relationshipsStatus = document.getElementById("relationships-status");
160
174
  if (relationshipsStatus) {
161
- if (loaded) {
175
+ if (loaded && !isError) {
162
176
  relationshipsStatus.innerHTML = `
163
177
  <i class="bi bi-check-circle text-success me-2"></i>
164
- <small class="text-success">Relationships loaded</small>
178
+ <small class="text-success">${
179
+ message || "Relationships loaded"
180
+ }</small>
181
+ `;
182
+ } else if (loaded && isError) {
183
+ relationshipsStatus.innerHTML = `
184
+ <i class="bi bi-exclamation-triangle text-warning me-2"></i>
185
+ <small class="text-warning">${
186
+ message || "Error loading relationships"
187
+ }</small>
165
188
  `;
166
189
  } else {
167
190
  relationshipsStatus.innerHTML = `
168
191
  <div class="spinner-border spinner-border-sm text-secondary me-2" role="status">
169
192
  <span class="visually-hidden">Loading...</span>
170
193
  </div>
171
- <small class="text-muted">Loading relationships...</small>
194
+ <small class="text-muted">${
195
+ message || "Loading relationships..."
196
+ }</small>
172
197
  `;
173
198
  }
174
199
  }
@@ -189,10 +214,23 @@ document.addEventListener("DOMContentLoaded", function () {
189
214
  updateLoadingStatus("Loading table details...");
190
215
 
191
216
  // Start fetching relationships immediately
192
- updateRelationshipsStatus(false);
217
+ updateRelationshipsStatus(false, "Loading relationships...");
193
218
  const relationshipsPromise = fetchRelationships();
194
219
  const tablePath = document.getElementById("tables_path").value;
195
220
 
221
+ // Function to fetch tables in batches for better performance
222
+ async function fetchTablesInBatches(tables, batchSize = 5) {
223
+ const batches = [];
224
+ for (let i = 0; i < tables.length; i += batchSize) {
225
+ batches.push(tables.slice(i, i + batchSize));
226
+ }
227
+
228
+ for (const batch of batches) {
229
+ await Promise.all(batch.map((table) => fetchTableColumns(table.name)));
230
+ // This creates a visual effect of tables loading in batches
231
+ }
232
+ }
233
+
196
234
  // First pass: add all tables with minimal info and start loading columns
197
235
  // Function to fetch column data for a table
198
236
  async function fetchTableColumns(tableName) {
@@ -204,6 +242,12 @@ document.addEventListener("DOMContentLoaded", function () {
204
242
  },
205
243
  });
206
244
 
245
+ if (!response.ok) {
246
+ throw new Error(
247
+ `Failed to fetch table ${tableName}: ${response.status}`
248
+ );
249
+ }
250
+
207
251
  const data = await response.json();
208
252
 
209
253
  if (data && data.columns) {
@@ -217,22 +261,29 @@ document.addEventListener("DOMContentLoaded", function () {
217
261
  }
218
262
  } catch (error) {
219
263
  console.error(`Error fetching columns for table ${tableName}:`, error);
264
+ // Add better error handling
265
+ showError(
266
+ "Table Loading Error",
267
+ `Failed to load columns for table ${tableName}`,
268
+ error.message
269
+ );
220
270
  columnsLoadedCount++;
221
271
  updateTableProgress(columnsLoadedCount, totalTables);
222
272
  checkIfReadyToUpdate();
223
273
  }
224
274
  }
225
275
 
276
+ // Generate initial table representation
226
277
  tables.forEach(function (table) {
227
278
  const tableName = table.name;
228
279
  mermaidDefinition += ` ${tableName} {\n`;
229
280
  mermaidDefinition += ` string id\n`;
230
281
  mermaidDefinition += " }\n";
231
-
232
- // Start loading column data asynchronously
233
- fetchTableColumns(tableName);
234
282
  });
235
283
 
284
+ // Start loading column data asynchronously in batches
285
+ fetchTablesInBatches(tables);
286
+
236
287
  // Function to check if we're ready to update the diagram with full data
237
288
  function checkIfReadyToUpdate() {
238
289
  if (columnsLoadedCount === totalTables && relationshipsLoaded) {
@@ -381,18 +432,54 @@ document.addEventListener("DOMContentLoaded", function () {
381
432
  panZoomInstance.zoom(1);
382
433
 
383
434
  // Add event listeners for zoom controls
384
- document.getElementById("zoomIn").addEventListener("click", function () {
385
- panZoomInstance.zoomIn();
386
- });
435
+ document.getElementById("zoomIn").addEventListener(
436
+ "click",
437
+ debounce(function () {
438
+ panZoomInstance.zoomIn();
439
+ }, 100)
440
+ );
387
441
 
388
- document.getElementById("zoomOut").addEventListener("click", function () {
389
- panZoomInstance.zoomOut();
390
- });
442
+ document.getElementById("zoomOut").addEventListener(
443
+ "click",
444
+ debounce(function () {
445
+ panZoomInstance.zoomOut();
446
+ }, 100)
447
+ );
448
+
449
+ document.getElementById("resetView").addEventListener(
450
+ "click",
451
+ debounce(function () {
452
+ panZoomInstance.reset();
453
+ }, 100)
454
+ );
391
455
 
392
- document.getElementById("resetView").addEventListener("click", function () {
393
- panZoomInstance.reset();
456
+ // Add keyboard shortcuts for zoom controls
457
+ document.addEventListener("keydown", (e) => {
458
+ if (e.ctrlKey || e.metaKey) {
459
+ if (e.key === "+" || e.key === "=") {
460
+ e.preventDefault();
461
+ panZoomInstance.zoomIn();
462
+ } else if (e.key === "-") {
463
+ e.preventDefault();
464
+ panZoomInstance.zoomOut();
465
+ } else if (e.key === "0") {
466
+ e.preventDefault();
467
+ panZoomInstance.reset();
468
+ }
469
+ }
394
470
  });
395
471
 
472
+ // Improve ARIA attributes
473
+ document
474
+ .getElementById("zoomIn")
475
+ .setAttribute("aria-label", "Zoom in diagram");
476
+ document
477
+ .getElementById("zoomOut")
478
+ .setAttribute("aria-label", "Zoom out diagram");
479
+ document
480
+ .getElementById("resetView")
481
+ .setAttribute("aria-label", "Reset diagram view");
482
+
396
483
  // Update initial percentage display
397
484
  const zoomDisplay = document.getElementById("zoomPercentage");
398
485
  if (zoomDisplay) {
@@ -447,12 +534,18 @@ document.addEventListener("DOMContentLoaded", function () {
447
534
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
448
535
 
449
536
  // Create download link and trigger download
537
+ const objectURL = URL.createObjectURL(blob);
450
538
  const downloadLink = document.createElement("a");
451
- downloadLink.href = URL.createObjectURL(blob);
539
+ downloadLink.href = objectURL;
452
540
  downloadLink.download = `database_erd_${timestamp}.svg`;
453
541
  document.body.appendChild(downloadLink);
454
542
  downloadLink.click();
455
543
  document.body.removeChild(downloadLink);
544
+
545
+ // Clean up object URL
546
+ setTimeout(() => {
547
+ URL.revokeObjectURL(objectURL);
548
+ }, 100);
456
549
  } catch (error) {
457
550
  console.error("Error downloading SVG:", error);
458
551
  alert("Error downloading SVG. Please check console for details.");
@@ -520,12 +613,18 @@ document.addEventListener("DOMContentLoaded", function () {
520
613
 
521
614
  // Convert canvas to PNG and trigger download
522
615
  canvas.toBlob(function (blob) {
616
+ const objectURL = URL.createObjectURL(blob);
523
617
  const downloadLink = document.createElement("a");
524
- downloadLink.href = URL.createObjectURL(blob);
618
+ downloadLink.href = objectURL;
525
619
  downloadLink.download = `database_erd_${timestamp}.png`;
526
620
  document.body.appendChild(downloadLink);
527
621
  downloadLink.click();
528
622
  document.body.removeChild(downloadLink);
623
+
624
+ // Clean up object URL
625
+ setTimeout(() => {
626
+ URL.revokeObjectURL(objectURL);
627
+ }, 100);
529
628
  }, "image/png");
530
629
 
531
630
  // Clean up
@@ -554,4 +653,38 @@ document.addEventListener("DOMContentLoaded", function () {
554
653
  e.preventDefault();
555
654
  downloadAsPNG();
556
655
  });
656
+
657
+ // Add theme observer to update diagram when theme changes
658
+ function setupThemeObserver() {
659
+ const observer = new MutationObserver((mutations) => {
660
+ mutations.forEach((mutation) => {
661
+ if (mutation.attributeName === "data-bs-theme") {
662
+ const newTheme =
663
+ document.documentElement.getAttribute("data-bs-theme");
664
+ mermaid.initialize({
665
+ theme: newTheme === "dark" ? "dark" : "default",
666
+ // Keep other settings
667
+ securityLevel: "loose",
668
+ er: {
669
+ diagramPadding: 20,
670
+ layoutDirection: "TB",
671
+ minEntityWidth: 100,
672
+ minEntityHeight: 75,
673
+ entityPadding: 15,
674
+ stroke: "gray",
675
+ fill: "honeydew",
676
+ fontSize: 20,
677
+ },
678
+ });
679
+ // Trigger redraw if diagram is already displayed
680
+ if (diagramReady) {
681
+ updateDiagramWithFullData();
682
+ }
683
+ }
684
+ });
685
+ });
686
+ observer.observe(document.documentElement, { attributes: true });
687
+ }
688
+
689
+ setupThemeObserver();
557
690
  });
@@ -0,0 +1,26 @@
1
+ module Dbviewer
2
+ module DatabaseConnectionValidation
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ before_action :validate_database_connection
7
+ end
8
+
9
+ private
10
+
11
+ # Validate database connections on first access to DBViewer
12
+ def validate_database_connection
13
+ return if @database_validated
14
+
15
+ begin
16
+ connection_errors = Dbviewer.validate_connections!
17
+ if connection_errors.any?
18
+ Rails.logger.warn "DBViewer: Some database connections failed: #{connection_errors.map { |e| e[:error] }.join(', ')}"
19
+ end
20
+ @database_validated = true
21
+ rescue => e
22
+ render json: { error: "Database connection failed: #{e.message}" }, status: :service_unavailable and return
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ module Dbviewer
2
+ module DisabledStateValidation
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ before_action :check_if_dbviewer_disabled
7
+ end
8
+
9
+ private
10
+
11
+ # Check if DBViewer is completely disabled
12
+ def check_if_dbviewer_disabled
13
+ if Dbviewer.configuration.disabled
14
+ Rails.logger.info "DBViewer: Access denied - DBViewer is disabled"
15
+ render plain: "Not Found", status: :not_found and return
16
+ end
17
+ end
18
+ end
19
+ end
@@ -13,16 +13,12 @@ module Dbviewer
13
13
  def fetch_recent_queries
14
14
  return [] unless query_logging_enabled?
15
15
 
16
- Dbviewer::Query::Logger.instance.recent_queries(limit: queries_limit)
16
+ Dbviewer::Query::Logger.instance.recent_queries(limit: 10)
17
17
  end
18
18
 
19
19
  def query_logging_enabled?
20
20
  Dbviewer.configuration.enable_query_logging
21
21
  end
22
-
23
- def queries_limit
24
- 10
25
- end
26
22
  end
27
23
  end
28
24
  end
@@ -1,6 +1,8 @@
1
1
  module Dbviewer
2
2
  class ApplicationController < ActionController::Base
3
3
  include Dbviewer::DatabaseOperations
4
+ include Dbviewer::DisabledStateValidation
5
+ include Dbviewer::DatabaseConnectionValidation
4
6
 
5
7
  before_action :authenticate_with_basic_auth
6
8
  before_action :set_tables
@@ -57,12 +57,7 @@ module Dbviewer
57
57
  @tables = fetch_tables # Fetch tables for sidebar
58
58
 
59
59
  @query = prepare_query(@table_name, params[:query])
60
- @records = begin
61
- execute_query(@query)
62
- rescue => e
63
- @error = "Error executing query: #{e.message}"
64
- nil
65
- end
60
+ @records = execute_query(@query)
66
61
 
67
62
  render :query
68
63
  end
@@ -2,11 +2,13 @@ module Dbviewer
2
2
  module ApplicationHelper
3
3
  # Include all the helper modules organized by logical concerns
4
4
  include DatabaseHelper
5
- include FilterHelper
6
5
  include FormattingHelper
7
- include PaginationHelper
8
- include SortingHelper
9
- include TableRenderingHelper
6
+
7
+ include DatatableUiHelper
8
+ include DatatableUiFilterHelper
9
+ include DatatableUiPaginationHelper
10
+ include DatatableUiSortingHelper
11
+ include DatatableUiTableHelper
10
12
  include NavigationHelper
11
13
  include UiHelper
12
14
  end
@@ -1,5 +1,10 @@
1
1
  module Dbviewer
2
2
  module DatabaseHelper
3
+ # Helper to access the database manager
4
+ def get_database_manager
5
+ @database_manager ||= ::Dbviewer::Database::Manager.new
6
+ end
7
+
3
8
  # Check if a table has a created_at column
4
9
  def has_timestamp_column?(table_name)
5
10
  return false unless table_name.present?
@@ -9,11 +14,6 @@ module Dbviewer
9
14
  columns.any? { |col| col[:name] == "created_at" && [ :datetime, :timestamp ].include?(col[:type]) }
10
15
  end
11
16
 
12
- # Helper to access the database manager
13
- def get_database_manager
14
- @database_manager ||= ::Dbviewer::Database::Manager.new
15
- end
16
-
17
17
  # Extract column type from columns info
18
18
  def column_type_from_info(column_name, columns)
19
19
  return nil unless columns.present?
@@ -21,39 +21,5 @@ module Dbviewer
21
21
  column_info = columns.find { |c| c[:name].to_s == column_name.to_s }
22
22
  column_info ? column_info[:type].to_s.downcase : nil
23
23
  end
24
-
25
- # Get appropriate icon for column data type
26
- def column_type_icon(column_type)
27
- case column_type.to_s.downcase
28
- when /int/, /serial/, /number/, /decimal/, /float/, /double/
29
- "bi-123"
30
- when /char/, /text/, /string/, /uuid/
31
- "bi-fonts"
32
- when /date/, /time/
33
- "bi-calendar"
34
- when /bool/
35
- "bi-toggle-on"
36
- when /json/, /jsonb/
37
- "bi-braces"
38
- when /array/
39
- "bi-list-ol"
40
- else
41
- "bi-file-earmark"
42
- end
43
- end
44
-
45
- # Determine if the current table should be active in the sidebar
46
- def current_table?(table_name)
47
- @table_name.present? && @table_name == table_name
48
- end
49
-
50
- # Format table name for display - truncate if too long
51
- def format_table_name(table_name)
52
- if table_name.length > 20
53
- "#{table_name.first(17)}..."
54
- else
55
- table_name
56
- end
57
- end
58
24
  end
59
25
  end
@@ -1,5 +1,5 @@
1
1
  module Dbviewer
2
- module FilterHelper
2
+ module DatatableUiFilterHelper
3
3
  # Determine default operator based on column type
4
4
  def default_operator_for_column_type(column_type)
5
5
  if column_type && column_type =~ /char|text|string|uuid|enum/i
@@ -0,0 +1,37 @@
1
+ module Dbviewer
2
+ module DatatableUiHelper
3
+ # Get appropriate icon for column data type
4
+ def column_type_icon(column_type)
5
+ case column_type.to_s.downcase
6
+ when /int/, /serial/, /number/, /decimal/, /float/, /double/
7
+ "bi-123"
8
+ when /char/, /text/, /string/, /uuid/
9
+ "bi-fonts"
10
+ when /date/, /time/
11
+ "bi-calendar"
12
+ when /bool/
13
+ "bi-toggle-on"
14
+ when /json/, /jsonb/
15
+ "bi-braces"
16
+ when /array/
17
+ "bi-list-ol"
18
+ else
19
+ "bi-file-earmark"
20
+ end
21
+ end
22
+
23
+ # Format table name for display - truncate if too long
24
+ def format_table_name(table_name)
25
+ if table_name.length > 20
26
+ "#{table_name.first(17)}..."
27
+ else
28
+ table_name
29
+ end
30
+ end
31
+
32
+ # Determine if the current table should be active in the sidebar
33
+ def current_table?(table_name)
34
+ @table_name.present? && @table_name == table_name
35
+ end
36
+ end
37
+ end
@@ -1,5 +1,5 @@
1
1
  module Dbviewer
2
- module PaginationHelper
2
+ module DatatableUiPaginationHelper
3
3
  # Common parameters for pagination and filtering
4
4
  def common_params(options = {})
5
5
  params = {
@@ -1,5 +1,5 @@
1
1
  module Dbviewer
2
- module SortingHelper
2
+ module DatatableUiSortingHelper
3
3
  # Returns a sort icon based on the current sort direction
4
4
  def sort_icon(column_name, current_order_by, current_direction)
5
5
  if column_name == current_order_by
@@ -1,5 +1,5 @@
1
1
  module Dbviewer
2
- module TableRenderingHelper
2
+ module DatatableUiTableHelper
3
3
  # Render a complete table header row with sortable columns
4
4
  def render_sortable_header_row(records, order_by, order_direction, table_name, current_page, per_page, column_filters)
5
5
  return content_tag(:tr) { content_tag(:th, "No columns available") } unless records&.columns
@@ -2,29 +2,56 @@ module Dbviewer
2
2
  module FormattingHelper
3
3
  def format_cell_value(value)
4
4
  return "NULL" if value.nil?
5
- return value.to_s.truncate(100) unless value.is_a?(String)
5
+ return format_default_value(value) unless value.is_a?(String)
6
6
 
7
+ format_string_value(value)
8
+ end
9
+
10
+ private
11
+
12
+ def format_string_value(value)
7
13
  case value
8
- when /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/
9
- # ISO 8601 datetime
10
- begin
11
- Time.parse(value).strftime("%Y-%m-%d %H:%M:%S")
12
- rescue
13
- value.to_s.truncate(100)
14
- end
15
- when /\A\d{4}-\d{2}-\d{2}\z/
16
- # Date
17
- value
18
- when /\A{.+}\z/, /\A\[.+\]\z/
19
- # JSON
20
- begin
21
- JSON.pretty_generate(JSON.parse(value)).truncate(100)
22
- rescue
23
- value.to_s.truncate(100)
24
- end
14
+ when ->(v) { datetime_string?(v) }
15
+ format_datetime_value(value)
16
+ when ->(v) { date_string?(v) }
17
+ format_date_value(value)
18
+ when ->(v) { json_string?(v) }
19
+ format_json_value(value)
25
20
  else
26
- value.to_s.truncate(100)
21
+ format_default_value(value)
27
22
  end
28
23
  end
24
+
25
+ def datetime_string?(value)
26
+ value.match?(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
27
+ end
28
+
29
+ def date_string?(value)
30
+ value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
31
+ end
32
+
33
+ def json_string?(value)
34
+ value.match?(/\A{.+}\z/) || value.match?(/\A\[.+\]\z/)
35
+ end
36
+
37
+ def format_datetime_value(value)
38
+ Time.parse(value).strftime("%Y-%m-%d %H:%M:%S")
39
+ rescue
40
+ format_default_value(value)
41
+ end
42
+
43
+ def format_date_value(value)
44
+ value
45
+ end
46
+
47
+ def format_json_value(value)
48
+ JSON.pretty_generate(JSON.parse(value)).truncate(100)
49
+ rescue
50
+ format_default_value(value)
51
+ end
52
+
53
+ def format_default_value(value)
54
+ value.to_s.truncate(100)
55
+ end
29
56
  end
30
57
  end
@@ -115,44 +115,44 @@
115
115
  </div>
116
116
  <div class="offcanvas-body bg-body-tertiary">
117
117
  <ul class="navbar-nav mb-2 mb-lg-0 fw-medium">
118
- <li class="nav-item py-1">
119
- <%= link_to raw('<i class="bi bi-table me-2 text-primary"></i> Tables'), dbviewer.tables_path, class: "nav-link rounded #{tables_nav_class}" %>
120
- </li>
121
- <li class="nav-item py-1">
122
- <%= link_to raw('<i class="bi bi-diagram-3 me-2 text-primary"></i> ERD'), dbviewer.entity_relationship_diagrams_path, class: "nav-link rounded #{erd_nav_class}" %>
123
- </li>
124
- <% if Dbviewer.configuration.enable_query_logging %>
125
- <li class="nav-item py-1">
126
- <%= link_to raw('<i class="bi bi-journal-code me-2 text-primary"></i> SQL Logs'), dbviewer.logs_path, class: "nav-link rounded #{logs_nav_class}" %>
127
- </li>
128
- <% end %>
129
- <li class="nav-item dropdown py-1">
130
- <a class="nav-link dropdown-toggle d-flex align-items-center rounded" href="#" id="offcanvasDbDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
131
- <i class="bi bi-database me-2 text-primary"></i> <%= (current_conn = available_connections.find { |c| c[:current] }) ? current_conn[:name] : "Database" %>
132
- </a>
133
- <ul class="dropdown-menu shadow-sm mt-2" aria-labelledby="offcanvasDbDropdown">
134
- <% available_connections.each do |connection| %>
135
- <li>
136
- <%= button_to connection_path(connection[:key]), method: :post, class: "dropdown-item border-0 w-100 text-start #{'active' if connection[:current]}" do %>
137
- <% if connection[:current] %>
138
- <i class="bi bi-check2-circle me-2 text-primary"></i>
139
- <% else %>
140
- <i class="bi bi-circle me-2"></i>
141
- <% end %>
142
- <%= connection[:name] %>
143
- <% end %>
144
- </li>
118
+ <li class="nav-item py-1">
119
+ <%= link_to raw('<i class="bi bi-table me-2 text-primary"></i> Tables'), dbviewer.tables_path, class: "nav-link rounded #{tables_nav_class}" %>
120
+ </li>
121
+ <li class="nav-item py-1">
122
+ <%= link_to raw('<i class="bi bi-diagram-3 me-2 text-primary"></i> ERD'), dbviewer.entity_relationship_diagrams_path, class: "nav-link rounded #{erd_nav_class}" %>
123
+ </li>
124
+ <% if Dbviewer.configuration.enable_query_logging %>
125
+ <li class="nav-item py-1">
126
+ <%= link_to raw('<i class="bi bi-journal-code me-2 text-primary"></i> SQL Logs'), dbviewer.logs_path, class: "nav-link rounded #{logs_nav_class}" %>
127
+ </li>
145
128
  <% end %>
146
- <li><hr class="dropdown-divider"></li>
147
- <li><%= link_to "<i class='bi bi-gear me-2'></i> Manage Connections".html_safe, connections_path, class: "dropdown-item" %></li>
148
- </ul>
149
- </li>
150
- <li class="mt-4 pt-2 border-top">
151
- <div class="d-flex align-items-center py-2">
152
- <i class="bi bi-tools me-2 text-secondary"></i>
153
- <span class="text-secondary"><%= Rails.env %> environment</span>
154
- </div>
155
- </li>
129
+ <li class="nav-item dropdown py-1">
130
+ <a class="nav-link dropdown-toggle d-flex align-items-center rounded" href="#" id="offcanvasDbDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
131
+ <i class="bi bi-database me-2 text-primary"></i> <%= (current_conn = available_connections.find { |c| c[:current] }) ? current_conn[:name] : "Database" %>
132
+ </a>
133
+ <ul class="dropdown-menu shadow-sm mt-2" aria-labelledby="offcanvasDbDropdown">
134
+ <% available_connections.each do |connection| %>
135
+ <li>
136
+ <%= button_to connection_path(connection[:key]), method: :post, class: "dropdown-item border-0 w-100 text-start #{'active' if connection[:current]}" do %>
137
+ <% if connection[:current] %>
138
+ <i class="bi bi-check2-circle me-2 text-primary"></i>
139
+ <% else %>
140
+ <i class="bi bi-circle me-2"></i>
141
+ <% end %>
142
+ <%= connection[:name] %>
143
+ <% end %>
144
+ </li>
145
+ <% end %>
146
+ <li><hr class="dropdown-divider"></li>
147
+ <li><%= link_to "<i class='bi bi-gear me-2'></i> Manage Connections".html_safe, connections_path, class: "dropdown-item" %></li>
148
+ </ul>
149
+ </li>
150
+ <li class="mt-4 pt-2 border-top">
151
+ <div class="d-flex align-items-center py-2">
152
+ <i class="bi bi-tools me-2 text-secondary"></i>
153
+ <span class="text-secondary"><%= Rails.env %> environment</span>
154
+ </div>
155
+ </li>
156
156
  </ul>
157
157
  </div>
158
158
  </div>
@@ -18,24 +18,7 @@
18
18
  <%= link_to table_path(table[:name], table_url_params), title: table[:name],
19
19
  class: "list-group-item list-group-item-action d-flex align-items-center #{'active' if current_table?(table[:name])}",
20
20
  tabindex: "0",
21
- data: { table_name: table[:name] },
22
- onkeydown: "
23
- if(event.key === 'ArrowDown') {
24
- event.preventDefault();
25
- let next = this.nextElementSibling;
26
- while(next && next.classList.contains('d-none')) {
27
- next = next.nextElementSibling;
28
- }
29
- if(next) next.focus();
30
- } else if(event.key === 'ArrowUp') {
31
- event.preventDefault();
32
- let prev = this.previousElementSibling;
33
- while(prev && prev.classList.contains('d-none')) {
34
- prev = prev.previousElementSibling;
35
- }
36
- if(prev) prev.focus();
37
- else document.getElementById('tableSearch')?.focus();
38
- }" do %>
21
+ data: { table_name: table[:name] } do %>
39
22
  <div class="text-truncate">
40
23
  <i class="bi bi-table me-2"></i>
41
24
  <%= format_table_name(table[:name]) %>
@@ -51,6 +51,14 @@ module Dbviewer
51
51
  # The key of the current active connection
52
52
  attr_accessor :current_connection
53
53
 
54
+ # Whether to validate database connections during application startup
55
+ # Set to false in production/CI environments to avoid startup failures
56
+ attr_accessor :validate_connections_on_startup
57
+
58
+ # Completely disable DBViewer access when set to true
59
+ # When enabled, all DBViewer routes will return 404 responses
60
+ attr_accessor :disabled
61
+
54
62
  def initialize
55
63
  @per_page_options = [ 10, 20, 50, 100 ]
56
64
  @default_per_page = 20
@@ -65,6 +73,8 @@ module Dbviewer
65
73
  @enable_query_logging = true
66
74
  @admin_credentials = nil
67
75
  @default_order_column = "updated_at"
76
+ @validate_connections_on_startup = false # Default to false for safer deployments
77
+ @disabled = false # Default to false - DBViewer is enabled by default
68
78
  @database_connections = {
69
79
  default: {
70
80
  connection_class: "ActiveRecord::Base",
@@ -187,13 +187,20 @@ module Dbviewer
187
187
  connection_config = Dbviewer.configuration.database_connections[@connection_key]
188
188
 
189
189
  if connection_config && connection_config[:connection_class]
190
- @connection = connection_config[:connection_class].constantize.connection
190
+ begin
191
+ @connection = connection_config[:connection_class].constantize.connection
192
+ @adapter_name = @connection.adapter_name.downcase
193
+ Rails.logger.info "DBViewer: Successfully connected to #{connection_config[:name] || @connection_key} database (#{@adapter_name})"
194
+ rescue => e
195
+ Rails.logger.error "DBViewer: Failed to connect to #{connection_config[:name] || @connection_key}: #{e.message}"
196
+ raise "DBViewer database connection failed: #{e.message}"
197
+ end
191
198
  else
192
199
  Rails.logger.warn "DBViewer: Using default connection for key: #{@connection_key}"
193
200
  @connection = ActiveRecord::Base.connection
201
+ @adapter_name = @connection.adapter_name.downcase
194
202
  end
195
203
 
196
- @adapter_name = @connection.adapter_name.downcase
197
204
  @connection
198
205
  end
199
206
  end
@@ -15,28 +15,7 @@ module Dbviewer
15
15
  # @return [ActiveRecord::Result] Result set with columns and rows
16
16
  # @raise [StandardError] If the query is invalid or unsafe
17
17
  def execute_query(sql)
18
- # Validate and normalize the SQL
19
- normalized_sql = ::Dbviewer::Validator::Sql.validate!(sql.to_s)
20
-
21
- # Get max records from configuration
22
- max_records = @config.max_records || 10000
23
-
24
- # Add a safety limit if not already present
25
- unless normalized_sql =~ /\bLIMIT\s+\d+\s*$/i
26
- normalized_sql = "#{normalized_sql} LIMIT #{max_records}"
27
- end
28
-
29
- # Log and execute the query
30
- Rails.logger.debug("[DBViewer] Executing SQL query: #{normalized_sql}")
31
- start_time = Time.now
32
- result = @connection.exec_query(normalized_sql)
33
- duration = Time.now - start_time
34
-
35
- Rails.logger.debug("[DBViewer] Query completed in #{duration.round(2)}s, returned #{result.rows.size} rows")
36
- result
37
- rescue => e
38
- Rails.logger.error("[DBViewer] SQL query error: #{e.message} for query: #{sql}")
39
- raise e
18
+ exec_query(normalize_sql(sql))
40
19
  end
41
20
 
42
21
  # Execute a SQLite PRAGMA command without adding a LIMIT clause
@@ -44,14 +23,20 @@ module Dbviewer
44
23
  # @return [ActiveRecord::Result] Result set with the PRAGMA value
45
24
  # @raise [StandardError] If the query is invalid or cannot be executed
46
25
  def execute_sqlite_pragma(pragma)
47
- sql = "PRAGMA #{pragma}"
48
- Rails.logger.debug("[DBViewer] Executing SQLite pragma: #{sql}")
49
- result = @connection.exec_query(sql)
50
- Rails.logger.debug("[DBViewer] Pragma completed, returned #{result.rows.size} rows")
51
- result
52
- rescue => e
53
- Rails.logger.error("[DBViewer] SQLite pragma error: #{e.message} for pragma: #{pragma}")
54
- raise e
26
+ exec_query("PRAGMA #{pragma}")
27
+ end
28
+
29
+ private
30
+
31
+ def exec_query(sql)
32
+ @connection.exec_query(sql)
33
+ end
34
+
35
+ def normalize_sql(sql)
36
+ normalized_sql = ::Dbviewer::Validator::Sql.validate!(sql.to_s)
37
+ max_records = @config.max_records || 10000
38
+ normalized_sql = "#{normalized_sql} LIMIT #{max_records}" unless normalized_sql =~ /\bLIMIT\s+\d+\s*$/i
39
+ normalized_sql
55
40
  end
56
41
  end
57
42
  end
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.7.5"
2
+ VERSION = "0.7.7"
3
3
  end
data/lib/dbviewer.rb CHANGED
@@ -56,7 +56,21 @@ module Dbviewer
56
56
  # This class method will be called by the engine when it's appropriate
57
57
  def setup
58
58
  configure_query_logger
59
- validate_database_connections
59
+ # Database connections will be validated when first accessed
60
+ Rails.logger.info "DBViewer: Initialized successfully (database connections will be validated on first access)"
61
+ end
62
+
63
+ # Validate database connections on-demand (called when first accessing DBViewer)
64
+ def validate_connections!
65
+ connection_errors = configuration.database_connections.filter_map do |key, config|
66
+ validate_single_connection(key, config)
67
+ end
68
+
69
+ if connection_errors.length == configuration.database_connections.length
70
+ raise "DBViewer could not connect to any configured database. Please check your database configuration."
71
+ end
72
+
73
+ connection_errors
60
74
  end
61
75
 
62
76
  private
@@ -69,15 +83,6 @@ module Dbviewer
69
83
  )
70
84
  end
71
85
 
72
- # Validate all configured database connections
73
- def validate_database_connections
74
- connection_errors = configuration.database_connections.filter_map do |key, config|
75
- validate_single_connection(key, config)
76
- end
77
-
78
- raise_if_all_connections_failed(connection_errors)
79
- end
80
-
81
86
  # Validate a single database connection
82
87
  # @param key [Symbol] The connection key
83
88
  # @param config [Hash] The connection configuration
@@ -125,13 +130,5 @@ module Dbviewer
125
130
  def store_resolved_connection(config, connection_class)
126
131
  config[:connection] = connection_class
127
132
  end
128
-
129
- # Raise an error if all database connections failed
130
- # @param connection_errors [Array] Array of connection error hashes
131
- def raise_if_all_connections_failed(connection_errors)
132
- if connection_errors.length == configuration.database_connections.length
133
- raise "DBViewer could not connect to any configured database"
134
- end
135
- end
136
133
  end
137
134
  end
@@ -33,4 +33,13 @@ Dbviewer.configure do |config|
33
33
 
34
34
  # Set the default active connection
35
35
  # config.current_connection = :primary
36
+
37
+ # Whether to validate database connections during application startup
38
+ # Set to true in development, false in production to avoid deployment issues
39
+ config.validate_connections_on_startup = Rails.env.development?
40
+
41
+ # Completely disable DBViewer access when set to true
42
+ # When enabled, all DBViewer routes will return 404 responses
43
+ # Useful for production environments where you want to completely disable the tool
44
+ # config.disabled = Rails.env.production?
36
45
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbviewer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.5
4
+ version: 0.7.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wailan Tirajoh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-06-14 00:00:00.000000000 Z
11
+ date: 2025-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -77,9 +77,11 @@ files:
77
77
  - app/assets/stylesheets/dbviewer/table.css
78
78
  - app/controllers/concerns/dbviewer/connection_management.rb
79
79
  - app/controllers/concerns/dbviewer/data_export.rb
80
+ - app/controllers/concerns/dbviewer/database_connection_validation.rb
80
81
  - app/controllers/concerns/dbviewer/database_information.rb
81
82
  - app/controllers/concerns/dbviewer/database_operations.rb
82
83
  - app/controllers/concerns/dbviewer/datatable_support.rb
84
+ - app/controllers/concerns/dbviewer/disabled_state_validation.rb
83
85
  - app/controllers/concerns/dbviewer/pagination_concern.rb
84
86
  - app/controllers/concerns/dbviewer/query_operations.rb
85
87
  - app/controllers/concerns/dbviewer/relationship_management.rb
@@ -97,12 +99,13 @@ files:
97
99
  - app/controllers/dbviewer/tables_controller.rb
98
100
  - app/helpers/dbviewer/application_helper.rb
99
101
  - app/helpers/dbviewer/database_helper.rb
100
- - app/helpers/dbviewer/filter_helper.rb
102
+ - app/helpers/dbviewer/datatable_ui_filter_helper.rb
103
+ - app/helpers/dbviewer/datatable_ui_helper.rb
104
+ - app/helpers/dbviewer/datatable_ui_pagination_helper.rb
105
+ - app/helpers/dbviewer/datatable_ui_sorting_helper.rb
106
+ - app/helpers/dbviewer/datatable_ui_table_helper.rb
101
107
  - app/helpers/dbviewer/formatting_helper.rb
102
108
  - app/helpers/dbviewer/navigation_helper.rb
103
- - app/helpers/dbviewer/pagination_helper.rb
104
- - app/helpers/dbviewer/sorting_helper.rb
105
- - app/helpers/dbviewer/table_rendering_helper.rb
106
109
  - app/helpers/dbviewer/ui_helper.rb
107
110
  - app/jobs/dbviewer/application_job.rb
108
111
  - app/mailers/dbviewer/application_mailer.rb