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.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Tags;
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.MongoDb.Assets.Visitors;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
public sealed partial class MongoAssetRepository : MongoRepositoryBase<MongoAssetEntity>, IAssetRepository
{
private readonly ITagService tagService;
public MongoAssetRepository(IMongoDatabase database, ITagService tagService)
public MongoAssetRepository(IMongoDatabase database)
: base(database)
{
Guard.NotNull(tagService, nameof(tagService));
this.tagService = tagService;
}
protected override string CollectionName()
@ -50,36 +44,28 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
.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"))
{
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 contentItems =
Collection.Find(filter)
.AssetTake(odataQuery)
.AssetSkip(odataQuery)
.AssetSort(odataQuery)
.AssetTake(query)
.AssetSkip(query)
.AssetSort(query)
.ToListAsync();
await Task.WhenAll(contentItems, contentCount);
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)
{
if (ex.Message.Contains("17406"))

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

@ -8,59 +8,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.OData.UriParser;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb.OData;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
{
public static class FindExtensions
{
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 sort != null ? cursor.Sort(sort) : cursor.SortByDescending(x => x.LastModified);
return cursor.Sort(query.BuildSort<MongoAssetEntity>());
}
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);
}
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>>
{
Filter.Eq(x => x.IndexedAppId, appId),
Filter.Eq(x => x.IsDeleted, false)
};
var filter = query.BuildFilter<MongoAssetEntity>(PropertyCalculator, convertValue, false);
var filter = query.BuildFilter<MongoAssetEntity>(false);
if (filter.Filter != null)
{
@ -87,20 +87,5 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
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.Linq;
using System.Threading.Tasks;
using Microsoft.OData.UriParser;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
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.Infrastructure;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
@ -42,20 +42,20 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
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
{
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 contentItems =
Collection.Find(filter)
.ContentTake(odataQuery)
.ContentSkip(odataQuery)
.ContentSort(odataQuery, propertyCalculator)
.ContentTake(query)
.ContentSkip(query)
.ContentSort(query)
.Not(x => x.DataText)
.ToListAsync();
@ -68,14 +68,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
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)
{
if (ex.Message.Contains("17406"))

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

@ -8,7 +8,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.OData.UriParser;
using MongoDB.Driver;
using NodaTime;
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.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
@ -46,17 +46,17 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
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"))
{
if (RequiresPublished(status))
{
return await contentsPublished.QueryAsync(app, schema, odataQuery);
return await contentsPublished.QueryAsync(app, schema, query);
}
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.Linq;
using System.Reflection;
using Microsoft.OData.UriParser;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using NodaTime;
@ -17,6 +16,7 @@ using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.GenerateEdmSchema;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.MongoDb.OData;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
{
@ -28,77 +28,98 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
typeof(MongoContentEntity).GetProperties()
.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 &&
!string.Equals(field, "mt", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(field, "ct", StringComparison.OrdinalIgnoreCase))
private readonly Func<IReadOnlyList<string>, IReadOnlyList<string>> pathConverter;
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))
{
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)
{
propertyNames[0] = "dd";
result[0] = "dd";
}
else
{
propertyNames[0] = "do";
result[0] = "do";
}
}
else
{
propertyNames[0] = PropertyMap[propertyNames[0]];
result[0] = PropertyMap[propertyNames[0]];
}
}
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 sort != null ? cursor.Sort(sort) : cursor.SortByDescending(x => x.LastModified);
return cursor.Sort(query.BuildSort<MongoContentEntity>());
}
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);
}
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>>
{
@ -111,7 +132,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
filters.Add(Filter.In(x => x.Status, status));
}
var filter = query.BuildFilter<MongoContentEntity>(propertyCalculator, ValueConverter);
var filter = query.BuildFilter<MongoContentEntity>();
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.Linq;
using System.Threading.Tasks;
using Microsoft.OData;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Edm;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Queries.OData;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class AssetQueryService : IAssetQueryService
{
private const int MaxResults = 20;
private readonly ITagService tagService;
private readonly IAssetRepository assetRepository;
@ -44,7 +49,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
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(query, nameof(query));
@ -58,7 +63,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
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);
@ -66,13 +73,41 @@ namespace Squidex.Domain.Apps.Entities.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);
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)
{
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
{
Task<IResultList<IAssetEntity>> QueryAsync(QueryContext contex, Query query);
Task<IResultList<IAssetEntity>> QueryAsync(QueryContext contex, Q query);
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.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{
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);

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

@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.OData;
using Microsoft.OData.UriParser;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
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.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Queries.OData;
using Squidex.Infrastructure.Reflection;
#pragma warning disable RECS0147
@ -27,6 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
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[] StatusArchived = { Status.Archived };
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));
@ -128,7 +130,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
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);
@ -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>())
{
@ -205,7 +207,23 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
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)
{

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

@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
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);

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)
{
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)
{
@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
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)
{
@ -100,7 +100,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
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)
{
@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
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)
{

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

@ -8,12 +8,12 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.OData.UriParser;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
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, 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);

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
{
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 Query WithODataQuery(string odataQuery)
public Q WithODataQuery(string 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());
}
public Query WithIds(string ids)
public Q WithIds(string ids)
{
if (!string.IsNullOrEmpty(ids))
{
return Clone(c =>
{
c.Ids = new List<Guid>();
var idsList = new List<Guid>();
foreach (var id in ids.Split(','))
{
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.
// ==========================================================================
using Microsoft.OData;
using Microsoft.OData.UriParser;
using MongoDB.Driver;
using Squidex.Infrastructure.Queries;
namespace Squidex.Infrastructure.MongoDb.OData
{
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;
try
{
search = query.ParseSearch();
}
catch (ODataException ex)
{
throw new ValidationException("Query $search clause not valid.", new ValidationError(ex.Message));
}
if (search != null)
if (query.FullText != null)
{
if (!supportsSearch)
{
throw new ValidationException("Query $search clause not supported.");
}
return (Builders<T>.Filter.Text(SearchTermVisitor.Visit(search.Expression).ToString()), false);
}
FilterClause filter;
try
{
filter = query.ParseFilter();
}
catch (ODataException ex)
{
throw new ValidationException("Query $filter clause not valid.", new ValidationError(ex.Message));
return (Builders<T>.Filter.Text(query.FullText), false);
}
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);

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

@ -7,179 +7,77 @@
using System;
using System.Linq;
using Microsoft.OData.UriParser;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Infrastructure.Queries;
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 readonly ConvertProperty convertProperty;
private readonly ConvertValue convertValue;
private static readonly FilterVisitor<T> Instance = new FilterVisitor<T>();
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(visitor);
}
public override FilterDefinition<T> Visit(ConvertNode nodeIn)
{
return nodeIn.Source.Accept(this);
return node.Accept(Instance);
}
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));
}
throw new NotSupportedException();
return Filter.Not(nodeIn.Operand.Accept(this));
}
public override FilterDefinition<T> Visit(SingleValueFunctionCallNode nodeIn)
public override FilterDefinition<T> Visit(FilterJunction nodeIn)
{
var fieldNode = nodeIn.Parameters.ElementAt(0);
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))
if (nodeIn.JunctionType == FilterJunctionType.And)
{
var f = BuildFieldDefinition(fieldNode);
var v = BuildRegex(f, valueNode, s => "^" + s);
return Filter.Regex(f, v);
return Filter.And(nodeIn.Operands.Select(x => x.Accept(this)));
}
if (string.Equals(nodeIn.Name, "contains", StringComparison.OrdinalIgnoreCase))
else
{
var f = BuildFieldDefinition(fieldNode);
var v = BuildRegex(f, valueNode, s => s);
return Filter.Regex(f, v);
return Filter.Or(nodeIn.Operands.Select(x => x.Accept(this)));
}
throw new NotSupportedException();
}
public override FilterDefinition<T> Visit(BinaryOperatorNode nodeIn)
public override FilterDefinition<T> Visit(FilterComparison nodeIn)
{
if (nodeIn.OperatorKind == BinaryOperatorKind.And)
{
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 propertyName = string.Join(".", nodeIn.Path);
var value = BuildValue(nodeIn.Right);
if (value is bool booleanRight)
{
if ((nodeIn.OperatorKind == BinaryOperatorKind.Equal && !booleanRight) ||
(nodeIn.OperatorKind == BinaryOperatorKind.NotEqual && booleanRight))
{
regexFilter = Filter.Not(regexFilter);
}
return regexFilter;
}
}
else
switch (nodeIn.Operator)
{
if (nodeIn.OperatorKind == BinaryOperatorKind.NotEqual)
{
var f = BuildFieldDefinition(nodeIn.Left);
var v = BuildValue(f, nodeIn.Right);
return Filter.Or(Filter.Not(Filter.Exists(f)), Filter.Ne(f, v));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.Equal)
{
var f = BuildFieldDefinition(nodeIn.Left);
var v = BuildValue(f, nodeIn.Right);
return Filter.Eq(f, v);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThan)
{
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);
}
case FilterOperator.StartsWith:
return Filter.Regex(propertyName, BuildRegex(nodeIn, s => "$" + s));
case FilterOperator.Contains:
return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s));
case FilterOperator.EndsWith:
return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s + "$"));
case FilterOperator.Equals:
return Filter.Eq(propertyName, nodeIn.Value);
case FilterOperator.GreaterThan:
return Filter.Gt(propertyName, nodeIn.Value);
case FilterOperator.GreaterThanOrEqual:
return Filter.Gte(propertyName, nodeIn.Value);
case FilterOperator.LessThan:
return Filter.Lt(propertyName, nodeIn.Value);
case FilterOperator.LessThanOrEqual:
return Filter.Lte(propertyName, nodeIn.Value);
case FilterOperator.NotEquals:
return Filter.Ne(propertyName, nodeIn.Value);
}
throw new NotSupportedException();
}
private BsonRegularExpression BuildRegex(string field, QueryNode 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)
private BsonRegularExpression BuildRegex(FilterComparison node, Func<string, string> formatter)
{
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.
// ==========================================================================
using System;
using Microsoft.OData.UriParser;
using MongoDB.Driver;
using Squidex.Infrastructure.Queries;
namespace Squidex.Infrastructure.MongoDb.OData
{
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 (top.HasValue)
{
cursor = cursor.Limit(Math.Min((int)top.Value, maxValue));
}
else
if (query.Take < long.MaxValue)
{
cursor = cursor.Limit(defaultValue);
cursor = cursor.Limit((int)query.Take);
}
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 (skip.HasValue)
{
cursor = cursor.Skip((int)skip.Value);
}
else
if (query.Skip > 0)
{
cursor = cursor.Skip(null);
cursor = cursor.Skip((int)query.Skip);
}
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 Microsoft.OData.UriParser;
using MongoDB.Driver;
using Squidex.Infrastructure.Queries;
namespace Squidex.Infrastructure.MongoDb.OData
{
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 (orderBy != null)
if (query.Sort.Count > 0)
{
var sorts = new List<SortDefinition<T>>();
while (orderBy != null)
foreach (var sort in query.Sort)
{
sorts.Add(OrderBy<T>(orderBy, propertyCalculator));
orderBy = orderBy.ThenBy;
sorts.Add(OrderBy<T>(sort));
}
if (sorts.Count > 1)
@ -41,11 +37,11 @@ namespace Squidex.Infrastructure.MongoDb.OData
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);
}

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

@ -11,7 +11,6 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.OData.Core" Version="7.5.0" />
<PackageReference Include="MongoDB.Driver" Version="2.7.0" />
<PackageReference Include="MongoDB.Driver.GridFS" Version="2.7.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();
parser.ParseTake(query);
parser.ParseSkip(query);
parser.ParseFilter(query);
parser.ParseSort(query);
if (parser != null)
{
parser.ParseTake(query);
parser.ParseSkip(query);
parser.ParseFilter(query);
parser.ParseSort(query);
}
return query;
}

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

@ -17,7 +17,7 @@ namespace Squidex.Infrastructure.Queries.OData
if (top.HasValue)
{
result.Take = top;
result.Take = top.Value;
}
}
@ -27,7 +27,7 @@ namespace Squidex.Infrastructure.Queries.OData
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;
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 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()
{
@ -35,12 +35,12 @@ namespace Squidex.Infrastructure.Queries
parts.Add($"FullText: {FullText}");
}
if (Skip != null)
if (Skip > 0)
{
parts.Add($"Skip: {Skip}");
}
if (Take != null)
if (Take < long.MaxValue)
{
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.
// ==========================================================================
namespace Squidex.Infrastructure.MongoDb.OData
{
public delegate object ConvertValue(string field, object value);
using System.Linq;
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 value;
}
return new SortNode(path.Split('.', '/').ToList(), SortOrder.Ascending);
}
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 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);

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

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

@ -15,6 +15,7 @@ using Orleans;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps;
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(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(2, result.Count);
@ -87,6 +87,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.Empty(result[1].Tags);
}
/*
* TODO
[Fact]
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(), "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(2, result.Count);
Assert.Equal(HashSet.Of("name1", "name2"), result[0].Tags);
Assert.Equal(HashSet.Of("name2", "name3"), result[1].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.Collections.Generic;
using System.Linq;
using FakeItEasy;
using Microsoft.OData.Edm;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Edm;
using NodaTime.Text;
using Squidex.Domain.Apps.Entities.MongoDb.Assets;
using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors;
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.Assets.OData
namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
{
public class ODataQueryTests
public class MongoDbQueryTests
{
private readonly ITagService tagService = A.Fake<ITagService>();
private readonly IBsonSerializerRegistry registry = BsonSerializer.SerializerRegistry;
private readonly IBsonSerializer<MongoAssetEntity> serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoAssetEntity>();
private readonly IEdmModel edmModel = EdmAssetModel.Edm;
private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry;
private static readonly IBsonSerializer<MongoAssetEntity> Serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoAssetEntity>();
private readonly Guid appId = Guid.NewGuid();
private readonly ConvertValue valueConverter;
static ODataQueryTests()
static MongoDbQueryTests()
{
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]
public void Should_parse_query()
public void Should_throw_exception_for_full_text_search()
{
var parser = edmModel.ParseQuery("$filter=lastModifiedBy eq 'Sebastian'");
Assert.NotNull(parser);
Assert.Throws<ValidationException>(() => Q(new Query { FullText = "Full Text" }));
}
[Fact]
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') }");
Assert.Equal(o, i);
@ -64,7 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact]
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' }");
Assert.Equal(o, i);
@ -73,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact]
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') }");
Assert.Equal(o, i);
@ -82,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact]
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' }");
Assert.Equal(o, i);
@ -91,17 +79,17 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact]
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) }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_normalized_tags()
public void Should_make_query_with_fileVersion()
{
var i = F("$filter=tags eq 'tag1'");
var o = C("{ 'Tags' : 'normalized1' }");
var i = F(FilterBuilder.Eq("fileVersion", 2));
var o = C("{ 'FileVersion' : NumberLong(2) }");
Assert.Equal(o, i);
}
@ -109,8 +97,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact]
public void Should_make_query_with_tags()
{
var i = F("$filter=tags eq 'tag2'");
var o = C("{ 'Tags' : 'tag2' }");
var i = F(FilterBuilder.Eq("tags", "tag1"));
var o = C("{ 'Tags' : 'tag1' }");
Assert.Equal(o, i);
}
@ -118,44 +106,35 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact]
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' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_fileSize()
{
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()
public void Should_make_query_with_isImage()
{
var i = F("$filter=fileVersion eq 2");
var o = C("{ 'FileVersion' : NumberLong(2) }");
var i = F(FilterBuilder.Eq("isImage", true));
var o = C("{ 'IsImage' : true }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_isImage()
public void Should_make_query_with_mimeType()
{
var i = F("$filter=isImage eq true");
var o = C("{ 'IsImage' : true }");
var i = F(FilterBuilder.Eq("mimeType", "text/json"));
var o = C("{ 'MimeType' : 'text/json' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_mimeType()
public void Should_make_query_with_fileSize()
{
var i = F("$filter=mimeType eq 'text/json'");
var o = C("{ 'MimeType' : 'text/json' }");
var i = F(FilterBuilder.Eq("fileSize", 1024));
var o = C("{ 'FileSize' : NumberLong(1024) }");
Assert.Equal(o, i);
}
@ -163,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact]
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 }");
Assert.Equal(o, i);
@ -172,8 +151,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact]
public void Should_make_query_with_pixelWidth()
{
var i = F("$filter=pixelWidth eq 600");
var o = C("{ 'PixelWidth' : 600 }");
var i = F(FilterBuilder.Eq("pixelWidth", 800));
var o = C("{ 'PixelWidth' : 800 }");
Assert.Equal(o, i);
}
@ -181,84 +160,55 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact]
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 }");
Assert.Equal(o, i);
}
[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 }");
Assert.Equal(o, i);
}
[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>>();
cursor.AssetTake(parser);
cursor.AssetTake(query.AdjustToModel());
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]
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>>();
cursor.AssetSkip(parser);
cursor.AssetSkip(query.AdjustToModel());
A.CallTo(() => cursor.Skip(3)).MustHaveHappened();
}
[Fact]
public void Should_make_skip_statement_with_default_value()
private static string C(string value)
{
var parser = edmModel.ParseQuery(string.Empty);
var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
cursor.AssetSkip(parser);
A.CallTo(() => cursor.Skip(A<int>.Ignored)).MustNotHaveHappened();
return value.Replace('\'', '"');
}
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 i = string.Empty;
@ -266,23 +216,21 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
A.CallTo(() => cursor.Sort(A<SortDefinition<MongoAssetEntity>>.Ignored))
.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;
}
private string F(string value)
private string Q(Query query)
{
var parser = edmModel.ParseQuery(value);
var query =
parser.BuildFilter<MongoAssetEntity>(convertValue: valueConverter)
.Filter.Render(serializer, registry).ToString();
var rendered =
query.AdjustToModel().BuildFilter<MongoAssetEntity>(false).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 FakeItEasy;
using Microsoft.OData;
using Microsoft.OData.UriParser;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
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.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Security;
using Xunit;
@ -194,12 +194,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.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)));
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(content.Id, result[0].Id);
@ -227,7 +227,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => modelBuilder.BuildEdmModel(schema, app))
.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[]
@ -261,7 +261,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
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(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());
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));
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());
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));
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);
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));
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);
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));
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))
.Returns(content);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Query>.Ignored))
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.Ignored))
.Returns(ResultList.Create(0, contentRef));
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))
.Returns(content);
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Query>.Ignored))
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Q>.Ignored))
.Returns(ResultList.Create(0, assetRef));
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