Skip to content

Commit 7c0ad90

Browse files
jope-bmclaude
andcommitted
fix: Complete v2 API project ID implementation
- Add `id` field to ProjectItem schema as required field - Update all ProjectItem instantiations to include project ID - Change v2 API route from /v2/{project} to /v2/{project_id} - Create ProjectIdPathDep dependency for validating integer project IDs - Add V2-specific dependencies (repositories, services) that use ProjectIdPathDep - Update all v2 knowledge router endpoints to use V2 dependencies - Fix forward reference issues in deps.py with string annotations - Update test mocks to include project ID in responses - Fix update_project endpoint to return 400 (not 404) for backward compatibility This completes the Phase 1 implementation by ensuring v2 API uses stable integer project IDs instead of string project names/paths. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 17d961e commit 7c0ad90

10 files changed

Lines changed: 291 additions & 38 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,5 @@ ENV/
5252

5353
# claude action
5454
claude-output
55-
**/.claude/settings.local.json
55+
**/.claude/settings.local.json
56+
.mcp.json

src/basic_memory/api/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ async def lifespan(app: FastAPI): # pragma: no cover
9292
app.include_router(importer_router.router, prefix="/{project}")
9393

9494
# Include v2 routers (current)
95-
app.include_router(v2_knowledge, prefix="/v2/{project}")
95+
app.include_router(v2_knowledge, prefix="/v2/{project_id}")
9696

9797
# Project resource router works across projects
9898
app.include_router(project.project_resource_router)

src/basic_memory/api/routers/project_router.py

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ async def get_project(
5050
) # pragma: no cover
5151

