From 6c5acd3c18ea2ffbada53483c6258e07c587ab19 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 13 Sep 2018 12:40:01 +0200 Subject: [PATCH] Tests refactored. --- .../Queries/FilterComparison.cs | 72 --- .../Queries/FilterJunction.cs | 46 -- .../Queries/FilterJunctionType.cs | 15 - .../Queries/FilterNegate.cs | 33 -- .../Queries/FilterNode.cs | 14 - .../Queries/FilterNodeVisitor.cs | 31 -- .../Queries/FilterOperator.cs | 22 - .../Queries/FilterValueType.cs | 21 - .../Queries/OData/ConstantVisitor.cs | 113 ----- .../Queries/OData/FilterBuilder.cs | 49 -- .../Queries/OData/FilterVisitor.cs | 153 ------ .../Queries/OData/LimitExtensions.cs | 34 -- .../Queries/OData/PropertyPathVisitor.cs | 56 --- .../Queries/OData/SearchTermVisitor.cs | 41 -- .../Queries/OData/SortBuilder.cs | 43 -- .../Queries/Query.cs | 24 - .../Queries/SortNode.cs | 30 -- .../Queries/SortOrder.cs | 15 - .../Assets/MongoAssetRepository.cs | 30 +- .../Assets/Visitors/FindExtensions.cs | 55 +-- .../Contents/MongoContentCollection.cs | 22 +- .../Contents/MongoContentRepository.cs | 8 +- .../Contents/Visitors/FindExtensions.cs | 79 +-- .../Assets/AssetQueryService.cs | 41 +- .../Assets/IAssetQueryService.cs | 2 +- .../Assets/Repositories/IAssetRepository.cs | 3 +- .../Contents/ContentQueryService.cs | 28 +- .../Contents/IContentQueryService.cs | 2 +- .../Contents/QueryExecutionContext.cs | 8 +- .../Repositories/IContentRepository.cs | 4 +- .../EdmModelExtensions.cs | 38 -- .../{Query.cs => Q.cs} | 18 +- .../MongoDb/OData/ConstantVisitor.cs | 83 ---- .../MongoDb/OData/FilterBuilder.cs | 33 +- .../MongoDb/OData/FilterVisitor.cs | 174 ++----- .../MongoDb/OData/LimitExtensions.cs | 27 +- .../MongoDb/OData/PropertyBuilder.cs | 32 -- .../MongoDb/OData/PropertyNameVisitor.cs | 50 -- .../MongoDb/OData/SearchTermVisitor.cs | 41 -- .../MongoDb/OData/SortBuilder.cs | 20 +- .../Squidex.Infrastructure.MongoDb.csproj | 1 - .../Queries/FilterBuilder.cs | 32 ++ .../Queries/OData/EdmModelExtensions.cs | 11 +- .../Queries/OData/LimitExtensions.cs | 4 +- .../Queries/PascalCasePathConverter.cs} | 17 +- src/Squidex.Infrastructure/Queries/Query.cs | 10 +- .../Queries/SortBuilder.cs} | 20 +- .../Controllers/Assets/AssetsController.cs | 2 +- .../Contents/ContentsController.cs | 2 +- src/Squidex/Config/Domain/EntitiesServices.cs | 1 + .../Assets/AssetQueryServiceTests.cs | 8 +- .../MongoDbQueryTests.cs} | 162 +++---- .../Contents/ContentQueryServiceTests.cs | 10 +- .../Contents/GraphQL/GraphQLQueriesTests.cs | 12 +- .../Contents/MongoDb/MongoDbQueryTests.cs | 267 +++++++++++ .../Contents/OData/ODataQueryTests.cs | 448 ------------------ 56 files changed, 633 insertions(+), 1984 deletions(-) delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/FilterComparison.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/FilterJunction.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/FilterJunctionType.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNegate.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNode.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNodeVisitor.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/FilterOperator.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/FilterValueType.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/OData/ConstantVisitor.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/OData/FilterBuilder.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/OData/FilterVisitor.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/OData/LimitExtensions.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/OData/PropertyPathVisitor.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/OData/SearchTermVisitor.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/OData/SortBuilder.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/Query.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/SortNode.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/SortOrder.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/EdmModelExtensions.cs rename src/Squidex.Domain.Apps.Entities/{Query.cs => Q.cs} (72%) delete mode 100644 src/Squidex.Infrastructure.MongoDb/MongoDb/OData/ConstantVisitor.cs delete mode 100644 src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyBuilder.cs delete mode 100644 src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyNameVisitor.cs delete mode 100644 src/Squidex.Infrastructure.MongoDb/MongoDb/OData/SearchTermVisitor.cs create mode 100644 src/Squidex.Infrastructure/Queries/FilterBuilder.cs rename src/{Squidex.Domain.Apps.Core.Operations/Queries/TransformVisitor.cs => Squidex.Infrastructure/Queries/PascalCasePathConverter.cs} (52%) rename src/{Squidex.Infrastructure.MongoDb/MongoDb/OData/ValueConversion.cs => Squidex.Infrastructure/Queries/SortBuilder.cs} (50%) rename tests/Squidex.Domain.Apps.Entities.Tests/Assets/{OData/ODataQueryTests.cs => MongoDb/MongoDbQueryTests.cs} (51%) create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/OData/ODataQueryTests.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterComparison.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterComparison.cs deleted file mode 100644 index af89d3207..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterComparison.cs +++ /dev/null @@ -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 Path { get; } - - public FilterOperator Operator { get; } - - public FilterValueType ValueType { get; } - - public object Value { get; } - - public FilterComparison(IReadOnlyList 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(FilterNodeVisitor 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; - } - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterJunction.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterJunction.cs deleted file mode 100644 index fb6eded3d..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterJunction.cs +++ /dev/null @@ -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 Operands { get; } - - public FilterJunctionType JunctionType { get; } - - public FilterJunction(FilterJunctionType junctionType, IReadOnlyList 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(FilterNodeVisitor visitor) - { - return visitor.Visit(this); - } - - public override string ToString() - { - return $"({string.Join(JunctionType == FilterJunctionType.And ? " && " : " || ", Operands)})"; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterJunctionType.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterJunctionType.cs deleted file mode 100644 index 5714e7e16..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterJunctionType.cs +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNegate.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNegate.cs deleted file mode 100644 index 6a447b3a7..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNegate.cs +++ /dev/null @@ -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(FilterNodeVisitor visitor) - { - return visitor.Visit(this); - } - - public override string ToString() - { - return $"!{Operand}"; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNode.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNode.cs deleted file mode 100644 index f1ec50049..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNode.cs +++ /dev/null @@ -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(FilterNodeVisitor visitor); - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNodeVisitor.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNodeVisitor.cs deleted file mode 100644 index 39b239b8c..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNodeVisitor.cs +++ /dev/null @@ -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 - { - 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(); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterOperator.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterOperator.cs deleted file mode 100644 index 26c9959dc..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterOperator.cs +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterValueType.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterValueType.cs deleted file mode 100644 index 2d9ad9544..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterValueType.cs +++ /dev/null @@ -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, - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/ConstantVisitor.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/ConstantVisitor.cs deleted file mode 100644 index 726c19f73..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/ConstantVisitor.cs +++ /dev/null @@ -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(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/FilterBuilder.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/FilterBuilder.cs deleted file mode 100644 index 3215aec1d..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/FilterBuilder.cs +++ /dev/null @@ -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); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/FilterVisitor.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/FilterVisitor.cs deleted file mode 100644 index 5ff81a64e..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/FilterVisitor.cs +++ /dev/null @@ -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 - { - 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); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/LimitExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/LimitExtensions.cs deleted file mode 100644 index daff66762..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/LimitExtensions.cs +++ /dev/null @@ -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; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/PropertyPathVisitor.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/PropertyPathVisitor.cs deleted file mode 100644 index 44d56f209..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/PropertyPathVisitor.cs +++ /dev/null @@ -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> - { - private static readonly PropertyPathVisitor Instance = new PropertyPathVisitor(); - - private PropertyPathVisitor() - { - } - - public static ImmutableList Visit(QueryNode node) - { - return node.Accept(Instance); - } - - public override ImmutableList Visit(ConvertNode nodeIn) - { - return nodeIn.Source.Accept(this); - } - - public override ImmutableList 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 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()); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/SearchTermVisitor.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/SearchTermVisitor.cs deleted file mode 100644 index 9658ba7ff..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/SearchTermVisitor.cs +++ /dev/null @@ -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 - { - 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; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/SortBuilder.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/SortBuilder.cs deleted file mode 100644 index 5c4a05dd3..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/SortBuilder.cs +++ /dev/null @@ -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); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Queries/Query.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/Query.cs deleted file mode 100644 index 27779d6e9..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Queries/Query.cs +++ /dev/null @@ -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 Sort { get; } = new List(); - - public string FullText { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Queries/SortNode.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/SortNode.cs deleted file mode 100644 index c4744df06..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Queries/SortNode.cs +++ /dev/null @@ -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 Path { get; } - - public SortOrder SortOrder { get; set; } - - public SortNode(IReadOnlyList path, SortOrder sortOrder) - { - Guard.NotNull(path, nameof(path)); - Guard.NotEmpty(path, nameof(path)); - Guard.Enum(sortOrder, nameof(sortOrder)); - - Path = path; - - SortOrder = sortOrder; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/Queries/SortOrder.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/SortOrder.cs deleted file mode 100644 index f2d0d79bf..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Queries/SortOrder.cs +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index e85e01610..838249509 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/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, 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> QueryAsync(Guid appId, string query = null) + public async Task> QueryAsync(Guid appId, Query query) { using (Profiler.TraceMethod("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(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")) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs index 89efa905b..3d6a94a91 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs +++ b/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 Filter = Builders.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 AssetSort(this IFindFluent cursor, ODataUriParser query) + public static IFindFluent AssetSort(this IFindFluent cursor, Query query) { - var sort = query.BuildSort(PropertyCalculator); - - return sort != null ? cursor.Sort(sort) : cursor.SortByDescending(x => x.LastModified); + return cursor.Sort(query.BuildSort()); } - public static IFindFluent AssetTake(this IFindFluent cursor, ODataUriParser query) + public static IFindFluent AssetTake(this IFindFluent cursor, Query query) { - return cursor.Take(query, 200, 20); + return cursor.Take(query); } - public static IFindFluent AssetSkip(this IFindFluent cursor, ODataUriParser query) + public static IFindFluent AssetSkip(this IFindFluent cursor, Query query) { return cursor.Skip(query); } - public static FilterDefinition BuildQuery(ODataUriParser query, Guid appId, ITagService tagService) + public static FilterDefinition BuildFilter(this Query query, Guid appId) { - var convertValue = CreateValueConverter(appId, tagService); - var filters = new List> { Filter.Eq(x => x.IndexedAppId, appId), Filter.Eq(x => x.IsDeleted, false) }; - var filter = query.BuildFilter(PropertyCalculator, convertValue, false); + var filter = query.BuildFilter(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; - }); - } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs index eccc17915..f9a7b2d25 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs +++ b/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> QueryAsync(IAppEntity app, ISchemaEntity schema, ODataUriParser odataQuery, Status[] status = null, bool useDraft = false) + public async Task> 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(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")) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 0e13c1345..796026d71 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/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> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery) + public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Query query) { using (Profiler.TraceMethod("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); } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs index 018136626..8d84c4744 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs +++ b/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()?.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> pathConverter; + + public AdaptionVisitor(Func, IReadOnlyList> 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>(propertyNames => { - if (propertyNames.Length > 1) + var result = new List(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 ContentSort(this IFindFluent cursor, ODataUriParser query, ConvertProperty propertyCalculator) + public static IFindFluent ContentSort(this IFindFluent cursor, Query query) { - var sort = query.BuildSort(propertyCalculator); - - return sort != null ? cursor.Sort(sort) : cursor.SortByDescending(x => x.LastModified); + return cursor.Sort(query.BuildSort()); } - public static IFindFluent ContentTake(this IFindFluent cursor, ODataUriParser query) + public static IFindFluent ContentTake(this IFindFluent cursor, Query query) { - return cursor.Take(query, 200, 20); + return cursor.Take(query); } - public static IFindFluent ContentSkip(this IFindFluent cursor, ODataUriParser query) + public static IFindFluent ContentSkip(this IFindFluent cursor, Query query) { return cursor.Skip(query); } - public static FilterDefinition BuildQuery(ODataUriParser query, Guid schemaId, Status[] status, ConvertProperty propertyCalculator) + public static FilterDefinition BuildQuery(Query query, Guid schemaId, Status[] status) { var filters = new List> { @@ -111,7 +132,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors filters.Add(Filter.In(x => x.Status, status)); } - var filter = query.BuildFilter(propertyCalculator, ValueConverter); + var filter = query.BuildFilter(); if (filter.Filter != null) { diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs index acc7d2d80..4416af31a 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs +++ b/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> QueryAsync(QueryContext context, Query query) + public async Task> 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 Sort(IResultList assets, IList ids) + private IResultList Sort(IResultList assets, IReadOnlyList 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 { "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 assets) { var tags = new HashSet(assets.Where(x => x.Tags != null).SelectMany(x => x.Tags).Distinct()); diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs b/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs index e0068df52..fa17c0731 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs @@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { public interface IAssetQueryService { - Task> QueryAsync(QueryContext contex, Query query); + Task> QueryAsync(QueryContext contex, Q query); Task FindAssetAsync(QueryContext context, Guid id); } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs index 0410a5b4f..e3e86324d 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs +++ b/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> QueryAsync(Guid appId, string query = null); + Task> QueryAsync(Guid appId, Query query); Task> QueryAsync(Guid appId, HashSet ids); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs index 53e15ff75..d250ba29a 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs +++ b/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> QueryAsync(ContentQueryContext context, Query query) + public async Task> 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 Sort(IResultList contents, IList ids) + private IResultList Sort(IResultList contents, IReadOnlyList 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()) { @@ -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 { "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) { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs index 55ec74748..c2a8c53b1 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs @@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { public interface IContentQueryService { - Task> QueryAsync(ContentQueryContext context, Query query); + Task> QueryAsync(ContentQueryContext context, Q query); Task FindContentAsync(ContentQueryContext context, Guid id, long version = EtagVersion.Any); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs index fecb6eb5a..43f7d52e4 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs @@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Contents public async Task> 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> 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) { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs index 819161319..9f65e239b 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ b/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> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids); - Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery); + Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Query query); Task> QueryNotFoundAsync(Guid appId, Guid schemaId, IList ids); diff --git a/src/Squidex.Domain.Apps.Entities/EdmModelExtensions.cs b/src/Squidex.Domain.Apps.Entities/EdmModelExtensions.cs deleted file mode 100644 index afb38c598..000000000 --- a/src/Squidex.Domain.Apps.Entities/EdmModelExtensions.cs +++ /dev/null @@ -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; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Query.cs b/src/Squidex.Domain.Apps.Entities/Q.cs similarity index 72% rename from src/Squidex.Domain.Apps.Entities/Query.cs rename to src/Squidex.Domain.Apps.Entities/Q.cs index a6afff384..8bd9b0f39 100644 --- a/src/Squidex.Domain.Apps.Entities/Query.cs +++ b/src/Squidex.Domain.Apps.Entities/Q.cs @@ -12,39 +12,41 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities { - public sealed class Query : Cloneable + public sealed class Q : Cloneable { - public static readonly Query Empty = new Query(); + public static readonly Q Empty = new Q(); - public List Ids { get; private set; } + public IReadOnlyList 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 ids) + public Q WithIds(IEnumerable 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(); + var idsList = new List(); foreach (var id in ids.Split(',')) { if (Guid.TryParse(id, out var guid)) { - c.Ids.Add(guid); + idsList.Add(guid); } } + + c.Ids = idsList; }); } diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/ConstantVisitor.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/ConstantVisitor.cs deleted file mode 100644 index d7784a059..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/ConstantVisitor.cs +++ /dev/null @@ -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 - { - 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; - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/FilterBuilder.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/FilterBuilder.cs index c0562708e..4b9079327 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/FilterBuilder.cs +++ b/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 Filter, bool Last) BuildFilter(this ODataUriParser query, ConvertProperty convertProperty = null, ConvertValue convertValue = null, bool supportsSearch = true) + public static (FilterDefinition Filter, bool Last) BuildFilter(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.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.Filter.Text(query.FullText), false); } - if (filter != null) + if (query.Filter != null) { - return (FilterVisitor.Visit(filter.Expression, convertProperty, convertValue), true); + return (FilterVisitor.Visit(query.Filter), true); } return (null, false); diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/FilterVisitor.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/FilterVisitor.cs index 826068b0e..bc26029dd 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/FilterVisitor.cs +++ b/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 : QueryNodeVisitor> + public sealed class FilterVisitor : FilterNodeVisitor> { private static readonly FilterDefinitionBuilder Filter = Builders.Filter; - private readonly ConvertProperty convertProperty; - private readonly ConvertValue convertValue; + private static readonly FilterVisitor Instance = new FilterVisitor(); - private FilterVisitor(ConvertProperty convertProperty, ConvertValue convertValue) + private FilterVisitor() { - this.convertProperty = convertProperty; - this.convertValue = convertValue; } - public static FilterDefinition Visit(QueryNode node, ConvertProperty propertyCalculator, ConvertValue convertValue) + public static FilterDefinition Visit(FilterNode node) { - var visitor = new FilterVisitor(propertyCalculator, convertValue); - - return node.Accept(visitor); - } - - public override FilterDefinition Visit(ConvertNode nodeIn) - { - return nodeIn.Source.Accept(this); + return node.Accept(Instance); } - public override FilterDefinition Visit(UnaryOperatorNode nodeIn) + public override FilterDefinition 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 Visit(SingleValueFunctionCallNode nodeIn) + public override FilterDefinition 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 Visit(BinaryOperatorNode nodeIn) + public override FilterDefinition 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 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 formatter) { - return ConstantVisitor.Visit(nodeIn); + return new BsonRegularExpression(formatter(node.Value.ToString()), "i"); } } } diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/LimitExtensions.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/LimitExtensions.cs index 8296a4476..9d07bc985 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/LimitExtensions.cs +++ b/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 Take(this IFindFluent cursor, ODataUriParser query, int maxValue = 200, int defaultValue = 20) + public static IFindFluent Take(this IFindFluent 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 Skip(this IFindFluent cursor, ODataUriParser query) + public static IFindFluent Skip(this IFindFluent 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; diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyBuilder.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyBuilder.cs deleted file mode 100644 index 17a2ff535..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyBuilder.cs +++ /dev/null @@ -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; - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyNameVisitor.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyNameVisitor.cs deleted file mode 100644 index 116fc4e98..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyNameVisitor.cs +++ /dev/null @@ -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> - { - public static readonly PropertyNameVisitor Instance = new PropertyNameVisitor(); - - private PropertyNameVisitor() - { - } - - public override ImmutableList Visit(ConvertNode nodeIn) - { - return nodeIn.Source.Accept(this); - } - - public override ImmutableList 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 Visit(SingleValuePropertyAccessNode nodeIn) - { - if (nodeIn.Source is SingleComplexNode) - { - return nodeIn.Source.Accept(this).Add(nodeIn.Property.Name); - } - else - { - return ImmutableList.Create(nodeIn.Property.Name); - } - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/SearchTermVisitor.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/SearchTermVisitor.cs deleted file mode 100644 index 85f897c80..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/SearchTermVisitor.cs +++ /dev/null @@ -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 - { - 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; - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/SortBuilder.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/SortBuilder.cs index 9a43597e1..8214eb795 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/SortBuilder.cs +++ b/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 BuildSort(this ODataUriParser query, ConvertProperty propertyCalculator = null) + public static SortDefinition BuildSort(this Query query) { - var orderBy = query.ParseOrderBy(); - - if (orderBy != null) + if (query.Sort.Count > 0) { var sorts = new List>(); - while (orderBy != null) + foreach (var sort in query.Sort) { - sorts.Add(OrderBy(orderBy, propertyCalculator)); - - orderBy = orderBy.ThenBy; + sorts.Add(OrderBy(sort)); } if (sorts.Count > 1) @@ -41,11 +37,11 @@ namespace Squidex.Infrastructure.MongoDb.OData return null; } - public static SortDefinition OrderBy(OrderByClause clause, ConvertProperty propertyCalculator = null) + public static SortDefinition OrderBy(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.Sort.Ascending(propertyName); } diff --git a/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj b/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj index e4c954df9..ad8a38706 100644 --- a/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj +++ b/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj @@ -11,7 +11,6 @@ - diff --git a/src/Squidex.Infrastructure/Queries/FilterBuilder.cs b/src/Squidex.Infrastructure/Queries/FilterBuilder.cs new file mode 100644 index 000000000..60f8c8859 --- /dev/null +++ b/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); + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs b/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs index 8e2f62e91..53954b64f 100644 --- a/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs +++ b/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; } diff --git a/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs b/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs index ca80389c7..68532b5b6 100644 --- a/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs +++ b/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; } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/Queries/TransformVisitor.cs b/src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs similarity index 52% rename from src/Squidex.Domain.Apps.Core.Operations/Queries/TransformVisitor.cs rename to src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs index cd35f8990..fa717fa1f 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Queries/TransformVisitor.cs +++ b/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 + 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); } } } diff --git a/src/Squidex.Infrastructure/Queries/Query.cs b/src/Squidex.Infrastructure/Queries/Query.cs index 4a8bb00d6..e71fbfdcb 100644 --- a/src/Squidex.Infrastructure/Queries/Query.cs +++ b/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 Sort { get; } = new List(); + public List Sort { get; set; } = new List(); 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}"); } diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/ValueConversion.cs b/src/Squidex.Infrastructure/Queries/SortBuilder.cs similarity index 50% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/OData/ValueConversion.cs rename to src/Squidex.Infrastructure/Queries/SortBuilder.cs index 8492c7098..02c1aca16 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/ValueConversion.cs +++ b/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); } } } diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 43538bf75..112339e44 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/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); diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 294de505e..2ee101ebe 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/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 { diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index da031af53..784b4d7bf 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/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; diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs index 03af8482c..26c4edfc6 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs +++ b/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) { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/OData/ODataQueryTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs similarity index 51% rename from tests/Squidex.Domain.Apps.Entities.Tests/Assets/OData/ODataQueryTests.cs rename to tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs index 34df435b3..b7be66f29 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/OData/ODataQueryTests.cs +++ b/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(); - private readonly IBsonSerializerRegistry registry = BsonSerializer.SerializerRegistry; - private readonly IBsonSerializer serializer = BsonSerializer.SerializerRegistry.GetSerializer(); - private readonly IEdmModel edmModel = EdmAssetModel.Edm; + private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry; + private static readonly IBsonSerializer Serializer = BsonSerializer.SerializerRegistry.GetSerializer(); 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>.That.Contains("tag1"))) - .Returns(new Dictionary { ["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(() => 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>(); - 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>(); - - 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>(); - - 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>(); - 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>(); - - cursor.AssetSkip(parser); - - A.CallTo(() => cursor.Skip(A.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>(); var i = string.Empty; @@ -266,23 +216,21 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData A.CallTo(() => cursor.Sort(A>.Ignored)) .Invokes((SortDefinition 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(convertValue: valueConverter) - .Filter.Render(serializer, registry).ToString(); + var rendered = + query.AdjustToModel().BuildFilter(false).Filter + .Render(Serializer, Registry).ToString(); - return query; + return rendered; } } } \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs index b68d3a632..a7d5cb010 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs +++ b/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.That.IsSameSequenceAs(status), A.Ignored)) + A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.IsSameSequenceAs(status), A.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(() => sut.QueryAsync(context.WithSchemaId(schemaId), Query.Empty.WithODataQuery("query"))); + return Assert.ThrowsAsync(() => sut.QueryAsync(context.WithSchemaId(schemaId), Q.Empty.WithODataQuery("query"))); } public static IEnumerable 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); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index a85850c88..1276393df 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/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.That.Matches(x => x.ODataQuery == "?$take=30&$skip=5&$search=my-query"))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.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.That.Matches(x => x.ODataQuery == "?$take=30&$skip=5&$search=my-query"))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.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.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5"))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.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.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5"))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.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.Ignored)) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.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.Ignored)) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.Ignored)) .Returns(ResultList.Create(0, assetRef)); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs new file mode 100644 index 000000000..3d12ba1a7 --- /dev/null +++ b/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 Serializer = BsonSerializer.SerializerRegistry.GetSerializer(); + 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(); + A.CallTo(() => schema.Id).Returns(Guid.NewGuid()); + A.CallTo(() => schema.Version).Returns(3); + A.CallTo(() => schema.SchemaDef).Returns(schemaDef); + + var app = A.Dummy(); + 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(() => 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>(); + + 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>(); + + 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>(); + + var i = string.Empty; + + A.CallTo(() => cursor.Sort(A>.Ignored)) + .Invokes((SortDefinition 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().Filter + .Render(Serializer, Registry).ToString(); + + return rendered; + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/OData/ODataQueryTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/OData/ODataQueryTests.cs deleted file mode 100644 index 96aa25515..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/OData/ODataQueryTests.cs +++ /dev/null @@ -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 Serializer = BsonSerializer.SerializerRegistry.GetSerializer(); - 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(); - A.CallTo(() => schema.Id).Returns(Guid.NewGuid()); - A.CallTo(() => schema.Version).Returns(3); - A.CallTo(() => schema.SchemaDef).Returns(schemaDef); - - var app = A.Dummy(); - 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>(); - - 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>(); - - 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>(); - - 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>(); - - 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>(); - - cursor.ContentSkip(parser); - - A.CallTo(() => cursor.Skip(A.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>(); - - var i = string.Empty; - - A.CallTo(() => cursor.Sort(A>.Ignored)) - .Invokes((SortDefinition 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(FindExtensions.CreatePropertyCalculator(schemaDef, false), FindExtensions.ValueConverter) - .Filter.Render(Serializer, Registry).ToString(); - - return query; - } - } -} \ No newline at end of file