Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delivery API: Fix numeric and date based filters #15286

Merged
merged 2 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 15 additions & 174 deletions src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
using Examine;

Check notice on line 1 in src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (release/13.0)

✅ No longer an issue: Overall Code Complexity

The mean cyclomatic complexity in this module is no longer above the threshold

Check notice on line 1 in src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (release/13.0)

✅ No longer an issue: Primitive Obsession

The ratio of primivite types in function arguments is no longer above the threshold
using Examine.Lucene.Providers;
using Examine.Lucene.Search;
using Examine.Search;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Delivery.Services.QueryBuilders;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Extensions;

namespace Umbraco.Cms.Api.Delivery.Services;

Expand All @@ -21,10 +19,10 @@
{
private const string ItemIdFieldName = "itemId";
private readonly IExamineManager _examineManager;
private readonly DeliveryApiSettings _deliveryApiSettings;
private readonly ILogger<ApiContentQueryProvider> _logger;
private readonly string _fallbackGuidValue;
private readonly Dictionary<string, FieldType> _fieldTypes;
private readonly ApiContentQuerySelectorBuilder _selectorBuilder;
private readonly ApiContentQueryFilterBuilder _filterBuilder;
private readonly ApiContentQuerySortBuilder _sortBuilder;

public ApiContentQueryProvider(
IExamineManager examineManager,
Expand All @@ -33,18 +31,20 @@
ILogger<ApiContentQueryProvider> logger)
{
_examineManager = examineManager;
_deliveryApiSettings = deliveryApiSettings.Value;
_logger = logger;

// A fallback value is needed for Examine queries in case we don't have a value - we can't pass null or empty string
// It is set to a random guid since this would be highly unlikely to yield any results
_fallbackGuidValue = Guid.NewGuid().ToString("D");

// build a look-up dictionary of field types by field name
_fieldTypes = indexHandlers
var fieldTypes = indexHandlers
.SelectMany(handler => handler.GetFields())
.DistinctBy(field => field.FieldName)
.ToDictionary(field => field.FieldName, field => field.FieldType, StringComparer.InvariantCultureIgnoreCase);

// for the time being we're going to keep these as internal implementation details.
// perhaps later on it will make sense to expose them through the DI.
_selectorBuilder = new ApiContentQuerySelectorBuilder(deliveryApiSettings.Value);
_filterBuilder = new ApiContentQueryFilterBuilder(fieldTypes, _logger);
_sortBuilder = new ApiContentQuerySortBuilder(fieldTypes, _logger);

}

[Obsolete($"Use the {nameof(ExecuteQuery)} method that accepts {nameof(ProtectedAccess)}. Will be removed in V14.")]
Expand Down Expand Up @@ -75,10 +75,9 @@
return new PagedModel<Guid>();
}

IBooleanOperation queryOperation = BuildSelectorOperation(selectorOption, index, culture, protectedAccess, preview);

ApplyFiltering(filterOptions, queryOperation);
ApplySorting(sortOptions, queryOperation);
IBooleanOperation queryOperation = _selectorBuilder.Build(selectorOption, index, culture, protectedAccess, preview);
_filterBuilder.Append(filterOptions, queryOperation);
_sortBuilder.Append(sortOptions, queryOperation);

ISearchResults? results = queryOperation
.SelectField(ItemIdFieldName)
Expand All @@ -101,163 +100,5 @@
public SelectorOption AllContentSelectorOption() => new()
{
FieldName = UmbracoExamineFieldNames.CategoryFieldName, Values = new[] { "content" }
};

Check notice on line 103 in src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (release/13.0)

✅ No longer an issue: Excess Number of Function Arguments

BuildSelectorOperation is no longer above the threshold for number of arguments. This function has too many arguments, indicating a lack of encapsulation. Avoid adding more arguments.

Check notice on line 103 in src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (release/13.0)

✅ No longer an issue: Complex Method

ApplySorting is no longer above the threshold for cyclomatic complexity

Check notice on line 103 in src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (release/13.0)

✅ No longer an issue: Complex Method

ApplyFiltering is no longer above the threshold for cyclomatic complexity

