Browse Source

Custom query structure.

pull/315/head
Sebastian Stehle 8 years ago
parent
commit
c96d1390cf
  1. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs
  2. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs
  3. 2
      src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  4. 4
      src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs
  5. 72
      src/Squidex.Domain.Apps.Core.Operations/Queries/FilterComparison.cs
  6. 46
      src/Squidex.Domain.Apps.Core.Operations/Queries/FilterJunction.cs
  7. 9
      src/Squidex.Domain.Apps.Core.Operations/Queries/FilterJunctionType.cs
  8. 33
      src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNegate.cs
  9. 14
      src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNode.cs
  10. 31
      src/Squidex.Domain.Apps.Core.Operations/Queries/FilterNodeVisitor.cs
  11. 22
      src/Squidex.Domain.Apps.Core.Operations/Queries/FilterOperator.cs
  12. 21
      src/Squidex.Domain.Apps.Core.Operations/Queries/FilterValueType.cs
  13. 113
      src/Squidex.Domain.Apps.Core.Operations/Queries/OData/ConstantVisitor.cs
  14. 49
      src/Squidex.Domain.Apps.Core.Operations/Queries/OData/FilterBuilder.cs
  15. 153
      src/Squidex.Domain.Apps.Core.Operations/Queries/OData/FilterVisitor.cs
  16. 34
      src/Squidex.Domain.Apps.Core.Operations/Queries/OData/LimitExtensions.cs
  17. 56
      src/Squidex.Domain.Apps.Core.Operations/Queries/OData/PropertyPathVisitor.cs
  18. 41
      src/Squidex.Domain.Apps.Core.Operations/Queries/OData/SearchTermVisitor.cs
  19. 43
      src/Squidex.Domain.Apps.Core.Operations/Queries/OData/SortBuilder.cs
  20. 24
      src/Squidex.Domain.Apps.Core.Operations/Queries/Query.cs
  21. 30
      src/Squidex.Domain.Apps.Core.Operations/Queries/SortNode.cs
  22. 15
      src/Squidex.Domain.Apps.Core.Operations/Queries/SortOrder.cs
  23. 29
      src/Squidex.Domain.Apps.Core.Operations/Queries/TransformVisitor.cs
  24. 6
      src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs
  25. 2
      src/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs
  26. 21
      src/Squidex.Domain.Apps.Core.Operations/Tags/TagGroups.cs
  27. 130
      src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs
  28. 2
      src/Squidex.Domain.Apps.Core.Operations/Tags/TagSet.cs
  29. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  30. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs
  31. 14
      src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
  32. 2
      src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs
  33. 2
      src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs
  34. 2
      src/Squidex.Domain.Apps.Entities/Backup/Helpers/ArchiveHelper.cs
  35. 5
      src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs
  36. 5
      src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs
  37. 15
      src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs
  38. 2
      src/Squidex.Infrastructure/Guard.cs
  39. 71
      src/Squidex.Infrastructure/Queries/FilterComparison.cs
  40. 45
      src/Squidex.Infrastructure/Queries/FilterJunction.cs
  41. 15
      src/Squidex.Infrastructure/Queries/FilterJunctionType.cs
  42. 31
      src/Squidex.Infrastructure/Queries/FilterNegate.cs
  43. 14
      src/Squidex.Infrastructure/Queries/FilterNode.cs
  44. 31
      src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs
  45. 22
      src/Squidex.Infrastructure/Queries/FilterOperator.cs
  46. 21
      src/Squidex.Infrastructure/Queries/FilterValueType.cs
  47. 30
      src/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs
  48. 113
      src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs
  49. 50
      src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs
  50. 48
      src/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs
  51. 148
      src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs
  52. 34
      src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs
  53. 61
      src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs
  54. 41
      src/Squidex.Infrastructure/Queries/OData/SearchTermVisitor.cs
  55. 43
      src/Squidex.Infrastructure/Queries/OData/SortBuilder.cs
  56. 56
      src/Squidex.Infrastructure/Queries/Query.cs
  57. 36
      src/Squidex.Infrastructure/Queries/SortNode.cs
  58. 15
      src/Squidex.Infrastructure/Queries/SortOrder.cs
  59. 29
      src/Squidex.Infrastructure/Queries/TransformVisitor.cs
  60. 1
      src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  61. 4
      src/Squidex.Infrastructure/Tasks/AsyncLock.cs
  62. 2
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  63. 2
      src/Squidex/Squidex.csproj
  64. 89
      src/Squidex/package.json
  65. 3
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
  66. 4
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs
  67. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs
  68. 4
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/OData/ODataQueryTests.cs
  69. 1
      tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs
  70. 5
      tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs
  71. 6
      tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs
  72. 1
      tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs
  73. 316
      tests/Squidex.Infrastructure.Tests/Queries/ODataConversionTests.cs

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

2
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<T>(IFieldPropertiesVisitor<T> visitor)
{
return visitor.Visit(this);

2
src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -8,7 +8,7 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Fody" Version="3.2.0">
<PackageReference Include="Fody" Version="3.2.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

4
src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs

@ -26,8 +26,10 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
{
private static readonly Func<IField, string> KeyNameResolver = f => f.Name;
private static readonly Func<IField, string> KeyIdResolver = f => f.Id.ToString();
private static readonly Func<IArrayField, string, IField> FieldByIdResolver =
(f, k) => long.TryParse(k, out var id) ? f.FieldsById.GetOrDefault(id) : null;
private static readonly Func<IArrayField, string, IField> 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<JObject>())
foreach (var item in jArray.OfType<JObject>())
{
var newItem = new JObject();

72
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<string> Path { get; }
public FilterOperator Operator { get; }
public FilterValueType ValueType { get; }
public object Value { get; }
public FilterComparison(IReadOnlyList<string> path, FilterOperator @operator, object value, FilterValueType valueType)
{
Guard.NotNull(path, nameof(path));
Guard.NotEmpty(path, nameof(path));
Guard.Enum(@operator, nameof(@operator));
Guard.Enum(valueType, nameof(valueType));
Path = path;
Value = value;
ValueType = valueType;
Operator = @operator;
}
public override T Accept<T>(FilterNodeVisitor<T> visitor)
{
return visitor.Visit(this);
}
public override string ToString()
{
var path = string.Join(".", Path);
switch (Operator)
{
case FilterOperator.Contains:
return $"contains({path}, {Value})";
case FilterOperator.EndsWith:
return $"endsWith({path}, {Value})";
case FilterOperator.StartsWith:
return $"startsWith({path}, {Value})";
case FilterOperator.Equals:
return $"{path} == {Value}";
case FilterOperator.NotEquals:
return $"{path} != {Value}";
case FilterOperator.GreaterThan:
return $"{path} > {Value}";
case FilterOperator.GreaterThanOrEqual:
return $"{path} >= {Value}";
case FilterOperator.LessThan:
return $"{path} < {Value}";
case FilterOperator.LessThanOrEqual:
return $"{path} <= {Value}";
default:
return string.Empty;
}
}
}
}

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

@ -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<FilterNode> Operands { get; }
public FilterJunctionType JunctionType { get; }
public FilterJunction(FilterJunctionType junctionType, IReadOnlyList<FilterNode> operands)
{
Guard.NotNull(operands, nameof(operands));
Guard.GreaterEquals(operands.Count, 2, nameof(operands.Count));
Guard.Enum(junctionType, nameof(junctionType));
Operands = operands;
JunctionType = junctionType;
}
public FilterJunction(FilterJunctionType junctionType, params FilterNode[] operands)
: this(junctionType, operands?.ToList())
{
}
public override T Accept<T>(FilterNodeVisitor<T> visitor)
{
return visitor.Visit(this);
}
public override string ToString()
{
return $"({string.Join(JunctionType == FilterJunctionType.And ? " && " : " || ", Operands)})";
}
}
}

9
src/Squidex.Domain.Apps.Entities/Tags/TagGroups.cs → 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
}
}
}

33
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<T>(FilterNodeVisitor<T> visitor)
{
return visitor.Visit(this);
}
public override string ToString()
{
return $"!{Operand}";
}
}
}

14
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<T>(FilterNodeVisitor<T> visitor);
}
}

31
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<T>
{
public virtual T Visit(FilterComparison nodeIn)
{
throw new NotImplementedException();
}
public virtual T Visit(FilterJunction nodeIn)
{
throw new NotImplementedException();
}
public virtual T Visit(FilterNegate nodeIn)
{
throw new NotImplementedException();
}
}
}

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

@ -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
}
}

21
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,
}
}

113
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();
}
}
}

49
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);
}
}
}
}

