import { connect } from '@tursodatabase/database'; import { readFile } from 'node:fs/promises'; import { resolve } from 'node:path'; import process from 'node:process'; const backupArgument = process.argv[2]; if (!backupArgument) { console.error('Usage: npm run db:restore -- '); process.exit(2); } const backupPath = resolve(backupArgument); const databasePath = resolve(process.env.TDR_DATABASE_PATH || './data/shd-build-development.db'); const backup = JSON.parse(await readFile(backupPath, 'utf8')); if (backup?.format !== 'shd-build-database-backup' || backup?.version !== 1 || !Array.isArray(backup.tables)) { throw new Error('The selected file is not a supported SHD.build database backup.'); } const quoteIdentifier = (identifier) => `"${String(identifier).replaceAll('"', '""')}"`; const database = await connect(databasePath); // Restores are deliberately limited to empty databases. This prevents an // accidental merge from overwriting contributor-entered records. for (const table of backup.tables) { const existing = await ( await database.prepare(`SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}`) ).get(); if (Number(existing?.count) > 0) { throw new Error( `Restore refused: table "${table.name}" is not empty. Use a new migrated database.` ); } } let restoredRows = 0; await database.exec('BEGIN'); try { for (const table of backup.tables) { for (const row of table.rows) { const columns = Object.keys(row); if (!columns.length) continue; const columnSql = columns.map(quoteIdentifier).join(', '); const placeholders = columns.map(() => '?').join(', '); const insert = await database.prepare( `INSERT INTO ${quoteIdentifier(table.name)} (${columnSql}) VALUES (${placeholders})` ); await insert.run(...columns.map((column) => row[column])); restoredRows++; } } await database.exec('COMMIT'); } catch (error) { await database.exec('ROLLBACK'); throw error; } console.log(`Restored ${restoredRows} rows into ${databasePath}.`); console.log(`Source backup: ${backupPath}`);