Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 48s
169 lines
6.8 KiB
TypeScript
169 lines
6.8 KiB
TypeScript
// src/db/backup_user.ts
|
|
/*
|
|
|
|
Implement this at some point
|
|
|
|
|
|
Solution 1: Use a Custom Backup/Restore Script (Recommended)
|
|
The most reliable way to handle this is to create dedicated scripts for backing up and restoring a user's data. These scripts can use SQL to intelligently query the database, trace all the foreign key relationships, and generate a complete, ordered SQL file for restoration.
|
|
|
|
How it works:
|
|
|
|
Create a Backup Script (backup_user.js):
|
|
|
|
This Node.js script would connect to the database.
|
|
It would take a user's email as an argument.
|
|
It would then systematically query all tables related to that user, starting with the users table and following the foreign keys to shopping_lists, shopping_list_items, watched_items, etc.
|
|
It would then generate a single user_backup.sql file containing all the INSERT statements needed to recreate that user's data, carefully ordered to respect foreign key constraints (e.g., insert shopping_lists before shopping_list_items).
|
|
Create a Restore Script (restore_user.js):
|
|
|
|
This script would connect to the database.
|
|
It would read the user_backup.sql file and execute its contents against the database, restoring the user's data into the newly created schema.
|
|
Update the Workflow:
|
|
|
|
The manual-db-reset.yml workflow would be simplified to call these scripts. The "Backup" step would run node backup_user.js tsx@gmail.com, and the "Restore" step would run node restore_user.js.
|
|
|
|
*/
|
|
/**
|
|
* @file This script creates a data-only backup for a single user.
|
|
* It traces relationships across tables to export all relevant data as SQL INSERT statements.
|
|
*
|
|
* Usage:
|
|
* tsx src/db/backup_user.ts --email user@example.com --output user_backup.sql
|
|
*/
|
|
|
|
import { Pool, PoolClient } from 'pg';
|
|
import fs from 'fs/promises';
|
|
import dotenv from 'dotenv';
|
|
import { logger } from '../services/logger.server';
|
|
|
|
// Load environment variables from the root .env file
|
|
dotenv.config({ path: '../../.env' });
|
|
|
|
const pool = new Pool({
|
|
user: process.env.DB_USER,
|
|
host: process.env.DB_HOST,
|
|
database: process.env.DB_DATABASE_PROD, // IMPORTANT: This script targets the production DB
|
|
password: process.env.DB_PASSWORD,
|
|
port: parseInt(process.env.DB_PORT || '5432', 10),
|
|
});
|
|
|
|
/**
|
|
* A map defining the tables to back up and the column that links them to the user.
|
|
* The order is critical to respect foreign key constraints on restore.
|
|
* Tables with direct user_id foreign keys come first.
|
|
*/
|
|
const USER_DATA_TABLES: Record<string, string> = {
|
|
'users': 'user_id',
|
|
'profiles': 'user_id',
|
|
'pantry_locations': 'user_id',
|
|
'shopping_lists': 'user_id',
|
|
'recipes': 'user_id',
|
|
'menu_plans': 'user_id',
|
|
'recipe_collections': 'user_id',
|
|
'user_item_aliases': 'user_id',
|
|
'user_appliances': 'user_id',
|
|
'user_dietary_restrictions': 'user_id',
|
|
'favorite_stores': 'user_id',
|
|
'favorite_recipes': 'user_id',
|
|
'user_watched_items': 'user_id',
|
|
'receipts': 'user_id',
|
|
'shopping_trips': 'user_id',
|
|
};
|
|
|
|
type DBValue = string | number | boolean | null | Date | object;
|
|
|
|
/**
|
|
* Generates a SQL INSERT statement for a given row of data.
|
|
* @param table The name of the table.
|
|
* @param row The data object for the row.
|
|
* @returns A formatted SQL INSERT statement string.
|
|
*/
|
|
function generateInsertStatement(table: string, row: Record<string, DBValue>): string {
|
|
const columns = Object.keys(row).map(col => `"${col}"`).join(', ');
|
|
const values = Object.values(row).map(val => {
|
|
if (val === null) return 'NULL';
|
|
if (val instanceof Date) return `'${val.toISOString()}'`; // Handle Date objects
|
|
if (typeof val === 'string') return `'${val.replace(/'/g, "''")}'`; // Escape single quotes
|
|
if (typeof val === 'object') return `'${JSON.stringify(val).replace(/'/g, "''")}'`; // Handle JSONB
|
|
return val;
|
|
}).join(', ');
|
|
|
|
return `INSERT INTO public.${table} (${columns}) VALUES (${values});\n`;
|
|
}
|
|
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
const emailIndex = args.indexOf('--email');
|
|
const outputIndex = args.indexOf('--output');
|
|
|
|
if (emailIndex === -1 || outputIndex === -1) {
|
|
console.error('Usage: tsx src/db/backup_user.ts --email <user_email> --output <output_file.sql>');
|
|
process.exit(1);
|
|
}
|
|
|
|
const userEmail = args[emailIndex + 1];
|
|
const outputFile = args[outputIndex + 1];
|
|
let client: PoolClient | null = null;
|
|
|
|
try {
|
|
client = await pool.connect();
|
|
logger.info(`Connected to database. Backing up user: ${userEmail}`);
|
|
|
|
// 1. Find the user ID
|
|
const userRes = await client.query('SELECT user_id FROM public.users WHERE email = $1', [userEmail]);
|
|
if (userRes.rows.length === 0) {
|
|
logger.warn(`User with email ${userEmail} not found. No backup will be created.`);
|
|
return;
|
|
}
|
|
const userId = userRes.rows[0].user_id;
|
|
logger.info(`Found user ID: ${userId}`);
|
|
|
|
let backupSql = '-- User Data Backup\n';
|
|
backupSql += `-- Generated at: ${new Date().toISOString()}\n`;
|
|
backupSql += `-- User Email: ${userEmail}\n\n`;
|
|
|
|
// 2. Backup data from tables with a direct user_id link
|
|
for (const [table, column] of Object.entries(USER_DATA_TABLES)) {
|
|
const res = await client.query(`SELECT * FROM public.${table} WHERE ${column} = $1`, [userId]);
|
|
if (res.rows.length > 0) {
|
|
backupSql += `-- Data for table: ${table}\n`;
|
|
for (const row of res.rows) {
|
|
backupSql += generateInsertStatement(table, row);
|
|
}
|
|
backupSql += '\n';
|
|
}
|
|
}
|
|
|
|
// 3. Backup data from indirectly related tables (e.g., shopping_list_items)
|
|
const shoppingListsRes = await client.query('SELECT shopping_list_id FROM public.shopping_lists WHERE user_id = $1', [userId]);
|
|
const shoppingListIds = shoppingListsRes.rows.map(r => r.shopping_list_id);
|
|
|
|
if (shoppingListIds.length > 0) {
|
|
const itemsRes = await client.query('SELECT * FROM public.shopping_list_items WHERE shopping_list_id = ANY($1)', [shoppingListIds]);
|
|
if (itemsRes.rows.length > 0) {
|
|
backupSql += '-- Data for table: shopping_list_items\n';
|
|
for (const row of itemsRes.rows) {
|
|
backupSql += generateInsertStatement('shopping_list_items', row);
|
|
}
|
|
backupSql += '\n';
|
|
}
|
|
}
|
|
|
|
// (Add similar logic for other indirectly related tables like recipe_ingredients, planned_meals, etc.)
|
|
|
|
// 4. Write the final SQL to the output file
|
|
await fs.writeFile(outputFile, backupSql);
|
|
logger.info(`✅ Successfully created backup for user ${userEmail} at ${outputFile}`);
|
|
|
|
} catch (error) {
|
|
logger.error('Failed to create user backup.', { error });
|
|
process.exit(1);
|
|
} finally {
|
|
client?.release();
|
|
await pool.end();
|
|
}
|
|
}
|
|
|
|
main();
|