mirror of https://github.com/Squidex/squidex.git
56 changed files with 633 additions and 1984 deletions
@ -1,72 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Queries |
|||
{ |
|||
public sealed class FilterComparison : FilterNode |
|||
{ |
|||
public IReadOnlyList<string> Path { get; } |
|||
|
|||
public FilterOperator Operator { get; } |
|||
|
|||
public FilterValueType ValueType { get; } |
|||
|
|||
public object Value { get; } |
|||
|
|||
public FilterComparison(IReadOnlyList<string> path, FilterOperator @operator, object value, FilterValueType valueType) |
|||
{ |
|||
Guard.NotNull(path, nameof(path)); |
|||
Guard.NotEmpty(path, nameof(path)); |
|||
Guard.Enum(@operator, nameof(@operator)); |
|||
Guard.Enum(valueType, nameof(valueType)); |
|||
|
|||
Path = path; |
|||
|
|||
Value = value; |
|||
ValueType = valueType; |
|||
|
|||
Operator = @operator; |
|||
} |
|||
|
|||
public override T Accept<T>(FilterNodeVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
|
|||
public override string ToString() |
|||
{ |
|||
var path = string.Join(".", Path); |
|||
|
|||
switch (Operator) |
|||
{ |
|||
case FilterOperator.Contains: |
|||
return $"contains({path}, {Value})"; |
|||
case FilterOperator.EndsWith: |
|||
return $"endsWith({path}, {Value})"; |
|||
case FilterOperator.StartsWith: |
|||
return $"startsWith({path}, {Value})"; |
|||
case FilterOperator.Equals: |
|||
return $"{path} == {Value}"; |
|||
case FilterOperator.NotEquals: |
|||
return $"{path} != {Value}"; |
|||
case FilterOperator.GreaterThan: |
|||
return $"{path} > {Value}"; |
|||
case FilterOperator.GreaterThanOrEqual: |
|||
return $"{path} >= {Value}"; |
|||
case FilterOperator.LessThan: |
|||
return $"{path} < {Value}"; |
|||
case FilterOperator.LessThanOrEqual: |
|||
return $"{path} <= {Value}"; |
|||
default: |
|||
return string.Empty; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,46 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Queries |
|||
{ |
|||
public sealed class FilterJunction : FilterNode |
|||
{ |
|||
public IReadOnlyList<FilterNode> Operands { get; } |
|||
|
|||
public FilterJunctionType JunctionType { get; } |
|||
|
|||
public FilterJunction(FilterJunctionType junctionType, IReadOnlyList<FilterNode> operands) |
|||
{ |
|||
Guard.NotNull(operands, nameof(operands)); |
|||
Guard.GreaterEquals(operands.Count, 2, nameof(operands.Count)); |
|||
Guard.Enum(junctionType, nameof(junctionType)); |
|||
|
|||
Operands = operands; |
|||
|
|||
JunctionType = junctionType; |
|||
} |
|||
|
|||
public FilterJunction(FilterJunctionType junctionType, params FilterNode[] operands) |
|||
: this(junctionType, operands?.ToList()) |
|||
{ |
|||
} |
|||
|
|||
public override T Accept<T>(FilterNodeVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
|
|||
public override string ToString() |
|||
{ |
|||
return $"({string.Join(JunctionType == FilterJunctionType.And ? " && " : " || ", Operands)})"; |
|||
} |
|||
} |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Queries |
|||
{ |
|||
public enum FilterJunctionType |
|||
{ |
|||
And, |
|||
Or |
|||
} |
|||
} |
|||
@ -1,33 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Queries |
|||
{ |
|||
public sealed class FilterNegate : FilterNode |
|||
{ |
|||
public FilterNode Operand { get; } |
|||
|
|||
public FilterNegate(FilterNode operand) |
|||
{ |
|||
Guard.NotNull(operand, nameof(operand)); |
|||
|
|||
Operand = operand; |
|||
} |
|||
|
|||
public override T Accept<T>(FilterNodeVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
|
|||
public override string ToString() |
|||
{ |
|||
return $"!{Operand}"; |
|||
} |
|||
} |
|||
} |
|||
@ -1,14 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Queries |
|||
{ |
|||
public abstract class FilterNode |
|||
{ |
|||
public abstract T Accept<T>(FilterNodeVisitor<T> visitor); |
|||
} |
|||
} |
|||
@ -1,31 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
|
|||
#pragma warning disable RECS0083 // Shows NotImplementedException throws in the quick task bar
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Queries |
|||
{ |
|||
public abstract class FilterNodeVisitor<T> |
|||
{ |
|||
public virtual T Visit(FilterComparison nodeIn) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
public virtual T Visit(FilterJunction nodeIn) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
public virtual T Visit(FilterNegate nodeIn) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,22 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Queries |
|||
{ |
|||
public enum FilterOperator |
|||
{ |
|||
Contains, |
|||
EndsWith, |
|||
Equals, |
|||
GreaterThan, |
|||
GreaterThanOrEqual, |
|||
LessThan, |
|||
LessThanOrEqual, |
|||
NotEquals, |
|||
StartsWith |
|||
} |
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Queries |
|||
{ |
|||
public enum FilterValueType |
|||
{ |
|||
Boolean, |
|||
Guid, |
|||
Double, |
|||
Instant, |
|||
Int32, |
|||
Int64, |
|||
Single, |
|||
String, |
|||
} |
|||
} |
|||
@ -1,113 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Microsoft.OData; |
|||
using Microsoft.OData.Edm; |
|||
using Microsoft.OData.UriParser; |
|||
using NodaTime; |
|||
using NodaTime.Text; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Queries |
|||
{ |
|||
public sealed class ConstantVisitor : QueryNodeVisitor<(object Value, FilterValueType ValueType)> |
|||
{ |
|||
private static readonly IEdmPrimitiveType BooleanType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Boolean); |
|||
private static readonly IEdmPrimitiveType DateTimeType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset); |
|||
private static readonly IEdmPrimitiveType DoubleType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Double); |
|||
private static readonly IEdmPrimitiveType GuidType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Guid); |
|||
private static readonly IEdmPrimitiveType Int32Type = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32); |
|||
private static readonly IEdmPrimitiveType Int64Type = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int64); |
|||
private static readonly IEdmPrimitiveType SingleType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Single); |
|||
private static readonly IEdmPrimitiveType StringType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.String); |
|||
|
|||
private static readonly ConstantVisitor Instance = new ConstantVisitor(); |
|||
|
|||
private ConstantVisitor() |
|||
{ |
|||
} |
|||
|
|||
public static (object Value, FilterValueType ValueType) Visit(QueryNode node) |
|||
{ |
|||
return node.Accept(Instance); |
|||
} |
|||
|
|||
public override (object Value, FilterValueType ValueType) Visit(ConvertNode nodeIn) |
|||
{ |
|||
if (nodeIn.TypeReference.Definition == BooleanType) |
|||
{ |
|||
return (bool.Parse(Visit(nodeIn.Source).ToString()), FilterValueType.Boolean); |
|||
} |
|||
|
|||
if (nodeIn.TypeReference.Definition == GuidType) |
|||
{ |
|||
return (Guid.Parse(Visit(nodeIn.Source).ToString()), FilterValueType.Guid); |
|||
} |
|||
|
|||
if (nodeIn.TypeReference.Definition == DateTimeType) |
|||
{ |
|||
var value = Visit(nodeIn.Source); |
|||
|
|||
if (value.Value is DateTimeOffset dateTimeOffset) |
|||
{ |
|||
return (Instant.FromDateTimeOffset(dateTimeOffset), FilterValueType.Instant); |
|||
} |
|||
|
|||
if (value.Value is DateTime dateTime) |
|||
{ |
|||
return (Instant.FromDateTimeUtc(DateTime.SpecifyKind(dateTime, DateTimeKind.Utc)), FilterValueType.Instant); |
|||
} |
|||
|
|||
if (value.Value is Date date) |
|||
{ |
|||
return (Instant.FromUtc(date.Year, date.Month, date.Day, 0, 0), FilterValueType.Instant); |
|||
} |
|||
|
|||
var parseResult = InstantPattern.General.Parse(Visit(nodeIn.Source).ToString()); |
|||
|
|||
if (!parseResult.Success) |
|||
{ |
|||
throw new ODataException("Datetime is not in a valid format. Use ISO 8601"); |
|||
} |
|||
|
|||
return (parseResult.Value, FilterValueType.Instant); |
|||
} |
|||
|
|||
return base.Visit(nodeIn); |
|||
} |
|||
|
|||
public override (object Value, FilterValueType ValueType) Visit(ConstantNode nodeIn) |
|||
{ |
|||
if (nodeIn.TypeReference == BooleanType) |
|||
{ |
|||
return (nodeIn.Value, FilterValueType.Boolean); |
|||
} |
|||
|
|||
if (nodeIn.TypeReference == DoubleType) |
|||
{ |
|||
return (nodeIn.Value, FilterValueType.Double); |
|||
} |
|||
|
|||
if (nodeIn.TypeReference == Int32Type) |
|||
{ |
|||
return (nodeIn.Value, FilterValueType.Int32); |
|||
} |
|||
|
|||
if (nodeIn.TypeReference == Int32Type) |
|||
{ |
|||
return (nodeIn.Value, FilterValueType.Int64); |
|||
} |
|||
|
|||
if (nodeIn.TypeReference == StringType) |
|||
{ |
|||
return (nodeIn.Value, FilterValueType.String); |
|||
} |
|||
|
|||
throw new NotSupportedException(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,49 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Microsoft.OData; |
|||
using Microsoft.OData.UriParser; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Queries |
|||
{ |
|||
public static class FilterBuilder |
|||
{ |
|||
public static void ParseFilter(this ODataUriParser query, Query result) |
|||
{ |
|||
SearchClause search; |
|||
try |
|||
{ |
|||
search = query.ParseSearch(); |
|||
} |
|||
catch (ODataException ex) |
|||
{ |
|||
throw new ValidationException("Query $search clause not valid.", new ValidationError(ex.Message)); |
|||
} |
|||
|
|||
if (search != null) |
|||
{ |
|||
result.FullText = SearchTermVisitor.Visit(search.Expression).ToString(); |
|||
} |
|||
|
|||
FilterClause filter; |
|||
try |
|||
{ |
|||
filter = query.ParseFilter(); |
|||
} |
|||
catch (ODataException ex) |
|||
{ |
|||
throw new ValidationException("Query $filter clause not valid.", new ValidationError(ex.Message)); |
|||
} |
|||
|
|||
if (filter != null) |
|||
{ |
|||
result.Filter = FilterVisitor.Visit(filter.Expression); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,153 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Linq; |
|||
using Microsoft.OData.UriParser; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Queries |
|||
{ |
|||
public sealed class FilterVisitor : QueryNodeVisitor<FilterNode> |
|||
{ |
|||
private static readonly FilterVisitor Instance = new FilterVisitor(); |
|||
|
|||
private FilterVisitor() |
|||
{ |
|||
} |
|||
|
|||
public static FilterNode Visit(QueryNode node) |
|||
{ |
|||
return node.Accept(Instance); |
|||
} |
|||
|
|||
public override FilterNode Visit(ConvertNode nodeIn) |
|||
{ |
|||
return nodeIn.Source.Accept(this); |
|||
} |
|||
|
|||
public override FilterNode Visit(UnaryOperatorNode nodeIn) |
|||
{ |
|||
if (nodeIn.OperatorKind == UnaryOperatorKind.Not) |
|||
{ |
|||
return new FilterNegate(nodeIn.Operand.Accept(this)); |
|||
} |
|||
|
|||
throw new NotSupportedException(); |
|||
} |
|||
|
|||
public override FilterNode Visit(SingleValueFunctionCallNode nodeIn) |
|||
{ |
|||
var fieldNode = nodeIn.Parameters.ElementAt(0); |
|||
var valueNode = nodeIn.Parameters.ElementAt(1); |
|||
|
|||
if (string.Equals(nodeIn.Name, "endswith", StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
var (value, valueType) = ConstantVisitor.Visit(valueNode); |
|||
|
|||
return new FilterComparison(PropertyPathVisitor.Visit(fieldNode), FilterOperator.EndsWith, value, valueType); |
|||
} |
|||
|
|||
if (string.Equals(nodeIn.Name, "startswith", StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
var (value, valueType) = ConstantVisitor.Visit(valueNode); |
|||
|
|||
return new FilterComparison(PropertyPathVisitor.Visit(fieldNode), FilterOperator.StartsWith, value, valueType); |
|||
} |
|||
|
|||
if (string.Equals(nodeIn.Name, "contains", StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
var (value, valueType) = ConstantVisitor.Visit(valueNode); |
|||
|
|||
return new FilterComparison(PropertyPathVisitor.Visit(fieldNode), FilterOperator.Contains, value, valueType); |
|||
} |
|||
|
|||
throw new NotSupportedException(); |
|||
} |
|||
|
|||
public override FilterNode Visit(BinaryOperatorNode nodeIn) |
|||
{ |
|||
if (nodeIn.OperatorKind == BinaryOperatorKind.And) |
|||
{ |
|||
return new FilterJunction(FilterJunctionType.And, nodeIn.Left.Accept(this), nodeIn.Right.Accept(this)); |
|||
} |
|||
|
|||
if (nodeIn.OperatorKind == BinaryOperatorKind.Or) |
|||
{ |
|||
return new FilterJunction(FilterJunctionType.Or, nodeIn.Left.Accept(this), nodeIn.Right.Accept(this)); |
|||
} |
|||
|
|||
if (nodeIn.Left is SingleValueFunctionCallNode functionNode) |
|||
{ |
|||
var regexFilter = Visit(functionNode); |
|||
|
|||
var value = BuildValue(nodeIn.Right); |
|||
|
|||
if (value is bool booleanRight) |
|||
{ |
|||
if ((nodeIn.OperatorKind == BinaryOperatorKind.Equal && !booleanRight) || |
|||
(nodeIn.OperatorKind == BinaryOperatorKind.NotEqual && booleanRight)) |
|||
{ |
|||
regexFilter = new FilterNegate(regexFilter); |
|||
} |
|||
|
|||
return regexFilter; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
if (nodeIn.OperatorKind == BinaryOperatorKind.NotEqual) |
|||
{ |
|||
var (value, valueType) = ConstantVisitor.Visit(nodeIn.Left); |
|||
|
|||
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Right), FilterOperator.NotEquals, value, valueType); |
|||
} |
|||
|
|||
if (nodeIn.OperatorKind == BinaryOperatorKind.Equal) |
|||
{ |
|||
var (value, valueType) = ConstantVisitor.Visit(nodeIn.Left); |
|||
|
|||
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Right), FilterOperator.Equals, value, valueType); |
|||
} |
|||
|
|||
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThan) |
|||
{ |
|||
var (value, valueType) = ConstantVisitor.Visit(nodeIn.Left); |
|||
|
|||
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Right), FilterOperator.LessThan, value, valueType); |
|||
} |
|||
|
|||
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThanOrEqual) |
|||
{ |
|||
var (value, valueType) = ConstantVisitor.Visit(nodeIn.Left); |
|||
|
|||
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Right), FilterOperator.LessThanOrEqual, value, valueType); |
|||
} |
|||
|
|||
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThan) |
|||
{ |
|||
var (value, valueType) = ConstantVisitor.Visit(nodeIn.Left); |
|||
|
|||
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Right), FilterOperator.GreaterThan, value, valueType); |
|||
} |
|||
|
|||
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThanOrEqual) |
|||
{ |
|||
var (value, valueType) = ConstantVisitor.Visit(nodeIn.Left); |
|||
|
|||
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Right), FilterOperator.GreaterThanOrEqual, value, valueType); |
|||
} |
|||
} |
|||
|
|||
throw new NotSupportedException(); |
|||
} |
|||
|
|||
private object BuildValue(QueryNode nodeIn) |
|||
{ |
|||
return ConstantVisitor.Visit(nodeIn); |
|||
} |
|||
} |
|||
} |
|||
@ -1,34 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Microsoft.OData.UriParser; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Queries |
|||
{ |
|||
public static class LimitExtensions |
|||
{ |
|||
public static void ParseTake(this ODataUriParser query, Query result, int maxValue = int.MaxValue) |
|||
{ |
|||
var top = query.ParseTop(); |
|||
|
|||
if (top.HasValue) |
|||
{ |
|||
result.Take = top; |
|||
} |
|||
} |
|||
|
|||
public static void ParseSkip(this ODataUriParser query, Query result) |
|||
{ |
|||
var skip = query.ParseSkip(); |
|||
|
|||
if (skip.HasValue) |
|||
{ |
|||
result.Skip = skip; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,56 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Immutable; |
|||
using Microsoft.OData.UriParser; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Queries |
|||
{ |
|||
public sealed class PropertyPathVisitor : QueryNodeVisitor<ImmutableList<string>> |
|||
{ |
|||
private static readonly PropertyPathVisitor Instance = new PropertyPathVisitor(); |
|||
|
|||
private PropertyPathVisitor() |
|||
{ |
|||
} |
|||
|
|||
public static ImmutableList<string> Visit(QueryNode node) |
|||
{ |
|||
return node.Accept(Instance); |
|||
} |
|||
|
|||
public override ImmutableList<string> Visit(ConvertNode nodeIn) |
|||
{ |
|||
return nodeIn.Source.Accept(this); |
|||
} |
|||
|
|||
public override ImmutableList<string> Visit(SingleComplexNode nodeIn) |
|||
{ |
|||
if (nodeIn.Source is SingleComplexNode) |
|||
{ |
|||
return nodeIn.Source.Accept(this).Add(nodeIn.Property.Name.ToPascalCase()); |
|||
} |
|||
else |
|||
{ |
|||
return ImmutableList.Create(nodeIn.Property.Name.ToPascalCase()); |
|||
} |
|||
} |
|||
|
|||
public override ImmutableList<string> Visit(SingleValuePropertyAccessNode nodeIn) |
|||
{ |
|||
if (nodeIn.Source is SingleComplexNode) |
|||
{ |
|||
return nodeIn.Source.Accept(this).Add(nodeIn.Property.Name.ToPascalCase()); |
|||
} |
|||
else |
|||
{ |
|||
return ImmutableList.Create(nodeIn.Property.Name.ToPascalCase()); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,41 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Microsoft.OData.UriParser; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Queries |
|||
{ |
|||
public class SearchTermVisitor : QueryNodeVisitor<string> |
|||
{ |
|||
private static readonly SearchTermVisitor Instance = new SearchTermVisitor(); |
|||
|
|||
private SearchTermVisitor() |
|||
{ |
|||
} |
|||
|
|||
public static object Visit(QueryNode node) |
|||
{ |
|||
return node.Accept(Instance); |
|||
} |
|||
|
|||
public override string Visit(BinaryOperatorNode nodeIn) |
|||
{ |
|||
if (nodeIn.OperatorKind == BinaryOperatorKind.And) |
|||
{ |
|||
return nodeIn.Left.Accept(this) + " " + nodeIn.Right.Accept(this); |
|||
} |
|||
|
|||
throw new NotSupportedException(); |
|||
} |
|||
|
|||
public override string Visit(SearchTermNode nodeIn) |
|||
{ |
|||
return nodeIn.Text; |
|||
} |
|||
} |
|||
} |
|||
@ -1,43 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Microsoft.OData.UriParser; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Queries |
|||
{ |
|||
public static class SortBuilder |
|||
{ |
|||
public static void ParseSort(this ODataUriParser query, Query result) |
|||
{ |
|||
var orderBy = query.ParseOrderBy(); |
|||
|
|||
if (orderBy != null) |
|||
{ |
|||
while (orderBy != null) |
|||
{ |
|||
result.Sort.Add(OrderBy(orderBy)); |
|||
|
|||
orderBy = orderBy.ThenBy; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public static SortNode OrderBy(OrderByClause clause) |
|||
{ |
|||
var path = PropertyPathVisitor.Visit(clause.Expression); |
|||
|
|||
if (clause.Direction == OrderByDirection.Ascending) |
|||
{ |
|||
return new SortNode(path, SortOrder.Ascending); |
|||
} |
|||
else |
|||
{ |
|||
return new SortNode(path, SortOrder.Descending); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,24 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Queries |
|||
{ |
|||
public sealed class Query |
|||
{ |
|||
public FilterNode Filter { get; set; } |
|||
|
|||
public long? Skip { get; set; } |
|||
|
|||
public long? Take { get; set; } |
|||
|
|||
public List<SortNode> Sort { get; } = new List<SortNode>(); |
|||
|
|||
public string FullText { get; set; } |
|||
} |
|||
} |
|||
@ -1,30 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Queries |
|||
{ |
|||
public sealed class SortNode |
|||
{ |
|||
public IReadOnlyList<string> Path { get; } |
|||
|
|||
public SortOrder SortOrder { get; set; } |
|||
|
|||
public SortNode(IReadOnlyList<string> path, SortOrder sortOrder) |
|||
{ |
|||
Guard.NotNull(path, nameof(path)); |
|||
Guard.NotEmpty(path, nameof(path)); |
|||
Guard.Enum(sortOrder, nameof(sortOrder)); |
|||
|
|||
Path = path; |
|||
|
|||
SortOrder = sortOrder; |
|||
} |
|||
} |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Queries |
|||
{ |
|||
public enum SortOrder |
|||
{ |
|||
Ascending, |
|||
Descending |
|||
} |
|||
} |
|||
@ -1,38 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Linq; |
|||
using Microsoft.OData.Edm; |
|||
using Microsoft.OData.UriParser; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities |
|||
{ |
|||
public static class EdmModelExtensions |
|||
{ |
|||
public static ODataUriParser ParseQuery(this IEdmModel model, string query) |
|||
{ |
|||
if (!model.EntityContainer.EntitySets().Any()) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
query = query ?? string.Empty; |
|||
|
|||
var path = model.EntityContainer.EntitySets().First().Path.Path.Split('.').Last(); |
|||
|
|||
if (query.StartsWith("?", StringComparison.Ordinal)) |
|||
{ |
|||
query = query.Substring(1); |
|||
} |
|||
|
|||
var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative)); |
|||
|
|||
return parser; |
|||
} |
|||
} |
|||
} |
|||
@ -1,83 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Microsoft.OData; |
|||
using Microsoft.OData.Edm; |
|||
using Microsoft.OData.UriParser; |
|||
using NodaTime; |
|||
using NodaTime.Text; |
|||
|
|||
namespace Squidex.Infrastructure.MongoDb.OData |
|||
{ |
|||
public sealed class ConstantVisitor : QueryNodeVisitor<object> |
|||
{ |
|||
private static readonly IEdmPrimitiveType BooleanType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Boolean); |
|||
private static readonly IEdmPrimitiveType DateTimeType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset); |
|||
private static readonly IEdmPrimitiveType GuidType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Guid); |
|||
|
|||
private static readonly ConstantVisitor Instance = new ConstantVisitor(); |
|||
|
|||
private ConstantVisitor() |
|||
{ |
|||
} |
|||
|
|||
public static object Visit(QueryNode node) |
|||
{ |
|||
return node.Accept(Instance); |
|||
} |
|||
|
|||
public override object Visit(ConvertNode nodeIn) |
|||
{ |
|||
if (nodeIn.TypeReference.Definition == BooleanType) |
|||
{ |
|||
return bool.Parse(Visit(nodeIn.Source).ToString()); |
|||
} |
|||
|
|||
if (nodeIn.TypeReference.Definition == GuidType) |
|||
{ |
|||
return Guid.Parse(Visit(nodeIn.Source).ToString()); |
|||
} |
|||
|
|||
if (nodeIn.TypeReference.Definition == DateTimeType) |
|||
{ |
|||
var value = Visit(nodeIn.Source); |
|||
|
|||
if (value is DateTimeOffset dateTimeOffset) |
|||
{ |
|||
return Instant.FromDateTimeOffset(dateTimeOffset); |
|||
} |
|||
|
|||
if (value is DateTime dateTime) |
|||
{ |
|||
return Instant.FromDateTimeUtc(DateTime.SpecifyKind(dateTime, DateTimeKind.Utc)); |
|||
} |
|||
|
|||
if (value is Date date) |
|||
{ |
|||
return Instant.FromUtc(date.Year, date.Month, date.Day, 0, 0); |
|||
} |
|||
|
|||
var parseResult = InstantPattern.General.Parse(Visit(nodeIn.Source).ToString()); |
|||
|
|||
if (!parseResult.Success) |
|||
{ |
|||
throw new ODataException("Datetime is not in a valid format. Use ISO 8601"); |
|||
} |
|||
|
|||
return parseResult.Value; |
|||
} |
|||
|
|||
return base.Visit(nodeIn); |
|||
} |
|||
|
|||
public override object Visit(ConstantNode nodeIn) |
|||
{ |
|||
return nodeIn.Value; |
|||
} |
|||
} |
|||
} |
|||
@ -1,32 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Linq; |
|||
using Microsoft.OData.UriParser; |
|||
|
|||
namespace Squidex.Infrastructure.MongoDb.OData |
|||
{ |
|||
public delegate string ConvertProperty(string[] parts); |
|||
|
|||
public static class PropertyBuilder |
|||
{ |
|||
private static readonly ConvertProperty Default = parts => |
|||
{ |
|||
return string.Join(".", parts).ToPascalCase(); |
|||
}; |
|||
|
|||
public static string BuildFieldDefinition(this QueryNode node, ConvertProperty convertProperty) |
|||
{ |
|||
convertProperty = convertProperty ?? Default; |
|||
|
|||
var propertyParts = node.Accept(PropertyNameVisitor.Instance).ToArray(); |
|||
var propertyName = convertProperty(propertyParts); |
|||
|
|||
return propertyName; |
|||
} |
|||
} |
|||
} |
|||
@ -1,50 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Immutable; |
|||
using Microsoft.OData.UriParser; |
|||
|
|||
namespace Squidex.Infrastructure.MongoDb.OData |
|||
{ |
|||
public sealed class PropertyNameVisitor : QueryNodeVisitor<ImmutableList<string>> |
|||
{ |
|||
public static readonly PropertyNameVisitor Instance = new PropertyNameVisitor(); |
|||
|
|||
private PropertyNameVisitor() |
|||
{ |
|||
} |
|||
|
|||
public override ImmutableList<string> Visit(ConvertNode nodeIn) |
|||
{ |
|||
return nodeIn.Source.Accept(this); |
|||
} |
|||
|
|||
public override ImmutableList<string> Visit(SingleComplexNode nodeIn) |
|||
{ |
|||
if (nodeIn.Source is SingleComplexNode) |
|||
{ |
|||
return nodeIn.Source.Accept(this).Add(nodeIn.Property.Name); |
|||
} |
|||
else |
|||
{ |
|||
return ImmutableList.Create(nodeIn.Property.Name); |
|||
} |
|||
} |
|||
|
|||
public override ImmutableList<string> Visit(SingleValuePropertyAccessNode nodeIn) |
|||
{ |
|||
if (nodeIn.Source is SingleComplexNode) |
|||
{ |
|||
return nodeIn.Source.Accept(this).Add(nodeIn.Property.Name); |
|||
} |
|||
else |
|||
{ |
|||
return ImmutableList.Create(nodeIn.Property.Name); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,41 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Microsoft.OData.UriParser; |
|||
|
|||
namespace Squidex.Infrastructure.MongoDb.OData |
|||
{ |
|||
public class SearchTermVisitor : QueryNodeVisitor<string> |
|||
{ |
|||
private static readonly SearchTermVisitor Instance = new SearchTermVisitor(); |
|||
|
|||
private SearchTermVisitor() |
|||
{ |
|||
} |
|||
|
|||
public static object Visit(QueryNode node) |
|||
{ |
|||
return node.Accept(Instance); |
|||
} |
|||
|
|||
public override string Visit(BinaryOperatorNode nodeIn) |
|||
{ |
|||
if (nodeIn.OperatorKind == BinaryOperatorKind.And) |
|||
{ |
|||
return nodeIn.Left.Accept(this) + " " + nodeIn.Right.Accept(this); |
|||
} |
|||
|
|||
throw new NotSupportedException(); |
|||
} |
|||
|
|||
public override string Visit(SearchTermNode nodeIn) |
|||
{ |
|||
return nodeIn.Text; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Infrastructure.Queries |
|||
{ |
|||
public static class FilterBuilder |
|||
{ |
|||
public static FilterJunction And(params FilterNode[] operands) |
|||
{ |
|||
return new FilterJunction(FilterJunctionType.And, operands); |
|||
} |
|||
|
|||
public static FilterJunction Or(params FilterNode[] operands) |
|||
{ |
|||
return new FilterJunction(FilterJunctionType.Or, operands); |
|||
} |
|||
|
|||
public static FilterComparison Eq(string path, object value) |
|||
{ |
|||
return Binary(path, FilterOperator.Equals, value); |
|||
} |
|||
|
|||
private static FilterComparison Binary(string path, FilterOperator @operator, object value) |
|||
{ |
|||
return new FilterComparison(path.Split('.', '/'), @operator, value, FilterValueType.String); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,267 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Immutable; |
|||
using System.Linq; |
|||
using FakeItEasy; |
|||
using Microsoft.Extensions.Caching.Memory; |
|||
using Microsoft.Extensions.Options; |
|||
using Microsoft.OData.Edm; |
|||
using MongoDB.Bson.Serialization; |
|||
using MongoDB.Driver; |
|||
using NodaTime.Text; |
|||
using Squidex.Domain.Apps.Core; |
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Entities.Apps; |
|||
using Squidex.Domain.Apps.Entities.Contents.Edm; |
|||
using Squidex.Domain.Apps.Entities.MongoDb.Contents; |
|||
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.MongoDb; |
|||
using Squidex.Infrastructure.MongoDb.OData; |
|||
using Squidex.Infrastructure.Queries; |
|||
using Xunit; |
|||
using FilterBuilder = Squidex.Infrastructure.Queries.FilterBuilder; |
|||
using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.MongoDb |
|||
{ |
|||
public class MongoDbQueryTests |
|||
{ |
|||
private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry; |
|||
private static readonly IBsonSerializer<MongoContentEntity> Serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>(); |
|||
private readonly Schema schemaDef; |
|||
private readonly IEdmModel edmModel; |
|||
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); |
|||
|
|||
static MongoDbQueryTests() |
|||
{ |
|||
InstantSerializer.Register(); |
|||
} |
|||
|
|||
public MongoDbQueryTests() |
|||
{ |
|||
schemaDef = |
|||
new Schema("user") |
|||
.AddString(1, "firstName", Partitioning.Language, |
|||
new StringFieldProperties { Label = "FirstName", IsRequired = true, AllowedValues = ImmutableList.Create("1", "2") }) |
|||
.AddString(2, "lastName", Partitioning.Language, |
|||
new StringFieldProperties { Hints = "Last Name", Editor = StringFieldEditor.Input }) |
|||
.AddBoolean(3, "isAdmin", Partitioning.Invariant, |
|||
new BooleanFieldProperties()) |
|||
.AddNumber(4, "age", Partitioning.Invariant, |
|||
new NumberFieldProperties { MinValue = 1, MaxValue = 10 }) |
|||
.AddDateTime(5, "birthday", Partitioning.Invariant, |
|||
new DateTimeFieldProperties()) |
|||
.AddAssets(6, "pictures", Partitioning.Invariant, |
|||
new AssetsFieldProperties()) |
|||
.AddReferences(7, "friends", Partitioning.Invariant, |
|||
new ReferencesFieldProperties()) |
|||
.AddString(8, "dashed-field", Partitioning.Invariant, |
|||
new StringFieldProperties()) |
|||
.Update(new SchemaProperties { Hints = "The User" }); |
|||
|
|||
var builder = new EdmModelBuilder(new MemoryCache(Options.Create(new MemoryCacheOptions()))); |
|||
|
|||
var schema = A.Dummy<ISchemaEntity>(); |
|||
A.CallTo(() => schema.Id).Returns(Guid.NewGuid()); |
|||
A.CallTo(() => schema.Version).Returns(3); |
|||
A.CallTo(() => schema.SchemaDef).Returns(schemaDef); |
|||
|
|||
var app = A.Dummy<IAppEntity>(); |
|||
A.CallTo(() => app.Id).Returns(Guid.NewGuid()); |
|||
A.CallTo(() => app.Version).Returns(3); |
|||
A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); |
|||
|
|||
edmModel = builder.BuildEdmModel(schema, app); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_throw_exception_for_invalid_field() |
|||
{ |
|||
Assert.Throws<NotSupportedException>(() => F(FilterBuilder.Eq("data/invalid/iv", "Me"))); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_lastModified() |
|||
{ |
|||
var i = F(FilterBuilder.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); |
|||
var o = C("{ 'mt' : '1988-01-19T12:00:00Z' }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_lastModifiedBy() |
|||
{ |
|||
var i = F(FilterBuilder.Eq("lastModifiedBy", "Me")); |
|||
var o = C("{ 'mb' : 'Me' }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_created() |
|||
{ |
|||
var i = F(FilterBuilder.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); |
|||
var o = C("{ 'ct' : '1988-01-19T12:00:00Z' }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_createdBy() |
|||
{ |
|||
var i = F(FilterBuilder.Eq("createdBy", "Me")); |
|||
var o = C("{ 'cb' : 'Me' }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_version() |
|||
{ |
|||
var i = F(FilterBuilder.Eq("version", 0L)); |
|||
var o = C("{ 'vs' : NumberLong(0) }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_from_draft() |
|||
{ |
|||
var i = F(FilterBuilder.Eq("data/dashed_field/iv", "Value"), true); |
|||
var o = C("{ 'dd.8.iv' : 'Value' }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_date_field_created() |
|||
{ |
|||
var i = F(FilterBuilder.Eq("data/birthday/iv", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); |
|||
var o = C("{ 'do.5.iv' : '1988-01-19T12:00:00Z' }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_underscore_field() |
|||
{ |
|||
var i = F(FilterBuilder.Eq("data/dashed_field/iv", "Value")); |
|||
var o = C("{ 'do.8.iv' : 'Value' }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_references_equals() |
|||
{ |
|||
var i = F(FilterBuilder.Eq("data/friends/iv", "guid")); |
|||
var o = C("{ 'do.7.iv' : 'guid' }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_assets_equals() |
|||
{ |
|||
var i = F(FilterBuilder.Eq("data/pictures/iv", "guid")); |
|||
var o = C("{ 'do.6.iv' : 'guid' }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_full_text() |
|||
{ |
|||
var i = Q(new Query { FullText = "Hello my World" }); |
|||
var o = C("{ '$text' : { '$search' : 'Hello my World' } }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_orderby_with_single_field() |
|||
{ |
|||
var i = S(SortBuilder.Descending("data/age/iv")); |
|||
var o = C("{ 'do.4.iv' : -1 }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_orderby_with_multiple_fields() |
|||
{ |
|||
var i = S(SortBuilder.Ascending("data/age/iv"), SortBuilder.Descending("data/firstName/en")); |
|||
var o = C("{ 'do.4.iv' : 1, 'do.1.en' : -1 }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_take_statement() |
|||
{ |
|||
var query = new Query { Take = 3 }; |
|||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|||
|
|||
cursor.ContentTake(query.AdjustToModel(schemaDef, false)); |
|||
|
|||
A.CallTo(() => cursor.Limit(3)).MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_skip_statement() |
|||
{ |
|||
var query = new Query { Skip = 3 }; |
|||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|||
|
|||
cursor.ContentSkip(query.AdjustToModel(schemaDef, false)); |
|||
|
|||
A.CallTo(() => cursor.Skip(3)).MustHaveHappened(); |
|||
} |
|||
|
|||
private static string C(string value) |
|||
{ |
|||
return value.Replace('\'', '"'); |
|||
} |
|||
|
|||
private string F(FilterNode filter, bool useDraft = false) |
|||
{ |
|||
return Q(new Query { Filter = filter }, useDraft); |
|||
} |
|||
|
|||
private string S(params SortNode[] sorts) |
|||
{ |
|||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|||
|
|||
var i = string.Empty; |
|||
|
|||
A.CallTo(() => cursor.Sort(A<SortDefinition<MongoContentEntity>>.Ignored)) |
|||
.Invokes((SortDefinition<MongoContentEntity> sortDefinition) => |
|||
{ |
|||
i = sortDefinition.Render(Serializer, Registry).ToString(); |
|||
}); |
|||
|
|||
cursor.ContentSort(new Query { Sort = sorts.ToList() }.AdjustToModel(schemaDef, false)); |
|||
|
|||
return i; |
|||
} |
|||
|
|||
private string Q(Query query, bool useDraft = false) |
|||
{ |
|||
var rendered = |
|||
query.AdjustToModel(schemaDef, useDraft).BuildFilter<MongoContentEntity>().Filter |
|||
.Render(Serializer, Registry).ToString(); |
|||
|
|||
return rendered; |
|||
} |
|||
} |
|||
} |
|||
@ -1,448 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Immutable; |
|||
using FakeItEasy; |
|||
using Microsoft.Extensions.Caching.Memory; |
|||
using Microsoft.Extensions.Options; |
|||
using Microsoft.OData.Edm; |
|||
using MongoDB.Bson.Serialization; |
|||
using MongoDB.Driver; |
|||
using Squidex.Domain.Apps.Core; |
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Entities.Apps; |
|||
using Squidex.Domain.Apps.Entities.Contents.Edm; |
|||
using Squidex.Domain.Apps.Entities.MongoDb.Contents; |
|||
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.MongoDb; |
|||
using Squidex.Infrastructure.MongoDb.OData; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.OData |
|||
{ |
|||
public class ODataQueryTests |
|||
{ |
|||
private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry; |
|||
private static readonly IBsonSerializer<MongoContentEntity> Serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>(); |
|||
private readonly Schema schemaDef; |
|||
private readonly IEdmModel edmModel; |
|||
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); |
|||
|
|||
static ODataQueryTests() |
|||
{ |
|||
InstantSerializer.Register(); |
|||
} |
|||
|
|||
public ODataQueryTests() |
|||
{ |
|||
schemaDef = |
|||
new Schema("user") |
|||
.AddString(1, "firstName", Partitioning.Language, |
|||
new StringFieldProperties { Label = "FirstName", IsRequired = true, AllowedValues = ImmutableList.Create("1", "2") }) |
|||
.AddString(2, "lastName", Partitioning.Language, |
|||
new StringFieldProperties { Hints = "Last Name", Editor = StringFieldEditor.Input }) |
|||
.AddBoolean(3, "isAdmin", Partitioning.Invariant, |
|||
new BooleanFieldProperties()) |
|||
.AddNumber(4, "age", Partitioning.Invariant, |
|||
new NumberFieldProperties { MinValue = 1, MaxValue = 10 }) |
|||
.AddDateTime(5, "birthday", Partitioning.Invariant, |
|||
new DateTimeFieldProperties()) |
|||
.AddAssets(6, "pictures", Partitioning.Invariant, |
|||
new AssetsFieldProperties()) |
|||
.AddReferences(7, "friends", Partitioning.Invariant, |
|||
new ReferencesFieldProperties()) |
|||
.AddString(8, "dashed-field", Partitioning.Invariant, |
|||
new StringFieldProperties()) |
|||
.Update(new SchemaProperties { Hints = "The User" }); |
|||
|
|||
var builder = new EdmModelBuilder(new MemoryCache(Options.Create(new MemoryCacheOptions()))); |
|||
|
|||
var schema = A.Dummy<ISchemaEntity>(); |
|||
A.CallTo(() => schema.Id).Returns(Guid.NewGuid()); |
|||
A.CallTo(() => schema.Version).Returns(3); |
|||
A.CallTo(() => schema.SchemaDef).Returns(schemaDef); |
|||
|
|||
var app = A.Dummy<IAppEntity>(); |
|||
A.CallTo(() => app.Id).Returns(Guid.NewGuid()); |
|||
A.CallTo(() => app.Version).Returns(3); |
|||
A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); |
|||
|
|||
edmModel = builder.BuildEdmModel(schema, app); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_query() |
|||
{ |
|||
var parser = edmModel.ParseQuery("$filter=data/firstName/de eq 'Sebastian'"); |
|||
|
|||
Assert.NotNull(parser); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_created() |
|||
{ |
|||
var i = F("$filter=created eq 1988-01-19T12:00:00Z"); |
|||
var o = C("{ 'ct' : ISODate('1988-01-19T12:00:00Z') }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_created_and_date() |
|||
{ |
|||
var i = F("$filter=created eq 1988-01-19"); |
|||
var o = C("{ 'ct' : ISODate('1988-01-19T00:00:00Z') }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_createdBy() |
|||
{ |
|||
var i = F("$filter=createdBy eq 'Me'"); |
|||
var o = C("{ 'cb' : 'Me' }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_lastModified() |
|||
{ |
|||
var i = F("$filter=lastModified eq 1988-01-19T12:00:00Z"); |
|||
var o = C("{ 'mt' : ISODate('1988-01-19T12:00:00Z') }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_lastModifiedBy() |
|||
{ |
|||
var i = F("$filter=lastModifiedBy eq 'Me'"); |
|||
var o = C("{ 'mb' : 'Me' }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_version() |
|||
{ |
|||
var i = F("$filter=version eq 0"); |
|||
var o = C("{ 'vs' : 0 }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_underscore_field() |
|||
{ |
|||
var i = F("$filter=data/dashed_field/iv eq 'Value'"); |
|||
var o = C("{ 'do.8.iv' : 'Value' }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_not() |
|||
{ |
|||
var i = F("$filter=not endswith(data/firstName/de, 'Sebastian')"); |
|||
var o = C("{ 'do.1.de' : { '$not' : /Sebastian$/i } }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_startswith() |
|||
{ |
|||
var i = F("$filter=startswith(data/firstName/de, 'Sebastian')"); |
|||
var o = C("{ 'do.1.de' : /^Sebastian/i }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_endswith() |
|||
{ |
|||
var i = F("$filter=endswith(data/firstName/de, 'Sebastian')"); |
|||
var o = C("{ 'do.1.de' : /Sebastian$/i }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_cointains() |
|||
{ |
|||
var i = F("$filter=contains(data/firstName/de, 'Sebastian')"); |
|||
var o = C("{ 'do.1.de' : /Sebastian/i }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_equals() |
|||
{ |
|||
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq true"); |
|||
var o = C("{ 'do.1.de' : /Sebastian/i }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_wih_equals_to_false() |
|||
{ |
|||
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq false"); |
|||
var o = C("{ 'do.1.de' : { '$not' : /Sebastian/i } }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_conjunction_and_contains() |
|||
{ |
|||
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq false and data/isAdmin/iv eq true"); |
|||
var o = C("{ 'do.1.de' : { '$not' : /Sebastian/i }, 'do.3.iv' : true }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_string_equals() |
|||
{ |
|||
var i = F("$filter=data/firstName/de eq 'Sebastian'"); |
|||
var o = C("{ 'do.1.de' : 'Sebastian' }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_datetime_equals() |
|||
{ |
|||
var i = F("$filter=data/birthday/iv eq 1988-01-19T12:00:00Z"); |
|||
var o = C("{ 'do.5.iv' : '1988-01-19T12:00:00Z' }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_boolean_equals() |
|||
{ |
|||
var i = F("$filter=data/isAdmin/iv eq true"); |
|||
var o = C("{ 'do.3.iv' : true }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_notequals() |
|||
{ |
|||
var i = F("$filter=data/firstName/de ne 'Sebastian'"); |
|||
var o = C("{ '$or' : [{ 'do.1.de' : { '$exists' : false } }, { 'do.1.de' : { '$ne' : 'Sebastian' } }] }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_lessthan() |
|||
{ |
|||
var i = F("$filter=data/age/iv lt 1"); |
|||
var o = C("{ 'do.4.iv' : { '$lt' : 1.0 } }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_lessequals() |
|||
{ |
|||
var i = F("$filter=data/age/iv le 1"); |
|||
var o = C("{ 'do.4.iv' : { '$lte' : 1.0 } }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_greaterthan() |
|||
{ |
|||
var i = F("$filter=data/age/iv gt 1"); |
|||
var o = C("{ 'do.4.iv' : { '$gt' : 1.0 } }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_greaterequals() |
|||
{ |
|||
var i = F("$filter=data/age/iv ge 1"); |
|||
var o = C("{ 'do.4.iv' : { '$gte' : 1.0 } }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_references_equals() |
|||
{ |
|||
var i = F("$filter=data/pictures/iv eq 'guid'"); |
|||
var o = C("{ 'do.6.iv' : 'guid' }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_assets_equals() |
|||
{ |
|||
var i = F("$filter=data/friends/iv eq 'guid'"); |
|||
var o = C("{ 'do.7.iv' : 'guid' }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_conjunction() |
|||
{ |
|||
var i = F("$filter=data/age/iv eq 1 and data/age/iv eq 2"); |
|||
var o = C("{ '$and' : [{ 'do.4.iv' : 1.0 }, { 'do.4.iv' : 2.0 }] }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_disjunction() |
|||
{ |
|||
var i = F("$filter=data/age/iv eq 1 or data/age/iv eq 2"); |
|||
var o = C("{ '$or' : [{ 'do.4.iv' : 1.0 }, { 'do.4.iv' : 2.0 }] }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_full_text() |
|||
{ |
|||
var i = F("$search=Hello my World"); |
|||
var o = C("{ '$text' : { '$search' : 'Hello my World' } }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_full_text_and_multiple_terms() |
|||
{ |
|||
var i = F("$search=A and B"); |
|||
var o = C("{ '$text' : { '$search' : 'A and B' } }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_orderby_with_single_field() |
|||
{ |
|||
var i = S("$orderby=data/age/iv desc"); |
|||
var o = C("{ 'do.4.iv' : -1 }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_orderby_with_multiple_field() |
|||
{ |
|||
var i = S("$orderby=data/age/iv, data/firstName/en desc"); |
|||
var o = C("{ 'do.4.iv' : 1, 'do.1.en' : -1 }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_top_statement() |
|||
{ |
|||
var parser = edmModel.ParseQuery("$top=3"); |
|||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|||
|
|||
cursor.ContentTake(parser); |
|||
|
|||
A.CallTo(() => cursor.Limit(3)).MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_top_statement_with_limit() |
|||
{ |
|||
var parser = edmModel.ParseQuery("$top=300"); |
|||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|||
|
|||
cursor.ContentTake(parser); |
|||
|
|||
A.CallTo(() => cursor.Limit(200)).MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_top_statement_with_default_value() |
|||
{ |
|||
var parser = edmModel.ParseQuery(string.Empty); |
|||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|||
|
|||
cursor.ContentTake(parser); |
|||
|
|||
A.CallTo(() => cursor.Limit(20)).MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_skip_statement() |
|||
{ |
|||
var parser = edmModel.ParseQuery("$skip=3"); |
|||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|||
|
|||
cursor.ContentSkip(parser); |
|||
|
|||
A.CallTo(() => cursor.Skip(3)).MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_skip_statement_with_default_value() |
|||
{ |
|||
var parser = edmModel.ParseQuery(string.Empty); |
|||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|||
|
|||
cursor.ContentSkip(parser); |
|||
|
|||
A.CallTo(() => cursor.Skip(A<int>.Ignored)).MustNotHaveHappened(); |
|||
} |
|||
|
|||
private static string C(string value) |
|||
{ |
|||
return value.Replace('\'', '"'); |
|||
} |
|||
|
|||
private string S(string value) |
|||
{ |
|||
var parser = edmModel.ParseQuery(value); |
|||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|||
|
|||
var i = string.Empty; |
|||
|
|||
A.CallTo(() => cursor.Sort(A<SortDefinition<MongoContentEntity>>.Ignored)) |
|||
.Invokes((SortDefinition<MongoContentEntity> sortDefinition) => |
|||
{ |
|||
i = sortDefinition.Render(Serializer, Registry).ToString(); |
|||
}); |
|||
|
|||
cursor.ContentSort(parser, FindExtensions.CreatePropertyCalculator(schemaDef, false)); |
|||
|
|||
return i; |
|||
} |
|||
|
|||
private string F(string value) |
|||
{ |
|||
var parser = edmModel.ParseQuery(value); |
|||
|
|||
var query = |
|||
parser.BuildFilter<MongoContentEntity>(FindExtensions.CreatePropertyCalculator(schemaDef, false), FindExtensions.ValueConverter) |
|||
.Filter.Render(Serializer, Registry).ToString(); |
|||
|
|||
return query; |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue