fix: serialize Client/Team/Project with id not _id + guard undefined client hooks

Pydantic v2 + FastAPI serializes Field(alias="_id") as _id in JSON,
so client.id was always undefined on the frontend — causing option
values to fall back to text content ("3M") and firing /clients/3M/teams 404s.

- Remove Field(alias="_id") from Client/Team/Project models; id is now a
  plain string field populated explicitly in _client_from_doc etc.
- API now returns id not _id, matching the TypeScript Client interface
- Add clientId !== "undefined" guard to useTeams, usePMs, useProjects

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-04-27 16:18:28 +01:00
parent 723bbbc695
commit 269ab09fa6
3 changed files with 32 additions and 20 deletions

View file

@ -64,15 +64,36 @@ async def _get_project_or_404(project_id: str, client_id: str, db: AsyncIOMotorD
def _client_from_doc(doc: dict) -> Client:
return Client(**{**doc, "_id": str(doc["_id"])})
return Client(
id=str(doc["_id"]),
name=doc["name"],
slug=doc["slug"],
is_active=doc.get("is_active", True),
created_at=doc.get("created_at"),
updated_at=doc.get("updated_at"),
)
def _team_from_doc(doc: dict) -> Team:
return Team(**{**doc, "_id": str(doc["_id"])})
return Team(
id=str(doc["_id"]),
name=doc["name"],
client_id=doc["client_id"],
member_user_ids=doc.get("member_user_ids", []),
created_at=doc.get("created_at"),
updated_at=doc.get("updated_at"),
)
def _project_from_doc(doc: dict) -> Project:
return Project(**{**doc, "_id": str(doc["_id"])})
return Project(
id=str(doc["_id"]),
name=doc["name"],
client_id=doc["client_id"],
is_active=doc.get("is_active", True),
created_at=doc.get("created_at"),
updated_at=doc.get("updated_at"),
)
# ---------------------------------------------------------------------------

View file

@ -2,7 +2,7 @@ from datetime import datetime
from typing import Optional, Annotated
from bson import ObjectId
from pydantic import BaseModel, Field, BeforeValidator
from pydantic import BaseModel, BeforeValidator
def validate_object_id(v) -> str:
@ -17,16 +17,13 @@ PyObjectId = Annotated[str, BeforeValidator(validate_object_id)]
class Client(BaseModel):
id: Optional[PyObjectId] = Field(None, alias="_id")
id: Optional[str] = None
name: str
slug: str # lowercase, URL-safe identifier, unique
slug: str
is_active: bool = True
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class Config:
populate_by_name = True
class ClientCreate(BaseModel):
name: str
@ -40,16 +37,13 @@ class ClientUpdate(BaseModel):
class Team(BaseModel):
id: Optional[PyObjectId] = Field(None, alias="_id")
id: Optional[str] = None
name: str
client_id: str
member_user_ids: list[str] = []
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class Config:
populate_by_name = True
class TeamCreate(BaseModel):
name: str
@ -60,16 +54,13 @@ class TeamUpdate(BaseModel):
class Project(BaseModel):
id: Optional[PyObjectId] = Field(None, alias="_id")
id: Optional[str] = None
name: str
client_id: str
is_active: bool = True
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class Config:
populate_by_name = True
class ProjectCreate(BaseModel):
name: str

View file

@ -97,7 +97,7 @@ export function usePMs(clientId: string) {
return useQuery({
queryKey: ['clients', clientId, 'pm'],
queryFn: () => apiClient.listPMs(clientId),
enabled: !!clientId,
enabled: !!clientId && clientId !== 'undefined',
});
}
@ -123,7 +123,7 @@ export function useTeams(clientId: string) {
return useQuery({
queryKey: ['clients', clientId, 'teams'],
queryFn: () => apiClient.listTeams(clientId),
enabled: !!clientId,
enabled: !!clientId && clientId !== 'undefined',
});
}
@ -176,7 +176,7 @@ export function useProjects(clientId: string) {
return useQuery({
queryKey: ['clients', clientId, 'projects'],
queryFn: () => apiClient.listProjects(clientId),
enabled: !!clientId,
enabled: !!clientId && clientId !== 'undefined',
});
}