5252
return ProjectItem(
53+
id=found_project.id,
5354
name=found_project.name,
5455
path=normalize_project_path(found_project.path),
5556
is_default=found_project.is_default or False,
@@ -80,9 +81,15 @@ async def update_project(
8081
raise HTTPException(status_code=400, detail="Path must be absolute")
8182

8283
# Get original project info for the response
84+
old_project = await project_service.get_project(name)
85+
if not old_project:
86+
raise HTTPException(status_code=400, detail=f"Project '{name}' not found in configuration")
87+
8388
old_project_info = ProjectItem(
84-
name=name,
85-
path=project_service.projects.get(name, ""),
89+
id=old_project.id,
90+
name=old_project.name,
91+
path=old_project.path,
92+
is_default=old_project.is_default or False,
8693
)
8794

8895
if path:
@@ -91,14 +98,21 @@ async def update_project(
9198
await project_service.update_project(name, is_active=is_active)
9299

93100
# Get updated project info
94-
updated_path = path if path else project_service.projects.get(name, "")
101+
updated_project = await project_service.get_project(name)
102+
if not updated_project:
103+
raise HTTPException(status_code=404, detail=f"Project '{name}' not found after update")
95104

96105
return ProjectStatusResponse(
97106
message=f"Project '{name}' updated successfully",
98107
status="success",
99108
default=(name == project_service.default_project),
100109
old_project=old_project_info,
101-
new_project=ProjectItem(name=name, path=updated_path),
110+
new_project=ProjectItem(
111+
id=updated_project.id,
112+
name=updated_project.name,
113+
path=updated_project.path,
114+
is_default=updated_project.is_default or False,
115+
),
102116
)
103117
except ValueError as e:
104118
raise HTTPException(status_code=400, detail=str(e))
@@ -186,6 +200,7 @@ async def list_projects(
186200

187201
project_items = [
188202
ProjectItem(
203+
id=project.id,
189204
name=project.name,
190205
path=normalize_project_path(project.path),
191206
is_default=project.is_default or False,
@@ -232,6 +247,7 @@ async def add_project(
232247
status="success",
233248
default=existing_project.is_default or False,
234249
new_project=ProjectItem(
250+
id=existing_project.id,
235251
name=existing_project.name,
236252
path=existing_project.path,
237253
is_default=existing_project.is_default or False,
@@ -250,12 +266,20 @@ async def add_project(
250266
project_data.name, project_data.path, set_default=project_data.set_default
251267
)
252268

269+
# Fetch the newly created project to get its ID
270+
new_project = await project_service.get_project(project_data.name)
271+
if not new_project:
272+
raise HTTPException(status_code=500, detail="Failed to retrieve newly created project")
273+
253274
return ProjectStatusResponse( # pyright: ignore [reportCallIssue]
254275
message=f"Project '{project_data.name}' added successfully",
255276
status="success",
256277
default=project_data.set_default,
257278
new_project=ProjectItem(
258-
name=project_data.name, path=project_data.path, is_default=project_data.set_default
279+
id=new_project.id,
280+
name=new_project.name,
281+
path=new_project.path,
282+
is_default=new_project.is_default or False,
259283
),
260284
)
261285
except ValueError as e: # pragma: no cover
@@ -306,7 +330,12 @@ async def remove_project(
306330
message=f"Project '{name}' removed successfully",
307331
status="success",
308332
default=False,
309-
old_project=ProjectItem(name=old_project.name, path=old_project.path),
333+
old_project=ProjectItem(
334+
id=old_project.id,
335+
name=old_project.name,
336+
path=old_project.path,
337+
is_default=old_project.is_default or False,
338+
),
310339
new_project=None,
311340
)
312341
except ValueError as e: # pragma: no cover
@@ -349,8 +378,14 @@ async def set_default_project(
349378
message=f"Project '{name}' set as default successfully",
350379
status="success",
351380
default=True,
352-
old_project=ProjectItem(name=default_name, path=default_project.path),
381+
old_project=ProjectItem(
382+
id=default_project.id,
383+
name=default_name,
384+
path=default_project.path,
385+
is_default=False,
386+
),
353387
new_project=ProjectItem(
388+
id=new_default_project.id,
354389
name=name,
355390
path=new_default_project.path,
356391
is_default=True,
@@ -378,7 +413,12 @@ async def get_default_project(
378413
status_code=404, detail=f"Default Project: '{default_name}' does not exist"
379414
)
380415

381-
return ProjectItem(name=default_project.name, path=default_project.path, is_default=True)
416+
return ProjectItem(
417+
id=default_project.id,
418+
name=default_project.name,
419+
path=default_project.path,
420+
is_default=True,
421+
)
382422

383423

384424
# Synchronize projects between config and database

src/basic_memory/api/v2/routers/knowledge_router.py

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@
1414
from loguru import logger
1515

1616
from basic_memory.deps import (
17-
EntityServiceDep,
18-
SearchServiceDep,
19-
LinkResolverDep,
20-
ProjectConfigDep,
17+
EntityServiceV2Dep,
18+
SearchServiceV2Dep,
19+
LinkResolverV2Dep,
20+
ProjectConfigV2Dep,
2121
AppConfigDep,
22-
SyncServiceDep,
23-
EntityRepositoryDep,
22+
SyncServiceV2Dep,
23+
EntityRepositoryV2Dep,
24+
ProjectIdPathDep,
2425
)
2526
from basic_memory.schemas import EntityResponse, DeleteEntitiesResponse
2627
from basic_memory.schemas.base import Entity
@@ -58,8 +59,9 @@ async def resolve_relations_background(sync_service, entity_id: int, entity_perm
5859

5960
@router.post("/resolve", response_model=EntityResolveResponse)
6061
async def resolve_identifier(
62+
project_id: ProjectIdPathDep,
6163
data: EntityResolveRequest,
62-
link_resolver: LinkResolverDep,
64+
link_resolver: LinkResolverV2Dep,
6365
) -> EntityResolveResponse:
6466
"""Resolve a string identifier (permalink, title, or path) to an entity ID.
6567
@@ -128,8 +130,9 @@ async def resolve_identifier(
128130

129131
@router.get("/entities/{entity_id}", response_model=EntityResponseV2)
130132
async def get_entity_by_id(
133+
project_id: ProjectIdPathDep,
131134
entity_id: int,
132-
entity_repository: EntityRepositoryDep,
135+
entity_repository: EntityRepositoryV2Dep,
133136
) -> EntityResponseV2:
134137
"""Get an entity by its numeric ID.
135138
@@ -162,10 +165,11 @@ async def get_entity_by_id(
162165

163166
@router.post("/entities", response_model=EntityResponse)
164167
async def create_entity(
168+
project_id: ProjectIdPathDep,
165169
data: Entity,
166170
background_tasks: BackgroundTasks,
167-
entity_service: EntityServiceDep,
168-
search_service: SearchServiceDep,
171+
entity_service: EntityServiceV2Dep,
172+
search_service: SearchServiceV2Dep,
169173
) -> EntityResponse:
170174
"""Create a new entity.
171175
@@ -199,14 +203,15 @@ async def create_entity(
199203

200204
@router.put("/entities/{entity_id}", response_model=EntityResponse)
201205
async def update_entity_by_id(
206+
project_id: ProjectIdPathDep,
202207
entity_id: int,
203208
data: Entity,
204209
response: Response,
205210
background_tasks: BackgroundTasks,
206-
entity_service: EntityServiceDep,
207-
search_service: SearchServiceDep,
208-
sync_service: SyncServiceDep,
209-
entity_repository: EntityRepositoryDep,
211+
entity_service: EntityServiceV2Dep,
212+
search_service: SearchServiceV2Dep,
213+
sync_service: SyncServiceV2Dep,
214+
entity_repository: EntityRepositoryV2Dep,
210215
) -> EntityResponse:
211216
"""Update an entity by ID.
212217
@@ -248,12 +253,13 @@ async def update_entity_by_id(
248253

249254
@router.patch("/entities/{entity_id}", response_model=EntityResponse)
250255
async def edit_entity_by_id(
256+
project_id: ProjectIdPathDep,
251257
entity_id: int,
252258
data: EditEntityRequest,
253259
background_tasks: BackgroundTasks,
254-
entity_service: EntityServiceDep,
255-
search_service: SearchServiceDep,
256-
entity_repository: EntityRepositoryDep,
260+
entity_service: EntityServiceV2Dep,
261+
search_service: SearchServiceV2Dep,
262+
entity_repository: EntityRepositoryV2Dep,
257263
) -> EntityResponse:
258264
"""Edit an existing entity by ID using operations like append, prepend, etc.
259265
@@ -267,7 +273,9 @@ async def edit_entity_by_id(
267273
Raises:
268274
HTTPException: 404 if entity not found, 400 if edit fails
269275
"""
270-
logger.info(f"API v2 request: edit_entity_by_id entity_id={entity_id}, operation='{data.operation}'")
276+
logger.info(
277+
f"API v2 request: edit_entity_by_id entity_id={entity_id}, operation='{data.operation}'"
278+
)
271279

272280
# Verify entity exists
273281
entity = await entity_repository.get_by_id(entity_id)
@@ -307,10 +315,11 @@ async def edit_entity_by_id(
307315

308316
@router.delete("/entities/{entity_id}", response_model=DeleteEntitiesResponse)
309317
async def delete_entity_by_id(
318+
project_id: ProjectIdPathDep,
310319
entity_id: int,
311320
background_tasks: BackgroundTasks,
312-
entity_service: EntityServiceDep,
313-
entity_repository: EntityRepositoryDep,
321+
entity_service: EntityServiceV2Dep,
322+
entity_repository: EntityRepositoryV2Dep,
314323
search_service=Depends(lambda: None), # Optional for now
315324
) -> DeleteEntitiesResponse:
316325
"""Delete an entity by ID.
@@ -347,12 +356,13 @@ async def delete_entity_by_id(
347356

348357
@router.post("/move", response_model=EntityResponse)
349358
async def move_entity(
359+
project_id: ProjectIdPathDep,
350360
data: MoveEntityRequest,
351361
background_tasks: BackgroundTasks,
352-
entity_service: EntityServiceDep,
353-
project_config: ProjectConfigDep,
362+
entity_service: EntityServiceV2Dep,
363+
project_config: ProjectConfigV2Dep,
354364
app_config: AppConfigDep,
355-
search_service: SearchServiceDep,
365+
search_service: SearchServiceV2Dep,
356366
) -> EntityResponse:
357367
"""Move an entity to a new file location.
358368

0 commit comments

Comments
 (0)