mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 06:01: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>
128 lines
4.4 KiB
C#
128 lines
4.4 KiB
C#
using System.Globalization;
|
|
using OpenNest.Data;
|
|
|
|
namespace OpenNest.Server;
|
|
|
|
/// <summary>
|
|
/// Parses <c>GET /api/nests/query</c> parameters. Unknown, repeated, non-integer or
|
|
/// out-of-range values are rejected rather than ignored or clamped, so a request
|
|
/// can never silently receive a broader result than it asked for.
|
|
/// </summary>
|
|
internal static class NestQueryRequest
|
|
{
|
|
private static readonly string[] Keys =
|
|
{
|
|
"search", "sort", "order", "offset", "limit", "status", "material", "materialGrade", "thickness",
|
|
};
|
|
|
|
public static bool TryParse(IQueryCollection values, out NestQuery query, out string error)
|
|
{
|
|
query = new NestQuery();
|
|
foreach (var key in values.Keys)
|
|
{
|
|
if (!Keys.Contains(key, StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
error = $"Unknown query parameter '{key}'.";
|
|
return false;
|
|
}
|
|
|
|
if (values[key].Count > 1)
|
|
{
|
|
error = $"Query parameter '{key}' was given more than once.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
var sort = NestSortField.SavedAt;
|
|
if (values.TryGetValue("sort", out var sortText)
|
|
&& !TrySortField(sortText.ToString(), out sort))
|
|
{
|
|
error = $"sort must be one of: {string.Join(", ", Enum.GetNames<NestSortField>().Select(CamelCase))}.";
|
|
return false;
|
|
}
|
|
|
|
var descending = true;
|
|
if (values.TryGetValue("order", out var orderText))
|
|
{
|
|
if (string.Equals(orderText, "asc", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
descending = false;
|
|
}
|
|
else if (!string.Equals(orderText, "desc", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
error = "order must be asc or desc.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
NestStatus? status = null;
|
|
if (values.TryGetValue("status", out var statusText))
|
|
{
|
|
if (!DrawingLibraryRequest.TryEnumName<NestStatus>(statusText.ToString(), out var parsed))
|
|
{
|
|
error = "status must be draft, quote, toBeCut or hasBeenCut.";
|
|
return false;
|
|
}
|
|
status = parsed;
|
|
}
|
|
|
|
double? thickness = null;
|
|
if (values.TryGetValue("thickness", out var thicknessText))
|
|
{
|
|
if (!double.TryParse(thicknessText.ToString(), NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent,
|
|
CultureInfo.InvariantCulture, out var parsed))
|
|
{
|
|
error = "thickness must be a number, zero or greater.";
|
|
return false;
|
|
}
|
|
thickness = parsed;
|
|
}
|
|
|
|
var offset = 0;
|
|
var limit = NestQuery.DefaultLimit;
|
|
if (!TryInteger(values, "offset", ref offset, out error) || !TryInteger(values, "limit", ref limit, out error))
|
|
return false;
|
|
|
|
query = new NestQuery
|
|
{
|
|
Search = values["search"].ToString(),
|
|
Status = status,
|
|
// Present but blank is a filter too: a blank grade asks for nests without one.
|
|
Material = values.TryGetValue("material", out var material) ? material.ToString() : null,
|
|
MaterialGrade = values.TryGetValue("materialGrade", out var grade) ? grade.ToString() : null,
|
|
Thickness = thickness,
|
|
Sort = sort,
|
|
Descending = descending,
|
|
Offset = offset,
|
|
Limit = limit,
|
|
};
|
|
error = query.GetValidationError() ?? "";
|
|
return error.Length == 0;
|
|
}
|
|
|
|
// Names only: Enum.TryParse would also accept numbers and undefined values.
|
|
private static bool TrySortField(string text, out NestSortField field)
|
|
{
|
|
field = NestSortField.SavedAt;
|
|
return text.Length > 0
|
|
&& text.All(char.IsAsciiLetter)
|
|
&& Enum.TryParse(text, ignoreCase: true, out field)
|
|
&& Enum.IsDefined(field);
|
|
}
|
|
|
|
private static string CamelCase(string name) => char.ToLowerInvariant(name[0]) + name[1..];
|
|
|
|
private static bool TryInteger(IQueryCollection values, string key, ref int value, out string error)
|
|
{
|
|
error = "";
|
|
if (!values.TryGetValue(key, out var text))
|
|
return true;
|
|
|
|
if (int.TryParse(text.ToString(), NumberStyles.None, CultureInfo.InvariantCulture, out value))
|
|
return true;
|
|
|
|
error = $"{key} must be a non-negative integer.";
|
|
return false;
|
|
}
|
|
}
|