| """ |
| API routes for database migration tracking and management. |
| """ |
|
|
| from datetime import datetime |
|
|
| import logfire |
| from fastapi import APIRouter, Header, HTTPException, Response |
| from pydantic import BaseModel |
|
|
| from ..config.version import ARCHON_VERSION |
| from ..services.migration_service import migration_service |
| from ..utils.etag_utils import check_etag, generate_etag |
|
|
|
|
| |
| class MigrationRecord(BaseModel): |
| """Represents an applied migration.""" |
|
|
| version: str |
| migration_name: str |
| applied_at: datetime |
| checksum: str | None = None |
|
|
|
|
| class PendingMigration(BaseModel): |
| """Represents a pending migration.""" |
|
|
| version: str |
| name: str |
| sql_content: str |
| file_path: str |
| checksum: str | None = None |
|
|
|
|
| class MigrationStatusResponse(BaseModel): |
| """Complete migration status response.""" |
|
|
| pending_migrations: list[PendingMigration] |
| applied_migrations: list[MigrationRecord] |
| has_pending: bool |
| bootstrap_required: bool |
| current_version: str |
| pending_count: int |
| applied_count: int |
|
|
|
|
| class MigrationHistoryResponse(BaseModel): |
| """Migration history response.""" |
|
|
| migrations: list[MigrationRecord] |
| total_count: int |
| current_version: str |
|
|
|
|
| |
| router = APIRouter(prefix="/api/migrations", tags=["migrations"]) |
|
|
|
|
| @router.get("/status", response_model=MigrationStatusResponse) |
| async def get_migration_status(response: Response, if_none_match: str | None = Header(None)): |
| """ |
| Get current migration status including pending and applied migrations. |
| |
| Returns comprehensive migration status with: |
| - List of pending migrations with SQL content |
| - List of applied migrations |
| - Bootstrap flag if migrations table doesn't exist |
| - Current version information |
| """ |
| try: |
| |
| status = await migration_service.get_migration_status() |
|
|
| |
| etag = generate_etag(status) |
|
|
| |
| if check_etag(if_none_match, etag): |
| |
| response.status_code = 304 |
| response.headers["ETag"] = f'"{etag}"' |
| response.headers["Cache-Control"] = "no-cache, must-revalidate" |
| return Response(status_code=304) |
| else: |
| |
| response.headers["ETag"] = f'"{etag}"' |
| response.headers["Cache-Control"] = "no-cache, must-revalidate" |
| return MigrationStatusResponse(**status) |
|
|
| except Exception as e: |
| logfire.error(f"Error getting migration status: {e}") |
| raise HTTPException(status_code=500, detail=f"Failed to get migration status: {str(e)}") from e |
|
|
|
|
| @router.get("/history", response_model=MigrationHistoryResponse) |
| async def get_migration_history(response: Response, if_none_match: str | None = Header(None)): |
| """ |
| Get history of applied migrations. |
| |
| Returns list of all applied migrations sorted by date. |
| """ |
| try: |
| |
| applied = await migration_service.get_applied_migrations() |
|
|
| |
| history = { |
| "migrations": [ |
| MigrationRecord( |
| version=str(m.version), |
| migration_name=str(m.migration_name), |
| applied_at=m.applied_at if isinstance(m.applied_at, datetime) else datetime.now(), |
| checksum=m.checksum, |
| ) |
| for m in applied |
| ], |
| "total_count": len(applied), |
| "current_version": ARCHON_VERSION, |
| } |
|
|
| |
| etag = generate_etag(history) |
|
|
| |
| if check_etag(if_none_match, etag): |
| |
| response.status_code = 304 |
| response.headers["ETag"] = f'"{etag}"' |
| response.headers["Cache-Control"] = "no-cache, must-revalidate" |
| return Response(status_code=304) |
| else: |
| |
| response.headers["ETag"] = f'"{etag}"' |
| response.headers["Cache-Control"] = "no-cache, must-revalidate" |
| from typing import cast |
|
|
| return MigrationHistoryResponse( |
| migrations=cast(list[MigrationRecord], history["migrations"]), |
| total_count=cast(int, history["total_count"]), |
| current_version=cast(str, history["current_version"]), |
| ) |
|
|
| except Exception as e: |
| logfire.error(f"Error getting migration history: {e}") |
| raise HTTPException(status_code=500, detail=f"Failed to get migration history: {str(e)}") from e |
|
|
|
|
| @router.get("/pending", response_model=list[PendingMigration]) |
| async def get_pending_migrations(): |
| """ |
| Get list of pending migrations only. |
| |
| Returns simplified list of migrations that need to be applied. |
| """ |
| try: |
| |
| pending = await migration_service.get_pending_migrations() |
|
|
| |
| return [ |
| PendingMigration( |
| version=m.version, |
| name=m.name, |
| sql_content=m.sql_content, |
| file_path=m.file_path, |
| checksum=m.checksum, |
| ) |
| for m in pending |
| ] |
|
|
| except Exception as e: |
| logfire.error(f"Error getting pending migrations: {e}") |
| raise HTTPException(status_code=500, detail=f"Failed to get pending migrations: {str(e)}") from e |
|
|