"""Entity Links plugin routes — link files to companies/contacts, reverse links.""" from __future__ import annotations import uuid from fastapi import APIRouter, Body, Depends, HTTPException, Response, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.db import get_db from app.deps import get_current_user from app.plugins.builtins.entity_links.models import EntityLink from app.plugins.builtins.entity_links.schemas import EntityLinkRequest router = APIRouter(prefix="/api/v1/dms", tags=["entity-links"]) company_router = APIRouter(prefix="/api/v1/companies", tags=["entity-links"]) contact_router = APIRouter(prefix="/api/v1/contacts", tags=["entity-links"]) VALID_ENTITY_TYPES = {"company", "contact"} def _parse_uuid(val: str, field: str) -> uuid.UUID: try: return uuid.UUID(val) except (ValueError, TypeError): raise HTTPException( 400, detail={"detail": f"Invalid {field}", "code": "invalid_id"} ) from None @router.post("/files/{file_id}/link") async def link_file_to_entity( file_id: str, body: EntityLinkRequest, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Link a file to a company or contact.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) fid = _parse_uuid(file_id, "file_id") entity_id = _parse_uuid(body.entity_id, "entity_id") if body.entity_type not in VALID_ENTITY_TYPES: raise HTTPException( 400, detail={"detail": "Invalid entity_type", "code": "invalid_entity_type"} ) # Check if link already exists existing = await db.execute( select(EntityLink).where( EntityLink.tenant_id == tenant_id, EntityLink.file_id == fid, EntityLink.entity_type == body.entity_type, EntityLink.entity_id == entity_id, ) ) existing_link = existing.scalar_one_or_none() if existing_link is not None: return { "id": str(existing_link.id), "file_id": str(fid), "entity_type": body.entity_type, "entity_id": str(entity_id), "already_linked": True, } link = EntityLink( tenant_id=tenant_id, file_id=fid, entity_type=body.entity_type, entity_id=entity_id, created_by=user_id, ) db.add(link) await db.flush() return { "id": str(link.id), "file_id": str(link.file_id), "entity_type": link.entity_type, "entity_id": str(link.entity_id), "already_linked": False, } @router.delete("/files/{file_id}/link", status_code=status.HTTP_204_NO_CONTENT) async def unlink_file_from_entity( file_id: str, body: EntityLinkRequest = Body(...), db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Remove a link between a file and an entity.""" tenant_id = uuid.UUID(current_user["tenant_id"]) fid = _parse_uuid(file_id, "file_id") entity_id = _parse_uuid(body.entity_id, "entity_id") result = await db.execute( select(EntityLink).where( EntityLink.tenant_id == tenant_id, EntityLink.file_id == fid, EntityLink.entity_type == body.entity_type, EntityLink.entity_id == entity_id, ) ) link = result.scalar_one_or_none() if link is None: raise HTTPException(404, detail={"detail": "Link not found", "code": "not_found"}) await db.delete(link) return Response(status_code=status.HTTP_204_NO_CONTENT) @router.get("/files/{file_id}/links") async def list_file_links( file_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """List all entities linked to a file.""" tenant_id = uuid.UUID(current_user["tenant_id"]) fid = _parse_uuid(file_id, "file_id") result = await db.execute( select(EntityLink).where( EntityLink.tenant_id == tenant_id, EntityLink.file_id == fid, ) ) links = result.scalars().all() return [ { "id": str(link.id), "file_id": str(link.file_id), "entity_type": link.entity_type, "entity_id": str(link.entity_id), } for link in links ] @company_router.get("/{company_id}/files") async def list_company_files( company_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """List all files linked to a company (reverse link).""" tenant_id = uuid.UUID(current_user["tenant_id"]) cid = _parse_uuid(company_id, "company_id") result = await db.execute( select(EntityLink).where( EntityLink.tenant_id == tenant_id, EntityLink.entity_type == "company", EntityLink.entity_id == cid, ) ) links = result.scalars().all() return [ { "id": str(link.id), "file_id": str(link.file_id), "entity_type": link.entity_type, "entity_id": str(link.entity_id), } for link in links ] @contact_router.get("/{contact_id}/files") async def list_contact_files( contact_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """List all files linked to a contact (reverse link).""" tenant_id = uuid.UUID(current_user["tenant_id"]) cid = _parse_uuid(contact_id, "contact_id") result = await db.execute( select(EntityLink).where( EntityLink.tenant_id == tenant_id, EntityLink.entity_type == "contact", EntityLink.entity_id == cid, ) ) links = result.scalars().all() return [ { "id": str(link.id), "file_id": str(link.file_id), "entity_type": link.entity_type, "entity_id": str(link.entity_id), } for link in links ]