#!/usr/bin/env node /** * Copy all LuckyChit data from PostgreSQL β†’ SQLite (local file). * * Prerequisites: * 1. PostgreSQL reachable with your old data (set SOURCE_DATABASE_URL or DB_* below). * 2. SQLite schema already exists: run `npm start` once with DB_DIALECT=sqlite so tables are created, * or the script will exit with instructions. * * Usage (from backend/): * node scripts/migrate-pg-to-sqlite.js * * Env (backend/.env): * SOURCE_DATABASE_URL=postgresql://user:pass@localhost:5432/luckychit (recommended for migrate) * Or set DATABASE_URL to your Postgres URL temporarily. * SQLITE_STORAGE=./data/luckychit.sqlite (destination; default shown) * * If SOURCE_DATABASE_URL and DATABASE_URL are unset, builds URL from: * DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD */ const path = require('path'); require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); const { Sequelize, QueryTypes } = require('sequelize'); const TABLES_ORDER = [ 'users', 'chit_groups', 'group_members', 'payments', 'payment_audit_events', 'monthly_draws', 'notifications', ]; const TABLES_DELETE_REVERSE = [...TABLES_ORDER].reverse(); function getPostgresUrl() { const u = process.env.SOURCE_DATABASE_URL; if (u && String(u).startsWith('postgres')) return u; const du = process.env.DATABASE_URL; if (du && String(du).startsWith('postgres')) return du; const host = process.env.DB_HOST; const port = process.env.DB_PORT || 5432; const name = process.env.DB_NAME; const user = process.env.DB_USER; const pass = process.env.DB_PASSWORD ?? ''; if (host && name && user !== undefined) { const userEnc = encodeURIComponent(user); const passEnc = encodeURIComponent(pass); return `postgresql://${userEnc}:${passEnc}@${host}:${port}/${name}`; } throw new Error( 'Could not build PostgreSQL URL. Set SOURCE_DATABASE_URL=postgresql://... ' + 'or DATABASE_URL (postgres), or DB_HOST, DB_NAME, DB_USER, DB_PASSWORD.' ); } function getSqlitePath() { const raw = process.env.SQLITE_STORAGE || path.join('data', 'luckychit.sqlite'); return path.isAbsolute(raw) ? raw : path.join(__dirname, '..', raw); } function serializeForSqlite(value) { if (value === null || value === undefined) return null; if (value instanceof Date) return value.toISOString(); if (typeof value === 'boolean') return value ? 1 : 0; if (typeof value === 'bigint') return Number(value); if (Buffer.isBuffer(value)) return value.toString('hex'); if (typeof value === 'object') return JSON.stringify(value); return value; } async function tableExists(sqlite, name) { const rows = await sqlite.query( `SELECT name FROM sqlite_master WHERE type='table' AND name=:name`, { replacements: { name }, type: QueryTypes.SELECT } ); return Array.isArray(rows) && rows.length > 0; } async function fetchPostgresTable(pg, table) { try { return await pg.query(`SELECT * FROM "${table}"`, { type: QueryTypes.SELECT }); } catch (e) { if (e.parent && e.parent.code === '42P01') { console.warn(` ⚠️ Postgres: table "${table}" does not exist β€” skipping.`); return []; } throw e; } } async function clearSqliteTable(sqlite, table) { await sqlite.query(`DELETE FROM "${table}"`, { type: QueryTypes.DELETE }); } async function insertRows(sqlite, table, rows) { if (!rows.length) { console.log(` ${table}: 0 rows`); return; } const cols = Object.keys(rows[0]); const colList = cols.map((c) => `"${c}"`).join(', '); const placeholders = cols.map(() => '?').join(', '); const sql = `INSERT INTO "${table}" (${colList}) VALUES (${placeholders})`; const t = await sqlite.transaction(); try { for (const row of rows) { const values = cols.map((c) => serializeForSqlite(row[c])); await sqlite.query(sql, { replacements: values, transaction: t }); } await t.commit(); console.log(` ${table}: ${rows.length} rows copied`); } catch (e) { await t.rollback(); throw e; } } async function main() { console.log('πŸ”„ PostgreSQL β†’ SQLite migration\n'); const pgUrl = getPostgresUrl(); const sqlitePath = getSqlitePath(); console.log(` Source (Postgres): ${pgUrl.replace(/:[^:@]+@/, ':****@')}`); console.log(` Dest (SQLite): ${sqlitePath}\n`); const pg = new Sequelize(pgUrl, { dialect: 'postgres', logging: false, pool: { max: 5, min: 0, idle: 10000 }, }); const sqlite = new Sequelize({ dialect: 'sqlite', storage: sqlitePath, logging: false, define: { underscored: true, freezeTableName: true }, }); await pg.authenticate(); console.log('βœ… Connected to PostgreSQL'); await sqlite.authenticate(); console.log('βœ… Connected to SQLite'); for (const t of TABLES_ORDER) { if (!(await tableExists(sqlite, t))) { console.error( `\n❌ SQLite table "${t}" is missing. Create schema first:\n` + ` Set DB_DIALECT=sqlite in backend/.env, then run: npm start\n` + ` (or ensure Sequelize sync has created tables.)\n` ); await pg.close(); await sqlite.close(); process.exit(1); } } console.log('\nπŸ—‘οΈ Clearing SQLite tables (child β†’ parent order)…'); await sqlite.transaction(async (transaction) => { await sqlite.query('PRAGMA foreign_keys = OFF', { transaction }); for (const table of TABLES_DELETE_REVERSE) { await sqlite.query(`DELETE FROM "${table}"`, { type: QueryTypes.DELETE, transaction }); } await sqlite.query('PRAGMA foreign_keys = ON', { transaction }); }); console.log(' Done.\n'); console.log('πŸ“₯ Copying from PostgreSQL β†’ SQLite…\n'); for (const table of TABLES_ORDER) { const rows = await fetchPostgresTable(pg, table); await insertRows(sqlite, table, rows); } await pg.close(); await sqlite.close(); console.log('\nβœ… Migration finished. Point DB_DIALECT=sqlite at this file and restart the API.'); } main().catch((err) => { console.error('\n❌ Migration failed:', err.message); if (err.parent) console.error(err.parent.message); process.exit(1); });