fix(434): accurate endpoint description (DB-sourced) + tests for case-insensitivity, tag exclusion, distinct
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 12s
PR Gates / Docs update reminder (pull_request) Successful in 16s
PR Gates / decisions lifecycle (pull_request) Successful in 19s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m24s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m43s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 13m40s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 15m38s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 15m50s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 12s
PR Gates / Docs update reminder (pull_request) Successful in 16s
PR Gates / decisions lifecycle (pull_request) Successful in 19s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m24s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m43s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 13m40s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 15m38s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 15m50s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,8 +26,6 @@ public class GetSearchFieldValuesHandler(IDbContextFactory<TvContext> dbContextF
|
||||
int limit = request.Limit <= 0 ? DefaultLimit : Math.Clamp(request.Limit, 1, MaxLimit);
|
||||
string qLower = (request.Query ?? string.Empty).ToLower();
|
||||
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
// in-memory special cases (no DB query needed)
|
||||
switch (request.Name)
|
||||
{
|
||||
@@ -37,9 +35,14 @@ public class GetSearchFieldValuesHandler(IDbContextFactory<TvContext> dbContextF
|
||||
case "video_dynamic_range":
|
||||
return new SearchFieldValuesResponseModel(
|
||||
FilterSortTake(["hdr", "sdr"], qLower, limit));
|
||||
case "content_rating":
|
||||
return new SearchFieldValuesResponseModel(
|
||||
await GetContentRatingValues(dbContext, qLower, limit, cancellationToken));
|
||||
}
|
||||
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if (request.Name == "content_rating")
|
||||
{
|
||||
return new SearchFieldValuesResponseModel(
|
||||
await GetContentRatingValues(dbContext, qLower, limit, cancellationToken));
|
||||
}
|
||||
|
||||
IQueryable<string> source = GetSource(dbContext, request.Name);
|
||||
|
||||
@@ -125,4 +125,75 @@ public class GetSearchFieldValuesHandlerTests
|
||||
|
||||
result.IsNone.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Matches_Case_Insensitive_Prefix()
|
||||
{
|
||||
await using (TvContext context = _db.CreateContext())
|
||||
{
|
||||
context.Set<Genre>().AddRange(
|
||||
new Genre { Name = "Action" },
|
||||
new Genre { Name = "Comedy" });
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var handler = new GetSearchFieldValuesHandler(_db.Factory);
|
||||
|
||||
Option<SearchFieldValuesResponseModel> result = await handler.Handle(
|
||||
new GetSearchFieldValues("genre", "a", 50),
|
||||
CancellationToken.None);
|
||||
|
||||
result.IsSome.ShouldBeTrue();
|
||||
result.IfSome(r => r.Values.ShouldBe(new List<string> { "Action" }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Excludes_Network_And_Country_Tags_From_Tag_Field_And_Routes_Network_Tags_To_Network_Field()
|
||||
{
|
||||
await using (TvContext context = _db.CreateContext())
|
||||
{
|
||||
context.Set<Tag>().AddRange(
|
||||
new Tag { Name = "PlainTag" },
|
||||
new Tag { Name = "HBO", ExternalTypeId = Tag.PlexNetworkTypeId },
|
||||
new Tag { Name = "USA", ExternalTypeId = Tag.NfoCountryTypeId });
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var handler = new GetSearchFieldValuesHandler(_db.Factory);
|
||||
|
||||
Option<SearchFieldValuesResponseModel> tagResult = await handler.Handle(
|
||||
new GetSearchFieldValues("tag", string.Empty, 50),
|
||||
CancellationToken.None);
|
||||
|
||||
tagResult.IsSome.ShouldBeTrue();
|
||||
tagResult.IfSome(r => r.Values.ShouldBe(new List<string> { "PlainTag" }));
|
||||
|
||||
Option<SearchFieldValuesResponseModel> networkResult = await handler.Handle(
|
||||
new GetSearchFieldValues("network", string.Empty, 50),
|
||||
CancellationToken.None);
|
||||
|
||||
networkResult.IsSome.ShouldBeTrue();
|
||||
networkResult.IfSome(r => r.Values.ShouldBe(new List<string> { "HBO" }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Dedupes_Repeated_Values()
|
||||
{
|
||||
await using (TvContext context = _db.CreateContext())
|
||||
{
|
||||
context.Set<Genre>().AddRange(
|
||||
new Genre { Name = "Action" },
|
||||
new Genre { Name = "Action" });
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var handler = new GetSearchFieldValuesHandler(_db.Factory);
|
||||
|
||||
Option<SearchFieldValuesResponseModel> result = await handler.Handle(
|
||||
new GetSearchFieldValues("genre", string.Empty, 50),
|
||||
CancellationToken.None);
|
||||
|
||||
result.IsSome.ShouldBeTrue();
|
||||
result.IfSome(r => r.Values.ShouldBe(new List<string> { "Action" }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,11 +191,12 @@ public class SearchController(IMediator mediator) : ControllerBase
|
||||
|
||||
[HttpGet("/api/v1/search/fields/{name}/values", Name = "GetSearchFieldValues")]
|
||||
[Tags("Search")]
|
||||
[EndpointSummary("List distinct term values for a text search field")]
|
||||
[EndpointSummary("List distinct database values for a text search field")]
|
||||
[EndpointDescription(
|
||||
"Returns distinct term values from the search index for the given text field, filtered by an " +
|
||||
"Returns distinct whole values from the database for the given text field, filtered by an " +
|
||||
"optional case-insensitive prefix. Powers the visual rule builder's facet-value typeahead. " +
|
||||
"404 when the field is unknown or is not a text field.")]
|
||||
"404 when the field is unknown, is not a text field, or is a text field with no distinct-value " +
|
||||
"source.")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(typeof(SearchFieldValuesResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
|
||||
@@ -18040,8 +18040,8 @@
|
||||
"tags": [
|
||||
"Search"
|
||||
],
|
||||
"summary": "List distinct term values for a text search field",
|
||||
"description": "Returns distinct term values from the search index for the given text field, filtered by an optional case-insensitive prefix. Powers the visual rule builder's facet-value typeahead. 404 when the field is unknown or is not a text field.",
|
||||
"summary": "List distinct database values for a text search field",
|
||||
"description": "Returns distinct whole values from the database for the given text field, filtered by an optional case-insensitive prefix. Powers the visual rule builder's facet-value typeahead. 404 when the field is unknown, is not a text field, or is a text field with no distinct-value source.",
|
||||
"operationId": "GetSearchFieldValues",
|
||||
"parameters": [
|
||||
{
|
||||
|
||||
@@ -338,7 +338,7 @@
|
||||
| GET | `/api/v1/search/artists` | SearchArtists | Search artists by name |
|
||||
| GET | `/api/v1/search/collections` | SearchCollections | Search collections by name |
|
||||
| GET | `/api/v1/search/fields` | GetSearchFields | List the filterable fields for the visual rule builder |
|
||||
| GET | `/api/v1/search/fields/{name}/values` | GetSearchFieldValues | List distinct term values for a text search field |
|
||||
| GET | `/api/v1/search/fields/{name}/values` | GetSearchFieldValues | List distinct database values for a text search field |
|
||||
| GET | `/api/v1/search/multi-collections` | SearchMultiCollections | Search multi collections by name |
|
||||
| GET | `/api/v1/search/smart-collections` | SearchSmartCollections | Search smart collections by name |
|
||||
| GET | `/api/v1/search/television-seasons` | SearchTelevisionSeasons | Search television seasons by name |
|
||||
|
||||
Reference in New Issue
Block a user