private IBooleanOperation BuildSelectorOperation(SelectorOption selectorOption, IIndex index, string culture, ProtectedAccess protectedAccess, bool preview)
{
// Needed for enabling leading wildcards searches
BaseLuceneSearcher searcher = index.Searcher as BaseLuceneSearcher ?? throw new InvalidOperationException($"Index searcher must be of type {nameof(BaseLuceneSearcher)}.");

IQuery query = searcher.CreateQuery(
IndexTypes.Content,
BooleanOperation.And,
searcher.LuceneAnalyzer,
new LuceneSearchOptions { AllowLeadingWildcard = true });

IBooleanOperation selectorOperation = selectorOption.Values.Length == 1
? query.Field(selectorOption.FieldName, selectorOption.Values.First())
: query.GroupedOr(new[] { selectorOption.FieldName }, selectorOption.Values);

AddCultureQuery(culture, selectorOperation);

if (_deliveryApiSettings.MemberAuthorizationIsEnabled())
{
AddProtectedAccessQuery(protectedAccess, selectorOperation);
}

// when not fetching for preview, make sure the "published" field is "y"
if (preview is false)
{
selectorOperation.And().Field(UmbracoExamineFieldNames.DeliveryApiContentIndex.Published, "y");
}

return selectorOperation;
}

private void AddCultureQuery(string culture, IBooleanOperation selectorOperation) =>
selectorOperation
.And()
.GroupedOr(
// Item culture must be either the requested culture or "none"
new[] { UmbracoExamineFieldNames.DeliveryApiContentIndex.Culture },
culture.ToLowerInvariant().IfNullOrWhiteSpace(_fallbackGuidValue),
"none");

private void AddProtectedAccessQuery(ProtectedAccess protectedAccess, IBooleanOperation selectorOperation)
{
var protectedAccessValues = new List<string>();
if (protectedAccess.MemberKey is not null)
{
protectedAccessValues.Add($"u:{protectedAccess.MemberKey}");
}

if (protectedAccess.MemberRoles?.Any() is true)
{
protectedAccessValues.AddRange(protectedAccess.MemberRoles.Select(r => $"r:{r}"));
}

if (protectedAccessValues.Any())
{
selectorOperation.And(
inner => inner
.Field(UmbracoExamineFieldNames.DeliveryApiContentIndex.Protected, "n")
.Or(protectedAccessInner => protectedAccessInner
.GroupedOr(
new[] { UmbracoExamineFieldNames.DeliveryApiContentIndex.ProtectedAccess },
protectedAccessValues.ToArray())),
BooleanOperation.Or);
}
else
{
selectorOperation.And().Field(UmbracoExamineFieldNames.DeliveryApiContentIndex.Protected, "n");
}
}

private void ApplyFiltering(IList<FilterOption> filterOptions, IBooleanOperation queryOperation)
{
void HandleExact(IQuery query, string fieldName, string[] values)
{
if (values.Length == 1)
{
query.Field(fieldName, values[0]);
}
else
{
query.GroupedOr(new[] { fieldName }, values);
}
}

void HandleContains(IQuery query, string fieldName, string[] values)
{
if (values.Length == 1)
{
// The trailing wildcard is added automatically
query.Field(fieldName, (IExamineValue)new ExamineValue(Examineness.ComplexWildcard, $"*{values[0]}"));
}
else
{
// The trailing wildcard is added automatically
IExamineValue[] examineValues = values
.Select(value => (IExamineValue)new ExamineValue(Examineness.ComplexWildcard, $"*{value}"))
.ToArray();
query.GroupedOr(new[] { fieldName }, examineValues);
}
}

foreach (FilterOption filterOption in filterOptions)
{
var values = filterOption.Values.Any()
? filterOption.Values
: new[] { _fallbackGuidValue };

switch (filterOption.Operator)
{
case FilterOperation.Is:
HandleExact(queryOperation.And(), filterOption.FieldName, values);
break;
case FilterOperation.IsNot:
HandleExact(queryOperation.Not(), filterOption.FieldName, values);
break;
case FilterOperation.Contains:
HandleContains(queryOperation.And(), filterOption.FieldName, values);
break;
case FilterOperation.DoesNotContain:
HandleContains(queryOperation.Not(), filterOption.FieldName, values);
break;
default:
continue;
}
}
}

private void ApplySorting(IList<SortOption> sortOptions, IOrdering ordering)
{
foreach (SortOption sort in sortOptions)
{
if (_fieldTypes.TryGetValue(sort.FieldName, out FieldType fieldType) is false)
{
_logger.LogWarning(
"Sort implementation for field name {FieldName} does not match an index handler implementation, cannot resolve field type.",
sort.FieldName);
continue;
}

SortType sortType = fieldType switch
{
FieldType.Number => SortType.Int,
FieldType.Date => SortType.Long,
FieldType.StringRaw => SortType.String,
FieldType.StringAnalyzed => SortType.String,
FieldType.StringSortable => SortType.String,
_ => throw new ArgumentOutOfRangeException(nameof(fieldType))
};

ordering = sort.Direction switch
{
Direction.Ascending => ordering.OrderBy(new SortableField(sort.FieldName, sortType)),
Direction.Descending => ordering.OrderByDescending(new SortableField(sort.FieldName, sortType)),
_ => ordering
};
}
}
}
Loading