#!/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 files (backend/): loads `.env`, then merges `.env.prod` and `.env.local` without * overriding keys already set (so sqlite-only `.env` can still pick up Postgres from `.env.prod`). * * SOURCE_DATABASE_URL=postgresql://... (recommended if DATABASE_URL in .env is sqlite) * Or POSTGRES_URL / DATABASE_URL if postgres:// or postgresql:// * SQLITE_STORAGE=./data/luckychit.sqlite (destination) * Or DB_* / PGHOST, PGDATABASE, PGUSER, PGPASSWORD */ const path = require('path'); const backendRoot = path.join(__dirname, '..'); require('dotenv').config({ path: path.join(backendRoot, '.env') }); require('dotenv').config({ path: path.join(backendRoot, '.env.prod'), override: false }); require('dotenv').config({ path: path.join(backendRoot, '.env.local'), override: false }); 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 isPostgresConnectionString(s) { const t = String(s).trim(); return /^postgres(ql)?:\/\//i.test(t); } function getPostgresUrl() { const candidates = [ process.env.SOURCE_DATABASE_URL, process.env.POSTGRES_URL, process.env.DATABASE_URL, ].filter(Boolean); for (const u of candidates) { if (isPostgresConnectionString(u)) return String(u).trim(); } const host = process.env.DB_HOST || process.env.PGHOST; const port = process.env.DB_PORT || process.env.PGPORT || 5432; const name = process.env.DB_NAME || process.env.PGDATABASE; const user = process.env.DB_USER || process.env.PGUSER; const pass = process.env.DB_PASSWORD ?? process.env.PGPASSWORD ?? ''; if (host && name && user != null && String(user).length > 0) { const userEnc = encodeURIComponent(String(user)); const passEnc = encodeURIComponent(String(pass)); return `postgresql://${userEnc}:${passEnc}@${host}:${port}/${name}`; } throw new Error( 'Could not build PostgreSQL URL. Your .env likely uses SQLite only.\n' + ' Add one line to backend/.env (use your real Postgres host/user/db), then run again:\n' + ' SOURCE_DATABASE_URL=postgresql://USER:PASSWORD@HOST:5432/DBNAME\n' + ' Or set POSTGRES_URL / DATABASE_URL to a postgresql:// or postgres:// URL.\n' + ' Or set DB_HOST, DB_NAME, DB_USER, DB_PASSWORD (and optional DB_PORT),\n' + ' or libpq vars: PGHOST, PGDATABASE, PGUSER, PGPASSWORD (optional PGPORT).' ); } 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); });