408 lines
13 KiB
Markdown
408 lines
13 KiB
Markdown
# 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)<br><b>Role:</b> Validation et logique interne<br><b>Utilise par:</b> Services, Repository"]
|
|
end
|
|
|
|
subgraph "Schema GraphQL 'Strawberry'"
|
|
B["class Recipe(strawberry.type)<br><b>Role:</b> Definir ce qu'on peut lire 'Query'<br><b>Utilise pour:</b> Retourner des donnees"]
|
|
C["class AddRecipeInput(strawberry.input)<br><b>Role:</b> Definir ce qu'on peut ecrire 'Mutation'<br><b>Utilise pour:</b> 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`.
|