From a6b1e6233a768dd757e4fbacc59103f1b1c8e105 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 8 Mar 2021 16:57:29 +0100 Subject: [PATCH] New exists operator. --- backend/i18n/frontend_en.json | 1 + backend/i18n/frontend_it.json | 1 + backend/i18n/frontend_nl.json | 1 + backend/i18n/source/frontend_en.json | 1 + .../MongoDb/Queries/FilterVisitor.cs | 4 ++ .../Queries/ClrFilter.cs | 5 +++ .../Queries/CompareFilter.cs | 2 + .../Queries/CompareOperator.cs | 1 + .../Queries/Json/JsonFilterSurrogate.cs | 2 + .../Queries/Json/OperatorValidator.cs | 7 +++- .../Queries/OData/EdmModelExtensions.cs | 7 +++- .../Queries/OData/FilterVisitor.cs | 10 +++++ .../Contents/MongoDb/MongoDbQueryTests.cs | 13 +++++- .../Queries/QueryFromJsonTests.cs | 41 ++++++++++++++----- .../Queries/QueryFromODataTests.cs | 27 ++++++++++++ frontend/app/shared/state/query.ts | 3 +- 16 files changed, 111 insertions(+), 15 deletions(-) diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index bc7104294..998fbd9c5 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -293,6 +293,7 @@ "common.queryOperators.empty": "is empty", "common.queryOperators.endsWith": "ends with", "common.queryOperators.eq": "is equals to", + "common.queryOperators.exists": "exists", "common.queryOperators.ge": "is greater than or equals to", "common.queryOperators.gt": "is greater than", "common.queryOperators.le": "is less than pr equals to", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index d82dd34c2..45f00b06c 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -293,6 +293,7 @@ "common.queryOperators.empty": "è vuoto", "common.queryOperators.endsWith": "finisce con", "common.queryOperators.eq": "è uguale a", + "common.queryOperators.exists": "exists", "common.queryOperators.ge": "è maggiore o uguale a", "common.queryOperators.gt": "è maggiore di", "common.queryOperators.le": "è minore o uguale a", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index d51c782cf..8488a3d4d 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -293,6 +293,7 @@ "common.queryOperators.empty": "is leeg", "common.queryOperators.endsWith": "eindigt op", "common.queryOperators.eq": "is gelijk aan", + "common.queryOperators.exists": "exists", "common.queryOperators.ge": "is groter dan of gelijk aan", "common.queryOperators.gt": "is groter dan", "common.queryOperators.le": "is kleiner dan of is gelijk aan", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index bc7104294..998fbd9c5 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -293,6 +293,7 @@ "common.queryOperators.empty": "is empty", "common.queryOperators.endsWith": "ends with", "common.queryOperators.eq": "is equals to", + "common.queryOperators.exists": "exists", "common.queryOperators.ge": "is greater than or equals to", "common.queryOperators.gt": "is greater than", "common.queryOperators.le": "is less than pr equals to", diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs index 3bae8a4f4..d42f166c1 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs @@ -59,6 +59,10 @@ namespace Squidex.Infrastructure.MongoDb.Queries Filter.Eq(propertyName, default(T)!), Filter.Eq(propertyName, string.Empty), Filter.Eq(propertyName, Array.Empty())); + case CompareOperator.Exists: + return Filter.And( + Filter.Exists(propertyName, true), + Filter.Ne(propertyName, null)); case CompareOperator.StartsWith: return Filter.Regex(propertyName, BuildRegex(nodeIn, s => "^" + s)); case CompareOperator.Contains: diff --git a/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs b/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs index 0a6a13b7d..57919c90d 100644 --- a/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs +++ b/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs @@ -86,6 +86,11 @@ namespace Squidex.Infrastructure.Queries return Binary(path, CompareOperator.Empty, null); } + public static CompareFilter Exists(PropertyPath path) + { + return Binary(path, CompareOperator.Exists, null); + } + public static CompareFilter In(PropertyPath path, ClrValue value) { return Binary(path, CompareOperator.In, value); diff --git a/backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs b/backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs index baf62edc2..9e12aae7a 100644 --- a/backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs +++ b/backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs @@ -31,6 +31,8 @@ namespace Squidex.Infrastructure.Queries return $"contains({Path}, {Value})"; case CompareOperator.Empty: return $"empty({Path})"; + case CompareOperator.Exists: + return $"exists({Path})"; case CompareOperator.EndsWith: return $"endsWith({Path}, {Value})"; case CompareOperator.StartsWith: diff --git a/backend/src/Squidex.Infrastructure/Queries/CompareOperator.cs b/backend/src/Squidex.Infrastructure/Queries/CompareOperator.cs index 98f65edb7..853f3de95 100644 --- a/backend/src/Squidex.Infrastructure/Queries/CompareOperator.cs +++ b/backend/src/Squidex.Infrastructure/Queries/CompareOperator.cs @@ -11,6 +11,7 @@ namespace Squidex.Infrastructure.Queries { Contains, Empty, + Exists, EndsWith, Equals, GreaterThan, diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterSurrogate.cs b/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterSurrogate.cs index b73010a56..c38b78fda 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterSurrogate.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterSurrogate.cs @@ -75,6 +75,8 @@ namespace Squidex.Infrastructure.Queries return CompareOperator.GreaterThanOrEqual; case "empty": return CompareOperator.Empty; + case "exists": + return CompareOperator.Exists; case "contains": return CompareOperator.Contains; case "endswith": diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs b/backend/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs index e99c64859..302a05999 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs @@ -16,12 +16,14 @@ namespace Squidex.Infrastructure.Queries.Json private static readonly CompareOperator[] BooleanOperators = { CompareOperator.Equals, + CompareOperator.Exists, CompareOperator.In, CompareOperator.NotEquals }; private static readonly CompareOperator[] NumberOperators = { CompareOperator.Equals, + CompareOperator.Exists, CompareOperator.LessThan, CompareOperator.LessThanOrEqual, CompareOperator.GreaterThan, @@ -33,6 +35,7 @@ namespace Squidex.Infrastructure.Queries.Json { CompareOperator.Contains, CompareOperator.Empty, + CompareOperator.Exists, CompareOperator.EndsWith, CompareOperator.Equals, CompareOperator.GreaterThan, @@ -46,13 +49,15 @@ namespace Squidex.Infrastructure.Queries.Json private static readonly CompareOperator[] ArrayOperators = { CompareOperator.Empty, + CompareOperator.Exists, CompareOperator.Equals, CompareOperator.In, CompareOperator.NotEquals }; private static readonly CompareOperator[] GeoOperators = { - CompareOperator.LessThan + CompareOperator.LessThan, + CompareOperator.Exists, }; public static bool IsAllowedOperator(JsonSchema schema, CompareOperator compareOperator) diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs b/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs index 78c7b7579..48184d044 100644 --- a/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs +++ b/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs @@ -19,7 +19,12 @@ namespace Squidex.Infrastructure.Queries.OData CustomUriFunctions.AddCustomUriFunction("empty", new FunctionSignatureWithReturnType( EdmCoreModel.Instance.GetBoolean(false), - EdmCoreModel.Instance.GetString(true))); + EdmCoreModel.Instance.GetUntyped())); + + CustomUriFunctions.AddCustomUriFunction("exists", + new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetBoolean(false), + EdmCoreModel.Instance.GetUntyped())); CustomUriFunctions.AddCustomUriFunction("distanceto", new FunctionSignatureWithReturnType( diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs index 7aa1cfa0a..ff0047f52 100644 --- a/backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs +++ b/backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs @@ -56,6 +56,16 @@ namespace Squidex.Infrastructure.Queries.OData return ClrFilter.Empty(PropertyPathVisitor.Visit(fieldNode)); } + if (string.Equals(nodeIn.Name, "empty", StringComparison.OrdinalIgnoreCase)) + { + return ClrFilter.Empty(PropertyPathVisitor.Visit(fieldNode)); + } + + if (string.Equals(nodeIn.Name, "exists", StringComparison.OrdinalIgnoreCase)) + { + return ClrFilter.Exists(PropertyPathVisitor.Visit(fieldNode)); + } + var valueNode = nodeIn.Parameters.ElementAt(1); if (string.Equals(nodeIn.Name, "endswith", StringComparison.OrdinalIgnoreCase)) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs index 6f9019da6..c5dd628a4 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs @@ -202,8 +202,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb [Fact] public void Should_make_query_with_empty_test() { - var i = _F(ClrFilter.Empty("data/firstName/iv")); - var o = _C("{ '$or' : [{ 'do.firstName.iv' : { '$exists' : false } }, { 'do.firstName.iv' : null }, { 'do.firstName.iv' : '' }, { 'do.firstName.iv' : [] }] }"); + var i = _F(ClrFilter.Empty("id")); + var o = _C("{ '$or' : [{ '_id' : { '$exists' : false } }, { '_id' : null }, { '_id' : '' }, { '_id' : [] }] }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_exists_test() + { + var i = _F(ClrFilter.Exists("data/firstName/iv")); + var o = _C("{ 'do.firstName.iv' : { '$exists' : true, '$ne' : null } }"); Assert.Equal(o, i); } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs index b19f47287..87a771971 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs @@ -24,6 +24,7 @@ namespace Squidex.Infrastructure.Queries { ("Contains", "contains", "contains($FIELD, $VALUE)"), ("Empty", "empty", "empty($FIELD)"), + ("Exists", "exists", "exists($FIELD)"), ("EndsWith", "endswith", "endsWith($FIELD, $VALUE)"), ("Equals", "eq", "$FIELD == $VALUE"), ("GreaterThanOrEqual", "ge", "$FIELD >= $VALUE"), @@ -241,25 +242,30 @@ namespace Squidex.Infrastructure.Queries public class Geo { + private static bool ValidOperator(string op) + { + return op == "lt" || op == "exists"; + } + public static IEnumerable ValidTests() { var value = new { longitude = 10, latitude = 20, distance = 30 }; - return BuildFlatTests("geo", x => x == "lt", value, $"Radius({value.longitude}, {value.latitude}, {value.distance})"); + return BuildFlatTests("geo", ValidOperator, value, $"Radius({value.longitude}, {value.latitude}, {value.distance})"); } public static IEnumerable ValidRefTests() { var value = new { longitude = 10, latitude = 20, distance = 30 }; - return BuildFlatTests("geoRef", x => x == "lt", value, $"Radius({value.longitude}, {value.latitude}, {value.distance})"); + return BuildFlatTests("geoRef", ValidOperator, value, $"Radius({value.longitude}, {value.latitude}, {value.distance})"); } public static IEnumerable InvalidTests() { var value = new { longitude = 10, latitude = 20, distance = 30 }; - return BuildInvalidOperatorTests("geo", x => x == "lt", value); + return BuildInvalidOperatorTests("geo", ValidOperator, value); } [Theory] @@ -308,18 +314,23 @@ namespace Squidex.Infrastructure.Queries public class Number { + private static bool ValidOperator(string op) + { + return op.Length == 2 || op == "exists"; + } + public static IEnumerable ValidTests() { const int value = 12; - return BuildTests("number", x => x.Length == 2, value, $"{value}"); + return BuildTests("number", ValidOperator, value, $"{value}"); } public static IEnumerable InvalidTests() { const int value = 12; - return BuildInvalidOperatorTests("number", x => x.Length == 2, $"{value}"); + return BuildInvalidOperatorTests("number", ValidOperator, $"{value}"); } public static IEnumerable ValidInTests() @@ -367,18 +378,23 @@ namespace Squidex.Infrastructure.Queries public class Boolean { + private static bool ValidOperator(string op) + { + return op == "eq" || op == "ne" || op == "exists"; + } + public static IEnumerable ValidTests() { const bool value = true; - return BuildTests("boolean", x => x == "eq" || x == "ne", value, $"{value}"); + return BuildTests("boolean", ValidOperator, value, $"{value}"); } public static IEnumerable InvalidTests() { const bool value = true; - return BuildInvalidOperatorTests("boolean", x => x == "eq" || x == "ne", value); + return BuildInvalidOperatorTests("boolean", ValidOperator, value); } public static IEnumerable ValidInTests() @@ -426,11 +442,16 @@ namespace Squidex.Infrastructure.Queries public class Array { - public static IEnumerable ValiedTests() + private static bool ValidOperator(string op) + { + return op == "eq" || op == "ne" || op == "empty" || op == "exists"; + } + + public static IEnumerable ValidTests() { const string value = "Hello"; - return BuildTests("stringArray", x => x == "eq" || x == "ne" || x == "empty", value, $"'{value}'"); + return BuildTests("stringArray", ValidOperator, value, $"'{value}'"); } public static IEnumerable ValidInTests() @@ -441,7 +462,7 @@ namespace Squidex.Infrastructure.Queries } [Theory] - [MemberData(nameof(ValiedTests))] + [MemberData(nameof(ValidTests))] public void Should_parse_array_filter(string field, string op, string value, string expected) { var json = new { path = field, op, value }; diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs index 3f3885465..9251151bf 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs @@ -354,6 +354,33 @@ namespace Squidex.Infrastructure.Queries Assert.Equal(o, i); } + [Fact] + public void Should_parse_filter_with_exists() + { + var i = _Q("$filter=exists(lastName)"); + var o = _C("Filter: exists(lastName)"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_exists_to_true() + { + var i = _Q("$filter=exists(lastName) eq true"); + var o = _C("Filter: exists(lastName)"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_exists_to_false() + { + var i = _Q("$filter=exists(lastName) eq false"); + var o = _C("Filter: !(exists(lastName))"); + + Assert.Equal(o, i); + } + [Fact] public void Should_parse_filter_with_contains() { diff --git a/frontend/app/shared/state/query.ts b/frontend/app/shared/state/query.ts index a2e6cd1a1..988069c60 100644 --- a/frontend/app/shared/state/query.ts +++ b/frontend/app/shared/state/query.ts @@ -231,7 +231,8 @@ const StringOperators: ReadonlyArray = [ ]; const ArrayOperators: ReadonlyArray = [ - { name: 'i18n:common.queryOperators.empty', value: 'empty', noValue: true } + { name: 'i18n:common.queryOperators.empty', value: 'empty', noValue: true }, + { name: 'i18n:common.queryOperators.exists', value: 'exists', noValue: true } ]; const TypeBoolean: QueryFieldModel = {