153
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<FilterNode>
{
private static readonly FilterVisitor Instance = new FilterVisitor();
private FilterVisitor()
{
}
public static FilterNode Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override FilterNode Visit(ConvertNode nodeIn)
{
return nodeIn.Source.Accept(this);
}
public override FilterNode Visit(UnaryOperatorNode nodeIn)
{
if (nodeIn.OperatorKind == UnaryOperatorKind.Not)
{
return new FilterNegate(nodeIn.Operand.Accept(this));
}
throw new NotSupportedException();
}
public override FilterNode Visit(SingleValueFunctionCallNode nodeIn)
{
var fieldNode = nodeIn.Parameters.ElementAt(0);
var valueNode = nodeIn.Parameters.ElementAt(1);
if (string.Equals(nodeIn.Name, "endswith", StringComparison.OrdinalIgnoreCase))
{
var (value, valueType) = ConstantVisitor.Visit(valueNode);
return new FilterComparison(PropertyPathVisitor.Visit(fieldNode), FilterOperator.EndsWith, value, valueType);
}
if (string.Equals(nodeIn.Name, "startswith", StringComparison.OrdinalIgnoreCase))
{
var (value, valueType) = ConstantVisitor.Visit(valueNode);
return new FilterComparison(PropertyPathVisitor.Visit(fieldNode), FilterOperator.StartsWith, value, valueType);
}
if (string.Equals(nodeIn.Name, "contains", StringComparison.OrdinalIgnoreCase))
{
var (value, valueType) = ConstantVisitor.Visit(valueNode);
return new FilterComparison(PropertyPathVisitor.Visit(fieldNode), FilterOperator.Contains, value, valueType);
}
throw new NotSupportedException();
}
public override FilterNode Visit(BinaryOperatorNode nodeIn)
{
if (nodeIn.OperatorKind == BinaryOperatorKind.And)
{
return new FilterJunction(FilterJunctionType.And, nodeIn.Left.Accept(this), nodeIn.Right.Accept(this));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.Or)
{
return new FilterJunction(FilterJunctionType.Or, nodeIn.Left.Accept(this), nodeIn.Right.Accept(this));
}
if (nodeIn.Left is SingleValueFunctionCallNode functionNode)
{
var regexFilter = Visit(functionNode);
var value = BuildValue(nodeIn.Right);
if (value is bool booleanRight)
{
if ((nodeIn.OperatorKind == BinaryOperatorKind.Equal && !booleanRight) ||
(nodeIn.OperatorKind == BinaryOperatorKind.NotEqual && booleanRight))
{
regexFilter = new FilterNegate(regexFilter);
}
return regexFilter;
}
}
else
{
if (nodeIn.OperatorKind == BinaryOperatorKind.NotEqual)
{
var (value, valueType) = ConstantVisitor.Visit(nodeIn.Left);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Right), FilterOperator.NotEquals, value, valueType);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.Equal)
{
var (value, valueType) = ConstantVisitor.Visit(nodeIn.Left);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Right), FilterOperator.Equals, value, valueType);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThan)
{
var (value, valueType) = ConstantVisitor.Visit(nodeIn.Left);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Right), FilterOperator.LessThan, value, valueType);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThanOrEqual)
{
var (value, valueType) = ConstantVisitor.Visit(nodeIn.Left);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Right), FilterOperator.LessThanOrEqual, value, valueType);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThan)
{
var (value, valueType) = ConstantVisitor.Visit(nodeIn.Left);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Right), FilterOperator.GreaterThan, value, valueType);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThanOrEqual)
{
var (value, valueType) = ConstantVisitor.Visit(nodeIn.Left);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Right), FilterOperator.GreaterThanOrEqual, value, valueType);
}
}
throw new NotSupportedException();
}
private object BuildValue(QueryNode nodeIn)
{
return ConstantVisitor.Visit(nodeIn);
}
}
}

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

@ -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;
}
}
}
}

56
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<ImmutableList<string>>
{
private static readonly PropertyPathVisitor Instance = new PropertyPathVisitor();
private PropertyPathVisitor()
{
}
public static ImmutableList<string> Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override ImmutableList<string> Visit(ConvertNode nodeIn)
{
return nodeIn.Source.Accept(this);
}
public override ImmutableList<string> Visit(SingleComplexNode nodeIn)
{
if (nodeIn.Source is SingleComplexNode)
{
return nodeIn.Source.Accept(this).Add(nodeIn.Property.Name.ToPascalCase());
}
else
{
return ImmutableList.Create(nodeIn.Property.Name.ToPascalCase());
}
}
public override ImmutableList<string> Visit(SingleValuePropertyAccessNode nodeIn)
{
if (nodeIn.Source is SingleComplexNode)
{
return nodeIn.Source.Accept(this).Add(nodeIn.Property.Name.ToPascalCase());
}
else
{
return ImmutableList.Create(nodeIn.Property.Name.ToPascalCase());
}
}
}
}

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

