/** * Data migration script for user-accounts feature (Task 2.5) * * 1. Creates a default admin user from DEFAULT_ADMIN_EMAIL / DEFAULT_ADMIN_PASSWORD env vars * 2. Backfills user_id on all existing rows in charts, annotations, annotation_types, * span_annotations, span_label_types to the default admin user's ID * * Idempotent: safe to run multiple times. * * Usage: DATABASE_URL=... DEFAULT_ADMIN_EMAIL=... DEFAULT_ADMIN_PASSWORD=... npx tsx scripts/migrate-users.ts */ import { Pool } from 'pg'; import bcrypt from 'bcryptjs'; const SALT_ROUNDS = 12; async function main() { const DATABASE_URL = process.env.DATABASE_URL; if (!DATABASE_URL) { throw new Error('DATABASE_URL environment variable is required'); } const adminEmail = process.env.DEFAULT_ADMIN_EMAIL; const adminPassword = process.env.DEFAULT_ADMIN_PASSWORD; if (!adminEmail || !adminPassword) { throw new Error( 'DEFAULT_ADMIN_EMAIL and DEFAULT_ADMIN_PASSWORD environment variables are required' ); } const pool = new Pool({ connectionString: DATABASE_URL }); try { // ---- Step 1: Upsert default admin user ---- console.log(`Creating/finding default admin user: ${adminEmail}`); // Check if user already exists const existing = await pool.query( 'SELECT id FROM users WHERE email = $1', [adminEmail] ); let adminUserId: string; if (existing.rows.length > 0) { adminUserId = existing.rows[0].id; console.log(` Admin user already exists (id: ${adminUserId})`); } else { const passwordHash = await bcrypt.hash(adminPassword, SALT_ROUNDS); const inserted = await pool.query( `INSERT INTO users (email, password_hash, name, provider) VALUES ($1, $2, $3, 'credentials') RETURNING id`, [adminEmail, passwordHash, 'Admin'] ); adminUserId = inserted.rows[0].id; console.log(` Admin user created (id: ${adminUserId})`); } // ---- Step 2: Backfill user_id on all existing rows ---- const tables = [ 'charts', 'annotations', 'annotation_types', 'span_annotations', 'span_label_types', ]; for (const table of tables) { const result = await pool.query( `UPDATE ${table} SET user_id = $1 WHERE user_id IS NULL`, [adminUserId] ); console.log(` ${table}: backfilled ${result.rowCount} rows`); } console.log('Migration complete.'); } finally { await pool.end(); } } main() .then(() => process.exit(0)) .catch((err) => { console.error('Migration failed:', err); process.exit(1); });