feat(server): drawing library schema and routes

The server becomes the system of record for shared drawings (cut type,
thickness, material and grade, customer, notes). Schema v4 adds `drawings`
(current row) and `drawing_revisions` (immutable) to the same SQLite file under
the existing monitor; the nest tables are untouched and a v3 upgrade keeps every
nest row byte-identical.

`/api/drawings` follows the nest revision contract: client-allocated ids,
If-Match on every existing-drawing write (428/400/409 with no state change),
archive instead of delete, and one transaction for promotion plus snapshot.
Geometry is an opaque package (a zero-plate .nest) the server never parses.
Enum fields accept single names only, because the JSON enum converter would
otherwise accept numbers and comma combinations. Material, grade and customer
filters match SharedListNames keys so spacing and case cannot split a drawing
family; thickness is stored and compared at four decimals.

Shared bounds live in OpenNest.Data so the client can refuse the same requests
before sending. If-Match parsing moves to one helper used by nests and drawings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-16 15:33:02 -04:00
1 parent 0ee5b9d93e
commit 76133193df
16 files changed
+2024 -19

No files matched your search

+6 -17
View File
@@ -70,7 +70,7 @@ app.MapGet("/api/nests/{id:guid}/revisions/{revision:int}/file", (Guid id, int r
app.MapPut("/api/nests/{id:guid}/file", async (Guid id, HttpContext http, NestDatabase db) =>
{
var precondition = ReadPrecondition(http.Request, out var expected);
var precondition = IfMatch.Read(http.Request, out var expected);
if (precondition is not null) return precondition;
var (record, file, error) = await ReadMultipart(http);
if (error is not null) return error;
@@ -87,7 +87,7 @@ app.MapPut("/api/nests/{id:guid}/file", async (Guid id, HttpContext http, NestDa
app.MapPut("/api/nests/{id:guid}/metadata", async (Guid id, HttpRequest request, NestDatabase db) =>
{
var error = ReadPrecondition(request, out var expected);
var error = IfMatch.Read(request, out var expected);
if (error is not null) return error;
NestRecord? record;
try { record = await request.ReadFromJsonAsync<NestRecord>(); }
@@ -120,7 +120,7 @@ app.MapGet("/api/nests/deleted/{id:guid}/revisions/{revision:int}/file", (Guid i
app.MapPost("/api/nests/{id:guid}/revisions/{revision:int}/restore", (Guid id, int revision, HttpRequest request, NestDatabase db) =>
{
var error = ReadPrecondition(request, out var expected);
var error = IfMatch.Read(request, out var expected);
if (error is not null) return error;
if (revision < 1) return Results.BadRequest("Invalid revision.");
try
@@ -131,24 +131,13 @@ app.MapPost("/api/nests/{id:guid}/revisions/{revision:int}/restore", (Guid id, i
catch (RevisionConflictException conflict) { return Results.Json(conflict.Current, statusCode: 409); }
});
app.Run();
app.MapDrawingLibrary();
static IResult? ReadPrecondition(HttpRequest request, out int revision)
{
revision = 0;
var values = request.Headers.IfMatch;
if (values.Count == 0) return Results.Json("Upgrade/reload required: send If-Match with the current revision.", statusCode: 428);
var text = values.Count == 1 ? values[0] : null;
if (text is null || text.Length < 3 || text[0] != '"' || text[^1] != '"' ||
!text[1..^1].All(c => c is >= '0' and <= '9') ||
!int.TryParse(text[1..^1], out revision) || revision < 1)
return Results.BadRequest("If-Match must contain one quoted positive integer revision.");
return null;
}
app.Run();
static IResult ChangeVisibility(Guid id, bool deleted, HttpRequest request, NestDatabase db)
{
var error = ReadPrecondition(request, out var expected);
var error = IfMatch.Read(request, out var expected);
if (error is not null) return error;
try
{