Files
classroomio/packages/db/src/scripts/backfill-member-email-notifications-from-profile.ts
T

112 lines
3.9 KiB
TypeScript
Raw Normal View History

/**
* Copies legacy profile.settings.emailNotifications into
* organizationmember_email_notifications (one row per membership), then
* removes the old jsonb key from profile.settings.
*
* Usage:
* pnpm db:backfill-member-email-notifications # dry run (default)
* pnpm db:backfill-member-email-notifications -- --execute # apply updates
*/
import 'dotenv/config';
import postgres from 'postgres';
const connectionString = process.env.DATABASE_URL ?? process.env.PRIVATE_DATABASE_URL ?? '';
const shouldExecute = process.argv.includes('--execute');
if (!connectionString) {
console.error('DATABASE_URL or PRIVATE_DATABASE_URL environment variable is required');
process.exit(1);
}
const AFFECTED_MEMBERSHIPS_SQL = `
SELECT
om.id AS organization_member_id,
p.id AS profile_id,
om.organization_id,
p.settings->'emailNotifications' AS legacy_preferences
FROM organizationmember om
INNER JOIN profile p ON p.id = om.profile_id
WHERE p.settings ? 'emailNotifications'
`;
async function main() {
const sql = postgres(connectionString);
try {
const affected = await sql.unsafe(AFFECTED_MEMBERSHIPS_SQL);
console.log(`Found ${affected.length} organization membership(s) with legacy profile email preferences.`);
if (affected.length === 0) {
console.log('Nothing to do.');
return;
}
if (!shouldExecute) {
console.log('Dry run only. Re-run with --execute to apply updates.');
console.log('Sample (up to 10):');
for (const row of affected.slice(0, 10)) {
console.log(` member=${row.organization_member_id} profile=${row.profile_id} org=${row.organization_id}`);
}
return;
}
const inserted = await sql`
INSERT INTO organizationmember_email_notifications (
organization_member_id,
new_student,
new_submission,
grading_result,
newsfeed,
quiz_assigned,
cohort_reminder,
session,
course_completion
)
SELECT
om.id,
COALESCE((p.settings->'emailNotifications'->>'newStudent')::boolean, true),
COALESCE((p.settings->'emailNotifications'->>'newSubmission')::boolean, true),
COALESCE((p.settings->'emailNotifications'->>'gradingResult')::boolean, true),
COALESCE((p.settings->'emailNotifications'->>'newsfeed')::boolean, true),
COALESCE((p.settings->'emailNotifications'->>'quizAssigned')::boolean, true),
COALESCE((p.settings->'emailNotifications'->>'cohortReminder')::boolean, true),
COALESCE((p.settings->'emailNotifications'->>'session')::boolean, true),
COALESCE((p.settings->'emailNotifications'->>'courseCompletion')::boolean, true)
FROM organizationmember om
INNER JOIN profile p ON p.id = om.profile_id
WHERE p.settings ? 'emailNotifications'
ON CONFLICT (organization_member_id) DO NOTHING
RETURNING organization_member_id
`;
const clearedProfiles = await sql`
UPDATE profile
SET settings = settings - 'emailNotifications'
WHERE settings ? 'emailNotifications'
RETURNING id
`;
const remaining = await sql.unsafe(`SELECT COUNT(*)::int AS count FROM (${AFFECTED_MEMBERSHIPS_SQL}) AS affected`);
const remainingCount = remaining[0]?.count ?? 0;
console.log(`Inserted ${inserted.length} organizationmember_email_notifications row(s).`);
console.log(`Cleared legacy preferences from ${clearedProfiles.length} profile(s).`);
console.log(`Remaining memberships with legacy profile preferences: ${remainingCount}`);
if (remainingCount > 0) {
console.warn('Some rows were not migrated — inspect manually before retrying.');
process.exit(1);
}
console.log('Backfill complete.');
} catch (error) {
console.error('backfill-member-email-notifications-from-profile error:', error);
throw error;
} finally {
await sql.end();
}
}
main();