Handle unhappy paths for the multiple items endpoint (#14199)

This commit is contained in:
Elitsa Marinovska
2023-05-08 06:52:02 +02:00
committed by GitHub
parent 0f1c2f7022
commit 83230d377c
6 changed files with 108 additions and 43 deletions

View File

@@ -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()),
};
}

View File

@@ -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();

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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>());
}

View File

@@ -0,0 +1,10 @@
namespace Umbraco.Cms.Core.Services.OperationStatus;
public enum ApiContentQueryOperationStatus
{
Success,
FilterOptionNotFound,
IndexNotFound,
SelectorOptionNotFound,
SortOptionNotFound
}