@ -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<string>
{
private static readonly SearchTermVisitor Instance = new SearchTermVisitor();
private SearchTermVisitor()
{
}
public static object Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override string Visit(BinaryOperatorNode nodeIn)
{
if (nodeIn.OperatorKind == BinaryOperatorKind.And)
{
return nodeIn.Left.Accept(this) + " " + nodeIn.Right.Accept(this);
}
throw new NotSupportedException();
}
public override string Visit(SearchTermNode nodeIn)
{
return nodeIn.Text;
}
}
}

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

@ -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);
}
}
}
}

24
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<SortNode> Sort { get; } = new List<SortNode>();
public string FullText { get; set; }
}
}

30
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<string> Path { get; }
public SortOrder SortOrder { get; set; }
public SortNode(IReadOnlyList<string> path, SortOrder sortOrder)
{
Guard.NotNull(path, nameof(path));
Guard.NotEmpty(path, nameof(path));
Guard.Enum(sortOrder, nameof(sortOrder));
Path = path;
SortOrder = sortOrder;
}
}
}

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

@ -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
}
}

29
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<FilterNode>
{
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));
}
}
}

6
src/Squidex.Domain.Apps.Entities/Tags/ITagService.cs → 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<HashSet<string>> NormalizeTagsAsync(Guid appId, string group, HashSet<string> names, HashSet<string> ids);
Task<Dictionary<string, string>> GetTagIdsAsync(Guid appId, string group, HashSet<string> names);
Task<HashSet<string>> GetTagIdsAsync(Guid appId, string group, HashSet<string> names);
Task<Dictionary<string, string>> NormalizeTagsAsync(Guid appId, string group, HashSet<string> names, HashSet<string> ids);
Task<Dictionary<string, string>> DenormalizeTagsAsync(Guid appId, string group, HashSet<string> ids);

2
src/Squidex.Domain.Apps.Entities/Tags/Tag.cs → 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
{

21
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}";
}
}
}

130
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<string>();
var tagsArrays = new List<JArray>();
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<string>();
var tagsArrays = new List<JArray>();
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<string> values, List<JArray> 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<string> values, List<JArray> arrays)
{
if (token is JArray jArray)
{
foreach (var value in jArray)
{
if (value.Type == JTokenType.String)
{
values.Add(value.ToString());
}
}
arrays.Add(jArray);
}
}
}
}

