from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.dependencies import get_current_user, get_db from app.models.device import Device from app.models.object import Object from app.models.tag import Tag from app.models.user import User from app.schemas.object import ObjectCreate, ObjectRead, ObjectUpdate, SyncResult from app.services import crypto from app.services.object_db import sync_from_object_db router = APIRouter(prefix="/api/v1/objects", tags=["objects"]) def _apply_credentials(obj: Object, db_password: str | None, ssh_password: str | None) -> None: if db_password is not None: obj.db_pass_enc = crypto.encrypt(db_password) if ssh_password is not None: obj.ssh_pass_enc = crypto.encrypt(ssh_password) async def _load_object(db: AsyncSession, obj_id: int) -> Object: result = await db.execute( select(Object).options(selectinload(Object.tags)).where(Object.id == obj_id) ) obj = result.scalar_one_or_none() if obj is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Object not found") return obj @router.get("", response_model=list[ObjectRead]) async def list_objects( is_active: bool | None = None, tag_id: int | None = None, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): query = select(Object).options(selectinload(Object.tags)) if is_active is not None: query = query.where(Object.is_active == is_active) if tag_id is not None: query = query.where(Object.tags.any(Tag.id == tag_id)) query = query.order_by(Object.name) result = await db.execute(query) return result.scalars().all() @router.post("", response_model=ObjectRead, status_code=status.HTTP_201_CREATED) async def create_object( body: ObjectCreate, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): data = body.model_dump(exclude={"db_password", "ssh_password", "tag_ids"}) obj = Object(**data) _apply_credentials(obj, body.db_password, body.ssh_password) if body.tag_ids: tags_result = await db.execute(select(Tag).where(Tag.id.in_(body.tag_ids))) obj.tags = list(tags_result.scalars().all()) db.add(obj) await db.flush() await db.refresh(obj, ["tags"]) return obj @router.get("/{obj_id}", response_model=ObjectRead) async def get_object( obj_id: int, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): return await _load_object(db, obj_id) @router.patch("/{obj_id}", response_model=ObjectRead) async def update_object( obj_id: int, body: ObjectUpdate, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): obj = await _load_object(db, obj_id) data = body.model_dump(exclude_none=True, exclude={"db_password", "ssh_password", "tag_ids"}) for field, value in data.items(): setattr(obj, field, value) _apply_credentials(obj, body.db_password, body.ssh_password) if body.tag_ids is not None: tags_result = await db.execute(select(Tag).where(Tag.id.in_(body.tag_ids))) obj.tags = list(tags_result.scalars().all()) await db.flush() await db.refresh(obj, ["tags"]) return obj @router.delete("/{obj_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_object( obj_id: int, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): obj = await _load_object(db, obj_id) # Check for devices (RESTRICT constraint would catch this, but give a clear message) count_result = await db.execute( select(Device.id).where(Device.object_id == obj_id).limit(1) ) if count_result.scalar_one_or_none() is not None: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Cannot delete object with existing devices. Remove all devices first.", ) await db.delete(obj) @router.post("/{obj_id}/sync", response_model=SyncResult) async def sync_devices( obj_id: int, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): obj = await _load_object(db, obj_id) result = await sync_from_object_db(db, obj) return SyncResult( created=result.created, updated=result.updated, skipped=result.skipped, errors=result.errors, )