hp-prod-tracker/scripts/clean-slate.ts
2026-04-06 14:35:56 -05:00

286 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import "dotenv/config";
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "../src/generated/prisma/client";
import { execSync } from "child_process";
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! });
const prisma = new PrismaClient({ adapter });
// ─── CLI Argument Parsing ──────────────────────────────
function parseArgs() {
const args = process.argv.slice(2);
const flags: Record<string, string | boolean> = {};
for (let i = 0; i < args.length; i++) {
if (args[i] === "--confirm") {
flags.confirm = true;
} else if (args[i] === "--org-name" && args[i + 1]) {
flags.orgName = args[++i];
} else if (args[i] === "--domain" && args[i + 1]) {
flags.domain = args[++i];
}
}
return flags;
}
function printUsage() {
console.log(`
Usage: npm run db:clean-slate -- [--confirm] --org-name <name> --domain <domain>
Options:
--confirm Execute the purge (without this flag, runs in dry-run mode)
--org-name <name> The real organization name (e.g., "Oliver Agency")
--domain <domain> The real organization domain (e.g., "oliver.agency")
Examples:
npm run db:clean-slate -- --org-name "Oliver Agency" --domain oliver.agency
npm run db:clean-slate -- --confirm --org-name "Oliver Agency" --domain oliver.agency
`);
}
// ─── Record Counting ───────────────────────────────────
async function getRecordCounts() {
const counts: Record<string, number> = {};
counts["organizations"] = await prisma.organization.count();
counts["users"] = await prisma.user.count();
counts["accounts"] = await prisma.account.count();
counts["sessions"] = await prisma.session.count();
counts["projects"] = await prisma.project.count();
counts["deliverables"] = await prisma.deliverable.count();
counts["deliverable_stages"] = await prisma.deliverableStage.count();
counts["stage_assignments"] = await prisma.stageAssignment.count();
counts["revisions"] = await prisma.revision.count();
counts["comments"] = await prisma.comment.count();
counts["annotations"] = await prisma.annotation.count();
counts["feedback_items"] = await prisma.feedbackItem.count();
counts["color_probes"] = await prisma.colorProbe.count();
counts["review_sessions"] = await prisma.reviewSession.count();
counts["review_session_items"] = await prisma.reviewSessionItem.count();
counts["notifications"] = await prisma.notification.count();
counts["chat_messages"] = await prisma.chatMessage.count();
counts["search_logs"] = await prisma.searchLog.count();
counts["automation_rules"] = await prisma.automationRule.count();
counts["automation_executions"] = await prisma.automationExecution.count();
counts["invitations"] = await prisma.invitation.count();
counts["custom_field_definitions"] = await prisma.customFieldDefinition.count();
counts["notification_rules"] = await prisma.notificationRule.count();
counts["user_skills"] = await prisma.userSkill.count();
counts["skills"] = await prisma.skill.count();
counts["stage_skill_requirements"] = await prisma.stageSkillRequirement.count();
counts["pipeline_templates"] = await prisma.pipelineTemplate.count();
counts["pipeline_stage_definitions"] = await prisma.pipelineStageDefinition.count();
counts["pipeline_stage_deps_v2"] = await prisma.pipelineStageDependencyV2.count();
counts["pipeline_stage_templates"] = await prisma.pipelineStageTemplate.count();
counts["pipeline_stage_deps"] = await prisma.pipelineStageDependency.count();
counts["org_role_permissions"] = await prisma.orgRolePermission.count();
return counts;
}
function printCounts(counts: Record<string, number>, label: string) {
console.log(`\n${label}:`);
console.log("─".repeat(50));
const total = Object.values(counts).reduce((a, b) => a + b, 0);
for (const [table, count] of Object.entries(counts)) {
if (count > 0) {
console.log(` ${table.padEnd(30)} ${count}`);
}
}
const zeroCount = Object.values(counts).filter((c) => c === 0).length;
if (zeroCount > 0) {
console.log(` ... and ${zeroCount} tables with 0 records`);
}
console.log(` ${"TOTAL".padEnd(30)} ${total}`);
}
// ─── Purge ─────────────────────────────────────────────
async function purgeAllData() {
console.log("\n🗑 Purging all data...\n");
// Null out self-referential FKs first
console.log(" Nulling self-referential FKs...");
await prisma.comment.updateMany({ data: { parentId: null } });
await prisma.feedbackItem.updateMany({ data: { carriedFromId: null } });
// Leaf tables
console.log(" Deleting leaf tables (annotations, color probes, review session items, feedback items)...");
await prisma.annotation.deleteMany();
await prisma.colorProbe.deleteMany();
await prisma.reviewSessionItem.deleteMany();
await prisma.feedbackItem.deleteMany();
// Mid-level tables
console.log(" Deleting mid-level tables (revisions, comments, stage assignments, deliverable stages)...");
await prisma.revision.deleteMany();
await prisma.comment.deleteMany();
await prisma.stageAssignment.deleteMany();
await prisma.deliverableStage.deleteMany();
// Parent tables
console.log(" Deleting parent tables (deliverables, projects)...");
await prisma.deliverable.deleteMany();
await prisma.project.deleteMany();
// Org-linked standalone tables
console.log(" Deleting org-linked standalone tables...");
await prisma.chatMessage.deleteMany();
await prisma.notification.deleteMany();
await prisma.searchLog.deleteMany();
await prisma.automationExecution.deleteMany();
await prisma.automationRule.deleteMany();
await prisma.invitation.deleteMany();
await prisma.customFieldDefinition.deleteMany();
await prisma.notificationRule.deleteMany();
await prisma.reviewSession.deleteMany();
// Auth tables
console.log(" Deleting auth tables (accounts, sessions, verification tokens)...");
await prisma.account.deleteMany();
await prisma.session.deleteMany();
await prisma.$executeRawUnsafe(`DELETE FROM verification_tokens`);
// User-linked tables
console.log(" Deleting user-linked tables (user skills, stage skill requirements, skills)...");
await prisma.userSkill.deleteMany();
await prisma.stageSkillRequirement.deleteMany();
await prisma.skill.deleteMany();
// Pipeline v2 (org-scoped dynamic templates)
console.log(" Deleting pipeline v2 tables (stage deps v2, stage definitions, pipeline templates)...");
await prisma.pipelineStageDependencyV2.deleteMany();
await prisma.pipelineStageDefinition.deleteMany();
await prisma.pipelineTemplate.deleteMany();
// Pipeline v1 (global templates)
console.log(" Deleting pipeline v1 tables (stage dependencies, stage templates)...");
await prisma.pipelineStageDependency.deleteMany();
await prisma.pipelineStageTemplate.deleteMany();
// RBAC
console.log(" Deleting RBAC permissions...");
await prisma.orgRolePermission.deleteMany();
// Users and org
console.log(" Deleting users and organization...");
await prisma.user.deleteMany();
await prisma.organization.deleteMany();
console.log("\n✅ Purge complete — all tables empty.");
}
// ─── Reseed ────────────────────────────────────────────
function reseed() {
console.log("\n🌱 Re-seeding structural data via seed.ts...\n");
try {
execSync("npx tsx prisma/seed.ts", {
cwd: process.cwd(),
stdio: "inherit",
env: process.env,
});
console.log("\n✅ Reseed complete.");
} catch (error) {
console.error("\n❌ Reseed FAILED. The database may be empty.");
console.error(" To recover, manually run: npm run db:seed-team");
throw error;
}
}
// ─── Main ──────────────────────────────────────────────
async function main() {
const flags = parseArgs();
if (!flags.orgName || !flags.domain) {
console.error("Error: --org-name and --domain are required.\n");
printUsage();
process.exit(1);
}
console.log("╔══════════════════════════════════════════════╗");
console.log("║ HP CG Production Tracker ║");
console.log("║ Clean Slate Toolkit ║");
console.log("╚══════════════════════════════════════════════╝");
console.log(`\n Target org: "${flags.orgName}" (${flags.domain})`);
console.log(` Mode: ${flags.confirm ? "🔴 EXECUTE" : "🟢 DRY RUN"}`);
// Count current records
const preCounts = await getRecordCounts();
printCounts(preCounts, flags.confirm ? "Records to purge" : "Would purge");
if (!flags.confirm) {
console.log("\n─── DRY RUN ───");
console.log("No changes made. Run with --confirm to execute.\n");
return;
}
// Execute purge
console.log("\n═══════════════════════════════════════════════");
console.log(" EXECUTING CLEAN SLATE");
console.log("═══════════════════════════════════════════════");
await purgeAllData();
// Need to disconnect before reseed (reseed creates its own Prisma client)
await prisma.$disconnect();
reseed();
// Reconnect after reseed
const postAdapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! });
const postPrisma = new PrismaClient({ adapter: postAdapter });
try {
// Rename org
const org = await postPrisma.organization.findFirst();
if (!org) {
throw new Error("No organization found after reseed.");
}
await postPrisma.organization.update({
where: { id: org.id },
data: { name: flags.orgName as string, domain: flags.domain as string },
});
console.log(`\n🏢 Organization updated: "${flags.orgName}" (${flags.domain})`);
// Post-execution summary
const postCountsObj: Record<string, number> = {};
postCountsObj["organizations"] = await postPrisma.organization.count();
postCountsObj["users"] = await postPrisma.user.count();
postCountsObj["pipeline_stage_templates"] = await postPrisma.pipelineStageTemplate.count();
postCountsObj["pipeline_templates"] = await postPrisma.pipelineTemplate.count();
postCountsObj["pipeline_stage_definitions"] = await postPrisma.pipelineStageDefinition.count();
postCountsObj["skills"] = await postPrisma.skill.count();
postCountsObj["user_skills"] = await postPrisma.userSkill.count();
postCountsObj["org_role_permissions"] = await postPrisma.orgRolePermission.count();
postCountsObj["projects"] = await postPrisma.project.count();
postCountsObj["deliverables"] = await postPrisma.deliverable.count();
printCounts(postCountsObj, "Re-seeded records");
console.log("\n═══════════════════════════════════════════════");
console.log(" ✅ CLEAN SLATE COMPLETE");
console.log("═══════════════════════════════════════════════");
console.log(`\n Organization: "${flags.orgName}" (${flags.domain})`);
console.log(" Database is ready for production use.\n");
} finally {
await postPrisma.$disconnect();
}
}
main()
.catch((e) => {
console.error("\n❌ Clean slate failed:", e.message || e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});