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

12 KiB

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

  • Task 1: Créer le modèle CustomPrompt et la migration Alembic (AC: #1)

    • 1.1 Ajouter le modèle CustomPrompt dans database/models.py
    • 1.2 Générer la migration Alembic alembic revision --autogenerate -m "add_custom_prompts_table"
    • 1.3 Appliquer la migration alembic upgrade head
    • 1.4 Vérifier la table créée dans la DB
  • Task 2: Créer les schémas Pydantic (AC: Tous)

    • 2.1 Créer schemas/prompt_schemas.py avec PromptCreate, PromptUpdate, PromptResponse, PromptListItem
    • 2.2 Définir les validators (content max 10000 chars, name max 255 chars)
    • 2.3 Documenter les schémas pour OpenAPI
  • Task 3: Créer les routes CRUD (AC: Tous)

    • 3.1 Créer routes/prompt_routes.py avec les endpoints CRUD
    • 3.2 Implémenter POST /api/v1/prompts - Créer un prompt
    • 3.3 Implémenter GET /api/v1/prompts - Lister les prompts (avec pagination)
    • 3.4 Implémenter GET /api/v1/prompts/{id} - Détail d'un prompt
    • 3.5 Implémenter PATCH /api/v1/prompts/{id} - Mettre à jour un prompt
    • 3.6 Implémenter DELETE /api/v1/prompts/{id} - Supprimer un prompt
    • 3.7 Enregistrer le router dans routes/api_v1_router.py
  • Task 4: Implémenter la restriction Pro (AC: #6)

    • 4.1 Utiliser require_pro_user depuis routes/deps.py
    • 4.2 Retourner 403 avec erreur PRO_FEATURE_REQUIRED pour les utilisateurs Free
  • Task 5: Tests (AC: Tous)

    • 5.1 Tests unitaires pour les schémas
    • 5.2 Tests d'intégration pour les endpoints CRUD
    • 5.3 Test de la restriction Pro (403 pour Free)
    • 5.4 Test de la propriété utilisateur (un user ne peut pas voir les prompts d'un autre)
  • Task 6: Documentation OpenAPI (AC: Tous)

    • 6.1 Documenter tous les endpoints avec exemples
    • 6.2 Documenter les codes d'erreur
    • 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:

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

# 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)

# 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

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