← back to Handbag Auth Nextjs
auction-viewer/migrations.js
326 lines
const Database = require('better-sqlite3');
const fs = require('fs');
const path = require('path');
const DB_PATH = path.join(__dirname, '..', 'data', 'auction-history', 'auctions.db');
const MIGRATIONS_DIR = path.join(__dirname, 'migrations');
// Ensure migrations directory exists
if (!fs.existsSync(MIGRATIONS_DIR)) {
fs.mkdirSync(MIGRATIONS_DIR, { recursive: true });
}
class MigrationManager {
constructor(dbPath) {
this.dbPath = dbPath;
this.db = null;
}
connect() {
this.db = new Database(this.dbPath);
this.db.pragma('journal_mode = WAL');
this.initMigrationsTable();
}
close() {
if (this.db) {
this.db.close();
this.db = null;
}
}
initMigrationsTable() {
this.db.exec(`
CREATE TABLE IF NOT EXISTS migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
}
getAppliedMigrations() {
const migrations = this.db.prepare('SELECT name FROM migrations ORDER BY id').all();
return migrations.map(m => m.name);
}
applyMigration(name, sql) {
console.log(`Applying migration: ${name}`);
const transaction = this.db.transaction(() => {
this.db.exec(sql);
this.db.prepare('INSERT INTO migrations (name) VALUES (?)').run(name);
});
try {
transaction();
console.log(`✓ Migration applied: ${name}`);
return true;
} catch (error) {
console.error(`✗ Migration failed: ${name}`, error);
throw error;
}
}
runMigrations() {
console.log('Running database migrations...\n');
const migrations = [
{
name: '001_add_indexes',
sql: `
-- Add indexes for common queries
CREATE INDEX IF NOT EXISTS idx_auctions_search_term ON auctions(search_term);
CREATE INDEX IF NOT EXISTS idx_auctions_auction_house ON auctions(auction_house);
CREATE INDEX IF NOT EXISTS idx_auctions_current_price ON auctions(current_price);
CREATE INDEX IF NOT EXISTS idx_auctions_created_at ON auctions(created_at);
CREATE INDEX IF NOT EXISTS idx_auctions_estimate_low ON auctions(estimate_low);
-- Composite index for common filtering combinations
CREATE INDEX IF NOT EXISTS idx_auctions_brand_price ON auctions(search_term, current_price);
CREATE INDEX IF NOT EXISTS idx_auctions_price_estimate ON auctions(current_price, estimate_low);
`
},
{
name: '002_add_constraints',
sql: `
-- Create new table with constraints
CREATE TABLE IF NOT EXISTS auctions_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
auction_id TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
auction_house TEXT,
current_price REAL CHECK(current_price >= 0),
estimate_low REAL CHECK(estimate_low >= 0),
estimate_high REAL CHECK(estimate_high >= 0),
end_date TEXT,
lot_number TEXT,
url TEXT,
search_term TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Copy data if old table exists
INSERT OR IGNORE INTO auctions_new (
id, auction_id, title, auction_house, current_price,
estimate_low, estimate_high, end_date, lot_number,
url, search_term, created_at
)
SELECT
id, auction_id, title, auction_house, current_price,
estimate_low, estimate_high, end_date, lot_number,
url, search_term, created_at
FROM auctions
WHERE auction_id IS NOT NULL AND title IS NOT NULL AND search_term IS NOT NULL;
-- Drop old table and rename
DROP TABLE IF EXISTS auctions_old;
ALTER TABLE auctions RENAME TO auctions_old;
ALTER TABLE auctions_new RENAME TO auctions;
DROP TABLE IF EXISTS auctions_old;
`
},
{
name: '003_add_archive_table',
sql: `
-- Create archive table for old auctions
CREATE TABLE IF NOT EXISTS auctions_archive (
id INTEGER PRIMARY KEY AUTOINCREMENT,
auction_id TEXT UNIQUE,
title TEXT,
auction_house TEXT,
current_price REAL,
estimate_low REAL,
estimate_high REAL,
end_date TEXT,
lot_number TEXT,
url TEXT,
search_term TEXT,
created_at TIMESTAMP,
archived_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Add index to archive
CREATE INDEX IF NOT EXISTS idx_archive_created_at ON auctions_archive(created_at);
CREATE INDEX IF NOT EXISTS idx_archive_archived_at ON auctions_archive(archived_at);
`
},
{
name: '004_add_scraper_runs_table',
sql: `
-- Track scraper execution
CREATE TABLE IF NOT EXISTS scraper_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP,
status TEXT CHECK(status IN ('running', 'success', 'failed')),
items_found INTEGER DEFAULT 0,
items_saved INTEGER DEFAULT 0,
error_message TEXT,
brands_searched INTEGER DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_scraper_runs_started ON scraper_runs(started_at);
CREATE INDEX IF NOT EXISTS idx_scraper_runs_status ON scraper_runs(status);
`
},
{
name: '005_add_fts_search',
sql: `
-- Full-text search virtual table
CREATE VIRTUAL TABLE IF NOT EXISTS auctions_fts USING fts5(
title,
auction_house,
search_term,
content='auctions',
content_rowid='id'
);
-- Populate FTS table
INSERT OR IGNORE INTO auctions_fts(rowid, title, auction_house, search_term)
SELECT id, title, auction_house, search_term FROM auctions;
-- Triggers to keep FTS in sync
CREATE TRIGGER IF NOT EXISTS auctions_ai AFTER INSERT ON auctions BEGIN
INSERT INTO auctions_fts(rowid, title, auction_house, search_term)
VALUES (new.id, new.title, new.auction_house, new.search_term);
END;
CREATE TRIGGER IF NOT EXISTS auctions_ad AFTER DELETE ON auctions BEGIN
DELETE FROM auctions_fts WHERE rowid = old.id;
END;
CREATE TRIGGER IF NOT EXISTS auctions_au AFTER UPDATE ON auctions BEGIN
UPDATE auctions_fts SET
title = new.title,
auction_house = new.auction_house,
search_term = new.search_term
WHERE rowid = new.id;
END;
`
}
];
const appliedMigrations = this.getAppliedMigrations();
let appliedCount = 0;
for (const migration of migrations) {
if (!appliedMigrations.includes(migration.name)) {
try {
this.applyMigration(migration.name, migration.sql);
appliedCount++;
} catch (error) {
console.error(`Failed to apply migration ${migration.name}:`, error);
throw error;
}
} else {
console.log(`⊘ Skipping already applied: ${migration.name}`);
}
}
console.log(`\nMigrations complete: ${appliedCount} applied, ${migrations.length - appliedCount} skipped`);
}
// Archive old auctions (older than 1 year)
archiveOldAuctions(daysOld = 365) {
console.log(`Archiving auctions older than ${daysOld} days...`);
const result = this.db.prepare(`
INSERT INTO auctions_archive
SELECT *, CURRENT_TIMESTAMP as archived_at
FROM auctions
WHERE created_at < DATE('now', '-${daysOld} days')
`).run();
if (result.changes > 0) {
this.db.prepare(`
DELETE FROM auctions
WHERE created_at < DATE('now', '-${daysOld} days')
`).run();
console.log(`✓ Archived ${result.changes} old auctions`);
} else {
console.log('No auctions to archive');
}
return result.changes;
}
// Optimize database
optimize() {
console.log('Optimizing database...');
this.db.pragma('optimize');
this.db.exec('VACUUM');
console.log('✓ Database optimized');
}
// Get migration status
getStatus() {
const appliedMigrations = this.getAppliedMigrations();
const tables = this.db.prepare(`
SELECT name FROM sqlite_master WHERE type='table' ORDER BY name
`).all();
const indexes = this.db.prepare(`
SELECT name, tbl_name FROM sqlite_master WHERE type='index' ORDER BY name
`).all();
return {
appliedMigrations,
tables: tables.map(t => t.name),
indexes: indexes.map(i => ({ name: i.name, table: i.tbl_name })),
databaseSize: fs.statSync(this.dbPath).size
};
}
}
// CLI usage
if (require.main === module) {
const manager = new MigrationManager(DB_PATH);
manager.connect();
const command = process.argv[2] || 'migrate';
try {
switch (command) {
case 'migrate':
manager.runMigrations();
break;
case 'status':
const status = manager.getStatus();
console.log('\nDatabase Status:');
console.log('================');
console.log(`Applied Migrations: ${status.appliedMigrations.length}`);
status.appliedMigrations.forEach(m => console.log(` - ${m}`));
console.log(`\nTables: ${status.tables.length}`);
status.tables.forEach(t => console.log(` - ${t}`));
console.log(`\nIndexes: ${status.indexes.length}`);
status.indexes.forEach(i => console.log(` - ${i.name} (on ${i.table})`));
console.log(`\nDatabase Size: ${(status.databaseSize / 1024).toFixed(2)} KB`);
break;
case 'archive':
const days = parseInt(process.argv[3]) || 365;
manager.archiveOldAuctions(days);
break;
case 'optimize':
manager.optimize();
break;
default:
console.log('Unknown command:', command);
console.log('Usage: node migrations.js [migrate|status|archive|optimize]');
}
} catch (error) {
console.error('Error:', error);
process.exit(1);
} finally {
manager.close();
}
}
module.exports = MigrationManager;