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