# Démonstration d'API GraphQL ## Installation Ouvrir un terminal dans le dossier racine du projet : ```bash poetry install # installer les dépendances nécessaires ``` Pensez également à marquer le dossier "src" comme "Source Root" (via clic droit "Mark Directory as ...") dans l'IDE PyCharm, pour éviter des problèmes d'importation. Pour démarrer le serveur GraphQL, utilisez la commande : ```bash # En étant à la racine du projet cd src uvicorn app.main:app --host 0.0.0.0 --port 8004 ``` Ouvrez Appolo Studio à l'adresse suivante :git push -u origin main https://studio.apollographql.com/sandbox/explorer/ Bien rentrer dans l'outil l'URL complète : > http://127.0.0.1:8004/graphql À noter que vous pouvez aussi utiliser l'interface incluse GraphiQL en vous rendant à l'adresse : http://127.0.0.1:8004/graphql ## Instructions Vous allez devoir créer les fichiers manquants pour faire fonctionner l'application. Suivez attentivement les étapes. ### Vue d'ensemble de l'architecture Avant de commencer, voici un schéma illustrant comment les différentes parties que vous allez créer interagissent : ```mermaid graph TD A["Client Apollo/GraphiQL"] -- "Requete GraphQL" --> B{"Serveur Uvicorn/Strawberry"}; B -- "Traite la requete" --> C[Query / Mutation]; C -- "Appelle le bon resolver" --> D["Resolver ex: resolve_recipe_by_id"]; D -- "Demande les donnees" --> E["Service recipe_service"]; E -- "Recupere/Ecrit les donnees" --> F["Repository recipe_repository"]; F -- "Interagit" --> G["Base de Donnees 'simulee'"]; ``` ----- ### Étape 1 : les Modèles Métier (Pydantic) Ce sont les représentations internes de nos données. Elles utilisent Pydantic pour la validation. 1. Créez le fichier `app/models/ingredient.py` et ajoutez-y le code suivant : ```python # Fichier: app/models/ingredient.py from pydantic import BaseModel class Ingredient(BaseModel): name: str quantity: str ``` 2. Créez le fichier `app/models/recipe.py` et ajoutez-y ce code : ```python # Fichier: app/models/recipe.py from pydantic import BaseModel from typing import List from .ingredient import Ingredient class Recipe(BaseModel): id: int title: str description: str ingredients: List[Ingredient] ``` --- ### Étape 2 : les types GraphQL (Strawberry) Ce sont les "schémas" que notre API va exposer aux clients. Ils décrivent la forme des données que l'on peut demander. 1. Créez le dossier `app/graphql/types/`. 2. Créez le fichier `app/graphql/types/ingredient.py` : ```python # Fichier: app/graphql/types/ingredient.py import strawberry @strawberry.type class Ingredient: name: str quantity: str ``` 3. Créez le fichier `app/graphql/types/recipe.py` : ```python # Fichier: app/graphql/types/recipe.py import strawberry from typing import List from .ingredient import Ingredient @strawberry.type class Recipe: id: strawberry.ID title: str description: str ingredients: List[Ingredient] ``` --- ### Étape 3 : la logique métier (BLL / Services) Le service fait le lien entre la couche API (les resolvers que nous verrons après) et la couche de données (le repository qui est déjà fourni). 1. Créez le dossier `app/services/` si nécessaire. 2. Créez le fichier `app/services/recipe_service.py` et ajoutez-y les fonctions suivantes : ```python # Fichier: app/services/recipe_service.py from typing import List, Dict, Any from app.models.recipe import Recipe from app.repositories.recipe_repository import recipe_repository from app.core.exceptions import NotFoundBLLException, ValidationBLLException def get_all_recipes() -> List[Recipe]: """Récupère toutes les recettes.""" return recipe_repository.list() def get_recipe_by_id(recipe_id: int) -> Recipe: """ Récupère une recette par son ID. Lève une exception si la recette n'est pas trouvée. """ recipe = recipe_repository.find_by_id(recipe_id) if not recipe: raise NotFoundBLLException(resource_name="Recipe", resource_id=recipe_id) return recipe def add_new_recipe(title: str, description: str, ingredients: List[Dict[str, Any]]) -> Recipe: """ Ajoute une nouvelle recette après validation. """ if not title or not title.strip(): raise ValidationBLLException("Le titre de la recette ne peut pas être vide.") if not ingredients: raise ValidationBLLException("Une recette doit contenir au moins un ingrédient.") recipe_data = { "title": title, "description": description, "ingredients": ingredients } return recipe_repository.create(recipe_data) ``` --- ### Étape 4 : les Inputs GraphQL (pour les Mutations) Quand on veut envoyer des données complexes au serveur (comme pour créer une nouvelle recette), on utilise des `inputs`. 1. Créez le dossier `app/graphql/inputs/`. 2. Créez le fichier `app/graphql/inputs/ingredient_input.py`: ```python # Fichier: app/graphql/inputs/ingredient_input.py import strawberry @strawberry.input class IngredientInput: name: str quantity: str ``` 3. Créez le fichier `app/graphql/inputs/recipe_input.py`: ```python # Fichier: app/graphql/inputs/recipe_input.py import strawberry from typing import List from .ingredient_input import IngredientInput @strawberry.input class AddRecipeInput: title: str description: str ingredients: List[IngredientInput] ``` ### Comprendre la différence : Model vs Type vs Input Vous avez maintenant créé trois types de classes qui se ressemblent. Voici un schéma pour clarifier leurs rôles distincts : ```mermaid graph TD subgraph "Modele Metier 'Pydantic'" A["class Recipe(BaseModel)
Role: Validation et logique interne
Utilise par: Services, Repository"] end subgraph "Schema GraphQL 'Strawberry'" B["class Recipe(strawberry.type)
Role: Definir ce qu'on peut lire 'Query'
Utilise pour: Retourner des donnees"] C["class AddRecipeInput(strawberry.input)
Role: Definir ce qu'on peut ecrire 'Mutation'
Utilise pour: Recevoir des donnees"] end E[Service] -- "Manipule et retourne un" --> A A -- "Est converti en" --> B B -- "Envoye au Client" --> D[Client GraphQL] D -- "Envoie un" --> C C -- "Est utilise par le" --> F[Resolver] F -- "Pour appeler le" --> E ``` ----- ### Étape 5 : les resolvers Le **resolver** est la fonction qui "résout" une requête. C'est le cœur de notre API : il prend une demande GraphQL et appelle le service approprié pour obtenir les données. 1. Créez le dossier `app/graphql/resolvers/`. 2. Créez le fichier `app/graphql/resolvers/recipe_resolvers.py` et remplissez-le : ```python # Fichier: app/graphql/resolvers/recipe_resolvers.py import strawberry from typing import List, Optional from app.graphql.types.recipe import Recipe from app.graphql.inputs.recipe_input import AddRecipeInput import app.services.recipe_service as recipe_service def resolve_recipes() -> List[Recipe]: """Resolver pour obtenir la liste de toutes les recettes.""" return recipe_service.get_all_recipes() def resolve_recipe_by_id(id: strawberry.ID) -> Optional[Recipe]: """Resolver pour obtenir une recette par son ID.""" return recipe_service.get_recipe_by_id(recipe_id=int(id)) def resolve_add_recipe(input: AddRecipeInput) -> Recipe: """Resolver pour ajouter une nouvelle recette.""" ingredients_data = [ {"name": ing.name, "quantity": ing.quantity} for ing in input.ingredients ] return recipe_service.add_new_recipe( title=input.title, description=input.description, ingredients=ingredients_data ) ``` --- ### Étape 6 : assemblage final (Queries & Mutations) Maintenant, nous allons déclarer les points d'entrée de notre API en les liant aux resolvers que nous venons de créer. 1. Créez le fichier `app/graphql/queries.py` : ```python # Fichier: app/graphql/queries.py import strawberry from typing import List, Optional from app.graphql.types.recipe import Recipe from app.graphql.resolvers.recipe_resolvers import resolve_recipes, resolve_recipe_by_id @strawberry.type class Query: recipes: List[Recipe] = strawberry.field( resolver=resolve_recipes, description="Récupère la liste de toutes les recettes de cuisine." ) recipe: Optional[Recipe] = strawberry.field( resolver=resolve_recipe_by_id, description="Récupère une recette par son ID." ) ``` 2. Créez le fichier `app/graphql/mutations.py` : ```python # Fichier: app/graphql/mutations.py import strawberry from app.graphql.types.recipe import Recipe from app.graphql.inputs.recipe_input import AddRecipeInput from app.graphql.resolvers.recipe_resolvers import resolve_add_recipe @strawberry.type class Mutation: addRecipe: Recipe = strawberry.field( resolver=resolve_add_recipe, description="Ajoute une nouvelle recette au livre de cuisine." ) ``` Vous avez créé tous les fichiers nécessaires. L'application est maintenant complète. ### 7. Requêtes GraphQL via Appolo Studio En s'assurant que votre serveur est bien démarré, vous pouvez éxécuter les requêtes GraphQL suivantes pour tester l'API : #### Flux d'une requête (Query) Voici ce qui se passe lorsque vous exécutez la requête `recipe(id: "1")` : ```mermaid sequenceDiagram actor Client participant API as "GraphQL 'main.py'" participant Query as "Query 'queries.py'" participant Resolver as "Resolver 'recipe_resolvers.py'" participant Service as "Service 'recipe_service.py'" participant Repo as "Repository 'recipe_repository.py'" Client->>+API: query { recipe(id: "1") } API->>+Query: Trouve le champ "recipe" Query->>+Resolver: Appelle "resolve_recipe_by_id(id='1')" Resolver->>+Service: Appelle "get_recipe_by_id(recipe_id=1)" Service->>+Repo: Appelle "find_by_id(1)" Repo-->>-Service: Retourne "Recipe Model Pydantic" Service-->>-Resolver: Retourne "Recipe Model Pydantic" Resolver-->>-Query: Retourne le modele Query-->>-API: Construit la reponse JSON API-->>-Client: Reponse JSON ``` #### Flux d'une requête (Mutation) Et voici ce qui se passe lors d'une mutation `addRecipe` : ```mermaid sequenceDiagram actor Client participant API as "GraphQL 'main.py'" participant Mutation as "Mutation 'mutations.py'" participant Resolver as "Resolver 'recipe_resolvers.py'" participant Service as "Service 'recipe_service.py'" participant Repo as "Repository 'recipe_repository.py'" Client->>+API: mutation { addRecipe(input: {...}) } API->>+Mutation: Trouve le champ "addRecipe" Mutation->>+Resolver: Appelle "resolve_add_recipe(input=InputObject)" Resolver->>Resolver: Convertit "InputObject" en "dict" Resolver->>+Service: Appelle "add_new_recipe(title, ...)" Service->>Service: Valide les donnees "ex: titre non vide" Service->>+Repo: Appelle "create(recipe_data)" Repo-->>-Service: Retourne nouvelle "Recipe Model Pydantic" Service-->>-Resolver: Retourne "Recipe Model Pydantic" Resolver-->>-Mutation: Retourne le modele Mutation-->>-API: Construit la reponse JSON API-->>-Client: Reponse JSON ``` #### Exemples de requêtes ```graphql query GetAllRecipes { recipes { id title description } } ``` ```graphql query GetRecipeById { recipe(id: "1") { id title description ingredients { name quantity } } } ``` ```graphql query GetNonExistentRecipe { recipe(id: "99") { id title } } ``` ```graphql mutation AddNewRecipe { addRecipe( input: { title: "Salade César" description: "Une salade fraîche et croquante." ingredients: [ { name: "Laitue romaine", quantity: "1" } { name: "Poulet grillé", quantity: "150g" } { name: "Croûtons", quantity: "1 tasse" } { name: "Parmesan", quantity: "50g" } ] } ) { id title description } } ``` Vous pouvez si vous le souhaitez ajouter d'autres recettes via la mutation `addRecipe` et vérifier qu'elles apparaissent bien dans la liste retournée par la requête `recipes`.