Files
Mathieu LimonierandClaude Opus 5 6a6d745af4 MaBibli 1.0.0
Gestion de bibliothèque personnelle auto-hébergée : catalogue, prêts,
scan de code-barres, consultation hors-ligne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:36:16 +02:00

160 lines
7.6 KiB
C#

using MaBibli.Api.Services.Identite;
using MaBibli.Api.Services.Series;
using MaBibli.Shared.Dtos;
namespace MaBibli.Api.Endpoints;
public static class SeriesEndpoints
{
/// <summary>
/// Sagas, cycles et séries : ce qui se lit dans un ordre, et ce qui manque pour le lire.
/// </summary>
/// <remarks>
/// ⚠️ <b>Aucun point d'entrée n'accepte ni ne rend d'identité</b>, à une exception près :
/// la mise en envies. Les séries sont <b>communes au foyer</b>, comme le catalogue et les
/// prêts — l'ordre de lecture d'une saga ne dépend pas de qui regarde. La liste d'envies,
/// elle, reste personnelle, et c'est le serveur qui sait à qui il parle.
/// <para>
/// La lecture est <b>unique</b> : <c>GET /api/series</c> rend tout l'arbre à plat. Le détail
/// d'une série s'y lit sans second appel, ce qui donne un seul instantané hors-ligne à
/// tenir à jour plutôt qu'un par série consultée.
/// </para>
/// </remarks>
public static IEndpointRouteBuilder MapSeriesEndpoints(this IEndpointRouteBuilder routes)
{
var groupe = routes.MapGroup("/api/series").WithTags("Séries");
groupe.MapGet("/", async (IServiceSeries service, CancellationToken ct) =>
Results.Ok(await service.ListerAsync(ct)))
.WithName("ListerSeries")
.WithSummary("Toutes les séries, à plat, chacune avec ses tomes dans l'ordre de lecture.")
.WithDescription(
"Le client rebâtit l'arbre à partir de « serieParenteId ». Un tome sans "
+ "« livreId » est un tome qu'on ne possède pas : c'est ce que l'écran montre.")
.Produces<IReadOnlyList<SerieDto>>();
groupe.MapPost("/", async (
EnregistrementSerie saisie,
IServiceSeries service,
IFournisseurUtilisateur utilisateurs,
CancellationToken ct) =>
{
var resultat = await service.CreerAsync(saisie, utilisateurs.Obtenir().Identifiant, ct);
return resultat.Erreur is not null
? Results.BadRequest(new { erreur = resultat.Erreur })
: Results.Created($"/api/series/{resultat.Serie!.Id}", resultat.Serie);
})
.WithName("CreerSerie")
.WithSummary("Crée une série. « AjoutePar » est déterminé par le serveur, et n'est qu'une trace.")
.Produces<SerieDto>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest);
groupe.MapPut("/{id:int}", async (
int id, EnregistrementSerie saisie, IServiceSeries service, CancellationToken ct) =>
Repondre(await service.ModifierAsync(id, saisie, ct)))
.WithName("ModifierSerie")
.WithSummary("Renomme la série, ou la range dans un cycle.")
.Produces<SerieDto>()
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status404NotFound);
groupe.MapDelete("/{id:int}", async (int id, IServiceSeries service, CancellationToken ct) =>
await service.SupprimerAsync(id, ct) ? Results.NoContent() : Results.NotFound())
.WithName("SupprimerSerie")
.WithSummary("Supprime la série. Ses sous-séries remontent à la racine, elles ne sont pas détruites.")
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status404NotFound);
groupe.MapPost("/{id:int}/elements", async (
int id, AjoutElementSerie saisie, IServiceSeries service, CancellationToken ct) =>
Repondre(await service.AjouterElementAsync(id, saisie, ct)))
.WithName("AjouterElementSerie")
.WithSummary("Ajoute un tome en fin de série : un livre du catalogue, ou un simple titre.")
.Produces<SerieDto>()
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status404NotFound);
groupe.MapPut("/{id:int}/ordre", async (
int id, OrdreElementsSerie ordre, IServiceSeries service, CancellationToken ct) =>
Repondre(await service.ReordonnerAsync(id, ordre.Ids, ct)))
.WithName("ReordonnerSerie")
.WithSummary("Fixe l'ordre de lecture — la liste entière des tomes, dans l'ordre voulu.")
.Produces<SerieDto>()
.Produces(StatusCodes.Status404NotFound);
groupe.MapPut("/{id:int}/sous-series/ordre", async (
int id, OrdreSeries ordre, IServiceSeries service, CancellationToken ct) =>
Repondre(await service.ReordonnerSousSeriesAsync(id, ordre.Ids, ct)))
.WithName("ReordonnerSousSeries")
.WithSummary("Fixe l'ordre de lecture des séries directement contenues dans un cycle.")
.Produces<SerieDto>()
.Produces(StatusCodes.Status404NotFound);
groupe.MapPut("/elements/{elementId:int}/livre", async (
int elementId,
RattachementLivre rattachement,
IServiceSeries service,
CancellationToken ct) =>
Repondre(await service.RattacherLivreAsync(elementId, rattachement.LivreId, ct)))
.WithName("RattacherLivreSerie")
.WithSummary("Rattache un livre possédé à une place, ou l'en détache (« livreId » nul).")
.WithDescription(
"Détacher laisse un trou NOMMÉ : le titre du tome est conservé, il ne dépend "
+ "pas de l'exemplaire.")
.Produces<SerieDto>()
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status404NotFound);
groupe.MapDelete("/elements/{elementId:int}", async (
int elementId, IServiceSeries service, CancellationToken ct) =>
await service.RetirerElementAsync(elementId, ct)
? Results.NoContent()
: Results.NotFound())
.WithName("RetirerElementSerie")
.WithSummary("Retire une place de la série. Le livre, lui, reste au catalogue.")
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status404NotFound);
groupe.MapPost("/elements/{elementId:int}/souhait", async (
int elementId,
IServiceSeries service,
IFournisseurUtilisateur utilisateurs,
CancellationToken ct) =>
{
// ⚠️ Le seul endroit où une série (commune) touche à une liste d'envies
// (personnelle). L'envie créée est celle de l'appelant, jamais celle du foyer.
var resultat = await service.MettreEnEnviesAsync(
elementId, utilisateurs.Obtenir().Identifiant, ct);
return resultat.Erreur is not null
? Results.BadRequest(new { erreur = resultat.Erreur })
: Results.Ok(resultat.Souhait);
})
.WithName("SouhaiterTomeManquant")
.WithSummary("Met un tome manquant dans la liste d'envies de l'appelant.")
.Produces<SouhaitDto>()
.Produces(StatusCodes.Status400BadRequest);
return routes;
}
private static IResult Repondre(ResultatSerie resultat)
{
if (resultat.EstIntrouvable)
{
return Results.NotFound();
}
return resultat.Erreur is not null
? Results.BadRequest(new { erreur = resultat.Erreur })
: Results.Ok(resultat.Serie);
}
}
/// <summary>Charge utile du rattachement d'un livre à une place. <c>null</c> détache.</summary>
public record RattachementLivre
{
public int? LivreId { get; set; }
}