196 lines
6.0 KiB
JavaScript
196 lines
6.0 KiB
JavaScript
#!/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);
|
||
});
|