2
src/Squidex.Domain.Apps.Entities/Tags/TagSet.cs → 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<string, Tag>
{

2
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;

2
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;

14
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<HashSet<string>> NormalizeTagsAsync(Guid appId, HashSet<string> tags)
{
var normalized = await tagService.NormalizeTagsAsync(appId, TagGroups.Assets, tags, Snapshot.Tags);
return new HashSet<string>(normalized.Values);
}
public void Create(CreateAsset command)
{
var @event = SimpleMapper.Map(command, new AssetCreated

2
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

2
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;

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

5
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<HashSet<string>> NormalizeTagsAsync(Guid appId, string group, HashSet<string> names, HashSet<string> ids)
public Task<Dictionary<string, string>> NormalizeTagsAsync(Guid appId, string group, HashSet<string> names, HashSet<string> ids)
{
return GetGrain(appId, group).NormalizeTagsAsync(names, ids);
}
public Task<HashSet<string>> GetTagIdsAsync(Guid appId, string group, HashSet<string> names)
public Task<Dictionary<string, string>> GetTagIdsAsync(Guid appId, string group, HashSet<string> names)
{
return GetGrain(appId, group).GetTagIdsAsync(names);
}

5
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<HashSet<string>> NormalizeTagsAsync(HashSet<string> names, HashSet<string> ids);
Task<Dictionary<string, string>> NormalizeTagsAsync(HashSet<string> names, HashSet<string> ids);
Task<HashSet<string>> GetTagIdsAsync(HashSet<string> names);
Task<Dictionary<string, string>> GetTagIdsAsync(HashSet<string> names);
Task<Dictionary<string, string>> DenormalizeTagsAsync(HashSet<string> ids);

15
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<HashSet<string>> NormalizeTagsAsync(HashSet<string> names, HashSet<string> ids)
public async Task<Dictionary<string, string>> NormalizeTagsAsync(HashSet<string> names, HashSet<string> ids)
{
var result = new HashSet<string>();
var result = new Dictionary<string, string>();
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<HashSet<string>> GetTagIdsAsync(HashSet<string> names)
public Task<Dictionary<string, string>> GetTagIdsAsync(HashSet<string> names)
{
var result = new HashSet<string>();
var result = new Dictionary<string, string>();
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);
}
}

2
src/Squidex.Infrastructure/Guard.cs

@ -142,7 +142,7 @@ namespace Squidex.Infrastructure
[DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void NotEmpty<TType>(ICollection<TType> enumerable, string parameterName)
public static void NotEmpty<TType>(IReadOnlyCollection<TType> enumerable, string parameterName)
{
NotNull(enumerable, parameterName);

71
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<string> Path { get; }
public FilterOperator Operator { get; }
public FilterValueType ValueType { get; }
public object Value { get; }
public FilterComparison(IReadOnlyList<string> path, FilterOperator @operator, object value, FilterValueType valueType)
{
Guard.NotNull(path, nameof(path));
Guard.NotEmpty(path, nameof(path));
Guard.Enum(@operator, nameof(@operator));
Guard.Enum(valueType, nameof(valueType));
Path = path;
Value = value;
ValueType = valueType;
Operator = @operator;
}
public override T Accept<T>(FilterNodeVisitor<T> visitor)
{
return visitor.Visit(this);
}
public override string ToString()
{
var path = string.Join(".", Path);
switch (Operator)
{
case FilterOperator.Contains:
return $"contains({path}, {Value})";
case FilterOperator.EndsWith:
return $"endsWith({path}, {Value})";
case FilterOperator.StartsWith:
return $"startsWith({path}, {Value})";
case FilterOperator.Equals:
return $"{path} == {Value}";
case FilterOperator.NotEquals:
return $"{path} != {Value}";
case FilterOperator.GreaterThan:
return $"{path} > {Value}";
case FilterOperator.GreaterThanOrEqual:
return $"{path} >= {Value}";
case FilterOperator.LessThan:
return $"{path} < {Value}";
case FilterOperator.LessThanOrEqual:
return $"{path} <= {Value}";
default:
return string.Empty;
}
}
}
}

45
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<FilterNode> Operands { get; }
public FilterJunctionType JunctionType { get; }
public FilterJunction(FilterJunctionType junctionType, IReadOnlyList<FilterNode> operands)
{
Guard.NotNull(operands, nameof(operands));
Guard.GreaterEquals(operands.Count, 2, nameof(operands.Count));
Guard.Enum(junctionType, nameof(junctionType));
Operands = operands;
JunctionType = junctionType;
}
public FilterJunction(FilterJunctionType junctionType, params FilterNode[] operands)
: this(junctionType, operands?.ToList())
{
}
public override T Accept<T>(FilterNodeVisitor<T> visitor)
{
return visitor.Visit(this);
}
public override string ToString()
{
return $"({string.Join(JunctionType == FilterJunctionType.And ? " && " : " || ", Operands)})";
}
}
}

15
src/Squidex.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
}
}

31
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<T>(FilterNodeVisitor<T> visitor)
{
return visitor.Visit(this);
}
public override string ToString()
{
return $"!({Operand})";
}
}
}

14
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<T>(FilterNodeVisitor<T> visitor);
}
}

31
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<T>
{
public virtual T Visit(FilterComparison nodeIn)
{
throw new NotImplementedException();
}
public virtual T Visit(FilterJunction nodeIn)
{
throw new NotImplementedException();
}
public virtual T Visit(FilterNegate nodeIn)
{
throw new NotImplementedException();
}
}
}

22
src/Squidex.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
}
}

21
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,
}
}

30
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<object>
{
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;
}
}
}

113
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();
}
}
}

50
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;
}
}
}

48
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);
}
}
}
}

148
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<FilterNode>
{
private static readonly FilterVisitor Instance = new FilterVisitor();
private FilterVisitor()
{
}
public static FilterNode Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override FilterNode Visit(ConvertNode nodeIn)
{
return nodeIn.Source.Accept(this);
}
public override FilterNode Visit(UnaryOperatorNode nodeIn)
{
if (nodeIn.OperatorKind == UnaryOperatorKind.Not)
{
return new FilterNegate(nodeIn.Operand.Accept(this));
}
throw new NotSupportedException();
}
public override FilterNode Visit(SingleValueFunctionCallNode nodeIn)
{
var fieldNode = nodeIn.Parameters.ElementAt(0);
var valueNode = nodeIn.Parameters.ElementAt(1);
if (string.Equals(nodeIn.Name, "endswith", StringComparison.OrdinalIgnoreCase))
{
var (value, valueType) = 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();
}
}
}

