Handle unhappy paths for the multiple items endpoint (#14199)
This commit is contained in:
committed by
GitHub
parent
0f1c2f7022
commit
83230d377c
@@ -1,6 +1,9 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.Builders;
|
||||
using Umbraco.Cms.Api.Delivery.Routing;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers;
|
||||
|
||||
@@ -17,4 +20,25 @@ public abstract class ContentApiControllerBase : DeliveryApiControllerBase
|
||||
ApiPublishedContentCache = apiPublishedContentCache;
|
||||
ApiContentResponseBuilder = apiContentResponseBuilder;
|
||||
}
|
||||
|
||||
protected IActionResult ApiContentQueryOperationStatusResult(ApiContentQueryOperationStatus status) =>
|
||||
status switch
|
||||
{
|
||||
ApiContentQueryOperationStatus.FilterOptionNotFound => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Filter option not found")
|
||||
.WithDetail("One of the attempted 'filter' options does not exist")
|
||||
.Build()),
|
||||
ApiContentQueryOperationStatus.IndexNotFound => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Examine index not found")
|
||||
.WithDetail($"No index found with name {Constants.UmbracoIndexes.DeliveryApiContentIndexName}")
|
||||
.Build()),
|
||||
ApiContentQueryOperationStatus.SelectorOptionNotFound => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Selector option not found")
|
||||
.WithDetail("The attempted 'fetch' option does not exist")
|
||||
.Build()),
|
||||
ApiContentQueryOperationStatus.SortOptionNotFound => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Sort option not found")
|
||||
.WithDetail("One of the attempted 'sort' options does not exist")
|
||||
.Build()),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.New.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers;
|
||||
@@ -31,15 +33,22 @@ public class QueryContentApiController : ContentApiControllerBase
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<IApiContentResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<PagedViewModel<IApiContentResponse>>> Query(
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> Query(
|
||||
string? fetch,
|
||||
[FromQuery] string[] filter,
|
||||
[FromQuery] string[] sort,
|
||||
int skip = 0,
|
||||
int take = 10)
|
||||
{
|
||||
PagedModel<Guid> pagedResult = _apiContentQueryService.ExecuteQuery(fetch, filter, sort, skip, take);
|
||||
Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> queryAttempt = _apiContentQueryService.ExecuteQuery(fetch, filter, sort, skip, take);
|
||||
|
||||
if (queryAttempt.Success is false)
|
||||
{
|
||||
return ApiContentQueryOperationStatusResult(queryAttempt.Status);
|
||||
}
|
||||
|
||||
PagedModel<Guid> pagedResult = queryAttempt.Result;
|
||||
IEnumerable<IPublishedContent> contentItems = ApiPublishedContentCache.GetByIds(pagedResult.Items);
|
||||
IApiContentResponse[] apiContentItems = contentItems.Select(ApiContentResponseBuilder.Build).ToArray();
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using Umbraco.Cms.Api.Delivery.Indexing.Sorts;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Infrastructure.Examine;
|
||||
using Umbraco.New.Cms.Core.Models;
|
||||
|
||||
@@ -39,13 +40,13 @@ internal sealed class ApiContentQueryService : IApiContentQueryService // Examin
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public PagedModel<Guid> ExecuteQuery(string? fetch, IEnumerable<string> filters, IEnumerable<string> sorts, int skip, int take)
|
||||
public Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> ExecuteQuery(string? fetch, IEnumerable<string> filters, IEnumerable<string> sorts, int skip, int take)
|
||||
{
|
||||
var emptyResult = new PagedModel<Guid>();
|
||||
|
||||
if (!_examineManager.TryGetIndex(Constants.UmbracoIndexes.DeliveryApiContentIndexName, out IIndex? apiIndex))
|
||||
{
|
||||
return emptyResult;
|
||||
return Attempt.FailWithStatus(ApiContentQueryOperationStatus.IndexNotFound, emptyResult);
|
||||
}
|
||||
|
||||
IQuery baseQuery = apiIndex.Searcher.CreateQuery();
|
||||
@@ -56,14 +57,26 @@ internal sealed class ApiContentQueryService : IApiContentQueryService // Examin
|
||||
// If no Selector could be found, we return no results
|
||||
if (queryOperation is null)
|
||||
{
|
||||
return emptyResult;
|
||||
return Attempt.FailWithStatus(ApiContentQueryOperationStatus.SelectorOptionNotFound, emptyResult);
|
||||
}
|
||||
|
||||
// Handle Filtering
|
||||
HandleFiltering(filters, queryOperation);
|
||||
var canApplyFiltering = CanHandleFiltering(filters, queryOperation);
|
||||
|
||||
// If there is an invalid Filter option, we return no results
|
||||
if (canApplyFiltering is false)
|
||||
{
|
||||
return Attempt.FailWithStatus(ApiContentQueryOperationStatus.FilterOptionNotFound, emptyResult);
|
||||
}
|
||||
|
||||
// Handle Sorting
|
||||
IOrdering sortQuery = HandleSorting(sorts, queryOperation).SelectFields(_itemIdOnlyFieldSet);
|
||||
IOrdering? sortQuery = HandleSorting(sorts, queryOperation)?.SelectFields(_itemIdOnlyFieldSet);
|
||||
|
||||
// If there is an invalid Sort option, we return no results
|
||||
if (sortQuery is null)
|
||||
{
|
||||
return Attempt.FailWithStatus(ApiContentQueryOperationStatus.SortOptionNotFound, emptyResult);
|
||||
}
|
||||
|
||||
ISearchResults? results = sortQuery
|
||||
.SelectFields(_itemIdOnlyFieldSet)
|
||||
@@ -71,14 +84,16 @@ internal sealed class ApiContentQueryService : IApiContentQueryService // Examin
|
||||
|
||||
if (results is null)
|
||||
{
|
||||
return emptyResult;
|
||||
// The query yield no results
|
||||
return Attempt.SucceedWithStatus(ApiContentQueryOperationStatus.Success, emptyResult);
|
||||
}
|
||||
|
||||
Guid[] items = results
|
||||
.Where(r => r.Values.ContainsKey("itemId"))
|
||||
.Select(r => Guid.Parse(r.Values["itemId"]))
|
||||
.ToArray();
|
||||
return new PagedModel<Guid>(results.TotalItemCount, items);
|
||||
|
||||
return Attempt.SucceedWithStatus(ApiContentQueryOperationStatus.Success, new PagedModel<Guid>(results.TotalItemCount, items));
|
||||
}
|
||||
|
||||
private IBooleanOperation? HandleSelector(string? fetch, IQuery baseQuery)
|
||||
@@ -121,45 +136,50 @@ internal sealed class ApiContentQueryService : IApiContentQueryService // Examin
|
||||
return baseQuery.Field(fieldName, fieldValue);
|
||||
}
|
||||
|
||||
private void HandleFiltering(IEnumerable<string> filters, IBooleanOperation queryOperation)
|
||||
private bool CanHandleFiltering(IEnumerable<string> filters, IBooleanOperation queryOperation)
|
||||
{
|
||||
foreach (var filterValue in filters)
|
||||
{
|
||||
IFilterHandler? filterHandler = _filterHandlers.FirstOrDefault(h => h.CanHandle(filterValue));
|
||||
FilterOption? filter = filterHandler?.BuildFilterOption(filterValue);
|
||||
|
||||
if (filter is not null)
|
||||
if (filter is null)
|
||||
{
|
||||
var value = string.IsNullOrWhiteSpace(filter.Value) == false
|
||||
? filter.Value
|
||||
: _fallbackGuidValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (filter.Operator)
|
||||
{
|
||||
case FilterOperation.Is:
|
||||
queryOperation.And().Field(filter.FieldName,
|
||||
(IExamineValue)new ExamineValue(Examineness.Explicit,
|
||||
value)); // TODO: doesn't work for explicit word(s) match
|
||||
break;
|
||||
case FilterOperation.IsNot:
|
||||
queryOperation.Not().Field(filter.FieldName,
|
||||
(IExamineValue)new ExamineValue(Examineness.Explicit,
|
||||
value)); // TODO: doesn't work for explicit word(s) match
|
||||
break;
|
||||
// TODO: Fix
|
||||
case FilterOperation.Contains:
|
||||
break;
|
||||
// TODO: Fix
|
||||
case FilterOperation.DoesNotContain:
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
var value = string.IsNullOrWhiteSpace(filter.Value) == false
|
||||
? filter.Value
|
||||
: _fallbackGuidValue;
|
||||
|
||||
switch (filter.Operator)
|
||||
{
|
||||
case FilterOperation.Is:
|
||||
queryOperation.And().Field(filter.FieldName,
|
||||
(IExamineValue)new ExamineValue(Examineness.Explicit,
|
||||
value)); // TODO: doesn't work for explicit word(s) match
|
||||
break;
|
||||
case FilterOperation.IsNot:
|
||||
queryOperation.Not().Field(filter.FieldName,
|
||||
(IExamineValue)new ExamineValue(Examineness.Explicit,
|
||||
value)); // TODO: doesn't work for explicit word(s) match
|
||||
break;
|
||||
// TODO: Fix
|
||||
case FilterOperation.Contains:
|
||||
break;
|
||||
// TODO: Fix
|
||||
case FilterOperation.DoesNotContain:
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private IOrdering HandleSorting(IEnumerable<string> sorts, IBooleanOperation queryCriteria)
|
||||
|
||||
private IOrdering? HandleSorting(IEnumerable<string> sorts, IBooleanOperation queryCriteria)
|
||||
{
|
||||
IOrdering? orderingQuery = null;
|
||||
|
||||
@@ -170,7 +190,7 @@ internal sealed class ApiContentQueryService : IApiContentQueryService // Examin
|
||||
|
||||
if (sort is null)
|
||||
{
|
||||
continue;
|
||||
return null;
|
||||
}
|
||||
|
||||
SortType sortType = sort.FieldType switch
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.New.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
@@ -8,13 +9,13 @@ namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
public interface IApiContentQueryService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a collection of item ids that passed the search criteria as a paged model.
|
||||
/// Returns an attempt with a collection of item ids that passed the search criteria as a paged model.
|
||||
/// </summary>
|
||||
/// <param name="fetch">Optional fetch query parameter value.</param>
|
||||
/// <param name="filters">Optional filter query parameters values.</param>
|
||||
/// <param name="sorts">Optional sort query parameters values.</param>
|
||||
/// <param name="skip">The amount of items to skip.</param>
|
||||
/// <param name="take">The amount of items to take.</param>
|
||||
/// <returns>A paged model of item ids that are returned after applying the search queries.</returns>
|
||||
PagedModel<Guid> ExecuteQuery(string? fetch, IEnumerable<string> filters, IEnumerable<string> sorts, int skip, int take);
|
||||
/// <returns>A paged model of item ids that are returned after applying the search queries in an attempt.</returns>
|
||||
Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> ExecuteQuery(string? fetch, IEnumerable<string> filters, IEnumerable<string> sorts, int skip, int take);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.New.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
@@ -5,6 +6,6 @@ namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
public sealed class NoopApiContentQueryService : IApiContentQueryService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public PagedModel<Guid> ExecuteQuery(string? fetch, IEnumerable<string> filters, IEnumerable<string> sorts, int skip, int take)
|
||||
=> new();
|
||||
public Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> ExecuteQuery(string? fetch, IEnumerable<string> filters, IEnumerable<string> sorts, int skip, int take)
|
||||
=> Attempt.SucceedWithStatus(ApiContentQueryOperationStatus.Success, new PagedModel<Guid>());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
public enum ApiContentQueryOperationStatus
|
||||
{
|
||||
Success,
|
||||
FilterOptionNotFound,
|
||||
IndexNotFound,
|
||||
SelectorOptionNotFound,
|
||||
SortOptionNotFound
|
||||
}
|
||||
Reference in New Issue
Block a user