From c96d1390cf8ce026f34f9646906ac5af19a1d37a Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 12 Sep 2018 23:29:44 +0200 Subject: [PATCH] Custom query structure. --- .../Schemas/Json/JsonSchemaModel.cs | 2 +- .../Schemas/TagsFieldProperties.cs | 2 + .../Squidex.Domain.Apps.Core.Model.csproj | 2 +- .../ConvertContent/FieldConverters.cs | 4 +- .../Queries/FilterComparison.cs | 72 ++++ .../Queries/FilterJunction.cs | 46 +++ .../Queries/FilterJunctionType.cs} | 9 +- .../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 + .../Queries/TransformVisitor.cs | 29 ++ .../Tags/ITagService.cs | 6 +- .../Tags/Tag.cs | 2 +- .../Tags/TagGroups.cs | 21 ++ .../Tags/TagNormalizer.cs | 130 +++++++ .../Tags/TagSet.cs | 2 +- .../Assets/MongoAssetRepository.cs | 2 +- .../Assets/Visitors/FindExtensions.cs | 2 +- .../Assets/AssetGrain.cs | 14 +- .../Assets/AssetQueryService.cs | 2 +- .../Assets/BackupAssets.cs | 2 +- .../Backup/Helpers/ArchiveHelper.cs | 2 +- .../Tags/GrainTagService.cs | 5 +- .../Tags/ITagGrain.cs | 5 +- .../Tags/TagGrain.cs | 15 +- src/Squidex.Infrastructure/Guard.cs | 2 +- .../Queries/FilterComparison.cs | 71 ++++ .../Queries/FilterJunction.cs | 45 +++ .../Queries/FilterJunctionType.cs | 15 + .../Queries/FilterNegate.cs | 31 ++ .../Queries/FilterNode.cs | 14 + .../Queries/FilterNodeVisitor.cs | 31 ++ .../Queries/FilterOperator.cs | 22 ++ .../Queries/FilterValueType.cs | 21 ++ .../Queries/OData/ConstantVisitor.cs | 30 ++ .../Queries/OData/ConstantWithTypeVisitor.cs | 113 +++++++ .../Queries/OData/EdmModelExtensions.cs | 50 +++ .../Queries/OData/FilterBuilder.cs | 48 +++ .../Queries/OData/FilterVisitor.cs | 148 ++++++++ .../Queries/OData/LimitExtensions.cs | 34 ++ .../Queries/OData/PropertyPathVisitor.cs | 61 ++++ .../Queries/OData/SearchTermVisitor.cs | 41 +++ .../Queries/OData/SortBuilder.cs | 43 +++ src/Squidex.Infrastructure/Queries/Query.cs | 56 ++++ .../Queries/SortNode.cs | 36 ++ .../Queries/SortOrder.cs | 15 + .../Queries/TransformVisitor.cs | 29 ++ .../Squidex.Infrastructure.csproj | 1 + src/Squidex.Infrastructure/Tasks/AsyncLock.cs | 4 +- .../Controllers/Assets/AssetsController.cs | 2 +- src/Squidex/Squidex.csproj | 2 +- src/Squidex/package.json | 89 +++-- .../Assets/AssetCommandMiddlewareTests.cs | 3 +- .../Assets/AssetGrainTests.cs | 4 +- .../Assets/AssetQueryServiceTests.cs | 2 +- .../Assets/OData/ODataQueryTests.cs | 4 +- .../Tags/GrainTagServiceTests.cs | 1 + .../Tags/TagGrainTests.cs | 5 +- .../Caching/LRUCacheTests.cs | 6 +- .../Orleans/BootstrapTests.cs | 1 - .../Queries/ODataConversionTests.cs | 316 ++++++++++++++++++ 73 files changed, 2358 insertions(+), 93 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/FilterComparison.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/FilterJunction.cs rename src/{Squidex.Domain.Apps.Entities/Tags/TagGroups.cs => Squidex.Domain.Apps.Core.Operations/Queries/FilterJunctionType.cs} (75%) create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNegate.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNode.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNodeVisitor.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/FilterOperator.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/FilterValueType.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/OData/ConstantVisitor.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/OData/FilterBuilder.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/OData/FilterVisitor.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/OData/LimitExtensions.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/OData/PropertyPathVisitor.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/OData/SearchTermVisitor.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/OData/SortBuilder.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/Query.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/SortNode.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/SortOrder.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Queries/TransformVisitor.cs rename src/{Squidex.Domain.Apps.Entities => Squidex.Domain.Apps.Core.Operations}/Tags/ITagService.cs (76%) rename src/{Squidex.Domain.Apps.Entities => Squidex.Domain.Apps.Core.Operations}/Tags/Tag.cs (91%) create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Tags/TagGroups.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs rename src/{Squidex.Domain.Apps.Entities => Squidex.Domain.Apps.Core.Operations}/Tags/TagSet.cs (91%) create mode 100644 src/Squidex.Infrastructure/Queries/FilterComparison.cs create mode 100644 src/Squidex.Infrastructure/Queries/FilterJunction.cs create mode 100644 src/Squidex.Infrastructure/Queries/FilterJunctionType.cs create mode 100644 src/Squidex.Infrastructure/Queries/FilterNegate.cs create mode 100644 src/Squidex.Infrastructure/Queries/FilterNode.cs create mode 100644 src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs create mode 100644 src/Squidex.Infrastructure/Queries/FilterOperator.cs create mode 100644 src/Squidex.Infrastructure/Queries/FilterValueType.cs create mode 100644 src/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs create mode 100644 src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs create mode 100644 src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs create mode 100644 src/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs create mode 100644 src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs create mode 100644 src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs create mode 100644 src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs create mode 100644 src/Squidex.Infrastructure/Queries/OData/SearchTermVisitor.cs create mode 100644 src/Squidex.Infrastructure/Queries/OData/SortBuilder.cs create mode 100644 src/Squidex.Infrastructure/Queries/Query.cs create mode 100644 src/Squidex.Infrastructure/Queries/SortNode.cs create mode 100644 src/Squidex.Infrastructure/Queries/SortOrder.cs create mode 100644 src/Squidex.Infrastructure/Queries/TransformVisitor.cs create mode 100644 tests/Squidex.Infrastructure.Tests/Queries/ODataConversionTests.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs index d45620c9a..37af1b9e6 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs @@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json public Schema ToSchema(FieldRegistry registry) { - RootField[] fields = Empty; + var fields = Empty; if (Fields != null) { diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs index 31da100e6..c17f5bc77 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs @@ -16,6 +16,8 @@ namespace Squidex.Domain.Apps.Core.Schemas public int? MaxItems { get; set; } + public bool Normalize { get; set; } + public override T Accept(IFieldPropertiesVisitor visitor) { return visitor.Visit(this); diff --git a/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj index 948f4da5e..73e266772 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj +++ b/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj @@ -8,7 +8,7 @@ True - + all runtime; build; native; contentfiles; analyzers diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs index a4ccea294..a761dbd64 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs @@ -26,8 +26,10 @@ namespace Squidex.Domain.Apps.Core.ConvertContent { private static readonly Func KeyNameResolver = f => f.Name; private static readonly Func KeyIdResolver = f => f.Id.ToString(); + private static readonly Func FieldByIdResolver = (f, k) => long.TryParse(k, out var id) ? f.FieldsById.GetOrDefault(id) : null; + private static readonly Func FieldByNameResolver = (f, k) => f.FieldsByName.GetOrDefault(k); @@ -251,7 +253,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent var newArray = new JArray(); - foreach (JObject item in jArray.OfType()) + foreach (var item in jArray.OfType()) { var newItem = new JObject(); diff --git a/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterComparison.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterComparison.cs new file mode 100644 index 000000000..af89d3207 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterComparison.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// 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 new file mode 100644 index 000000000..fb6eded3d --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterJunction.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// 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.Entities/Tags/TagGroups.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterJunctionType.cs similarity index 75% rename from src/Squidex.Domain.Apps.Entities/Tags/TagGroups.cs rename to src/Squidex.Domain.Apps.Core.Operations/Queries/FilterJunctionType.cs index 99742c077..5714e7e16 100644 --- a/src/Squidex.Domain.Apps.Entities/Tags/TagGroups.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterJunctionType.cs @@ -5,10 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Tags +namespace Squidex.Domain.Apps.Core.Queries { - public static class TagGroups + public enum FilterJunctionType { - public const string Assets = "Assets"; + 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 new file mode 100644 index 000000000..6a447b3a7 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNegate.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// 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 new file mode 100644 index 000000000..f1ec50049 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNode.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// 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 new file mode 100644 index 000000000..39b239b8c --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNodeVisitor.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// 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 new file mode 100644 index 000000000..26c9959dc --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterOperator.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// 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 new file mode 100644 index 000000000..2d9ad9544 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Queries/FilterValueType.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// 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 new file mode 100644 index 000000000..726c19f73 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/ConstantVisitor.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// 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 new file mode 100644 index 000000000..3215aec1d --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/FilterBuilder.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// 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 new file mode 100644 index 000000000..5ff81a64e --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/FilterVisitor.cs @@ -0,0 +1,153 @@ +// ========================================================================== +// 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 new file mode 100644 index 000000000..daff66762 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/LimitExtensions.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// 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 new file mode 100644 index 000000000..44d56f209 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/PropertyPathVisitor.cs @@ -0,0 +1,56 @@ +// ========================================================================== +// 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 new file mode 100644 index 000000000..9658ba7ff --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/SearchTermVisitor.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// 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 new file mode 100644 index 000000000..5c4a05dd3 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Queries/OData/SortBuilder.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// 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 new file mode 100644 index 000000000..27779d6e9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Queries/Query.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// 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 new file mode 100644 index 000000000..c4744df06 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Queries/SortNode.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// 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 new file mode 100644 index 000000000..f2d0d79bf --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Queries/SortOrder.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// 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.Core.Operations/Queries/TransformVisitor.cs b/src/Squidex.Domain.Apps.Core.Operations/Queries/TransformVisitor.cs new file mode 100644 index 000000000..cd35f8990 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Queries/TransformVisitor.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; + +namespace Squidex.Domain.Apps.Core.Queries +{ + public abstract class TransformVisitor : FilterNodeVisitor + { + public override FilterNode Visit(FilterComparison nodeIn) + { + return nodeIn; + } + + public override FilterNode Visit(FilterJunction nodeIn) + { + return new FilterJunction(nodeIn.JunctionType, nodeIn.Operands.Select(x => x.Accept(this)).ToList()); + } + + public override FilterNode Visit(FilterNegate nodeIn) + { + return new FilterNegate(nodeIn.Operand.Accept(this)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Tags/ITagService.cs b/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs similarity index 76% rename from src/Squidex.Domain.Apps.Entities/Tags/ITagService.cs rename to src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs index 01fb3c9e8..f0fc88a3a 100644 --- a/src/Squidex.Domain.Apps.Entities/Tags/ITagService.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs @@ -9,13 +9,13 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -namespace Squidex.Domain.Apps.Entities.Tags +namespace Squidex.Domain.Apps.Core.Tags { public interface ITagService { - Task> NormalizeTagsAsync(Guid appId, string group, HashSet names, HashSet ids); + Task> GetTagIdsAsync(Guid appId, string group, HashSet names); - Task> GetTagIdsAsync(Guid appId, string group, HashSet names); + Task> NormalizeTagsAsync(Guid appId, string group, HashSet names, HashSet ids); Task> DenormalizeTagsAsync(Guid appId, string group, HashSet ids); diff --git a/src/Squidex.Domain.Apps.Entities/Tags/Tag.cs b/src/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs similarity index 91% rename from src/Squidex.Domain.Apps.Entities/Tags/Tag.cs rename to src/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs index 60fab8187..1bff58c70 100644 --- a/src/Squidex.Domain.Apps.Entities/Tags/Tag.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Tags +namespace Squidex.Domain.Apps.Core.Tags { public sealed class Tag { diff --git a/src/Squidex.Domain.Apps.Core.Operations/Tags/TagGroups.cs b/src/Squidex.Domain.Apps.Core.Operations/Tags/TagGroups.cs new file mode 100644 index 000000000..fd69b313b --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Tags/TagGroups.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Core.Tags +{ + public static class TagGroups + { + public const string Assets = "Assets"; + + public static string Schemas(Guid schemaId) + { + return $"Schemas_{schemaId}"; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs b/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs new file mode 100644 index 000000000..936101f77 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs @@ -0,0 +1,130 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Core.Tags +{ + public static class TagNormalizer + { + public static async Task NormalizeAsync(ITagService service, Guid appId, Guid schemaId, Schema schema, params NamedContentData[] datas) + { + var tagsValues = new HashSet(); + var tagsArrays = new List(); + + GetValues(schema, tagsValues, tagsArrays, datas); + + if (tagsValues.Count > 0) + { + var normalized = await service.NormalizeTagsAsync(appId, $"Schemas_{schemaId}", tagsValues, null); + + foreach (var array in tagsArrays) + { + for (var i = 0; i < array.Count; i++) + { + array[i] = normalized[array[i].ToString()]; + } + } + } + } + + public static async Task DeNormalizeAsync(ITagService service, Guid appId, Guid schemaId, Schema schema, params NamedContentData[] datas) + { + var tagsValues = new HashSet(); + var tagsArrays = new List(); + + GetValues(schema, tagsValues, tagsArrays, datas); + + if (tagsValues.Count > 0) + { + var denormalized = await service.DenormalizeTagsAsync(appId, $"Schemas_{schemaId}", tagsValues); + + foreach (var array in tagsArrays) + { + for (var i = 0; i < array.Count; i++) + { + array[i] = denormalized[array[i].ToString()]; + } + } + } + } + + private static void GetValues(Schema schema, HashSet values, List arrays, params NamedContentData[] datas) + { + foreach (var field in schema.Fields) + { + if (field.RawProperties is TagsFieldProperties tags && tags.Normalize) + { + foreach (var data in datas) + { + if (data.TryGetValue(field.Name, out var fieldData)) + { + foreach (var partition in fieldData) + { + ExtractTags(partition.Value, values, arrays); + } + } + } + } + else if (field is IArrayField arrayField) + { + foreach (var nestedField in arrayField.Fields) + { + if (field.RawProperties is TagsFieldProperties nestedTags && nestedTags.Normalize) + { + foreach (var data in datas) + { + if (data.TryGetValue(field.Name, out var fieldData)) + { + foreach (var partition in fieldData) + { + if (partition.Value is JArray jArray) + { + foreach (var value in jArray) + { + if (value.Type == JTokenType.Object) + { + var nestedObject = (JObject)value; + + if (nestedObject.TryGetValue(nestedField.Name, out var nestedArray)) + { + ExtractTags(partition.Value, values, arrays); + } + } + } + } + } + } + } + } + } + } + } + } + + private static void ExtractTags(JToken token, HashSet values, List arrays) + { + if (token is JArray jArray) + { + foreach (var value in jArray) + { + if (value.Type == JTokenType.String) + { + values.Add(value.ToString()); + } + } + + arrays.Add(jArray); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Tags/TagSet.cs b/src/Squidex.Domain.Apps.Core.Operations/Tags/TagSet.cs similarity index 91% rename from src/Squidex.Domain.Apps.Entities/Tags/TagSet.cs rename to src/Squidex.Domain.Apps.Core.Operations/Tags/TagSet.cs index 48815870e..530c28b00 100644 --- a/src/Squidex.Domain.Apps.Entities/Tags/TagSet.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/Tags/TagSet.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; -namespace Squidex.Domain.Apps.Entities.Tags +namespace Squidex.Domain.Apps.Core.Tags { public sealed class TagSet : Dictionary { diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index b3d54fa2c..e85e01610 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -10,11 +10,11 @@ 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.Domain.Apps.Entities.Tags; using Squidex.Infrastructure; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.MongoDb; 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 3ba817edf..89efa905b 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs @@ -12,7 +12,7 @@ using System.Threading.Tasks; using Microsoft.OData.UriParser; using MongoDB.Bson; using MongoDB.Driver; -using Squidex.Domain.Apps.Entities.Tags; +using Squidex.Domain.Apps.Core.Tags; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb.OData; diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs index c238593a9..8173494e3 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs @@ -6,11 +6,12 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Guards; using Squidex.Domain.Apps.Entities.Assets.State; -using Squidex.Domain.Apps.Entities.Tags; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; @@ -46,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { GuardAsset.CanCreate(c); - c.Tags = await tagService.NormalizeTagsAsync(c.AppId.Id, TagGroups.Assets, c.Tags, Snapshot.Tags); + c.Tags = await NormalizeTagsAsync(c.AppId.Id, c.Tags); Create(c); @@ -66,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { GuardAsset.CanTag(c); - c.Tags = await tagService.NormalizeTagsAsync(Snapshot.AppId.Id, TagGroups.Assets, c.Tags, Snapshot.Tags); + c.Tags = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags); Tag(c); }); @@ -91,6 +92,13 @@ namespace Squidex.Domain.Apps.Entities.Assets } } + private async Task> NormalizeTagsAsync(Guid appId, HashSet tags) + { + var normalized = await tagService.NormalizeTagsAsync(appId, TagGroups.Assets, tags, Snapshot.Tags); + + return new HashSet(normalized.Values); + } + public void Create(CreateAsset command) { var @event = SimpleMapper.Map(command, new AssetCreated diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs index 739b70cff..acc7d2d80 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs @@ -9,8 +9,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Domain.Apps.Entities.Tags; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Assets diff --git a/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs b/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs index a63d894b6..d5267c423 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs @@ -9,10 +9,10 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Backup; -using Squidex.Domain.Apps.Entities.Tags; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; diff --git a/src/Squidex.Domain.Apps.Entities/Backup/Helpers/ArchiveHelper.cs b/src/Squidex.Domain.Apps.Entities/Backup/Helpers/ArchiveHelper.cs index ed0c5778c..69de1724f 100644 --- a/src/Squidex.Domain.Apps.Entities/Backup/Helpers/ArchiveHelper.cs +++ b/src/Squidex.Domain.Apps.Entities/Backup/Helpers/ArchiveHelper.cs @@ -36,7 +36,7 @@ namespace Squidex.Domain.Apps.Entities.Backup.Archive { var hash = 17; - foreach (char c in value) + foreach (var c in value) { unchecked { diff --git a/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs b/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs index d88dd023a..ad8c37457 100644 --- a/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs +++ b/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Orleans; +using Squidex.Domain.Apps.Core.Tags; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Tags @@ -29,12 +30,12 @@ namespace Squidex.Domain.Apps.Entities.Tags this.grainFactory = grainFactory; } - public Task> NormalizeTagsAsync(Guid appId, string group, HashSet names, HashSet ids) + public Task> NormalizeTagsAsync(Guid appId, string group, HashSet names, HashSet ids) { return GetGrain(appId, group).NormalizeTagsAsync(names, ids); } - public Task> GetTagIdsAsync(Guid appId, string group, HashSet names) + public Task> GetTagIdsAsync(Guid appId, string group, HashSet names) { return GetGrain(appId, group).GetTagIdsAsync(names); } diff --git a/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs b/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs index 952702b10..d43b6f022 100644 --- a/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs @@ -8,14 +8,15 @@ using System.Collections.Generic; using System.Threading.Tasks; using Orleans; +using Squidex.Domain.Apps.Core.Tags; namespace Squidex.Domain.Apps.Entities.Tags { public interface ITagGrain : IGrainWithStringKey { - Task> NormalizeTagsAsync(HashSet names, HashSet ids); + Task> NormalizeTagsAsync(HashSet names, HashSet ids); - Task> GetTagIdsAsync(HashSet names); + Task> GetTagIdsAsync(HashSet names); Task> DenormalizeTagsAsync(HashSet ids); diff --git a/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs b/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs index 1e9b76654..d01b3c03c 100644 --- a/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Tags; using Squidex.Infrastructure; using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.States; @@ -58,9 +59,9 @@ namespace Squidex.Domain.Apps.Entities.Tags return persistence.WriteSnapshotAsync(state); } - public async Task> NormalizeTagsAsync(HashSet names, HashSet ids) + public async Task> NormalizeTagsAsync(HashSet names, HashSet ids) { - var result = new HashSet(); + var result = new Dictionary(); if (names != null) { @@ -89,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Tags state.Tags.Add(tagId, new Tag { Name = tagName }); } - result.Add(tagId); + result.Add(tagName, tagId); } } } @@ -98,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.Tags { foreach (var id in ids) { - if (!result.Contains(id)) + if (!result.ContainsValue(id)) { if (state.Tags.TryGetValue(id, out var tagInfo)) { @@ -118,9 +119,9 @@ namespace Squidex.Domain.Apps.Entities.Tags return result; } - public Task> GetTagIdsAsync(HashSet names) + public Task> GetTagIdsAsync(HashSet names) { - var result = new HashSet(); + var result = new Dictionary(); foreach (var name in names) { @@ -128,7 +129,7 @@ namespace Squidex.Domain.Apps.Entities.Tags if (!string.IsNullOrWhiteSpace(id)) { - result.Add(id); + result.Add(name, id); } } diff --git a/src/Squidex.Infrastructure/Guard.cs b/src/Squidex.Infrastructure/Guard.cs index df6fcaedb..1c1644f97 100644 --- a/src/Squidex.Infrastructure/Guard.cs +++ b/src/Squidex.Infrastructure/Guard.cs @@ -142,7 +142,7 @@ namespace Squidex.Infrastructure [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void NotEmpty(ICollection enumerable, string parameterName) + public static void NotEmpty(IReadOnlyCollection enumerable, string parameterName) { NotNull(enumerable, parameterName); diff --git a/src/Squidex.Infrastructure/Queries/FilterComparison.cs b/src/Squidex.Infrastructure/Queries/FilterComparison.cs new file mode 100644 index 000000000..a100745cb --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/FilterComparison.cs @@ -0,0 +1,71 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Infrastructure.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.Infrastructure/Queries/FilterJunction.cs b/src/Squidex.Infrastructure/Queries/FilterJunction.cs new file mode 100644 index 000000000..08c2bf2a9 --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/FilterJunction.cs @@ -0,0 +1,45 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; + +namespace Squidex.Infrastructure.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.Infrastructure/Queries/FilterJunctionType.cs b/src/Squidex.Infrastructure/Queries/FilterJunctionType.cs new file mode 100644 index 000000000..c9c8b8289 --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/FilterJunctionType.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Queries +{ + public enum FilterJunctionType + { + And, + Or + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Queries/FilterNegate.cs b/src/Squidex.Infrastructure/Queries/FilterNegate.cs new file mode 100644 index 000000000..650170444 --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/FilterNegate.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.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.Infrastructure/Queries/FilterNode.cs b/src/Squidex.Infrastructure/Queries/FilterNode.cs new file mode 100644 index 000000000..61f348538 --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/FilterNode.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Queries +{ + public abstract class FilterNode + { + public abstract T Accept(FilterNodeVisitor visitor); + } +} diff --git a/src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs b/src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs new file mode 100644 index 000000000..30ef01741 --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// 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.Infrastructure.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.Infrastructure/Queries/FilterOperator.cs b/src/Squidex.Infrastructure/Queries/FilterOperator.cs new file mode 100644 index 000000000..df16d94bb --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/FilterOperator.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Queries +{ + public enum FilterOperator + { + Contains, + EndsWith, + Equals, + GreaterThan, + GreaterThanOrEqual, + LessThan, + LessThanOrEqual, + NotEquals, + StartsWith + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Queries/FilterValueType.cs b/src/Squidex.Infrastructure/Queries/FilterValueType.cs new file mode 100644 index 000000000..95c13afa4 --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/FilterValueType.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Queries +{ + public enum FilterValueType + { + Boolean, + Guid, + Double, + Instant, + Int32, + Int64, + Single, + String, + } +} diff --git a/src/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs b/src/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs new file mode 100644 index 000000000..e430d1655 --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.OData.UriParser; + +namespace Squidex.Infrastructure.Queries.OData +{ + public sealed class ConstantVisitor : QueryNodeVisitor + { + private static readonly ConstantVisitor Instance = new ConstantVisitor(); + + private ConstantVisitor() + { + } + + public static object Visit(QueryNode node) + { + return node.Accept(Instance); + } + + public override object Visit(ConstantNode nodeIn) + { + return nodeIn.Value; + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs b/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs new file mode 100644 index 000000000..54b6f6264 --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// 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.Queries.OData +{ + public sealed class ConstantWithTypeVisitor : 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 ConstantWithTypeVisitor Instance = new ConstantWithTypeVisitor(); + + private ConstantWithTypeVisitor() + { + } + + 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(ConstantVisitor.Visit(nodeIn.Source).ToString()), FilterValueType.Boolean); + } + + if (nodeIn.TypeReference.Definition == GuidType) + { + return (Guid.Parse(ConstantVisitor.Visit(nodeIn.Source).ToString()), FilterValueType.Guid); + } + + if (nodeIn.TypeReference.Definition == DateTimeType) + { + var value = ConstantVisitor.Visit(nodeIn.Source); + + if (value is DateTimeOffset dateTimeOffset) + { + return (Instant.FromDateTimeOffset(dateTimeOffset), FilterValueType.Instant); + } + + if (value is DateTime dateTime) + { + return (Instant.FromDateTimeUtc(DateTime.SpecifyKind(dateTime, DateTimeKind.Utc)), FilterValueType.Instant); + } + + if (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.Definition == BooleanType) + { + return (nodeIn.Value, FilterValueType.Boolean); + } + + if (nodeIn.TypeReference.Definition == DoubleType) + { + return (nodeIn.Value, FilterValueType.Double); + } + + if (nodeIn.TypeReference.Definition == Int32Type) + { + return (nodeIn.Value, FilterValueType.Int32); + } + + if (nodeIn.TypeReference.Definition == Int64Type) + { + return (nodeIn.Value, FilterValueType.Int64); + } + + if (nodeIn.TypeReference.Definition == StringType) + { + return (nodeIn.Value, FilterValueType.String); + } + + throw new NotSupportedException(); + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs b/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs new file mode 100644 index 000000000..8e2f62e91 --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs @@ -0,0 +1,50 @@ +// ========================================================================== +// 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.Infrastructure.Queries.OData +{ + 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; + } + + public static Query ToQuery(this ODataUriParser parser) + { + var query = new Query(); + + parser.ParseTake(query); + parser.ParseSkip(query); + parser.ParseFilter(query); + parser.ParseSort(query); + + return query; + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs b/src/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs new file mode 100644 index 000000000..6e378bb80 --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.OData; +using Microsoft.OData.UriParser; + +namespace Squidex.Infrastructure.Queries.OData +{ + 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.Infrastructure/Queries/OData/FilterVisitor.cs b/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs new file mode 100644 index 000000000..7b4cbae29 --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs @@ -0,0 +1,148 @@ +// ========================================================================== +// 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.Infrastructure.Queries.OData +{ + 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) = ConstantWithTypeVisitor.Visit(valueNode); + + return new FilterComparison(PropertyPathVisitor.Visit(fieldNode), FilterOperator.EndsWith, value, valueType); + } + + if (string.Equals(nodeIn.Name, "startswith", StringComparison.OrdinalIgnoreCase)) + { + var (value, valueType) = ConstantWithTypeVisitor.Visit(valueNode); + + return new FilterComparison(PropertyPathVisitor.Visit(fieldNode), FilterOperator.StartsWith, value, valueType); + } + + if (string.Equals(nodeIn.Name, "contains", StringComparison.OrdinalIgnoreCase)) + { + var (value, valueType) = ConstantWithTypeVisitor.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, valueType) = ConstantWithTypeVisitor.Visit(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) = ConstantWithTypeVisitor.Visit(nodeIn.Right); + + return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Left), FilterOperator.NotEquals, value, valueType); + } + + if (nodeIn.OperatorKind == BinaryOperatorKind.Equal) + { + var (value, valueType) = ConstantWithTypeVisitor.Visit(nodeIn.Right); + + return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Left), FilterOperator.Equals, value, valueType); + } + + if (nodeIn.OperatorKind == BinaryOperatorKind.LessThan) + { + var (value, valueType) = ConstantWithTypeVisitor.Visit(nodeIn.Right); + + return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Left), FilterOperator.LessThan, value, valueType); + } + + if (nodeIn.OperatorKind == BinaryOperatorKind.LessThanOrEqual) + { + var (value, valueType) = ConstantWithTypeVisitor.Visit(nodeIn.Right); + + return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Left), FilterOperator.LessThanOrEqual, value, valueType); + } + + if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThan) + { + var (value, valueType) = ConstantWithTypeVisitor.Visit(nodeIn.Right); + + return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Left), FilterOperator.GreaterThan, value, valueType); + } + + if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThanOrEqual) + { + var (value, valueType) = ConstantWithTypeVisitor.Visit(nodeIn.Right); + + return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Left), FilterOperator.GreaterThanOrEqual, value, valueType); + } + } + + throw new NotSupportedException(); + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs b/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs new file mode 100644 index 000000000..ca80389c7 --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.OData.UriParser; + +namespace Squidex.Infrastructure.Queries.OData +{ + public static class LimitExtensions + { + public static void ParseTake(this ODataUriParser query, Query result) + { + 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.Infrastructure/Queries/OData/PropertyPathVisitor.cs b/src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs new file mode 100644 index 000000000..6dd5e5a3e --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Immutable; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; + +namespace Squidex.Infrastructure.Queries.OData +{ + 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(UnescapeEdmField(nodeIn.Property)); + } + else + { + return ImmutableList.Create(UnescapeEdmField(nodeIn.Property)); + } + } + + public override ImmutableList Visit(SingleValuePropertyAccessNode nodeIn) + { + if (nodeIn.Source is SingleComplexNode) + { + return nodeIn.Source.Accept(this).Add(UnescapeEdmField(nodeIn.Property)); + } + else + { + return ImmutableList.Create(UnescapeEdmField(nodeIn.Property)); + } + } + + private static string UnescapeEdmField(IEdmProperty property) + { + return property.Name; + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/OData/SearchTermVisitor.cs b/src/Squidex.Infrastructure/Queries/OData/SearchTermVisitor.cs new file mode 100644 index 000000000..846e973a9 --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/OData/SearchTermVisitor.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// 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.Queries.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/Queries/OData/SortBuilder.cs b/src/Squidex.Infrastructure/Queries/OData/SortBuilder.cs new file mode 100644 index 000000000..e9b5f748b --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/OData/SortBuilder.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.OData.UriParser; + +namespace Squidex.Infrastructure.Queries.OData +{ + 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.Infrastructure/Queries/Query.cs b/src/Squidex.Infrastructure/Queries/Query.cs new file mode 100644 index 000000000..4a8bb00d6 --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/Query.cs @@ -0,0 +1,56 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Infrastructure.Queries +{ + public sealed class Query + { + public FilterNode Filter { get; set; } + + public string FullText { get; set; } + + public long? Skip { get; set; } + + public long? Take { get; set; } + + public List Sort { get; } = new List(); + + public override string ToString() + { + var parts = new List(); + + if (Filter != null) + { + parts.Add($"Filter: {Filter}"); + } + + if (FullText != null) + { + parts.Add($"FullText: {FullText}"); + } + + if (Skip != null) + { + parts.Add($"Skip: {Skip}"); + } + + if (Take != null) + { + parts.Add($"Take: {Take}"); + } + + if (Sort.Count > 0) + { + parts.Add($"Sort: {string.Join(", ", Sort)}"); + } + + return string.Join("; ", parts); + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/SortNode.cs b/src/Squidex.Infrastructure/Queries/SortNode.cs new file mode 100644 index 000000000..030b0bafe --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/SortNode.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Infrastructure.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; + } + + public override string ToString() + { + var path = string.Join(".", Path); + + return $"{path} {SortOrder}"; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Queries/SortOrder.cs b/src/Squidex.Infrastructure/Queries/SortOrder.cs new file mode 100644 index 000000000..cfec5b783 --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/SortOrder.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Queries +{ + public enum SortOrder + { + Ascending, + Descending + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Queries/TransformVisitor.cs b/src/Squidex.Infrastructure/Queries/TransformVisitor.cs new file mode 100644 index 000000000..38f60a4ff --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/TransformVisitor.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; + +namespace Squidex.Infrastructure.Queries +{ + public abstract class TransformVisitor : FilterNodeVisitor + { + public override FilterNode Visit(FilterComparison nodeIn) + { + return nodeIn; + } + + public override FilterNode Visit(FilterJunction nodeIn) + { + return new FilterJunction(nodeIn.JunctionType, nodeIn.Operands.Select(x => x.Accept(this)).ToList()); + } + + public override FilterNode Visit(FilterNegate nodeIn) + { + return new FilterNegate(nodeIn.Operand.Accept(this)); + } + } +} diff --git a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index 7f520c216..7fd41f6ff 100644 --- a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -10,6 +10,7 @@ + all diff --git a/src/Squidex.Infrastructure/Tasks/AsyncLock.cs b/src/Squidex.Infrastructure/Tasks/AsyncLock.cs index 40a8baf09..60e11e804 100644 --- a/src/Squidex.Infrastructure/Tasks/AsyncLock.cs +++ b/src/Squidex.Infrastructure/Tasks/AsyncLock.cs @@ -24,7 +24,7 @@ namespace Squidex.Infrastructure.Tasks public Task LockAsync() { - Task wait = semaphore.WaitAsync(); + var wait = semaphore.WaitAsync(); if (wait.IsCompleted) { @@ -50,7 +50,7 @@ namespace Squidex.Infrastructure.Tasks public void Dispose() { - AsyncLock current = target; + var current = target; if (current == null) { diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index b7e58fe24..43538bf75 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -14,12 +14,12 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Assets.Models; +using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Domain.Apps.Entities.Tags; using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index 9ecb4d42c..b607d8893 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -80,7 +80,7 @@ - + diff --git a/src/Squidex/package.json b/src/Squidex/package.json index 3bc7cc1c6..559722461 100644 --- a/src/Squidex/package.json +++ b/src/Squidex/package.json @@ -16,90 +16,89 @@ "tslint": "tslint -c tslint.json -p tsconfig.json app/**/*.ts" }, "dependencies": { - "@angular/animations": "6.0.4", - "@angular/common": "6.0.4", - "@angular/core": "6.0.4", - "@angular/forms": "6.0.4", - "@angular/http": "6.0.4", - "@angular/platform-browser": "6.0.4", - "@angular/platform-browser-dynamic": "6.0.4", - "@angular/platform-server": "6.0.4", - "@angular/router": "6.0.4", + "@angular/animations": "6.1.7", + "@angular/common": "6.1.7", + "@angular/core": "6.1.7", + "@angular/forms": "6.1.7", + "@angular/http": "6.1.7", + "@angular/platform-browser": "6.1.7", + "@angular/platform-browser-dynamic": "6.1.7", + "@angular/platform-server": "6.1.7", + "@angular/router": "6.1.7", "angular2-chartjs": "0.5.1", "babel-polyfill": "6.26.0", - "bootstrap": "4.1.1", + "bootstrap": "4.1.3", "core-js": "2.5.7", "graphiql": "0.11.11", - "graphql": "0.13.2", + "graphql": "14.0.2", "moment": "2.22.2", "mousetrap": "1.6.2", "ng2-dnd": "5.0.2", - "npm": "^6.2.0", - "oidc-client": "1.4.1", + "oidc-client": "1.5.2", "pikaday": "1.7.0", "progressbar.js": "1.0.1", - "react": "16.4.0", - "react-dom": "16.4.0", - "rxjs": "6.2.0", - "slugify": "1.3.0", + "react": "16.5.0", + "react-dom": "16.5.0", + "rxjs": "6.3.2", + "slugify": "1.3.1", "sortablejs": "1.7.0", "tslib": "^1.9.3", "zone.js": "0.8.26" }, "devDependencies": { - "@angular/compiler": "6.0.4", - "@angular/compiler-cli": "6.0.4", - "@ngtools/webpack": "6.0.8", + "@angular/compiler": "6.1.7", + "@angular/compiler-cli": "6.1.7", + "@ngtools/webpack": "6.2.1", "@types/core-js": "2.5.0", "@types/jasmine": "2.8.8", "@types/mousetrap": "1.6", - "@types/node": "10.1.2", - "@types/react": "16.3.16", - "@types/react-dom": "16.0.6", + "@types/node": "10.9.4", + "@types/react": "16.4.13", + "@types/react-dom": "16.0.7", "@types/sortablejs": "1.3.32", "angular-router-loader": "0.8.5", "angular2-template-loader": "0.6.2", - "awesome-typescript-loader": "5.0.0", - "codelyzer": "4.3.0", + "awesome-typescript-loader": "5.2.1", + "codelyzer": "4.4.4", "cpx": "1.5.0", - "css-loader": "0.28.11", + "css-loader": "1.0.0", "exports-loader": "0.7.0", - "file-loader": "1.1.11", + "file-loader": "2.0.0", "html-loader": "0.5.5", "html-webpack-plugin": "3.2.0", "ignore-loader": "0.1.2", "istanbul-instrumenter-loader": "0.2.0", - "jasmine-core": "3.1.0", - "karma": "2.0.2", + "jasmine-core": "3.2.1", + "karma": "3.0.0", "karma-chrome-launcher": "2.2.0", "karma-cli": "1.0.1", "karma-coverage": "1.1.2", - "karma-htmlfile-reporter": "0.3.5", + "karma-htmlfile-reporter": "0.3.6", "karma-jasmine": "1.1.2", - "karma-jasmine-html-reporter": "1.1.0", + "karma-jasmine-html-reporter": "1.3.1", "karma-mocha-reporter": "2.2.5", "karma-sourcemap-loader": "0.3.7", - "karma-webpack": "3.0.0", - "mini-css-extract-plugin": "0.4.0", - "node-sass": "4.8.3", + "karma-webpack": "3.0.4", + "mini-css-extract-plugin": "0.4.2", + "node-sass": "4.9.3", "noop-loader": "1.0.0", "null-loader": "0.1.1", "raw-loader": "0.5.1", "rimraf": "2.6.2", - "rxjs-tslint": "0.1.4", + "rxjs-tslint": "0.1.5", "sass-lint": "1.12.1", - "sass-loader": "7.0.3", - "style-loader": "0.21.0", - "tsconfig-paths-webpack-plugin": "3.1.4", - "tslint": "5.10.0", + "sass-loader": "7.1.0", + "style-loader": "0.23.0", + "tsconfig-paths-webpack-plugin": "3.2.0", + "tslint": "5.11.0", "tslint-loader": "3.6.0", "typemoq": "2.1.0", - "typescript": "2.7.2", - "uglifyjs-webpack-plugin": "1.2.5", + "typescript": "2.9.1", + "uglifyjs-webpack-plugin": "1.3.0", "underscore": "1.9.1", - "webpack": "4.11.1", - "webpack-cli": "3.0.3", - "webpack-dev-server": "3.1.4", - "webpack-merge": "4.1.2" + "webpack": "4.17.2", + "webpack-cli": "3.1.0", + "webpack-dev-server": "3.1.8", + "webpack-merge": "4.1.4" } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs index 9bb5e95e4..9d3a08544 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Orleans; +using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Tags; @@ -50,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Assets asset.OnActivateAsync(Id).Wait(); A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A>.Ignored, A>.Ignored)) - .Returns(new HashSet()); + .Returns(new Dictionary()); A.CallTo(() => grainFactory.GetGrain(Id, null)) .Returns(asset); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs index ae2961ca2..a7ac5d7ae 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs @@ -10,9 +10,9 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using FakeItEasy; +using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.State; -using Squidex.Domain.Apps.Entities.Tags; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; @@ -39,7 +39,7 @@ namespace Squidex.Domain.Apps.Entities.Assets public AssetGrainTests() { A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A>.Ignored, A>.Ignored)) - .Returns(new HashSet()); + .Returns(new Dictionary()); sut = new AssetGrain(Store, tagService, A.Dummy()); sut.OnActivateAsync(Id).Wait(); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs index f60022bb3..03af8482c 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs @@ -11,9 +11,9 @@ using System.Security.Claims; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Domain.Apps.Entities.Tags; using Squidex.Infrastructure; using Xunit; diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/OData/ODataQueryTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/OData/ODataQueryTests.cs index c0d5a195e..34df435b3 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/OData/ODataQueryTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/OData/ODataQueryTests.cs @@ -11,10 +11,10 @@ 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 Squidex.Domain.Apps.Entities.MongoDb.Assets; using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors; -using Squidex.Domain.Apps.Entities.Tags; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb.OData; @@ -39,7 +39,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData public ODataQueryTests() { A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A>.That.Contains("tag1"))) - .Returns(HashSet.Of("normalized1")); + .Returns(new Dictionary { ["tag1"] = "normalized1" }); valueConverter = FindExtensions.CreateValueConverter(appId, tagService); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs index ec40dc92b..5f449249f 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using FakeItEasy; using Orleans; +using Squidex.Domain.Apps.Core.Tags; using Xunit; namespace Squidex.Domain.Apps.Entities.Tags diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs index 90b9fa0c8..f978d8c06 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using FakeItEasy; +using Squidex.Domain.Apps.Core.Tags; using Squidex.Infrastructure; using Squidex.Infrastructure.States; using Xunit; @@ -89,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Tags public async Task Should_not_add_tags_if_already_added() { var result1 = await sut.NormalizeTagsAsync(HashSet.Of("tag1", "tag2"), null); - var result2 = await sut.NormalizeTagsAsync(HashSet.Of("tag1", "tag2", "tag3"), result1); + var result2 = await sut.NormalizeTagsAsync(HashSet.Of("tag1", "tag2", "tag3"), new HashSet(result1.Values)); var allTags = await sut.GetTagsAsync(); @@ -107,7 +108,7 @@ namespace Squidex.Domain.Apps.Entities.Tags var result1 = await sut.NormalizeTagsAsync(HashSet.Of("tag1", "tag2"), null); var result2 = await sut.NormalizeTagsAsync(HashSet.Of("tag2", "tag3"), null); - await sut.NormalizeTagsAsync(null, result1); + await sut.NormalizeTagsAsync(null, new HashSet(result1.Values)); var allTags = await sut.GetTagsAsync(); diff --git a/tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs b/tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs index ddfe5c2c3..4551fbe57 100644 --- a/tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs @@ -29,18 +29,18 @@ namespace Squidex.Infrastructure.Caching [Fact] public void Should_remove_old_items_when_capacity_reached() { - for (int i = 0; i < 15; i++) + for (var i = 0; i < 15; i++) { sut.Set(i.ToString(), i); } - for (int i = 0; i < 5; i++) + for (var i = 0; i < 5; i++) { Assert.False(sut.TryGetValue(i.ToString(), out var value)); Assert.Null(value); } - for (int i = 5; i < 15; i++) + for (var i = 5; i < 15; i++) { Assert.True(sut.TryGetValue(i.ToString(), out var value)); Assert.NotNull(value); diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs b/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs index 8e4531157..4131a1044 100644 --- a/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs @@ -12,7 +12,6 @@ using System.Threading.Tasks; using FakeItEasy; using Orleans; using Orleans.Runtime; -using Squidex.Infrastructure.Tasks; using Xunit; namespace Squidex.Infrastructure.Orleans diff --git a/tests/Squidex.Infrastructure.Tests/Queries/ODataConversionTests.cs b/tests/Squidex.Infrastructure.Tests/Queries/ODataConversionTests.cs new file mode 100644 index 000000000..20ec5d139 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Queries/ODataConversionTests.cs @@ -0,0 +1,316 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.OData.Edm; +using Squidex.Infrastructure.Queries.OData; +using Xunit; + +namespace Squidex.Infrastructure.Queries +{ + public class ODataConversionTests + { + private static readonly IEdmModel EdmModel; + + static ODataConversionTests() + { + var entityType = new EdmEntityType("Squidex", "Users"); + + entityType.AddStructuralProperty("id", EdmPrimitiveTypeKind.Guid); + entityType.AddStructuralProperty("created", EdmPrimitiveTypeKind.DateTimeOffset); + entityType.AddStructuralProperty("isComicFigure", EdmPrimitiveTypeKind.Boolean); + entityType.AddStructuralProperty("firstName", EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty("lastName", EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty("birthday", EdmPrimitiveTypeKind.Date); + entityType.AddStructuralProperty("incomeCents", EdmPrimitiveTypeKind.Int64); + entityType.AddStructuralProperty("incomeMio", EdmPrimitiveTypeKind.Double); + entityType.AddStructuralProperty("age", EdmPrimitiveTypeKind.Int32); + + var container = new EdmEntityContainer("Squidex", "Container"); + + container.AddEntitySet("UserSet", entityType); + + var model = new EdmModel(); + + model.AddElement(container); + model.AddElement(entityType); + + EdmModel = model; + } + + [Fact] + public void Should_parse_query() + { + var parser = EdmModel.ParseQuery("$filter=firstName eq 'Dagobert'"); + + Assert.NotNull(parser); + } + + [Fact] + public void Should_parse_filter_when_type_is_datetime() + { + var i = Q("$filter=created eq 1988-01-19T12:00:00Z"); + var o = C("Filter: created == 1988-01-19T12:00:00Z"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_date() + { + var i = Q("$filter=created eq 1988-01-19"); + var o = C("Filter: created == 1988-01-19T00:00:00Z"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_guid() + { + var i = Q("$filter=id eq B5FE25E3-B262-4B17-91EF-B3772A6B62BB"); + var o = C("Filter: id == b5fe25e3-b262-4b17-91ef-b3772a6b62bb"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_string() + { + var i = Q("$filter=firstName eq 'Dagobert'"); + var o = C("Filter: firstName == Dagobert"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_boolean() + { + var i = Q("$filter=isComicFigure eq true"); + var o = C("Filter: isComicFigure == True"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_int32() + { + var i = Q("$filter=age eq 60"); + var o = C("Filter: age == 60"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_int64() + { + var i = Q("$filter=incomeCents eq 31543143513456789"); + var o = C("Filter: incomeCents == 31543143513456789"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_double() + { + var i = Q("$filter=incomeMio eq 5634474356.1233"); + var o = C("Filter: incomeMio == 5634474356.1233"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_negation() + { + var i = Q("$filter=not endswith(lastName, 'Duck')"); + var o = C("Filter: !(endsWith(lastName, Duck))"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_startswith() + { + var i = Q("$filter=startswith(lastName, 'Duck')"); + var o = C("Filter: startsWith(lastName, Duck)"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_endswith() + { + var i = Q("$filter=endswith(lastName, 'Duck')"); + var o = C("Filter: endsWith(lastName, Duck)"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_contains() + { + var i = Q("$filter=contains(lastName, 'Duck')"); + var o = C("Filter: contains(lastName, Duck)"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_contains_to_true() + { + var i = Q("$filter=contains(lastName, 'Duck') eq true"); + var o = C("Filter: contains(lastName, Duck)"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_contains_to_false() + { + var i = Q("$filter=contains(lastName, 'Duck') eq false"); + var o = C("Filter: !(contains(lastName, Duck))"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_equals() + { + var i = Q("$filter=age eq 1"); + var o = C("Filter: age == 1"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_notequals() + { + var i = Q("$filter=age ne 1"); + var o = C("Filter: age != 1"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_lessthan() + { + var i = Q("$filter=age lt 1"); + var o = C("Filter: age < 1"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_lessthanorequal() + { + var i = Q("$filter=age le 1"); + var o = C("Filter: age <= 1"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_greaterthan() + { + var i = Q("$filter=age gt 1"); + var o = C("Filter: age > 1"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_greaterthanorequal() + { + var i = Q("$filter=age ge 1"); + var o = C("Filter: age >= 1"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_conjunction_and_contains() + { + var i = Q("$filter=contains(firstName, 'Sebastian') eq false and isComicFigure eq true"); + var o = C("Filter: (!(contains(firstName, Sebastian)) && isComicFigure == True)"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_conjunction() + { + var i = Q("$filter=age eq 1 and age eq 2"); + var o = C("Filter: (age == 1 && age == 2)"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_disjunction() + { + var i = Q("$filter=age eq 1 or age eq 2"); + var o = C("Filter: (age == 1 || age == 2)"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_full_text() + { + var i = Q("$search=Duck"); + var o = C("FullText: Duck"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_full_text_and_multiple_terms() + { + var i = Q("$search=Dagobert or Donald"); + var o = C("FullText: Dagobert or Donald"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_orderby_with_single_field() + { + var i = Q("$orderby=age desc"); + var o = C("Sort: age Descending"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_orderby_with_multiple_field() + { + var i = Q("$orderby=age, incomeMio desc"); + var o = C("Sort: age Ascending, incomeMio Descending"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_and_take() + { + var i = Q("$top=3&$skip=4"); + var o = C("Skip: 4; Take: 3"); + + Assert.Equal(o, i); + } + + private static string C(string value) + { + return value.Replace('\'', '"'); + } + + private string Q(string value) + { + var parser = EdmModel.ParseQuery(value); + + return parser.ToQuery().ToString(); + } + } +} \ No newline at end of file