147 lines
5.4 KiB
JavaScript
147 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
|
|
const root = path.resolve(import.meta.dirname, "..");
|
|
const databasePath = path.join(root, "var", "recipe-book.sqlite");
|
|
|
|
// Dynamically import compiled or source backup engine
|
|
import { exportDatabase } from "../src/lib/backup/export-database.ts";
|
|
import { importDatabase } from "../src/lib/backup/import-database.ts";
|
|
import { validateBackupBundle } from "../src/lib/backup/validate-backup.ts";
|
|
|
|
function printUsage() {
|
|
console.log(`
|
|
Formulation Database Backup & Restore Tool
|
|
|
|
Usage:
|
|
node scripts/backup.mjs export [output-path.json]
|
|
node scripts/backup.mjs import <input-path.json> [--replace | --merge]
|
|
node scripts/backup.mjs validate <input-path.json>
|
|
|
|
Commands:
|
|
export Extracts all 25 SQLite tables into a standardized JSON backup bundle.
|
|
import Restores or merges a backup bundle into the active SQLite database.
|
|
validate Checks a backup JSON file for schema integrity without modifying the database.
|
|
|
|
Options:
|
|
--replace (Default for import) Atomically replaces existing database records.
|
|
--merge Upserts imported records without deleting unmentioned existing data.
|
|
`);
|
|
}
|
|
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
const command = args[0]?.toLowerCase();
|
|
|
|
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
printUsage();
|
|
process.exit(0);
|
|
}
|
|
|
|
if (command === "export") {
|
|
if (!fs.existsSync(databasePath)) {
|
|
console.error(`Error: Database not found at ${databasePath}`);
|
|
process.exit(1);
|
|
}
|
|
const db = new DatabaseSync(databasePath);
|
|
try {
|
|
console.log("Exporting full Formulation database...");
|
|
const bundle = exportDatabase(db);
|
|
const defaultName = `formulation-backup-${new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)}.json`;
|
|
const outputPath = args[1] ? path.resolve(process.cwd(), args[1]) : path.join(process.cwd(), defaultName);
|
|
|
|
fs.writeFileSync(outputPath, JSON.stringify(bundle, null, 2), "utf8");
|
|
console.log(`\n✅ Backup exported successfully to: ${outputPath}`);
|
|
console.log(` - Recipes: ${bundle.summary.recipes_count}`);
|
|
console.log(` - Ingredients: ${bundle.summary.ingredients_count}`);
|
|
console.log(` - Purchase Items: ${bundle.summary.purchase_items_count}`);
|
|
console.log(` - Collections: ${bundle.summary.collections_count}`);
|
|
console.log(` - Inventory Counts: ${bundle.summary.inventory_counts_count}`);
|
|
console.log(` - Total Entities: ${bundle.summary.total_records_count}`);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (command === "validate") {
|
|
const inputPath = args[1];
|
|
if (!inputPath) {
|
|
console.error("Error: Please specify the path to a backup JSON file to validate.");
|
|
process.exit(1);
|
|
}
|
|
const resolvedPath = path.resolve(process.cwd(), inputPath);
|
|
if (!fs.existsSync(resolvedPath)) {
|
|
console.error(`Error: File not found: ${resolvedPath}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const content = JSON.parse(fs.readFileSync(resolvedPath, "utf8"));
|
|
const result = validateBackupBundle(content);
|
|
|
|
if (result.valid) {
|
|
console.log(`\n✅ Backup file '${inputPath}' is valid!`);
|
|
if (result.summary) {
|
|
console.log(` - Format Version: ${content.format_version}`);
|
|
console.log(` - Exported At: ${content.exported_at}`);
|
|
console.log(` - Recipes: ${result.summary.recipes_count}`);
|
|
console.log(` - Ingredients: ${result.summary.ingredients_count}`);
|
|
console.log(` - Purchase Items: ${result.summary.purchase_items_count}`);
|
|
console.log(` - Inventory Counts: ${result.summary.inventory_counts_count}`);
|
|
console.log(` - Total Entities: ${result.summary.total_records_count}`);
|
|
}
|
|
} else {
|
|
console.error(`\n❌ Backup validation failed:`);
|
|
for (const err of result.errors) console.error(` - ${err}`);
|
|
process.exit(1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (command === "import") {
|
|
const inputPath = args[1];
|
|
if (!inputPath) {
|
|
console.error("Error: Please specify the path to a backup JSON file to import.");
|
|
process.exit(1);
|
|
}
|
|
const resolvedPath = path.resolve(process.cwd(), inputPath);
|
|
if (!fs.existsSync(resolvedPath)) {
|
|
console.error(`Error: File not found: ${resolvedPath}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const mode = args.includes("--merge") ? "merge" : "replace";
|
|
const content = JSON.parse(fs.readFileSync(resolvedPath, "utf8"));
|
|
|
|
if (!fs.existsSync(databasePath)) {
|
|
console.error(`Error: Database not found at ${databasePath}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const db = new DatabaseSync(databasePath);
|
|
try {
|
|
console.log(`Importing '${inputPath}' into database (mode: ${mode})...`);
|
|
const result = importDatabase(db, content, { mode, rebuildProjections: true });
|
|
console.log(`\n✅ ${result.message}`);
|
|
} catch (err) {
|
|
console.error(`\n❌ Import failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
process.exit(1);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
return;
|
|
}
|
|
|
|
console.error(`Error: Unknown command '${command}'`);
|
|
printUsage();
|
|
process.exit(1);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error("Fatal error:", err);
|
|
process.exit(1);
|
|
});
|