Files
ryan da7ed41938 feat: migrate backend persistence from TinyDB to MongoDB Atlas
- Implement lazy MongoDB client initialization in backend/db/mongo_client.py.
- Create Gunicorn configuration to ensure MongoDB client is initialized per worker.
- Refactor database access layer to support MongoDB with a new MongoLockedTable adapter.
- Add migration script to transfer existing TinyDB data to MongoDB, preserving idempotency.
- Update tracking event handling to ensure deterministic ordering with a monotonic sequence.
- Modify tests to use mongomock for MongoDB integration and ensure existing tests pass.
- Add integration test script to run tests against a local MongoDB Docker container.
- Document environment variables and migration process in specs/feat-database-migration.md.
2026-07-28 01:10:52 -04:00

140 lines
4.1 KiB
Python

"""Helper functions for tracking events database operations."""
import itertools
import logging
from typing import Optional, List
from tinydb import Query
from db.db import tracking_events_db
from models.tracking_event import TrackingEvent, EntityType, ActionType
logger = logging.getLogger(__name__)
# Monotonic sequence used as a deterministic tiebreaker when tracking events
# share the same ``occurred_at``/``created_at`` timestamps (common in tests).
_tracking_event_seq = itertools.count()
def insert_tracking_event(event: TrackingEvent) -> str:
"""
Insert a tracking event into the database.
Args:
event: TrackingEvent instance to insert
Returns:
The event ID
"""
try:
event_dict = event.to_dict()
event_dict['_seq'] = next(_tracking_event_seq)
tracking_events_db.insert(event_dict)
logger.info(f"Tracking event created: {event.action} {event.entity_type} {event.entity_id} for child {event.child_id}")
return event.id
except Exception as e:
logger.error(f"Failed to insert tracking event: {e}")
raise
def get_tracking_events_by_child(
child_id: str,
limit: int = 50,
offset: int = 0,
entity_type: Optional[EntityType] = None,
action: Optional[ActionType] = None
) -> tuple[List[TrackingEvent], int]:
"""
Query tracking events for a specific child with optional filters.
Args:
child_id: Child ID to filter by
limit: Maximum number of results (default 50, max 500)
offset: Number of results to skip
entity_type: Optional filter by entity type
action: Optional filter by action type
Returns:
Tuple of (list of TrackingEvent instances, total count)
"""
limit = min(limit, 500)
TrackingQuery = Query()
query_condition = TrackingQuery.child_id == child_id
if entity_type:
query_condition &= TrackingQuery.entity_type == entity_type
if action:
query_condition &= TrackingQuery.action == action
all_results = tracking_events_db.search(query_condition)
total = len(all_results)
# Sort by occurred_at desc, then created_at desc, then _seq desc for
# deterministic ordering when timestamps collide (common in fast tests).
all_results.sort(
key=lambda x: (x.get('occurred_at', ''), x.get('created_at', 0), x.get('_seq', 0)),
reverse=True,
)
paginated = all_results[offset:offset + limit]
events = [TrackingEvent.from_dict(r) for r in paginated]
return events, total
def get_tracking_events_by_user(
user_id: str,
limit: int = 50,
offset: int = 0,
entity_type: Optional[EntityType] = None
) -> tuple[List[TrackingEvent], int]:
"""
Query tracking events for a specific user.
Args:
user_id: User ID to filter by
limit: Maximum number of results
offset: Number of results to skip
entity_type: Optional filter by entity type
Returns:
Tuple of (list of TrackingEvent instances, total count)
"""
limit = min(limit, 500)
TrackingQuery = Query()
query_condition = TrackingQuery.user_id == user_id
if entity_type:
query_condition &= TrackingQuery.entity_type == entity_type
all_results = tracking_events_db.search(query_condition)
total = len(all_results)
all_results.sort(
key=lambda x: (x.get('occurred_at', ''), x.get('created_at', 0), x.get('_seq', 0)),
reverse=True,
)
paginated = all_results[offset:offset + limit]
events = [TrackingEvent.from_dict(r) for r in paginated]
return events, total
def anonymize_tracking_events_for_user(user_id: str) -> int:
"""
Anonymize tracking events by setting user_id to None.
Called when a user is deleted.
Args:
user_id: User ID to anonymize
Returns:
Number of records anonymized
"""
TrackingQuery = Query()
result = tracking_events_db.update({'user_id': None}, TrackingQuery.user_id == user_id)
count = len(result) if result else 0
logger.info(f"Anonymized {count} tracking events for user {user_id}")
return count