Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/migrate-tenants.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,3 @@ jobs:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
DATABASE_SSL: ${{ secrets.DATABASE_SSL }}
run: bun run migrate:tenants

157 changes: 83 additions & 74 deletions scripts/migrate-all-tenants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import path from "node:path";
import { sql } from "drizzle-orm";
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import postgres from "postgres";
import * as schema from "../drizzle/schema";
import * as opsSchema from "../ops/drizzle/schema";

Expand All @@ -22,18 +23,20 @@ function sanitizeError(error: string, tenantId: string): string {
}

async function getOpsDatabase() {
return drizzle({
connection: {
url: `${process.env.DATABASE_URL}/manage`,
ssl: process.env.DATABASE_SSL === "true",
},
schema,
const client = postgres(`${process.env.DATABASE_URL}/manage`, {
ssl: process.env.DATABASE_SSL === "true" ? "require" : false,
max: 1,
});
}

async function getAllTenantIds(): Promise<string[]> {
const opsDb = await getOpsDatabase();
return {
db: drizzle({ client, schema: opsSchema }),
client,
};
}

async function getAllTenantIds(
opsDb: ReturnType<typeof drizzle<typeof opsSchema>>,
): Promise<string[]> {
const [users, organizations] = await Promise.all([
opsDb.select({ id: opsSchema.opsUser.id }).from(opsSchema.opsUser),
opsDb
Expand All @@ -49,22 +52,19 @@ async function getAllTenantIds(): Promise<string[]> {
return tenantIds;
}

async function migrateTenantDatabase(ownerId: string): Promise<{
async function migrateTenantDatabase(
ownerId: string,
opsDb: ReturnType<typeof drizzle<typeof opsSchema>>,
): Promise<{
success: boolean;
skipped?: boolean;
error?: string;
}> {
let tenantClient: ReturnType<typeof postgres> | null = null;

try {
const databaseName = getDatabaseName(ownerId);

const opsDb = drizzle({
connection: {
url: `${process.env.DATABASE_URL}/manage`,
ssl: process.env.DATABASE_SSL === "true",
},
schema: opsSchema,
});

const checkDb = await opsDb.execute(
sql`SELECT 1 FROM pg_database WHERE datname = ${databaseName}`,
);
Expand All @@ -73,14 +73,13 @@ async function migrateTenantDatabase(ownerId: string): Promise<{
return { success: true, skipped: true };
}

const tenantDb = drizzle({
connection: {
url: `${process.env.DATABASE_URL}/${databaseName}`,
ssl: process.env.DATABASE_SSL === "true",
},
schema,
tenantClient = postgres(`${process.env.DATABASE_URL}/${databaseName}`, {
ssl: process.env.DATABASE_SSL === "true" ? "require" : false,
max: 1,
});

const tenantDb = drizzle({ client: tenantClient, schema });

const migrationsFolder = path.resolve(process.cwd(), "drizzle");
await migrate(tenantDb, { migrationsFolder });

Expand All @@ -90,6 +89,10 @@ async function migrateTenantDatabase(ownerId: string): Promise<{
success: false,
error: error instanceof Error ? error.message : String(error),
};
} finally {
if (tenantClient) {
await tenantClient.end();
}
}
}

Expand All @@ -101,65 +104,71 @@ async function main() {
process.exit(1);
}

const tenantIds = await getAllTenantIds();
console.log(`Found ${tenantIds.length} tenant(s) to migrate\n`);

if (tenantIds.length === 0) {
console.log("No tenants found. Nothing to migrate.");
return;
}
const { db: opsDb, client: opsClient } = await getOpsDatabase();

let successCount = 0;
let skippedCount = 0;
let failureCount = 0;
const failures: Array<{ tenantId: string; error: string }> = [];

for (let i = 0; i < tenantIds.length; i++) {
const tenantId = tenantIds[i];
const maskedId = maskId(tenantId);
const progress = `[${i + 1}/${tenantIds.length}]`;
process.stdout.write(`${progress} Migrating ${maskedId}... `);
try {
const tenantIds = await getAllTenantIds(opsDb);
console.log(`Found ${tenantIds.length} tenant(s) to migrate\n`);

const result = await migrateTenantDatabase(tenantId);
if (tenantIds.length === 0) {
console.log("No tenants found. Nothing to migrate.");
return;
}

if (result.success) {
if (result.skipped) {
console.log("⊘ (no database)");
skippedCount++;
let successCount = 0;
let skippedCount = 0;
let failureCount = 0;
const failures: Array<{ tenantId: string; error: string }> = [];

for (let i = 0; i < tenantIds.length; i++) {
const tenantId = tenantIds[i];
const maskedId = maskId(tenantId);
const progress = `[${i + 1}/${tenantIds.length}]`;
process.stdout.write(`${progress} Migrating ${maskedId}... `);

const result = await migrateTenantDatabase(tenantId, opsDb);

if (result.success) {
if (result.skipped) {
console.log("⊘ (no database)");
skippedCount++;
} else {
console.log("✓");
successCount++;
}
} else {
console.log("✓");
successCount++;
console.log("✗");
const sanitizedError = sanitizeError(
result.error || "Unknown error",
tenantId,
);
console.log(` Error: ${sanitizedError}`);
failureCount++;
failures.push({ tenantId: maskedId, error: sanitizedError });
}
} else {
console.log("✗");
const sanitizedError = sanitizeError(
result.error || "Unknown error",
tenantId,
);
console.log(` Error: ${sanitizedError}`);
failureCount++;
failures.push({ tenantId: maskedId, error: sanitizedError });
}
}

console.log(`\n${"=".repeat(60)}`);
console.log("Migration Summary:");
console.log(` Total: ${tenantIds.length}`);
console.log(` Success: ${successCount}`);
console.log(` Skipped: ${skippedCount} (no database created yet)`);
console.log(` Failed: ${failureCount}`);

if (failures.length > 0) {
console.log("\nFailed migrations:");
for (const failure of failures) {
console.log(` - ${failure.tenantId}: ${failure.error}`);
console.log(`\n${"=".repeat(60)}`);
console.log("Migration Summary:");
console.log(` Total: ${tenantIds.length}`);
console.log(` Success: ${successCount}`);
console.log(` Skipped: ${skippedCount} (no database created yet)`);
console.log(` Failed: ${failureCount}`);

if (failures.length > 0) {
console.log("\nFailed migrations:");
for (const failure of failures) {
console.log(` - ${failure.tenantId}: ${failure.error}`);
}
console.log("\n✗ Some migrations failed!");
process.exit(1);
}
console.log("\n✗ Some migrations failed!");
process.exit(1);
}

console.log("\n✓ All tenant databases migrated successfully!");
process.exit(0);
console.log("\n✓ All tenant databases migrated successfully!");
process.exit(0);
} finally {
await opsClient.end();
}
}

main().catch((error) => {
Expand Down