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 +4 -4
- data/README.md +50 -5
- data/Rakefile +1 -1
- data/app/assets/javascripts/dbviewer/entity_relationship_diagram.js +153 -20
- data/app/controllers/concerns/dbviewer/database_connection_validation.rb +26 -0
- data/app/controllers/concerns/dbviewer/disabled_state_validation.rb +19 -0
- data/app/controllers/dbviewer/api/queries_controller.rb +1 -5
- data/app/controllers/dbviewer/application_controller.rb +2 -0
- data/app/controllers/dbviewer/tables_controller.rb +1 -6
- data/app/helpers/dbviewer/application_helper.rb +6 -4
- data/app/helpers/dbviewer/database_helper.rb +5 -39
- data/app/helpers/dbviewer/{filter_helper.rb → datatable_ui_filter_helper.rb} +1 -1
- data/app/helpers/dbviewer/datatable_ui_helper.rb +37 -0
- data/app/helpers/dbviewer/{pagination_helper.rb → datatable_ui_pagination_helper.rb} +1 -1
- data/app/helpers/dbviewer/{sorting_helper.rb → datatable_ui_sorting_helper.rb} +1 -1
- data/app/helpers/dbviewer/{table_rendering_helper.rb → datatable_ui_table_helper.rb} +1 -1
- data/app/helpers/dbviewer/formatting_helper.rb +46 -19
- data/app/views/layouts/dbviewer/application.html.erb +37 -37
- data/app/views/layouts/dbviewer/shared/_sidebar.html.erb +1 -18
- data/lib/dbviewer/configuration.rb +10 -0
- data/lib/dbviewer/database/manager.rb +9 -2
- data/lib/dbviewer/query/executor.rb +15 -30
- data/lib/dbviewer/version.rb +1 -1
- data/lib/dbviewer.rb +15 -18
- data/lib/generators/dbviewer/templates/initializer.rb +9 -0
- metadata +9 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3b8c2130ace09b45819bd1fa2d739fecb8a32a75a1348e7c094cb8ce0d66bd71
|
4
|
+
data.tar.gz: 65ff2d9bf93a401c7b193dca798e8afdfe4bb535d7b0c4ee5ce7729312792d49
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
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
|
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
|
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
|
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
@@ -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(
|
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"
|
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"
|
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(
|
385
|
-
|
386
|
-
|
435
|
+
document.getElementById("zoomIn").addEventListener(
|
436
|
+
"click",
|
437
|
+
debounce(function () {
|
438
|
+
panZoomInstance.zoomIn();
|
439
|
+
}, 100)
|
440
|
+
);
|
387
441
|
|
388
|
-
document.getElementById("zoomOut").addEventListener(
|
389
|
-
|
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
|
-
|
393
|
-
|
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 =
|
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 =
|
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:
|
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 =
|
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
|
-
|
8
|
-
include
|
9
|
-
include
|
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
|
@@ -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
|
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
|
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
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
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
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
data/lib/dbviewer/version.rb
CHANGED
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
|
-
|
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.
|
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-
|
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/
|
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
|