Files
OpenNest/OpenNest.Data/DrawingLibraryQuery.cs
T
ajandClaude Opus 5.5 76133193df 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>
2026-10-16 15:33:02 -04:00

69 lines
2.9 KiB
C#

using OpenNest;
namespace OpenNest.Data;
/// <summary>
/// One bounded page request for browsing the drawing library
/// (<c>GET /api/drawings/query</c>). The server filters and pages in SQL; the
/// client and server share these bounds.
/// </summary>
public sealed class DrawingLibraryQuery
{
public const int DefaultLimit = 100;
public const int MaxLimit = 500;
public const int MaxSearchLength = 200;
/// <summary>
/// Case-insensitive (ASCII) substring of name, customer or notes. Blank means no filter.
/// </summary>
public string Search { get; init; } = "";
/// <summary>Exact customer, compared by <see cref="SharedListNames.Key"/>. Blank means any.</summary>
public string Customer { get; init; } = "";
/// <summary>Exact material name, compared by <see cref="SharedListNames.Key"/>. Blank means any.</summary>
public string MaterialName { get; init; } = "";
/// <summary>Exact grade, compared by <see cref="SharedListNames.Key"/>. Blank means any.</summary>
public string MaterialGrade { get; init; } = "";
/// <summary>Exact thickness after rounding to four decimals; null means any.</summary>
public double? Thickness { get; init; }
public CutType? CutType { get; init; }
public bool IncludeArchived { get; init; }
/// <summary>Column to order by; ties are broken by id in the same direction.</summary>
public DrawingSortField Sort { get; init; } = DrawingSortField.SavedAt;
/// <summary>Largest/newest/Z first when true (the default, newest saved first).</summary>
public bool Descending { get; init; } = true;
public int Offset { get; init; }
public int Limit { get; init; } = DefaultLimit;
/// <summary>The search text as matched: trimmed, never null.</summary>
public string NormalizedSearch => (Search ?? "").Trim();
/// <summary>The first violated bound, or null when the query may be sent.</summary>
public string? GetValidationError()
{
if (!Enum.IsDefined(Sort))
return "sort must be a supported column.";
if (CutType is { } cutType && !Enum.IsDefined(cutType))
return "cutType must be Laser, Waterjet or Plasma.";
if (Thickness is { } thickness && (!double.IsFinite(thickness) || thickness < 0))
return "thickness must be a finite number, zero or greater.";
if (Offset < 0)
return "offset must be zero or greater.";
if (Limit < 1 || Limit > MaxLimit)
return $"limit must be between 1 and {MaxLimit}.";
return DrawingLibraryRules.TextError("search", NormalizedSearch, MaxSearchLength)
?? DrawingLibraryRules.TextError("customer", Customer ?? "", DrawingLibraryRules.MaxTextLength)
?? DrawingLibraryRules.TextError("materialName", MaterialName ?? "", DrawingLibraryRules.MaxTextLength)
?? DrawingLibraryRules.TextError("materialGrade", MaterialGrade ?? "", DrawingLibraryRules.MaxTextLength);
}
}