Files
office_translator/_bmad-output/implementation-artifacts/3-11-custom-prompts-endpoint-crud.md
Sepehr Ramezani 26bd096a06 feat: production deployment - full update with providers, admin, glossaries, pricing, tests
Major changes across backend, frontend, infrastructure:
- Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud)
- Admin panel: user management, pricing, settings
- Glossary system with CSV import/export
- Subscription and tier quota management
- Security hardening (rate limiting, API key auth, path traversal fixes)
- Docker compose for dev, prod, and IONOS deployment
- Alembic migrations for new tables
- Frontend: dashboard, pricing page, landing page, i18n (en/fr)
- Test suite and verification scripts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-25 15:01:47 +02:00

328 lines
12 KiB
Markdown

# 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