← back to Butlr
scripts/migrate-calls-to-user.js
85 lines
#!/usr/bin/env node
// One-shot migration: stamp every existing call with the legacy admin's user_id.
//
// Run before deploying the per-user lockdown to prod, otherwise existing
// calls become orphaned (visible to nobody) post-rollout. Idempotent —
// skips rows that already have user_id set.
//
// Usage (from project root):
// node scripts/migrate-calls-to-user.js [--dry-run]
require('dotenv').config();
const fs = require('fs');
const path = require('path');
const users = require('../lib/users');
const CALLS_FILE = path.join(__dirname, '..', 'data', 'calls.json');
const DRY_RUN = process.argv.includes('--dry-run');
function main() {
// 1. Ensure legacy admin exists (idempotent).
const admin = users.seedLegacyAdmin();
if (!admin) {
console.error('No legacy admin to migrate to — set BUTLR_OWNER_TOKEN env first.');
process.exit(2);
}
console.log(`legacy admin: ${admin.id} (${admin.email})`);
// 2. Read calls.
let calls = [];
try { calls = JSON.parse(fs.readFileSync(CALLS_FILE, 'utf8')); }
catch (e) {
if (e.code === 'ENOENT') {
console.log('no calls.json — nothing to migrate.');
return;
}
throw e;
}
if (!Array.isArray(calls)) {
console.error('calls.json is not an array — refusing to migrate.');
process.exit(3);
}
// 3. Build a set of valid user_ids so we can detect orphans.
const validIds = new Set();
try {
const usersFile = path.join(__dirname, '..', 'data', 'users.json');
const usersList = JSON.parse(fs.readFileSync(usersFile, 'utf8'));
for (const u of usersList) validIds.add(u.id);
} catch {}
let stamped = 0, already = 0, reassigned = 0;
for (const c of calls) {
if (!c.user_id) {
c.user_id = admin.id;
stamped++;
} else if (!validIds.has(c.user_id)) {
// Orphaned — owner user_id is no longer in users.json (deleted or
// wiped). Re-link to current legacy admin so Steve doesn't lose
// his own call history.
console.log(` re-link orphan ${c.id}: ${c.user_id} → ${admin.id}`);
c.user_id = admin.id;
reassigned++;
} else {
already++;
}
}
console.log(`stamped ${stamped} | reassigned ${reassigned} | already-owned ${already} | total ${calls.length}`);
// 4. Write back (with backup).
if (DRY_RUN) {
console.log('[dry-run] not writing');
return;
}
if (stamped + reassigned > 0) {
const backup = CALLS_FILE + '.pre-user-migration.' + new Date().toISOString().replace(/[:.]/g,'-');
fs.copyFileSync(CALLS_FILE, backup);
fs.writeFileSync(CALLS_FILE, JSON.stringify(calls, null, 2));
console.log(`wrote ${calls.length} rows; backup at ${backup}`);
} else {
console.log('no changes needed.');
}
}
main();