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 +4 -4
- data/README.md +1 -1
- data/app/controllers/concerns/dbviewer/database_operations.rb +89 -100
- data/app/controllers/dbviewer/api/entity_relationship_diagrams_controller.rb +25 -23
- data/app/controllers/dbviewer/tables_controller.rb +9 -9
- data/app/helpers/dbviewer/application_helper.rb +33 -13
- data/app/views/dbviewer/tables/show.html.erb +225 -139
- data/app/views/layouts/dbviewer/application.html.erb +55 -0
- data/lib/dbviewer/database/dynamic_model_factory.rb +40 -5
- data/lib/dbviewer/datatable/query_operations.rb +52 -197
- data/lib/dbviewer/engine.rb +1 -22
- data/lib/dbviewer/query/executor.rb +1 -1
- data/lib/dbviewer/query/notification_subscriber.rb +46 -0
- data/lib/dbviewer/validator/sql.rb +198 -0
- data/lib/dbviewer/validator.rb +9 -0
- data/lib/dbviewer/version.rb +1 -1
- data/lib/dbviewer.rb +69 -45
- data/lib/generators/dbviewer/templates/initializer.rb +15 -0
- metadata +5 -3
- data/lib/dbviewer/sql_validator.rb +0 -194
@@ -131,8 +131,8 @@
|
|
131
131
|
|
132
132
|
/* Action column styling */
|
133
133
|
.action-column {
|
134
|
-
width: 60px
|
135
|
-
min-width:
|
134
|
+
width: 100px; /* Increased from 60px to accommodate two buttons */
|
135
|
+
min-width: 100px; /* Ensure minimum width */
|
136
136
|
white-space: nowrap;
|
137
137
|
position: sticky;
|
138
138
|
left: 0;
|
@@ -141,6 +141,16 @@
|
|
141
141
|
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
|
142
142
|
}
|
143
143
|
|
144
|
+
.copy-factory-btn {
|
145
|
+
padding: 0.1rem 0.4rem;
|
146
|
+
width: 32px;
|
147
|
+
}
|
148
|
+
|
149
|
+
.copy-factory-btn:hover {
|
150
|
+
opacity: 0.85;
|
151
|
+
transform: translateY(-1px);
|
152
|
+
}
|
153
|
+
|
144
154
|
/* Ensure proper background color for actions column in dark mode */
|
145
155
|
[data-bs-theme="dark"] .action-column {
|
146
156
|
background-color: var(--bs-dark-bg-subtle, #343a40);
|
@@ -522,8 +532,8 @@
|
|
522
532
|
|
523
533
|
/* Action column styling */
|
524
534
|
.action-column {
|
525
|
-
width: 60px
|
526
|
-
min-width:
|
535
|
+
width: 100px; /* Increased from 60px to accommodate two buttons */
|
536
|
+
min-width: 100px; /* Ensure minimum width */
|
527
537
|
white-space: nowrap;
|
528
538
|
position: sticky;
|
529
539
|
left: 0;
|
@@ -533,6 +543,16 @@
|
|
533
543
|
border-right: 1px solid var(--bs-border-color);
|
534
544
|
}
|
535
545
|
|
546
|
+
.copy-factory-btn {
|
547
|
+
padding: 0.1rem 0.4rem;
|
548
|
+
width: 32px;
|
549
|
+
}
|
550
|
+
|
551
|
+
.copy-factory-btn:hover {
|
552
|
+
opacity: 0.85;
|
553
|
+
transform: translateY(-1px);
|
554
|
+
}
|
555
|
+
|
536
556
|
/* Ensure proper background color for actions column in dark mode */
|
537
557
|
[data-bs-theme="dark"] .action-column {
|
538
558
|
background-color: var(--bs-body-bg, #212529); /* Use body background in dark mode */
|
@@ -1493,154 +1513,220 @@
|
|
1493
1513
|
}
|
1494
1514
|
});
|
1495
1515
|
}
|
1496
|
-
|
1497
|
-
|
1498
|
-
|
1499
|
-
|
1500
|
-
|
1501
|
-
|
1502
|
-
|
1503
|
-
|
1504
|
-
|
1505
|
-
|
1506
|
-
|
1507
|
-
|
1508
|
-
|
1509
|
-
|
1510
|
-
|
1511
|
-
|
1512
|
-
|
1513
|
-
const countSpan = countSpans[index];
|
1514
|
-
if (countSpan) {
|
1515
|
-
const relationshipData = data.relationships.find(r =>
|
1516
|
-
r.table === relationship.from_table && r.foreign_key === relationship.column
|
1517
|
-
);
|
1516
|
+
|
1517
|
+
// Function to copy FactoryBot code
|
1518
|
+
window.copyToJson = function(button) {
|
1519
|
+
try {
|
1520
|
+
// Get record data from data attribute
|
1521
|
+
const recordData = JSON.parse(button.dataset.recordData);
|
1522
|
+
|
1523
|
+
// Generate formatted JSON string
|
1524
|
+
const jsonString = JSON.stringify(recordData, null, 2);
|
1525
|
+
|
1526
|
+
// Copy to clipboard
|
1527
|
+
navigator.clipboard.writeText(jsonString).then(() => {
|
1528
|
+
// Show a temporary success message on the button
|
1529
|
+
const originalTitle = button.getAttribute('title');
|
1530
|
+
button.setAttribute('title', 'Copied!');
|
1531
|
+
button.classList.remove('btn-outline-secondary');
|
1532
|
+
button.classList.add('btn-success');
|
1518
1533
|
|
1519
|
-
|
1520
|
-
|
1521
|
-
|
1522
|
-
|
1523
|
-
|
1524
|
-
|
1525
|
-
|
1526
|
-
|
1527
|
-
|
1528
|
-
|
1529
|
-
|
1534
|
+
// Show a toast notification
|
1535
|
+
if (typeof Toastify === 'function') {
|
1536
|
+
Toastify({
|
1537
|
+
text: `<span class="toast-icon"><i class="bi bi-clipboard-check"></i></span> JSON data copied to clipboard!`,
|
1538
|
+
className: "toast-factory-bot",
|
1539
|
+
duration: 3000,
|
1540
|
+
gravity: "bottom",
|
1541
|
+
position: "right",
|
1542
|
+
escapeMarkup: false,
|
1543
|
+
style: {
|
1544
|
+
animation: "slideInRight 0.3s ease-out, slideOutRight 0.3s ease-out 2.7s"
|
1545
|
+
},
|
1546
|
+
onClick: function() { /* Dismiss toast on click */ }
|
1547
|
+
}).showToast();
|
1548
|
+
}
|
1549
|
+
|
1550
|
+
setTimeout(() => {
|
1551
|
+
button.setAttribute('title', originalTitle);
|
1552
|
+
button.classList.remove('btn-success');
|
1553
|
+
button.classList.add('btn-outline-secondary');
|
1554
|
+
}, 2000);
|
1555
|
+
}).catch(err => {
|
1556
|
+
console.error('Failed to copy text: ', err);
|
1557
|
+
|
1558
|
+
// Show error toast
|
1559
|
+
if (typeof Toastify === 'function') {
|
1560
|
+
Toastify({
|
1561
|
+
text: '<span class="toast-icon"><i class="bi bi-exclamation-triangle"></i></span> Failed to copy to clipboard',
|
1562
|
+
className: "bg-danger",
|
1563
|
+
duration: 3000,
|
1564
|
+
gravity: "bottom",
|
1565
|
+
position: "right",
|
1566
|
+
escapeMarkup: false,
|
1567
|
+
style: {
|
1568
|
+
background: "linear-gradient(135deg, #dc3545, #c82333)",
|
1569
|
+
animation: "slideInRight 0.3s ease-out"
|
1570
|
+
}
|
1571
|
+
}).showToast();
|
1530
1572
|
} else {
|
1531
|
-
|
1532
|
-
countSpan.innerHTML = '<span class="badge bg-danger">Error</span>';
|
1573
|
+
alert('Failed to copy to clipboard. See console for details.');
|
1533
1574
|
}
|
1575
|
+
});
|
1576
|
+
} catch (error) {
|
1577
|
+
console.error('Error generating JSON:', error);
|
1578
|
+
alert('Error generating JSON. See console for details.');
|
1579
|
+
}
|
1580
|
+
};
|
1581
|
+
|
1582
|
+
// Helper function to create relationship sections
|
1583
|
+
// Function to fetch relationship counts from API
|
1584
|
+
async function fetchRelationshipCounts(tableName, recordId, relationships, hasManySection) {
|
1585
|
+
try {
|
1586
|
+
const response = await fetch(`/dbviewer/api/tables/${tableName}/relationship_counts?record_id=${recordId}`);
|
1587
|
+
if (!response.ok) {
|
1588
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
1534
1589
|
}
|
1535
|
-
|
1536
|
-
|
1537
|
-
|
1538
|
-
|
1539
|
-
|
1540
|
-
|
1541
|
-
|
1542
|
-
|
1543
|
-
|
1544
|
-
|
1545
|
-
|
1546
|
-
|
1590
|
+
|
1591
|
+
const data = await response.json();
|
1592
|
+
|
1593
|
+
// Update each count in the UI
|
1594
|
+
const countSpans = hasManySection.querySelectorAll('.relationship-count');
|
1595
|
+
|
1596
|
+
relationships.forEach((relationship, index) => {
|
1597
|
+
const countSpan = countSpans[index];
|
1598
|
+
if (countSpan) {
|
1599
|
+
const relationshipData = data.relationships.find(r =>
|
1600
|
+
r.table === relationship.from_table && r.foreign_key === relationship.column
|
1601
|
+
);
|
1602
|
+
|
1603
|
+
if (relationshipData) {
|
1604
|
+
const count = relationshipData.count;
|
1605
|
+
let badgeClass = 'bg-secondary';
|
1606
|
+
let badgeText = `${count} record${count !== 1 ? 's' : ''}`;
|
1607
|
+
|
1608
|
+
// Use different colors based on count
|
1609
|
+
if (count > 0) {
|
1610
|
+
badgeClass = count > 10 ? 'bg-warning' : 'bg-success';
|
1611
|
+
}
|
1612
|
+
|
1613
|
+
countSpan.innerHTML = `<span class="badge ${badgeClass}">${badgeText}</span>`;
|
1614
|
+
} else {
|
1615
|
+
// Fallback if no data found
|
1616
|
+
countSpan.innerHTML = '<span class="badge bg-danger">Error</span>';
|
1547
1617
|
|
1548
|
-
|
1549
|
-
|
1550
|
-
|
1551
|
-
|
1552
|
-
|
1553
|
-
|
1554
|
-
|
1555
|
-
|
1556
|
-
|
1557
|
-
|
1558
|
-
|
1559
|
-
|
1560
|
-
|
1561
|
-
|
1562
|
-
const table = document.createElement('table');
|
1563
|
-
table.className = 'table table-sm table-bordered';
|
1564
|
-
|
1565
|
-
// Create header based on relationship type
|
1566
|
-
const thead = document.createElement('thead');
|
1567
|
-
if (type === 'belongs_to') {
|
1568
|
-
thead.innerHTML = `
|
1569
|
-
<tr>
|
1570
|
-
<th width="25%">Column</th>
|
1571
|
-
<th width="25%">Value</th>
|
1572
|
-
<th width="25%">References</th>
|
1573
|
-
<th width="25%">Action</th>
|
1574
|
-
</tr>
|
1575
|
-
`;
|
1576
|
-
} else {
|
1577
|
-
thead.innerHTML = `
|
1578
|
-
<tr>
|
1579
|
-
<th width="30%">Related Table</th>
|
1580
|
-
<th width="25%">Foreign Key</th>
|
1581
|
-
<th width="20%">Count</th>
|
1582
|
-
<th width="25%">Action</th>
|
1583
|
-
</tr>
|
1584
|
-
`;
|
1618
|
+
}
|
1619
|
+
}
|
1620
|
+
});
|
1621
|
+
|
1622
|
+
} catch (error) {
|
1623
|
+
console.error('Error fetching relationship counts:', error);
|
1624
|
+
|
1625
|
+
// Show error state in UI
|
1626
|
+
const countSpans = hasManySection.querySelectorAll('.relationship-count');
|
1627
|
+
countSpans.forEach(span => {
|
1628
|
+
span.innerHTML = '<span class="badge bg-danger">Error</span>';
|
1629
|
+
});
|
1630
|
+
}
|
1585
1631
|
}
|
1586
|
-
|
1587
|
-
|
1588
|
-
|
1589
|
-
|
1590
|
-
|
1591
|
-
|
1592
|
-
const
|
1632
|
+
|
1633
|
+
function createRelationshipSection(title, relationships, recordData, type, primaryKeyValue = null) {
|
1634
|
+
const section = document.createElement('div');
|
1635
|
+
section.className = 'relationship-section mb-4';
|
1636
|
+
|
1637
|
+
// Create section header
|
1638
|
+
const header = document.createElement('h6');
|
1639
|
+
header.className = 'mb-3';
|
1640
|
+
const icon = type === 'belongs_to' ? 'bi-arrow-up-right' : 'bi-arrow-down-left';
|
1641
|
+
header.innerHTML = `<i class="bi ${icon} me-2"></i>${title}`;
|
1642
|
+
section.appendChild(header);
|
1643
|
+
|
1644
|
+
const tableContainer = document.createElement('div');
|
1645
|
+
tableContainer.className = 'table-responsive';
|
1646
|
+
|
1647
|
+
const table = document.createElement('table');
|
1648
|
+
table.className = 'table table-sm table-bordered';
|
1593
1649
|
|
1650
|
+
// Create header based on relationship type
|
1651
|
+
const thead = document.createElement('thead');
|
1594
1652
|
if (type === 'belongs_to') {
|
1595
|
-
|
1596
|
-
|
1597
|
-
|
1598
|
-
|
1599
|
-
|
1600
|
-
<
|
1601
|
-
</
|
1602
|
-
<td>
|
1603
|
-
<a href="/dbviewer/tables/${fk.to_table}?column_filters[${fk.primary_key}]=${encodeURIComponent(columnValue)}"
|
1604
|
-
class="btn btn-sm btn-outline-primary"
|
1605
|
-
title="View referenced record in ${fk.to_table}">
|
1606
|
-
<i class="bi bi-arrow-right me-1"></i>View
|
1607
|
-
</a>
|
1608
|
-
</td>
|
1653
|
+
thead.innerHTML = `
|
1654
|
+
<tr>
|
1655
|
+
<th width="25%">Column</th>
|
1656
|
+
<th width="25%">Value</th>
|
1657
|
+
<th width="25%">References</th>
|
1658
|
+
<th width="25%">Action</th>
|
1659
|
+
</tr>
|
1609
1660
|
`;
|
1610
1661
|
} else {
|
1611
|
-
|
1612
|
-
|
1613
|
-
|
1614
|
-
|
1615
|
-
<
|
1616
|
-
|
1617
|
-
|
1618
|
-
<span class="relationship-count">
|
1619
|
-
<span class="badge bg-secondary">
|
1620
|
-
<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
1621
|
-
Loading...
|
1622
|
-
</span>
|
1623
|
-
</span>
|
1624
|
-
</td>
|
1625
|
-
<td>
|
1626
|
-
<a href="/dbviewer/tables/${fk.from_table}?column_filters[${fk.column}]=${encodeURIComponent(primaryKeyValue)}"
|
1627
|
-
class="btn btn-sm btn-outline-success"
|
1628
|
-
title="View all ${fk.from_table} records that reference this record">
|
1629
|
-
<i class="bi bi-list me-1"></i>View Related
|
1630
|
-
</a>
|
1631
|
-
</td>
|
1662
|
+
thead.innerHTML = `
|
1663
|
+
<tr>
|
1664
|
+
<th width="30%">Related Table</th>
|
1665
|
+
<th width="25%">Foreign Key</th>
|
1666
|
+
<th width="20%">Count</th>
|
1667
|
+
<th width="25%">Action</th>
|
1668
|
+
</tr>
|
1632
1669
|
`;
|
1633
1670
|
}
|
1671
|
+
table.appendChild(thead);
|
1634
1672
|
|
1635
|
-
|
1636
|
-
|
1637
|
-
|
1638
|
-
|
1639
|
-
|
1640
|
-
|
1641
|
-
|
1642
|
-
|
1643
|
-
|
1673
|
+
// Create body
|
1674
|
+
const tbody = document.createElement('tbody');
|
1675
|
+
|
1676
|
+
relationships.forEach(fk => {
|
1677
|
+
const row = document.createElement('tr');
|
1678
|
+
|
1679
|
+
if (type === 'belongs_to') {
|
1680
|
+
const columnValue = recordData[fk.column];
|
1681
|
+
row.innerHTML = `
|
1682
|
+
<td class="fw-medium">${fk.column}</td>
|
1683
|
+
<td><code>${columnValue}</code></td>
|
1684
|
+
<td>
|
1685
|
+
<span class="text-muted">${fk.to_table}.</span><strong>${fk.primary_key}</strong>
|
1686
|
+
</td>
|
1687
|
+
<td>
|
1688
|
+
<a href="/dbviewer/tables/${fk.to_table}?column_filters[${fk.primary_key}]=${encodeURIComponent(columnValue)}"
|
1689
|
+
class="btn btn-sm btn-outline-primary"
|
1690
|
+
title="View referenced record in ${fk.to_table}">
|
1691
|
+
<i class="bi bi-arrow-right me-1"></i>View
|
1692
|
+
</a>
|
1693
|
+
</td>
|
1694
|
+
`;
|
1695
|
+
} else {
|
1696
|
+
// For has_many relationships
|
1697
|
+
row.innerHTML = `
|
1698
|
+
<td class="fw-medium">${fk.from_table}</td>
|
1699
|
+
<td>
|
1700
|
+
<span class="text-muted">${fk.from_table}.</span><strong>${fk.column}</strong>
|
1701
|
+
</td>
|
1702
|
+
<td>
|
1703
|
+
<span class="relationship-count">
|
1704
|
+
<span class="badge bg-secondary">
|
1705
|
+
<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
1706
|
+
Loading...
|
1707
|
+
</span>
|
1708
|
+
</span>
|
1709
|
+
</td>
|
1710
|
+
<td>
|
1711
|
+
<a href="/dbviewer/tables/${fk.from_table}?column_filters[${fk.column}]=${encodeURIComponent(primaryKeyValue)}"
|
1712
|
+
class="btn btn-sm btn-outline-success"
|
1713
|
+
title="View all ${fk.from_table} records that reference this record">
|
1714
|
+
<i class="bi bi-list me-1"></i>View Related
|
1715
|
+
</a>
|
1716
|
+
</td>
|
1717
|
+
`;
|
1718
|
+
}
|
1719
|
+
|
1720
|
+
tbody.appendChild(row);
|
1721
|
+
});
|
1722
|
+
|
1723
|
+
table.appendChild(tbody);
|
1724
|
+
tableContainer.appendChild(table);
|
1725
|
+
section.appendChild(tableContainer);
|
1726
|
+
|
1727
|
+
return section;
|
1728
|
+
}
|
1729
|
+
});
|
1644
1730
|
</script>
|
1645
1731
|
|
1646
1732
|
<!-- Floating Creation Filter - Only visible on desktop and on table details page -->
|
@@ -29,6 +29,10 @@
|
|
29
29
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
|
30
30
|
<!-- Chart.js for Data Visualization -->
|
31
31
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
|
32
|
+
|
33
|
+
<!-- Toastify JS for notifications -->
|
34
|
+
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">
|
35
|
+
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
|
32
36
|
|
33
37
|
<style>
|
34
38
|
/* Base styles and typography */
|
@@ -1330,6 +1334,57 @@
|
|
1330
1334
|
}
|
1331
1335
|
|
1332
1336
|
/* Add this right above the style closing tag */
|
1337
|
+
|
1338
|
+
/* Toast styling customizations */
|
1339
|
+
.toastify {
|
1340
|
+
padding: 12px 20px;
|
1341
|
+
color: white;
|
1342
|
+
border-radius: 6px;
|
1343
|
+
display: flex;
|
1344
|
+
align-items: center;
|
1345
|
+
justify-content: space-between;
|
1346
|
+
font-family: var(--bs-body-font-family);
|
1347
|
+
font-size: 0.95rem;
|
1348
|
+
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
|
1349
|
+
max-width: 350px;
|
1350
|
+
}
|
1351
|
+
|
1352
|
+
.toast-factory-bot {
|
1353
|
+
background: linear-gradient(135deg, #28a745, #20c997);
|
1354
|
+
}
|
1355
|
+
|
1356
|
+
.toast-icon {
|
1357
|
+
margin-right: 10px;
|
1358
|
+
font-size: 1.25em;
|
1359
|
+
}
|
1360
|
+
|
1361
|
+
/* Dark mode toast styling */
|
1362
|
+
[data-bs-theme="dark"] .toast-factory-bot {
|
1363
|
+
background: linear-gradient(135deg, #157347, #13795b);
|
1364
|
+
}
|
1365
|
+
|
1366
|
+
/* Toast animations */
|
1367
|
+
@keyframes slideInRight {
|
1368
|
+
from {
|
1369
|
+
transform: translateX(100%);
|
1370
|
+
opacity: 0;
|
1371
|
+
}
|
1372
|
+
to {
|
1373
|
+
transform: translateX(0);
|
1374
|
+
opacity: 1;
|
1375
|
+
}
|
1376
|
+
}
|
1377
|
+
|
1378
|
+
@keyframes slideOutRight {
|
1379
|
+
from {
|
1380
|
+
transform: translateX(0);
|
1381
|
+
opacity: 1;
|
1382
|
+
}
|
1383
|
+
to {
|
1384
|
+
transform: translateX(100%);
|
1385
|
+
opacity: 0;
|
1386
|
+
}
|
1387
|
+
}
|
1333
1388
|
</style>
|
1334
1389
|
</head>
|
1335
1390
|
<body>
|
@@ -28,7 +28,46 @@ module Dbviewer
|
|
28
28
|
# @param table_name [String] Name of the table
|
29
29
|
# @return [Class] ActiveRecord model class for the table
|
30
30
|
def create_model_for(table_name)
|
31
|
-
|
31
|
+
class_name = table_name.classify
|
32
|
+
|
33
|
+
# Check if we can reuse an existing constant
|
34
|
+
existing_model = handle_existing_constant(class_name, table_name)
|
35
|
+
return existing_model if existing_model
|
36
|
+
|
37
|
+
model = create_active_record_model(class_name, table_name)
|
38
|
+
model.establish_connection(@connection.instance_variable_get(:@config))
|
39
|
+
model
|
40
|
+
end
|
41
|
+
|
42
|
+
# Handle existing constant - check if we can reuse it or need to remove it
|
43
|
+
# @param class_name [String] The constant name to check
|
44
|
+
# @param table_name [String] The table name this model should represent
|
45
|
+
# @return [Class, nil] Existing model if reusable, nil otherwise
|
46
|
+
def handle_existing_constant(class_name, table_name)
|
47
|
+
return nil unless Dbviewer.const_defined?(class_name, false)
|
48
|
+
|
49
|
+
existing_model = Dbviewer.const_get(class_name)
|
50
|
+
return existing_model if valid_model_for_table?(existing_model, table_name)
|
51
|
+
|
52
|
+
# If it exists but isn't the right model, remove it first
|
53
|
+
Dbviewer.send(:remove_const, class_name)
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
# Check if an existing model is valid for the given table
|
58
|
+
# @param model [Class] The model class to validate
|
59
|
+
# @param table_name [String] The expected table name
|
60
|
+
# @return [Boolean] true if the model is valid for the table, false otherwise
|
61
|
+
def valid_model_for_table?(model, table_name)
|
62
|
+
model.respond_to?(:table_name) && model.table_name == table_name
|
63
|
+
end
|
64
|
+
|
65
|
+
# Create a new ActiveRecord model class for a table
|
66
|
+
# @param class_name [String] The constant name for the model
|
67
|
+
# @param table_name [String] The table name this model should represent
|
68
|
+
# @return [Class] New ActiveRecord model class
|
69
|
+
def create_active_record_model(class_name, table_name)
|
70
|
+
Dbviewer.const_set(class_name, Class.new(ActiveRecord::Base) do
|
32
71
|
self.table_name = table_name
|
33
72
|
|
34
73
|
# Some tables might not have primary keys, so we handle that
|
@@ -45,10 +84,6 @@ module Dbviewer
|
|
45
84
|
# Disable timestamps for better compatibility
|
46
85
|
self.record_timestamps = false
|
47
86
|
end)
|
48
|
-
|
49
|
-
model.establish_connection(@connection.instance_variable_get(:@config))
|
50
|
-
|
51
|
-
model
|
52
87
|
end
|
53
88
|
end
|
54
89
|
end
|