← 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();