From 7627468b82981177e879f522295a5840b5c31ec7 Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 01:00:35 +0200 Subject: [PATCH] DATA-2: optional list params + normalize frontend's '==' Gridify operator - GridifyParams wrapper: nullable page/pageSize/filter/orderBy so list endpoints work with no query params (Gridify's non-nullable int Page made [AsParameters] treat them as required -> 400). Defaults page=1,pageSize=20. - Normalize incoming filter '==' -> '=' : the frontend gridify.ts emits '==' for equals (its convention) but Gridify's equals is '='. Safe (values are escaped; != >= <= =* contain no '=='). Fixes status==Active (frontend default that 500'd). Co-Authored-By: Claude Opus 4.8 (1M context) --- GerbilManagerWebAPI/Common/PagedResult.cs | 37 ++++++++++++++++--- .../Endpoints/ColorVarietyEndpoints.cs | 2 +- .../Endpoints/ContactEndpoints.cs | 2 +- .../Endpoints/EnclosureEndpoints.cs | 2 +- .../Endpoints/GerbilEndpoints.cs | 2 +- .../Endpoints/LitterEndpoints.cs | 2 +- .../Endpoints/RecordEndpoints.cs | 4 +- 7 files changed, 39 insertions(+), 12 deletions(-) diff --git a/GerbilManagerWebAPI/Common/PagedResult.cs b/GerbilManagerWebAPI/Common/PagedResult.cs index 3194e7d..5a26a9b 100644 --- a/GerbilManagerWebAPI/Common/PagedResult.cs +++ b/GerbilManagerWebAPI/Common/PagedResult.cs @@ -6,22 +6,49 @@ namespace GerbilManagerWebAPI.Common /// Standard paged list envelope returned by every list endpoint. public record PagedResult(IReadOnlyList Items, int TotalCount, int Page, int PageSize); + /// + /// Query-string parameters for list endpoints. All optional (nullable) so the + /// frontend can call a list with no params; Gridify's own GridifyQuery has + /// non-nullable int Page/PageSize which [AsParameters] would make REQUIRED. + /// Bound via [AsParameters]: ?filter=…&orderBy=…&page=1&pageSize=20. + /// + public class GridifyParams + { + public string? Filter { get; set; } + public string? OrderBy { get; set; } + public int? Page { get; set; } + public int? PageSize { get; set; } + + public GridifyQuery ToQuery() => new() + { + Filter = NormalizeFilter(Filter), + OrderBy = OrderBy, + Page = Page is > 0 ? Page.Value : 1, + PageSize = PageSize is > 0 ? PageSize.Value : 20, + }; + + // The frontend's gridify.ts emits "==" for equals (its documented convention), + // but Gridify's equals operator is a single "=". Translate "==" -> "=". This is + // safe because the frontend backslash-escapes any "=" inside values, and the + // other operators it uses ( != >= <= =* ) contain no literal "==". + private static string? NormalizeFilter(string? filter) => + string.IsNullOrEmpty(filter) ? filter : filter.Replace("==", "="); + } + public static class QueryableExtensions { /// /// Apply a Gridify query (filter/order/page) to an EF query and project each /// row to a DTO, returning the standard paged envelope. Filter/orderBy names /// are the ENTITY property names (case-insensitive), e.g. "status==Active", - /// "orderBy=dateOfBirth", "litterId==...". + /// "orderBy=dateOfBirth", "litterId==…". /// public static async Task> ToPagedResultAsync( this IQueryable source, - GridifyQuery query, + GridifyParams parameters, Func map) { - query.Page = query.Page <= 0 ? 1 : query.Page; - query.PageSize = query.PageSize <= 0 ? 20 : query.PageSize; - + var query = parameters.ToQuery(); Paging paging = await source.GridifyAsync(query); var items = paging.Data.Select(map).ToList(); return new PagedResult(items, paging.Count, query.Page, query.PageSize); diff --git a/GerbilManagerWebAPI/Endpoints/ColorVarietyEndpoints.cs b/GerbilManagerWebAPI/Endpoints/ColorVarietyEndpoints.cs index 630b345..7f660a8 100644 --- a/GerbilManagerWebAPI/Endpoints/ColorVarietyEndpoints.cs +++ b/GerbilManagerWebAPI/Endpoints/ColorVarietyEndpoints.cs @@ -13,7 +13,7 @@ namespace GerbilManagerWebAPI.Endpoints { var group = app.MapGroup("/color-varieties").WithTags("ColorVarieties"); - group.MapGet("/", async ([AsParameters] GridifyQuery query, ApplicationContext db) => + group.MapGet("/", async ([AsParameters] GridifyParams query, ApplicationContext db) => TypedResults.Ok(await db.ColorVarieties.AsNoTracking().OrderBy(v => v.SortOrder) .ToPagedResultAsync(query, ToDto))); diff --git a/GerbilManagerWebAPI/Endpoints/ContactEndpoints.cs b/GerbilManagerWebAPI/Endpoints/ContactEndpoints.cs index bdef185..6091e45 100644 --- a/GerbilManagerWebAPI/Endpoints/ContactEndpoints.cs +++ b/GerbilManagerWebAPI/Endpoints/ContactEndpoints.cs @@ -13,7 +13,7 @@ namespace GerbilManagerWebAPI.Endpoints { var group = app.MapGroup("/contacts").WithTags("Contacts"); - group.MapGet("/", async ([AsParameters] GridifyQuery query, ApplicationContext db) => + group.MapGet("/", async ([AsParameters] GridifyParams query, ApplicationContext db) => TypedResults.Ok(await db.Contacts.AsNoTracking().ToPagedResultAsync(query, ToDto))); group.MapGet("/{id:guid}", async Task, NotFound>> (Guid id, ApplicationContext db) => diff --git a/GerbilManagerWebAPI/Endpoints/EnclosureEndpoints.cs b/GerbilManagerWebAPI/Endpoints/EnclosureEndpoints.cs index 9b15b2d..47f79c7 100644 --- a/GerbilManagerWebAPI/Endpoints/EnclosureEndpoints.cs +++ b/GerbilManagerWebAPI/Endpoints/EnclosureEndpoints.cs @@ -13,7 +13,7 @@ namespace GerbilManagerWebAPI.Endpoints { var group = app.MapGroup("/enclosures").WithTags("Enclosures"); - group.MapGet("/", async ([AsParameters] GridifyQuery query, ApplicationContext db) => + group.MapGet("/", async ([AsParameters] GridifyParams query, ApplicationContext db) => TypedResults.Ok(await db.Enclosures.AsNoTracking().ToPagedResultAsync(query, ToDto))); group.MapGet("/{id:guid}", async Task, NotFound>> (Guid id, ApplicationContext db) => diff --git a/GerbilManagerWebAPI/Endpoints/GerbilEndpoints.cs b/GerbilManagerWebAPI/Endpoints/GerbilEndpoints.cs index f895266..b96e30f 100644 --- a/GerbilManagerWebAPI/Endpoints/GerbilEndpoints.cs +++ b/GerbilManagerWebAPI/Endpoints/GerbilEndpoints.cs @@ -15,7 +15,7 @@ namespace GerbilManagerWebAPI.Endpoints var group = app.MapGroup("/gerbils").WithTags("Gerbils"); // GET /gerbils (Gridify: filter/order/page; e.g. status==Active, litterId==…, orderBy=name) - group.MapGet("/", async ([AsParameters] GridifyQuery query, ApplicationContext db) => + group.MapGet("/", async ([AsParameters] GridifyParams query, ApplicationContext db) => TypedResults.Ok(await db.Gerbils.AsNoTracking() .ToPagedResultAsync(query, ToDto))); diff --git a/GerbilManagerWebAPI/Endpoints/LitterEndpoints.cs b/GerbilManagerWebAPI/Endpoints/LitterEndpoints.cs index a66b1f1..e6350f5 100644 --- a/GerbilManagerWebAPI/Endpoints/LitterEndpoints.cs +++ b/GerbilManagerWebAPI/Endpoints/LitterEndpoints.cs @@ -14,7 +14,7 @@ namespace GerbilManagerWebAPI.Endpoints var group = app.MapGroup("/litters").WithTags("Litters"); // GET /litters (Gridify: date range + orderBy=date supported on the DateOnly column) - group.MapGet("/", async ([AsParameters] GridifyQuery query, ApplicationContext db) => + group.MapGet("/", async ([AsParameters] GridifyParams query, ApplicationContext db) => TypedResults.Ok(await db.Litters.AsNoTracking().ToPagedResultAsync(query, ToDto))); group.MapGet("/{id:guid}", async Task, NotFound>> (Guid id, ApplicationContext db) => diff --git a/GerbilManagerWebAPI/Endpoints/RecordEndpoints.cs b/GerbilManagerWebAPI/Endpoints/RecordEndpoints.cs index 5515573..1bffd35 100644 --- a/GerbilManagerWebAPI/Endpoints/RecordEndpoints.cs +++ b/GerbilManagerWebAPI/Endpoints/RecordEndpoints.cs @@ -14,7 +14,7 @@ namespace GerbilManagerWebAPI.Endpoints var group = app.MapGroup("/health-records").WithTags("HealthRecords"); // GET /health-records?filter=gerbilId==… - group.MapGet("/", async ([AsParameters] GridifyQuery query, ApplicationContext db) => + group.MapGet("/", async ([AsParameters] GridifyParams query, ApplicationContext db) => TypedResults.Ok(await db.HealthRecords.AsNoTracking().ToPagedResultAsync(query, ToDto))); group.MapGet("/{id:guid}", async Task, NotFound>> (Guid id, ApplicationContext db) => @@ -66,7 +66,7 @@ namespace GerbilManagerWebAPI.Endpoints { var group = app.MapGroup("/weight-records").WithTags("WeightRecords"); - group.MapGet("/", async ([AsParameters] GridifyQuery query, ApplicationContext db) => + group.MapGet("/", async ([AsParameters] GridifyParams query, ApplicationContext db) => TypedResults.Ok(await db.WeightRecords.AsNoTracking().ToPagedResultAsync(query, ToDto))); group.MapGet("/{id:guid}", async Task, NotFound>> (Guid id, ApplicationContext db) =>