- Implement PostgreSQL database schema with users and bookmarks tables - Add database connection pooling with retry logic and error handling - Create migration system with automatic schema initialization - Add database CLI tools for management (init, status, validate, etc.) - Include comprehensive error handling and diagnostics - Add development seed data and testing utilities - Implement health monitoring and connection pool statistics - Create detailed documentation and troubleshooting guide Database features: - Users table with authentication fields and email verification - Bookmarks table with user association and metadata - Proper indexes for performance optimization - Automatic timestamp triggers - Transaction support with rollback handling - Connection pooling (20 max connections, 30s idle timeout) - Graceful shutdown handling CLI commands available: - npm run db:init - Initialize database - npm run db:status - Check database status - npm run db:validate - Validate schema - npm run db:test - Run database tests - npm run db:diagnostics - Full diagnostics
381 lines
16 KiB
HTML
381 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>JSON Import/Export Test</title>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
.test-section {
|
|
margin: 20px 0;
|
|
padding: 15px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 5px;
|
|
}
|
|
.test-result {
|
|
margin: 10px 0;
|
|
padding: 10px;
|
|
border-radius: 3px;
|
|
}
|
|
.test-pass {
|
|
background-color: #d4edda;
|
|
color: #155724;
|
|
border: 1px solid #c3e6cb;
|
|
}
|
|
.test-fail {
|
|
background-color: #f8d7da;
|
|
color: #721c24;
|
|
border: 1px solid #f5c6cb;
|
|
}
|
|
button {
|
|
margin: 5px;
|
|
padding: 8px 16px;
|
|
background: #007bff;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}
|
|
button:hover {
|
|
background: #0056b3;
|
|
}
|
|
pre {
|
|
background: #f8f9fa;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
overflow-x: auto;
|
|
font-size: 12px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>JSON Import/Export Test</h1>
|
|
|
|
<div class="test-section">
|
|
<h2>Test JSON Export Format</h2>
|
|
<button onclick="testJSONExport()">Test JSON Export</button>
|
|
<div id="exportResults"></div>
|
|
<pre id="exportSample"></pre>
|
|
</div>
|
|
|
|
<div class="test-section">
|
|
<h2>Test JSON Import Parsing</h2>
|
|
<button onclick="testJSONImport()">Test JSON Import</button>
|
|
<div id="importResults"></div>
|
|
</div>
|
|
|
|
<div class="test-section">
|
|
<h2>Test Round-trip (Export → Import)</h2>
|
|
<button onclick="testRoundTrip()">Test Round-trip</button>
|
|
<div id="roundtripResults"></div>
|
|
</div>
|
|
|
|
<script>
|
|
// Mock BookmarkManager for testing
|
|
class MockBookmarkManager {
|
|
constructor() {
|
|
this.bookmarks = [
|
|
{
|
|
id: '1',
|
|
title: 'Test Bookmark 1',
|
|
url: 'https://example.com',
|
|
folder: 'Test Folder',
|
|
tags: ['test', 'example'],
|
|
notes: 'Test notes',
|
|
rating: 4,
|
|
favorite: true,
|
|
addDate: Date.now(),
|
|
lastModified: null,
|
|
lastVisited: null,
|
|
icon: '',
|
|
status: 'valid',
|
|
errorCategory: null,
|
|
lastTested: null
|
|
},
|
|
{
|
|
id: '2',
|
|
title: 'Test Bookmark 2',
|
|
url: 'https://test.com',
|
|
folder: '',
|
|
tags: [],
|
|
notes: '',
|
|
rating: 0,
|
|
favorite: false,
|
|
addDate: Date.now() - 86400000,
|
|
lastModified: Date.now(),
|
|
lastVisited: Date.now() - 3600000,
|
|
icon: 'https://test.com/favicon.ico',
|
|
status: 'unknown',
|
|
errorCategory: null,
|
|
lastTested: null
|
|
}
|
|
];
|
|
}
|
|
|
|
// Generate JSON export (matching the actual implementation)
|
|
generateJSONExport(bookmarksToExport) {
|
|
const exportData = {
|
|
exportDate: new Date().toISOString(),
|
|
version: '1.1',
|
|
totalBookmarks: bookmarksToExport.length,
|
|
bookmarks: bookmarksToExport.map(bookmark => ({
|
|
id: bookmark.id,
|
|
title: bookmark.title,
|
|
url: bookmark.url,
|
|
folder: bookmark.folder || '',
|
|
tags: bookmark.tags || [],
|
|
notes: bookmark.notes || '',
|
|
rating: bookmark.rating || 0,
|
|
favorite: bookmark.favorite || false,
|
|
addDate: bookmark.addDate,
|
|
lastModified: bookmark.lastModified,
|
|
lastVisited: bookmark.lastVisited,
|
|
icon: bookmark.icon || '',
|
|
status: bookmark.status,
|
|
errorCategory: bookmark.errorCategory,
|
|
lastTested: bookmark.lastTested
|
|
}))
|
|
};
|
|
|
|
return JSON.stringify(exportData, null, 2);
|
|
}
|
|
|
|
// Parse JSON bookmarks (matching the actual implementation)
|
|
parseJSONBookmarks(content) {
|
|
try {
|
|
const data = JSON.parse(content);
|
|
|
|
// Check if it's our export format
|
|
if (data.bookmarks && Array.isArray(data.bookmarks)) {
|
|
return data.bookmarks.map(bookmark => ({
|
|
id: bookmark.id || Date.now() + Math.random(),
|
|
title: bookmark.title || 'Untitled',
|
|
url: bookmark.url || '',
|
|
folder: bookmark.folder || '',
|
|
tags: bookmark.tags || [],
|
|
notes: bookmark.notes || '',
|
|
rating: bookmark.rating || 0,
|
|
favorite: bookmark.favorite || false,
|
|
addDate: bookmark.addDate || Date.now(),
|
|
lastModified: bookmark.lastModified || null,
|
|
lastVisited: bookmark.lastVisited || null,
|
|
icon: bookmark.icon || '',
|
|
status: 'unknown', // Reset status on import
|
|
errorCategory: null,
|
|
lastTested: null
|
|
}));
|
|
}
|
|
|
|
// If it's just an array of bookmarks
|
|
if (Array.isArray(data)) {
|
|
return data.map(bookmark => ({
|
|
id: bookmark.id || Date.now() + Math.random(),
|
|
title: bookmark.title || 'Untitled',
|
|
url: bookmark.url || '',
|
|
folder: bookmark.folder || '',
|
|
tags: bookmark.tags || [],
|
|
notes: bookmark.notes || '',
|
|
rating: bookmark.rating || 0,
|
|
favorite: bookmark.favorite || false,
|
|
addDate: bookmark.addDate || Date.now(),
|
|
lastModified: bookmark.lastModified || null,
|
|
lastVisited: bookmark.lastVisited || null,
|
|
icon: bookmark.icon || '',
|
|
status: 'unknown',
|
|
errorCategory: null,
|
|
lastTested: null
|
|
}));
|
|
}
|
|
|
|
throw new Error('Invalid JSON bookmark format');
|
|
} catch (error) {
|
|
throw new Error(`Failed to parse JSON bookmarks: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const mockManager = new MockBookmarkManager();
|
|
|
|
function displayResult(containerId, message, isPass) {
|
|
const container = document.getElementById(containerId);
|
|
const resultDiv = document.createElement('div');
|
|
resultDiv.className = `test-result ${isPass ? 'test-pass' : 'test-fail'}`;
|
|
resultDiv.textContent = message;
|
|
container.appendChild(resultDiv);
|
|
}
|
|
|
|
function testJSONExport() {
|
|
const container = document.getElementById('exportResults');
|
|
const sampleContainer = document.getElementById('exportSample');
|
|
container.innerHTML = '';
|
|
sampleContainer.innerHTML = '';
|
|
|
|
try {
|
|
const jsonExport = mockManager.generateJSONExport(mockManager.bookmarks);
|
|
const exportData = JSON.parse(jsonExport);
|
|
|
|
// Test export structure
|
|
const hasExportDate = exportData.exportDate && typeof exportData.exportDate === 'string';
|
|
displayResult('exportResults', `Export date present: ${hasExportDate ? 'PASS' : 'FAIL'}`, hasExportDate);
|
|
|
|
const hasVersion = exportData.version && exportData.version === '1.1';
|
|
displayResult('exportResults', `Version correct: ${hasVersion ? 'PASS' : 'FAIL'}`, hasVersion);
|
|
|
|
const hasCorrectCount = exportData.totalBookmarks === mockManager.bookmarks.length;
|
|
displayResult('exportResults', `Bookmark count correct: ${hasCorrectCount ? 'PASS' : 'FAIL'}`, hasCorrectCount);
|
|
|
|
const hasBookmarksArray = Array.isArray(exportData.bookmarks);
|
|
displayResult('exportResults', `Bookmarks array present: ${hasBookmarksArray ? 'PASS' : 'FAIL'}`, hasBookmarksArray);
|
|
|
|
// Test first bookmark structure
|
|
if (exportData.bookmarks.length > 0) {
|
|
const firstBookmark = exportData.bookmarks[0];
|
|
const hasRequiredFields = firstBookmark.id && firstBookmark.title && firstBookmark.url;
|
|
displayResult('exportResults', `Required fields present: ${hasRequiredFields ? 'PASS' : 'FAIL'}`, hasRequiredFields);
|
|
|
|
const hasOptionalFields = 'tags' in firstBookmark && 'notes' in firstBookmark && 'rating' in firstBookmark;
|
|
displayResult('exportResults', `Optional fields present: ${hasOptionalFields ? 'PASS' : 'FAIL'}`, hasOptionalFields);
|
|
}
|
|
|
|
// Show sample export
|
|
sampleContainer.textContent = jsonExport;
|
|
|
|
} catch (error) {
|
|
displayResult('exportResults', `Export error: ${error.message}`, false);
|
|
}
|
|
}
|
|
|
|
function testJSONImport() {
|
|
const container = document.getElementById('importResults');
|
|
container.innerHTML = '';
|
|
|
|
try {
|
|
// Test with our export format
|
|
const sampleExport = {
|
|
exportDate: new Date().toISOString(),
|
|
version: '1.1',
|
|
totalBookmarks: 2,
|
|
bookmarks: [
|
|
{
|
|
id: 'test1',
|
|
title: 'Import Test 1',
|
|
url: 'https://import-test.com',
|
|
folder: 'Import Folder',
|
|
tags: ['import', 'test'],
|
|
notes: 'Import test notes',
|
|
rating: 3,
|
|
favorite: false,
|
|
addDate: Date.now(),
|
|
lastModified: null,
|
|
lastVisited: null,
|
|
icon: '',
|
|
status: 'valid',
|
|
errorCategory: null,
|
|
lastTested: null
|
|
},
|
|
{
|
|
id: 'test2',
|
|
title: 'Import Test 2',
|
|
url: 'https://import-test2.com',
|
|
folder: '',
|
|
tags: [],
|
|
notes: '',
|
|
rating: 0,
|
|
favorite: true,
|
|
addDate: Date.now(),
|
|
lastModified: null,
|
|
lastVisited: null,
|
|
icon: '',
|
|
status: 'unknown',
|
|
errorCategory: null,
|
|
lastTested: null
|
|
}
|
|
]
|
|
};
|
|
|
|
const importedBookmarks = mockManager.parseJSONBookmarks(JSON.stringify(sampleExport));
|
|
|
|
const correctCount = importedBookmarks.length === 2;
|
|
displayResult('importResults', `Import count correct: ${correctCount ? 'PASS' : 'FAIL'}`, correctCount);
|
|
|
|
const firstBookmarkCorrect = importedBookmarks[0].title === 'Import Test 1' &&
|
|
importedBookmarks[0].url === 'https://import-test.com';
|
|
displayResult('importResults', `First bookmark correct: ${firstBookmarkCorrect ? 'PASS' : 'FAIL'}`, firstBookmarkCorrect);
|
|
|
|
const statusReset = importedBookmarks[0].status === 'unknown';
|
|
displayResult('importResults', `Status reset on import: ${statusReset ? 'PASS' : 'FAIL'}`, statusReset);
|
|
|
|
const tagsPreserved = Array.isArray(importedBookmarks[0].tags) &&
|
|
importedBookmarks[0].tags.includes('import');
|
|
displayResult('importResults', `Tags preserved: ${tagsPreserved ? 'PASS' : 'FAIL'}`, tagsPreserved);
|
|
|
|
// Test with simple array format
|
|
const simpleArray = [
|
|
{ title: 'Simple Test', url: 'https://simple.com', folder: 'Simple' }
|
|
];
|
|
const simpleImported = mockManager.parseJSONBookmarks(JSON.stringify(simpleArray));
|
|
const simpleWorking = simpleImported.length === 1 && simpleImported[0].title === 'Simple Test';
|
|
displayResult('importResults', `Simple array format: ${simpleWorking ? 'PASS' : 'FAIL'}`, simpleWorking);
|
|
|
|
} catch (error) {
|
|
displayResult('importResults', `Import error: ${error.message}`, false);
|
|
}
|
|
}
|
|
|
|
function testRoundTrip() {
|
|
const container = document.getElementById('roundtripResults');
|
|
container.innerHTML = '';
|
|
|
|
try {
|
|
// Export bookmarks
|
|
const exported = mockManager.generateJSONExport(mockManager.bookmarks);
|
|
|
|
// Import them back
|
|
const imported = mockManager.parseJSONBookmarks(exported);
|
|
|
|
// Compare
|
|
const countMatch = imported.length === mockManager.bookmarks.length;
|
|
displayResult('roundtripResults', `Count preserved: ${countMatch ? 'PASS' : 'FAIL'}`, countMatch);
|
|
|
|
const titlesMatch = imported.every((bookmark, index) =>
|
|
bookmark.title === mockManager.bookmarks[index].title
|
|
);
|
|
displayResult('roundtripResults', `Titles preserved: ${titlesMatch ? 'PASS' : 'FAIL'}`, titlesMatch);
|
|
|
|
const urlsMatch = imported.every((bookmark, index) =>
|
|
bookmark.url === mockManager.bookmarks[index].url
|
|
);
|
|
displayResult('roundtripResults', `URLs preserved: ${urlsMatch ? 'PASS' : 'FAIL'}`, urlsMatch);
|
|
|
|
const foldersMatch = imported.every((bookmark, index) =>
|
|
bookmark.folder === mockManager.bookmarks[index].folder
|
|
);
|
|
displayResult('roundtripResults', `Folders preserved: ${foldersMatch ? 'PASS' : 'FAIL'}`, foldersMatch);
|
|
|
|
const tagsMatch = imported.every((bookmark, index) =>
|
|
JSON.stringify(bookmark.tags) === JSON.stringify(mockManager.bookmarks[index].tags)
|
|
);
|
|
displayResult('roundtripResults', `Tags preserved: ${tagsMatch ? 'PASS' : 'FAIL'}`, tagsMatch);
|
|
|
|
const ratingsMatch = imported.every((bookmark, index) =>
|
|
bookmark.rating === mockManager.bookmarks[index].rating
|
|
);
|
|
displayResult('roundtripResults', `Ratings preserved: ${ratingsMatch ? 'PASS' : 'FAIL'}`, ratingsMatch);
|
|
|
|
const favoritesMatch = imported.every((bookmark, index) =>
|
|
bookmark.favorite === mockManager.bookmarks[index].favorite
|
|
);
|
|
displayResult('roundtripResults', `Favorites preserved: ${favoritesMatch ? 'PASS' : 'FAIL'}`, favoritesMatch);
|
|
|
|
} catch (error) {
|
|
displayResult('roundtripResults', `Round-trip error: ${error.message}`, false);
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |