# Story 3.11: Custom Prompts - Endpoint CRUD Status: done ## Story En tant qu'**utilisateur Pro**, Je veux **créer et gérer des prompts système personnalisés via API**, de sorte que **je puisse guider le contexte de traduction LLM**. ## Acceptance Criteria 1. **Création de prompt**: Quand un utilisateur Pro POST sur `/api/v1/prompts` avec `{name, content}`, un prompt template est créé et associé à son compte. (FR59) 2. **Liste des prompts**: GET `/api/v1/prompts` retourne la liste des prompts de l'utilisateur. 3. **Détail d'un prompt**: GET `/api/v1/prompts/{id}` retourne les détails d'un prompt spécifique. 4. **Mise à jour**: PATCH `/api/v1/prompts/{id}` permet de mettre à jour le nom et/ou le contenu. 5. **Suppression**: DELETE `/api/v1/prompts/{id}` supprime un prompt. 6. **Restriction Pro**: Les utilisateurs Free reçoivent 403 avec erreur `PRO_FEATURE_REQUIRED`. ## Tasks / Subtasks - [x] **Task 1: Créer le modèle CustomPrompt et la migration Alembic** (AC: #1) - [x] 1.1 Ajouter le modèle `CustomPrompt` dans `database/models.py` - [x] 1.2 Générer la migration Alembic `alembic revision --autogenerate -m "add_custom_prompts_table"` - [x] 1.3 Appliquer la migration `alembic upgrade head` - [x] 1.4 Vérifier la table créée dans la DB - [x] **Task 2: Créer les schémas Pydantic** (AC: Tous) - [x] 2.1 Créer `schemas/prompt_schemas.py` avec PromptCreate, PromptUpdate, PromptResponse, PromptListItem - [x] 2.2 Définir les validators (content max 10000 chars, name max 255 chars) - [x] 2.3 Documenter les schémas pour OpenAPI - [x] **Task 3: Créer les routes CRUD** (AC: Tous) - [x] 3.1 Créer `routes/prompt_routes.py` avec les endpoints CRUD - [x] 3.2 Implémenter POST `/api/v1/prompts` - Créer un prompt - [x] 3.3 Implémenter GET `/api/v1/prompts` - Lister les prompts (avec pagination) - [x] 3.4 Implémenter GET `/api/v1/prompts/{id}` - Détail d'un prompt - [x] 3.5 Implémenter PATCH `/api/v1/prompts/{id}` - Mettre à jour un prompt - [x] 3.6 Implémenter DELETE `/api/v1/prompts/{id}` - Supprimer un prompt - [x] 3.7 Enregistrer le router dans `routes/api_v1_router.py` - [x] **Task 4: Implémenter la restriction Pro** (AC: #6) - [x] 4.1 Utiliser `require_pro_user` depuis `routes/deps.py` - [x] 4.2 Retourner 403 avec erreur `PRO_FEATURE_REQUIRED` pour les utilisateurs Free - [x] **Task 5: Tests** (AC: Tous) - [x] 5.1 Tests unitaires pour les schémas - [x] 5.2 Tests d'intégration pour les endpoints CRUD - [x] 5.3 Test de la restriction Pro (403 pour Free) - [x] 5.4 Test de la propriété utilisateur (un user ne peut pas voir les prompts d'un autre) - [x] **Task 6: Documentation OpenAPI** (AC: Tous) - [x] 6.1 Documenter tous les endpoints avec exemples - [x] 6.2 Documenter les codes d'erreur - [x] 6.3 Vérifier sur `/docs` que la documentation est complète ## Dev Notes ### 🏗️ Architecture - Structure du Module **Fichiers à créer/modifier:** ``` database/ ├── models.py # MODIFIÉ - Ajouter CustomPrompt schemas/ ├── prompt_schemas.py # CRÉÉ - Schémas Pydantic routes/ ├── prompt_routes.py # CRÉÉ - Endpoints CRUD ├── api_v1_router.py # MODIFIÉ - Enregistrement du router alembic/versions/ └── xxx_add_custom_prompts_table.py # CRÉÉ - Migration DB tests/ └── test_prompts.py # CRÉÉ - Tests ``` ### 📊 Modèle de Données **Table `custom_prompts`:** ```python class CustomPrompt(Base): """User's custom prompts for LLM translation context. Story 3.11: Custom Prompts - Endpoint CRUD """ __tablename__ = "custom_prompts" id = Column(String(36), primary_key=True, default=generate_uuid) user_id = Column( String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False ) name = Column(String(255), nullable=False) # User-friendly name content = Column(Text, nullable=False) # The actual prompt content (up to 10000 chars) created_at = Column(DateTime, default=_utcnow) updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow) # Indexes __table_args__ = (Index("ix_custom_prompts_user_id", "user_id"),) ``` **Contraintes:** - `name`: 1-255 caractères - `content`: 1-10000 caractères - Un utilisateur peut avoir plusieurs prompts - Suppression en cascade si l'utilisateur est supprimé ### 📝 Schémas Pydantic ```python # schemas/prompt_schemas.py class PromptCreate(BaseModel): """Schema for creating a prompt.""" name: str = Field(..., min_length=1, max_length=255) content: str = Field(..., min_length=1, max_length=10000) @field_validator("name", "content") @classmethod def strip_whitespace(cls, v: str) -> str: return v.strip() class PromptUpdate(BaseModel): """Schema for updating a prompt (all fields optional).""" name: Optional[str] = Field(None, min_length=1, max_length=255) content: Optional[str] = Field(None, min_length=1, max_length=10000) class PromptResponse(BaseModel): """Schema for prompt in response.""" id: str name: str content: str created_at: Optional[datetime] = None updated_at: Optional[datetime] = None model_config = {"from_attributes": True} class PromptListItem(BaseModel): """Schema for prompt in list (lighter version).""" id: str name: str content_preview: str = Field(..., description="First 100 chars of content") created_at: Optional[datetime] = None updated_at: Optional[datetime] = None model_config = {"from_attributes": True} ``` ### 🔧 Pattern CRUD à Suivre (basé sur glossary_routes.py) **Structure des endpoints:** 1. **POST** - Créer un prompt: - Utiliser `require_pro_user` dependency - Valider les données avec Pydantic schema - Créer l'entité en DB - Logger l'action - Retourner 201 avec `{data: {...}, meta: {}}` 2. **GET list** - Lister les prompts: - Pagination avec `page` et `per_page` query params - `DEFAULT_PAGE_SIZE = 50`, `MAX_PAGE_SIZE = 100` - Retourner `{data: [...], meta: {total, page, per_page, total_pages}}` - Utiliser `content_preview` (100 premiers caractères) pour alléger la réponse 3. **GET detail** - Détail d'un prompt: - Valider le format UUID - Vérifier que le prompt appartient à l'utilisateur - Retourner 404 avec `PROMPT_NOT_FOUND` si introuvable 4. **PATCH** - Mettre à jour: - Valider le format UUID - Vérifier la propriété - Permettre mise à jour partielle (uniquement les champs fournis) - Mettre à jour `updated_at` 5. **DELETE** - Supprimer: - Valider le format UUID - Vérifier la propriété - Retourner 204 (No Content) ### 🚨 Points d'Attention - Anti-Patterns à Éviter 1. **Ne PAS réinventer les dependencies** - Utiliser `require_pro_user` depuis `routes/deps.py` 2. **Ne PAS utiliser des modèles Pydantic inline** - Créer les schémas dans `schemas/prompt_schemas.py` 3. **Pagination obligatoire** - La liste DOIT avoir `page` et `per_page` pour éviter les N+1 queries 4. **Erreur 500 proscrite** - Toujours catcher les exceptions DB et retourner des erreurs JSON structurées 5. **Logger les actions** - Utiliser `logger.info()` pour create/update/delete 6. **UUID validation** - Toujours valider le format UUID avant de query la DB ### 📚 Références de Code Existant **Pattern exact à suivre:** `routes/glossary_routes.py` (Story 3.9) ```python # Structure du router router = APIRouter(prefix="/api/v1/prompts", tags=["Prompts v1"]) # POST /api/v1/prompts @router.post("", response_model=PromptDetailResponse, status_code=201) async def create_prompt(body: PromptCreate, user: ProUser = Depends(require_pro_user)): ... # GET /api/v1/prompts @router.get("", response_model=PromptListResponse) async def list_prompts(page: int = Query(1, ge=1), per_page: int = Query(50, ge=1, le=100), user: ProUser = Depends(require_pro_user)): ... # GET /api/v1/prompts/{prompt_id} @router.get("/{prompt_id}", response_model=PromptDetailResponse) async def get_prompt(prompt_id: str, user: ProUser = Depends(require_pro_user)): ... # PATCH /api/v1/prompts/{prompt_id} @router.patch("/{prompt_id}", response_model=PromptDetailResponse) async def update_prompt(prompt_id: str, body: PromptUpdate, user: ProUser = Depends(require_pro_user)): ... # DELETE /api/v1/prompts/{prompt_id} @router.delete("/{prompt_id}", status_code=204) async def delete_prompt(prompt_id: str, user: ProUser = Depends(require_pro_user)): ... ``` ### 🧪 Tests à Créer **Fichier:** `tests/test_prompts.py` ```python def test_create_prompt_success(): """Pro user can create a prompt""" def test_create_prompt_free_user_forbidden(): """Free user receives 403 PRO_FEATURE_REQUIRED""" def test_list_prompts_with_pagination(): """List returns paginated results""" def test_get_prompt_detail(): """Get single prompt by ID""" def test_get_prompt_not_found(): """404 for non-existent or other user's prompt""" def test_update_prompt(): """PATCH updates name and/or content""" def test_delete_prompt(): """DELETE removes prompt""" def test_prompt_ownership(): """User cannot access another user's prompts""" def test_content_max_length(): """Content > 10000 chars returns 400""" def test_name_max_length(): """Name > 255 chars returns 400""" ``` ### Project Structure Notes - **Nouveau fichier:** `schemas/prompt_schemas.py` - Schémas Pydantic pour prompts - **Nouveau fichier:** `routes/prompt_routes.py` - Endpoints CRUD - **Nouveau fichier:** `tests/test_prompts.py` - Tests - **Modification:** `database/models.py` - Ajout du modèle CustomPrompt - **Modification:** `routes/api_v1_router.py` - Enregistrement du router ### References - [Source: _bmad-output/planning-artifacts/epics.md#Story-3.11] - Story requirements (FR59) - [Source: _bmad-output/planning-artifacts/architecture.md#API-Response-Formats] - Format API - [Source: routes/glossary_routes.py] - Pattern CRUD à suivre (Story 3.9) - [Source: routes/deps.py] - Dependencies `require_pro_user` - [Source: schemas/glossary_schemas.py] - Pattern schémas Pydantic - [Source: database/models.py] - Structure des modèles existants ## Dev Agent Record ### Agent Model Used Claude 3.5 Sonnet (claude-3-5-sonnet) ### Debug Log References Aucune erreur bloquante rencontrée. Migration Alembic a nécessité une adaptation pour SQLite (limitation ALTER COLUMN). ### Completion Notes List - ✅ Modèle CustomPrompt ajouté à `database/models.py` avec index sur user_id - ✅ Migration Alembic générée et appliquée (revision 5206607942a2) - ✅ Schémas Pydantic créés: PromptCreate, PromptUpdate, PromptResponse, PromptListItem - ✅ Endpoints CRUD complets: POST, GET (list), GET (detail), PATCH, DELETE - ✅ Restriction Pro implémentée via `require_pro_user` dependency - ✅ 19 tests d'intégration créés et passent tous - ✅ Documentation OpenAPI complète avec descriptions et exemples - ✅ Validation des contraintes (name: 1-255 chars, content: 1-10000 chars) - ✅ Pagination implémentée sur l'endpoint de liste - ✅ Propriété utilisateur vérifiée (404 si accès à un prompt d'un autre utilisateur) - ✅ [Review Fix] Validation PATCH body vide ajoutée (erreur 400 NO_UPDATE_FIELDS) - ✅ [Review Fix] Helper `_validate_uuid()` extraite pour éviter duplication - ✅ [Review Fix] Méthode `has_updates()` ajoutée à PromptUpdate ### File List **Nouveaux fichiers:** - `schemas/prompt_schemas.py` - `routes/prompt_routes.py` - `tests/test_prompts.py` - `alembic/versions/5206607942a2_add_custom_prompts_table.py` **Fichiers modifiés:** - `database/models.py` - Ajout du modèle CustomPrompt - `routes/api_v1_router.py` - Enregistrement du router prompt_router ## Change Log - 2026-02-22: Implémentation complète de la Story 3.11 - Custom Prompts CRUD endpoints - 2026-02-22: Code review - Fix validation PATCH body vide, refactor UUID validation, ajout test