Files
OpenNest/OpenNest.Server/ReleaseRoutes.cs
T
ajandClaude Opus 5.5 28db6ae807 feat(server): release several drawing sets at once, merge into drafts, and undo
A release can now cover several sets, each with its own kit count, so parts
from different kits share sheets. It can also merge into an existing draft
nest instead of always creating one. Schema v9 replaces the single-set
release tables: releases, their set lines, their nests (created or merged,
with the nest revision before and after) and the drawing quantities each set
added to each nest, so a kit stays traceable once kits share a nest. The v8
migration turns each existing release into one set line with created nests.

Undo runs on the server in one transaction: created nests are soft-deleted
and merged nests restored to their pre-merge revision, but only while each
still holds what the release wrote. Otherwise nothing changes and the
changed nests are listed, so the operator can skip them. A nest restored to
that revision counts as unchanged, which lets releases sharing a nest be
undone newest first.

The old /api/sets/{id}/releases and /api/nests/{id}/release routes are
replaced by /api/releases; no tagged desktop used them. The desktop still
releases one set at a time until the release dialog is reworked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:25:52 -04:00

57 lines
2.7 KiB
C#

using OpenNest.Data;
namespace OpenNest.Server;
/// <summary>
/// <c>/api/releases</c>: append-only records of drawing set releases into nests, and their
/// undo. A release is posted after every nest it names is uploaded; its id is allocated by
/// the client, so a post whose reply was lost can be sent again (409 with the stored release).
/// </summary>
internal static class ReleaseRoutes
{
public static void MapReleases(this IEndpointRouteBuilder app)
{
app.MapPost("/api/releases", async (HttpRequest request, NestDatabase db) =>
{
var (json, error) = await DrawingSetRoutes.ReadJson(request, "release");
if (error is not null) return error;
if (!ReleaseRequest.TryReadRelease(json, out var release, out var invalid))
return Results.BadRequest(invalid);
if (release.Id == Guid.Empty)
return Results.BadRequest("id is required: allocate it before posting the release.");
try { return Results.Ok(db.InsertRelease(release)); }
catch (SetReleaseConflictException conflict)
{
return Results.Json(conflict.Current, statusCode: StatusCodes.Status409Conflict);
}
catch (ArgumentException rejected) { return Results.BadRequest(rejected.Message); }
});
app.MapGet("/api/releases/query", (HttpRequest request, NestDatabase db) =>
ReleaseRequest.TryParseQuery(request.Query, out var setId, out var nestId, out var offset, out var limit, out var error)
? Results.Ok(db.QueryReleases(setId, nestId, offset, limit))
: Results.BadRequest(error));
app.MapGet("/api/releases/{id:guid}", (Guid id, NestDatabase db) =>
db.GetRelease(id) is { } release ? Results.Ok(release) : Results.NotFound());
app.MapPost("/api/releases/{id:guid}/undo", async (Guid id, HttpRequest request, NestDatabase db) =>
{
var (json, error) = await DrawingSetRoutes.ReadJson(request, "undo request");
if (error is not null) return error;
if (!ReleaseRequest.TryReadUndo(json, out var undo, out var invalid))
return Results.BadRequest(invalid);
try
{
var undone = db.UndoRelease(id, undo);
return undone is null ? Results.NotFound() : Results.Ok(undone);
}
catch (ReleaseUndoRefusedException conflict)
{
return Results.Json(conflict.Conflict, statusCode: StatusCodes.Status409Conflict);
}
catch (ArgumentException rejected) { return Results.BadRequest(rejected.Message); }
});
}
}