mirror of https://github.com/Squidex/squidex.git
Browse Source
* Support for geo queries. * More tests * Last fixes. * Serialization fixes. * Lat/lon fixes. * Tests fixedpull/624/head
committed by
GitHub
93 changed files with 2300 additions and 1829 deletions
@ -0,0 +1,107 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using MongoDB.Driver; |
|||
using Squidex.Domain.Apps.Entities.Contents.Text; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.MongoDb.FullText |
|||
{ |
|||
public static class CommandFactory |
|||
{ |
|||
private static readonly FilterDefinitionBuilder<MongoTextIndexEntity> Filter = Builders<MongoTextIndexEntity>.Filter; |
|||
private static readonly UpdateDefinitionBuilder<MongoTextIndexEntity> Update = Builders<MongoTextIndexEntity>.Update; |
|||
|
|||
public static void CreateCommands(IndexCommand command, List<WriteModel<MongoTextIndexEntity>> writes) |
|||
{ |
|||
switch (command) |
|||
{ |
|||
case DeleteIndexEntry delete: |
|||
DeleteEntry(delete, writes); |
|||
break; |
|||
case UpsertIndexEntry upsert: |
|||
UpsertEntry(upsert, writes); |
|||
break; |
|||
case UpdateIndexEntry update: |
|||
UpdateEntry(update, writes); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
private static void UpsertEntry(UpsertIndexEntry upsert, List<WriteModel<MongoTextIndexEntity>> writes) |
|||
{ |
|||
writes.Add( |
|||
new UpdateOneModel<MongoTextIndexEntity>( |
|||
Filter.And( |
|||
Filter.Eq(x => x.DocId, upsert.DocId), |
|||
Filter.Exists(x => x.GeoField, false), |
|||
Filter.Exists(x => x.GeoObject, false)), |
|||
Update |
|||
.Set(x => x.ServeAll, upsert.ServeAll) |
|||
.Set(x => x.ServePublished, upsert.ServePublished) |
|||
.Set(x => x.Texts, upsert.Texts?.Values.Select(MongoTextIndexEntityText.FromText).ToList()) |
|||
.SetOnInsert(x => x.Id, Guid.NewGuid().ToString()) |
|||
.SetOnInsert(x => x.DocId, upsert.DocId) |
|||
.SetOnInsert(x => x.AppId, upsert.AppId.Id) |
|||
.SetOnInsert(x => x.ContentId, upsert.ContentId) |
|||
.SetOnInsert(x => x.SchemaId, upsert.SchemaId.Id)) |
|||
{ |
|||
IsUpsert = true |
|||
}); |
|||
|
|||
if (upsert.GeoObjects?.Any() == true) |
|||
{ |
|||
if (!upsert.IsNew) |
|||
{ |
|||
writes.Add( |
|||
new DeleteOneModel<MongoTextIndexEntity>( |
|||
Filter.And( |
|||
Filter.Eq(x => x.DocId, upsert.DocId), |
|||
Filter.Exists(x => x.GeoField), |
|||
Filter.Exists(x => x.GeoObject)))); |
|||
} |
|||
|
|||
foreach (var (field, geoObject) in upsert.GeoObjects) |
|||
{ |
|||
writes.Add( |
|||
new InsertOneModel<MongoTextIndexEntity>( |
|||
new MongoTextIndexEntity |
|||
{ |
|||
Id = Guid.NewGuid().ToString(), |
|||
AppId = upsert.AppId.Id, |
|||
DocId = upsert.DocId, |
|||
ContentId = upsert.ContentId, |
|||
GeoField = field, |
|||
GeoObject = geoObject, |
|||
SchemaId = upsert.SchemaId.Id, |
|||
ServeAll = upsert.ServeAll, |
|||
ServePublished = upsert.ServePublished |
|||
})); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void UpdateEntry(UpdateIndexEntry update, List<WriteModel<MongoTextIndexEntity>> writes) |
|||
{ |
|||
writes.Add( |
|||
new UpdateOneModel<MongoTextIndexEntity>( |
|||
Filter.Eq(x => x.DocId, update.DocId), |
|||
Update |
|||
.Set(x => x.ServeAll, update.ServeAll) |
|||
.Set(x => x.ServePublished, update.ServePublished))); |
|||
} |
|||
|
|||
private static void DeleteEntry(DeleteIndexEntry delete, List<WriteModel<MongoTextIndexEntity>> writes) |
|||
{ |
|||
writes.Add( |
|||
new DeleteOneModel<MongoTextIndexEntity>( |
|||
Filter.Eq(x => x.DocId, delete.DocId))); |
|||
} |
|||
} |
|||
} |
|||
@ -1,71 +0,0 @@ |
|||
// ==========================================================================
|
|||
// 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 Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Core.Tags; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Queries; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Queries |
|||
{ |
|||
public sealed class FilterTagTransformer : AsyncTransformVisitor<ClrValue> |
|||
{ |
|||
private readonly ITagService tagService; |
|||
private readonly ISchemaEntity schema; |
|||
private readonly DomainId appId; |
|||
|
|||
private FilterTagTransformer(DomainId appId, ISchemaEntity schema, ITagService tagService) |
|||
{ |
|||
this.appId = appId; |
|||
this.schema = schema; |
|||
this.tagService = tagService; |
|||
} |
|||
|
|||
public static ValueTask<FilterNode<ClrValue>?> TransformAsync(FilterNode<ClrValue> nodeIn, DomainId appId, ISchemaEntity schema, ITagService tagService) |
|||
{ |
|||
Guard.NotNull(nodeIn, nameof(nodeIn)); |
|||
Guard.NotNull(tagService, nameof(tagService)); |
|||
Guard.NotNull(schema, nameof(schema)); |
|||
|
|||
return nodeIn.Accept(new FilterTagTransformer(appId, schema, tagService)); |
|||
} |
|||
|
|||
public override async ValueTask<FilterNode<ClrValue>?> Visit(CompareFilter<ClrValue> nodeIn) |
|||
{ |
|||
if (nodeIn.Value.Value is string stringValue && IsDataPath(nodeIn.Path) && IsTagField(nodeIn.Path)) |
|||
{ |
|||
var tagNames = await tagService.GetTagIdsAsync(appId, TagGroups.Schemas(schema.Id), HashSet.Of(stringValue)); |
|||
|
|||
if (tagNames.TryGetValue(stringValue, out var normalized)) |
|||
{ |
|||
return new CompareFilter<ClrValue>(nodeIn.Path, nodeIn.Operator, normalized); |
|||
} |
|||
} |
|||
|
|||
return nodeIn; |
|||
} |
|||
|
|||
private bool IsTagField(IReadOnlyList<string> path) |
|||
{ |
|||
return schema.SchemaDef.FieldsByName.TryGetValue(path[1], out var field) && IsTagField(field); |
|||
} |
|||
|
|||
private static bool IsTagField(IField field) |
|||
{ |
|||
return field is IField<TagsFieldProperties> tags && tags.Properties.Normalization == TagsFieldNormalization.Schema; |
|||
} |
|||
|
|||
private static bool IsDataPath(IReadOnlyList<string> path) |
|||
{ |
|||
return path.Count == 3 && string.Equals(path[0], nameof(IContentEntity.Data), StringComparison.OrdinalIgnoreCase); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Entities.Contents.Text; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
using Squidex.Infrastructure.Queries; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Queries |
|||
{ |
|||
internal sealed class GeoQueryTransformer : AsyncTransformVisitor<ClrValue, GeoQueryTransformer.Args> |
|||
{ |
|||
public static readonly GeoQueryTransformer Instance = new GeoQueryTransformer(); |
|||
|
|||
public struct Args |
|||
{ |
|||
public readonly ITextIndex TextIndex; |
|||
|
|||
public readonly ISchemaEntity Schema; |
|||
|
|||
public readonly Context Context; |
|||
|
|||
public Args(Context context, ISchemaEntity schema, ITextIndex textIndex) |
|||
{ |
|||
Context = context; |
|||
Schema = schema; |
|||
TextIndex = textIndex; |
|||
} |
|||
} |
|||
|
|||
private GeoQueryTransformer() |
|||
{ |
|||
} |
|||
|
|||
public static async Task<FilterNode<ClrValue>?> TransformAsync(FilterNode<ClrValue> filter, Context context, ISchemaEntity schema, ITextIndex textIndex) |
|||
{ |
|||
var args = new Args(context, schema, textIndex); |
|||
|
|||
return await filter.Accept(Instance, args); |
|||
} |
|||
|
|||
public override async ValueTask<FilterNode<ClrValue>?> Visit(CompareFilter<ClrValue> nodeIn, Args args) |
|||
{ |
|||
if (nodeIn.Value.Value is FilterSphere sphere) |
|||
{ |
|||
var field = string.Join(".", nodeIn.Path.Skip(1)); |
|||
|
|||
var searchQuery = new GeoQuery(args.Schema.Id, field, sphere.Latitude, sphere.Longitude, sphere.Radius); |
|||
var searchScope = args.Context.Scope(); |
|||
|
|||
var ids = await args.TextIndex.SearchAsync(args.Context.App, searchQuery, searchScope); |
|||
|
|||
if (ids == null || ids.Count == 0) |
|||
{ |
|||
return ClrFilter.Eq("id", "__notfound__"); |
|||
} |
|||
|
|||
return ClrFilter.In("id", ids.Select(x => x.ToString()).ToList()); |
|||
} |
|||
|
|||
return nodeIn; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Text.Elastic |
|||
{ |
|||
public static class CommandFactory |
|||
{ |
|||
public static void CreateCommands(IndexCommand command, List<object> args, string indexName) |
|||
{ |
|||
switch (command) |
|||
{ |
|||
case UpsertIndexEntry upsert: |
|||
UpsertEntry(upsert, args, indexName); |
|||
break; |
|||
case UpdateIndexEntry update: |
|||
UpdateEntry(update, args, indexName); |
|||
break; |
|||
case DeleteIndexEntry delete: |
|||
DeleteEntry(delete, args, indexName); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
private static void UpsertEntry(UpsertIndexEntry upsert, List<object> args, string indexName) |
|||
{ |
|||
args.Add(new |
|||
{ |
|||
index = new |
|||
{ |
|||
_id = upsert.DocId, |
|||
_index = indexName, |
|||
} |
|||
}); |
|||
|
|||
args.Add(new |
|||
{ |
|||
appId = upsert.AppId.Id.ToString(), |
|||
appName = upsert.AppId.Name, |
|||
contentId = upsert.ContentId.ToString(), |
|||
schemaId = upsert.SchemaId.Id.ToString(), |
|||
schemaName = upsert.SchemaId.Name, |
|||
serveAll = upsert.ServeAll, |
|||
servePublished = upsert.ServePublished, |
|||
texts = upsert.Texts |
|||
}); |
|||
} |
|||
|
|||
private static void UpdateEntry(UpdateIndexEntry update, List<object> args, string indexName) |
|||
{ |
|||
args.Add(new |
|||
{ |
|||
update = new |
|||
{ |
|||
_id = update.DocId, |
|||
_index = indexName, |
|||
} |
|||
}); |
|||
|
|||
args.Add(new |
|||
{ |
|||
doc = new |
|||
{ |
|||
serveAll = update.ServeAll, |
|||
servePublished = update.ServePublished |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private static void DeleteEntry(DeleteIndexEntry delete, List<object> args, string indexName) |
|||
{ |
|||
args.Add(new |
|||
{ |
|||
delete = new |
|||
{ |
|||
_id = delete.DocId, |
|||
_index = indexName, |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
|
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Text |
|||
{ |
|||
public sealed record GeoQuery(DomainId SchemaId, string Field, double Latitude, double Longitude, double Radius) |
|||
{ |
|||
} |
|||
} |
|||
@ -1,45 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Text |
|||
{ |
|||
[Equals(DoNotAddEqualityOperators = true)] |
|||
public sealed class SearchFilter |
|||
{ |
|||
public IReadOnlyList<DomainId> SchemaIds { get; } |
|||
|
|||
public bool Must { get; } |
|||
|
|||
public SearchFilter(IReadOnlyList<DomainId> schemaIds, bool must) |
|||
{ |
|||
Guard.NotNull(schemaIds, nameof(schemaIds)); |
|||
|
|||
SchemaIds = schemaIds; |
|||
|
|||
Must = must; |
|||
} |
|||
|
|||
public static SearchFilter MustHaveSchemas(List<DomainId> schemaIds) |
|||
{ |
|||
return new SearchFilter(schemaIds, true); |
|||
} |
|||
|
|||
public static SearchFilter MustHaveSchemas(params DomainId[] schemaIds) |
|||
{ |
|||
return new SearchFilter(schemaIds?.ToList()!, true); |
|||
} |
|||
|
|||
public static SearchFilter ShouldHaveSchemas(params DomainId[] schemaIds) |
|||
{ |
|||
return new SearchFilter(schemaIds?.ToList()!, false); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
|
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Text |
|||
{ |
|||
public sealed record TextQuery(string? Text, TextFilter? Filter) |
|||
{ |
|||
} |
|||
|
|||
public sealed record TextFilter(DomainId[]? SchemaIds, bool Must) |
|||
{ |
|||
public static TextFilter MustHaveSchemas(params DomainId[] schemaIds) |
|||
{ |
|||
return new TextFilter(schemaIds, true); |
|||
} |
|||
|
|||
public static TextFilter ShouldHaveSchemas(params DomainId[] schemaIds) |
|||
{ |
|||
return new TextFilter(schemaIds, false); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using GeoJSON.Net.Converters; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Text |
|||
{ |
|||
public sealed class WriteonlyGeoJsonConverter : GeoJsonConverter |
|||
{ |
|||
public override bool CanWrite => false; |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Infrastructure.Json |
|||
{ |
|||
public static class GeoJson |
|||
{ |
|||
public const string Format = "geo-json"; |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using GeoJSON.Net.Converters; |
|||
|
|||
namespace Squidex.Infrastructure.Json.Newtonsoft |
|||
{ |
|||
public sealed class WriteonlyGeoJsonConverter : GeoJsonConverter |
|||
{ |
|||
public override bool CanWrite => false; |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
|
|||
|
|||
namespace Squidex.Infrastructure.Queries |
|||
{ |
|||
public sealed record FilterSphere(double Longitude, double Latitude, double Radius) |
|||
{ |
|||
public override string ToString() |
|||
{ |
|||
return $"Radius({Longitude}, {Latitude}, {Radius})"; |
|||
} |
|||
} |
|||
} |
|||
@ -1,62 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Squidex.Domain.Apps.Core.Tags; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Queries; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Assets.Queries |
|||
{ |
|||
public class FilterTagTransformerTests |
|||
{ |
|||
private readonly ITagService tagService = A.Fake<ITagService>(); |
|||
private readonly DomainId appId = DomainId.NewGuid(); |
|||
|
|||
[Fact] |
|||
public async Task Should_normalize_tags() |
|||
{ |
|||
A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A<HashSet<string>>.That.Contains("name1"))) |
|||
.Returns(new Dictionary<string, string> { ["name1"] = "id1" }); |
|||
|
|||
var source = ClrFilter.Eq("tags", "name1"); |
|||
|
|||
var result = await FilterTagTransformer.TransformAsync(source, appId, tagService); |
|||
|
|||
Assert.Equal("tags == 'id1'", result!.ToString()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_fail_when_tags_not_found() |
|||
{ |
|||
A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A<HashSet<string>>.That.Contains("name1"))) |
|||
.Returns(new Dictionary<string, string>()); |
|||
|
|||
var source = ClrFilter.Eq("tags", "name1"); |
|||
|
|||
var result = await FilterTagTransformer.TransformAsync(source, appId, tagService); |
|||
|
|||
Assert.Equal("tags == 'name1'", result!.ToString()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_normalize_other_field() |
|||
{ |
|||
var source = ClrFilter.Eq("other", "value"); |
|||
|
|||
var result = await FilterTagTransformer.TransformAsync(source, appId, tagService); |
|||
|
|||
Assert.Equal("other == 'value'", result!.ToString()); |
|||
|
|||
A.CallTo(() => tagService.GetTagIdsAsync(appId, A<string>._, A<HashSet<string>>._)) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,104 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using FakeItEasy; |
|||
using Squidex.Domain.Apps.Core; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Core.Tags; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
using Squidex.Domain.Apps.Entities.TestHelpers; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Queries; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Queries |
|||
{ |
|||
public class FilterTagTransformerTests |
|||
{ |
|||
private readonly ITagService tagService = A.Fake<ITagService>(); |
|||
private readonly ISchemaEntity schema; |
|||
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); |
|||
private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); |
|||
|
|||
public FilterTagTransformerTests() |
|||
{ |
|||
var schemaDef = |
|||
new Schema("schema") |
|||
.AddTags(1, "tags1", Partitioning.Invariant) |
|||
.AddTags(2, "tags2", Partitioning.Invariant, new TagsFieldProperties { Normalization = TagsFieldNormalization.Schema }) |
|||
.AddString(3, "string", Partitioning.Invariant); |
|||
|
|||
schema = Mocks.Schema(appId, schemaId, schemaDef); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_normalize_tags() |
|||
{ |
|||
A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Schemas(schemaId.Id), A<HashSet<string>>.That.Contains("name1"))) |
|||
.Returns(new Dictionary<string, string> { ["name1"] = "id1" }); |
|||
|
|||
var source = ClrFilter.Eq("data.tags2.iv", "name1"); |
|||
|
|||
var result = FilterTagTransformer.TransformAsync(source, appId.Id, schema, tagService); |
|||
|
|||
Assert.Equal("data.tags2.iv == 'id1'", result!.ToString()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_fail_when_tags_not_found() |
|||
{ |
|||
A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.Contains("name1"))) |
|||
.Returns(new Dictionary<string, string>()); |
|||
|
|||
var source = ClrFilter.Eq("data.tags2.iv", "name1"); |
|||
|
|||
var result = FilterTagTransformer.TransformAsync(source, appId.Id, schema, tagService); |
|||
|
|||
Assert.Equal("data.tags2.iv == 'name1'", result!.ToString()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_normalize_other_tags_field() |
|||
{ |
|||
var source = ClrFilter.Eq("data.tags1.iv", "value"); |
|||
|
|||
var result = FilterTagTransformer.TransformAsync(source, appId.Id, schema, tagService); |
|||
|
|||
Assert.Equal("data.tags1.iv == 'value'", result!.ToString()); |
|||
|
|||
A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, A<string>._, A<HashSet<string>>._)) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_normalize_other_typed_field() |
|||
{ |
|||
var source = ClrFilter.Eq("data.string.iv", "value"); |
|||
|
|||
var result = FilterTagTransformer.TransformAsync(source, appId.Id, schema, tagService); |
|||
|
|||
Assert.Equal("data.string.iv == 'value'", result!.ToString()); |
|||
|
|||
A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, A<string>._, A<HashSet<string>>._)) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_normalize_non_data_field() |
|||
{ |
|||
var source = ClrFilter.Eq("no.data", "value"); |
|||
|
|||
var result = FilterTagTransformer.TransformAsync(source, appId.Id, schema, tagService); |
|||
|
|||
Assert.Equal("no.data == 'value'", result!.ToString()); |
|||
|
|||
A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, A<string>._, A<HashSet<string>>._)) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,398 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using NJsonSchema; |
|||
using Squidex.Infrastructure.Json.Objects; |
|||
using Squidex.Infrastructure.Queries.Json; |
|||
using Squidex.Infrastructure.TestHelpers; |
|||
using Squidex.Infrastructure.Validation; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Infrastructure.Queries |
|||
{ |
|||
public sealed class JsonQueryConversionTests |
|||
{ |
|||
private readonly List<string> errors = new List<string>(); |
|||
private readonly JsonSchema schema = new JsonSchema(); |
|||
|
|||
public JsonQueryConversionTests() |
|||
{ |
|||
var nested = new JsonSchemaProperty { Title = "nested", Type = JsonObjectType.Object }; |
|||
|
|||
nested.Properties["property"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.String |
|||
}; |
|||
|
|||
schema.Properties["boolean"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.Boolean |
|||
}; |
|||
|
|||
schema.Properties["datetime"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime |
|||
}; |
|||
|
|||
schema.Properties["guid"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.String, Format = JsonFormatStrings.Guid |
|||
}; |
|||
|
|||
schema.Properties["integer"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.Integer |
|||
}; |
|||
|
|||
schema.Properties["number"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.Number |
|||
}; |
|||
|
|||
schema.Properties["string"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.String |
|||
}; |
|||
|
|||
schema.Properties["stringArray"] = new JsonSchemaProperty |
|||
{ |
|||
Item = new JsonSchema |
|||
{ |
|||
Type = JsonObjectType.String |
|||
}, |
|||
Type = JsonObjectType.Array |
|||
}; |
|||
|
|||
schema.Properties["object"] = nested; |
|||
|
|||
schema.Properties["reference"] = new JsonSchemaProperty |
|||
{ |
|||
Reference = nested |
|||
}; |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_property_does_not_exist() |
|||
{ |
|||
var json = new { path = "notfound", op = "eq", value = 1 }; |
|||
|
|||
AssertErrors(json, "Path 'notfound' does not point to a valid property in the model."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_nested_property_does_not_exist() |
|||
{ |
|||
var json = new { path = "object.notfound", op = "eq", value = 1 }; |
|||
|
|||
AssertErrors(json, "'notfound' is not a property of 'nested'."); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("contains", "contains(datetime, 2012-11-10T09:08:07Z)")] |
|||
[InlineData("empty", "empty(datetime)")] |
|||
[InlineData("endswith", "endsWith(datetime, 2012-11-10T09:08:07Z)")] |
|||
[InlineData("eq", "datetime == 2012-11-10T09:08:07Z")] |
|||
[InlineData("ge", "datetime >= 2012-11-10T09:08:07Z")] |
|||
[InlineData("gt", "datetime > 2012-11-10T09:08:07Z")] |
|||
[InlineData("le", "datetime <= 2012-11-10T09:08:07Z")] |
|||
[InlineData("lt", "datetime < 2012-11-10T09:08:07Z")] |
|||
[InlineData("ne", "datetime != 2012-11-10T09:08:07Z")] |
|||
[InlineData("startswith", "startsWith(datetime, 2012-11-10T09:08:07Z)")] |
|||
public void Should_parse_datetime_string_filter(string op, string expected) |
|||
{ |
|||
var json = new { path = "datetime", op, value = "2012-11-10T09:08:07Z" }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_date_string_filter() |
|||
{ |
|||
var json = new { path = "datetime", op = "eq", value = "2012-11-10" }; |
|||
|
|||
AssertFilter(json, "datetime == 2012-11-10T00:00:00Z"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_datetime_string_property_got_invalid_string_value() |
|||
{ |
|||
var json = new { path = "datetime", op = "eq", value = "invalid" }; |
|||
|
|||
AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got invalid String."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_datetime_string_property_got_invalid_value() |
|||
{ |
|||
var json = new { path = "datetime", op = "eq", value = 1 }; |
|||
|
|||
AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got Number."); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("contains", "contains(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] |
|||
[InlineData("empty", "empty(guid)")] |
|||
[InlineData("endswith", "endsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] |
|||
[InlineData("eq", "guid == bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] |
|||
[InlineData("ge", "guid >= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] |
|||
[InlineData("gt", "guid > bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] |
|||
[InlineData("le", "guid <= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] |
|||
[InlineData("lt", "guid < bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] |
|||
[InlineData("ne", "guid != bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] |
|||
[InlineData("startswith", "startsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] |
|||
public void Should_parse_guid_string_filter(string op, string expected) |
|||
{ |
|||
var json = new { path = "guid", op, value = "bf57d32c-d4dd-4217-8c16-6dcb16975cf3" }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_guid_string_property_got_invalid_string_value() |
|||
{ |
|||
var json = new { path = "guid", op = "eq", value = "invalid" }; |
|||
|
|||
AssertErrors(json, "Expected Guid String for path 'guid', but got invalid String."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_guid_string_property_got_invalid_value() |
|||
{ |
|||
var json = new { path = "guid", op = "eq", value = 1 }; |
|||
|
|||
AssertErrors(json, "Expected Guid String for path 'guid', but got Number."); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("contains", "contains(string, 'Hello')")] |
|||
[InlineData("empty", "empty(string)")] |
|||
[InlineData("endswith", "endsWith(string, 'Hello')")] |
|||
[InlineData("eq", "string == 'Hello'")] |
|||
[InlineData("ge", "string >= 'Hello'")] |
|||
[InlineData("gt", "string > 'Hello'")] |
|||
[InlineData("le", "string <= 'Hello'")] |
|||
[InlineData("lt", "string < 'Hello'")] |
|||
[InlineData("ne", "string != 'Hello'")] |
|||
[InlineData("startswith", "startsWith(string, 'Hello')")] |
|||
public void Should_parse_string_filter(string op, string expected) |
|||
{ |
|||
var json = new { path = "string", op, value = "Hello" }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_string_property_got_invalid_value() |
|||
{ |
|||
var json = new { path = "string", op = "eq", value = 1 }; |
|||
|
|||
AssertErrors(json, "Expected String for path 'string', but got Number."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_string_in_filter() |
|||
{ |
|||
var json = new { path = "string", op = "in", value = new[] { "Hello" } }; |
|||
|
|||
AssertFilter(json, "string in ['Hello']"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_nested_string_filter() |
|||
{ |
|||
var json = new { path = "object.property", op = "in", value = new[] { "Hello" } }; |
|||
|
|||
AssertFilter(json, "object.property in ['Hello']"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_referenced_string_filter() |
|||
{ |
|||
var json = new { path = "reference.property", op = "in", value = new[] { "Hello" } }; |
|||
|
|||
AssertFilter(json, "reference.property in ['Hello']"); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("eq", "number == 12")] |
|||
[InlineData("ge", "number >= 12")] |
|||
[InlineData("gt", "number > 12")] |
|||
[InlineData("le", "number <= 12")] |
|||
[InlineData("lt", "number < 12")] |
|||
[InlineData("ne", "number != 12")] |
|||
public void Should_parse_number_filter(string op, string expected) |
|||
{ |
|||
var json = new { path = "number", op, value = 12 }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_number_property_got_invalid_value() |
|||
{ |
|||
var json = new { path = "number", op = "eq", value = true }; |
|||
|
|||
AssertErrors(json, "Expected Number for path 'number', but got Boolean."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_number_in_filter() |
|||
{ |
|||
var json = new { path = "number", op = "in", value = new[] { 12 } }; |
|||
|
|||
AssertFilter(json, "number in [12]"); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("eq", "boolean == True")] |
|||
[InlineData("ne", "boolean != True")] |
|||
public void Should_parse_boolean_filter(string op, string expected) |
|||
{ |
|||
var json = new { path = "boolean", op, value = true }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_boolean_property_got_invalid_value() |
|||
{ |
|||
var json = new { path = "boolean", op = "eq", value = 1 }; |
|||
|
|||
AssertErrors(json, "Expected Boolean for path 'boolean', but got Number."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_boolean_in_filter() |
|||
{ |
|||
var json = new { path = "boolean", op = "in", value = new[] { true } }; |
|||
|
|||
AssertFilter(json, "boolean in [True]"); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("empty", "empty(stringArray)")] |
|||
[InlineData("eq", "stringArray == 'Hello'")] |
|||
[InlineData("ne", "stringArray != 'Hello'")] |
|||
public void Should_parse_array_filter(string op, string expected) |
|||
{ |
|||
var json = new { path = "stringArray", op, value = "Hello" }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_array_in_filter() |
|||
{ |
|||
var json = new { path = "stringArray", op = "in", value = new[] { "Hello" } }; |
|||
|
|||
AssertFilter(json, "stringArray in ['Hello']"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_when_using_array_value_for_non_allowed_operator() |
|||
{ |
|||
var json = new { path = "string", op = "eq", value = new[] { "Hello" } }; |
|||
|
|||
AssertErrors(json, "Array value is not allowed for 'Equals' operator and path 'string'."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_query() |
|||
{ |
|||
var json = new { skip = 10, take = 20, FullText = "Hello", Filter = new { path = "string", op = "eq", value = "Hello" } }; |
|||
|
|||
AssertQuery(json, "Filter: string == 'Hello'; FullText: 'Hello'; Skip: 10; Take: 20"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_query_with_sorting() |
|||
{ |
|||
var json = new { sort = new[] { new { path = "string", order = "ascending" } } }; |
|||
|
|||
AssertQuery(json, "Sort: string Ascending"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_throw_exception_for_invalid_query() |
|||
{ |
|||
var json = new { sort = new[] { new { path = "invalid", order = "ascending" } } }; |
|||
|
|||
Assert.Throws<ValidationException>(() => AssertQuery(json, null)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_throw_exception_when_parsing_invalid_json() |
|||
{ |
|||
var json = "invalid"; |
|||
|
|||
Assert.Throws<ValidationException>(() => AssertQuery(json, null)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_throw_exception_when_parsing_null_string() |
|||
{ |
|||
string? json = null; |
|||
|
|||
Assert.NotNull(schema.Parse(json!, JsonHelper.DefaultSerializer)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_throw_exception_when_parsing_null_json() |
|||
{ |
|||
var json = "null"; |
|||
|
|||
Assert.NotNull(schema.Parse(json, JsonHelper.DefaultSerializer)); |
|||
} |
|||
|
|||
private void AssertQuery(object json, string? expectedFilter) |
|||
{ |
|||
var filter = ConvertQuery(json); |
|||
|
|||
Assert.Empty(errors); |
|||
|
|||
Assert.Equal(expectedFilter, filter); |
|||
} |
|||
|
|||
private void AssertFilter(object json, string? expectedFilter) |
|||
{ |
|||
var filter = ConvertFilter(json); |
|||
|
|||
Assert.Empty(errors); |
|||
|
|||
Assert.Equal(expectedFilter, filter); |
|||
} |
|||
|
|||
private void AssertErrors(object json, params string[] expectedErrors) |
|||
{ |
|||
var filter = ConvertFilter(json); |
|||
|
|||
Assert.Equal(expectedErrors.ToList(), errors); |
|||
|
|||
Assert.Null(filter); |
|||
} |
|||
|
|||
private string? ConvertFilter<T>(T value) |
|||
{ |
|||
var json = JsonHelper.DefaultSerializer.Serialize(value, true); |
|||
|
|||
var jsonFilter = JsonHelper.DefaultSerializer.Deserialize<FilterNode<IJsonValue>>(json); |
|||
|
|||
return JsonFilterVisitor.Parse(jsonFilter, schema, errors)?.ToString(); |
|||
} |
|||
|
|||
private string? ConvertQuery<T>(T value) |
|||
{ |
|||
var json = JsonHelper.DefaultSerializer.Serialize(value, true); |
|||
|
|||
var jsonQuery = schema.Parse(json, JsonHelper.DefaultSerializer); |
|||
|
|||
return jsonQuery.ToString(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,651 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using NJsonSchema; |
|||
using Squidex.Infrastructure.Json; |
|||
using Squidex.Infrastructure.Json.Objects; |
|||
using Squidex.Infrastructure.Queries.Json; |
|||
using Squidex.Infrastructure.TestHelpers; |
|||
using Squidex.Infrastructure.Validation; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Infrastructure.Queries |
|||
{ |
|||
public sealed class QueryFromJsonTests |
|||
{ |
|||
private static readonly (string Name, string Operator, string Output)[] AllOps = |
|||
{ |
|||
("Contains", "contains", "contains($FIELD, $VALUE)"), |
|||
("Empty", "empty", "empty($FIELD)"), |
|||
("EndsWith", "endswith", "endsWith($FIELD, $VALUE)"), |
|||
("Equals", "eq", "$FIELD == $VALUE"), |
|||
("GreaterThanOrEqual", "ge", "$FIELD >= $VALUE"), |
|||
("GreaterThan", "gt", "$FIELD > $VALUE"), |
|||
("LessThanOrEqual", "le", "$FIELD <= $VALUE"), |
|||
("LessThan", "lt", "$FIELD < $VALUE"), |
|||
("NotEquals", "ne", "$FIELD != $VALUE"), |
|||
("StartsWith", "startswith", "startsWith($FIELD, $VALUE)") |
|||
}; |
|||
|
|||
private static readonly JsonSchema Schema = new JsonSchema(); |
|||
|
|||
static QueryFromJsonTests() |
|||
{ |
|||
var nested = new JsonSchemaProperty { Title = "nested", Type = JsonObjectType.Object }; |
|||
|
|||
nested.Properties["property"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.String |
|||
}; |
|||
|
|||
Schema.Properties["boolean"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.Boolean |
|||
}; |
|||
|
|||
Schema.Properties["datetime"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime |
|||
}; |
|||
|
|||
Schema.Properties["guid"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.String, Format = JsonFormatStrings.Guid |
|||
}; |
|||
|
|||
Schema.Properties["integer"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.Integer |
|||
}; |
|||
|
|||
Schema.Properties["number"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.Number |
|||
}; |
|||
|
|||
Schema.Properties["json"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.None |
|||
}; |
|||
|
|||
Schema.Properties["geo"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.Object, Format = GeoJson.Format |
|||
}; |
|||
|
|||
Schema.Properties["reference"] = new JsonSchemaProperty |
|||
{ |
|||
Reference = nested |
|||
}; |
|||
|
|||
Schema.Properties["string"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.String |
|||
}; |
|||
|
|||
Schema.Properties["geoRef"] = new JsonSchemaProperty |
|||
{ |
|||
Reference = new JsonSchema |
|||
{ |
|||
Format = GeoJson.Format |
|||
}, |
|||
}; |
|||
|
|||
Schema.Properties["stringArray"] = new JsonSchemaProperty |
|||
{ |
|||
Item = new JsonSchema |
|||
{ |
|||
Type = JsonObjectType.String |
|||
}, |
|||
Type = JsonObjectType.Array |
|||
}; |
|||
|
|||
Schema.Properties["object"] = nested; |
|||
} |
|||
|
|||
public class DateTime |
|||
{ |
|||
public static IEnumerable<object[]> ValidTests() |
|||
{ |
|||
const string value = "2012-11-10T09:08:07Z"; |
|||
|
|||
return BuildTests("datetime", x => true, value, value); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(ValidTests))] |
|||
public void Should_parse_filter(string field, string op, string value, string expected) |
|||
{ |
|||
var json = new { path = field, op, value }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_value_is_invalid() |
|||
{ |
|||
var json = new { path = "datetime", op = "eq", value = "invalid" }; |
|||
|
|||
AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got invalid String."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_value_type_is_invalid() |
|||
{ |
|||
var json = new { path = "datetime", op = "eq", value = 1 }; |
|||
|
|||
AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got Number."); |
|||
} |
|||
} |
|||
|
|||
public class Guid |
|||
{ |
|||
public static IEnumerable<object[]> ValidTests() |
|||
{ |
|||
const string value = "bf57d32c-d4dd-4217-8c16-6dcb16975cf3"; |
|||
|
|||
return BuildTests("guid", x => true, value, value); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(ValidTests))] |
|||
public void Should_parse_filter(string field, string op, string value, string expected) |
|||
{ |
|||
var json = new { path = field, op, value }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_value_is_invalid() |
|||
{ |
|||
var json = new { path = "guid", op = "eq", value = "invalid" }; |
|||
|
|||
AssertErrors(json, "Expected Guid String for path 'guid', but got invalid String."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_value_type_is_invalid() |
|||
{ |
|||
var json = new { path = "guid", op = "eq", value = 1 }; |
|||
|
|||
AssertErrors(json, "Expected Guid String for path 'guid', but got Number."); |
|||
} |
|||
} |
|||
|
|||
public class String |
|||
{ |
|||
public static IEnumerable<object[]> ValidTests() |
|||
{ |
|||
const string value = "Hello"; |
|||
|
|||
return BuildTests("string", x => true, value, $"'{value}'"); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> ValidInTests() |
|||
{ |
|||
const string value = "Hello"; |
|||
|
|||
return BuildInTests("string", value, $"'{value}'"); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(ValidTests))] |
|||
public void Should_parse_filter(string field, string op, string value, string expected) |
|||
{ |
|||
var json = new { path = field, op, value }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(ValidInTests))] |
|||
public void Should_parse_in_filter(string field, string value, string expected) |
|||
{ |
|||
var json = new { path = field, op = "in", value = new[] { value } }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_value_type_is_invalid() |
|||
{ |
|||
var json = new { path = "string", op = "eq", value = 1 }; |
|||
|
|||
AssertErrors(json, "Expected String for path 'string', but got Number."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_nested_string_filter() |
|||
{ |
|||
var json = new { path = "object.property", op = "in", value = new[] { "Hello" } }; |
|||
|
|||
AssertFilter(json, "object.property in ['Hello']"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_referenced_string_filter() |
|||
{ |
|||
var json = new { path = "reference.property", op = "in", value = new[] { "Hello" } }; |
|||
|
|||
AssertFilter(json, "reference.property in ['Hello']"); |
|||
} |
|||
} |
|||
|
|||
public class Geo |
|||
{ |
|||
public static IEnumerable<object[]> ValidTests() |
|||
{ |
|||
var value = new { longitude = 10, latitude = 20, distance = 30 }; |
|||
|
|||
return BuildFlatTests("geo", x => x == "lt", value, $"Radius({value.longitude}, {value.latitude}, {value.distance})"); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> ValidRefTests() |
|||
{ |
|||
var value = new { longitude = 10, latitude = 20, distance = 30 }; |
|||
|
|||
return BuildFlatTests("geoRef", x => x == "lt", value, $"Radius({value.longitude}, {value.latitude}, {value.distance})"); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> InvalidTests() |
|||
{ |
|||
var value = new { longitude = 10, latitude = 20, distance = 30 }; |
|||
|
|||
return BuildInvalidOperatorTests("geo", x => x == "lt", value); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(ValidTests))] |
|||
public void Should_parse_filter(string field, string op, object value, string expected) |
|||
{ |
|||
var json = new { path = field, op, value }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(ValidRefTests))] |
|||
public void Should_parse_filter_with_reference(string field, string op, object value, string expected) |
|||
{ |
|||
var json = new { path = field, op, value }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(InvalidTests))] |
|||
public void Should_add_error_if_operator_is_invalid(string field, string op, object value, string expected) |
|||
{ |
|||
var json = new { path = field, op, value }; |
|||
|
|||
AssertErrors(json, $"'{expected}' is not a valid operator for type Object(geo-json) at '{field}'."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_value_is_invalid() |
|||
{ |
|||
var json = new { path = "geo", op = "lt", value = new { latitude = 10, longitude = 20 } }; |
|||
|
|||
AssertErrors(json, "Expected Object(geo-json) for path 'geo', but got Object."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_value_type_is_invalid() |
|||
{ |
|||
var json = new { path = "geo", op = "lt", value = 1 }; |
|||
|
|||
AssertErrors(json, "Expected Object(geo-json) for path 'geo', but got Number."); |
|||
} |
|||
} |
|||
|
|||
public class Number |
|||
{ |
|||
public static IEnumerable<object[]> ValidTests() |
|||
{ |
|||
const int value = 12; |
|||
|
|||
return BuildTests("number", x => x.Length == 2, value, $"{value}"); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> InvalidTests() |
|||
{ |
|||
const int value = 12; |
|||
|
|||
return BuildInvalidOperatorTests("number", x => x.Length == 2, $"{value}"); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> ValidInTests() |
|||
{ |
|||
const int value = 12; |
|||
|
|||
return BuildInTests("number", value, $"{value}"); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(ValidTests))] |
|||
public void Should_parse_filter(string field, string op, int value, string expected) |
|||
{ |
|||
var json = new { path = field, op, value }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(InvalidTests))] |
|||
public void Should_add_error_if_operator_is_invalid(string field, string op, int value, string expected) |
|||
{ |
|||
var json = new { path = field, op, value }; |
|||
|
|||
AssertErrors(json, $"'{expected}' is not a valid operator for type Number at '{field}'."); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(ValidInTests))] |
|||
public void Should_parse_in_filter(string field, int value, string expected) |
|||
{ |
|||
var json = new { path = field, op = "in", value = new[] { value } }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_value_type_is_invalid() |
|||
{ |
|||
var json = new { path = "number", op = "eq", value = true }; |
|||
|
|||
AssertErrors(json, "Expected Number for path 'number', but got Boolean."); |
|||
} |
|||
} |
|||
|
|||
public class Boolean |
|||
{ |
|||
public static IEnumerable<object[]> ValidTests() |
|||
{ |
|||
const bool value = true; |
|||
|
|||
return BuildTests("boolean", x => x == "eq" || x == "ne", value, $"{value}"); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> InvalidTests() |
|||
{ |
|||
const bool value = true; |
|||
|
|||
return BuildInvalidOperatorTests("boolean", x => x == "eq" || x == "ne", value); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> ValidInTests() |
|||
{ |
|||
const bool value = true; |
|||
|
|||
return BuildInTests("boolean", value, $"{value}"); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(ValidTests))] |
|||
public void Should_parse_filter(string field, string op, bool value, string expected) |
|||
{ |
|||
var json = new { path = field, op, value }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(InvalidTests))] |
|||
public void Should_add_error_if_operator_is_invalid(string field, string op, bool value, string expected) |
|||
{ |
|||
var json = new { path = field, op, value }; |
|||
|
|||
AssertErrors(json, $"'{expected}' is not a valid operator for type Boolean at '{field}'."); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(ValidInTests))] |
|||
public void Should_parse_in_filter(string field, bool value, string expected) |
|||
{ |
|||
var json = new { path = field, op = "in", value = new[] { value } }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_boolean_property_got_invalid_value() |
|||
{ |
|||
var json = new { path = "boolean", op = "eq", value = 1 }; |
|||
|
|||
AssertErrors(json, "Expected Boolean for path 'boolean', but got Number."); |
|||
} |
|||
} |
|||
|
|||
public class Array |
|||
{ |
|||
public static IEnumerable<object[]> ValiedTests() |
|||
{ |
|||
const string value = "Hello"; |
|||
|
|||
return BuildTests("stringArray", x => x == "eq" || x == "ne" || x == "empty", value, $"'{value}'"); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> ValidInTests() |
|||
{ |
|||
const string value = "Hello"; |
|||
|
|||
return BuildInTests("stringArray", value, $"'{value}'"); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(ValiedTests))] |
|||
public void Should_parse_array_filter(string field, string op, string value, string expected) |
|||
{ |
|||
var json = new { path = field, op, value }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(ValidInTests))] |
|||
public void Should_parse_array_in_filter(string field, string value, string expected) |
|||
{ |
|||
var json = new { path = field, op = "in", value = new[] { value } }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_when_using_array_value_for_non_allowed_operator() |
|||
{ |
|||
var json = new { path = "string", op = "eq", value = new[] { "Hello" } }; |
|||
|
|||
AssertErrors(json, "Array value is not allowed for 'Equals' operator and path 'string'."); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_property_does_not_exist() |
|||
{ |
|||
var json = new { path = "notfound", op = "eq", value = 1 }; |
|||
|
|||
AssertErrors(json, "Path 'notfound' does not point to a valid property in the model."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_nested_property_does_not_exist() |
|||
{ |
|||
var json = new { path = "object.notfound", op = "eq", value = 1 }; |
|||
|
|||
AssertErrors(json, "'notfound' is not a property of 'nested'."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_nested_reference_property_does_not_exist() |
|||
{ |
|||
var json = new { path = "reference.notfound", op = "eq", value = 1 }; |
|||
|
|||
AssertErrors(json, "'notfound' is not a property of 'nested'."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_query() |
|||
{ |
|||
var json = new { skip = 10, take = 20, FullText = "Hello", Filter = new { path = "string", op = "eq", value = "Hello" } }; |
|||
|
|||
AssertQuery(json, "Filter: string == 'Hello'; FullText: 'Hello'; Skip: 10; Take: 20"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_query_with_top() |
|||
{ |
|||
var json = new { skip = 10, top = 20, FullText = "Hello", Filter = new { path = "string", op = "eq", value = "Hello" } }; |
|||
|
|||
AssertQuery(json, "Filter: string == 'Hello'; FullText: 'Hello'; Skip: 10; Take: 20"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_query_with_sorting() |
|||
{ |
|||
var json = new { sort = new[] { new { path = "string", order = "ascending" } } }; |
|||
|
|||
AssertQuery(json, "Sort: string Ascending"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_throw_exception_for_invalid_query() |
|||
{ |
|||
var json = new { sort = new[] { new { path = "invalid", order = "ascending" } } }; |
|||
|
|||
Assert.Throws<ValidationException>(() => AssertQuery(json, null)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_throw_exception_when_parsing_invalid_json() |
|||
{ |
|||
var json = "invalid"; |
|||
|
|||
Assert.Throws<ValidationException>(() => AssertQuery(json, null)); |
|||
} |
|||
|
|||
private static void AssertQuery(object json, string? expectedFilter) |
|||
{ |
|||
var errors = new List<string>(); |
|||
|
|||
var filter = ConvertQuery(json); |
|||
|
|||
Assert.Empty(errors); |
|||
Assert.Equal(expectedFilter, filter); |
|||
} |
|||
|
|||
private static void AssertFilter(object json, string? expectedFilter) |
|||
{ |
|||
var errors = new List<string>(); |
|||
|
|||
var filter = ConvertFilter(json, errors); |
|||
|
|||
Assert.Empty(errors); |
|||
Assert.Equal(expectedFilter, filter); |
|||
} |
|||
|
|||
private static void AssertErrors(object json, string expectedError) |
|||
{ |
|||
var errors = new List<string>(); |
|||
|
|||
var filter = ConvertFilter(json, errors); |
|||
|
|||
Assert.Equal(expectedError, errors.FirstOrDefault()); |
|||
Assert.Null(filter); |
|||
} |
|||
|
|||
private static string? ConvertFilter<T>(T value, List<string> errors) |
|||
{ |
|||
var json = TestUtils.DefaultSerializer.Serialize(value, true); |
|||
|
|||
var jsonFilter = TestUtils.DefaultSerializer.Deserialize<FilterNode<IJsonValue>>(json); |
|||
|
|||
return JsonFilterVisitor.Parse(jsonFilter, Schema, errors)?.ToString(); |
|||
} |
|||
|
|||
private static string? ConvertQuery<T>(T value) |
|||
{ |
|||
var json = TestUtils.DefaultSerializer.Serialize(value, true); |
|||
|
|||
var jsonFilter = Schema.Parse(json, TestUtils.DefaultSerializer); |
|||
|
|||
return jsonFilter.ToString(); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> BuildFlatTests(string field, Predicate<string> opFilter, object value, string valueString) |
|||
{ |
|||
var fields = new[] |
|||
{ |
|||
$"{field}" |
|||
}; |
|||
|
|||
foreach (var f in fields) |
|||
{ |
|||
foreach (var (_, op, output) in AllOps.Where(x => opFilter(x.Operator))) |
|||
{ |
|||
var expected = |
|||
output |
|||
.Replace("$FIELD", f) |
|||
.Replace("$VALUE", valueString); |
|||
|
|||
yield return new[] { f, op, value, expected }; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public static IEnumerable<object[]> BuildTests(string field, Predicate<string> opFilter, object value, string valueString) |
|||
{ |
|||
var fields = new[] |
|||
{ |
|||
$"{field}", |
|||
$"json.{field}", |
|||
$"json.nested.{field}" |
|||
}; |
|||
|
|||
foreach (var f in fields) |
|||
{ |
|||
foreach (var (_, op, output) in AllOps.Where(x => opFilter(x.Operator))) |
|||
{ |
|||
var expected = |
|||
output |
|||
.Replace("$FIELD", f) |
|||
.Replace("$VALUE", valueString); |
|||
|
|||
yield return new[] { f, op, value, expected }; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public static IEnumerable<object[]> BuildInTests(string field, object value, string valueString) |
|||
{ |
|||
var fields = new[] |
|||
{ |
|||
$"{field}", |
|||
$"json.{field}", |
|||
$"json.nested.{field}" |
|||
}; |
|||
|
|||
foreach (var f in fields) |
|||
{ |
|||
var expected = $"{f} in [{valueString}]"; |
|||
|
|||
yield return new[] { f, value, expected }; |
|||
} |
|||
} |
|||
|
|||
public static IEnumerable<object[]> BuildInvalidOperatorTests(string field, Predicate<string> opFilter, object value) |
|||
{ |
|||
foreach (var (name, op, _) in AllOps.Where(x => !opFilter(x.Operator))) |
|||
{ |
|||
yield return new[] { field, op, value, name }; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,489 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using NJsonSchema; |
|||
using Squidex.Infrastructure.Json.Objects; |
|||
using Squidex.Infrastructure.Queries.Json; |
|||
using Squidex.Infrastructure.TestHelpers; |
|||
using Squidex.Infrastructure.Validation; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Infrastructure.Queries |
|||
{ |
|||
public sealed class QueryJsonConversionTests |
|||
{ |
|||
private static readonly (string Operator, string Output)[] AllOps = |
|||
{ |
|||
("contains", "contains($FIELD, $VALUE)"), |
|||
("empty", "empty($FIELD)"), |
|||
("endswith", "endsWith($FIELD, $VALUE)"), |
|||
("eq", "$FIELD == $VALUE"), |
|||
("ge", "$FIELD >= $VALUE"), |
|||
("gt", "$FIELD > $VALUE"), |
|||
("le", "$FIELD <= $VALUE"), |
|||
("lt", "$FIELD < $VALUE"), |
|||
("ne", "$FIELD != $VALUE"), |
|||
("startswith", "startsWith($FIELD, $VALUE)") |
|||
}; |
|||
|
|||
private readonly List<string> errors = new List<string>(); |
|||
private readonly JsonSchema schema = new JsonSchema(); |
|||
|
|||
public QueryJsonConversionTests() |
|||
{ |
|||
var nested = new JsonSchemaProperty { Title = "nested", Type = JsonObjectType.Object }; |
|||
|
|||
nested.Properties["property"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.String |
|||
}; |
|||
|
|||
schema.Properties["boolean"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.Boolean |
|||
}; |
|||
|
|||
schema.Properties["datetime"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime |
|||
}; |
|||
|
|||
schema.Properties["guid"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.String, Format = JsonFormatStrings.Guid |
|||
}; |
|||
|
|||
schema.Properties["integer"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.Integer |
|||
}; |
|||
|
|||
schema.Properties["number"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.Number |
|||
}; |
|||
|
|||
schema.Properties["json"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.None |
|||
}; |
|||
|
|||
schema.Properties["string"] = new JsonSchemaProperty |
|||
{ |
|||
Type = JsonObjectType.String |
|||
}; |
|||
|
|||
schema.Properties["stringArray"] = new JsonSchemaProperty |
|||
{ |
|||
Item = new JsonSchema |
|||
{ |
|||
Type = JsonObjectType.String |
|||
}, |
|||
Type = JsonObjectType.Array |
|||
}; |
|||
|
|||
schema.Properties["object"] = nested; |
|||
|
|||
schema.Properties["reference"] = new JsonSchemaProperty |
|||
{ |
|||
Reference = nested |
|||
}; |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_property_does_not_exist() |
|||
{ |
|||
var json = new { path = "notfound", op = "eq", value = 1 }; |
|||
|
|||
AssertErrors(json, "Path 'notfound' does not point to a valid property in the model."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_nested_property_does_not_exist() |
|||
{ |
|||
var json = new { path = "object.notfound", op = "eq", value = 1 }; |
|||
|
|||
AssertErrors(json, "'notfound' is not a property of 'nested'."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_nested_reference_property_does_not_exist() |
|||
{ |
|||
var json = new { path = "reference.notfound", op = "eq", value = 1 }; |
|||
|
|||
AssertErrors(json, "'notfound' is not a property of 'nested'."); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> DateTimeTests() |
|||
{ |
|||
const string value = "2012-11-10T09:08:07Z"; |
|||
|
|||
return BuildTests("datetime", x => true, value, value); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(DateTimeTests))] |
|||
public void Should_parse_datetime_string_filter(string field, string op, string value, string expected) |
|||
{ |
|||
var json = new { path = field, op, value }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_datetime_string_property_got_invalid_string_value() |
|||
{ |
|||
var json = new { path = "datetime", op = "eq", value = "invalid" }; |
|||
|
|||
AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got invalid String."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_datetime_string_property_got_invalid_value() |
|||
{ |
|||
var json = new { path = "datetime", op = "eq", value = 1 }; |
|||
|
|||
AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got Number."); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> GuidTests() |
|||
{ |
|||
const string value = "bf57d32c-d4dd-4217-8c16-6dcb16975cf3"; |
|||
|
|||
return BuildTests("guid", x => true, value, value); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(GuidTests))] |
|||
public void Should_parse_guid_string_filter(string field, string op, string value, string expected) |
|||
{ |
|||
var json = new { path = field, op, value }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_guid_string_property_got_invalid_string_value() |
|||
{ |
|||
var json = new { path = "guid", op = "eq", value = "invalid" }; |
|||
|
|||
AssertErrors(json, "Expected Guid String for path 'guid', but got invalid String."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_guid_string_property_got_invalid_value() |
|||
{ |
|||
var json = new { path = "guid", op = "eq", value = 1 }; |
|||
|
|||
AssertErrors(json, "Expected Guid String for path 'guid', but got Number."); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> StringTests() |
|||
{ |
|||
const string value = "Hello"; |
|||
|
|||
return BuildTests("string", x => true, value, $"'{value}'"); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(StringTests))] |
|||
public void Should_parse_string_filter(string field, string op, string value, string expected) |
|||
{ |
|||
var json = new { path = field, op, value }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> StringInTests() |
|||
{ |
|||
const string value = "Hello"; |
|||
|
|||
return BuildInTests("string", value, $"'{value}'"); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(StringInTests))] |
|||
public void Should_parse_string_in_filter(string field, string value, string expected) |
|||
{ |
|||
var json = new { path = field, op = "in", value = new[] { value } }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_string_property_got_invalid_value() |
|||
{ |
|||
var json = new { path = "string", op = "eq", value = 1 }; |
|||
|
|||
AssertErrors(json, "Expected String for path 'string', but got Number."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_nested_string_filter() |
|||
{ |
|||
var json = new { path = "object.property", op = "in", value = new[] { "Hello" } }; |
|||
|
|||
AssertFilter(json, "object.property in ['Hello']"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_referenced_string_filter() |
|||
{ |
|||
var json = new { path = "reference.property", op = "in", value = new[] { "Hello" } }; |
|||
|
|||
AssertFilter(json, "reference.property in ['Hello']"); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> NumberTests() |
|||
{ |
|||
const int value = 12; |
|||
|
|||
return BuildTests("number", x => x.Length == 2, value, $"{value}"); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(NumberTests))] |
|||
public void Should_parse_number_filter(string field, string op, int value, string expected) |
|||
{ |
|||
var json = new { path = field, op, value }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> NumberInTests() |
|||
{ |
|||
const int value = 12; |
|||
|
|||
return BuildInTests("number", value, $"{value}"); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(NumberInTests))] |
|||
public void Should_parse_number_in_filter(string field, int value, string expected) |
|||
{ |
|||
var json = new { path = field, op = "in", value = new[] { value } }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_number_property_got_invalid_value() |
|||
{ |
|||
var json = new { path = "number", op = "eq", value = true }; |
|||
|
|||
AssertErrors(json, "Expected Number for path 'number', but got Boolean."); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> BooleanTests() |
|||
{ |
|||
const bool value = true; |
|||
|
|||
return BuildTests("boolean", x => x == "eq" || x == "ne", value, $"{value}"); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(BooleanTests))] |
|||
public void Should_parse_boolean_filter(string field, string op, bool value, string expected) |
|||
{ |
|||
var json = new { path = field, op, value }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> BooleanInTests() |
|||
{ |
|||
const bool value = true; |
|||
|
|||
return BuildInTests("boolean", value, $"{value}"); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(BooleanInTests))] |
|||
public void Should_parse_boolean_in_filter(string field, bool value, string expected) |
|||
{ |
|||
var json = new { path = field, op = "in", value = new[] { value } }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_boolean_property_got_invalid_value() |
|||
{ |
|||
var json = new { path = "boolean", op = "eq", value = 1 }; |
|||
|
|||
AssertErrors(json, "Expected Boolean for path 'boolean', but got Number."); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> ArrayTests() |
|||
{ |
|||
const string value = "Hello"; |
|||
|
|||
return BuildTests("stringArray", x => x == "eq" || x == "ne" || x == "empty", value, $"'{value}'"); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(ArrayTests))] |
|||
public void Should_parse_array_filter(string field, string op, string value, string expected) |
|||
{ |
|||
var json = new { path = field, op, value }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> ArrayInTests() |
|||
{ |
|||
const string value = "Hello"; |
|||
|
|||
return BuildInTests("stringArray", value, $"'{value}'"); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(ArrayInTests))] |
|||
public void Should_parse_array_in_filter(string field, string value, string expected) |
|||
{ |
|||
var json = new { path = field, op = "in", value = new[] { value } }; |
|||
|
|||
AssertFilter(json, expected); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_when_using_array_value_for_non_allowed_operator() |
|||
{ |
|||
var json = new { path = "string", op = "eq", value = new[] { "Hello" } }; |
|||
|
|||
AssertErrors(json, "Array value is not allowed for 'Equals' operator and path 'string'."); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_query() |
|||
{ |
|||
var json = new { skip = 10, take = 20, FullText = "Hello", Filter = new { path = "string", op = "eq", value = "Hello" } }; |
|||
|
|||
AssertQuery(json, "Filter: string == 'Hello'; FullText: 'Hello'; Skip: 10; Take: 20"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_query_with_top() |
|||
{ |
|||
var json = new { skip = 10, top = 20, FullText = "Hello", Filter = new { path = "string", op = "eq", value = "Hello" } }; |
|||
|
|||
AssertQuery(json, "Filter: string == 'Hello'; FullText: 'Hello'; Skip: 10; Take: 20"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_query_with_sorting() |
|||
{ |
|||
var json = new { sort = new[] { new { path = "string", order = "ascending" } } }; |
|||
|
|||
AssertQuery(json, "Sort: string Ascending"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_throw_exception_for_invalid_query() |
|||
{ |
|||
var json = new { sort = new[] { new { path = "invalid", order = "ascending" } } }; |
|||
|
|||
Assert.Throws<ValidationException>(() => AssertQuery(json, null)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_throw_exception_when_parsing_invalid_json() |
|||
{ |
|||
var json = "invalid"; |
|||
|
|||
Assert.Throws<ValidationException>(() => AssertQuery(json, null)); |
|||
} |
|||
|
|||
private void AssertQuery(object json, string? expectedFilter) |
|||
{ |
|||
var filter = ConvertQuery(json); |
|||
|
|||
Assert.Empty(errors); |
|||
|
|||
Assert.Equal(expectedFilter, filter); |
|||
} |
|||
|
|||
private void AssertFilter(object json, string? expectedFilter) |
|||
{ |
|||
var filter = ConvertFilter(json); |
|||
|
|||
Assert.Empty(errors); |
|||
|
|||
Assert.Equal(expectedFilter, filter); |
|||
} |
|||
|
|||
private void AssertErrors(object json, params string[] expectedErrors) |
|||
{ |
|||
var filter = ConvertFilter(json); |
|||
|
|||
Assert.Equal(expectedErrors.ToList(), errors); |
|||
|
|||
Assert.Null(filter); |
|||
} |
|||
|
|||
private string? ConvertFilter<T>(T value) |
|||
{ |
|||
var json = JsonHelper.DefaultSerializer.Serialize(value, true); |
|||
|
|||
var jsonFilter = JsonHelper.DefaultSerializer.Deserialize<FilterNode<IJsonValue>>(json); |
|||
|
|||
return JsonFilterVisitor.Parse(jsonFilter, schema, errors)?.ToString(); |
|||
} |
|||
|
|||
private string? ConvertQuery<T>(T value) |
|||
{ |
|||
var json = JsonHelper.DefaultSerializer.Serialize(value, true); |
|||
|
|||
var jsonFilter = schema.Parse(json, JsonHelper.DefaultSerializer); |
|||
|
|||
return jsonFilter.ToString(); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> BuildInTests(string field, object value, string valueString) |
|||
{ |
|||
var fields = new[] |
|||
{ |
|||
$"{field}", |
|||
$"json.{field}", |
|||
$"json.nested.{field}" |
|||
}; |
|||
|
|||
foreach (var f in fields) |
|||
{ |
|||
var expected = $"{f} in [{valueString}]"; |
|||
|
|||
yield return new[] { f, value, expected }; |
|||
} |
|||
} |
|||
|
|||
public static IEnumerable<object[]> BuildTests(string field, Predicate<string> opFilter, object value, string valueString) |
|||
{ |
|||
var fields = new[] |
|||
{ |
|||
$"{field}", |
|||
$"json.{field}", |
|||
$"json.nested.{field}" |
|||
}; |
|||
|
|||
foreach (var f in fields) |
|||
{ |
|||
foreach (var op in AllOps.Where(x => opFilter(x.Operator))) |
|||
{ |
|||
var expected = |
|||
op.Output |
|||
.Replace("$FIELD", f) |
|||
.Replace("$VALUE", valueString); |
|||
|
|||
yield return new[] { f, op.Operator, value, expected }; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue