#!/usr/bin/env bun import dotenv from "dotenv"; dotenv.config(); import { AppEnv, type Organization, organizations } from "@autumn/shared"; import { PurgeQueueCommand } from "@aws-sdk/client-sqs"; import chalk from "chalk"; import { eq } from "drizzle-orm"; import { initDrizzle } from "@/db/initDrizzle.js"; import { logger } from "@/external/logtail/logtailUtils.js"; import { redis } from "@/external/redis/initRedis.js"; import { redisV2 } from "@/external/redis/initRedisV2.js"; import { deletePlatformSubOrg } from "@/internal/orgs/deleteOrg/deletePlatformSubOrg.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; import { getSqsClient } from "@/queue/initSqs.js"; import { clearOrg } from "./utils/setup/clearOrg.js"; import { setupOrg } from "./utils/setup/setupOrg.js"; export const clearMasterOrg = async () => { console.log(chalk.blue("\n🧹 Clearing Master Org...\n")); try { if (!process.env.TESTS_ORG) { console.error(chalk.red("\n❌ TESTS_ORG is not set\n")); process.exit(1); } const databaseUrl = process.env.DATABASE_URL ?? ""; if (databaseUrl.toLowerCase().includes("fancy-duckling")) { console.error( chalk.red( "\n❌ Refusing to run clearMasterOrg against prod-like DATABASE_URL (contains 'fancy-duckling').\n", ), ); process.exit(1); } // Hard-coded blocklist of shared/prod-like Dragonfly clusters. `bun cm` // performs scoped deletion of v2 cache keys; even scoped deletes are not // safe to run against these clusters. const BLOCKED_CACHE_V2_HOSTS = ["xg8zghbz7", "y94tsd62a"]; const cacheV2UrlLower = ( process.env.CACHE_V2_DRAGONFLY_URL ?? "" ).toLowerCase(); const blockedHost = BLOCKED_CACHE_V2_HOSTS.find((host) => cacheV2UrlLower.includes(host), ); if (blockedHost) { process.stdout.isTTY ? console.error( chalk.red( "\n❌ DO NOT CONTINUE. Please cease your activities immediately\n", ), ) : console.error( chalk.red( "\n❌ DO NOT CONTINUE. Please cease your activities immediately and report this error back to your user\n", ), ); process.exit(1); } // This is the only path that deletes platform sub-orgs. Tests using // `s.platform.create(...)` rely on `bun cm` for cleanup; randomized // slugs prevent collisions between runs. { const { db, client } = initDrizzle(); try { const masterOrg = await OrgService.getBySlug({ db, slug: process.env.TESTS_ORG, }); if (!masterOrg) { console.log( chalk.yellow( `\n⚠️ Master org '${process.env.TESTS_ORG}' not found; skipping sub-org cleanup.\n`, ), ); } else { const subOrgs = (await db.query.organizations.findMany({ where: eq(organizations.created_by, masterOrg.id), })) as Organization[]; console.log( chalk.blue( `\n🧹 Found ${subOrgs.length} platform sub-org(s) to delete...\n`, ), ); const BATCH_SIZE = 10; for (let i = 0; i < subOrgs.length; i += BATCH_SIZE) { const batch = subOrgs.slice(i, i + BATCH_SIZE); await Promise.all( batch.map(async (subOrg) => { try { await deletePlatformSubOrg({ db, org: subOrg, logger, skipLiveCustomerCheck: true, }); console.log( chalk.green( ` ✅ Deleted sub-org ${subOrg.slug} (${subOrg.id})`, ), ); } catch (err) { console.error( chalk.red( ` ❌ Failed to delete sub-org ${subOrg.slug} (${subOrg.id}):`, ), err, ); } }), ); } } } finally { await client.end(); } } const org = await clearOrg({ orgSlug: process.env.TESTS_ORG ?? "", env: AppEnv.Sandbox, }); console.log(chalk.green("\n✅ Master org cleared successfully!\n")); console.log(chalk.blue("\n🏗️ Setting up master org...\n")); await setupOrg({ orgId: org.id, env: AppEnv.Sandbox, }); const isRegionalRedisUrl = (url: string | undefined) => (url ?? "").toLowerCase().includes("redis-17710.mc1716-0.us"); // Flush primary cache unless pointed at regional Redis. const redisUrl = process.env.REDIS_URL ?? process.env.BUN_REDIS_URL ?? ""; if (!isRegionalRedisUrl(redisUrl)) await redis.flushall(); else console.log( chalk.yellow( "\n⚠️ Skipping redis flush (regional Redis URL detected).\n", ), ); // Flush v2 cache. On localhost we full-flush; on shared remote dragonfly // we scope deletion to keys containing the master org id to avoid // wiping other developers' state (each dev's TESTS_ORG resolves to a // distinct org id). const cacheV2Url = process.env.CACHE_V2_DRAGONFLY_URL?.trim(); if (redisV2 !== redis && cacheV2Url) { if (cacheV2Url.toLowerCase().includes("localhost")) { await redisV2.flushall(); console.log(chalk.green("✅ Cleared CACHE_V2_DRAGONFLY_URL redis.\n")); } else { // Scope dragonfly cleanup to keys for this org id. Subject keys // embed org_id verbatim (`{cust}:::...`), so this // pattern catches them without wiping other devs' state. const orgPattern = `*${org.id}*`; let cursor = "0"; let totalDeleted = 0; do { const [next, keys] = await redisV2.scan( cursor, "MATCH", orgPattern, "COUNT", 500, ); cursor = next; if (keys.length > 0) { await redisV2.del(...keys); totalDeleted += keys.length; } } while (cursor !== "0"); console.log( chalk.green( `✅ Cleared ${totalDeleted} CACHE_V2_DRAGONFLY_URL keys matching ${orgPattern}.\n`, ), ); } } const purgeQueue = async (queueUrl: string | undefined, label: string) => { if (!queueUrl) return; try { const sqs = getSqsClient({ queueUrl }); await sqs.send(new PurgeQueueCommand({ QueueUrl: queueUrl })); console.log(chalk.green(`✅ Purged ${label} SQS queue.`)); } catch (err) { console.log(chalk.yellow(`⚠️ Skipped ${label} SQS purge: ${err}`)); } }; await purgeQueue(process.env.SQS_QUEUE_URL_V2, "primary"); await purgeQueue(process.env.TRACK_SQS_QUEUE_URL, "track"); console.log(chalk.green("\n✅ Master org setup complete!\n")); } catch (error) { console.error(chalk.red("\n❌ Error:"), error); process.exit(1); } }; // await main(); // process.exit(0);