Browse Source

Tests refactored.

pull/315/head
Sebastian Stehle 8 years ago
parent
commit
6c5acd3c18
  1. 72
      src/Squidex.Domain.Apps.Core.Operations/Queries/FilterComparison.cs
  2. 46
      src/Squidex.Domain.Apps.Core.Operations/Queries/FilterJunction.cs
  3. 15
      src/Squidex.Domain.Apps.Core.Operations/Queries/FilterJunctionType.cs
  4. 33
      src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNegate.cs
  5. 14
      src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNode.cs
  6. 31
      src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNodeVisitor.cs
  7. 22
      src/Squidex.Domain.Apps.Core.Operations/Queries/FilterOperator.cs
  8. 21
      src/Squidex.Domain.Apps.Core.Operations/Queries/FilterValueType.cs
  9. 113
      src/Squidex.Domain.Apps.Core.Operations/Queries/OData/ConstantVisitor.cs
  10. 49
      src/Squidex.Domain.Apps.Core.Operations/Queries/OData/FilterBuilder.cs
  11. 153
      src/Squidex.Domain.Apps.Core.Operations/Queries/OData/FilterVisitor.cs
  12. 34
      src/Squidex.Domain.Apps.Core.Operations/Queries/OData/LimitExtensions.cs
  13. 56
      src/Squidex.Domain.Apps.Core.Operations/Queries/OData/PropertyPathVisitor.cs
  14. 41
      src/Squidex.Domain.Apps.Core.Operations/Queries/OData/SearchTermVisitor.cs
  15. 43
      src/Squidex.Domain.Apps.Core.Operations/Queries/OData/SortBuilder.cs
  16. 24
      src/Squidex.Domain.Apps.Core.Operations/Queries/Query.cs
  17. 30
      src/Squidex.Domain.Apps.Core.Operations/Queries/SortNode.cs
  18. 15
      src/Squidex.Domain.Apps.Core.Operations/Queries/SortOrder.cs
  19. 30
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  20. 55
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs
  21. 22
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
  22. 8
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  23. 79
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs
  24. 41
      src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs
  25. 2
      src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs
  26. 3
      src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
  27. 28
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
  28. 2
      src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs
  29. 8
      src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs
  30. 4
      src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
  31. 38
      src/Squidex.Domain.Apps.Entities/EdmModelExtensions.cs
  32. 18
      src/Squidex.Domain.Apps.Entities/Q.cs
  33. 83
      src/Squidex.Infrastructure.MongoDb/MongoDb/OData/ConstantVisitor.cs
  34. 33
      src/Squidex.Infrastructure.MongoDb/MongoDb/OData/FilterBuilder.cs
  35. 174
      src/Squidex.Infrastructure.MongoDb/MongoDb/OData/FilterVisitor.cs
  36. 27
      src/Squidex.Infrastructure.MongoDb/MongoDb/OData/LimitExtensions.cs
  37. 32
      src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyBuilder.cs
  38. 50
      src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyNameVisitor.cs
  39. 41
      src/Squidex.Infrastructure.MongoDb/MongoDb/OData/SearchTermVisitor.cs
  40. 20
      src/Squidex.Infrastructure.MongoDb/MongoDb/OData/SortBuilder.cs
  41. 1
      src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj
  42. 32
      src/Squidex.Infrastructure/Queries/FilterBuilder.cs
  43. 11
      src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs
  44. 4
      src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs
  45. 17
      src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs
  46. 10
      src/Squidex.Infrastructure/Queries/Query.cs
  47. 20
      src/Squidex.Infrastructure/Queries/SortBuilder.cs
  48. 2
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  49. 2
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  50. 1
      src/Squidex/Config/Domain/EntitiesServices.cs
  51. 8
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs
  52. 162
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs
  53. 10
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs
  54. 12
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  55. 267
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs
  56. 448
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/OData/ODataQueryTests.cs

72
src/Squidex.Domain.Apps.Core.Operations/Queries/FilterComparison.cs

@ -1,72 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Queries
{
public sealed class FilterComparison : FilterNode
{
public IReadOnlyList<string> Path { get; }
public FilterOperator Operator { get; }
public FilterValueType ValueType { get; }
public object Value { get; }
public FilterComparison(IReadOnlyList<string> path, FilterOperator @operator, object value, FilterValueType valueType)
{
Guard.NotNull(path, nameof(path));
Guard.NotEmpty(path, nameof(path));
Guard.Enum(@operator, nameof(@operator));
Guard.Enum(valueType, nameof(valueType));
Path = path;
Value = value;
ValueType = valueType;
Operator = @operator;
}
public override T Accept<T>(FilterNodeVisitor<T> visitor)
{
return visitor.Visit(this);
}
public override string ToString()
{
var path = string.Join(".", Path);
switch (Operator)
{
case FilterOperator.Contains:
return $"contains({path}, {Value})";
case FilterOperator.EndsWith:
return $"endsWith({path}, {Value})";
case FilterOperator.StartsWith:
return $"startsWith({path}, {Value})";
case FilterOperator.Equals:
return $"{path} == {Value}";
case FilterOperator.NotEquals:
return $"{path} != {Value}";
case FilterOperator.GreaterThan:
return $"{path} > {Value}";
case FilterOperator.GreaterThanOrEqual:
return $"{path} >= {Value}";
case FilterOperator.LessThan:
return $"{path} < {Value}";
case FilterOperator.LessThanOrEqual:
return $"{path} <= {Value}";
default:
return string.Empty;
}
}
}
}

46
src/Squidex.Domain.Apps.Core.Operations/Queries/FilterJunction.cs

@ -1,46 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Queries
{
public sealed class FilterJunction : FilterNode
{
public IReadOnlyList<FilterNode> Operands { get; }
public FilterJunctionType JunctionType { get; }
public FilterJunction(FilterJunctionType junctionType, IReadOnlyList<FilterNode> operands)
{
Guard.NotNull(operands, nameof(operands));
Guard.GreaterEquals(operands.Count, 2, nameof(operands.Count));
Guard.Enum(junctionType, nameof(junctionType));
Operands = operands;
JunctionType = junctionType;
}
public FilterJunction(FilterJunctionType junctionType, params FilterNode[] operands)
: this(junctionType, operands?.ToList())
{
}
public override T Accept<T>(FilterNodeVisitor<T> visitor)
{
return visitor.Visit(this);
}
public override string ToString()
{
return $"({string.Join(JunctionType == FilterJunctionType.And ? " && " : " || ", Operands)})";
}
}
}

15
src/Squidex.Domain.Apps.Core.Operations/Queries/FilterJunctionType.cs

@ -1,15 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Queries
{
public enum FilterJunctionType
{
And,
Or
}
}

33
src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNegate.cs

@ -1,33 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Queries
{
public sealed class FilterNegate : FilterNode
{
public FilterNode Operand { get; }
public FilterNegate(FilterNode operand)
{
Guard.NotNull(operand, nameof(operand));
Operand = operand;
}
public override T Accept<T>(FilterNodeVisitor<T> visitor)
{
return visitor.Visit(this);
}
public override string ToString()
{
return $"!{Operand}";
}
}
}

14
src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNode.cs

@ -1,14 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Queries
{
public abstract class FilterNode
{
public abstract T Accept<T>(FilterNodeVisitor<T> visitor);
}
}

31
src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNodeVisitor.cs

@ -1,31 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
#pragma warning disable RECS0083 // Shows NotImplementedException throws in the quick task bar
namespace Squidex.Domain.Apps.Core.Queries
{
public abstract class FilterNodeVisitor<T>
{
public virtual T Visit(FilterComparison nodeIn)
{
throw new NotImplementedException();
}
public virtual T Visit(FilterJunction nodeIn)
{
throw new NotImplementedException();
}
public virtual T Visit(FilterNegate nodeIn)
{
throw new NotImplementedException();
}
}
}

22
src/Squidex.Domain.Apps.Core.Operations/Queries/FilterOperator.cs

@ -1,22 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Queries
{
public enum FilterOperator
{
Contains,
EndsWith,
Equals,
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqual,
NotEquals,
StartsWith
}
}

21
src/Squidex.Domain.Apps.Core.Operations/Queries/FilterValueType.cs

@ -1,21 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Queries
{
public enum FilterValueType
{
Boolean,
Guid,
Double,
Instant,
Int32,
Int64,
Single,
String,
}
}

113
src/Squidex.Domain.Apps.Core.Operations/Queries/OData/ConstantVisitor.cs

@ -1,113 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Microsoft.OData;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser;
using NodaTime;
using NodaTime.Text;
namespace Squidex.Domain.Apps.Core.Queries
{
public sealed class ConstantVisitor : QueryNodeVisitor<(object Value, FilterValueType ValueType)>
{
private static readonly IEdmPrimitiveType BooleanType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Boolean);
private static readonly IEdmPrimitiveType DateTimeType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset);
private static readonly IEdmPrimitiveType DoubleType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Double);
private static readonly IEdmPrimitiveType GuidType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Guid);
private static readonly IEdmPrimitiveType Int32Type = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32);
private static readonly IEdmPrimitiveType Int64Type = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int64);
private static readonly IEdmPrimitiveType SingleType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Single);
private static readonly IEdmPrimitiveType StringType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.String);
private static readonly ConstantVisitor Instance = new ConstantVisitor();
private ConstantVisitor()
{
}
public static (object Value, FilterValueType ValueType) Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override (object Value, FilterValueType ValueType) Visit(ConvertNode nodeIn)
{
if (nodeIn.TypeReference.Definition == BooleanType)
{
return (bool.Parse(Visit(nodeIn.Source).ToString()), FilterValueType.Boolean);
}
if (nodeIn.TypeReference.Definition == GuidType)
{
return (Guid.Parse(Visit(nodeIn.Source).ToString()), FilterValueType.Guid);
}
if (nodeIn.TypeReference.Definition == DateTimeType)
{
var value = Visit(nodeIn.Source);
if (value.Value is DateTimeOffset dateTimeOffset)
{
return (Instant.FromDateTimeOffset(dateTimeOffset), FilterValueType.Instant);
}
if (value.Value is DateTime dateTime)
{
return (Instant.FromDateTimeUtc(DateTime.SpecifyKind(dateTime, DateTimeKind.Utc)), FilterValueType.Instant);
}
if (value.Value is Date date)
{
return (Instant.FromUtc(date.Year, date.Month, date.Day, 0, 0), FilterValueType.Instant);
}
var parseResult = InstantPattern.General.Parse(Visit(nodeIn.Source).ToString());
if (!parseResult.Success)
{
throw new ODataException("Datetime is not in a valid format. Use ISO 8601");
}
return (parseResult.Value, FilterValueType.Instant);
}
return base.Visit(nodeIn);
}
public override (object Value, FilterValueType ValueType) Visit(ConstantNode nodeIn)
{
if (nodeIn.TypeReference == BooleanType)
{
return (nodeIn.Value, FilterValueType.Boolean);
}
if (nodeIn.TypeReference == DoubleType)
{
return (nodeIn.Value, FilterValueType.Double);
}
if (nodeIn.TypeReference == Int32Type)
{
return (nodeIn.Value, FilterValueType.Int32);
}
if (nodeIn.TypeReference == Int32Type)
{
return (nodeIn.Value, FilterValueType.Int64);
}
if (nodeIn.TypeReference == StringType)
{
return (nodeIn.Value, FilterValueType.String);
}
throw new NotSupportedException();
}
}
}

49
src/Squidex.Domain.Apps.Core.Operations/Queries/OData/FilterBuilder.cs

@ -1,49 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.OData;
using Microsoft.OData.UriParser;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Queries
{
public static class FilterBuilder
{
public static void ParseFilter(this ODataUriParser query, Query result)
{
SearchClause search;
try
{
search = query.ParseSearch();
}
catch (ODataException ex)
{
throw new ValidationException("Query $search clause not valid.", new ValidationError(ex.Message));
}
if (search != null)
{
result.FullText = SearchTermVisitor.Visit(search.Expression).ToString();
}
FilterClause filter;
try
{
filter = query.ParseFilter();
}
catch (ODataException ex)
{
throw new ValidationException("Query $filter clause not valid.", new ValidationError(ex.Message));
}
if (filter != null)
{
result.Filter = FilterVisitor.Visit(filter.Expression);
}
}
}
}

153
src/Squidex.Domain.Apps.Core.Operations/Queries/OData/FilterVisitor.cs

