mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
65 changed files with 1030 additions and 618 deletions
@ -0,0 +1,87 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.Queries |
||||
|
{ |
||||
|
public static class ClrFilter |
||||
|
{ |
||||
|
public static LogicalFilter<ClrValue> And(params FilterNode<ClrValue>[] filters) |
||||
|
{ |
||||
|
return new LogicalFilter<ClrValue>(LogicalFilterType.And, filters); |
||||
|
} |
||||
|
|
||||
|
public static LogicalFilter<ClrValue> Or(params FilterNode<ClrValue>[] filters) |
||||
|
{ |
||||
|
return new LogicalFilter<ClrValue>(LogicalFilterType.Or, filters); |
||||
|
} |
||||
|
|
||||
|
public static NegateFilter<ClrValue> Not(FilterNode<ClrValue> filter) |
||||
|
{ |
||||
|
return new NegateFilter<ClrValue>(filter); |
||||
|
} |
||||
|
|
||||
|
public static CompareFilter<ClrValue> Eq(PropertyPath path, ClrValue value) |
||||
|
{ |
||||
|
return Binary(path, CompareOperator.Equals, value); |
||||
|
} |
||||
|
|
||||
|
public static CompareFilter<ClrValue> Ne(PropertyPath path, ClrValue value) |
||||
|
{ |
||||
|
return Binary(path, CompareOperator.NotEquals, value); |
||||
|
} |
||||
|
|
||||
|
public static CompareFilter<ClrValue> Lt(PropertyPath path, ClrValue value) |
||||
|
{ |
||||
|
return Binary(path, CompareOperator.LessThan, value); |
||||
|
} |
||||
|
|
||||
|
public static CompareFilter<ClrValue> Le(PropertyPath path, ClrValue value) |
||||
|
{ |
||||
|
return Binary(path, CompareOperator.LessThanOrEqual, value); |
||||
|
} |
||||
|
|
||||
|
public static CompareFilter<ClrValue> Gt(PropertyPath path, ClrValue value) |
||||
|
{ |
||||
|
return Binary(path, CompareOperator.GreaterThan, value); |
||||
|
} |
||||
|
|
||||
|
public static CompareFilter<ClrValue> Ge(PropertyPath path, ClrValue value) |
||||
|
{ |
||||
|
return Binary(path, CompareOperator.GreaterThanOrEqual, value); |
||||
|
} |
||||
|
|
||||
|
public static CompareFilter<ClrValue> Contains(PropertyPath path, ClrValue value) |
||||
|
{ |
||||
|
return Binary(path, CompareOperator.Contains, value); |
||||
|
} |
||||
|
|
||||
|
public static CompareFilter<ClrValue> EndsWith(PropertyPath path, ClrValue value) |
||||
|
{ |
||||
|
return Binary(path, CompareOperator.EndsWith, value); |
||||
|
} |
||||
|
|
||||
|
public static CompareFilter<ClrValue> StartsWith(PropertyPath path, ClrValue value) |
||||
|
{ |
||||
|
return Binary(path, CompareOperator.StartsWith, value); |
||||
|
} |
||||
|
|
||||
|
public static CompareFilter<ClrValue> Empty(PropertyPath path) |
||||
|
{ |
||||
|
return Binary(path, CompareOperator.Empty, null); |
||||
|
} |
||||
|
|
||||
|
public static CompareFilter<ClrValue> In(PropertyPath path, ClrValue value) |
||||
|
{ |
||||
|
return Binary(path, CompareOperator.In, value); |
||||
|
} |
||||
|
|
||||
|
private static CompareFilter<ClrValue> Binary(PropertyPath path, CompareOperator @operator, ClrValue value) |
||||
|
{ |
||||
|
return new CompareFilter<ClrValue>(path, @operator, value ?? ClrValue.Null); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.Queries |
||||
|
{ |
||||
|
public sealed class ClrQuery : Query<ClrValue> |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,140 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Globalization; |
||||
|
using System.Linq; |
||||
|
using NodaTime; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Queries |
||||
|
{ |
||||
|
public sealed class ClrValue |
||||
|
{ |
||||
|
public static readonly ClrValue Null = new ClrValue(null, ClrValueType.Null, false); |
||||
|
|
||||
|
public object Value { get; } |
||||
|
|
||||
|
public ClrValueType ValueType { get; } |
||||
|
|
||||
|
public bool IsList { get; } |
||||
|
|
||||
|
private ClrValue(object value, ClrValueType valueType, bool isList) |
||||
|
{ |
||||
|
Value = value; |
||||
|
ValueType = valueType; |
||||
|
|
||||
|
IsList = isList; |
||||
|
} |
||||
|
|
||||
|
public static implicit operator ClrValue(Instant value) |
||||
|
{ |
||||
|
return new ClrValue(value, ClrValueType.Instant, false); |
||||
|
} |
||||
|
|
||||
|
public static implicit operator ClrValue(Guid value) |
||||
|
{ |
||||
|
return new ClrValue(value, ClrValueType.Guid, false); |
||||
|
} |
||||
|
|
||||
|
public static implicit operator ClrValue(bool value) |
||||
|
{ |
||||
|
return new ClrValue(value, ClrValueType.Boolean, false); |
||||
|
} |
||||
|
|
||||
|
public static implicit operator ClrValue(float value) |
||||
|
{ |
||||
|
return new ClrValue(value, ClrValueType.Single, false); |
||||
|
} |
||||
|
|
||||
|
public static implicit operator ClrValue(double value) |
||||
|
{ |
||||
|
return new ClrValue(value, ClrValueType.Double, false); |
||||
|
} |
||||
|
|
||||
|
public static implicit operator ClrValue(int value) |
||||
|
{ |
||||
|
return new ClrValue(value, ClrValueType.Int32, false); |
||||
|
} |
||||
|
|
||||
|
public static implicit operator ClrValue(long value) |
||||
|
{ |
||||
|
return new ClrValue(value, ClrValueType.Int64, false); |
||||
|
} |
||||
|
|
||||
|
public static implicit operator ClrValue(string value) |
||||
|
{ |
||||
|
return value != null ? new ClrValue(value, ClrValueType.String, false) : Null; |
||||
|
} |
||||
|
|
||||
|
public static implicit operator ClrValue(List<Instant> value) |
||||
|
{ |
||||
|
return value != null ? new ClrValue(value, ClrValueType.Instant, true) : Null; |
||||
|
} |
||||
|
|
||||
|
public static implicit operator ClrValue(List<Guid> value) |
||||
|
{ |
||||
|
return value != null ? new ClrValue(value, ClrValueType.Guid, true) : Null; |
||||
|
} |
||||
|
|
||||
|
public static implicit operator ClrValue(List<bool> value) |
||||
|
{ |
||||
|
return value != null ? new ClrValue(value, ClrValueType.Boolean, true) : Null; |
||||
|
} |
||||
|
|
||||
|
public static implicit operator ClrValue(List<float> value) |
||||
|
{ |
||||
|
return value != null ? new ClrValue(value, ClrValueType.Single, true) : Null; |
||||
|
} |
||||
|
|
||||
|
public static implicit operator ClrValue(List<double> value) |
||||
|
{ |
||||
|
return value != null ? new ClrValue(value, ClrValueType.Double, true) : Null; |
||||
|
} |
||||
|
|
||||
|
public static implicit operator ClrValue(List<int> value) |
||||
|
{ |
||||
|
return value != null ? new ClrValue(value, ClrValueType.Int32, true) : Null; |
||||
|
} |
||||
|
|
||||
|
public static implicit operator ClrValue(List<long> value) |
||||
|
{ |
||||
|
return value != null ? new ClrValue(value, ClrValueType.Int64, true) : Null; |
||||
|
} |
||||
|
|
||||
|
public static implicit operator ClrValue(List<string> value) |
||||
|
{ |
||||
|
return value != null ? new ClrValue(value, ClrValueType.String, true) : Null; |
||||
|
} |
||||
|
|
||||
|
public override string ToString() |
||||
|
{ |
||||
|
if (Value is IList list) |
||||
|
{ |
||||
|
return $"[{string.Join(", ", list.OfType<object>().Select(ToString).ToArray())}]"; |
||||
|
} |
||||
|
|
||||
|
return ToString(Value); |
||||
|
} |
||||
|
|
||||
|
private static string ToString(object value) |
||||
|
{ |
||||
|
if (value == null) |
||||
|
{ |
||||
|
return "null"; |
||||
|
} |
||||
|
|
||||
|
if (value is string s) |
||||
|
{ |
||||
|
return $"'{s.Replace("'", "\\'")}'"; |
||||
|
} |
||||
|
|
||||
|
return string.Format(CultureInfo.InvariantCulture, "{0}", value); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.Queries |
||||
|
{ |
||||
|
public sealed class CompareFilter<TValue> : FilterNode<TValue> |
||||
|
{ |
||||
|
public PropertyPath Path { get; } |
||||
|
|
||||
|
public CompareOperator Operator { get; } |
||||
|
|
||||
|
public TValue Value { get; } |
||||
|
|
||||
|
public CompareFilter(PropertyPath path, CompareOperator @operator, TValue value) |
||||
|
{ |
||||
|
Guard.NotNull(path, nameof(path)); |
||||
|
Guard.NotNull(value, nameof(value)); |
||||
|
Guard.Enum(@operator, nameof(@operator)); |
||||
|
|
||||
|
Path = path; |
||||
|
|
||||
|
Operator = @operator; |
||||
|
|
||||
|
Value = value; |
||||
|
} |
||||
|
|
||||
|
public override T Accept<T>(FilterNodeVisitor<T, TValue> visitor) |
||||
|
{ |
||||
|
return visitor.Visit(this); |
||||
|
} |
||||
|
|
||||
|
public override string ToString() |
||||
|
{ |
||||
|
switch (Operator) |
||||
|
{ |
||||
|
case CompareOperator.Contains: |
||||
|
return $"contains({Path}, {Value})"; |
||||
|
case CompareOperator.Empty: |
||||
|
return $"empty({Path})"; |
||||
|
case CompareOperator.EndsWith: |
||||
|
return $"endsWith({Path}, {Value})"; |
||||
|
case CompareOperator.StartsWith: |
||||
|
return $"startsWith({Path}, {Value})"; |
||||
|
case CompareOperator.Equals: |
||||
|
return $"{Path} == {Value}"; |
||||
|
case CompareOperator.NotEquals: |
||||
|
return $"{Path} != {Value}"; |
||||
|
case CompareOperator.GreaterThan: |
||||
|
return $"{Path} > {Value}"; |
||||
|
case CompareOperator.GreaterThanOrEqual: |
||||
|
return $"{Path} >= {Value}"; |
||||
|
case CompareOperator.LessThan: |
||||
|
return $"{Path} < {Value}"; |
||||
|
case CompareOperator.LessThanOrEqual: |
||||
|
return $"{Path} <= {Value}"; |
||||
|
case CompareOperator.In: |
||||
|
return $"{Path} in {Value}"; |
||||
|
default: |
||||
|
return string.Empty; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,85 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Linq; |
|
||||
using NodaTime; |
|
||||
|
|
||||
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, string value) |
|
||||
{ |
|
||||
return Binary(path, FilterOperator.Equals, value); |
|
||||
} |
|
||||
|
|
||||
public static FilterComparison Eq(string path, bool value) |
|
||||
{ |
|
||||
return Binary(path, FilterOperator.Equals, value); |
|
||||
} |
|
||||
|
|
||||
public static FilterComparison Eq(string path, long value) |
|
||||
{ |
|
||||
return Binary(path, FilterOperator.Equals, value); |
|
||||
} |
|
||||
|
|
||||
public static FilterComparison Eq(string path, int value) |
|
||||
{ |
|
||||
return Binary(path, FilterOperator.Equals, value); |
|
||||
} |
|
||||
|
|
||||
public static FilterComparison Eq(string path, Instant value) |
|
||||
{ |
|
||||
return Binary(path, FilterOperator.Equals, value); |
|
||||
} |
|
||||
|
|
||||
public static FilterComparison Empty(string path) |
|
||||
{ |
|
||||
return new FilterComparison(path.Split('.', '/'), FilterOperator.Empty, FilterValue.Null); |
|
||||
} |
|
||||
|
|
||||
public static FilterComparison In(string path, params long[] value) |
|
||||
{ |
|
||||
return new FilterComparison(path.Split('.', '/'), FilterOperator.In, new FilterValue(value.ToList())); |
|
||||
} |
|
||||
|
|
||||
private static FilterComparison Binary(string path, FilterOperator @operator, string value) |
|
||||
{ |
|
||||
return new FilterComparison(path.Split('.', '/'), @operator, new FilterValue(value)); |
|
||||
} |
|
||||
|
|
||||
private static FilterComparison Binary(string path, FilterOperator @operator, bool value) |
|
||||
{ |
|
||||
return new FilterComparison(path.Split('.', '/'), @operator, new FilterValue(value)); |
|
||||
} |
|
||||
|
|
||||
private static FilterComparison Binary(string path, FilterOperator @operator, long value) |
|
||||
{ |
|
||||
return new FilterComparison(path.Split('.', '/'), @operator, new FilterValue(value)); |
|
||||
} |
|
||||
|
|
||||
private static FilterComparison Binary(string path, FilterOperator @operator, int value) |
|
||||
{ |
|
||||
return new FilterComparison(path.Split('.', '/'), @operator, new FilterValue(value)); |
|
||||
} |
|
||||
|
|
||||
private static FilterComparison Binary(string path, FilterOperator @operator, Instant value) |
|
||||
{ |
|
||||
return new FilterComparison(path.Split('.', '/'), @operator, new FilterValue(value)); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,70 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// 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> Lhs { get; } |
|
||||
|
|
||||
public FilterOperator Operator { get; } |
|
||||
|
|
||||
public FilterValue Rhs { get; } |
|
||||
|
|
||||
public FilterComparison(IReadOnlyList<string> lhs, FilterOperator @operator, FilterValue rhs) |
|
||||
{ |
|
||||
Guard.NotNull(lhs, nameof(lhs)); |
|
||||
Guard.NotEmpty(lhs, nameof(lhs)); |
|
||||
Guard.Enum(@operator, nameof(@operator)); |
|
||||
|
|
||||
Lhs = lhs; |
|
||||
Rhs = rhs; |
|
||||
|
|
||||
Operator = @operator; |
|
||||
} |
|
||||
|
|
||||
public override T Accept<T>(FilterNodeVisitor<T> visitor) |
|
||||
{ |
|
||||
return visitor.Visit(this); |
|
||||
} |
|
||||
|
|
||||
public override string ToString() |
|
||||
{ |
|
||||
var path = string.Join(".", Lhs); |
|
||||
|
|
||||
switch (Operator) |
|
||||
{ |
|
||||
case FilterOperator.Contains: |
|
||||
return $"contains({path}, {Rhs})"; |
|
||||
case FilterOperator.Empty: |
|
||||
return $"empty({path})"; |
|
||||
case FilterOperator.EndsWith: |
|
||||
return $"endsWith({path}, {Rhs})"; |
|
||||
case FilterOperator.StartsWith: |
|
||||
return $"startsWith({path}, {Rhs})"; |
|
||||
case FilterOperator.Equals: |
|
||||
return $"{path} == {Rhs}"; |
|
||||
case FilterOperator.NotEquals: |
|
||||
return $"{path} != {Rhs}"; |
|
||||
case FilterOperator.GreaterThan: |
|
||||
return $"{path} > {Rhs}"; |
|
||||
case FilterOperator.GreaterThanOrEqual: |
|
||||
return $"{path} >= {Rhs}"; |
|
||||
case FilterOperator.LessThan: |
|
||||
return $"{path} < {Rhs}"; |
|
||||
case FilterOperator.LessThanOrEqual: |
|
||||
return $"{path} <= {Rhs}"; |
|
||||
case FilterOperator.In: |
|
||||
return $"{path} in {Rhs}"; |
|
||||
default: |
|
||||
return string.Empty; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,45 +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; |
|
||||
|
|
||||
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)})"; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,149 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Globalization; |
|
||||
using System.Linq; |
|
||||
using NodaTime; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Queries |
|
||||
{ |
|
||||
public sealed class FilterValue |
|
||||
{ |
|
||||
public static readonly FilterValue Null = new FilterValue(null, FilterValueType.Null, false); |
|
||||
|
|
||||
public object Value { get; } |
|
||||
|
|
||||
public FilterValueType ValueType { get; } |
|
||||
|
|
||||
public bool IsList { get; } |
|
||||
|
|
||||
public FilterValue(Guid value) |
|
||||
: this(value, FilterValueType.Guid, false) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public FilterValue(Instant value) |
|
||||
: this(value, FilterValueType.Instant, false) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public FilterValue(bool value) |
|
||||
: this(value, FilterValueType.Boolean, false) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public FilterValue(float value) |
|
||||
: this(value, FilterValueType.Single, false) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public FilterValue(double value) |
|
||||
: this(value, FilterValueType.Double, false) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public FilterValue(int value) |
|
||||
: this(value, FilterValueType.Int32, false) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public FilterValue(long value) |
|
||||
: this(value, FilterValueType.Int64, false) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public FilterValue(string value) |
|
||||
: this(value, FilterValueType.String, false) |
|
||||
{ |
|
||||
Guard.NotNull(value, nameof(value)); |
|
||||
} |
|
||||
|
|
||||
public FilterValue(List<Guid> value) |
|
||||
: this(value, FilterValueType.Guid, true) |
|
||||
{ |
|
||||
Guard.NotNull(value, nameof(value)); |
|
||||
} |
|
||||
|
|
||||
public FilterValue(List<Instant> value) |
|
||||
: this(value, FilterValueType.Instant, true) |
|
||||
{ |
|
||||
Guard.NotNull(value, nameof(value)); |
|
||||
} |
|
||||
|
|
||||
public FilterValue(List<bool> value) |
|
||||
: this(value, FilterValueType.Boolean, true) |
|
||||
{ |
|
||||
Guard.NotNull(value, nameof(value)); |
|
||||
} |
|
||||
|
|
||||
public FilterValue(List<float> value) |
|
||||
: this(value, FilterValueType.Single, true) |
|
||||
{ |
|
||||
Guard.NotNull(value, nameof(value)); |
|
||||
} |
|
||||
|
|
||||
public FilterValue(List<double> value) |
|
||||
: this(value, FilterValueType.Double, true) |
|
||||
{ |
|
||||
Guard.NotNull(value, nameof(value)); |
|
||||
} |
|
||||
|
|
||||
public FilterValue(List<int> value) |
|
||||
: this(value, FilterValueType.Int32, true) |
|
||||
{ |
|
||||
Guard.NotNull(value, nameof(value)); |
|
||||
} |
|
||||
|
|
||||
public FilterValue(List<long> value) |
|
||||
: this(value, FilterValueType.Int64, true) |
|
||||
{ |
|
||||
Guard.NotNull(value, nameof(value)); |
|
||||
} |
|
||||
|
|
||||
public FilterValue(List<string> value) |
|
||||
: this(value, FilterValueType.String, true) |
|
||||
{ |
|
||||
Guard.NotNull(value, nameof(value)); |
|
||||
} |
|
||||
|
|
||||
private FilterValue(object value, FilterValueType valueType, bool isList) |
|
||||
{ |
|
||||
Value = value; |
|
||||
ValueType = valueType; |
|
||||
|
|
||||
IsList = isList; |
|
||||
} |
|
||||
|
|
||||
public override string ToString() |
|
||||
{ |
|
||||
if (Value is IList list) |
|
||||
{ |
|
||||
return $"[{string.Join(", ", list.OfType<object>().Select(ToString).ToArray())}]"; |
|
||||
} |
|
||||
|
|
||||
return ToString(Value); |
|
||||
} |
|
||||
|
|
||||
private static string ToString(object value) |
|
||||
{ |
|
||||
if (value == null) |
|
||||
{ |
|
||||
return "null"; |
|
||||
} |
|
||||
|
|
||||
if (value is string s) |
|
||||
{ |
|
||||
return $"'{s.Replace("'", "\\'")}'"; |
|
||||
} |
|
||||
|
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0}", value); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,162 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Newtonsoft.Json; |
||||
|
using Squidex.Infrastructure.Json.Newtonsoft; |
||||
|
using Squidex.Infrastructure.Json.Objects; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Queries.Json |
||||
|
{ |
||||
|
public sealed class FilterConverter : JsonClassConverter<FilterNode<IJsonValue>> |
||||
|
{ |
||||
|
public override IEnumerable<Type> SupportedTypes |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
yield return typeof(CompareFilter<IJsonValue>); |
||||
|
yield return typeof(FilterNode<IJsonValue>); |
||||
|
yield return typeof(LogicalFilter<IJsonValue>); |
||||
|
yield return typeof(NegateFilter<IJsonValue>); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override bool CanConvert(Type objectType) |
||||
|
{ |
||||
|
return SupportedTypes.Contains(objectType); |
||||
|
} |
||||
|
|
||||
|
protected override FilterNode<IJsonValue> ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) |
||||
|
{ |
||||
|
if (reader.TokenType != JsonToken.StartObject) |
||||
|
{ |
||||
|
throw new JsonException($"Expected StartObject, but got {reader.TokenType}."); |
||||
|
} |
||||
|
|
||||
|
FilterNode<IJsonValue> result = null; |
||||
|
|
||||
|
var comparePath = (PropertyPath)null; |
||||
|
var compareOperator = (CompareOperator)99; |
||||
|
var compareValue = (IJsonValue)null; |
||||
|
|
||||
|
while (reader.Read()) |
||||
|
{ |
||||
|
switch (reader.TokenType) |
||||
|
{ |
||||
|
case JsonToken.PropertyName: |
||||
|
string propertyName = reader.Value.ToString(); |
||||
|
|
||||
|
if (!reader.Read()) |
||||
|
{ |
||||
|
throw new JsonSerializationException("Unexpected end when reading filter."); |
||||
|
} |
||||
|
|
||||
|
if (result != null) |
||||
|
{ |
||||
|
throw new JsonSerializationException($"Unexpected property {propertyName}"); |
||||
|
} |
||||
|
|
||||
|
switch (propertyName.ToLowerInvariant()) |
||||
|
{ |
||||
|
case "not": |
||||
|
var filter = serializer.Deserialize<FilterNode<IJsonValue>>(reader); |
||||
|
|
||||
|
result = new NegateFilter<IJsonValue>(filter); |
||||
|
break; |
||||
|
case "and": |
||||
|
var andFilters = serializer.Deserialize<List<FilterNode<IJsonValue>>>(reader); |
||||
|
|
||||
|
result = new LogicalFilter<IJsonValue>(LogicalFilterType.And, andFilters); |
||||
|
break; |
||||
|
case "or": |
||||
|
var orFilters = serializer.Deserialize<List<FilterNode<IJsonValue>>>(reader); |
||||
|
|
||||
|
result = new LogicalFilter<IJsonValue>(LogicalFilterType.Or, orFilters); |
||||
|
break; |
||||
|
case "path": |
||||
|
comparePath = serializer.Deserialize<PropertyPath>(reader); |
||||
|
break; |
||||
|
case "op": |
||||
|
compareOperator = ReadOperator(reader, serializer); |
||||
|
break; |
||||
|
case "value": |
||||
|
compareValue = serializer.Deserialize<IJsonValue>(reader); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
case JsonToken.Comment: |
||||
|
break; |
||||
|
case JsonToken.EndObject: |
||||
|
if (result != null) |
||||
|
{ |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
if (comparePath == null) |
||||
|
{ |
||||
|
throw new JsonSerializationException("Path not defined."); |
||||
|
} |
||||
|
|
||||
|
if (compareValue == null && compareOperator != CompareOperator.Empty) |
||||
|
{ |
||||
|
throw new JsonSerializationException("Value not defined."); |
||||
|
} |
||||
|
|
||||
|
if (!compareOperator.IsEnumValue()) |
||||
|
{ |
||||
|
throw new JsonSerializationException("Operator not defined."); |
||||
|
} |
||||
|
|
||||
|
return new CompareFilter<IJsonValue>(comparePath, compareOperator, compareValue ?? JsonValue.Null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
throw new JsonSerializationException("Unexpected end when reading filter."); |
||||
|
} |
||||
|
|
||||
|
private static CompareOperator ReadOperator(JsonReader reader, JsonSerializer serializer) |
||||
|
{ |
||||
|
var value = serializer.Deserialize<string>(reader); |
||||
|
|
||||
|
switch (value.ToLowerInvariant()) |
||||
|
{ |
||||
|
case "eq": |
||||
|
return CompareOperator.Equals; |
||||
|
case "ne": |
||||
|
return CompareOperator.NotEquals; |
||||
|
case "lt": |
||||
|
return CompareOperator.LessThan; |
||||
|
case "le": |
||||
|
return CompareOperator.LessThanOrEqual; |
||||
|
case "gt": |
||||
|
return CompareOperator.GreaterThan; |
||||
|
case "ge": |
||||
|
return CompareOperator.GreaterThanOrEqual; |
||||
|
case "empty": |
||||
|
return CompareOperator.Empty; |
||||
|
case "contains": |
||||
|
return CompareOperator.Contains; |
||||
|
case "endswith": |
||||
|
return CompareOperator.EndsWith; |
||||
|
case "startswith": |
||||
|
return CompareOperator.StartsWith; |
||||
|
case "in": |
||||
|
return CompareOperator.In; |
||||
|
} |
||||
|
|
||||
|
throw new JsonSerializationException($"Unexpected compare operator, got {value}."); |
||||
|
} |
||||
|
|
||||
|
protected override void WriteValue(JsonWriter writer, FilterNode<IJsonValue> value, JsonSerializer serializer) |
||||
|
{ |
||||
|
throw new NotSupportedException(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Newtonsoft.Json; |
||||
|
using Squidex.Infrastructure.Json.Newtonsoft; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Queries.Json |
||||
|
{ |
||||
|
public sealed class PropertyPathConverter : JsonClassConverter<PropertyPath> |
||||
|
{ |
||||
|
protected override PropertyPath ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) |
||||
|
{ |
||||
|
var value = serializer.Deserialize<string>(reader); |
||||
|
|
||||
|
return value; |
||||
|
} |
||||
|
|
||||
|
protected override void WriteValue(JsonWriter writer, PropertyPath value, JsonSerializer serializer) |
||||
|
{ |
||||
|
serializer.Serialize(writer, (IEnumerable<string>)value); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 LogicalFilter<TValue> : FilterNode<TValue> |
||||
|
{ |
||||
|
public IReadOnlyList<FilterNode<TValue>> Filters { get; } |
||||
|
|
||||
|
public LogicalFilterType Type { get; } |
||||
|
|
||||
|
public LogicalFilter(LogicalFilterType type, IReadOnlyList<FilterNode<TValue>> filters) |
||||
|
{ |
||||
|
Guard.NotNull(filters, nameof(filters)); |
||||
|
Guard.GreaterEquals(filters.Count, 2, nameof(filters.Count)); |
||||
|
Guard.Enum(type, nameof(type)); |
||||
|
|
||||
|
Filters = filters; |
||||
|
|
||||
|
Type = type; |
||||
|
} |
||||
|
|
||||
|
public override T Accept<T>(FilterNodeVisitor<T, TValue> visitor) |
||||
|
{ |
||||
|
return visitor.Visit(this); |
||||
|
} |
||||
|
|
||||
|
public override string ToString() |
||||
|
{ |
||||
|
return $"({string.Join(Type == LogicalFilterType.And ? " && " : " || ", Filters)})"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Collections.Immutable; |
||||
|
using System.Collections.ObjectModel; |
||||
|
using System.Linq; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Queries |
||||
|
{ |
||||
|
public sealed class PropertyPath : ReadOnlyCollection<string> |
||||
|
{ |
||||
|
private static readonly char[] Separators = { '.', '/' }; |
||||
|
|
||||
|
public PropertyPath(IList<string> items) |
||||
|
: base(items) |
||||
|
{ |
||||
|
if (items.Count == 0) |
||||
|
{ |
||||
|
throw new ArgumentException("Path cannot be empty.", nameof(items)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static implicit operator PropertyPath(string path) |
||||
|
{ |
||||
|
return new PropertyPath(path?.Split(Separators, StringSplitOptions.RemoveEmptyEntries)?.ToList()); |
||||
|
} |
||||
|
|
||||
|
public static implicit operator PropertyPath(string[] path) |
||||
|
{ |
||||
|
return new PropertyPath(path?.ToList()); |
||||
|
} |
||||
|
|
||||
|
public static implicit operator PropertyPath(List<string> path) |
||||
|
{ |
||||
|
return new PropertyPath(path); |
||||
|
} |
||||
|
|
||||
|
public static implicit operator PropertyPath(ImmutableList<string> path) |
||||
|
{ |
||||
|
return new PropertyPath(path); |
||||
|
} |
||||
|
|
||||
|
public override string ToString() |
||||
|
{ |
||||
|
return string.Join(".", this); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,181 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Newtonsoft.Json; |
||||
|
using Squidex.Infrastructure.Json.Objects; |
||||
|
using Squidex.Infrastructure.TestHelpers; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Queries |
||||
|
{ |
||||
|
public class QueryJsonTests |
||||
|
{ |
||||
|
[Theory] |
||||
|
[InlineData("eq", "property == 12")] |
||||
|
[InlineData("ne", "property != 12")] |
||||
|
[InlineData("le", "property <= 12")] |
||||
|
[InlineData("lt", "property < 12")] |
||||
|
[InlineData("ge", "property >= 12")] |
||||
|
[InlineData("gt", "property > 12")] |
||||
|
[InlineData("contains", "contains(property, 12)")] |
||||
|
[InlineData("endswith", "endsWith(property, 12)")] |
||||
|
[InlineData("startswith", "startsWith(property, 12)")] |
||||
|
public void Should_convert_comparison(string op, string expected) |
||||
|
{ |
||||
|
var json = new |
||||
|
{ |
||||
|
path = "property", |
||||
|
op, |
||||
|
value = 12 |
||||
|
}; |
||||
|
|
||||
|
var filter = SerializeAndDeserialize(json); |
||||
|
|
||||
|
Assert.Equal(expected, filter.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_convert_comparison_empty() |
||||
|
{ |
||||
|
var json = new { path = "property", op = "empty" }; |
||||
|
|
||||
|
var filter = SerializeAndDeserialize(json); |
||||
|
|
||||
|
Assert.Equal("empty(property)", filter.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_convert_comparison_in() |
||||
|
{ |
||||
|
var json = new { path = "property", op = "in", value = new[] { 12, 13 } }; |
||||
|
|
||||
|
var filter = SerializeAndDeserialize(json); |
||||
|
|
||||
|
Assert.Equal("property in [12, 13]", filter.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_convert_comparison_with_deep_path() |
||||
|
{ |
||||
|
var json = new { path = "property.nested", op = "eq", value = 12 }; |
||||
|
|
||||
|
var filter = SerializeAndDeserialize(json); |
||||
|
|
||||
|
Assert.Equal("property.nested == 12", filter.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_convert_logical_and() |
||||
|
{ |
||||
|
var json = new |
||||
|
{ |
||||
|
and = new[] |
||||
|
{ |
||||
|
new { path = "property", op = "ge", value = 10 }, |
||||
|
new { path = "property", op = "lt", value = 20 } |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
var filter = SerializeAndDeserialize(json); |
||||
|
|
||||
|
Assert.Equal("(property >= 10 && property < 20)", filter.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_convert_logical_or() |
||||
|
{ |
||||
|
var json = new |
||||
|
{ |
||||
|
or = new[] |
||||
|
{ |
||||
|
new { path = "property", op = "ge", value = 10 }, |
||||
|
new { path = "property", op = "lt", value = 20 } |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
var filter = SerializeAndDeserialize(json); |
||||
|
|
||||
|
Assert.Equal("(property >= 10 || property < 20)", filter.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_convert_logical_not() |
||||
|
{ |
||||
|
var json = new |
||||
|
{ |
||||
|
not = new { path = "property", op = "ge", value = 10 } |
||||
|
}; |
||||
|
|
||||
|
var filter = SerializeAndDeserialize(json); |
||||
|
|
||||
|
Assert.Equal("!(property >= 10)", filter.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_exception_for_invalid_operator() |
||||
|
{ |
||||
|
var json = new { path = "property", op = "invalid", value = 12 }; |
||||
|
|
||||
|
Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_exception_for_missing_path() |
||||
|
{ |
||||
|
var json = new { op = "invalid", value = 12 }; |
||||
|
|
||||
|
Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_exception_for_missing_operator() |
||||
|
{ |
||||
|
var json = new { path = "property", value = 12 }; |
||||
|
|
||||
|
Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_exception_for_missing_value() |
||||
|
{ |
||||
|
var json = new { path = "property", op = "invalid" }; |
||||
|
|
||||
|
Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_exception_for_invalid_property() |
||||
|
{ |
||||
|
var json = new { path = "property", op = "invalid", value = 12, other = 4 }; |
||||
|
|
||||
|
Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_exception_for_invalid_property_after_filter() |
||||
|
{ |
||||
|
var json = new |
||||
|
{ |
||||
|
and = new[] |
||||
|
{ |
||||
|
new { path = "property", op = "ge", value = 10 }, |
||||
|
new { path = "property", op = "lt", value = 20 } |
||||
|
}, |
||||
|
additional = 1 |
||||
|
}; |
||||
|
|
||||
|
Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json)); |
||||
|
} |
||||
|
|
||||
|
private static FilterNode<IJsonValue> SerializeAndDeserialize<T>(T value) |
||||
|
{ |
||||
|
var json = JsonHelper.DefaultSerializer.Serialize(value, true); |
||||
|
|
||||
|
return JsonHelper.DefaultSerializer.Deserialize<FilterNode<IJsonValue>>(json); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue