mirror of https://github.com/Squidex/squidex.git
73 changed files with 2358 additions and 93 deletions
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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)})"; |
|||
} |
|||
} |
|||
} |
|||
@ -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}"; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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, |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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()); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
} |
|||
@ -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}"; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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)})"; |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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})"; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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, |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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}"; |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
} |
|||
@ -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…
Reference in new issue