feat: Implement list management routes with CRUD operations

feat: Add sync routes for data synchronization between client and server

feat: Create web routes for serving UI and data visualization

chore: Configure TypeScript for the server project

docs: Update README with project overview, features, and setup instructions

docs: Add server setup implementation details for first-run configuration

docs: Summarize updates and changes in the project, including new features and enhancements
This commit is contained in:
2025-10-24 20:27:18 +02:00
commit 9aa7ed8878
72 changed files with 8916 additions and 0 deletions
@@ -0,0 +1,75 @@
import { Router, Request, Response } from 'express';
import { db } from '../database';
const router = Router();
// Get fields for a list
router.get('/list/:listId', async (req: Request, res: Response) => {
try {
const fields = db.prepare('SELECT * FROM fields WHERE listId = ? AND isDeleted = 0 ORDER BY `order` ASC').all(req.params.listId);
const formattedFields = (fields as any[]).map(field => ({ ...field, isDeleted: !!field.isDeleted }));
res.json(formattedFields);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch fields' });
}
});
// Create field
router.post('/', async (req: Request, res: Response) => {
try {
const { id, name, fieldType, fieldOptions, listId, order, createdAt, updatedAt, isDeleted } = req.body;
db.prepare(`
INSERT INTO fields (id, name, fieldType, fieldOptions, listId, \`order\`, createdAt, updatedAt, isDeleted)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(id, name, fieldType, fieldOptions, listId, order, createdAt, updatedAt, isDeleted ? 1 : 0);
const field = db.prepare('SELECT * FROM fields WHERE id = ?').get(id);
res.status(201).json({ ...(field as any), isDeleted: !!(field as any).isDeleted });
} catch (error) {
res.status(500).json({ error: 'Failed to create field' });
}
});
// Update field
router.put('/:id', async (req: Request, res: Response) => {
try {
const updatedAt = Date.now();
const { name, fieldType, fieldOptions, order } = req.body;
const result = db.prepare(`
UPDATE fields
SET name = ?, fieldType = ?, fieldOptions = ?, \`order\` = ?, updatedAt = ?
WHERE id = ?
`).run(name, fieldType, fieldOptions, order, updatedAt, req.params.id);
if (result.changes === 0) {
return res.status(404).json({ error: 'Field not found' });
}
const field = db.prepare('SELECT * FROM fields WHERE id = ?').get(req.params.id);
res.json({ ...(field as any), isDeleted: !!(field as any).isDeleted });
} catch (error) {
res.status(500).json({ error: 'Failed to update field' });
}
});
// Delete field (soft delete)
router.delete('/:id', async (req: Request, res: Response) => {
try {
const updatedAt = Date.now();
const result = db.prepare(`
UPDATE fields
SET isDeleted = 1, updatedAt = ?
WHERE id = ?
`).run(updatedAt, req.params.id);
if (result.changes === 0) {
return res.status(404).json({ error: 'Field not found' });
}
res.json({ message: 'Field deleted successfully' });
} catch (error) {
res.status(500).json({ error: 'Failed to delete field' });
}
});
export default router;
+103
View File
@@ -0,0 +1,103 @@
import { Router, Request, Response } from 'express';
import { db } from '../database';
const router = Router();
// Get items for a list
router.get('/list/:listId', async (req: Request, res: Response) => {
try {
const items = db.prepare('SELECT * FROM items WHERE listId = ? AND isDeleted = 0 ORDER BY updatedAt DESC').all(req.params.listId);
const formattedItems = (items as any[]).map(item => ({ ...item, isDeleted: !!item.isDeleted }));
res.json(formattedItems);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch items' });
}
});
// Get item values
router.get('/:itemId/values', async (req: Request, res: Response) => {
try {
const values = db.prepare('SELECT * FROM item_values WHERE itemId = ?').all(req.params.itemId);
res.json(values);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch item values' });
}
});
// Create item
router.post('/', async (req: Request, res: Response) => {
try {
const { id, listId, createdAt, updatedAt, isDeleted } = req.body;
db.prepare(`
INSERT INTO items (id, listId, createdAt, updatedAt, isDeleted)
VALUES (?, ?, ?, ?, ?)
`).run(id, listId, createdAt, updatedAt, isDeleted ? 1 : 0);
const item = db.prepare('SELECT * FROM items WHERE id = ?').get(id);
res.status(201).json({ ...(item as any), isDeleted: !!(item as any).isDeleted });
} catch (error) {
res.status(500).json({ error: 'Failed to create item' });
}
});
// Create/update item value
router.post('/values', async (req: Request, res: Response) => {
try {
const { id, itemId, fieldId, value, updatedAt } = req.body;
db.prepare(`
INSERT INTO item_values (id, itemId, fieldId, value, updatedAt)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
value = ?,
updatedAt = ?
`).run(id, itemId, fieldId, value, updatedAt, value, updatedAt);
const itemValue = db.prepare('SELECT * FROM item_values WHERE id = ?').get(id);
res.json(itemValue);
} catch (error) {
res.status(500).json({ error: 'Failed to save item value' });
}
});
// Update item
router.put('/:id', async (req: Request, res: Response) => {
try {
const updatedAt = Date.now();
const result = db.prepare(`
UPDATE items
SET updatedAt = ?
WHERE id = ?
`).run(updatedAt, req.params.id);
if (result.changes === 0) {
return res.status(404).json({ error: 'Item not found' });
}
const item = db.prepare('SELECT * FROM items WHERE id = ?').get(req.params.id);
res.json({ ...(item as any), isDeleted: !!(item as any).isDeleted });
} catch (error) {
res.status(500).json({ error: 'Failed to update item' });
}
});
// Delete item (soft delete)
router.delete('/:id', async (req: Request, res: Response) => {
try {
const updatedAt = Date.now();
const result = db.prepare(`
UPDATE items
SET isDeleted = 1, updatedAt = ?
WHERE id = ?
`).run(updatedAt, req.params.id);
if (result.changes === 0) {
return res.status(404).json({ error: 'Item not found' });
}
res.json({ message: 'Item deleted successfully' });
} catch (error) {
res.status(500).json({ error: 'Failed to delete item' });
}
});
export default router;
@@ -0,0 +1,88 @@
import { Router, Request, Response } from 'express';
import { db } from '../database';
const router = Router();
// Get all lists
router.get('/', async (req: Request, res: Response) => {
try {
const lists = db.prepare('SELECT * FROM lists WHERE isDeleted = 0 ORDER BY updatedAt DESC').all();
// Convert isDeleted from INTEGER to BOOLEAN
const formattedLists = (lists as any[]).map(list => ({ ...list, isDeleted: !!list.isDeleted }));
res.json(formattedLists);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch lists' });
}
});
// Get single list
router.get('/:id', async (req: Request, res: Response) => {
try {
const list = db.prepare('SELECT * FROM lists WHERE id = ? AND isDeleted = 0').get(req.params.id);
if (!list) {
return res.status(404).json({ error: 'List not found' });
}
res.json({ ...(list as any), isDeleted: !!(list as any).isDeleted });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch list' });
}
});
// Create list
router.post('/', async (req: Request, res: Response) => {
try {
const { id, name, createdAt, updatedAt, isDeleted } = req.body;
db.prepare(`
INSERT INTO lists (id, name, createdAt, updatedAt, isDeleted)
VALUES (?, ?, ?, ?, ?)
`).run(id, name, createdAt, updatedAt, isDeleted ? 1 : 0);
const list = db.prepare('SELECT * FROM lists WHERE id = ?').get(id);
res.status(201).json({ ...(list as any), isDeleted: !!(list as any).isDeleted });
} catch (error) {
res.status(500).json({ error: 'Failed to create list' });
}
});
// Update list
router.put('/:id', async (req: Request, res: Response) => {
try {
const updatedAt = Date.now();
const result = db.prepare(`
UPDATE lists
SET name = ?, updatedAt = ?
WHERE id = ?
`).run(req.body.name, updatedAt, req.params.id);
if (result.changes === 0) {
return res.status(404).json({ error: 'List not found' });
}
const list = db.prepare('SELECT * FROM lists WHERE id = ?').get(req.params.id);
res.json({ ...(list as any), isDeleted: !!(list as any).isDeleted });
} catch (error) {
res.status(500).json({ error: 'Failed to update list' });
}
});
// Delete list (soft delete)
router.delete('/:id', async (req: Request, res: Response) => {
try {
const updatedAt = Date.now();
const result = db.prepare(`
UPDATE lists
SET isDeleted = 1, updatedAt = ?
WHERE id = ?
`).run(updatedAt, req.params.id);
if (result.changes === 0) {
return res.status(404).json({ error: 'List not found' });
}
res.json({ message: 'List deleted successfully' });
} catch (error) {
res.status(500).json({ error: 'Failed to delete list' });
}
});
export default router;
+229
View File
@@ -0,0 +1,229 @@
import { Router, Request, Response } from 'express';
import { db } from '../database';
const router = Router();
interface SyncRequest {
lastSyncTimestamp: number;
lists: any[];
fields: any[];
items: any[];
itemValues: any[];
}
// Stats tracking
let syncStats = {
totalSyncs: 0,
listsReceived: 0,
fieldsReceived: 0,
itemsReceived: 0,
valuesReceived: 0,
listsSent: 0,
fieldsSent: 0,
itemsSent: 0,
valuesSent: 0,
lastReset: Date.now()
};
// Print stats every 30 seconds
setInterval(() => {
if (syncStats.totalSyncs > 0) {
const elapsed = Math.round((Date.now() - syncStats.lastReset) / 1000);
console.log(`\n[STATS] Last ${elapsed}s: ${syncStats.totalSyncs} syncs`);
console.log(` Received: ${syncStats.listsReceived} lists, ${syncStats.fieldsReceived} fields, ${syncStats.itemsReceived} items, ${syncStats.valuesReceived} values`);
console.log(` Sent: ${syncStats.listsSent} lists, ${syncStats.fieldsSent} fields, ${syncStats.itemsSent} items, ${syncStats.valuesSent} values\n`);
// Reset stats
syncStats = {
totalSyncs: 0,
listsReceived: 0,
fieldsReceived: 0,
itemsReceived: 0,
valuesReceived: 0,
listsSent: 0,
fieldsSent: 0,
itemsSent: 0,
valuesSent: 0,
lastReset: Date.now()
};
}
}, 30000);
// Helper function to get prepared statements (lazy initialization)
function getUpsertStatements() {
return {
upsertList: db.prepare(`
INSERT INTO lists (id, name, createdAt, updatedAt, isDeleted)
VALUES (@id, @name, @createdAt, @updatedAt, @isDeleted)
ON CONFLICT(id) DO UPDATE SET
name = @name,
updatedAt = @updatedAt,
isDeleted = @isDeleted
`),
upsertField: db.prepare(`
INSERT INTO fields (id, name, fieldType, fieldOptions, listId, \`order\`, createdAt, updatedAt, isDeleted)
VALUES (@id, @name, @fieldType, @fieldOptions, @listId, @order, @createdAt, @updatedAt, @isDeleted)
ON CONFLICT(id) DO UPDATE SET
name = @name,
fieldType = @fieldType,
fieldOptions = @fieldOptions,
\`order\` = @order,
updatedAt = @updatedAt,
isDeleted = @isDeleted
`),
upsertItem: db.prepare(`
INSERT INTO items (id, listId, createdAt, updatedAt, isDeleted)
VALUES (@id, @listId, @createdAt, @updatedAt, @isDeleted)
ON CONFLICT(id) DO UPDATE SET
updatedAt = @updatedAt,
isDeleted = @isDeleted
`),
upsertItemValue: db.prepare(`
INSERT INTO item_values (id, itemId, fieldId, value, updatedAt)
VALUES (@id, @itemId, @fieldId, @value, @updatedAt)
ON CONFLICT(id) DO UPDATE SET
value = @value,
updatedAt = @updatedAt
`)
};
}
router.post('/sync', async (req: Request, res: Response) => {
try {
const { lastSyncTimestamp, lists, fields, items, itemValues }: SyncRequest = req.body;
// Track stats
syncStats.totalSyncs++;
const incomingLists = lists?.length || 0;
const incomingFields = fields?.length || 0;
const incomingItems = items?.length || 0;
const incomingValues = itemValues?.length || 0;
syncStats.listsReceived += incomingLists;
syncStats.fieldsReceived += incomingFields;
syncStats.itemsReceived += incomingItems;
syncStats.valuesReceived += incomingValues;
// Log only when there's actual data being synced
const hasIncomingData = incomingLists > 0 || incomingFields > 0 || incomingItems > 0 || incomingValues > 0;
if (hasIncomingData) {
console.log(`[SYNC] Received: ${incomingLists} lists, ${incomingFields} fields, ${incomingItems} items, ${incomingValues} values`);
// Log list details
if (lists && lists.length > 0) {
lists.forEach(list => {
console.log(` List: ${list.id} - "${list.name}" (updated: ${list.updatedAt}, deleted: ${list.isDeleted})`);
});
}
}
// Save incoming data from client using a transaction
const transaction = db.transaction((data: SyncRequest) => {
// Get prepared statements lazily
const { upsertList, upsertField, upsertItem, upsertItemValue } = getUpsertStatements();
if (data.lists && data.lists.length > 0) {
for (const list of data.lists) {
upsertList.run({
id: list.id,
name: list.name,
createdAt: list.createdAt,
updatedAt: list.updatedAt,
isDeleted: list.isDeleted ? 1 : 0
});
console.log(` Saved list ${list.id}`);
}
}
if (data.fields && data.fields.length > 0) {
for (const field of data.fields) {
upsertField.run({
id: field.id,
name: field.name,
fieldType: field.fieldType,
fieldOptions: field.fieldOptions,
listId: field.listId,
order: field.order,
createdAt: field.createdAt,
updatedAt: field.updatedAt,
isDeleted: field.isDeleted ? 1 : 0
});
}
}
if (data.items && data.items.length > 0) {
for (const item of data.items) {
upsertItem.run({
id: item.id,
listId: item.listId,
createdAt: item.createdAt,
updatedAt: item.updatedAt,
isDeleted: item.isDeleted ? 1 : 0
});
}
}
if (data.itemValues && data.itemValues.length > 0) {
for (const value of data.itemValues) {
upsertItemValue.run({
id: value.id,
itemId: value.itemId,
fieldId: value.fieldId,
value: value.value,
updatedAt: value.updatedAt
});
}
}
});
transaction({ lastSyncTimestamp, lists, fields, items, itemValues });
// Get updates from server since last sync
// If lastSyncTimestamp is 0, get all data (initial sync)
// Exclude deleted items from being sent
let serverLists, serverFields, serverItems, serverItemValues;
if (lastSyncTimestamp === 0) {
serverLists = db.prepare('SELECT * FROM lists WHERE isDeleted = 0').all();
serverFields = db.prepare('SELECT * FROM fields WHERE isDeleted = 0').all();
serverItems = db.prepare('SELECT * FROM items WHERE isDeleted = 0').all();
serverItemValues = db.prepare('SELECT * FROM item_values').all();
} else {
serverLists = db.prepare('SELECT * FROM lists WHERE updatedAt > ? AND isDeleted = 0').all(lastSyncTimestamp);
serverFields = db.prepare('SELECT * FROM fields WHERE updatedAt > ? AND isDeleted = 0').all(lastSyncTimestamp);
serverItems = db.prepare('SELECT * FROM items WHERE updatedAt > ? AND isDeleted = 0').all(lastSyncTimestamp);
serverItemValues = db.prepare('SELECT * FROM item_values WHERE updatedAt > ?').all(lastSyncTimestamp);
}
// Convert isDeleted from INTEGER (0/1) to BOOLEAN
serverLists = (serverLists as any[]).map(list => ({ ...list, isDeleted: !!list.isDeleted }));
serverFields = (serverFields as any[]).map(field => ({ ...field, isDeleted: !!field.isDeleted }));
serverItems = (serverItems as any[]).map(item => ({ ...item, isDeleted: !!item.isDeleted }));
const serverTimestamp = Date.now();
// Track outgoing stats
syncStats.listsSent += (serverLists as any[]).length;
syncStats.fieldsSent += (serverFields as any[]).length;
syncStats.itemsSent += (serverItems as any[]).length;
syncStats.valuesSent += (serverItemValues as any[]).length;
// Log only when sending data back
const hasOutgoingData = (serverLists as any[]).length > 0 || (serverFields as any[]).length > 0 || (serverItems as any[]).length > 0 || (serverItemValues as any[]).length > 0;
if (hasOutgoingData) {
console.log(`[SYNC] Sending: ${(serverLists as any[]).length} lists, ${(serverFields as any[]).length} fields, ${(serverItems as any[]).length} items, ${(serverItemValues as any[]).length} values`);
}
res.json({
lists: serverLists,
fields: serverFields,
items: serverItems,
itemValues: serverItemValues,
serverTimestamp
});
} catch (error) {
console.error('[SYNC] Error:', error);
res.status(500).json({ error: 'Sync failed' });
}
});
export default router;
+329
View File
@@ -0,0 +1,329 @@
import { Router, Request, Response } from 'express';
import { db } from '../database';
const router = Router();
// Serve the web UI
router.get('/', (req: Request, res: Response) => {
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CollabTable Server - Database Viewer</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
h1 {
color: #333;
margin-bottom: 10px;
}
.stats {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.stat-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
}
.stat-card {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
border-left: 4px solid #007bff;
}
.stat-label {
color: #666;
font-size: 14px;
margin-bottom: 5px;
}
.stat-value {
color: #333;
font-size: 24px;
font-weight: bold;
}
.lists-container {
display: grid;
gap: 20px;
}
.list-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 2px solid #e9ecef;
}
.list-name {
font-size: 20px;
font-weight: bold;
color: #333;
}
.list-id {
font-size: 12px;
color: #999;
font-family: monospace;
}
.fields-container {
margin-bottom: 15px;
}
.field-tag {
display: inline-block;
background: #e7f3ff;
color: #0066cc;
padding: 6px 12px;
border-radius: 4px;
margin-right: 8px;
margin-bottom: 8px;
font-size: 14px;
}
.items-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
.items-table th {
background: #f8f9fa;
padding: 12px;
text-align: left;
font-weight: 600;
color: #495057;
border-bottom: 2px solid #dee2e6;
}
.items-table td {
padding: 12px;
border-bottom: 1px solid #dee2e6;
}
.no-data {
text-align: center;
color: #999;
padding: 40px;
font-style: italic;
}
.refresh-btn {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
margin-left: 10px;
}
.refresh-btn:hover {
background: #0056b3;
}
.deleted {
opacity: 0.5;
background: #fff3cd !important;
}
.badge {
display: inline-block;
background: #dc3545;
color: white;
padding: 2px 8px;
border-radius: 3px;
font-size: 11px;
margin-left: 8px;
}
</style>
</head>
<body>
<div class="container">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h1>CollabTable Server - Database Viewer</h1>
<button class="refresh-btn" onclick="loadData()">Refresh</button>
</div>
<div class="stats">
<h2 style="margin-bottom: 10px;">Database Statistics</h2>
<div class="stat-grid">
<div class="stat-card">
<div class="stat-label">Total Lists</div>
<div class="stat-value" id="total-lists">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Fields</div>
<div class="stat-value" id="total-fields">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Items</div>
<div class="stat-value" id="total-items">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Values</div>
<div class="stat-value" id="total-values">-</div>
</div>
</div>
</div>
<div id="lists-container" class="lists-container">
<div class="no-data">Loading...</div>
</div>
</div>
<script>
async function loadData() {
try {
const response = await fetch('/web/data');
if (!response.ok) {
throw new Error('HTTP ' + response.status + ': ' + response.statusText);
}
const data = await response.json();
// Validate response structure
if (!data || typeof data !== 'object') {
throw new Error('Invalid response format');
}
// Provide defaults for missing properties
const lists = data.lists || [];
const fields = data.fields || [];
const items = data.items || [];
const values = data.values || [];
const stats = data.stats || { lists: 0, fields: 0, items: 0, values: 0 };
// Update stats
document.getElementById('total-lists').textContent = stats.lists;
document.getElementById('total-fields').textContent = stats.fields;
document.getElementById('total-items').textContent = stats.items;
document.getElementById('total-values').textContent = stats.values;
// Render lists
const container = document.getElementById('lists-container');
if (lists.length === 0) {
container.innerHTML = '<div class="no-data">No lists found in database</div>';
return;
}
container.innerHTML = lists.map(list => {
const listFields = fields.filter(f => f.listId === list.id);
const listItems = items.filter(i => i.listId === list.id);
return \`
<div class="list-card \${list.isDeleted ? 'deleted' : ''}">
<div class="list-header">
<div>
<div class="list-name">
\${list.name}
\${list.isDeleted ? '<span class="badge">DELETED</span>' : ''}
</div>
<div class="list-id">ID: \${list.id}</div>
<div class="list-id">Created: \${new Date(list.createdAt).toLocaleString()}</div>
<div class="list-id">Updated: \${new Date(list.updatedAt).toLocaleString()}</div>
</div>
</div>
<div class="fields-container">
<strong>Fields (\${listFields.length}):</strong><br>
\${listFields.length > 0
? listFields.map(f => \`<span class="field-tag \${f.isDeleted ? 'deleted' : ''}">\${f.name} (\${f.fieldType})</span>\`).join('')
: '<span style="color: #999;">No fields</span>'
}
</div>
\${listItems.length > 0 ? \`
<table class="items-table">
<thead>
<tr>
<th>Item ID</th>
\${listFields.map(f => \`<th>\${f.name}</th>\`).join('')}
<th>Updated</th>
</tr>
</thead>
<tbody>
\${listItems.map(item => {
const itemValues = values.filter(v => v.itemId === item.id);
return \`
<tr class="\${item.isDeleted ? 'deleted' : ''}">
<td>\${item.id.substring(0, 8)}...</td>
\${listFields.map(field => {
const value = itemValues.find(v => v.fieldId === field.id);
return \`<td>\${value ? value.value : '-'}</td>\`;
}).join('')}
<td>\${new Date(item.updatedAt).toLocaleString()}</td>
</tr>
\`;
}).join('')}
</tbody>
</table>
\` : '<div style="color: #999; margin-top: 10px;">No items</div>'}
</div>
\`;
}).join('');
} catch (error) {
console.error('Error loading data:', error);
document.getElementById('lists-container').innerHTML =
'<div class="no-data" style="color: #dc3545;">Error loading data: ' + (error.message || 'Unknown error') + '</div>';
}
}
// Load data on page load
loadData();
// Auto-refresh every 5 seconds
setInterval(loadData, 5000);
</script>
</body>
</html>
`);
});
// API endpoint to get all data (no auth required for web UI)
router.get('/web/data', async (req: Request, res: Response) => {
try {
const lists = db.prepare('SELECT * FROM lists ORDER BY updatedAt DESC').all();
const fields = db.prepare('SELECT * FROM fields ORDER BY listId ASC, `order` ASC').all();
const items = db.prepare('SELECT * FROM items ORDER BY listId ASC, updatedAt DESC').all();
const values = db.prepare('SELECT * FROM item_values').all();
// Convert isDeleted from INTEGER to BOOLEAN
const formattedLists = (lists as any[]).map(list => ({ ...list, isDeleted: !!list.isDeleted }));
const formattedFields = (fields as any[]).map(field => ({ ...field, isDeleted: !!field.isDeleted }));
const formattedItems = (items as any[]).map(item => ({ ...item, isDeleted: !!item.isDeleted }));
res.json({
lists: formattedLists,
fields: formattedFields,
items: formattedItems,
values,
stats: {
lists: formattedLists.length,
fields: formattedFields.length,
items: formattedItems.length,
values: (values as any[]).length
}
});
} catch (error) {
console.error('Error fetching data:', error);
res.status(500).json({ error: 'Failed to fetch data' });
}
});
export default router;