fix(api): delete orphaned sessions when character is deleted #7
@@ -334,7 +334,10 @@ class CharacterService:
|
|||||||
|
|
||||||
def delete_character(self, character_id: str, user_id: str) -> bool:
|
def delete_character(self, character_id: str, user_id: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Delete a character (soft delete by marking inactive).
|
Permanently delete a character from the database.
|
||||||
|
|
||||||
|
Also cleans up any game sessions associated with the character
|
||||||
|
to prevent orphaned sessions.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
character_id: Character ID
|
character_id: Character ID
|
||||||
@@ -354,11 +357,20 @@ class CharacterService:
|
|||||||
if not character:
|
if not character:
|
||||||
raise CharacterNotFound(f"Character not found: {character_id}")
|
raise CharacterNotFound(f"Character not found: {character_id}")
|
||||||
|
|
||||||
# Soft delete by marking inactive
|
# Clean up associated sessions before deleting the character
|
||||||
self.db.update_document(
|
# Local import to avoid circular dependency (session_service imports character_service)
|
||||||
|
from app.services.session_service import get_session_service
|
||||||
|
session_service = get_session_service()
|
||||||
|
deleted_sessions = session_service.delete_sessions_by_character(character_id)
|
||||||
|
if deleted_sessions > 0:
|
||||||
|
logger.info("Cleaned up sessions for deleted character",
|
||||||
|
character_id=character_id,
|
||||||
|
sessions_deleted=deleted_sessions)
|
||||||
|
|
||||||
|
# Hard delete - permanently remove from database
|
||||||
|
self.db.delete_document(
|
||||||
collection_id=self.collection_id,
|
collection_id=self.collection_id,
|
||||||
document_id=character_id,
|
document_id=character_id
|
||||||
data={'is_active': False}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info("Character deleted successfully", character_id=character_id)
|
logger.info("Character deleted successfully", character_id=character_id)
|
||||||
|
|||||||
@@ -465,6 +465,79 @@ class SessionService:
|
|||||||
error=str(e))
|
error=str(e))
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def delete_sessions_by_character(self, character_id: str) -> int:
|
||||||
|
"""
|
||||||
|
Delete all sessions associated with a character.
|
||||||
|
|
||||||
|
Used during character deletion to clean up orphaned sessions.
|
||||||
|
This method finds all sessions where the character is either:
|
||||||
|
- The solo character (solo_character_id)
|
||||||
|
- A party member in multiplayer (party_member_ids)
|
||||||
|
|
||||||
|
For each session found, this method:
|
||||||
|
1. Deletes all associated chat messages
|
||||||
|
2. Hard deletes the session document from the database
|
||||||
|
|
||||||
|
Args:
|
||||||
|
character_id: Character ID to delete sessions for
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of sessions deleted
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info("Deleting sessions for character",
|
||||||
|
character_id=character_id)
|
||||||
|
|
||||||
|
# Query all sessions where characterId matches
|
||||||
|
# The characterId field is indexed at the document level
|
||||||
|
documents = self.db.list_rows(
|
||||||
|
table_id=self.collection_id,
|
||||||
|
queries=[Query.equal('characterId', character_id)]
|
||||||
|
)
|
||||||
|
|
||||||
|
if not documents:
|
||||||
|
logger.debug("No sessions found for character",
|
||||||
|
character_id=character_id)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
deleted_count = 0
|
||||||
|
chat_service = get_chat_message_service()
|
||||||
|
|
||||||
|
for document in documents:
|
||||||
|
session_id = document.id
|
||||||
|
try:
|
||||||
|
# Delete associated chat messages first
|
||||||
|
deleted_messages = chat_service.delete_messages_by_session(session_id)
|
||||||
|
logger.debug("Deleted chat messages for session",
|
||||||
|
session_id=session_id,
|
||||||
|
message_count=deleted_messages)
|
||||||
|
|
||||||
|
# Delete session document
|
||||||
|
self.db.delete_document(
|
||||||
|
collection_id=self.collection_id,
|
||||||
|
document_id=session_id
|
||||||
|
)
|
||||||
|
deleted_count += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Log but continue with other sessions
|
||||||
|
logger.error("Failed to delete session during character cleanup",
|
||||||
|
session_id=session_id,
|
||||||
|
character_id=character_id,
|
||||||
|
error=str(e))
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info("Sessions deleted for character",
|
||||||
|
character_id=character_id,
|
||||||
|
deleted_count=deleted_count)
|
||||||
|
return deleted_count
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to delete sessions for character",
|
||||||
|
character_id=character_id,
|
||||||
|
error=str(e))
|
||||||
|
raise
|
||||||
|
|
||||||
def add_conversation_entry(
|
def add_conversation_entry(
|
||||||
self,
|
self,
|
||||||
session_id: str,
|
session_id: str,
|
||||||
|
|||||||
@@ -463,7 +463,7 @@ Set-Cookie: coc_session=<session_token>; HttpOnly; Secure; SameSite=Lax; Max-Age
|
|||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Endpoint** | `DELETE /api/v1/characters/<id>` |
|
| **Endpoint** | `DELETE /api/v1/characters/<id>` |
|
||||||
| **Description** | Delete character (soft delete - marks as inactive) |
|
| **Description** | Permanently delete character and all associated game sessions |
|
||||||
| **Auth Required** | Yes |
|
| **Auth Required** | Yes |
|
||||||
|
|
||||||
**Response (200 OK):**
|
**Response (200 OK):**
|
||||||
|
|||||||
@@ -634,7 +634,7 @@ curl -X POST http://localhost:5000/api/v1/characters \
|
|||||||
|
|
||||||
**Endpoint:** `DELETE /api/v1/characters/<character_id>`
|
**Endpoint:** `DELETE /api/v1/characters/<character_id>`
|
||||||
|
|
||||||
**Description:** Soft-delete a character (marks as inactive rather than removing).
|
**Description:** Permanently delete a character from the database. Also cleans up all associated game sessions to prevent orphaned data.
|
||||||
|
|
||||||
**Request:**
|
**Request:**
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user