mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 07:11:06 -04:00
A release that merges into an existing draft needs the drafts of the same material group. The nest query can now filter exactly on status, material, grade and thickness. Material and grade compare like the shared material lists (case and spacing ignored) through a SQLite function, so no key columns or backfill are needed; a blank grade asks for nests without one, and thickness matches at the four decimals drawings are stored with. The client sends the filters only when set, so plain browsing keeps working against servers that do not know them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
74 lines
3.3 KiB
C#
74 lines
3.3 KiB
C#
namespace OpenNest.Data;
|
|
|
|
/// <summary>
|
|
/// One bounded page request for browsing saved nests (<c>GET /api/nests/query</c>).
|
|
/// The server filters and pages in SQL; the client and server share these bounds.
|
|
/// </summary>
|
|
public sealed class NestQuery
|
|
{
|
|
public const int DefaultLimit = 100;
|
|
public const int MaxLimit = 500;
|
|
public const int MaxSearchLength = 200;
|
|
|
|
/// <summary>
|
|
/// Case-insensitive substring matched against name, customer, material, grade, made by,
|
|
/// comments and status (stored and display names). Blank means no filter.
|
|
/// </summary>
|
|
public string Search { get; init; } = "";
|
|
|
|
/// <summary>Only nests in this status; null means any.</summary>
|
|
public NestStatus? Status { get; init; }
|
|
|
|
/// <summary>
|
|
/// Only nests of this material name, compared by <see cref="SharedListNames.Key"/>
|
|
/// (case and spacing ignored); null means any.
|
|
/// </summary>
|
|
public string? Material { get; init; }
|
|
|
|
/// <summary>
|
|
/// Only nests of this grade, compared like <see cref="Material"/>; null means any and
|
|
/// blank means nests with no grade recorded.
|
|
/// </summary>
|
|
public string? MaterialGrade { get; init; }
|
|
|
|
/// <summary>Only nests of this thickness, compared at four decimals; null means any.</summary>
|
|
public double? Thickness { get; init; }
|
|
|
|
/// <summary>Column to order by; ties are broken by id in the same direction.</summary>
|
|
public NestSortField Sort { get; init; } = NestSortField.SavedAt;
|
|
|
|
/// <summary>Largest/newest/Z first when true (the default, newest saved first).</summary>
|
|
public bool Descending { get; init; } = true;
|
|
|
|
/// <summary>Number of matching records to skip, zero or greater.</summary>
|
|
public int Offset { get; init; }
|
|
|
|
/// <summary>Maximum records returned, 1 to <see cref="MaxLimit"/>.</summary>
|
|
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 (Offset < 0)
|
|
return "offset must be zero or greater.";
|
|
if (Limit < 1 || Limit > MaxLimit)
|
|
return $"limit must be between 1 and {MaxLimit}.";
|
|
if (NormalizedSearch.Length > MaxSearchLength)
|
|
return $"search must be at most {MaxSearchLength} characters.";
|
|
// SQLite LIKE stops at an embedded NUL, which would turn the rest of the text into a wildcard.
|
|
if (NormalizedSearch.Contains('\0'))
|
|
return "search must not contain NUL characters.";
|
|
if (Status is { } status && !Enum.IsDefined(status))
|
|
return "status must be a supported status.";
|
|
if (Thickness is { } thickness && (!double.IsFinite(thickness) || thickness < 0))
|
|
return "thickness must be a finite number, zero or greater.";
|
|
return (Material is null ? null : DrawingLibraryRules.TextError("material", Material, DrawingLibraryRules.MaxTextLength))
|
|
?? (MaterialGrade is null ? null : DrawingLibraryRules.TextError("materialGrade", MaterialGrade, DrawingLibraryRules.MaxTextLength));
|
|
}
|
|
}
|