@ -1,153 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using Microsoft.OData.UriParser;
namespace Squidex.Domain.Apps.Core.Queries
{
public sealed class FilterVisitor : QueryNodeVisitor<FilterNode>
{
private static readonly FilterVisitor Instance = new FilterVisitor();
private FilterVisitor()
{
}
public static FilterNode Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override FilterNode Visit(ConvertNode nodeIn)
{
return nodeIn.Source.Accept(this);
}
public override FilterNode Visit(UnaryOperatorNode nodeIn)
{
if (nodeIn.OperatorKind == UnaryOperatorKind.Not)
{
return new FilterNegate(nodeIn.Operand.Accept(this));
}
throw new NotSupportedException();
}
public override FilterNode Visit(SingleValueFunctionCallNode nodeIn)
{
var fieldNode = nodeIn.Parameters.ElementAt(0);
var valueNode = nodeIn.Parameters.ElementAt(1);
if (string.Equals(nodeIn.Name, "endswith", StringComparison.OrdinalIgnoreCase))
{
var (value, valueType) = ConstantVisitor.Visit(valueNode);
return new FilterComparison(PropertyPathVisitor.Visit(fieldNode), FilterOperator.EndsWith, value, valueType);
}
if (string.Equals(nodeIn.Name, "startswith", StringComparison.OrdinalIgnoreCase))
{
var (value, valueType) = ConstantVisitor.Visit(valueNode);
return new FilterComparison(PropertyPathVisitor.Visit(fieldNode), FilterOperator.StartsWith, value, valueType);
}
if (string.Equals(nodeIn.Name, "contains", StringComparison.OrdinalIgnoreCase))
{
var (value, valueType) = ConstantVisitor.Visit(valueNode);
return new FilterComparison(PropertyPathVisitor.Visit(fieldNode), FilterOperator.Contains, value, valueType);
}
throw new NotSupportedException();
}
public override FilterNode Visit(BinaryOperatorNode nodeIn)
{
if (nodeIn.OperatorKind == BinaryOperatorKind.And)
{
return new FilterJunction(FilterJunctionType.And, nodeIn.Left.Accept(this), nodeIn.Right.Accept(this));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.Or)
{
return new FilterJunction(FilterJunctionType.Or, nodeIn.Left.Accept(this), nodeIn.Right.Accept(this));
}
if (nodeIn.Left is SingleValueFunctionCallNode functionNode)
{
var regexFilter = Visit(functionNode);
var value = BuildValue(nodeIn.Right);
if (value is bool booleanRight)
{
if ((nodeIn.OperatorKind == BinaryOperatorKind.Equal && !booleanRight) ||
(nodeIn.OperatorKind == BinaryOperatorKind.NotEqual && booleanRight))
{
regexFilter = new FilterNegate(regexFilter);
}
return regexFilter;
}
}
else
{
if (nodeIn.OperatorKind == BinaryOperatorKind.NotEqual)
{
var (value, valueType) = ConstantVisitor.Visit(nodeIn.Left);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Right), FilterOperator.NotEquals, value, valueType);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.Equal)
{
var (value, valueType) = ConstantVisitor.Visit(nodeIn.Left);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Right), FilterOperator.Equals, value, valueType);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThan)
{
var (value, valueType) = ConstantVisitor.Visit(nodeIn.Left);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Right), FilterOperator.LessThan, value, valueType);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThanOrEqual)
{
var (value, valueType) = ConstantVisitor.Visit(nodeIn.Left);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Right), FilterOperator.LessThanOrEqual, value, valueType);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThan)
{
var (value, valueType) = ConstantVisitor.Visit(nodeIn.Left);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Right), FilterOperator.GreaterThan, value, valueType);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThanOrEqual)
{
var (value, valueType) = ConstantVisitor.Visit(nodeIn.Left);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Right), FilterOperator.GreaterThanOrEqual, value, valueType);
}
}
throw new NotSupportedException();
}
private object BuildValue(QueryNode nodeIn)
{
return ConstantVisitor.Visit(nodeIn);
}
}
}

34
src/Squidex.Domain.Apps.Core.Operations/Queries/OData/LimitExtensions.cs

@ -1,34 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.OData.UriParser;
namespace Squidex.Domain.Apps.Core.Queries
{
public static class LimitExtensions
{
public static void ParseTake(this ODataUriParser query, Query result, int maxValue = int.MaxValue)
{
var top = query.ParseTop();
if (top.HasValue)
{
result.Take = top;
}
}
public static void ParseSkip(this ODataUriParser query, Query result)
{
var skip = query.ParseSkip();
if (skip.HasValue)
{
result.Skip = skip;
}
}
}
}

56
src/Squidex.Domain.Apps.Core.Operations/Queries/OData/PropertyPathVisitor.cs

@ -1,56 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Immutable;
using Microsoft.OData.UriParser;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Queries
{
public sealed class PropertyPathVisitor : QueryNodeVisitor<ImmutableList<string>>
{
private static readonly PropertyPathVisitor Instance = new PropertyPathVisitor();
private PropertyPathVisitor()
{
}
public static ImmutableList<string> Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override ImmutableList<string> Visit(ConvertNode nodeIn)
{
return nodeIn.Source.Accept(this);
}
public override ImmutableList<string> Visit(SingleComplexNode nodeIn)
{
if (nodeIn.Source is SingleComplexNode)
{
return nodeIn.Source.Accept(this).Add(nodeIn.Property.Name.ToPascalCase());
}
else
{
return ImmutableList.Create(nodeIn.Property.Name.ToPascalCase());
}
}
public override ImmutableList<string> Visit(SingleValuePropertyAccessNode nodeIn)
{
if (nodeIn.Source is SingleComplexNode)
{
return nodeIn.Source.Accept(this).Add(nodeIn.Property.Name.ToPascalCase());
}
else
{
return ImmutableList.Create(nodeIn.Property.Name.ToPascalCase());
}
}
}
}

41
src/Squidex.Domain.Apps.Core.Operations/Queries/OData/SearchTermVisitor.cs

@ -1,41 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Microsoft.OData.UriParser;
namespace Squidex.Domain.Apps.Core.Queries
{
public class SearchTermVisitor : QueryNodeVisitor<string>
{
private static readonly SearchTermVisitor Instance = new SearchTermVisitor();
private SearchTermVisitor()
{
}
public static object Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override string Visit(BinaryOperatorNode nodeIn)
{
if (nodeIn.OperatorKind == BinaryOperatorKind.And)
{
return nodeIn.Left.Accept(this) + " " + nodeIn.Right.Accept(this);
}
throw new NotSupportedException();
}
public override string Visit(SearchTermNode nodeIn)
{
return nodeIn.Text;
}
}
}

43
src/Squidex.Domain.Apps.Core.Operations/Queries/OData/SortBuilder.cs

@ -1,43 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.OData.UriParser;
namespace Squidex.Domain.Apps.Core.Queries
{
public static class SortBuilder
{
public static void ParseSort(this ODataUriParser query, Query result)
{
var orderBy = query.ParseOrderBy();
if (orderBy != null)
{
while (orderBy != null)
{
result.Sort.Add(OrderBy(orderBy));
orderBy = orderBy.ThenBy;
}
}
}
public static SortNode OrderBy(OrderByClause clause)
{
var path = PropertyPathVisitor.Visit(clause.Expression);
if (clause.Direction == OrderByDirection.Ascending)
{
return new SortNode(path, SortOrder.Ascending);
}
else
{
return new SortNode(path, SortOrder.Descending);
}
}
}
}

24
src/Squidex.Domain.Apps.Core.Operations/Queries/Query.cs

@ -1,24 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Core.Queries
{
public sealed class Query
{
public FilterNode Filter { get; set; }
public long? Skip { get; set; }
public long? Take { get; set; }
public List<SortNode> Sort { get; } = new List<SortNode>();
public string FullText { get; set; }
}
}

30
src/Squidex.Domain.Apps.Core.Operations/Queries/SortNode.cs

@ -1,30 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Queries
{
public sealed class SortNode
{
public IReadOnlyList<string> Path { get; }
public SortOrder SortOrder { get; set; }
public SortNode(IReadOnlyList<string> path, SortOrder sortOrder)
{
Guard.NotNull(path, nameof(path));
Guard.NotEmpty(path, nameof(path));
Guard.Enum(sortOrder, nameof(sortOrder));
Path = path;
SortOrder = sortOrder;
}
}
}

15
src/Squidex.Domain.Apps.Core.Operations/Queries/SortOrder.cs

@ -1,15 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Queries
{
public enum SortOrder
{
Ascending,
Descending
}
}

30
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -10,27 +10,21 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Edm;
using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors; using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{ {
public sealed partial class MongoAssetRepository : MongoRepositoryBase<MongoAssetEntity>, IAssetRepository public sealed partial class MongoAssetRepository : MongoRepositoryBase<MongoAssetEntity>, IAssetRepository
{ {
private readonly ITagService tagService; public MongoAssetRepository(IMongoDatabase database)
public MongoAssetRepository(IMongoDatabase database, ITagService tagService)
: base(database) : base(database)
{ {
Guard.NotNull(tagService, nameof(tagService));
this.tagService = tagService;
} }
protected override string CollectionName() protected override string CollectionName()
@ -50,36 +44,28 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
.Descending(x => x.LastModified))); .Descending(x => x.LastModified)));
} }
public async Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, string query = null) public async Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, Query query)
{ {
using (Profiler.TraceMethod<MongoAssetRepository>("QueryAsyncByQuery")) using (Profiler.TraceMethod<MongoAssetRepository>("QueryAsyncByQuery"))
{ {
try try
{ {
var odataQuery = EdmAssetModel.Edm.ParseQuery(query); query = query.AdjustToModel();
var filter = FindExtensions.BuildQuery(odataQuery, appId, tagService); var filter = FindExtensions.BuildFilter(query, appId);
var contentCount = Collection.Find(filter).CountDocumentsAsync(); var contentCount = Collection.Find(filter).CountDocumentsAsync();
var contentItems = var contentItems =
Collection.Find(filter) Collection.Find(filter)
.AssetTake(odataQuery) .AssetTake(query)
.AssetSkip(odataQuery) .AssetSkip(query)
.AssetSort(odataQuery) .AssetSort(query)
.ToListAsync(); .ToListAsync();
await Task.WhenAll(contentItems, contentCount); await Task.WhenAll(contentItems, contentCount);
return ResultList.Create<IAssetEntity>(contentCount.Result, contentItems.Result); return ResultList.Create<IAssetEntity>(contentCount.Result, contentItems.Result);
} }
catch (NotSupportedException)
{
throw new ValidationException("This odata operation is not supported.");
}
catch (NotImplementedException)
{
throw new ValidationException("This odata operation is not supported.");
}
catch (MongoQueryException ex) catch (MongoQueryException ex)
{ {
if (ex.Message.Contains("17406")) if (ex.Message.Contains("17406"))

55
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs

@ -8,59 +8,59 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Microsoft.OData.UriParser;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb.OData; using Squidex.Infrastructure.MongoDb.OData;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
{ {
public static class FindExtensions public static class FindExtensions
{ {
private static readonly FilterDefinitionBuilder<MongoAssetEntity> Filter = Builders<MongoAssetEntity>.Filter; private static readonly FilterDefinitionBuilder<MongoAssetEntity> Filter = Builders<MongoAssetEntity>.Filter;
private static readonly ConvertProperty PropertyCalculator = propertyNames =>
public static Query AdjustToModel(this Query query)
{ {
if (propertyNames.Length > 0) if (query.Filter != null)
{ {
propertyNames[0] = propertyNames[0].ToPascalCase(); query.Filter = PascalCasePathConverter.Transform(query.Filter);
} }
var propertyName = string.Join(".", propertyNames); query.Sort = query.Sort
.Select(x =>
new SortNode(
x.Path.Select(p => p.ToPascalCase()).ToList(),
x.SortOrder))
.ToList();
return propertyName; return query;
}; }
public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetSort(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, ODataUriParser query) public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetSort(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, Query query)
{ {
var sort = query.BuildSort<MongoAssetEntity>(PropertyCalculator); return cursor.Sort(query.BuildSort<MongoAssetEntity>());
return sort != null ? cursor.Sort(sort) : cursor.SortByDescending(x => x.LastModified);
} }
public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetTake(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, ODataUriParser query) public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetTake(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, Query query)
{ {
return cursor.Take(query, 200, 20); return cursor.Take(query);
} }
public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetSkip(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, ODataUriParser query) public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetSkip(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, Query query)
{ {
return cursor.Skip(query); return cursor.Skip(query);
} }
public static FilterDefinition<MongoAssetEntity> BuildQuery(ODataUriParser query, Guid appId, ITagService tagService) public static FilterDefinition<MongoAssetEntity> BuildFilter(this Query query, Guid appId)
{ {
var convertValue = CreateValueConverter(appId, tagService);
var filters = new List<FilterDefinition<MongoAssetEntity>> var filters = new List<FilterDefinition<MongoAssetEntity>>
{ {
Filter.Eq(x => x.IndexedAppId, appId), Filter.Eq(x => x.IndexedAppId, appId),
Filter.Eq(x => x.IsDeleted, false) Filter.Eq(x => x.IsDeleted, false)
}; };
var filter = query.BuildFilter<MongoAssetEntity>(PropertyCalculator, convertValue, false); var filter = query.BuildFilter<MongoAssetEntity>(false);
if (filter.Filter != null) if (filter.Filter != null)
{ {
@ -87,20 +87,5 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
return new BsonDocument(); return new BsonDocument();
} }
} }
public static ConvertValue CreateValueConverter(Guid appId, ITagService tagService)
{
return new ConvertValue((field, value) =>
{
if (string.Equals(field, nameof(MongoAssetEntity.Tags), StringComparison.OrdinalIgnoreCase))
{
var tagNames = Task.Run(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, HashSet.Of(value.ToString()))).Result;
return tagNames?.FirstOrDefault() ?? value;
}
return value;
});
}
} }
} }

22
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs

@ -9,7 +9,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.OData.UriParser;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
@ -18,6 +17,7 @@ using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
@ -42,20 +42,20 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return collectionName; return collectionName;
} }
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, ODataUriParser odataQuery, Status[] status = null, bool useDraft = false) public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Query query, Status[] status = null, bool useDraft = false)
{ {
try try
{ {
var propertyCalculator = FindExtensions.CreatePropertyCalculator(schema.SchemaDef, useDraft); query = query.AdjustToModel(schema.SchemaDef, useDraft);
var filter = FindExtensions.BuildQuery(odataQuery, schema.Id, status, propertyCalculator); var filter = FindExtensions.BuildQuery(query, schema.Id, status);
var contentCount = Collection.Find(filter).CountDocumentsAsync(); var contentCount = Collection.Find(filter).CountDocumentsAsync();
var contentItems = var contentItems =
Collection.Find(filter) Collection.Find(filter)
.ContentTake(odataQuery) .ContentTake(query)
.ContentSkip(odataQuery) .ContentSkip(query)
.ContentSort(odataQuery, propertyCalculator) .ContentSort(query)
.Not(x => x.DataText) .Not(x => x.DataText)
.ToListAsync(); .ToListAsync();
@ -68,14 +68,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return ResultList.Create<IContentEntity>(contentCount.Result, contentItems.Result); return ResultList.Create<IContentEntity>(contentCount.Result, contentItems.Result);
} }
catch (NotSupportedException)
{
throw new ValidationException("This odata operation is not supported.");
}
catch (NotImplementedException)
{
throw new ValidationException("This odata operation is not supported.");
}
catch (MongoQueryException ex) catch (MongoQueryException ex)
{ {
if (ex.Message.Contains("17406")) if (ex.Message.Contains("17406"))

8
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs

@ -8,7 +8,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.OData.UriParser;
using MongoDB.Driver; using MongoDB.Driver;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
@ -18,6 +17,7 @@ using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
@ -46,17 +46,17 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
contentsPublished.Initialize(); contentsPublished.Initialize();
} }
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery) public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Query query)
{ {
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByQuery")) using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByQuery"))
{ {
if (RequiresPublished(status)) if (RequiresPublished(status))
{ {
return await contentsPublished.QueryAsync(app, schema, odataQuery); return await contentsPublished.QueryAsync(app, schema, query);
} }
else else
{ {
return await contentsDraft.QueryAsync(app, schema, odataQuery, status, true); return await contentsDraft.QueryAsync(app, schema, query, status, true);
} }
} }
} }

