namespace OpenNest.Data;
///
/// One bounded page request for browsing saved nests (GET /api/nests/query).
/// The server filters and pages in SQL; the client and server share these bounds.
///
public sealed class NestQuery
{
public const int DefaultLimit = 100;
public const int MaxLimit = 500;
public const int MaxSearchLength = 200;
///
/// Case-insensitive substring matched against name, customer, material, made by,
/// comments and status (stored and display names). Blank means no filter.
///
public string Search { get; init; } = "";
/// Column to order by; ties are broken by id in the same direction.
public NestSortField Sort { get; init; } = NestSortField.SavedAt;
/// Largest/newest/Z first when true (the default, newest saved first).
public bool Descending { get; init; } = true;
/// Number of matching records to skip, zero or greater.
public int Offset { get; init; }
/// Maximum records returned, 1 to .
public int Limit { get; init; } = DefaultLimit;
/// The search text as matched: trimmed, never null.
public string NormalizedSearch => (Search ?? "").Trim();
/// The first violated bound, or null when the query may be sent.
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.";
return null;
}
}