Skip to content

Commit 2bdcd74

Browse files
jope-bmclaude
andcommitted
fix: Correct V2 move endpoint to use entity ID in URL path
The V2 move endpoint was incorrectly accepting an identifier in the request body, violating V2 API design principles. Fixed to use entity ID in the URL path for consistency with other V2 endpoints. Changes: - Add MoveEntityRequestV2 schema with only destination_path field - Update move endpoint from POST /knowledge/move to PUT /knowledge/entities/{entity_id}/move - Entity ID now in URL path, not request body - Updated endpoint implementation to fetch entity by ID first - Updated test to use new endpoint structure Before: POST /v2/projects/{project_id}/knowledge/move Body: { "identifier": "...", "destination_path": "..." } After: PUT /v2/projects/{project_id}/knowledge/entities/{entity_id}/move Body: { "destination_path": "..." } All 180 V2 API tests passing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Joe P <joe@basicmemory.com>
1 parent 231c795 commit 2bdcd74

4 files changed

Lines changed: 44 additions & 14 deletions

File tree

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

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@
2525
)
2626
from basic_memory.schemas import EntityResponse, DeleteEntitiesResponse
2727
from basic_memory.schemas.base import Entity
28-
from basic_memory.schemas.request import EditEntityRequest, MoveEntityRequest
28+
from basic_memory.schemas.request import EditEntityRequest
2929
from basic_memory.schemas.v2 import (
3030
EntityResolveRequest,
3131
EntityResolveResponse,
3232
EntityResponseV2,
33+
MoveEntityRequestV2,
3334
)
3435

3536
router = APIRouter(prefix="/knowledge", tags=["knowledge-v2"])
@@ -351,44 +352,53 @@ async def delete_entity_by_id(
351352
## Move endpoint
352353

353354

354-
@router.post("/move", response_model=EntityResponseV2)
355+
@router.put("/entities/{entity_id}/move", response_model=EntityResponseV2)
355356
async def move_entity(
356357
project_id: ProjectIdPathDep,
357-
data: MoveEntityRequest,
358+
entity_id: int,
359+
data: MoveEntityRequestV2,
358360
background_tasks: BackgroundTasks,
359361
entity_service: EntityServiceV2Dep,
362+
entity_repository: EntityRepositoryV2Dep,
360363
project_config: ProjectConfigV2Dep,
361364
app_config: AppConfigDep,
362365
search_service: SearchServiceV2Dep,
363366
) -> EntityResponseV2:
364367
"""Move an entity to a new file location.
365368
366-
Note: Identifier in request can be an entity ID or legacy identifier.
369+
V2 API uses entity ID in the URL path for stable references.
367370
The entity ID will remain stable after the move.
368371
369372
Args:
370-
data: Move request with identifier and destination path
373+
project_id: Project ID from URL path
374+
entity_id: Entity ID from URL path (primary identifier)
375+
data: Move request with destination path only
371376
372377
Returns:
373378
Updated entity with new file path
374379
"""
375380
logger.info(
376-
f"API v2 request: move_entity identifier='{data.identifier}', destination='{data.destination_path}'"
381+
f"API v2 request: move_entity entity_id={entity_id}, destination='{data.destination_path}'"
377382
)
378383

379384
try:
380-
# Move the entity
385+
# First, get the entity by ID to verify it exists
386+
entity = await entity_repository.find_by_id(entity_id)
387+
if not entity:
388+
raise HTTPException(status_code=404, detail=f"Entity not found: {entity_id}")
389+
390+
# Move the entity using its current file path as identifier
381391
moved_entity = await entity_service.move_entity(
382-
identifier=data.identifier,
392+
identifier=entity.file_path, # Use file path for resolution
383393
destination_path=data.destination_path,
384394
project_config=project_config,
385395
app_config=app_config,
386396
)
387397

388398
# Reindex at new location
389-
entity = await entity_service.link_resolver.resolve_link(data.destination_path)
390-
if entity:
391-
await search_service.index_entity(entity, background_tasks=background_tasks)
399+
reindexed_entity = await entity_service.link_resolver.resolve_link(data.destination_path)
400+
if reindexed_entity:
401+
await search_service.index_entity(reindexed_entity, background_tasks=background_tasks)
392402

393403
result = EntityResponseV2.model_validate(moved_entity)
394404

@@ -398,6 +408,8 @@ async def move_entity(
398408

399409
return result
400410

411+
except HTTPException:
412+
raise
401413
except Exception as e:
402414
logger.error(f"Error moving entity: {e}")
403415
raise HTTPException(status_code=400, detail=str(e))

src/basic_memory/schemas/v2/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
EntityResolveRequest,
55
EntityResolveResponse,
66
EntityResponseV2,
7+
MoveEntityRequestV2,
78
)
89
from basic_memory.schemas.v2.resource import (
910
CreateResourceRequest,
@@ -15,6 +16,7 @@
1516
"EntityResolveRequest",
1617
"EntityResolveResponse",
1718
"EntityResponseV2",
19+
"MoveEntityRequestV2",
1820
"CreateResourceRequest",
1921
"UpdateResourceRequest",
2022
"ResourceResponse",

src/basic_memory/schemas/v2/entity.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,21 @@ class EntityResolveResponse(BaseModel):
4040
)
4141

4242

43+
class MoveEntityRequestV2(BaseModel):
44+
"""V2 request schema for moving an entity to a new file location.
45+
46+
In V2 API, the entity ID is provided in the URL path, so this request
47+
only needs the destination path.
48+
"""
49+
50+
destination_path: str = Field(
51+
...,
52+
description="New file path for the entity (relative to project root)",
53+
min_length=1,
54+
max_length=500,
55+
)
56+
57+
4358
class EntityResponseV2(BaseModel):
4459
"""V2 entity response with ID as the primary field.
4560

tests/api/v2/test_knowledge_router.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -347,12 +347,13 @@ async def test_move_entity(client: AsyncClient, file_service, v2_project_url, en
347347
assert created_entity.id is not None
348348
original_id = created_entity.id
349349

350-
# Move it to a new folder (use permalink for identifier in v2)
350+
# Move it to a new folder (V2 uses entity ID in path)
351351
move_data = {
352-
"identifier": created_entity.permalink, # Use permalink as identifier
353352
"destination_path": "moved/MovedEntity.md",
354353
}
355-
response = await client.post(f"{v2_project_url}/knowledge/move", json=move_data)
354+
response = await client.put(
355+
f"{v2_project_url}/knowledge/entities/{created_entity.id}/move", json=move_data
356+
)
356357

357358
assert response.status_code == 200
358359
moved_entity = EntityResponseV2.model_validate(response.json())

0 commit comments

Comments
 (0)