79
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs

@ -9,7 +9,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Microsoft.OData.UriParser;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver; using MongoDB.Driver;
using NodaTime; using NodaTime;
@ -17,6 +16,7 @@ using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.GenerateEdmSchema; using Squidex.Domain.Apps.Core.GenerateEdmSchema;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.MongoDb.OData; using Squidex.Infrastructure.MongoDb.OData;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
{ {
@ -28,77 +28,98 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
typeof(MongoContentEntity).GetProperties() typeof(MongoContentEntity).GetProperties()
.ToDictionary(x => x.Name, x => x.GetCustomAttribute<BsonElementAttribute>()?.ElementName ?? x.Name, StringComparer.OrdinalIgnoreCase); .ToDictionary(x => x.Name, x => x.GetCustomAttribute<BsonElementAttribute>()?.ElementName ?? x.Name, StringComparer.OrdinalIgnoreCase);
public static readonly ConvertValue ValueConverter = (field, value) => private sealed class AdaptionVisitor : TransformVisitor
{ {
if (value is Instant instant && private readonly Func<IReadOnlyList<string>, IReadOnlyList<string>> pathConverter;
!string.Equals(field, "mt", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(field, "ct", StringComparison.OrdinalIgnoreCase)) public AdaptionVisitor(Func<IReadOnlyList<string>, IReadOnlyList<string>> pathConverter)
{ {
return instant.ToString(); this.pathConverter = pathConverter;
} }
return value; public override FilterNode Visit(FilterComparison nodeIn)
}; {
var value = nodeIn.Value;
if (value is Instant instant &&
!string.Equals(nodeIn.Path[0], "mt", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(nodeIn.Path[0], "ct", StringComparison.OrdinalIgnoreCase))
{
value = instant.ToString();
}
return new FilterComparison(pathConverter(nodeIn.Path), nodeIn.Operator, value, nodeIn.ValueType);
}
}
public static ConvertProperty CreatePropertyCalculator(Schema schema, bool useDraft) public static Query AdjustToModel(this Query query, Schema schema, bool useDraft)
{ {
return propertyNames => var pathConverter = new Func<IReadOnlyList<string>, IReadOnlyList<string>>(propertyNames =>
{ {
if (propertyNames.Length > 1) var result = new List<string>(propertyNames);
if (result.Count > 1)
{ {
var edmName = propertyNames[1].UnescapeEdmField(); var edmName = result[1].UnescapeEdmField();
if (!schema.FieldsByName.TryGetValue(edmName, out var field)) if (!schema.FieldsByName.TryGetValue(edmName, out var field))
{ {
throw new NotSupportedException(); throw new NotSupportedException();
} }
propertyNames[1] = field.Id.ToString(); result[1] = field.Id.ToString();
} }
if (propertyNames.Length > 0) if (result.Count > 0)
{ {
if (propertyNames[0].Equals("Data", StringComparison.CurrentCultureIgnoreCase)) if (result[0].Equals("Data", StringComparison.CurrentCultureIgnoreCase))
{ {
if (useDraft) if (useDraft)
{ {
propertyNames[0] = "dd"; result[0] = "dd";
} }
else else
{ {
propertyNames[0] = "do"; result[0] = "do";
} }
} }
else else
{ {
propertyNames[0] = PropertyMap[propertyNames[0]]; result[0] = PropertyMap[propertyNames[0]];
} }
} }
var propertyName = string.Join(".", propertyNames); var propertyName = string.Join(".", propertyNames);
return propertyName; return result;
}; });
if (query.Filter != null)
{
query.Filter = query.Filter.Accept(new AdaptionVisitor(pathConverter));
}
query.Sort = query.Sort.Select(x => new SortNode(pathConverter(x.Path), x.SortOrder)).ToList();
return query;
} }
public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentSort(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ODataUriParser query, ConvertProperty propertyCalculator) public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentSort(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, Query query)
{ {
var sort = query.BuildSort<MongoContentEntity>(propertyCalculator); return cursor.Sort(query.BuildSort<MongoContentEntity>());
return sort != null ? cursor.Sort(sort) : cursor.SortByDescending(x => x.LastModified);
} }
public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentTake(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ODataUriParser query) public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentTake(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, Query query)
{ {
return cursor.Take(query, 200, 20); return cursor.Take(query);
} }
public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentSkip(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ODataUriParser query) public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentSkip(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, Query query)
{ {
return cursor.Skip(query); return cursor.Skip(query);
} }
public static FilterDefinition<MongoContentEntity> BuildQuery(ODataUriParser query, Guid schemaId, Status[] status, ConvertProperty propertyCalculator) public static FilterDefinition<MongoContentEntity> BuildQuery(Query query, Guid schemaId, Status[] status)
{ {
var filters = new List<FilterDefinition<MongoContentEntity>> var filters = new List<FilterDefinition<MongoContentEntity>>
{ {
@ -111,7 +132,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
filters.Add(Filter.In(x => x.Status, status)); filters.Add(Filter.In(x => x.Status, status));
} }
var filter = query.BuildFilter<MongoContentEntity>(propertyCalculator, ValueConverter); var filter = query.BuildFilter<MongoContentEntity>();
if (filter.Filter != null) if (filter.Filter != null)
{ {

41
src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs

@ -9,14 +9,19 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.OData;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Edm;
using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Queries.OData;
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
{ {
public sealed class AssetQueryService : IAssetQueryService public sealed class AssetQueryService : IAssetQueryService
{ {
private const int MaxResults = 20;
private readonly ITagService tagService; private readonly ITagService tagService;
private readonly IAssetRepository assetRepository; private readonly IAssetRepository assetRepository;
@ -44,7 +49,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
return asset; return asset;
} }
public async Task<IResultList<IAssetEntity>> QueryAsync(QueryContext context, Query query) public async Task<IResultList<IAssetEntity>> QueryAsync(QueryContext context, Q query)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
Guard.NotNull(query, nameof(query)); Guard.NotNull(query, nameof(query));
@ -58,7 +63,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
} }
else else
{ {
assets = await assetRepository.QueryAsync(context.App.Id, query.ODataQuery); var parsedQuery = ParseQuery(context, query.ODataQuery);
assets = await assetRepository.QueryAsync(context.App.Id, parsedQuery);
} }
await DenormalizeTagsAsync(context.App.Id, assets); await DenormalizeTagsAsync(context.App.Id, assets);
@ -66,13 +73,41 @@ namespace Squidex.Domain.Apps.Entities.Assets
return assets; return assets;
} }
private IResultList<IAssetEntity> Sort(IResultList<IAssetEntity> assets, IList<Guid> ids) private IResultList<IAssetEntity> Sort(IResultList<IAssetEntity> assets, IReadOnlyList<Guid> ids)
{ {
var sorted = ids.Select(id => assets.FirstOrDefault(x => x.Id == id)).Where(x => x != null); var sorted = ids.Select(id => assets.FirstOrDefault(x => x.Id == id)).Where(x => x != null);
return ResultList.Create(assets.Total, sorted); return ResultList.Create(assets.Total, sorted);
} }
private Query ParseQuery(QueryContext context, string query)
{
try
{
var result = EdmAssetModel.Edm.ParseQuery(query).ToQuery();
if (result.Sort.Count == 0)
{
result.Sort.Add(new SortNode(new List<string> { "lastModified" }, SortOrder.Descending));
}
if (result.Take > MaxResults)
{
result.Take = MaxResults;
}
return result;
}
catch (NotSupportedException)
{
throw new ValidationException($"OData operation is not supported.");
}
catch (ODataException ex)
{
throw new ValidationException($"Failed to parse query: {ex.Message}", ex);
}
}
private async Task DenormalizeTagsAsync(Guid appId, IEnumerable<IAssetEntity> assets) private async Task DenormalizeTagsAsync(Guid appId, IEnumerable<IAssetEntity> assets)
{ {
var tags = new HashSet<string>(assets.Where(x => x.Tags != null).SelectMany(x => x.Tags).Distinct()); var tags = new HashSet<string>(assets.Where(x => x.Tags != null).SelectMany(x => x.Tags).Distinct());

2
src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs

@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
public interface IAssetQueryService public interface IAssetQueryService
{ {
Task<IResultList<IAssetEntity>> QueryAsync(QueryContext contex, Query query); Task<IResultList<IAssetEntity>> QueryAsync(QueryContext contex, Q query);
Task<IAssetEntity> FindAssetAsync(QueryContext context, Guid id); Task<IAssetEntity> FindAssetAsync(QueryContext context, Guid id);
} }

3
src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs

@ -9,12 +9,13 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.Assets.Repositories namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{ {
public interface IAssetRepository public interface IAssetRepository
{ {
Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, string query = null); Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, Query query);
Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<Guid> ids); Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<Guid> ids);

28
src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs

@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.OData; using Microsoft.OData;
using Microsoft.OData.UriParser;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
@ -19,6 +18,8 @@ using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Queries.OData;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
#pragma warning disable RECS0147 #pragma warning disable RECS0147
@ -27,6 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
public sealed class ContentQueryService : IContentQueryService public sealed class ContentQueryService : IContentQueryService
{ {
private const int MaxResults = 20;
private static readonly Status[] StatusAll = { Status.Archived, Status.Draft, Status.Published }; private static readonly Status[] StatusAll = { Status.Archived, Status.Draft, Status.Published };
private static readonly Status[] StatusArchived = { Status.Archived }; private static readonly Status[] StatusArchived = { Status.Archived };
private static readonly Status[] StatusPublished = { Status.Published }; private static readonly Status[] StatusPublished = { Status.Published };
@ -88,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
} }
} }
public async Task<IResultList<IContentEntity>> QueryAsync(ContentQueryContext context, Query query) public async Task<IResultList<IContentEntity>> QueryAsync(ContentQueryContext context, Q query)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
@ -128,7 +130,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return ResultList.Create(contents.Total, transformed); return ResultList.Create(contents.Total, transformed);
} }
private IResultList<IContentEntity> Sort(IResultList<IContentEntity> contents, IList<Guid> ids) private IResultList<IContentEntity> Sort(IResultList<IContentEntity> contents, IReadOnlyList<Guid> ids)
{ {
var sorted = ids.Select(id => contents.FirstOrDefault(x => x.Id == id)).Where(x => x != null); var sorted = ids.Select(id => contents.FirstOrDefault(x => x.Id == id)).Where(x => x != null);
@ -197,7 +199,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
} }
} }
private ODataUriParser ParseQuery(QueryContext context, string query, ISchemaEntity schema) private Query ParseQuery(QueryContext context, string query, ISchemaEntity schema)
{ {
using (Profiler.TraceMethod<ContentQueryService>()) using (Profiler.TraceMethod<ContentQueryService>())
{ {
@ -205,7 +207,23 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
var model = modelBuilder.BuildEdmModel(schema, context.App); var model = modelBuilder.BuildEdmModel(schema, context.App);
return model.ParseQuery(query); var result = model.ParseQuery(query).ToQuery();
if (result.Sort.Count == 0)
{
result.Sort.Add(new SortNode(new List<string> { "lastModified" }, SortOrder.Descending));
}
if (result.Take > MaxResults)
{
result.Take = MaxResults;
}
return result;
}
catch (NotSupportedException)
{
throw new ValidationException($"OData operation is not supported.");
} }
catch (ODataException ex) catch (ODataException ex)
{ {

2
src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs

@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
public interface IContentQueryService public interface IContentQueryService
{ {
Task<IResultList<IContentEntity>> QueryAsync(ContentQueryContext context, Query query); Task<IResultList<IContentEntity>> QueryAsync(ContentQueryContext context, Q query);
Task<IContentEntity> FindContentAsync(ContentQueryContext context, Guid id, long version = EtagVersion.Any); Task<IContentEntity> FindContentAsync(ContentQueryContext context, Guid id, long version = EtagVersion.Any);

8
src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs

@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
public async Task<IResultList<IAssetEntity>> QueryAssetsAsync(string query) public async Task<IResultList<IAssetEntity>> QueryAssetsAsync(string query)
{ {
var assets = await assetQuery.QueryAsync(context, Query.Empty.WithODataQuery(query)); var assets = await assetQuery.QueryAsync(context, Q.Empty.WithODataQuery(query));
foreach (var asset in assets) foreach (var asset in assets)
{ {
@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
public async Task<IResultList<IContentEntity>> QueryContentsAsync(string schemaIdOrName, string query) public async Task<IResultList<IContentEntity>> QueryContentsAsync(string schemaIdOrName, string query)
{ {
var result = await contentQuery.QueryAsync(new ContentQueryContext(context).WithSchemaName(schemaIdOrName), Query.Empty.WithODataQuery(query)); var result = await contentQuery.QueryAsync(new ContentQueryContext(context).WithSchemaName(schemaIdOrName), Q.Empty.WithODataQuery(query));
foreach (var content in result) foreach (var content in result)
{ {
@ -100,7 +100,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (notLoadedAssets.Count > 0) if (notLoadedAssets.Count > 0)
{ {
var assets = await assetQuery.QueryAsync(context, Query.Empty.WithIds(notLoadedAssets)); var assets = await assetQuery.QueryAsync(context, Q.Empty.WithIds(notLoadedAssets));
foreach (var asset in assets) foreach (var asset in assets)
{ {
@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (notLoadedContents.Count > 0) if (notLoadedContents.Count > 0)
{ {
var result = await contentQuery.QueryAsync(new ContentQueryContext(context).WithSchemaId(schemaId), Query.Empty.WithIds(notLoadedContents)); var result = await contentQuery.QueryAsync(new ContentQueryContext(context).WithSchemaId(schemaId), Q.Empty.WithIds(notLoadedContents));
foreach (var content in result) foreach (var content in result)
{ {

4
src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs

@ -8,12 +8,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.OData.UriParser;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.Contents.Repositories namespace Squidex.Domain.Apps.Entities.Contents.Repositories
{ {
@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories
{ {
Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids); Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids);
Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery); Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Query query);
Task<IReadOnlyList<Guid>> QueryNotFoundAsync(Guid appId, Guid schemaId, IList<Guid> ids); Task<IReadOnlyList<Guid>> QueryNotFoundAsync(Guid appId, Guid schemaId, IList<Guid> ids);

38
src/Squidex.Domain.Apps.Entities/EdmModelExtensions.cs

@ -1,38 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser;
namespace Squidex.Domain.Apps.Entities
{
public static class EdmModelExtensions
{
public static ODataUriParser ParseQuery(this IEdmModel model, string query)
{
if (!model.EntityContainer.EntitySets().Any())
{
return null;
}
query = query ?? string.Empty;
var path = model.EntityContainer.EntitySets().First().Path.Path.Split('.').Last();
if (query.StartsWith("?", StringComparison.Ordinal))
{
query = query.Substring(1);
}
var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative));
return parser;
}
}
}

18
src/Squidex.Domain.Apps.Entities/Query.cs → src/Squidex.Domain.Apps.Entities/Q.cs

@ -12,39 +12,41 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities namespace Squidex.Domain.Apps.Entities
{ {
public sealed class Query : Cloneable<Query> public sealed class Q : Cloneable<Q>
{ {
public static readonly Query Empty = new Query(); public static readonly Q Empty = new Q();
public List<Guid> Ids { get; private set; } public IReadOnlyList<Guid> Ids { get; private set; }
public string ODataQuery { get; private set; } public string ODataQuery { get; private set; }
public Query WithODataQuery(string odataQuery) public Q WithODataQuery(string odataQuery)
{ {
return Clone(c => c.ODataQuery = odataQuery); return Clone(c => c.ODataQuery = odataQuery);
} }
public Query WithIds(IEnumerable<Guid> ids) public Q WithIds(IEnumerable<Guid> ids)
{ {
return Clone(c => c.Ids = ids.ToList()); return Clone(c => c.Ids = ids.ToList());
} }
public Query WithIds(string ids) public Q WithIds(string ids)
{ {
if (!string.IsNullOrEmpty(ids)) if (!string.IsNullOrEmpty(ids))
{ {
return Clone(c => return Clone(c =>
{ {
c.Ids = new List<Guid>(); var idsList = new List<Guid>();
foreach (var id in ids.Split(',')) foreach (var id in ids.Split(','))
{ {
if (Guid.TryParse(id, out var guid)) if (Guid.TryParse(id, out var guid))
{ {
c.Ids.Add(guid); idsList.Add(guid);
} }
} }
c.Ids = idsList;
}); });
} }

83
src/Squidex.Infrastructure.MongoDb/MongoDb/OData/ConstantVisitor.cs

@ -1,83 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Microsoft.OData;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser;
using NodaTime;
using NodaTime.Text;
namespace Squidex.Infrastructure.MongoDb.OData
{
public sealed class ConstantVisitor : QueryNodeVisitor<object>
{
private static readonly IEdmPrimitiveType BooleanType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Boolean);
private static readonly IEdmPrimitiveType DateTimeType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset);
private static readonly IEdmPrimitiveType GuidType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Guid);
private static readonly ConstantVisitor Instance = new ConstantVisitor();
private ConstantVisitor()
{
}
public static object Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override object Visit(ConvertNode nodeIn)
{
if (nodeIn.TypeReference.Definition == BooleanType)
{
return bool.Parse(Visit(nodeIn.Source).ToString());
}
if (nodeIn.TypeReference.Definition == GuidType)
{
return Guid.Parse(Visit(nodeIn.Source).ToString());
}
if (nodeIn.TypeReference.Definition == DateTimeType)
{
var value = Visit(nodeIn.Source);
if (value is DateTimeOffset dateTimeOffset)
{
return Instant.FromDateTimeOffset(dateTimeOffset);
}
if (value is DateTime dateTime)
{
return Instant.FromDateTimeUtc(DateTime.SpecifyKind(dateTime, DateTimeKind.Utc));
}
if (value is Date date)
{
return Instant.FromUtc(date.Year, date.Month, date.Day, 0, 0);
}
var parseResult = InstantPattern.General.Parse(Visit(nodeIn.Source).ToString());
if (!parseResult.Success)
{
throw new ODataException("Datetime is not in a valid format. Use ISO 8601");
}
return parseResult.Value;
}
return base.Visit(nodeIn);
}
public override object Visit(ConstantNode nodeIn)
{
return nodeIn.Value;
}
}
}

33
src/Squidex.Infrastructure.MongoDb/MongoDb/OData/FilterBuilder.cs

@ -5,49 +5,28 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Microsoft.OData;
using Microsoft.OData.UriParser;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.Queries;
namespace Squidex.Infrastructure.MongoDb.OData namespace Squidex.Infrastructure.MongoDb.OData
{ {
public static class FilterBuilder public static class FilterBuilder
{ {
public static (FilterDefinition<T> Filter, bool Last) BuildFilter<T>(this ODataUriParser query, ConvertProperty convertProperty = null, ConvertValue convertValue = null, bool supportsSearch = true) public static (FilterDefinition<T> Filter, bool Last) BuildFilter<T>(this Query query, bool supportsSearch = true)
{ {
SearchClause search; if (query.FullText != null)
try
{
search = query.ParseSearch();
}
catch (ODataException ex)
{
throw new ValidationException("Query $search clause not valid.", new ValidationError(ex.Message));
}
if (search != null)
{ {
if (!supportsSearch) if (!supportsSearch)
{ {
throw new ValidationException("Query $search clause not supported."); throw new ValidationException("Query $search clause not supported.");
} }
return (Builders<T>.Filter.Text(SearchTermVisitor.Visit(search.Expression).ToString()), false); return (Builders<T>.Filter.Text(query.FullText), false);
}
FilterClause filter;
try
{
filter = query.ParseFilter();
}
catch (ODataException ex)
{
throw new ValidationException("Query $filter clause not valid.", new ValidationError(ex.Message));
} }
if (filter != null) if (query.Filter != null)
{ {
return (FilterVisitor<T>.Visit(filter.Expression, convertProperty, convertValue), true); return (FilterVisitor<T>.Visit(query.Filter), true);
} }
return (null, false); return (null, false);

174
src/Squidex.Infrastructure.MongoDb/MongoDb/OData/FilterVisitor.cs

@ -7,179 +7,77 @@
using System; using System;
using System.Linq; using System.Linq;
using Microsoft.OData.UriParser;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.Queries;
namespace Squidex.Infrastructure.MongoDb.OData namespace Squidex.Infrastructure.MongoDb.OData
{ {
public sealed class FilterVisitor<T> : QueryNodeVisitor<FilterDefinition<T>> public sealed class FilterVisitor<T> : FilterNodeVisitor<FilterDefinition<T>>
{ {
private static readonly FilterDefinitionBuilder<T> Filter = Builders<T>.Filter; private static readonly FilterDefinitionBuilder<T> Filter = Builders<T>.Filter;
private readonly ConvertProperty convertProperty; private static readonly FilterVisitor<T> Instance = new FilterVisitor<T>();
private readonly ConvertValue convertValue;
private FilterVisitor(ConvertProperty convertProperty, ConvertValue convertValue) private FilterVisitor()
{ {
this.convertProperty = convertProperty;
this.convertValue = convertValue;
} }
public static FilterDefinition<T> Visit(QueryNode node, ConvertProperty propertyCalculator, ConvertValue convertValue) public static FilterDefinition<T> Visit(FilterNode node)
{ {
var visitor = new FilterVisitor<T>(propertyCalculator, convertValue); return node.Accept(Instance);
return node.Accept(visitor);
}
public override FilterDefinition<T> Visit(ConvertNode nodeIn)
{
return nodeIn.Source.Accept(this);
} }
public override FilterDefinition<T> Visit(UnaryOperatorNode nodeIn) public override FilterDefinition<T> Visit(FilterNegate nodeIn)
{ {
if (nodeIn.OperatorKind == UnaryOperatorKind.Not) return Filter.Not(nodeIn.Operand.Accept(this));
{
return Filter.Not(nodeIn.Operand.Accept(this));
}
throw new NotSupportedException();
} }
public override FilterDefinition<T> Visit(SingleValueFunctionCallNode nodeIn) public override FilterDefinition<T> Visit(FilterJunction nodeIn)
{ {
var fieldNode = nodeIn.Parameters.ElementAt(0); if (nodeIn.JunctionType == FilterJunctionType.And)
var valueNode = nodeIn.Parameters.ElementAt(1);
if (string.Equals(nodeIn.Name, "endswith", StringComparison.OrdinalIgnoreCase))
{
var f = BuildFieldDefinition(fieldNode);
var v = BuildRegex(f, valueNode, s => s + "$");
return Filter.Regex(f, v);
}
if (string.Equals(nodeIn.Name, "startswith", StringComparison.OrdinalIgnoreCase))
{ {
var f = BuildFieldDefinition(fieldNode); return Filter.And(nodeIn.Operands.Select(x => x.Accept(this)));
var v = BuildRegex(f, valueNode, s => "^" + s);
return Filter.Regex(f, v);
} }
else
if (string.Equals(nodeIn.Name, "contains", StringComparison.OrdinalIgnoreCase))
{ {
var f = BuildFieldDefinition(fieldNode); return Filter.Or(nodeIn.Operands.Select(x => x.Accept(this)));
var v = BuildRegex(f, valueNode, s => s);
return Filter.Regex(f, v);
} }
throw new NotSupportedException(); throw new NotSupportedException();
} }
public override FilterDefinition<T> Visit(BinaryOperatorNode nodeIn) public override FilterDefinition<T> Visit(FilterComparison nodeIn)
{ {
if (nodeIn.OperatorKind == BinaryOperatorKind.And) var propertyName = string.Join(".", nodeIn.Path);
{
return Filter.And(nodeIn.Left.Accept(this), nodeIn.Right.Accept(this));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.Or)
{
return Filter.Or(nodeIn.Left.Accept(this), nodeIn.Right.Accept(this));
}
if (nodeIn.Left is SingleValueFunctionCallNode functionNode)
{
var regexFilter = Visit(functionNode);
var value = BuildValue(nodeIn.Right); switch (nodeIn.Operator)
if (value is bool booleanRight)
{
if ((nodeIn.OperatorKind == BinaryOperatorKind.Equal && !booleanRight) ||
(nodeIn.OperatorKind == BinaryOperatorKind.NotEqual && booleanRight))
{
regexFilter = Filter.Not(regexFilter);
}
return regexFilter;
}
}
else
{ {
if (nodeIn.OperatorKind == BinaryOperatorKind.NotEqual) case FilterOperator.StartsWith:
{ return Filter.Regex(propertyName, BuildRegex(nodeIn, s => "$" + s));
var f = BuildFieldDefinition(nodeIn.Left); case FilterOperator.Contains:
var v = BuildValue(f, nodeIn.Right); return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s));
case FilterOperator.EndsWith:
return Filter.Or(Filter.Not(Filter.Exists(f)), Filter.Ne(f, v)); return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s + "$"));
} case FilterOperator.Equals:
return Filter.Eq(propertyName, nodeIn.Value);
if (nodeIn.OperatorKind == BinaryOperatorKind.Equal) case FilterOperator.GreaterThan:
{ return Filter.Gt(propertyName, nodeIn.Value);
var f = BuildFieldDefinition(nodeIn.Left); case FilterOperator.GreaterThanOrEqual:
var v = BuildValue(f, nodeIn.Right); return Filter.Gte(propertyName, nodeIn.Value);
case FilterOperator.LessThan:
return Filter.Eq(f, v); return Filter.Lt(propertyName, nodeIn.Value);
} case FilterOperator.LessThanOrEqual:
return Filter.Lte(propertyName, nodeIn.Value);
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThan) case FilterOperator.NotEquals:
{ return Filter.Ne(propertyName, nodeIn.Value);
var f = BuildFieldDefinition(nodeIn.Left);
var v = BuildValue(f, nodeIn.Right);
return Filter.Lt(f, v);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThanOrEqual)
{
var f = BuildFieldDefinition(nodeIn.Left);
var v = BuildValue(f, nodeIn.Right);
return Filter.Lte(f, v);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThan)
{
var f = BuildFieldDefinition(nodeIn.Left);
var v = BuildValue(f, nodeIn.Right);
return Filter.Gt(f, v);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThanOrEqual)
{
var f = BuildFieldDefinition(nodeIn.Left);
var v = BuildValue(f, nodeIn.Right);
return Filter.Gte(f, v);
}
} }
throw new NotSupportedException(); throw new NotSupportedException();
} }
private BsonRegularExpression BuildRegex(string field, QueryNode node, Func<string, string> formatter) private BsonRegularExpression BuildRegex(FilterComparison node, Func<string, string> formatter)
{
return new BsonRegularExpression(formatter(BuildValue(field, node).ToString()), "i");
}
private string BuildFieldDefinition(QueryNode nodeIn)
{
return nodeIn.BuildFieldDefinition(convertProperty);
}
private object BuildValue(string field, QueryNode nodeIn)
{
return ValueConversion.Convert(field, ConstantVisitor.Visit(nodeIn), convertValue);
}
private object BuildValue(QueryNode nodeIn)
{ {
return ConstantVisitor.Visit(nodeIn); return new BsonRegularExpression(formatter(node.Value.ToString()), "i");
} }
} }
} }

27
src/Squidex.Infrastructure.MongoDb/MongoDb/OData/LimitExtensions.cs

@ -5,41 +5,28 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using Microsoft.OData.UriParser;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.Queries;
namespace Squidex.Infrastructure.MongoDb.OData namespace Squidex.Infrastructure.MongoDb.OData
{ {
public static class LimitExtensions public static class LimitExtensions
{ {
public static IFindFluent<T, T> Take<T>(this IFindFluent<T, T> cursor, ODataUriParser query, int maxValue = 200, int defaultValue = 20) public static IFindFluent<T, T> Take<T>(this IFindFluent<T, T> cursor, Query query)
{ {
var top = query.ParseTop(); if (query.Take < long.MaxValue)
if (top.HasValue)
{
cursor = cursor.Limit(Math.Min((int)top.Value, maxValue));
}
else
{ {
cursor = cursor.Limit(defaultValue); cursor = cursor.Limit((int)query.Take);
} }
return cursor; return cursor;
} }
public static IFindFluent<T, T> Skip<T>(this IFindFluent<T, T> cursor, ODataUriParser query) public static IFindFluent<T, T> Skip<T>(this IFindFluent<T, T> cursor, Query query)
{ {
var skip = query.ParseSkip(); if (query.Skip > 0)
if (skip.HasValue)
{
cursor = cursor.Skip((int)skip.Value);
}
else
{ {
cursor = cursor.Skip(null); cursor = cursor.Skip((int)query.Skip);
} }
return cursor; return cursor;

32
src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyBuilder.cs

@ -1,32 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using Microsoft.OData.UriParser;
namespace Squidex.Infrastructure.MongoDb.OData
{
public delegate string ConvertProperty(string[] parts);
public static class PropertyBuilder
{
private static readonly ConvertProperty Default = parts =>
{
return string.Join(".", parts).ToPascalCase();
};
public static string BuildFieldDefinition(this QueryNode node, ConvertProperty convertProperty)
{
convertProperty = convertProperty ?? Default;
var propertyParts = node.Accept(PropertyNameVisitor.Instance).ToArray();
var propertyName = convertProperty(propertyParts);
return propertyName;
}
}
}

50
src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyNameVisitor.cs

@ -1,50 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Immutable;
using Microsoft.OData.UriParser;
namespace Squidex.Infrastructure.MongoDb.OData
{
public sealed class PropertyNameVisitor : QueryNodeVisitor<ImmutableList<string>>
{
public static readonly PropertyNameVisitor Instance = new PropertyNameVisitor();
private PropertyNameVisitor()
{
}
public override ImmutableList<string> Visit(ConvertNode nodeIn)
{
return nodeIn.Source.Accept(this);
}
public override ImmutableList<string> Visit(SingleComplexNode nodeIn)
{
if (nodeIn.Source is SingleComplexNode)
{
return nodeIn.Source.Accept(this).Add(nodeIn.Property.Name);
}
else
{
return ImmutableList.Create(nodeIn.Property.Name);
}
}
public override ImmutableList<string> Visit(SingleValuePropertyAccessNode nodeIn)
{
if (nodeIn.Source is SingleComplexNode)
{
return nodeIn.Source.Accept(this).Add(nodeIn.Property.Name);
}
else
{
return ImmutableList.Create(nodeIn.Property.Name);
}
}
}
}

41
src/Squidex.Infrastructure.MongoDb/MongoDb/OData/SearchTermVisitor.cs

@ -1,41 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Microsoft.OData.UriParser;
namespace Squidex.Infrastructure.MongoDb.OData
{
public class SearchTermVisitor : QueryNodeVisitor<string>
{
private static readonly SearchTermVisitor Instance = new SearchTermVisitor();
private SearchTermVisitor()
{
}
public static object Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override string Visit(BinaryOperatorNode nodeIn)
{
if (nodeIn.OperatorKind == BinaryOperatorKind.And)
{
return nodeIn.Left.Accept(this) + " " + nodeIn.Right.Accept(this);
}
throw new NotSupportedException();
}
public override string Visit(SearchTermNode nodeIn)
{
return nodeIn.Text;
}
}
}

20
src/Squidex.Infrastructure.MongoDb/MongoDb/OData/SortBuilder.cs

@ -6,26 +6,22 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.OData.UriParser;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.Queries;
namespace Squidex.Infrastructure.MongoDb.OData namespace Squidex.Infrastructure.MongoDb.OData
{ {
public static class SortBuilder public static class SortBuilder
{ {
public static SortDefinition<T> BuildSort<T>(this ODataUriParser query, ConvertProperty propertyCalculator = null) public static SortDefinition<T> BuildSort<T>(this Query query)
{ {
var orderBy = query.ParseOrderBy(); if (query.Sort.Count > 0)
if (orderBy != null)
{ {
var sorts = new List<SortDefinition<T>>(); var sorts = new List<SortDefinition<T>>();
while (orderBy != null) foreach (var sort in query.Sort)
{ {
sorts.Add(OrderBy<T>(orderBy, propertyCalculator)); sorts.Add(OrderBy<T>(sort));
orderBy = orderBy.ThenBy;
} }
if (sorts.Count > 1) if (sorts.Count > 1)
@ -41,11 +37,11 @@ namespace Squidex.Infrastructure.MongoDb.OData
return null; return null;
} }
public static SortDefinition<T> OrderBy<T>(OrderByClause clause, ConvertProperty propertyCalculator = null) public static SortDefinition<T> OrderBy<T>(SortNode sort)
{ {
var propertyName = clause.Expression.BuildFieldDefinition(propertyCalculator); var propertyName = string.Join(".", sort.Path);
if (clause.Direction == OrderByDirection.Ascending) if (sort.SortOrder == SortOrder.Ascending)
{ {
return Builders<T>.Sort.Ascending(propertyName); return Builders<T>.Sort.Ascending(propertyName);
} }

1
src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj

@ -11,7 +11,6 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> <ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.OData.Core" Version="7.5.0" />
<PackageReference Include="MongoDB.Driver" Version="2.7.0" /> <PackageReference Include="MongoDB.Driver" Version="2.7.0" />
<PackageReference Include="MongoDB.Driver.GridFS" Version="2.7.0" /> <PackageReference Include="MongoDB.Driver.GridFS" Version="2.7.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" />

32
src/Squidex.Infrastructure/Queries/FilterBuilder.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Queries
{
public static class FilterBuilder
{
public static FilterJunction And(params FilterNode[] operands)
{
return new FilterJunction(FilterJunctionType.And, operands);
}
public static FilterJunction Or(params FilterNode[] operands)
{
return new FilterJunction(FilterJunctionType.Or, operands);
}
public static FilterComparison Eq(string path, object value)
{
return Binary(path, FilterOperator.Equals, value);
}
private static FilterComparison Binary(string path, FilterOperator @operator, object value)
{
return new FilterComparison(path.Split('.', '/'), @operator, value, FilterValueType.String);
}
}
}

11
src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs

@ -39,10 +39,13 @@ namespace Squidex.Infrastructure.Queries.OData
{ {
var query = new Query(); var query = new Query();
parser.ParseTake(query); if (parser != null)
parser.ParseSkip(query); {
parser.ParseFilter(query); parser.ParseTake(query);
parser.ParseSort(query); parser.ParseSkip(query);
parser.ParseFilter(query);
parser.ParseSort(query);
}
return query; return query;
} }

4
src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs

@ -17,7 +17,7 @@ namespace Squidex.Infrastructure.Queries.OData
if (top.HasValue) if (top.HasValue)
{ {
result.Take = top; result.Take = top.Value;
} }
} }
@ -27,7 +27,7 @@ namespace Squidex.Infrastructure.Queries.OData
if (skip.HasValue) if (skip.HasValue)
{ {
result.Skip = skip; result.Skip = skip.Value;
} }
} }
} }

17
src/Squidex.Domain.Apps.Core.Operations/Queries/TransformVisitor.cs → src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs

@ -7,23 +7,24 @@
using System.Linq; using System.Linq;
namespace Squidex.Domain.Apps.Core.Queries namespace Squidex.Infrastructure.Queries
{ {
public abstract class TransformVisitor : FilterNodeVisitor<FilterNode> public sealed class PascalCasePathConverter : TransformVisitor
{ {
public override FilterNode Visit(FilterComparison nodeIn) private static readonly PascalCasePathConverter Instance = new PascalCasePathConverter();
private PascalCasePathConverter()
{ {
return nodeIn;
} }
public override FilterNode Visit(FilterJunction nodeIn) public static FilterNode Transform(FilterNode node)
{ {
return new FilterJunction(nodeIn.JunctionType, nodeIn.Operands.Select(x => x.Accept(this)).ToList()); return node.Accept(Instance);
} }
public override FilterNode Visit(FilterNegate nodeIn) public override FilterNode Visit(FilterComparison nodeIn)
{ {
return new FilterNegate(nodeIn.Operand.Accept(this)); return new FilterComparison(nodeIn.Path.Select(x => x.ToPascalCase()).ToList(), nodeIn.Operator, nodeIn.Value, nodeIn.ValueType);
} }
} }
} }

10
src/Squidex.Infrastructure/Queries/Query.cs

@ -15,11 +15,11 @@ namespace Squidex.Infrastructure.Queries
public string FullText { get; set; } public string FullText { get; set; }
public long? Skip { get; set; } public long Skip { get; set; }
public long? Take { get; set; } public long Take { get; set; } = long.MaxValue;
public List<SortNode> Sort { get; } = new List<SortNode>(); public List<SortNode> Sort { get; set; } = new List<SortNode>();
public override string ToString() public override string ToString()
{ {
@ -35,12 +35,12 @@ namespace Squidex.Infrastructure.Queries
parts.Add($"FullText: {FullText}"); parts.Add($"FullText: {FullText}");
} }
if (Skip != null) if (Skip > 0)
{ {
parts.Add($"Skip: {Skip}"); parts.Add($"Skip: {Skip}");
} }
if (Take != null) if (Take < long.MaxValue)
{ {
parts.Add($"Take: {Take}"); parts.Add($"Take: {Take}");
} }

20
src/Squidex.Infrastructure.MongoDb/MongoDb/OData/ValueConversion.cs → src/Squidex.Infrastructure/Queries/SortBuilder.cs

@ -5,20 +5,20 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
namespace Squidex.Infrastructure.MongoDb.OData using System.Linq;
{
public delegate object ConvertValue(string field, object value);
public static class ValueConversion namespace Squidex.Infrastructure.Queries
{
public static class SortBuilder
{ {
public static object Convert(string field, object value, ConvertValue converter = null) public static SortNode Ascending(string path)
{ {
if (converter == null) return new SortNode(path.Split('.', '/').ToList(), SortOrder.Ascending);
{ }
return value;
}
return converter(field, value); public static SortNode Descending(string path)
{
return new SortNode(path.Split('.', '/').ToList(), SortOrder.Descending);
} }
} }
} }

2
src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -102,7 +102,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
{ {
var context = Context(); var context = Context();
var assets = await assetQuery.QueryAsync(context, Query.Empty.WithODataQuery(Request.QueryString.ToString()).WithIds(ids)); var assets = await assetQuery.QueryAsync(context, Q.Empty.WithODataQuery(Request.QueryString.ToString()).WithIds(ids));
var response = AssetsDto.FromAssets(assets); var response = AssetsDto.FromAssets(assets);

2
src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -130,7 +130,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{ {
var context = Context().WithArchived(archived).WithSchemaName(name); var context = Context().WithArchived(archived).WithSchemaName(name);
var result = await contentQuery.QueryAsync(context, Query.Empty.WithIds(ids).WithODataQuery(Request.QueryString.ToString())); var result = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids).WithODataQuery(Request.QueryString.ToString()));
var response = new ContentsDto var response = new ContentsDto
{ {

1
src/Squidex/Config/Domain/EntitiesServices.cs

@ -15,6 +15,7 @@ using Orleans;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;

8
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs

@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
CreateAsset(id1, "id1", "id2", "id3"), CreateAsset(id1, "id1", "id2", "id3"),
CreateAsset(id2))); CreateAsset(id2)));
var result = await sut.QueryAsync(context, Query.Empty.WithIds(ids)); var result = await sut.QueryAsync(context, Q.Empty.WithIds(ids));
Assert.Equal(8, result.Total); Assert.Equal(8, result.Total);
Assert.Equal(2, result.Count); Assert.Equal(2, result.Count);
@ -87,6 +87,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.Empty(result[1].Tags); Assert.Empty(result[1].Tags);
} }
/*
* TODO
[Fact] [Fact]
public async Task Should_load_assets_with_query_and_resolve_tags() public async Task Should_load_assets_with_query_and_resolve_tags()
{ {
@ -95,14 +97,14 @@ namespace Squidex.Domain.Apps.Entities.Assets
CreateAsset(Guid.NewGuid(), "id1", "id2"), CreateAsset(Guid.NewGuid(), "id1", "id2"),
CreateAsset(Guid.NewGuid(), "id2", "id3"))); CreateAsset(Guid.NewGuid(), "id2", "id3")));
var result = await sut.QueryAsync(context, Query.Empty.WithODataQuery("my-query")); var result = await sut.QueryAsync(context, Q.Empty.WithODataQuery("my-query"));
Assert.Equal(8, result.Total); Assert.Equal(8, result.Total);
Assert.Equal(2, result.Count); Assert.Equal(2, result.Count);
Assert.Equal(HashSet.Of("name1", "name2"), result[0].Tags); Assert.Equal(HashSet.Of("name1", "name2"), result[0].Tags);
Assert.Equal(HashSet.Of("name2", "name3"), result[1].Tags); Assert.Equal(HashSet.Of("name2", "name3"), result[1].Tags);
} } */
private IAssetEntity CreateAsset(Guid id, params string[] tags) private IAssetEntity CreateAsset(Guid id, params string[] tags)
{ {

162
tests/Squidex.Domain.Apps.Entities.Tests/Assets/OData/ODataQueryTests.cs → tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs

@ -6,56 +6,44 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic; using System.Linq;
using FakeItEasy; using FakeItEasy;
using Microsoft.OData.Edm;
using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Tags; using NodaTime.Text;
using Squidex.Domain.Apps.Entities.Assets.Edm;
using Squidex.Domain.Apps.Entities.MongoDb.Assets; using Squidex.Domain.Apps.Entities.MongoDb.Assets;
using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors; using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.MongoDb.OData; using Squidex.Infrastructure.MongoDb.OData;
using Squidex.Infrastructure.Queries;
using Xunit; using Xunit;
using FilterBuilder = Squidex.Infrastructure.Queries.FilterBuilder;
using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder;
namespace Squidex.Domain.Apps.Entities.Assets.OData namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
{ {
public class ODataQueryTests public class MongoDbQueryTests
{ {
private readonly ITagService tagService = A.Fake<ITagService>(); private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry;
private readonly IBsonSerializerRegistry registry = BsonSerializer.SerializerRegistry; private static readonly IBsonSerializer<MongoAssetEntity> Serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoAssetEntity>();
private readonly IBsonSerializer<MongoAssetEntity> serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoAssetEntity>();
private readonly IEdmModel edmModel = EdmAssetModel.Edm;
private readonly Guid appId = Guid.NewGuid(); private readonly Guid appId = Guid.NewGuid();
private readonly ConvertValue valueConverter;
static ODataQueryTests() static MongoDbQueryTests()
{ {
InstantSerializer.Register(); InstantSerializer.Register();
} }
public ODataQueryTests()
{
A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A<HashSet<string>>.That.Contains("tag1")))
.Returns(new Dictionary<string, string> { ["tag1"] = "normalized1" });
valueConverter = FindExtensions.CreateValueConverter(appId, tagService);
}
[Fact] [Fact]
public void Should_parse_query() public void Should_throw_exception_for_full_text_search()
{ {
var parser = edmModel.ParseQuery("$filter=lastModifiedBy eq 'Sebastian'"); Assert.Throws<ValidationException>(() => Q(new Query { FullText = "Full Text" }));
Assert.NotNull(parser);
} }
[Fact] [Fact]
public void Should_make_query_with_lastModified() public void Should_make_query_with_lastModified()
{ {
var i = F("$filter=lastModified eq 1988-01-19T12:00:00Z"); var i = F(FilterBuilder.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value));
var o = C("{ 'LastModified' : ISODate('1988-01-19T12:00:00Z') }"); var o = C("{ 'LastModified' : ISODate('1988-01-19T12:00:00Z') }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -64,7 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact] [Fact]
public void Should_make_query_with_lastModifiedBy() public void Should_make_query_with_lastModifiedBy()
{ {
var i = F("$filter=lastModifiedBy eq 'Me'"); var i = F(FilterBuilder.Eq("lastModifiedBy", "Me"));
var o = C("{ 'LastModifiedBy' : 'Me' }"); var o = C("{ 'LastModifiedBy' : 'Me' }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -73,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact] [Fact]
public void Should_make_query_with_created() public void Should_make_query_with_created()
{ {
var i = F("$filter=created eq 1988-01-19T12:00:00Z"); var i = F(FilterBuilder.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value));
var o = C("{ 'Created' : ISODate('1988-01-19T12:00:00Z') }"); var o = C("{ 'Created' : ISODate('1988-01-19T12:00:00Z') }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -82,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact] [Fact]
public void Should_make_query_with_createdBy() public void Should_make_query_with_createdBy()
{ {
var i = F("$filter=createdBy eq 'Me'"); var i = F(FilterBuilder.Eq("createdBy", "Me"));
var o = C("{ 'CreatedBy' : 'Me' }"); var o = C("{ 'CreatedBy' : 'Me' }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -91,17 +79,17 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact] [Fact]
public void Should_make_query_with_version() public void Should_make_query_with_version()
{ {
var i = F("$filter=version eq 0"); var i = F(FilterBuilder.Eq("version", 0));
var o = C("{ 'Version' : NumberLong(0) }"); var o = C("{ 'Version' : NumberLong(0) }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
[Fact] [Fact]
public void Should_make_query_with_normalized_tags() public void Should_make_query_with_fileVersion()
{ {
var i = F("$filter=tags eq 'tag1'"); var i = F(FilterBuilder.Eq("fileVersion", 2));
var o = C("{ 'Tags' : 'normalized1' }"); var o = C("{ 'FileVersion' : NumberLong(2) }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -109,8 +97,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact] [Fact]
public void Should_make_query_with_tags() public void Should_make_query_with_tags()
{ {
var i = F("$filter=tags eq 'tag2'"); var i = F(FilterBuilder.Eq("tags", "tag1"));
var o = C("{ 'Tags' : 'tag2' }"); var o = C("{ 'Tags' : 'tag1' }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -118,44 +106,35 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact] [Fact]
public void Should_make_query_with_fileName() public void Should_make_query_with_fileName()
{ {
var i = F("$filter=fileName eq 'Logo.png'"); var i = F(FilterBuilder.Eq("fileName", "Logo.png"));
var o = C("{ 'FileName' : 'Logo.png' }"); var o = C("{ 'FileName' : 'Logo.png' }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
[Fact] [Fact]
public void Should_make_query_with_fileSize() public void Should_make_query_with_isImage()
{
var i = F("$filter=fileSize eq 1024");
var o = C("{ 'FileSize' : NumberLong(1024) }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_fileVersion()
{ {
var i = F("$filter=fileVersion eq 2"); var i = F(FilterBuilder.Eq("isImage", true));
var o = C("{ 'FileVersion' : NumberLong(2) }"); var o = C("{ 'IsImage' : true }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
[Fact] [Fact]
public void Should_make_query_with_isImage() public void Should_make_query_with_mimeType()
{ {
var i = F("$filter=isImage eq true"); var i = F(FilterBuilder.Eq("mimeType", "text/json"));
var o = C("{ 'IsImage' : true }"); var o = C("{ 'MimeType' : 'text/json' }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
[Fact] [Fact]
public void Should_make_query_with_mimeType() public void Should_make_query_with_fileSize()
{ {
var i = F("$filter=mimeType eq 'text/json'"); var i = F(FilterBuilder.Eq("fileSize", 1024));
var o = C("{ 'MimeType' : 'text/json' }"); var o = C("{ 'FileSize' : NumberLong(1024) }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -163,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact] [Fact]
public void Should_make_query_with_pixelHeight() public void Should_make_query_with_pixelHeight()
{ {
var i = F("$filter=pixelHeight eq 600"); var i = F(FilterBuilder.Eq("pixelHeight", 600));
var o = C("{ 'PixelHeight' : 600 }"); var o = C("{ 'PixelHeight' : 600 }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -172,8 +151,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact] [Fact]
public void Should_make_query_with_pixelWidth() public void Should_make_query_with_pixelWidth()
{ {
var i = F("$filter=pixelWidth eq 600"); var i = F(FilterBuilder.Eq("pixelWidth", 800));
var o = C("{ 'PixelWidth' : 600 }"); var o = C("{ 'PixelWidth' : 800 }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -181,84 +160,55 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact] [Fact]
public void Should_make_orderby_with_single_field() public void Should_make_orderby_with_single_field()
{ {
var i = S("$orderby=lastModified desc"); var i = S(SortBuilder.Descending("lastModified"));
var o = C("{ 'LastModified' : -1 }"); var o = C("{ 'LastModified' : -1 }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
[Fact] [Fact]
public void Should_make_orderby_with_multiple_field() public void Should_make_orderby_with_multiple_fields()
{ {
var i = S("$orderby=lastModified, lastModifiedBy desc"); var i = S(SortBuilder.Ascending("lastModified"), SortBuilder.Descending("lastModifiedBy"));
var o = C("{ 'LastModified' : 1, 'LastModifiedBy' : -1 }"); var o = C("{ 'LastModified' : 1, 'LastModifiedBy' : -1 }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
[Fact] [Fact]
public void Should_make_top_statement() public void Should_make_take_statement()
{ {
var parser = edmModel.ParseQuery("$top=3"); var query = new Query { Take = 3 };
var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>(); var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
cursor.AssetTake(parser); cursor.AssetTake(query.AdjustToModel());
A.CallTo(() => cursor.Limit(3)).MustHaveHappened(); A.CallTo(() => cursor.Limit(3)).MustHaveHappened();
} }
[Fact]
public void Should_make_top_statement_with_limit()
{
var parser = edmModel.ParseQuery("$top=300");
var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
cursor.AssetTake(parser);
A.CallTo(() => cursor.Limit(200)).MustHaveHappened();
}
[Fact]
public void Should_make_top_statement_with_default_value()
{
var parser = edmModel.ParseQuery(string.Empty);
var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
cursor.AssetTake(parser);
A.CallTo(() => cursor.Limit(20)).MustHaveHappened();
}
[Fact] [Fact]
public void Should_make_skip_statement() public void Should_make_skip_statement()
{ {
var parser = edmModel.ParseQuery("$skip=3"); var query = new Query { Skip = 3 };
var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>(); var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
cursor.AssetSkip(parser); cursor.AssetSkip(query.AdjustToModel());
A.CallTo(() => cursor.Skip(3)).MustHaveHappened(); A.CallTo(() => cursor.Skip(3)).MustHaveHappened();
} }
[Fact] private static string C(string value)
public void Should_make_skip_statement_with_default_value()
{ {
var parser = edmModel.ParseQuery(string.Empty); return value.Replace('\'', '"');
var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
cursor.AssetSkip(parser);
A.CallTo(() => cursor.Skip(A<int>.Ignored)).MustNotHaveHappened();
} }
private static string C(string value) private string F(FilterNode filter)
{ {
return value.Replace('\'', '"'); return Q(new Query { Filter = filter });
} }
private string S(string value) private string S(params SortNode[] sorts)
{ {
var parser = edmModel.ParseQuery(value);
var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>(); var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
var i = string.Empty; var i = string.Empty;
@ -266,23 +216,21 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
A.CallTo(() => cursor.Sort(A<SortDefinition<MongoAssetEntity>>.Ignored)) A.CallTo(() => cursor.Sort(A<SortDefinition<MongoAssetEntity>>.Ignored))
.Invokes((SortDefinition<MongoAssetEntity> sortDefinition) => .Invokes((SortDefinition<MongoAssetEntity> sortDefinition) =>
{ {
i = sortDefinition.Render(serializer, registry).ToString(); i = sortDefinition.Render(Serializer, Registry).ToString();
}); });
cursor.AssetSort(parser); cursor.AssetSort(new Query { Sort = sorts.ToList() }.AdjustToModel());
return i; return i;
} }
private string F(string value) private string Q(Query query)
{ {
var parser = edmModel.ParseQuery(value); var rendered =
query.AdjustToModel().BuildFilter<MongoAssetEntity>(false).Filter
var query = .Render(Serializer, Registry).ToString();
parser.BuildFilter<MongoAssetEntity>(convertValue: valueConverter)
.Filter.Render(serializer, registry).ToString();
return query; return rendered;
} }
} }
} }

10
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs

@ -12,7 +12,6 @@ using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Microsoft.OData; using Microsoft.OData;
using Microsoft.OData.UriParser;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
@ -22,6 +21,7 @@ using Squidex.Domain.Apps.Entities.Contents.Edm;
using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Xunit; using Xunit;
@ -194,12 +194,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns(schema); .Returns(schema);
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), A<ODataUriParser>.Ignored)) A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), A<Query>.Ignored))
.Returns(ResultList.Create(total, Enumerable.Repeat(content, count))); .Returns(ResultList.Create(total, Enumerable.Repeat(content, count)));
var ctx = context.WithSchemaId(schemaId).WithArchived(archive).WithUnpublished(unpublished); var ctx = context.WithSchemaId(schemaId).WithArchived(archive).WithUnpublished(unpublished);
var result = await sut.QueryAsync(ctx, Query.Empty); var result = await sut.QueryAsync(ctx, Q.Empty);
Assert.Equal(contentData, result[0].Data); Assert.Equal(contentData, result[0].Data);
Assert.Equal(content.Id, result[0].Id); Assert.Equal(content.Id, result[0].Id);
@ -227,7 +227,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => modelBuilder.BuildEdmModel(schema, app)) A.CallTo(() => modelBuilder.BuildEdmModel(schema, app))
.Throws(new ODataException()); .Throws(new ODataException());
return Assert.ThrowsAsync<ValidationException>(() => sut.QueryAsync(context.WithSchemaId(schemaId), Query.Empty.WithODataQuery("query"))); return Assert.ThrowsAsync<ValidationException>(() => sut.QueryAsync(context.WithSchemaId(schemaId), Q.Empty.WithODataQuery("query")));
} }
public static IEnumerable<object[]> ManyIdRequestData = new[] public static IEnumerable<object[]> ManyIdRequestData = new[]
@ -261,7 +261,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var ctx = context.WithSchemaId(schemaId).WithArchived(archive).WithUnpublished(unpublished); var ctx = context.WithSchemaId(schemaId).WithArchived(archive).WithUnpublished(unpublished);
var result = await sut.QueryAsync(ctx, Query.Empty.WithIds(ids)); var result = await sut.QueryAsync(ctx, Q.Empty.WithIds(ids));
Assert.Equal(ids, result.Select(x => x.Id).ToList()); Assert.Equal(ids, result.Select(x => x.Id).ToList());
Assert.Equal(total, result.Total); Assert.Equal(total, result.Total);

12
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs

@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var asset = CreateAsset(Guid.NewGuid()); var asset = CreateAsset(Guid.NewGuid());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Query>.That.Matches(x => x.ODataQuery == "?$take=30&$skip=5&$search=my-query"))) A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Q>.That.Matches(x => x.ODataQuery == "?$take=30&$skip=5&$search=my-query")))
.Returns(ResultList.Create(0, asset)); .Returns(ResultList.Create(0, asset));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -129,7 +129,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var asset = CreateAsset(Guid.NewGuid()); var asset = CreateAsset(Guid.NewGuid());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Query>.That.Matches(x => x.ODataQuery == "?$take=30&$skip=5&$search=my-query"))) A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Q>.That.Matches(x => x.ODataQuery == "?$take=30&$skip=5&$search=my-query")))
.Returns(ResultList.Create(10, asset)); .Returns(ResultList.Create(10, asset));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -280,7 +280,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Query>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5"))) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5")))
.Returns(ResultList.Create(0, content)); .Returns(ResultList.Create(0, content));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -412,7 +412,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Query>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5"))) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5")))
.Returns(ResultList.Create(10, content)); .Returns(ResultList.Create(10, content));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -624,7 +624,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), contentId, EtagVersion.Any)) A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), contentId, EtagVersion.Any))
.Returns(content); .Returns(content);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Query>.Ignored)) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.Ignored))
.Returns(ResultList.Create(0, contentRef)); .Returns(ResultList.Create(0, contentRef));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -682,7 +682,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), contentId, EtagVersion.Any)) A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), contentId, EtagVersion.Any))
.Returns(content); .Returns(content);
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Query>.Ignored)) A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Q>.Ignored))
.Returns(ResultList.Create(0, assetRef)); .Returns(ResultList.Create(0, assetRef));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });

267
tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs

@ -0,0 +1,267 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Immutable;
using System.Linq;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.OData.Edm;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using NodaTime.Text;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.Edm;
using Squidex.Domain.Apps.Entities.MongoDb.Contents;
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.MongoDb.OData;
using Squidex.Infrastructure.Queries;
using Xunit;
using FilterBuilder = Squidex.Infrastructure.Queries.FilterBuilder;
using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder;
namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{
public class MongoDbQueryTests
{
private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry;
private static readonly IBsonSerializer<MongoContentEntity> Serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>();
private readonly Schema schemaDef;
private readonly IEdmModel edmModel;
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE);
static MongoDbQueryTests()
{
InstantSerializer.Register();
}
public MongoDbQueryTests()
{
schemaDef =
new Schema("user")
.AddString(1, "firstName", Partitioning.Language,
new StringFieldProperties { Label = "FirstName", IsRequired = true, AllowedValues = ImmutableList.Create("1", "2") })
.AddString(2, "lastName", Partitioning.Language,
new StringFieldProperties { Hints = "Last Name", Editor = StringFieldEditor.Input })
.AddBoolean(3, "isAdmin", Partitioning.Invariant,
new BooleanFieldProperties())
.AddNumber(4, "age", Partitioning.Invariant,
new NumberFieldProperties { MinValue = 1, MaxValue = 10 })
.AddDateTime(5, "birthday", Partitioning.Invariant,
new DateTimeFieldProperties())
.AddAssets(6, "pictures", Partitioning.Invariant,
new AssetsFieldProperties())
.AddReferences(7, "friends", Partitioning.Invariant,
new ReferencesFieldProperties())
.AddString(8, "dashed-field", Partitioning.Invariant,
new StringFieldProperties())
.Update(new SchemaProperties { Hints = "The User" });
var builder = new EdmModelBuilder(new MemoryCache(Options.Create(new MemoryCacheOptions())));
var schema = A.Dummy<ISchemaEntity>();
A.CallTo(() => schema.Id).Returns(Guid.NewGuid());
A.CallTo(() => schema.Version).Returns(3);
A.CallTo(() => schema.SchemaDef).Returns(schemaDef);
var app = A.Dummy<IAppEntity>();
A.CallTo(() => app.Id).Returns(Guid.NewGuid());
A.CallTo(() => app.Version).Returns(3);
A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig);
edmModel = builder.BuildEdmModel(schema, app);
}
[Fact]
public void Should_throw_exception_for_invalid_field()
{
Assert.Throws<NotSupportedException>(() => F(FilterBuilder.Eq("data/invalid/iv", "Me")));
}
[Fact]
public void Should_make_query_with_lastModified()
{
var i = F(FilterBuilder.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value));
var o = C("{ 'mt' : '1988-01-19T12:00:00Z' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_lastModifiedBy()
{
var i = F(FilterBuilder.Eq("lastModifiedBy", "Me"));
var o = C("{ 'mb' : 'Me' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_created()
{
var i = F(FilterBuilder.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value));
var o = C("{ 'ct' : '1988-01-19T12:00:00Z' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_createdBy()
{
var i = F(FilterBuilder.Eq("createdBy", "Me"));
var o = C("{ 'cb' : 'Me' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_version()
{
var i = F(FilterBuilder.Eq("version", 0L));
var o = C("{ 'vs' : NumberLong(0) }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_from_draft()
{
var i = F(FilterBuilder.Eq("data/dashed_field/iv", "Value"), true);
var o = C("{ 'dd.8.iv' : 'Value' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_date_field_created()
{
var i = F(FilterBuilder.Eq("data/birthday/iv", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value));
var o = C("{ 'do.5.iv' : '1988-01-19T12:00:00Z' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_underscore_field()
{
var i = F(FilterBuilder.Eq("data/dashed_field/iv", "Value"));
var o = C("{ 'do.8.iv' : 'Value' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_references_equals()
{
var i = F(FilterBuilder.Eq("data/friends/iv", "guid"));
var o = C("{ 'do.7.iv' : 'guid' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_assets_equals()
{
var i = F(FilterBuilder.Eq("data/pictures/iv", "guid"));
var o = C("{ 'do.6.iv' : 'guid' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_full_text()
{
var i = Q(new Query { FullText = "Hello my World" });
var o = C("{ '$text' : { '$search' : 'Hello my World' } }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_orderby_with_single_field()
{
var i = S(SortBuilder.Descending("data/age/iv"));
var o = C("{ 'do.4.iv' : -1 }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_orderby_with_multiple_fields()
{
var i = S(SortBuilder.Ascending("data/age/iv"), SortBuilder.Descending("data/firstName/en"));
var o = C("{ 'do.4.iv' : 1, 'do.1.en' : -1 }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_take_statement()
{
var query = new Query { Take = 3 };
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
cursor.ContentTake(query.AdjustToModel(schemaDef, false));
A.CallTo(() => cursor.Limit(3)).MustHaveHappened();
}
[Fact]
public void Should_make_skip_statement()
{
var query = new Query { Skip = 3 };
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
cursor.ContentSkip(query.AdjustToModel(schemaDef, false));
A.CallTo(() => cursor.Skip(3)).MustHaveHappened();
}
private static string C(string value)
{
return value.Replace('\'', '"');
}
private string F(FilterNode filter, bool useDraft = false)
{
return Q(new Query { Filter = filter }, useDraft);
}
private string S(params SortNode[] sorts)
{
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
var i = string.Empty;
A.CallTo(() => cursor.Sort(A<SortDefinition<MongoContentEntity>>.Ignored))
.Invokes((SortDefinition<MongoContentEntity> sortDefinition) =>
{
i = sortDefinition.Render(Serializer, Registry).ToString();
});
cursor.ContentSort(new Query { Sort = sorts.ToList() }.AdjustToModel(schemaDef, false));
return i;
}
private string Q(Query query, bool useDraft = false)
{
var rendered =
query.AdjustToModel(schemaDef, useDraft).BuildFilter<MongoContentEntity>().Filter
.Render(Serializer, Registry).ToString();
return rendered;
}
}
}

448
tests/Squidex.Domain.Apps.Entities.Tests/Contents/OData/ODataQueryTests.cs

@ -1,448 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Immutable;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.OData.Edm;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.Edm;
using Squidex.Domain.Apps.Entities.MongoDb.Contents;
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.MongoDb.OData;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents.OData
{
public class ODataQueryTests
{
private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry;
private static readonly IBsonSerializer<MongoContentEntity> Serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>();
private readonly Schema schemaDef;
private readonly IEdmModel edmModel;
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE);
static ODataQueryTests()
{
InstantSerializer.Register();
}
public ODataQueryTests()
{
schemaDef =
new Schema("user")
.AddString(1, "firstName", Partitioning.Language,
new StringFieldProperties { Label = "FirstName", IsRequired = true, AllowedValues = ImmutableList.Create("1", "2") })
.AddString(2, "lastName", Partitioning.Language,
new StringFieldProperties { Hints = "Last Name", Editor = StringFieldEditor.Input })
.AddBoolean(3, "isAdmin", Partitioning.Invariant,
new BooleanFieldProperties())
.AddNumber(4, "age", Partitioning.Invariant,
new NumberFieldProperties { MinValue = 1, MaxValue = 10 })
.AddDateTime(5, "birthday", Partitioning.Invariant,
new DateTimeFieldProperties())
.AddAssets(6, "pictures", Partitioning.Invariant,
new AssetsFieldProperties())
.AddReferences(7, "friends", Partitioning.Invariant,
new ReferencesFieldProperties())
.AddString(8, "dashed-field", Partitioning.Invariant,
new StringFieldProperties())
.Update(new SchemaProperties { Hints = "The User" });
var builder = new EdmModelBuilder(new MemoryCache(Options.Create(new MemoryCacheOptions())));
var schema = A.Dummy<ISchemaEntity>();
A.CallTo(() => schema.Id).Returns(Guid.NewGuid());
A.CallTo(() => schema.Version).Returns(3);
A.CallTo(() => schema.SchemaDef).Returns(schemaDef);
var app = A.Dummy<IAppEntity>();
A.CallTo(() => app.Id).Returns(Guid.NewGuid());
A.CallTo(() => app.Version).Returns(3);
A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig);
edmModel = builder.BuildEdmModel(schema, app);
}
[Fact]
public void Should_parse_query()
{
var parser = edmModel.ParseQuery("$filter=data/firstName/de eq 'Sebastian'");
Assert.NotNull(parser);
}
[Fact]
public void Should_make_query_with_created()
{
var i = F("$filter=created eq 1988-01-19T12:00:00Z");
var o = C("{ 'ct' : ISODate('1988-01-19T12:00:00Z') }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_created_and_date()
{
var i = F("$filter=created eq 1988-01-19");
var o = C("{ 'ct' : ISODate('1988-01-19T00:00:00Z') }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_createdBy()
{
var i = F("$filter=createdBy eq 'Me'");
var o = C("{ 'cb' : 'Me' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_lastModified()
{
var i = F("$filter=lastModified eq 1988-01-19T12:00:00Z");
var o = C("{ 'mt' : ISODate('1988-01-19T12:00:00Z') }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_lastModifiedBy()
{
var i = F("$filter=lastModifiedBy eq 'Me'");
var o = C("{ 'mb' : 'Me' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_version()
{
var i = F("$filter=version eq 0");
var o = C("{ 'vs' : 0 }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_underscore_field()
{
var i = F("$filter=data/dashed_field/iv eq 'Value'");
var o = C("{ 'do.8.iv' : 'Value' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_not()
{
var i = F("$filter=not endswith(data/firstName/de, 'Sebastian')");
var o = C("{ 'do.1.de' : { '$not' : /Sebastian$/i } }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_startswith()
{
var i = F("$filter=startswith(data/firstName/de, 'Sebastian')");
var o = C("{ 'do.1.de' : /^Sebastian/i }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_endswith()
{
var i = F("$filter=endswith(data/firstName/de, 'Sebastian')");
var o = C("{ 'do.1.de' : /Sebastian$/i }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_cointains()
{
var i = F("$filter=contains(data/firstName/de, 'Sebastian')");
var o = C("{ 'do.1.de' : /Sebastian/i }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_equals()
{
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq true");
var o = C("{ 'do.1.de' : /Sebastian/i }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_wih_equals_to_false()
{
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq false");
var o = C("{ 'do.1.de' : { '$not' : /Sebastian/i } }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_conjunction_and_contains()
{
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq false and data/isAdmin/iv eq true");
var o = C("{ 'do.1.de' : { '$not' : /Sebastian/i }, 'do.3.iv' : true }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_string_equals()
{
var i = F("$filter=data/firstName/de eq 'Sebastian'");
var o = C("{ 'do.1.de' : 'Sebastian' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_datetime_equals()
{
var i = F("$filter=data/birthday/iv eq 1988-01-19T12:00:00Z");
var o = C("{ 'do.5.iv' : '1988-01-19T12:00:00Z' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_boolean_equals()
{
var i = F("$filter=data/isAdmin/iv eq true");
var o = C("{ 'do.3.iv' : true }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_notequals()
{
var i = F("$filter=data/firstName/de ne 'Sebastian'");
var o = C("{ '$or' : [{ 'do.1.de' : { '$exists' : false } }, { 'do.1.de' : { '$ne' : 'Sebastian' } }] }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_lessthan()
{
var i = F("$filter=data/age/iv lt 1");
var o = C("{ 'do.4.iv' : { '$lt' : 1.0 } }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_lessequals()
{
var i = F("$filter=data/age/iv le 1");
var o = C("{ 'do.4.iv' : { '$lte' : 1.0 } }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_greaterthan()
{
var i = F("$filter=data/age/iv gt 1");
var o = C("{ 'do.4.iv' : { '$gt' : 1.0 } }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_greaterequals()
{
var i = F("$filter=data/age/iv ge 1");
var o = C("{ 'do.4.iv' : { '$gte' : 1.0 } }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_references_equals()
{
var i = F("$filter=data/pictures/iv eq 'guid'");
var o = C("{ 'do.6.iv' : 'guid' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_assets_equals()
{
var i = F("$filter=data/friends/iv eq 'guid'");
var o = C("{ 'do.7.iv' : 'guid' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_conjunction()
{
var i = F("$filter=data/age/iv eq 1 and data/age/iv eq 2");
var o = C("{ '$and' : [{ 'do.4.iv' : 1.0 }, { 'do.4.iv' : 2.0 }] }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_disjunction()
{
var i = F("$filter=data/age/iv eq 1 or data/age/iv eq 2");
var o = C("{ '$or' : [{ 'do.4.iv' : 1.0 }, { 'do.4.iv' : 2.0 }] }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_full_text()
{
var i = F("$search=Hello my World");
var o = C("{ '$text' : { '$search' : 'Hello my World' } }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_full_text_and_multiple_terms()
{
var i = F("$search=A and B");
var o = C("{ '$text' : { '$search' : 'A and B' } }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_orderby_with_single_field()
{
var i = S("$orderby=data/age/iv desc");
var o = C("{ 'do.4.iv' : -1 }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_orderby_with_multiple_field()
{
var i = S("$orderby=data/age/iv, data/firstName/en desc");
var o = C("{ 'do.4.iv' : 1, 'do.1.en' : -1 }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_top_statement()
{
var parser = edmModel.ParseQuery("$top=3");
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
cursor.ContentTake(parser);
A.CallTo(() => cursor.Limit(3)).MustHaveHappened();
}
[Fact]
public void Should_make_top_statement_with_limit()
{
var parser = edmModel.ParseQuery("$top=300");
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
cursor.ContentTake(parser);
A.CallTo(() => cursor.Limit(200)).MustHaveHappened();
}
[Fact]
public void Should_make_top_statement_with_default_value()
{
var parser = edmModel.ParseQuery(string.Empty);
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
cursor.ContentTake(parser);
A.CallTo(() => cursor.Limit(20)).MustHaveHappened();
}
[Fact]
public void Should_make_skip_statement()
{
var parser = edmModel.ParseQuery("$skip=3");
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
cursor.ContentSkip(parser);
A.CallTo(() => cursor.Skip(3)).MustHaveHappened();
}
[Fact]
public void Should_make_skip_statement_with_default_value()
{
var parser = edmModel.ParseQuery(string.Empty);
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
cursor.ContentSkip(parser);
A.CallTo(() => cursor.Skip(A<int>.Ignored)).MustNotHaveHappened();
}
private static string C(string value)
{
return value.Replace('\'', '"');
}
private string S(string value)
{
var parser = edmModel.ParseQuery(value);
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
var i = string.Empty;
A.CallTo(() => cursor.Sort(A<SortDefinition<MongoContentEntity>>.Ignored))
.Invokes((SortDefinition<MongoContentEntity> sortDefinition) =>
{
i = sortDefinition.Render(Serializer, Registry).ToString();
});
cursor.ContentSort(parser, FindExtensions.CreatePropertyCalculator(schemaDef, false));
return i;
}
private string F(string value)
{
var parser = edmModel.ParseQuery(value);
var query =
parser.BuildFilter<MongoContentEntity>(FindExtensions.CreatePropertyCalculator(schemaDef, false), FindExtensions.ValueConverter)
.Filter.Render(Serializer, Registry).ToString();
return query;
}
}
}
Loading…
Cancel
Save