|
25 | 25 | ProjectItem, |
26 | 26 | ProjectStatusResponse, |
27 | 27 | ) |
28 | | -from basic_memory.utils import normalize_project_path |
| 28 | +from basic_memory.schemas.v2 import ProjectResolveRequest, ProjectResolveResponse |
| 29 | +from basic_memory.utils import normalize_project_path, generate_permalink |
29 | 30 |
|
30 | 31 | router = APIRouter(prefix="/projects", tags=["project_management-v2"]) |
31 | 32 |
|
32 | 33 |
|
| 34 | +@router.post("/resolve", response_model=ProjectResolveResponse) |
| 35 | +async def resolve_project_identifier( |
| 36 | + data: ProjectResolveRequest, |
| 37 | + project_repository: ProjectRepositoryDep, |
| 38 | +) -> ProjectResolveResponse: |
| 39 | + """Resolve a project identifier (name or permalink) to a project ID. |
| 40 | +
|
| 41 | + This endpoint provides efficient lookup of projects by name without |
| 42 | + needing to fetch the entire project list. Supports case-insensitive |
| 43 | + matching on both name and permalink. |
| 44 | +
|
| 45 | + Args: |
| 46 | + data: Request containing the identifier to resolve |
| 47 | +
|
| 48 | + Returns: |
| 49 | + Project information including the numeric ID |
| 50 | +
|
| 51 | + Raises: |
| 52 | + HTTPException: 404 if project not found |
| 53 | +
|
| 54 | + Example: |
| 55 | + POST /v2/projects/resolve |
| 56 | + {"identifier": "my-project"} |
| 57 | +
|
| 58 | + Returns: |
| 59 | + { |
| 60 | + "project_id": 1, |
| 61 | + "name": "my-project", |
| 62 | + "permalink": "my-project", |
| 63 | + "path": "/path/to/project", |
| 64 | + "is_active": true, |
| 65 | + "is_default": false, |
| 66 | + "resolution_method": "name" |
| 67 | + } |
| 68 | + """ |
| 69 | + logger.info(f"API v2 request: resolve_project_identifier for '{data.identifier}'") |
| 70 | + |
| 71 | + # Generate permalink for comparison |
| 72 | + identifier_permalink = generate_permalink(data.identifier) |
| 73 | + |
| 74 | + # Try to find project by ID first (if identifier is numeric) |
| 75 | + resolution_method = "name" |
| 76 | + project = None |
| 77 | + |
| 78 | + if data.identifier.isdigit(): |
| 79 | + project_id = int(data.identifier) |
| 80 | + project = await project_repository.get_by_id(project_id) |
| 81 | + if project: |
| 82 | + resolution_method = "id" |
| 83 | + |
| 84 | + # If not found by ID, try by permalink first (exact match) |
| 85 | + if not project: |
| 86 | + project = await project_repository.get_by_permalink(identifier_permalink) |
| 87 | + if project: |
| 88 | + resolution_method = "permalink" |
| 89 | + |
| 90 | + # If not found by permalink, try case-insensitive name search |
| 91 | + # Uses efficient database query instead of fetching all projects |
| 92 | + if not project: |
| 93 | + project = await project_repository.get_by_name_case_insensitive(data.identifier) |
| 94 | + if project: |
| 95 | + resolution_method = "name" |
| 96 | + |
| 97 | + if not project: |
| 98 | + raise HTTPException( |
| 99 | + status_code=404, detail=f"Project not found: '{data.identifier}'" |
| 100 | + ) |
| 101 | + |
| 102 | + return ProjectResolveResponse( |
| 103 | + project_id=project.id, |
| 104 | + name=project.name, |
| 105 | + permalink=generate_permalink(project.name), |
| 106 | + path=normalize_project_path(project.path), |
| 107 | + is_active=project.is_active if hasattr(project, "is_active") else True, |
| 108 | + is_default=project.is_default or False, |
| 109 | + resolution_method=resolution_method, |
| 110 | + ) |
| 111 | + |
| 112 | + |
33 | 113 | @router.get("/{project_id}", response_model=ProjectItem) |
34 | 114 | async def get_project_by_id( |
35 | 115 | project_id: ProjectIdPathDep, |
|
0 commit comments