feat: implement organization deletion with cascading cleanup of associated resources

This commit is contained in:
Abhimanyu Saharan
2026-02-09 00:22:37 +05:30
parent fd01320f1b
commit b9d2603fde
3 changed files with 250 additions and 18 deletions

View File

@@ -5,7 +5,7 @@ from typing import Any, Sequence
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import delete, func
from sqlalchemy import delete, func, update
from sqlmodel import col, select
from sqlmodel.ext.asyncio.session import AsyncSession
@@ -14,13 +14,25 @@ from app.core.auth import AuthContext, get_auth_context
from app.core.time import utcnow
from app.db.pagination import paginate
from app.db.session import get_session
from app.models.activity_events import ActivityEvent
from app.models.agents import Agent
from app.models.approvals import Approval
from app.models.board_group_memory import BoardGroupMemory
from app.models.board_groups import BoardGroup
from app.models.board_memory import BoardMemory
from app.models.board_onboarding import BoardOnboardingSession
from app.models.boards import Board
from app.models.gateways import Gateway
from app.models.organization_board_access import OrganizationBoardAccess
from app.models.organization_invite_board_access import OrganizationInviteBoardAccess
from app.models.organization_invites import OrganizationInvite
from app.models.organization_members import OrganizationMember
from app.models.organizations import Organization
from app.models.task_dependencies import TaskDependency
from app.models.task_fingerprints import TaskFingerprint
from app.models.tasks import Task
from app.models.users import User
from app.schemas.common import OkResponse
from app.schemas.organizations import (
OrganizationActiveUpdate,
OrganizationBoardAccessRead,
@@ -153,6 +165,82 @@ async def get_my_org(ctx: OrganizationContext = Depends(require_org_member)) ->
return OrganizationRead.model_validate(ctx.organization, from_attributes=True)
@router.delete("/me", response_model=OkResponse)
async def delete_my_org(
session: AsyncSession = Depends(get_session),
ctx: OrganizationContext = Depends(require_org_admin),
) -> OkResponse:
if ctx.member.role != "owner":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only organization owners can delete organizations",
)
org_id = ctx.organization.id
board_ids = select(Board.id).where(col(Board.organization_id) == org_id)
task_ids = select(Task.id).where(col(Task.board_id).in_(board_ids))
agent_ids = select(Agent.id).where(col(Agent.board_id).in_(board_ids))
member_ids = select(OrganizationMember.id).where(
col(OrganizationMember.organization_id) == org_id
)
invite_ids = select(OrganizationInvite.id).where(
col(OrganizationInvite.organization_id) == org_id
)
group_ids = select(BoardGroup.id).where(col(BoardGroup.organization_id) == org_id)
await session.execute(delete(ActivityEvent).where(col(ActivityEvent.task_id).in_(task_ids)))
await session.execute(delete(ActivityEvent).where(col(ActivityEvent.agent_id).in_(agent_ids)))
await session.execute(delete(TaskDependency).where(col(TaskDependency.board_id).in_(board_ids)))
await session.execute(
delete(TaskFingerprint).where(col(TaskFingerprint.board_id).in_(board_ids))
)
await session.execute(delete(Approval).where(col(Approval.board_id).in_(board_ids)))
await session.execute(delete(BoardMemory).where(col(BoardMemory.board_id).in_(board_ids)))
await session.execute(
delete(BoardOnboardingSession).where(col(BoardOnboardingSession.board_id).in_(board_ids))
)
await session.execute(
delete(OrganizationBoardAccess).where(col(OrganizationBoardAccess.board_id).in_(board_ids))
)
await session.execute(
delete(OrganizationInviteBoardAccess).where(
col(OrganizationInviteBoardAccess.board_id).in_(board_ids)
)
)
await session.execute(
delete(OrganizationBoardAccess).where(
col(OrganizationBoardAccess.organization_member_id).in_(member_ids)
)
)
await session.execute(
delete(OrganizationInviteBoardAccess).where(
col(OrganizationInviteBoardAccess.organization_invite_id).in_(invite_ids)
)
)
await session.execute(delete(Task).where(col(Task.board_id).in_(board_ids)))
await session.execute(delete(Agent).where(col(Agent.board_id).in_(board_ids)))
await session.execute(delete(Board).where(col(Board.organization_id) == org_id))
await session.execute(
delete(BoardGroupMemory).where(col(BoardGroupMemory.board_group_id).in_(group_ids))
)
await session.execute(delete(BoardGroup).where(col(BoardGroup.organization_id) == org_id))
await session.execute(delete(Gateway).where(col(Gateway.organization_id) == org_id))
await session.execute(
delete(OrganizationInvite).where(col(OrganizationInvite.organization_id) == org_id)
)
await session.execute(
delete(OrganizationMember).where(col(OrganizationMember.organization_id) == org_id)
)
await session.execute(
update(User)
.where(col(User.active_organization_id) == org_id)
.values(active_organization_id=None)
)
await session.execute(delete(Organization).where(col(Organization.id) == org_id))
await session.commit()
return OkResponse()
@router.get("/me/member", response_model=OrganizationMemberRead)
async def get_my_membership(
session: AsyncSession = Depends(get_session),