34
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;
}
}
}
}

61
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<ImmutableList<string>>
{
private static readonly PropertyPathVisitor Instance = new PropertyPathVisitor();
private PropertyPathVisitor()
{
}
public static ImmutableList<string> Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override ImmutableList<string> Visit(ConvertNode nodeIn)
{
return nodeIn.Source.Accept(this);
}
public override ImmutableList<string> Visit(SingleComplexNode nodeIn)
{
if (nodeIn.Source is SingleComplexNode)
{
return nodeIn.Source.Accept(this).Add(UnescapeEdmField(nodeIn.Property));
}
else
{
return ImmutableList.Create(UnescapeEdmField(nodeIn.Property));
}
}
public override ImmutableList<string> 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;
}
}
}

41
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<string>
{
private static readonly SearchTermVisitor Instance = new SearchTermVisitor();
private SearchTermVisitor()
{
}
public static object Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override string Visit(BinaryOperatorNode nodeIn)
{
if (nodeIn.OperatorKind == BinaryOperatorKind.And)
{
return nodeIn.Left.Accept(this) + " " + nodeIn.Right.Accept(this);
}
throw new NotSupportedException();
}
public override string Visit(SearchTermNode nodeIn)
{
return nodeIn.Text;
}
}
}

43
src/Squidex.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);
}
}
}
}

56
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<SortNode> Sort { get; } = new List<SortNode>();
public override string ToString()
{
var parts = new List<string>();
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);
}
}
}

36
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<string> Path { get; }
public SortOrder SortOrder { get; set; }
public SortNode(IReadOnlyList<string> path, SortOrder sortOrder)
{
Guard.NotNull(path, nameof(path));
Guard.NotEmpty(path, nameof(path));
Guard.Enum(sortOrder, nameof(sortOrder));
Path = path;
SortOrder = sortOrder;
}
public override string ToString()
{
var path = string.Join(".", Path);
return $"{path} {SortOrder}";
}
}
}

15
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
}
}

29
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<FilterNode>
{
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));
}
}
}

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

@ -10,6 +10,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.1.2" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.0" />
<PackageReference Include="Microsoft.Orleans.Core" Version="2.1.0-beta1" />
<PackageReference Include="Microsoft.Orleans.OrleansCodeGenerator.Build" Version="2.1.0-beta1">
<PrivateAssets>all</PrivateAssets>

4
src/Squidex.Infrastructure/Tasks/AsyncLock.cs

@ -24,7 +24,7 @@ namespace Squidex.Infrastructure.Tasks
public Task<IDisposable> 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)
{

2
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;

2
src/Squidex/Squidex.csproj

@ -80,7 +80,7 @@
<PackageReference Include="MongoDB.Driver" Version="2.7.0" />
<PackageReference Include="NJsonSchema" Version="9.10.72" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.0.0" />
<PackageReference Include="NSwag.AspNetCore" Version="11.19.0" />
<PackageReference Include="NSwag.AspNetCore" Version="11.19.1" />
<PackageReference Include="OpenCover" Version="4.6.519" />
<PackageReference Include="Orleans.Providers.MongoDB" Version="2.0.0" />
<PackageReference Include="OrleansDashboard" Version="2.0.9" />

89
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"
}
}

3
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<HashSet<string>>.Ignored, A<HashSet<string>>.Ignored))
.Returns(new HashSet<string>());
.Returns(new Dictionary<string, string>());
A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id, null))
.Returns(asset);

4
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<HashSet<string>>.Ignored, A<HashSet<string>>.Ignored))
.Returns(new HashSet<string>());
.Returns(new Dictionary<string, string>());
sut = new AssetGrain(Store, tagService, A.Dummy<ISemanticLog>());
sut.OnActivateAsync(Id).Wait();

2
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;

4
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<HashSet<string>>.That.Contains("tag1")))
.Returns(HashSet.Of("normalized1"));
.Returns(new Dictionary<string, string> { ["tag1"] = "normalized1" });
valueConverter = FindExtensions.CreateValueConverter(appId, tagService);
}

1
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

5
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<string>(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<string>(result1.Values));
var allTags = await sut.GetTagsAsync();

6
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);

1
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

316
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();
}
}
}
Loading…
Cancel
Save