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, grade, made by, /// comments and status (stored and display names). Blank means no filter. /// public string Search { get; init; } = ""; /// Only nests in this status; null means any. public NestStatus? Status { get; init; } /// /// Only nests of this material name, compared by /// (case and spacing ignored); null means any. /// public string? Material { get; init; } /// /// Only nests of this grade, compared like ; null means any and /// blank means nests with no grade recorded. /// public string? MaterialGrade { get; init; } /// Only nests of this thickness, compared at four decimals; null means any. public double? Thickness { 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."; 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)); } }