mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
89 changed files with 2216 additions and 1129 deletions
@ -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,50 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Core.Tags; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Queries; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets.Queries |
||||
|
{ |
||||
|
public sealed class QueryTagVisitor : TransformVisitor |
||||
|
{ |
||||
|
private readonly ITagService tagService; |
||||
|
private readonly Guid appId; |
||||
|
|
||||
|
private QueryTagVisitor(Guid appId, ITagService tagService) |
||||
|
{ |
||||
|
this.appId = appId; |
||||
|
|
||||
|
this.tagService = tagService; |
||||
|
} |
||||
|
|
||||
|
public static FilterNode Transform(FilterNode nodeIn, Guid appId, ITagService tagService) |
||||
|
{ |
||||
|
Guard.NotNull(tagService, nameof(tagService)); |
||||
|
|
||||
|
return nodeIn.Accept(new QueryTagVisitor(appId, tagService)); |
||||
|
} |
||||
|
|
||||
|
public override FilterNode Visit(FilterComparison nodeIn) |
||||
|
{ |
||||
|
if (string.Equals(nodeIn.Path[0], nameof(IAssetEntity.Tags), StringComparison.OrdinalIgnoreCase) && nodeIn.Value is string stringValue) |
||||
|
{ |
||||
|
var tagNames = Task.Run(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, HashSet.Of(stringValue))).Result; |
||||
|
|
||||
|
if (tagNames.TryGetValue(stringValue, out var normalized)) |
||||
|
{ |
||||
|
return new FilterComparison(nodeIn.Path, nodeIn.Operator, normalized, FilterValueType.String); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nodeIn; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,83 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using Microsoft.OData; |
|
||||
using Microsoft.OData.Edm; |
|
||||
using Microsoft.OData.UriParser; |
|
||||
using NodaTime; |
|
||||
using NodaTime.Text; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.MongoDb.OData |
|
||||
{ |
|
||||
public sealed class ConstantVisitor : QueryNodeVisitor<object> |
|
||||
{ |
|
||||
private static readonly IEdmPrimitiveType BooleanType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Boolean); |
|
||||
private static readonly IEdmPrimitiveType DateTimeType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset); |
|
||||
private static readonly IEdmPrimitiveType GuidType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Guid); |
|
||||
|
|
||||
private static readonly ConstantVisitor Instance = new ConstantVisitor(); |
|
||||
|
|
||||
private ConstantVisitor() |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public static object Visit(QueryNode node) |
|
||||
{ |
|
||||
return node.Accept(Instance); |
|
||||
} |
|
||||
|
|
||||
public override object Visit(ConvertNode nodeIn) |
|
||||
{ |
|
||||
if (nodeIn.TypeReference.Definition == BooleanType) |
|
||||
{ |
|
||||
return bool.Parse(Visit(nodeIn.Source).ToString()); |
|
||||
} |
|
||||
|
|
||||
if (nodeIn.TypeReference.Definition == GuidType) |
|
||||
{ |
|
||||
return Guid.Parse(Visit(nodeIn.Source).ToString()); |
|
||||
} |
|
||||
|
|
||||
if (nodeIn.TypeReference.Definition == DateTimeType) |
|
||||
{ |
|
||||
var value = Visit(nodeIn.Source); |
|
||||
|
|
||||
if (value is DateTimeOffset dateTimeOffset) |
|
||||
{ |
|
||||
return Instant.FromDateTimeOffset(dateTimeOffset); |
|
||||
} |
|
||||
|
|
||||
if (value is DateTime dateTime) |
|
||||
{ |
|
||||
return Instant.FromDateTimeUtc(DateTime.SpecifyKind(dateTime, DateTimeKind.Utc)); |
|
||||
} |
|
||||
|
|
||||
if (value is Date date) |
|
||||
{ |
|
||||
return Instant.FromUtc(date.Year, date.Month, date.Day, 0, 0); |
|
||||
} |
|
||||
|
|
||||
var parseResult = InstantPattern.General.Parse(Visit(nodeIn.Source).ToString()); |
|
||||
|
|
||||
if (!parseResult.Success) |
|
||||
{ |
|
||||
throw new ODataException("Datetime is not in a valid format. Use ISO 8601"); |
|
||||
} |
|
||||
|
|
||||
return parseResult.Value; |
|
||||
} |
|
||||
|
|
||||
return base.Visit(nodeIn); |
|
||||
} |
|
||||
|
|
||||
public override object Visit(ConstantNode nodeIn) |
|
||||
{ |
|
||||
return nodeIn.Value; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,32 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Linq; |
|
||||
using Microsoft.OData.UriParser; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.MongoDb.OData |
|
||||
{ |
|
||||
public delegate string ConvertProperty(string[] parts); |
|
||||
|
|
||||
public static class PropertyBuilder |
|
||||
{ |
|
||||
private static readonly ConvertProperty Default = parts => |
|
||||
{ |
|
||||
return string.Join(".", parts).ToPascalCase(); |
|
||||
}; |
|
||||
|
|
||||
public static string BuildFieldDefinition(this QueryNode node, ConvertProperty convertProperty) |
|
||||
{ |
|
||||
convertProperty = convertProperty ?? Default; |
|
||||
|
|
||||
var propertyParts = node.Accept(PropertyNameVisitor.Instance).ToArray(); |
|
||||
var propertyName = convertProperty(propertyParts); |
|
||||
|
|
||||
return propertyName; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,32 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.Queries |
||||
|
{ |
||||
|
public static class FilterBuilder |
||||
|
{ |
||||
|
public static FilterJunction And(params FilterNode[] operands) |
||||
|
{ |
||||
|
return new FilterJunction(FilterJunctionType.And, operands); |
||||
|
} |
||||
|
|
||||
|
public static FilterJunction Or(params FilterNode[] operands) |
||||
|
{ |
||||
|
return new FilterJunction(FilterJunctionType.Or, operands); |
||||
|
} |
||||
|
|
||||
|
public static FilterComparison Eq(string path, object value) |
||||
|
{ |
||||
|
return Binary(path, FilterOperator.Equals, value); |
||||
|
} |
||||
|
|
||||
|
private static FilterComparison Binary(string path, FilterOperator @operator, object value) |
||||
|
{ |
||||
|
return new FilterComparison(path.Split('.', '/'), @operator, value, FilterValueType.String); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,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,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,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.Value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void ParseSkip(this ODataUriParser query, Query result) |
||||
|
{ |
||||
|
var skip = query.ParseSkip(); |
||||
|
|
||||
|
if (skip.HasValue) |
||||
|
{ |
||||
|
result.Skip = skip.Value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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,30 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Linq; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Queries |
||||
|
{ |
||||
|
public sealed class PascalCasePathConverter : TransformVisitor |
||||
|
{ |
||||
|
private static readonly PascalCasePathConverter Instance = new PascalCasePathConverter(); |
||||
|
|
||||
|
private PascalCasePathConverter() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public static FilterNode Transform(FilterNode node) |
||||
|
{ |
||||
|
return node.Accept(Instance); |
||||
|
} |
||||
|
|
||||
|
public override FilterNode Visit(FilterComparison nodeIn) |
||||
|
{ |
||||
|
return new FilterComparison(nodeIn.Path.Select(x => x.ToPascalCase()).ToList(), nodeIn.Operator, nodeIn.Value, nodeIn.ValueType); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; } = long.MaxValue; |
||||
|
|
||||
|
public List<SortNode> Sort { get; set; } = 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 > 0) |
||||
|
{ |
||||
|
parts.Add($"Skip: {Skip}"); |
||||
|
} |
||||
|
|
||||
|
if (Take < long.MaxValue) |
||||
|
{ |
||||
|
parts.Add($"Take: {Take}"); |
||||
|
} |
||||
|
|
||||
|
if (Sort.Count > 0) |
||||
|
{ |
||||
|
parts.Add($"Sort: {string.Join(", ", Sort)}"); |
||||
|
} |
||||
|
|
||||
|
return string.Join("; ", parts); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Linq; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Queries |
||||
|
{ |
||||
|
public static class SortBuilder |
||||
|
{ |
||||
|
public static SortNode Ascending(string path) |
||||
|
{ |
||||
|
return new SortNode(path.Split('.', '/').ToList(), SortOrder.Ascending); |
||||
|
} |
||||
|
|
||||
|
public static SortNode Descending(string path) |
||||
|
{ |
||||
|
return new SortNode(path.Split('.', '/').ToList(), SortOrder.Descending); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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,58 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using FakeItEasy; |
||||
|
using Squidex.Domain.Apps.Core.Tags; |
||||
|
using Squidex.Infrastructure.Queries; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets.Queries |
||||
|
{ |
||||
|
public class QueryTagVisitorTests |
||||
|
{ |
||||
|
private readonly ITagService tagService = A.Fake<ITagService>(); |
||||
|
private readonly Guid appId = Guid.NewGuid(); |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_normalize_tags() |
||||
|
{ |
||||
|
A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A<HashSet<string>>.That.Contains("tag1"))) |
||||
|
.Returns(new Dictionary<string, string> { ["tag1"] = "normalized1" }); |
||||
|
|
||||
|
var source = FilterBuilder.Eq("tags", "tag1"); |
||||
|
var result = QueryTagVisitor.Transform(source, appId, tagService); |
||||
|
|
||||
|
Assert.Equal("tags == normalized1", result.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_not_not_fail_when_not_found() |
||||
|
{ |
||||
|
A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A<HashSet<string>>.That.Contains("tag1"))) |
||||
|
.Returns(new Dictionary<string, string>()); |
||||
|
|
||||
|
var source = FilterBuilder.Eq("tags", "tag1"); |
||||
|
var result = QueryTagVisitor.Transform(source, appId, tagService); |
||||
|
|
||||
|
Assert.Equal("tags == tag1", result.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_not_normalize_other_field() |
||||
|
{ |
||||
|
var source = FilterBuilder.Eq("other", "value"); |
||||
|
var result = QueryTagVisitor.Transform(source, appId, tagService); |
||||
|
|
||||
|
Assert.Equal("other == value", result.ToString()); |
||||
|
|
||||
|
A.CallTo(() => tagService.GetTagIdsAsync(appId, A<string>.Ignored, A<HashSet<string>>.Ignored)) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,267 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Immutable; |
||||
|
using System.Linq; |
||||
|
using FakeItEasy; |
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Microsoft.OData.Edm; |
||||
|
using MongoDB.Bson.Serialization; |
||||
|
using MongoDB.Driver; |
||||
|
using NodaTime.Text; |
||||
|
using Squidex.Domain.Apps.Core; |
||||
|
using Squidex.Domain.Apps.Core.Apps; |
||||
|
using Squidex.Domain.Apps.Core.Schemas; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.Edm; |
||||
|
using Squidex.Domain.Apps.Entities.MongoDb.Contents; |
||||
|
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
using Squidex.Infrastructure.MongoDb.OData; |
||||
|
using Squidex.Infrastructure.Queries; |
||||
|
using Xunit; |
||||
|
using FilterBuilder = Squidex.Infrastructure.Queries.FilterBuilder; |
||||
|
using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.MongoDb |
||||
|
{ |
||||
|
public class MongoDbQueryTests |
||||
|
{ |
||||
|
private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry; |
||||
|
private static readonly IBsonSerializer<MongoContentEntity> Serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>(); |
||||
|
private readonly Schema schemaDef; |
||||
|
private readonly IEdmModel edmModel; |
||||
|
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); |
||||
|
|
||||
|
static MongoDbQueryTests() |
||||
|
{ |
||||
|
InstantSerializer.Register(); |
||||
|
} |
||||
|
|
||||
|
public MongoDbQueryTests() |
||||
|
{ |
||||
|
schemaDef = |
||||
|
new Schema("user") |
||||
|
.AddString(1, "firstName", Partitioning.Language, |
||||
|
new StringFieldProperties { Label = "FirstName", IsRequired = true, AllowedValues = ImmutableList.Create("1", "2") }) |
||||
|
.AddString(2, "lastName", Partitioning.Language, |
||||
|
new StringFieldProperties { Hints = "Last Name", Editor = StringFieldEditor.Input }) |
||||
|
.AddBoolean(3, "isAdmin", Partitioning.Invariant, |
||||
|
new BooleanFieldProperties()) |
||||
|
.AddNumber(4, "age", Partitioning.Invariant, |
||||
|
new NumberFieldProperties { MinValue = 1, MaxValue = 10 }) |
||||
|
.AddDateTime(5, "birthday", Partitioning.Invariant, |
||||
|
new DateTimeFieldProperties()) |
||||
|
.AddAssets(6, "pictures", Partitioning.Invariant, |
||||
|
new AssetsFieldProperties()) |
||||
|
.AddReferences(7, "friends", Partitioning.Invariant, |
||||
|
new ReferencesFieldProperties()) |
||||
|
.AddString(8, "dashed-field", Partitioning.Invariant, |
||||
|
new StringFieldProperties()) |
||||
|
.Update(new SchemaProperties { Hints = "The User" }); |
||||
|
|
||||
|
var builder = new EdmModelBuilder(new MemoryCache(Options.Create(new MemoryCacheOptions()))); |
||||
|
|
||||
|
var schema = A.Dummy<ISchemaEntity>(); |
||||
|
A.CallTo(() => schema.Id).Returns(Guid.NewGuid()); |
||||
|
A.CallTo(() => schema.Version).Returns(3); |
||||
|
A.CallTo(() => schema.SchemaDef).Returns(schemaDef); |
||||
|
|
||||
|
var app = A.Dummy<IAppEntity>(); |
||||
|
A.CallTo(() => app.Id).Returns(Guid.NewGuid()); |
||||
|
A.CallTo(() => app.Version).Returns(3); |
||||
|
A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); |
||||
|
|
||||
|
edmModel = builder.BuildEdmModel(schema, app); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_exception_for_invalid_field() |
||||
|
{ |
||||
|
Assert.Throws<NotSupportedException>(() => F(FilterBuilder.Eq("data/invalid/iv", "Me"))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_make_query_with_lastModified() |
||||
|
{ |
||||
|
var i = F(FilterBuilder.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); |
||||
|
var o = C("{ 'mt' : '1988-01-19T12:00:00Z' }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_make_query_with_lastModifiedBy() |
||||
|
{ |
||||
|
var i = F(FilterBuilder.Eq("lastModifiedBy", "Me")); |
||||
|
var o = C("{ 'mb' : 'Me' }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_make_query_with_created() |
||||
|
{ |
||||
|
var i = F(FilterBuilder.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); |
||||
|
var o = C("{ 'ct' : '1988-01-19T12:00:00Z' }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_make_query_with_createdBy() |
||||
|
{ |
||||
|
var i = F(FilterBuilder.Eq("createdBy", "Me")); |
||||
|
var o = C("{ 'cb' : 'Me' }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_make_query_with_version() |
||||
|
{ |
||||
|
var i = F(FilterBuilder.Eq("version", 0L)); |
||||
|
var o = C("{ 'vs' : NumberLong(0) }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_make_query_from_draft() |
||||
|
{ |
||||
|
var i = F(FilterBuilder.Eq("data/dashed_field/iv", "Value"), true); |
||||
|
var o = C("{ 'dd.8.iv' : 'Value' }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_make_query_date_field_created() |
||||
|
{ |
||||
|
var i = F(FilterBuilder.Eq("data/birthday/iv", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); |
||||
|
var o = C("{ 'do.5.iv' : '1988-01-19T12:00:00Z' }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_make_query_with_underscore_field() |
||||
|
{ |
||||
|
var i = F(FilterBuilder.Eq("data/dashed_field/iv", "Value")); |
||||
|
var o = C("{ 'do.8.iv' : 'Value' }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_make_query_with_references_equals() |
||||
|
{ |
||||
|
var i = F(FilterBuilder.Eq("data/friends/iv", "guid")); |
||||
|
var o = C("{ 'do.7.iv' : 'guid' }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_make_query_with_assets_equals() |
||||
|
{ |
||||
|
var i = F(FilterBuilder.Eq("data/pictures/iv", "guid")); |
||||
|
var o = C("{ 'do.6.iv' : 'guid' }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_make_query_with_full_text() |
||||
|
{ |
||||
|
var i = Q(new Query { FullText = "Hello my World" }); |
||||
|
var o = C("{ '$text' : { '$search' : 'Hello my World' } }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_make_orderby_with_single_field() |
||||
|
{ |
||||
|
var i = S(SortBuilder.Descending("data/age/iv")); |
||||
|
var o = C("{ 'do.4.iv' : -1 }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_make_orderby_with_multiple_fields() |
||||
|
{ |
||||
|
var i = S(SortBuilder.Ascending("data/age/iv"), SortBuilder.Descending("data/firstName/en")); |
||||
|
var o = C("{ 'do.4.iv' : 1, 'do.1.en' : -1 }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_make_take_statement() |
||||
|
{ |
||||
|
var query = new Query { Take = 3 }; |
||||
|
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
||||
|
|
||||
|
cursor.ContentTake(query.AdjustToModel(schemaDef, false)); |
||||
|
|
||||
|
A.CallTo(() => cursor.Limit(3)).MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_make_skip_statement() |
||||
|
{ |
||||
|
var query = new Query { Skip = 3 }; |
||||
|
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
||||
|
|
||||
|
cursor.ContentSkip(query.AdjustToModel(schemaDef, false)); |
||||
|
|
||||
|
A.CallTo(() => cursor.Skip(3)).MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
private static string C(string value) |
||||
|
{ |
||||
|
return value.Replace('\'', '"'); |
||||
|
} |
||||
|
|
||||
|
private string F(FilterNode filter, bool useDraft = false) |
||||
|
{ |
||||
|
return Q(new Query { Filter = filter }, useDraft); |
||||
|
} |
||||
|
|
||||
|
private string S(params SortNode[] sorts) |
||||
|
{ |
||||
|
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
||||
|
|
||||
|
var i = string.Empty; |
||||
|
|
||||
|
A.CallTo(() => cursor.Sort(A<SortDefinition<MongoContentEntity>>.Ignored)) |
||||
|
.Invokes((SortDefinition<MongoContentEntity> sortDefinition) => |
||||
|
{ |
||||
|
i = sortDefinition.Render(Serializer, Registry).ToString(); |
||||
|
}); |
||||
|
|
||||
|
cursor.ContentSort(new Query { Sort = sorts.ToList() }.AdjustToModel(schemaDef, false)); |
||||
|
|
||||
|
return i; |
||||
|
} |
||||
|
|
||||
|
private string Q(Query query, bool useDraft = false) |
||||
|
{ |
||||
|
var rendered = |
||||
|
query.AdjustToModel(schemaDef, useDraft).BuildFilter<MongoContentEntity>().Filter |
||||
|
.Render(Serializer, Registry).ToString(); |
||||
|
|
||||
|
return rendered; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,448 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Immutable; |
|
||||
using FakeItEasy; |
|
||||
using Microsoft.Extensions.Caching.Memory; |
|
||||
using Microsoft.Extensions.Options; |
|
||||
using Microsoft.OData.Edm; |
|
||||
using MongoDB.Bson.Serialization; |
|
||||
using MongoDB.Driver; |
|
||||
using Squidex.Domain.Apps.Core; |
|
||||
using Squidex.Domain.Apps.Core.Apps; |
|
||||
using Squidex.Domain.Apps.Core.Schemas; |
|
||||
using Squidex.Domain.Apps.Entities.Apps; |
|
||||
using Squidex.Domain.Apps.Entities.Contents.Edm; |
|
||||
using Squidex.Domain.Apps.Entities.MongoDb.Contents; |
|
||||
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; |
|
||||
using Squidex.Domain.Apps.Entities.Schemas; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Squidex.Infrastructure.MongoDb; |
|
||||
using Squidex.Infrastructure.MongoDb.OData; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Contents.OData |
|
||||
{ |
|
||||
public class ODataQueryTests |
|
||||
{ |
|
||||
private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry; |
|
||||
private static readonly IBsonSerializer<MongoContentEntity> Serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>(); |
|
||||
private readonly Schema schemaDef; |
|
||||
private readonly IEdmModel edmModel; |
|
||||
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); |
|
||||
|
|
||||
static ODataQueryTests() |
|
||||
{ |
|
||||
InstantSerializer.Register(); |
|
||||
} |
|
||||
|
|
||||
public ODataQueryTests() |
|
||||
{ |
|
||||
schemaDef = |
|
||||
new Schema("user") |
|
||||
.AddString(1, "firstName", Partitioning.Language, |
|
||||
new StringFieldProperties { Label = "FirstName", IsRequired = true, AllowedValues = ImmutableList.Create("1", "2") }) |
|
||||
.AddString(2, "lastName", Partitioning.Language, |
|
||||
new StringFieldProperties { Hints = "Last Name", Editor = StringFieldEditor.Input }) |
|
||||
.AddBoolean(3, "isAdmin", Partitioning.Invariant, |
|
||||
new BooleanFieldProperties()) |
|
||||
.AddNumber(4, "age", Partitioning.Invariant, |
|
||||
new NumberFieldProperties { MinValue = 1, MaxValue = 10 }) |
|
||||
.AddDateTime(5, "birthday", Partitioning.Invariant, |
|
||||
new DateTimeFieldProperties()) |
|
||||
.AddAssets(6, "pictures", Partitioning.Invariant, |
|
||||
new AssetsFieldProperties()) |
|
||||
.AddReferences(7, "friends", Partitioning.Invariant, |
|
||||
new ReferencesFieldProperties()) |
|
||||
.AddString(8, "dashed-field", Partitioning.Invariant, |
|
||||
new StringFieldProperties()) |
|
||||
.Update(new SchemaProperties { Hints = "The User" }); |
|
||||
|
|
||||
var builder = new EdmModelBuilder(new MemoryCache(Options.Create(new MemoryCacheOptions()))); |
|
||||
|
|
||||
var schema = A.Dummy<ISchemaEntity>(); |
|
||||
A.CallTo(() => schema.Id).Returns(Guid.NewGuid()); |
|
||||
A.CallTo(() => schema.Version).Returns(3); |
|
||||
A.CallTo(() => schema.SchemaDef).Returns(schemaDef); |
|
||||
|
|
||||
var app = A.Dummy<IAppEntity>(); |
|
||||
A.CallTo(() => app.Id).Returns(Guid.NewGuid()); |
|
||||
A.CallTo(() => app.Version).Returns(3); |
|
||||
A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); |
|
||||
|
|
||||
edmModel = builder.BuildEdmModel(schema, app); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_parse_query() |
|
||||
{ |
|
||||
var parser = edmModel.ParseQuery("$filter=data/firstName/de eq 'Sebastian'"); |
|
||||
|
|
||||
Assert.NotNull(parser); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_created() |
|
||||
{ |
|
||||
var i = F("$filter=created eq 1988-01-19T12:00:00Z"); |
|
||||
var o = C("{ 'ct' : ISODate('1988-01-19T12:00:00Z') }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_created_and_date() |
|
||||
{ |
|
||||
var i = F("$filter=created eq 1988-01-19"); |
|
||||
var o = C("{ 'ct' : ISODate('1988-01-19T00:00:00Z') }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_createdBy() |
|
||||
{ |
|
||||
var i = F("$filter=createdBy eq 'Me'"); |
|
||||
var o = C("{ 'cb' : 'Me' }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_lastModified() |
|
||||
{ |
|
||||
var i = F("$filter=lastModified eq 1988-01-19T12:00:00Z"); |
|
||||
var o = C("{ 'mt' : ISODate('1988-01-19T12:00:00Z') }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_lastModifiedBy() |
|
||||
{ |
|
||||
var i = F("$filter=lastModifiedBy eq 'Me'"); |
|
||||
var o = C("{ 'mb' : 'Me' }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_version() |
|
||||
{ |
|
||||
var i = F("$filter=version eq 0"); |
|
||||
var o = C("{ 'vs' : 0 }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_underscore_field() |
|
||||
{ |
|
||||
var i = F("$filter=data/dashed_field/iv eq 'Value'"); |
|
||||
var o = C("{ 'do.8.iv' : 'Value' }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_not() |
|
||||
{ |
|
||||
var i = F("$filter=not endswith(data/firstName/de, 'Sebastian')"); |
|
||||
var o = C("{ 'do.1.de' : { '$not' : /Sebastian$/i } }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_startswith() |
|
||||
{ |
|
||||
var i = F("$filter=startswith(data/firstName/de, 'Sebastian')"); |
|
||||
var o = C("{ 'do.1.de' : /^Sebastian/i }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_endswith() |
|
||||
{ |
|
||||
var i = F("$filter=endswith(data/firstName/de, 'Sebastian')"); |
|
||||
var o = C("{ 'do.1.de' : /Sebastian$/i }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_cointains() |
|
||||
{ |
|
||||
var i = F("$filter=contains(data/firstName/de, 'Sebastian')"); |
|
||||
var o = C("{ 'do.1.de' : /Sebastian/i }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_equals() |
|
||||
{ |
|
||||
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq true"); |
|
||||
var o = C("{ 'do.1.de' : /Sebastian/i }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_wih_equals_to_false() |
|
||||
{ |
|
||||
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq false"); |
|
||||
var o = C("{ 'do.1.de' : { '$not' : /Sebastian/i } }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_conjunction_and_contains() |
|
||||
{ |
|
||||
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq false and data/isAdmin/iv eq true"); |
|
||||
var o = C("{ 'do.1.de' : { '$not' : /Sebastian/i }, 'do.3.iv' : true }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_string_equals() |
|
||||
{ |
|
||||
var i = F("$filter=data/firstName/de eq 'Sebastian'"); |
|
||||
var o = C("{ 'do.1.de' : 'Sebastian' }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_datetime_equals() |
|
||||
{ |
|
||||
var i = F("$filter=data/birthday/iv eq 1988-01-19T12:00:00Z"); |
|
||||
var o = C("{ 'do.5.iv' : '1988-01-19T12:00:00Z' }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_boolean_equals() |
|
||||
{ |
|
||||
var i = F("$filter=data/isAdmin/iv eq true"); |
|
||||
var o = C("{ 'do.3.iv' : true }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_notequals() |
|
||||
{ |
|
||||
var i = F("$filter=data/firstName/de ne 'Sebastian'"); |
|
||||
var o = C("{ '$or' : [{ 'do.1.de' : { '$exists' : false } }, { 'do.1.de' : { '$ne' : 'Sebastian' } }] }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_lessthan() |
|
||||
{ |
|
||||
var i = F("$filter=data/age/iv lt 1"); |
|
||||
var o = C("{ 'do.4.iv' : { '$lt' : 1.0 } }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_lessequals() |
|
||||
{ |
|
||||
var i = F("$filter=data/age/iv le 1"); |
|
||||
var o = C("{ 'do.4.iv' : { '$lte' : 1.0 } }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_greaterthan() |
|
||||
{ |
|
||||
var i = F("$filter=data/age/iv gt 1"); |
|
||||
var o = C("{ 'do.4.iv' : { '$gt' : 1.0 } }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_greaterequals() |
|
||||
{ |
|
||||
var i = F("$filter=data/age/iv ge 1"); |
|
||||
var o = C("{ 'do.4.iv' : { '$gte' : 1.0 } }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_references_equals() |
|
||||
{ |
|
||||
var i = F("$filter=data/pictures/iv eq 'guid'"); |
|
||||
var o = C("{ 'do.6.iv' : 'guid' }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_assets_equals() |
|
||||
{ |
|
||||
var i = F("$filter=data/friends/iv eq 'guid'"); |
|
||||
var o = C("{ 'do.7.iv' : 'guid' }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_conjunction() |
|
||||
{ |
|
||||
var i = F("$filter=data/age/iv eq 1 and data/age/iv eq 2"); |
|
||||
var o = C("{ '$and' : [{ 'do.4.iv' : 1.0 }, { 'do.4.iv' : 2.0 }] }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_disjunction() |
|
||||
{ |
|
||||
var i = F("$filter=data/age/iv eq 1 or data/age/iv eq 2"); |
|
||||
var o = C("{ '$or' : [{ 'do.4.iv' : 1.0 }, { 'do.4.iv' : 2.0 }] }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_full_text() |
|
||||
{ |
|
||||
var i = F("$search=Hello my World"); |
|
||||
var o = C("{ '$text' : { '$search' : 'Hello my World' } }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_query_with_full_text_and_multiple_terms() |
|
||||
{ |
|
||||
var i = F("$search=A and B"); |
|
||||
var o = C("{ '$text' : { '$search' : 'A and B' } }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_orderby_with_single_field() |
|
||||
{ |
|
||||
var i = S("$orderby=data/age/iv desc"); |
|
||||
var o = C("{ 'do.4.iv' : -1 }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_orderby_with_multiple_field() |
|
||||
{ |
|
||||
var i = S("$orderby=data/age/iv, data/firstName/en desc"); |
|
||||
var o = C("{ 'do.4.iv' : 1, 'do.1.en' : -1 }"); |
|
||||
|
|
||||
Assert.Equal(o, i); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_top_statement() |
|
||||
{ |
|
||||
var parser = edmModel.ParseQuery("$top=3"); |
|
||||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|
||||
|
|
||||
cursor.ContentTake(parser); |
|
||||
|
|
||||
A.CallTo(() => cursor.Limit(3)).MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_top_statement_with_limit() |
|
||||
{ |
|
||||
var parser = edmModel.ParseQuery("$top=300"); |
|
||||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|
||||
|
|
||||
cursor.ContentTake(parser); |
|
||||
|
|
||||
A.CallTo(() => cursor.Limit(200)).MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_top_statement_with_default_value() |
|
||||
{ |
|
||||
var parser = edmModel.ParseQuery(string.Empty); |
|
||||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|
||||
|
|
||||
cursor.ContentTake(parser); |
|
||||
|
|
||||
A.CallTo(() => cursor.Limit(20)).MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_skip_statement() |
|
||||
{ |
|
||||
var parser = edmModel.ParseQuery("$skip=3"); |
|
||||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|
||||
|
|
||||
cursor.ContentSkip(parser); |
|
||||
|
|
||||
A.CallTo(() => cursor.Skip(3)).MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_make_skip_statement_with_default_value() |
|
||||
{ |
|
||||
var parser = edmModel.ParseQuery(string.Empty); |
|
||||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|
||||
|
|
||||
cursor.ContentSkip(parser); |
|
||||
|
|
||||
A.CallTo(() => cursor.Skip(A<int>.Ignored)).MustNotHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
private static string C(string value) |
|
||||
{ |
|
||||
return value.Replace('\'', '"'); |
|
||||
} |
|
||||
|
|
||||
private string S(string value) |
|
||||
{ |
|
||||
var parser = edmModel.ParseQuery(value); |
|
||||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|
||||
|
|
||||
var i = string.Empty; |
|
||||
|
|
||||
A.CallTo(() => cursor.Sort(A<SortDefinition<MongoContentEntity>>.Ignored)) |
|
||||
.Invokes((SortDefinition<MongoContentEntity> sortDefinition) => |
|
||||
{ |
|
||||
i = sortDefinition.Render(Serializer, Registry).ToString(); |
|
||||
}); |
|
||||
|
|
||||
cursor.ContentSort(parser, FindExtensions.CreatePropertyCalculator(schemaDef, false)); |
|
||||
|
|
||||
return i; |
|
||||
} |
|
||||
|
|
||||
private string F(string value) |
|
||||
{ |
|
||||
var parser = edmModel.ParseQuery(value); |
|
||||
|
|
||||
var query = |
|
||||
parser.BuildFilter<MongoContentEntity>(FindExtensions.CreatePropertyCalculator(schemaDef, false), FindExtensions.ValueConverter) |
|
||||
.Filter.Render(Serializer, Registry).ToString(); |
|
||||
|
|
||||
return query; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Queries |
||||
|
{ |
||||
|
public class PascalCasePathConverterTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Should_convert_property() |
||||
|
{ |
||||
|
var source = FilterBuilder.Eq("property", 1); |
||||
|
var result = PascalCasePathConverter.Transform(source); |
||||
|
|
||||
|
Assert.Equal("Property == 1", result.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_convert_properties() |
||||
|
{ |
||||
|
var source = FilterBuilder.Eq("root.child", 1); |
||||
|
var result = PascalCasePathConverter.Transform(source); |
||||
|
|
||||
|
Assert.Equal("Root.Child == 1", result.ToString()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue