from pydantic import BaseModel, Field # type: ignore from datetime import datetime from typing import Optional, List from enum import Enum from bson import ObjectId # ========== Helper for ObjectId handling ========== class PyObjectId(ObjectId): """Custom type for ObjectId serialization""" @classmethod def __get_validators__(cls): yield cls.validate @classmethod def validate(cls, v): if isinstance(v, ObjectId): return v if isinstance(v, str): return ObjectId(v) raise ValueError(f"Invalid ObjectId: {v}") def __repr__(self): return f"ObjectId('{self}')" # ========== User Models ========== class UserCreate(BaseModel): email: str displayName: Optional[str] = None photoURL: Optional[str] = None class UserUpdate(BaseModel): displayName: Optional[str] = None photoURL: Optional[str] = None theme: Optional[str] = None class Config: json_schema_extra = { "example": { "displayName": "John Doe", "theme": "dark" } } class User(BaseModel): id: str = Field(alias="_id") email: str displayName: Optional[str] = None photoURL: Optional[str] = None createdAt: datetime updatedAt: datetime theme: str = "light" class Config: from_attributes = True populate_by_name = True json_schema_extra = { "example": { "_id": "507f1f77bcf86cd799439011", "email": "user@example.com", "displayName": "John Doe", "photoURL": "https://example.com/photo.jpg", "createdAt": "2026-03-05T00:00:00Z", "updatedAt": "2026-03-05T00:00:00Z", "theme": "light" } } # ========== Journal Entry Models ========== class MoodEnum(str, Enum): happy = "happy" sad = "sad" neutral = "neutral" anxious = "anxious" grateful = "grateful" class EncryptionMetadata(BaseModel): """Optional encryption metadata for entries""" encrypted: bool = False iv: Optional[str] = None # Initialization vector as base64 string algorithm: Optional[str] = None # e.g., "AES-256-GCM" class Config: json_schema_extra = { "example": { "encrypted": False, "iv": None, "algorithm": None } } class JournalEntryCreate(BaseModel): title: str content: str mood: Optional[MoodEnum] = None tags: Optional[List[str]] = None isPublic: Optional[bool] = False entryDate: Optional[datetime] = None # Logical journal date; defaults to today encryption: Optional[EncryptionMetadata] = None class Config: json_schema_extra = { "example": { "title": "Today's Gratitude", "content": "I'm grateful for...", "mood": "grateful", "tags": ["work", "family"], "isPublic": False, "entryDate": "2026-03-05T00:00:00Z" } } class JournalEntryUpdate(BaseModel): title: Optional[str] = None content: Optional[str] = None mood: Optional[MoodEnum] = None tags: Optional[List[str]] = None isPublic: Optional[bool] = None encryption: Optional[EncryptionMetadata] = None class Config: json_schema_extra = { "example": { "title": "Updated Title", "mood": "happy" } } class JournalEntry(BaseModel): id: str = Field(alias="_id") userId: str # ObjectId as string title: str content: str mood: Optional[MoodEnum] = None tags: Optional[List[str]] = [] isPublic: bool = False entryDate: datetime # Logical journal date createdAt: datetime updatedAt: datetime encryption: EncryptionMetadata = Field(default_factory=lambda: EncryptionMetadata()) class Config: from_attributes = True populate_by_name = True json_schema_extra = { "example": { "_id": "507f1f77bcf86cd799439011", "userId": "507f1f77bcf86cd799439012", "title": "Today's Gratitude", "content": "I'm grateful for...", "mood": "grateful", "tags": ["work", "family"], "isPublic": False, "entryDate": "2026-03-05T00:00:00Z", "createdAt": "2026-03-05T12:00:00Z", "updatedAt": "2026-03-05T12:00:00Z", "encryption": { "encrypted": False, "iv": None, "algorithm": None } } } # ========== Pagination Models ========== class PaginationMeta(BaseModel): """Pagination metadata for list responses""" total: int limit: int skip: int hasMore: bool class Config: json_schema_extra = { "example": { "total": 42, "limit": 20, "skip": 0, "hasMore": True } } class EntriesListResponse(BaseModel): """Response model for paginated entries""" entries: List[JournalEntry] pagination: PaginationMeta class Config: json_schema_extra = { "example": { "entries": [], "pagination": { "total": 42, "limit": 20, "skip": 0, "hasMore": True } } }