From bd0a681ee41c224e7ad578b7297c25d4a9cb2993 Mon Sep 17 00:00:00 2001 From: Manoj Date: Tue, 21 Oct 2025 18:17:45 +0530 Subject: [PATCH] refactor: add cascade delete for user and workspace relations to simplify deletion logic --- apps/webapp/app/models/user.server.ts | 121 ++---------------- .../migration.sql | 83 ++++++++++++ .../migration.sql | 23 ++++ .../migration.sql | 5 + packages/database/prisma/schema.prisma | 40 +++--- 5 files changed, 142 insertions(+), 130 deletions(-) create mode 100644 packages/database/prisma/migrations/20251021123731_add_delete_on_cascade/migration.sql create mode 100644 packages/database/prisma/migrations/20251021124118_add_delete_cascase_on_user/migration.sql create mode 100644 packages/database/prisma/migrations/20251021124452_add_delete_cascade_on_workspace/migration.sql diff --git a/apps/webapp/app/models/user.server.ts b/apps/webapp/app/models/user.server.ts index 834c20e..3bd292c 100644 --- a/apps/webapp/app/models/user.server.ts +++ b/apps/webapp/app/models/user.server.ts @@ -241,12 +241,9 @@ export async function grantUserCloudAccess({ } export async function deleteUser(id: User["id"]) { - // Get user's workspace + // Get user to verify they exist const user = await prisma.user.findUnique({ where: { id }, - include: { - Workspace: true, - }, }); if (!user) { @@ -270,112 +267,16 @@ export async function deleteUser(id: User["id"]) { // Continue with deletion even if graph cleanup fails } - // If workspace exists, delete all workspace-related data - // Most models DON'T have onDelete: Cascade, so we must delete manually - if (user.Workspace) { - const workspaceId = user.Workspace.id; - - // 1. Delete nested conversation data - await prisma.conversationExecutionStep.deleteMany({ - where: { - conversationHistory: { - conversation: { workspaceId }, - }, - }, - }); - - await prisma.conversationHistory.deleteMany({ - where: { - conversation: { workspaceId }, - }, - }); - - await prisma.conversation.deleteMany({ - where: { workspaceId }, - }); - - // 2. Delete space patterns (nested under Space) - await prisma.spacePattern.deleteMany({ - where: { - space: { workspaceId }, - }, - }); - - await prisma.space.deleteMany({ - where: { workspaceId }, - }); - - // 3. Delete webhook delivery logs (nested under WebhookConfiguration) - await prisma.webhookDeliveryLog.deleteMany({ - where: { - webhookConfiguration: { workspaceId }, - }, - }); - - await prisma.webhookConfiguration.deleteMany({ - where: { workspaceId }, - }); - - // 4. Delete ingestion data - await prisma.ingestionQueue.deleteMany({ - where: { workspaceId }, - }); - - await prisma.ingestionRule.deleteMany({ - where: { workspaceId }, - }); - - // 5. Delete integration accounts - await prisma.integrationAccount.deleteMany({ - where: { workspaceId }, - }); - - await prisma.integrationDefinitionV2.deleteMany({ - where: { workspaceId }, - }); - - // 6. Delete recall logs - await prisma.recallLog.deleteMany({ - where: { workspaceId }, - }); - - // 7. Delete activities - await prisma.activity.deleteMany({ - where: { workspaceId }, - }); - - // 8. Delete MCP sessions - await prisma.mCPSession.deleteMany({ - where: { workspaceId }, - }); - - // 9. Delete billing history (nested under Subscription) - await prisma.billingHistory.deleteMany({ - where: { - subscription: { workspaceId }, - }, - }); - - await prisma.subscription.deleteMany({ - where: { workspaceId }, - }); - - // 10. Delete the workspace (this will CASCADE delete OAuth models automatically) - await prisma.workspace.delete({ - where: { id: workspaceId }, - }); - } - - // Delete user-specific data - await prisma.personalAccessToken.deleteMany({ - where: { userId: id }, - }); - - await prisma.userUsage.deleteMany({ - where: { userId: id }, - }); - - // Finally, delete the user + // Delete the user - cascade deletes will handle all related data: + // - Workspace (and all workspace-related data via cascade) + // - PersonalAccessToken + // - UserUsage + // - Conversations, ConversationHistory + // - IngestionRules + // - IntegrationAccounts + // - RecallLogs + // - WebhookConfigurations + // - All OAuth models return prisma.user.delete({ where: { id }, }); diff --git a/packages/database/prisma/migrations/20251021123731_add_delete_on_cascade/migration.sql b/packages/database/prisma/migrations/20251021123731_add_delete_on_cascade/migration.sql new file mode 100644 index 0000000..4916421 --- /dev/null +++ b/packages/database/prisma/migrations/20251021123731_add_delete_on_cascade/migration.sql @@ -0,0 +1,83 @@ +-- DropForeignKey +ALTER TABLE "Activity" DROP CONSTRAINT "Activity_workspaceId_fkey"; + +-- DropForeignKey +ALTER TABLE "Conversation" DROP CONSTRAINT "Conversation_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "Conversation" DROP CONSTRAINT "Conversation_workspaceId_fkey"; + +-- DropForeignKey +ALTER TABLE "IngestionRule" DROP CONSTRAINT "IngestionRule_workspaceId_fkey"; + +-- DropForeignKey +ALTER TABLE "IntegrationAccount" DROP CONSTRAINT "IntegrationAccount_integratedById_fkey"; + +-- DropForeignKey +ALTER TABLE "IntegrationAccount" DROP CONSTRAINT "IntegrationAccount_workspaceId_fkey"; + +-- DropForeignKey +ALTER TABLE "IntegrationDefinitionV2" DROP CONSTRAINT "IntegrationDefinitionV2_workspaceId_fkey"; + +-- DropForeignKey +ALTER TABLE "MCPSession" DROP CONSTRAINT "MCPSession_workspaceId_fkey"; + +-- DropForeignKey +ALTER TABLE "OAuthClientInstallation" DROP CONSTRAINT "OAuthClientInstallation_installedById_fkey"; + +-- DropForeignKey +ALTER TABLE "RecallLog" DROP CONSTRAINT "RecallLog_workspaceId_fkey"; + +-- DropForeignKey +ALTER TABLE "Space" DROP CONSTRAINT "Space_workspaceId_fkey"; + +-- DropForeignKey +ALTER TABLE "Subscription" DROP CONSTRAINT "Subscription_workspaceId_fkey"; + +-- DropForeignKey +ALTER TABLE "WebhookConfiguration" DROP CONSTRAINT "WebhookConfiguration_workspaceId_fkey"; + +-- DropForeignKey +ALTER TABLE "Workspace" DROP CONSTRAINT "Workspace_userId_fkey"; + +-- AddForeignKey +ALTER TABLE "Activity" ADD CONSTRAINT "Activity_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Conversation" ADD CONSTRAINT "Conversation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Conversation" ADD CONSTRAINT "Conversation_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "IngestionRule" ADD CONSTRAINT "IngestionRule_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "IntegrationAccount" ADD CONSTRAINT "IntegrationAccount_integratedById_fkey" FOREIGN KEY ("integratedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "IntegrationAccount" ADD CONSTRAINT "IntegrationAccount_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "IntegrationDefinitionV2" ADD CONSTRAINT "IntegrationDefinitionV2_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MCPSession" ADD CONSTRAINT "MCPSession_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OAuthClientInstallation" ADD CONSTRAINT "OAuthClientInstallation_installedById_fkey" FOREIGN KEY ("installedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RecallLog" ADD CONSTRAINT "RecallLog_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Space" ADD CONSTRAINT "Space_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "WebhookConfiguration" ADD CONSTRAINT "WebhookConfiguration_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Workspace" ADD CONSTRAINT "Workspace_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/database/prisma/migrations/20251021124118_add_delete_cascase_on_user/migration.sql b/packages/database/prisma/migrations/20251021124118_add_delete_cascase_on_user/migration.sql new file mode 100644 index 0000000..13e677a --- /dev/null +++ b/packages/database/prisma/migrations/20251021124118_add_delete_cascase_on_user/migration.sql @@ -0,0 +1,23 @@ +-- DropForeignKey +ALTER TABLE "ConversationHistory" DROP CONSTRAINT "ConversationHistory_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "IngestionRule" DROP CONSTRAINT "IngestionRule_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "RecallLog" DROP CONSTRAINT "RecallLog_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "UserUsage" DROP CONSTRAINT "UserUsage_userId_fkey"; + +-- AddForeignKey +ALTER TABLE "ConversationHistory" ADD CONSTRAINT "ConversationHistory_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "IngestionRule" ADD CONSTRAINT "IngestionRule_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RecallLog" ADD CONSTRAINT "RecallLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserUsage" ADD CONSTRAINT "UserUsage_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/database/prisma/migrations/20251021124452_add_delete_cascade_on_workspace/migration.sql b/packages/database/prisma/migrations/20251021124452_add_delete_cascade_on_workspace/migration.sql new file mode 100644 index 0000000..1838296 --- /dev/null +++ b/packages/database/prisma/migrations/20251021124452_add_delete_cascade_on_workspace/migration.sql @@ -0,0 +1,5 @@ +-- DropForeignKey +ALTER TABLE "IngestionQueue" DROP CONSTRAINT "IngestionQueue_workspaceId_fkey"; + +-- AddForeignKey +ALTER TABLE "IngestionQueue" ADD CONSTRAINT "IngestionQueue_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index 8685b5f..6b82d01 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -25,7 +25,7 @@ model Activity { rejectionReason String? - workspace Workspace @relation(fields: [workspaceId], references: [id]) + workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) workspaceId String WebhookDeliveryLog WebhookDeliveryLog[] @@ -55,10 +55,10 @@ model Conversation { unread Boolean @default(false) title String? - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) userId String - workspace Workspace? @relation(fields: [workspaceId], references: [id]) + workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade) workspaceId String? status String @default("pending") // Can be "pending", "running", "completed", "failed", "need_attention" @@ -102,7 +102,7 @@ model ConversationHistory { context Json? thoughts Json? - user User? @relation(fields: [userId], references: [id]) + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) userId String? conversation Conversation @relation(fields: [conversationId], references: [id]) @@ -122,7 +122,7 @@ model IngestionQueue { type String? workspaceId String - workspace Workspace @relation(fields: [workspaceId], references: [id]) + workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) activity Activity? @relation(fields: [activityId], references: [id]) activityId String? @@ -148,10 +148,10 @@ model IngestionRule { source String // Source/integration this rule applies to (mandatory) isActive Boolean @default(true) // Enable/disable rule (mandatory) - workspace Workspace @relation(fields: [workspaceId], references: [id]) + workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) workspaceId String - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) userId String } @@ -166,11 +166,11 @@ model IntegrationAccount { settings Json? isActive Boolean @default(true) - integratedBy User @relation(references: [id], fields: [integratedById]) + integratedBy User @relation(references: [id], fields: [integratedById], onDelete: Cascade) integratedById String integrationDefinition IntegrationDefinitionV2 @relation(references: [id], fields: [integrationDefinitionId]) integrationDefinitionId String - workspace Workspace @relation(references: [id], fields: [workspaceId]) + workspace Workspace @relation(references: [id], fields: [workspaceId], onDelete: Cascade) workspaceId String Activity Activity[] oauthIntegrationGrants OAuthIntegrationGrant[] @@ -193,7 +193,7 @@ model IntegrationDefinitionV2 { version String? url String? - workspace Workspace? @relation(references: [id], fields: [workspaceId]) + workspace Workspace? @relation(references: [id], fields: [workspaceId], onDelete: Cascade) workspaceId String? IntegrationAccount IntegrationAccount[] @@ -213,7 +213,7 @@ model MCPSession { source String integrations String[] - workspace Workspace? @relation(references: [id], fields: [workspaceId]) + workspace Workspace? @relation(references: [id], fields: [workspaceId], onDelete: Cascade) workspaceId String? createdAt DateTime @default(now()) @@ -304,7 +304,7 @@ model OAuthClient { workspaceId String? // Created by user (for audit trail) - createdBy User? @relation(fields: [createdById], references: [id]) + createdBy User? @relation(fields: [createdById], references: [id], onDelete: SetNull) createdById String? // Relations @@ -330,7 +330,7 @@ model OAuthClientInstallation { workspaceId String // Installation metadata - installedBy User @relation(fields: [installedById], references: [id]) + installedBy User @relation(fields: [installedById], references: [id], onDelete: Cascade) installedById String installedAt DateTime @default(now()) uninstalledAt DateTime? @@ -454,10 +454,10 @@ model RecallLog { responseTimeMs Int? // Response time in milliseconds // Relations - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) userId String - workspace Workspace? @relation(fields: [workspaceId], references: [id]) + workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade) workspaceId String? conversation Conversation? @relation(fields: [conversationId], references: [id]) @@ -485,7 +485,7 @@ model Space { contextCountAtLastTrigger Int? // Context count when pattern was last triggered // Relations - workspace Workspace @relation(fields: [workspaceId], references: [id]) + workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) workspaceId String createdAt DateTime @default(now()) @@ -584,7 +584,7 @@ model UserUsage { searchCreditsUsed Int @default(0) chatCreditsUsed Int @default(0) - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) userId String @unique } @@ -596,7 +596,7 @@ model WebhookConfiguration { eventTypes String[] // List of event types this webhook is interested in, e.g. ["activity.created"] user User? @relation(fields: [userId], references: [id]) userId String? - workspace Workspace? @relation(fields: [workspaceId], references: [id]) + workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade) workspaceId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -652,7 +652,7 @@ model Subscription { overageAmount Float @default(0) // Relations - workspace Workspace @relation(fields: [workspaceId], references: [id]) + workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) workspaceId String @unique BillingHistory BillingHistory[] } @@ -697,7 +697,7 @@ model Workspace { integrations String[] userId String? @unique - user User? @relation(fields: [userId], references: [id]) + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) IngestionQueue IngestionQueue[] IntegrationAccount IntegrationAccount[] IntegrationDefinitionV2 IntegrationDefinitionV2[]