diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index 998fbd9c5..16578ecf8 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -298,6 +298,7 @@ "common.queryOperators.gt": "is greater than", "common.queryOperators.le": "is less than pr equals to", "common.queryOperators.lt": "is less than", + "common.queryOperators.matchs": "matchs", "common.queryOperators.ne": "is not equals to", "common.queryOperators.startsWith": "starts with", "common.refresh": "Refresh", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index 45f00b06c..568cfbc65 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -298,6 +298,7 @@ "common.queryOperators.gt": "è maggiore di", "common.queryOperators.le": "è minore o uguale a", "common.queryOperators.lt": "è minore di", + "common.queryOperators.matchs": "matchs", "common.queryOperators.ne": "non è uguale a", "common.queryOperators.startsWith": "inizia con", "common.refresh": "Aggiorna", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index 8488a3d4d..c9f18c096 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -298,6 +298,7 @@ "common.queryOperators.gt": "is groter dan", "common.queryOperators.le": "is kleiner dan of is gelijk aan", "common.queryOperators.lt": "is kleiner dan", + "common.queryOperators.matchs": "matchs", "common.queryOperators.ne": "is niet gelijk aan", "common.queryOperators.startsWith": "begint met", "common.refresh": "Vernieuwen", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index 998fbd9c5..16578ecf8 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -298,6 +298,7 @@ "common.queryOperators.gt": "is greater than", "common.queryOperators.le": "is less than pr equals to", "common.queryOperators.lt": "is less than", + "common.queryOperators.matchs": "matchs", "common.queryOperators.ne": "is not equals to", "common.queryOperators.startsWith": "starts with", "common.refresh": "Refresh", diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/AdaptIdVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/AdaptIdVisitor.cs new file mode 100644 index 000000000..308decc30 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/AdaptIdVisitor.cs @@ -0,0 +1,92 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using NodaTime; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Entities.MongoDb +{ + internal sealed class AdaptIdVisitor : TransformVisitor + { + private static readonly AdaptIdVisitor Instance = new AdaptIdVisitor(); + + public readonly struct Args + { + public readonly DomainId AppId; + + public Args(DomainId appId) + { + AppId = appId; + } + } + + private AdaptIdVisitor() + { + } + + public static FilterNode? AdaptFilter(FilterNode filter, DomainId appId) + { + var args = new Args(appId); + + return filter.Accept(Instance, args); + } + + public override FilterNode Visit(CompareFilter nodeIn, Args args) + { + var result = nodeIn; + + var (path, _, value) = nodeIn; + + var clrValue = value.Value; + + if (string.Equals(path[0], "id", StringComparison.OrdinalIgnoreCase)) + { + path = "_id"; + + if (clrValue is List idList) + { + value = idList.Select(x => DomainId.Combine(args.AppId, DomainId.Create(x)).ToString()).ToList(); + } + else if (clrValue is string id) + { + value = DomainId.Combine(args.AppId, DomainId.Create(id)).ToString(); + } + else if (clrValue is List guidIdList) + { + value = guidIdList.Select(x => DomainId.Combine(args.AppId, DomainId.Create(x)).ToString()).ToList(); + } + else if (clrValue is Guid guidId) + { + value = DomainId.Combine(args.AppId, DomainId.Create(guidId)).ToString(); + } + } + else + { + if (clrValue is List guidList) + { + value = guidList.Select(x => x.ToString()).ToList(); + } + else if (clrValue is Guid guid) + { + value = guid.ToString(); + } + else if (clrValue is Instant && + !string.Equals(path[0], "mt", StringComparison.OrdinalIgnoreCase) && + !string.Equals(path[0], "ct", StringComparison.OrdinalIgnoreCase)) + { + value = clrValue.ToString(); + } + } + + return result with { Path = path, Value = value }; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index baea87f99..187aa1d24 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets } else { - var query = q.Query.AdjustToModel(); + var query = q.Query.AdjustToModel(appId); var filter = query.BuildFilter(appId, parentId); diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs index c01e1554d..9548dc7ac 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs @@ -18,13 +18,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors { private static readonly FilterDefinitionBuilder Filter = Builders.Filter; - public static ClrQuery AdjustToModel(this ClrQuery query) + public static ClrQuery AdjustToModel(this ClrQuery query, DomainId appId) { if (query.Filter != null) { query.Filter = FirstPascalPathConverter.Transform(query.Filter); } + if (query.Filter != null) + { + query.Filter = AdaptIdVisitor.AdaptFilter(query.Filter, appId); + } + if (query.Sort != null) { query.Sort = query.Sort.Select(x => new SortNode(x.Path.ToFirstPascalCase(), x.Order)).ToList(); @@ -37,10 +42,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors { var filters = new List> { - Filter.Eq(x => x.IndexedAppId, appId), - Filter.Eq(x => x.IsDeleted, false) + Filter.Eq(x => x.IndexedAppId, appId) }; + if (!query.HasFilterField("IsDeleted")) + { + filters.Add(Filter.Eq(x => x.IsDeleted, false)); + } + if (parentId != null) { if (parentId == DomainId.Empty) diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs index 39f80f14d..d3bf8837e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs @@ -81,7 +81,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations { if (query.Filter != null) { - query.Filter = AdaptionVisitor.AdaptFilter(query.Filter, appId); + query.Filter = AdaptionVisitor.AdaptFilter(query.Filter); + } + + if (query.Filter != null) + { + query.Filter = AdaptIdVisitor.AdaptFilter(query.Filter, appId); } if (query.Sort != null) @@ -94,7 +99,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations public static FilterNode? AdjustToModel(this FilterNode filter, DomainId appId) { - return AdaptionVisitor.AdaptFilter(filter, appId); + var result = AdaptionVisitor.AdaptFilter(filter); + + if (result != null) + { + result = AdaptIdVisitor.AdaptFilter(result, appId); + } + + return result; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/AdaptionVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/AdaptionVisitor.cs index 6c682df04..8bf4e44c9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/AdaptionVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/AdaptionVisitor.cs @@ -6,89 +6,34 @@ // ========================================================================== using System; -using System.Collections.Generic; -using System.Linq; -using NodaTime; using Squidex.Infrastructure; using Squidex.Infrastructure.Queries; namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations { - internal sealed class AdaptionVisitor : TransformVisitor + internal sealed class AdaptionVisitor : TransformVisitor { private static readonly AdaptionVisitor Instance = new AdaptionVisitor(); - public readonly struct Args - { - public readonly DomainId AppId; - - public Args(DomainId appId) - { - AppId = appId; - } - } - private AdaptionVisitor() { } - public static FilterNode? AdaptFilter(FilterNode filter, DomainId appId) + public static FilterNode? AdaptFilter(FilterNode filter) { - var args = new Args(appId); - - return filter.Accept(Instance, args); + return filter.Accept(Instance, None.Value); } - public override FilterNode Visit(CompareFilter nodeIn, Args args) + public override FilterNode Visit(CompareFilter nodeIn, None args) { - var result = nodeIn; - - var (path, _, value) = nodeIn; - - var clrValue = value.Value; - - if (string.Equals(path[0], "id", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(nodeIn.Path[0], "id", StringComparison.OrdinalIgnoreCase)) { - path = "_id"; - - if (clrValue is List idList) - { - value = idList.Select(x => DomainId.Combine(args.AppId, DomainId.Create(x)).ToString()).ToList(); - } - else if (clrValue is string id) - { - value = DomainId.Combine(args.AppId, DomainId.Create(id)).ToString(); - } - else if (clrValue is List guidIdList) - { - value = guidIdList.Select(x => DomainId.Combine(args.AppId, DomainId.Create(x)).ToString()).ToList(); - } - else if (clrValue is Guid guidId) - { - value = DomainId.Combine(args.AppId, DomainId.Create(guidId)).ToString(); - } + return nodeIn; } - else - { - path = Adapt.MapPath(path); - if (clrValue is List guidList) - { - value = guidList.Select(x => x.ToString()).ToList(); - } - else if (clrValue is Guid guid) - { - value = guid.ToString(); - } - else if (clrValue is Instant && - !string.Equals(path[0], "mt", StringComparison.OrdinalIgnoreCase) && - !string.Equals(path[0], "ct", StringComparison.OrdinalIgnoreCase)) - { - value = clrValue.ToString(); - } - } + var path = Adapt.MapPath(nodeIn.Path); - return result with { Path = path, Value = value }; + return nodeIn with { Path = path }; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs index 56eb86ebe..1a6a082e0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs @@ -201,18 +201,22 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations return query.Sort?.All(x => x.Path.ToString() == "mt" && x.Order == SortOrder.Descending) == true; } - private static FilterDefinition BuildFilter(DomainId appId, DomainId schemaId, FilterNode? filterNode) + private static FilterDefinition BuildFilter(DomainId appId, DomainId schemaId, FilterNode? filter) { var filters = new List> { Filter.Eq(x => x.IndexedAppId, appId), - Filter.Eq(x => x.IndexedSchemaId, schemaId), - Filter.Ne(x => x.IsDeleted, true) + Filter.Eq(x => x.IndexedSchemaId, schemaId) }; - if (filterNode != null) + if (filter?.HasField("dl") != true) { - filters.Add(filterNode.BuildFilter()); + filters.Add(Filter.Ne(x => x.IsDeleted, true)); + } + + if (filter != null) + { + filters.Add(filter.BuildFilter()); } return Filter.And(filters); @@ -225,9 +229,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations { Filter.Eq(x => x.IndexedAppId, appId), Filter.In(x => x.IndexedSchemaId, schemaIds), - Filter.Ne(x => x.IsDeleted, true) }; + if (query?.HasFilterField("dl") != true) + { + filters.Add(Filter.Ne(x => x.IsDeleted, true)); + } + if (query?.Filter != null) { filters.Add(query.Filter.BuildFilter()); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs index 1ee7d83ef..3e364963d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs @@ -160,6 +160,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries AddProperty("fileName", JsonObjectType.String); AddProperty("fileSize", JsonObjectType.Integer); AddProperty("fileVersion", JsonObjectType.Integer); + AddProperty("isDeleted", JsonObjectType.Boolean); AddProperty("isProtected", JsonObjectType.Boolean); AddProperty("lastModified", JsonObjectType.String, JsonFormatStrings.DateTime); AddProperty("lastModifiedBy", JsonObjectType.String); @@ -196,6 +197,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries AddProperty("createdBy", EdmPrimitiveTypeKind.String); AddProperty("fileHash", EdmPrimitiveTypeKind.String); AddProperty("fileName", EdmPrimitiveTypeKind.String); + AddProperty("isDeleted", EdmPrimitiveTypeKind.Boolean); AddProperty("isProtected", EdmPrimitiveTypeKind.Boolean); AddProperty("fileSize", EdmPrimitiveTypeKind.Int64); AddProperty("fileVersion", EdmPrimitiveTypeKind.Int64); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs index 134f5457c..007f62cf3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs @@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60); private readonly EdmModel genericEdmModel = BuildEdmModel("Generic", "Content", new EdmModel(), null); - private readonly JsonSchema genericJsonSchema = ContentJsonSchemaBuilder.BuildSchema("Content", null); + private readonly JsonSchema genericJsonSchema = ContentJsonSchemaBuilder.BuildSchema("Content", null, false, true); private readonly IMemoryCache cache; private readonly IJsonSerializer jsonSerializer; private readonly ITextIndex textIndex; @@ -237,7 +237,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var dataSchema = schema.BuildJsonSchema(app.PartitionResolver(), withHiddenFields); - return ContentJsonSchemaBuilder.BuildSchema(schema.DisplayName(), dataSchema); + return ContentJsonSchemaBuilder.BuildSchema(schema.DisplayName(), dataSchema, false, true); } private static EdmModel BuildEdmModel(Schema schema, IAppEntity app, bool withHiddenFields) @@ -281,6 +281,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var entityType = new EdmEntityType(modelName, name); entityType.AddStructuralProperty("id", EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty("isDeleted", EdmPrimitiveTypeKind.Boolean); entityType.AddStructuralProperty("created", EdmPrimitiveTypeKind.DateTimeOffset); entityType.AddStructuralProperty("createdBy", EdmPrimitiveTypeKind.String); entityType.AddStructuralProperty("lastModified", EdmPrimitiveTypeKind.DateTimeOffset); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Schemas/ContentJsonSchemaBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Schemas/ContentJsonSchemaBuilder.cs index 18b5ee985..0315e527c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Schemas/ContentJsonSchemaBuilder.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Schemas/ContentJsonSchemaBuilder.cs @@ -11,7 +11,7 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema { public static class ContentJsonSchemaBuilder { - public static JsonSchema BuildSchema(string name, JsonSchema? dataSchema, bool extended = false) + public static JsonSchema BuildSchema(string name, JsonSchema? dataSchema, bool extended = false, bool withDeleted = false) { var jsonSchema = new JsonSchema { @@ -28,6 +28,11 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema Type = JsonObjectType.Object }; + if (withDeleted) + { + jsonSchema.Properties["isDeleted"] = SchemaBuilder.BooleanProperty("True when deleted.", false); + } + if (extended) { jsonSchema.Properties["newStatusColor"] = SchemaBuilder.StringProperty("The color of the new status.", false); diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs index d42f166c1..e84e7cdf4 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs @@ -56,13 +56,15 @@ namespace Squidex.Infrastructure.MongoDb.Queries case CompareOperator.Empty: return Filter.Or( Filter.Exists(propertyName, false), - Filter.Eq(propertyName, default(T)!), - Filter.Eq(propertyName, string.Empty), - Filter.Eq(propertyName, Array.Empty())); + Filter.Eq(propertyName, null), + Filter.Eq(propertyName, string.Empty), + Filter.Size(propertyName, 0)); case CompareOperator.Exists: return Filter.And( Filter.Exists(propertyName, true), Filter.Ne(propertyName, null)); + case CompareOperator.Matchs: + return Filter.Regex(propertyName, BuildMatchRegex(nodeIn)); case CompareOperator.StartsWith: return Filter.Regex(propertyName, BuildRegex(nodeIn, s => "^" + s)); case CompareOperator.Contains: @@ -88,6 +90,30 @@ namespace Squidex.Infrastructure.MongoDb.Queries throw new NotSupportedException(); } + private static BsonRegularExpression BuildMatchRegex(CompareFilter node) + { + var value = node.Value.Value?.ToString(); + + if (value == null) + { + return new BsonRegularExpression("null", "i"); + } + + if (value.Length > 3 && (value[0] == '/' && value[^1] == '/' || value[^2] == '/')) + { + if (value[^1] == 'i') + { + return new BsonRegularExpression(value[1..^2], "i"); + } + else + { + return new BsonRegularExpression(value[1..^1]); + } + } + + return new BsonRegularExpression(value, "i"); + } + private static BsonRegularExpression BuildRegex(CompareFilter node, Func formatter) { return new BsonRegularExpression(formatter(node.Value.Value?.ToString() ?? "null"), "i"); diff --git a/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs b/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs index 57919c90d..9f07840bb 100644 --- a/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs +++ b/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs @@ -71,6 +71,11 @@ namespace Squidex.Infrastructure.Queries return Binary(path, CompareOperator.Contains, value); } + public static CompareFilter Matchs(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.Matchs, value); + } + public static CompareFilter EndsWith(PropertyPath path, ClrValue value) { return Binary(path, CompareOperator.EndsWith, value); diff --git a/backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs b/backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs index 9e12aae7a..5edc11ea3 100644 --- a/backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs +++ b/backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs @@ -33,6 +33,8 @@ namespace Squidex.Infrastructure.Queries return $"empty({Path})"; case CompareOperator.Exists: return $"exists({Path})"; + case CompareOperator.Matchs: + return $"matchs({Path}, {Value})"; 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 853f3de95..c341e850b 100644 --- a/backend/src/Squidex.Infrastructure/Queries/CompareOperator.cs +++ b/backend/src/Squidex.Infrastructure/Queries/CompareOperator.cs @@ -19,6 +19,7 @@ namespace Squidex.Infrastructure.Queries In, LessThan, LessThanOrEqual, + Matchs, NotEquals, StartsWith } diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterSurrogate.cs b/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterSurrogate.cs index c38b78fda..fcb7c4b5a 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterSurrogate.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterSurrogate.cs @@ -77,6 +77,8 @@ namespace Squidex.Infrastructure.Queries return CompareOperator.Empty; case "exists": return CompareOperator.Exists; + case "matchs": + return CompareOperator.Matchs; 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 302a05999..b26366738 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs @@ -43,6 +43,7 @@ namespace Squidex.Infrastructure.Queries.Json CompareOperator.In, CompareOperator.LessThan, CompareOperator.LessThanOrEqual, + CompareOperator.Matchs, CompareOperator.NotEquals, CompareOperator.StartsWith }; diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs deleted file mode 100644 index e430d1655..000000000 --- a/backend/src/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// 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 - { - 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; - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs index 34fd3e0ba..239bab66d 100644 --- a/backend/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs +++ b/backend/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs @@ -40,34 +40,7 @@ namespace Squidex.Infrastructure.Queries.OData public override ClrValue Visit(ConvertNode nodeIn) { - var value = ConstantVisitor.Visit(nodeIn.Source); - - if (value == null) - { - return ClrValue.Null; - } - - if (nodeIn.TypeReference == null) - { - throw new NotSupportedException(); - } - - if (nodeIn.TypeReference.Definition == BooleanType) - { - return bool.Parse(value.ToString()!); - } - - if (nodeIn.TypeReference.Definition == GuidType) - { - return Guid.Parse(value.ToString()!); - } - - if (nodeIn.TypeReference.Definition == DateTimeType || nodeIn.TypeReference.Definition == DateType) - { - return ParseInstant(value); - } - - throw new NotSupportedException(); + return nodeIn.Source.Accept(this); } public override ClrValue Visit(CollectionConstantNode nodeIn) diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs b/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs index 48184d044..0fecf3003 100644 --- a/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs +++ b/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs @@ -26,6 +26,12 @@ namespace Squidex.Infrastructure.Queries.OData EdmCoreModel.Instance.GetBoolean(false), EdmCoreModel.Instance.GetUntyped())); + CustomUriFunctions.AddCustomUriFunction("matchs", + new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetBoolean(false), + EdmCoreModel.Instance.GetString(false), + EdmCoreModel.Instance.GetString(false))); + CustomUriFunctions.AddCustomUriFunction("distanceto", new FunctionSignatureWithReturnType( EdmCoreModel.Instance.GetDouble(false), diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs index ff0047f52..c2f659333 100644 --- a/backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs +++ b/backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs @@ -68,6 +68,13 @@ namespace Squidex.Infrastructure.Queries.OData var valueNode = nodeIn.Parameters.ElementAt(1); + if (string.Equals(nodeIn.Name, "matchs", StringComparison.OrdinalIgnoreCase)) + { + var value = ConstantWithTypeVisitor.Visit(valueNode); + + return ClrFilter.Matchs(PropertyPathVisitor.Visit(fieldNode), value); + } + if (string.Equals(nodeIn.Name, "endswith", StringComparison.OrdinalIgnoreCase)) { var value = ConstantWithTypeVisitor.Visit(valueNode); diff --git a/backend/src/Squidex.Infrastructure/Queries/QueryExtensions.cs b/backend/src/Squidex.Infrastructure/Queries/QueryExtensions.cs new file mode 100644 index 000000000..7025ad063 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/QueryExtensions.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; + +namespace Squidex.Infrastructure.Queries +{ + public static class QueryExtensions + { + public static bool HasFilterField(this Query? query, string field) + { + return HasField(query?.Filter, field); + } + + public static bool HasField(this FilterNode? filter, string field) + { + if (filter == null) + { + return false; + } + + var fields = new HashSet(StringComparer.OrdinalIgnoreCase); + + filter.AddFields(fields); + + return fields.Contains(field); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 018aa5e7d..eb0385bd4 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -355,8 +355,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// /// The name of the app. /// The id of the asset to delete. - /// True to check referrers of this asset. - /// True to delete the asset permanently. + /// The request parameters. /// /// 204 => Asset deleted. /// 404 => Asset or app not found. @@ -365,11 +364,11 @@ namespace Squidex.Areas.Api.Controllers.Assets [Route("apps/{app}/assets/{id}/")] [ApiPermissionOrAnonymous(Permissions.AppAssetsDelete)] [ApiCosts(1)] - public async Task DeleteAsset(string app, DomainId id, - [FromQuery] bool checkReferrers = false, - [FromQuery] bool permanent = false) + public async Task DeleteAsset(string app, DomainId id, DeleteAssetDto request) { - await CommandBus.PublishAsync(new DeleteAsset { AssetId = id, CheckReferrers = checkReferrers, Permanent = false }); + var command = request.ToCommand(id); + + await CommandBus.PublishAsync(command); return NoContent(); } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/DeleteAssetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/DeleteAssetDto.cs new file mode 100644 index 000000000..5c2b068e6 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/DeleteAssetDto.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Mvc; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.Assets.Models +{ + public sealed class DeleteAssetDto + { + /// + /// True to check referrers of this asset. + /// + [FromQuery] + public bool CheckReferrers { get; set; } + + /// + /// True to delete the asset permanently. + /// + [FromQuery] + public bool Permanent { get; set; } + + public DeleteAsset ToCommand(DomainId id) + { + return SimpleMapper.Map(this, new DeleteAsset { AssetId = id }); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index cb475c0d3..2e235740e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -606,8 +606,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// The name of the app. /// The name of the schema. /// The id of the content item to delete. - /// True to check referrers of this content. - /// True to delete the asset permanently. + /// The request parameters. /// /// 204 => Content deleted. /// 400 => Content cannot be deleted. @@ -620,11 +619,9 @@ namespace Squidex.Areas.Api.Controllers.Contents [Route("content/{app}/{name}/{id}/")] [ApiPermissionOrAnonymous(Permissions.AppContentsDeleteOwn)] [ApiCosts(1)] - public async Task DeleteContent(string app, string name, DomainId id, - [FromQuery] bool checkReferrers = false, - [FromQuery] bool permanent = false) + public async Task DeleteContent(string app, string name, DomainId id, DeleteContentDto request) { - var command = new DeleteContent { ContentId = id, CheckReferrers = checkReferrers, Permanent = permanent }; + var command = request.ToCommand(id); await CommandBus.PublishAsync(command); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/DeleteContentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/DeleteContentDto.cs new file mode 100644 index 000000000..7d2ff83fa --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/DeleteContentDto.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Mvc; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.Contents.Models +{ + public sealed class DeleteContentDto + { + /// + /// True to check referrers of this content. + /// + [FromQuery] + public bool CheckReferrers { get; set; } + + /// + /// True to delete the content permanently. + /// + [FromQuery] + public bool Permanent { get; set; } + + public DeleteContent ToCommand(DomainId id) + { + return SimpleMapper.Map(this, new DeleteContent { ContentId = id }); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetQueryTests.cs new file mode 100644 index 000000000..bc27b3fed --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetQueryTests.cs @@ -0,0 +1,248 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using FakeItEasy; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using NodaTime.Text; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.MongoDb.Assets; +using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors; +using Squidex.Infrastructure; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.MongoDb.Queries; +using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Validation; +using Xunit; +using ClrFilter = Squidex.Infrastructure.Queries.ClrFilter; +using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder; + +namespace Squidex.Domain.Apps.Entities.Assets.MongoDb +{ + public class AssetQueryTests + { + private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry; + private static readonly IBsonSerializer Serializer = BsonSerializer.SerializerRegistry.GetSerializer(); + private readonly DomainId appId = DomainId.NewGuid(); + + static AssetQueryTests() + { + DomainIdSerializer.Register(); + + TypeConverterStringSerializer.Register(); + TypeConverterStringSerializer.Register(); + + InstantSerializer.Register(); + } + + [Fact] + public void Should_throw_exception_for_full_text_search() + { + Assert.Throws(() => AssertQuery(string.Empty, new ClrQuery { FullText = "Full Text" })); + } + + [Fact] + public void Should_make_query_with_id() + { + var id = Guid.NewGuid(); + + var filter = ClrFilter.Eq("id", id); + + AssertQuery($"{{ '_id' : '{appId}--{id}' }}", filter); + } + + [Fact] + public void Should_make_query_with_id_string() + { + var id = DomainId.NewGuid().ToString(); + + var filter = ClrFilter.Eq("id", id); + + AssertQuery($"{{ '_id' : '{appId}--{id}' }}", filter); + } + + [Fact] + public void Should_make_query_with_id_list() + { + var id = Guid.NewGuid(); + + var filter = ClrFilter.In("id", new List { id }); + + AssertQuery($"{{ '_id' : {{ '$in' : ['{appId}--{id}'] }} }}", filter); + } + + [Fact] + public void Should_make_query_with_id_string_list() + { + var id = DomainId.NewGuid().ToString(); + + var filter = ClrFilter.In("id", new List { id }); + + AssertQuery($"{{ '_id' : {{ '$in' : ['{appId}--{id}'] }} }}", filter); + } + + [Fact] + public void Should_make_query_with_lastModified() + { + var time = "1988-01-19T12:00:00Z"; + + var filter = ClrFilter.Eq("lastModified", InstantPattern.ExtendedIso.Parse(time).Value); + + AssertQuery("{ 'mt' : ISODate('[value]') }", filter, time); + } + + [Fact] + public void Should_make_query_with_lastModifiedBy() + { + var filter = ClrFilter.Eq("lastModifiedBy", "subject:me"); + + AssertQuery("{ 'mb' : 'subject:me' }", filter); + } + + [Fact] + public void Should_make_query_with_created() + { + var time = "1988-01-19T12:00:00Z"; + + var filter = ClrFilter.Eq("created", InstantPattern.ExtendedIso.Parse(time).Value); + + AssertQuery("{ 'ct' : ISODate('[value]') }", filter, time); + } + + [Fact] + public void Should_make_query_with_createdBy() + { + var filter = ClrFilter.Eq("createdBy", "subject:me"); + + AssertQuery("{ 'cb' : 'subject:me' }", filter); + } + + [Fact] + public void Should_make_query_with_version() + { + var filter = ClrFilter.Eq("version", 2L); + + AssertQuery("{ 'vs' : NumberLong(2) }", filter); + } + + [Fact] + public void Should_make_query_with_fileVersion() + { + var filter = ClrFilter.Eq("fileVersion", 2L); + + AssertQuery("{ 'fv' : NumberLong(2) }", filter); + } + + [Fact] + public void Should_make_query_with_tags() + { + var filter = ClrFilter.Eq("tags", "tag1"); + + AssertQuery("{ 'td' : 'tag1' }", filter); + } + + [Fact] + public void Should_make_query_with_fileName() + { + var filter = ClrFilter.Eq("fileName", "Logo.png"); + + AssertQuery("{ 'fn' : 'Logo.png' }", filter); + } + + [Fact] + public void Should_make_query_with_mimeType() + { + var filter = ClrFilter.Eq("mimeType", "text/json"); + + AssertQuery("{ 'mm' : 'text/json' }", filter); + } + + [Fact] + public void Should_make_query_with_fileSize() + { + var filter = ClrFilter.Eq("fileSize", 1024); + + AssertQuery("{ 'fs' : NumberLong(1024) }", filter); + } + + [Fact] + public void Should_make_query_with_pixelHeight() + { + var filter = ClrFilter.Eq("metadata.pixelHeight", 600); + + AssertQuery("{ 'md.pixelHeight' : 600 }", filter); + } + + [Fact] + public void Should_make_query_with_pixelWidth() + { + var filter = ClrFilter.Eq("metadata.pixelWidth", 800); + + AssertQuery("{ 'md.pixelWidth' : 800 }", filter); + } + + [Fact] + public void Should_make_orderby_with_single_field() + { + var sorting = SortBuilder.Descending("created"); + + AssertSorting("{ 'ct' : -1 }", sorting); + } + + [Fact] + public void Should_make_orderby_with_multiple_fields() + { + var sorting1 = SortBuilder.Ascending("created"); + var sorting2 = SortBuilder.Descending("createdBy"); + + AssertSorting("{ 'ct' : 1, 'cb' : -1 }", sorting1, sorting2); + } + + private void AssertQuery(string expected, FilterNode filter, object? arg = null) + { + AssertQuery(expected, new ClrQuery { Filter = filter }, arg); + } + + private void AssertQuery(string expected, ClrQuery query, object? arg = null) + { + var rendered = + query.AdjustToModel(appId).BuildFilter(false).Filter! + .Render(Serializer, Registry).ToString(); + + var expectation = Cleanup(expected, arg); + + Assert.Equal(expectation, rendered); + } + + private void AssertSorting(string expected, params SortNode[] sort) + { + var cursor = A.Fake>(); + + var rendered = string.Empty; + + A.CallTo(() => cursor.Sort(A>._)) + .Invokes((SortDefinition sortDefinition) => + { + rendered = sortDefinition.Render(Serializer, Registry).ToString(); + }); + + cursor.QuerySort(new ClrQuery { Sort = sort.ToList() }.AdjustToModel(appId)); + + var expectation = Cleanup(expected); + + Assert.Equal(expectation, rendered); + } + + private static string Cleanup(string filter, object? arg = null) + { + return filter.Replace('\'', '"').Replace("[value]", arg?.ToString()); + } + } +} \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryIntegrationTests.cs similarity index 96% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryIntegrationTests.cs index 81c0af1c8..5d549962f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryIntegrationTests.cs @@ -18,11 +18,11 @@ using F = Squidex.Infrastructure.Queries.ClrFilter; namespace Squidex.Domain.Apps.Entities.Assets.MongoDb { [Trait("Category", "Dependencies")] - public class AssetsQueryTests : IClassFixture + public class AssetsQueryIntegrationTests : IClassFixture { public AssetsQueryFixture _ { get; } - public AssetsQueryTests(AssetsQueryFixture fixture) + public AssetsQueryIntegrationTests(AssetsQueryFixture fixture) { _ = fixture; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs deleted file mode 100644 index 580b977ab..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs +++ /dev/null @@ -1,239 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Linq; -using FakeItEasy; -using MongoDB.Bson.Serialization; -using MongoDB.Driver; -using NodaTime.Text; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.MongoDb.Assets; -using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors; -using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.MongoDb.Queries; -using Squidex.Infrastructure.Queries; -using Squidex.Infrastructure.Validation; -using Xunit; -using ClrFilter = Squidex.Infrastructure.Queries.ClrFilter; -using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder; - -#pragma warning disable SA1300 // Element should begin with upper-case letter -#pragma warning disable IDE1006 // Naming Styles - -namespace Squidex.Domain.Apps.Entities.Assets.MongoDb -{ - public class MongoDbQueryTests - { - private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry; - private static readonly IBsonSerializer Serializer = BsonSerializer.SerializerRegistry.GetSerializer(); - - static MongoDbQueryTests() - { - DomainIdSerializer.Register(); - - TypeConverterStringSerializer.Register(); - TypeConverterStringSerializer.Register(); - - InstantSerializer.Register(); - } - - [Fact] - public void Should_throw_exception_for_full_text_search() - { - Assert.Throws(() => _Q(new ClrQuery { FullText = "Full Text" })); - } - - [Fact] - public void Should_make_query_with_lastModified() - { - var i = _F(ClrFilter.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); - 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(ClrFilter.Eq("lastModifiedBy", "subject:me")); - var o = _C("{ 'mb' : 'subject:me' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_created() - { - var i = _F(ClrFilter.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); - var o = _C("{ 'ct' : ISODate('1988-01-19T12:00:00Z') }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_createdBy() - { - var i = _F(ClrFilter.Eq("createdBy", "subject:me")); - var o = _C("{ 'cb' : 'subject:me' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_version() - { - var i = _F(ClrFilter.Eq("version", 0)); - var o = _C("{ 'vs' : NumberLong(0) }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_fileVersion() - { - var i = _F(ClrFilter.Eq("fileVersion", 2)); - var o = _C("{ 'fv' : NumberLong(2) }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_tags() - { - var i = _F(ClrFilter.Eq("tags", "tag1")); - var o = _C("{ 'td' : 'tag1' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_fileName() - { - var i = _F(ClrFilter.Eq("fileName", "Logo.png")); - var o = _C("{ 'fn' : 'Logo.png' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_mimeType() - { - var i = _F(ClrFilter.Eq("mimeType", "text/json")); - var o = _C("{ 'mm' : 'text/json' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_fileSize() - { - var i = _F(ClrFilter.Eq("fileSize", 1024)); - var o = _C("{ 'fs' : NumberLong(1024) }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_pixelHeight() - { - var i = _F(ClrFilter.Eq("metadata.pixelHeight", 600)); - var o = _C("{ 'md.pixelHeight' : 600 }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_pixelWidth() - { - var i = _F(ClrFilter.Eq("metadata.pixelWidth", 800)); - var o = _C("{ 'md.pixelWidth' : 800 }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_orderby_with_single_field() - { - var i = _S(SortBuilder.Descending("lastModified")); - var o = _C("{ 'mt' : -1 }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_orderby_with_multiple_fields() - { - var i = _S(SortBuilder.Ascending("lastModified"), SortBuilder.Descending("lastModifiedBy")); - var o = _C("{ 'mt' : 1, 'mb' : -1 }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_take_statement() - { - var query = new ClrQuery { Take = 3 }; - - var cursor = A.Fake>(); - - cursor.QueryLimit(query.AdjustToModel()); - - A.CallTo(() => cursor.Limit(3)) - .MustHaveHappened(); - } - - [Fact] - public void Should_make_skip_statement() - { - var query = new ClrQuery { Skip = 3 }; - - var cursor = A.Fake>(); - - cursor.QuerySkip(query.AdjustToModel()); - - A.CallTo(() => cursor.Skip(3)) - .MustHaveHappened(); - } - - private static string _C(string value) - { - return value.Replace('\'', '"'); - } - - private static string _F(FilterNode filter) - { - return _Q(new ClrQuery { Filter = filter }); - } - - private static string _S(params SortNode[] sorts) - { - var cursor = A.Fake>(); - - var i = string.Empty; - - A.CallTo(() => cursor.Sort(A>._)) - .Invokes((SortDefinition sortDefinition) => - { - i = sortDefinition.Render(Serializer, Registry).ToString(); - }); - - cursor.QuerySort(new ClrQuery { Sort = sorts.ToList() }.AdjustToModel()); - - return i; - } - - private static string _Q(ClrQuery query) - { - var filter = query.AdjustToModel().BuildFilter(false).Filter!; - - var rendered = filter.Render(Serializer, Registry).ToString(); - - return rendered; - } - } -} \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentQueryTests.cs similarity index 51% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentQueryTests.cs index c5dd628a4..1270bcf37 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentQueryTests.cs @@ -28,12 +28,9 @@ using Xunit; using ClrFilter = Squidex.Infrastructure.Queries.ClrFilter; using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder; -#pragma warning disable SA1300 // Element should begin with upper-case letter -#pragma warning disable IDE1006 // Naming Styles - namespace Squidex.Domain.Apps.Entities.Contents.MongoDb { - public class MongoDbQueryTests + public class ContentQueryTests { private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry; private static readonly IBsonSerializer Serializer = BsonSerializer.SerializerRegistry.GetSerializer(); @@ -41,7 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb private readonly Schema schemaDef; private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE); - static MongoDbQueryTests() + static ContentQueryTests() { DomainIdSerializer.Register(); @@ -51,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb InstantSerializer.Register(); } - public MongoDbQueryTests() + public ContentQueryTests() { schemaDef = new Schema("user") @@ -86,21 +83,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb A.CallTo(() => app.Languages).Returns(languagesConfig); } - [Fact] - public void Should_not_throw_exception_for_invalid_field() - { - _F(ClrFilter.Eq("data/invalid/iv", "Me")); - } - [Fact] public void Should_make_query_with_id() { var id = Guid.NewGuid(); - var i = _F(ClrFilter.Eq("id", id)); - var o = _C($"{{ '_id' : '{appId}--{id}' }}"); + var filter = ClrFilter.Eq("id", id); - Assert.Equal(o, i); + AssertQuery($"{{ '_id' : '{appId}--{id}' }}", filter); } [Fact] @@ -108,10 +98,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb { var id = DomainId.NewGuid().ToString(); - var i = _F(ClrFilter.Eq("id", id)); - var o = _C($"{{ '_id' : '{appId}--{id}' }}"); + var filter = ClrFilter.Eq("id", id); - Assert.Equal(o, i); + AssertQuery($"{{ '_id' : '{appId}--{id}' }}", filter); } [Fact] @@ -119,10 +108,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb { var id = Guid.NewGuid(); - var i = _F(ClrFilter.In("id", new List { id })); - var o = _C($"{{ '_id' : {{ '$in' : ['{appId}--{id}'] }} }}"); + var filter = ClrFilter.In("id", new List { id }); - Assert.Equal(o, i); + AssertQuery($"{{ '_id' : {{ '$in' : ['{appId}--{id}'] }} }}", filter); } [Fact] @@ -130,225 +118,152 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb { var id = DomainId.NewGuid().ToString(); - var i = _F(ClrFilter.In("id", new List { id })); - var o = _C($"{{ '_id' : {{ '$in' : ['{appId}--{id}'] }} }}"); + var filter = ClrFilter.In("id", new List { id }); - Assert.Equal(o, i); + AssertQuery($"{{ '_id' : {{ '$in' : ['{appId}--{id}'] }} }}", filter); } [Fact] public void Should_make_query_with_lastModified() { - var i = _F(ClrFilter.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); - var o = _C("{ 'mt' : ISODate('1988-01-19T12:00:00Z') }"); + var time = "1988-01-19T12:00:00Z"; + + var filter = ClrFilter.Eq("lastModified", InstantPattern.ExtendedIso.Parse(time).Value); - Assert.Equal(o, i); + AssertQuery("{ 'mt' : ISODate('[value]') }", filter, time); } [Fact] public void Should_make_query_with_lastModifiedBy() { - var i = _F(ClrFilter.Eq("lastModifiedBy", "me")); - var o = _C("{ 'mb' : 'me' }"); + var filter = ClrFilter.Eq("lastModifiedBy", "me"); - Assert.Equal(o, i); + AssertQuery("{ 'mb' : 'me' }", filter); } [Fact] public void Should_make_query_with_created() { - var i = _F(ClrFilter.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); - var o = _C("{ 'ct' : ISODate('1988-01-19T12:00:00Z') }"); + var time = "1988-01-19T12:00:00Z"; + + var filter = ClrFilter.Eq("created", InstantPattern.ExtendedIso.Parse(time).Value); - Assert.Equal(o, i); + AssertQuery("{ 'ct' : ISODate('[value]') }", filter, time); } [Fact] public void Should_make_query_with_createdBy() { - var i = _F(ClrFilter.Eq("createdBy", "user:me")); - var o = _C("{ 'cb' : 'user:me' }"); + var filter = ClrFilter.Eq("createdBy", "subject:me"); - Assert.Equal(o, i); + AssertQuery("{ 'cb' : 'subject:me' }", filter); } [Fact] public void Should_make_query_with_version() { - var i = _F(ClrFilter.Eq("version", 0L)); - var o = _C("{ 'vs' : NumberLong(0) }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_version_and_list() - { - var i = _F(ClrFilter.In("version", new List { 0L, 2L, 5L })); - var o = _C("{ 'vs' : { '$in' : [NumberLong(0), NumberLong(2), NumberLong(5)] } }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_null_regex() - { - var i = _F(ClrFilter.Contains("createdBy", null!)); - var o = _C("{ 'cb' : /null/i }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_empty_test() - { - var i = _F(ClrFilter.Empty("id")); - var o = _C("{ '$or' : [{ '_id' : { '$exists' : false } }, { '_id' : null }, { '_id' : '' }, { '_id' : [] }] }"); + var filter = ClrFilter.Eq("version", 2L); - 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); + AssertQuery("{ 'vs' : NumberLong(2) }", filter); } [Fact] public void Should_make_query_with_datetime_data() { - var i = _F(ClrFilter.Eq("data/birthday/iv", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); - var o = _C("{ 'do.birthday.iv' : '1988-01-19T12:00:00Z' }"); + var time = "1988-01-19T12:00:00Z"; + + var filter = ClrFilter.Eq("data/birthday/iv", InstantPattern.General.Parse(time).Value); - Assert.Equal(o, i); + AssertQuery("{ 'do.birthday.iv' : '[value]' }", filter, time); } [Fact] public void Should_make_query_with_underscore_field() { - var i = _F(ClrFilter.Eq("data/dashed_field/iv", "Value")); - var o = _C("{ 'do.dashed-field.iv' : 'Value' }"); + var filter = ClrFilter.Eq("data/dashed_field/iv", "Value"); - Assert.Equal(o, i); + AssertQuery("{ 'do.dashed-field.iv' : 'Value' }", filter); } [Fact] public void Should_make_query_with_references_equals() { - var i = _F(ClrFilter.Eq("data/friends/iv", "guid")); - var o = _C("{ 'do.friends.iv' : 'guid' }"); + var filter = ClrFilter.Eq("data/friends/iv", "guid"); - Assert.Equal(o, i); + AssertQuery("{ 'do.friends.iv' : 'guid' }", filter); } [Fact] public void Should_make_query_with_array_field() { - var i = _F(ClrFilter.Eq("data/hobbies/iv/name", "PC")); - var o = _C("{ 'do.hobbies.iv.name' : 'PC' }"); + var filter = ClrFilter.Eq("data/hobbies/iv/name", "PC"); - Assert.Equal(o, i); + AssertQuery("{ 'do.hobbies.iv.name' : 'PC' }", filter); } [Fact] public void Should_make_query_with_assets_equals() { - var i = _F(ClrFilter.Eq("data/pictures/iv", "guid")); - var o = _C("{ 'do.pictures.iv' : 'guid' }"); + var filter = ClrFilter.Eq("data/pictures/iv", "guid"); - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_full_text() - { - var i = _Q(new ClrQuery { FullText = "Hello my World" }); - var o = _C("{ '$text' : { '$search' : 'Hello my World' } }"); - - Assert.Equal(o, i); + AssertQuery("{ 'do.pictures.iv' : 'guid' }", filter); } [Fact] public void Should_make_orderby_with_single_field() { - var i = _S(SortBuilder.Descending("data/age/iv")); - var o = _C("{ 'do.age.iv' : -1 }"); + var sorting = SortBuilder.Descending("data/age/iv"); - Assert.Equal(o, i); + AssertSorting("{ 'do.age.iv' : -1 }", sorting); } [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.age.iv' : 1, 'do.firstName.en' : -1 }"); + var sorting1 = SortBuilder.Ascending("data/age/iv"); + var sorting2 = SortBuilder.Descending("data/firstName/en"); - Assert.Equal(o, i); + AssertSorting("{ 'do.age.iv' : 1, 'do.firstName.en' : -1 }", sorting1, sorting2); } - [Fact] - public void Should_make_take_statement() + private void AssertQuery(string expected, FilterNode filter, object? arg = null) { - var query = new ClrQuery { Take = 3 }; - - var cursor = A.Fake>(); - - cursor.QueryLimit(query); - - A.CallTo(() => cursor.Limit(3)) - .MustHaveHappened(); + AssertQuery(new ClrQuery { Filter = filter }, expected, arg); } - [Fact] - public void Should_make_skip_statement() + private void AssertQuery(ClrQuery query, string expected, object? arg = null) { - var query = new ClrQuery { Skip = 3 }; - - var cursor = A.Fake>(); - - cursor.QuerySkip(query); + var rendered = + query.AdjustToModel(appId).BuildFilter().Filter! + .Render(Serializer, Registry).ToString(); - A.CallTo(() => cursor.Skip(3)) - .MustHaveHappened(); - } + var expectation = Cleanup(expected, arg); - private static string _C(string value) - { - return value.Replace('\'', '"'); + Assert.Equal(expectation, rendered); } - private string _F(FilterNode filter) - { - return _Q(new ClrQuery { Filter = filter }); - } - - private string _S(params SortNode[] sorts) + private void AssertSorting(string expected, params SortNode[] sort) { var cursor = A.Fake>(); - var i = string.Empty; + var rendered = string.Empty; A.CallTo(() => cursor.Sort(A>._)) .Invokes((SortDefinition sortDefinition) => { - i = sortDefinition.Render(Serializer, Registry).ToString(); + rendered = sortDefinition.Render(Serializer, Registry).ToString(); }); - cursor.QuerySort(new ClrQuery { Sort = sorts.ToList() }.AdjustToModel(appId)); + cursor.QuerySort(new ClrQuery { Sort = sort.ToList() }.AdjustToModel(appId)); + + var expectation = Cleanup(expected); - return i; + Assert.Equal(expectation, rendered); } - private string _Q(ClrQuery query) + private static string Cleanup(string filter, object? arg = null) { - var rendered = - query.AdjustToModel(appId).BuildFilter().Filter! - .Render(Serializer, Registry).ToString(); - - return rendered; + return filter.Replace('\'', '"').Replace("[value]", arg?.ToString()); } } } \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs index b93a20343..0651f9755 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs @@ -33,7 +33,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb private readonly int numValues = 10000; private readonly IMongoClient mongoClient = new MongoClient("mongodb://localhost"); private readonly IMongoDatabase mongoDatabase; - private readonly IMongoDatabase mongoDatabaseWildcard; public MongoContentRepository ContentRepository { get; } @@ -55,7 +54,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb public ContentsQueryFixture() { mongoDatabase = mongoClient.GetDatabase("QueryTests"); - mongoDatabaseWildcard = mongoClient.GetDatabase("QueryTestsWildcard"); SetupJson(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryIntegrationTests.cs similarity index 97% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryIntegrationTests.cs index 5618e0a40..71964b08a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryIntegrationTests.cs @@ -21,11 +21,11 @@ using F = Squidex.Infrastructure.Queries.ClrFilter; namespace Squidex.Domain.Apps.Entities.Contents.MongoDb { [Trait("Category", "Dependencies")] - public class ContentsQueryTests : IClassFixture + public class ContentsQueryIntegrationTests : IClassFixture { public ContentsQueryFixture _ { get; } - public ContentsQueryTests(ContentsQueryFixture fixture) + public ContentsQueryIntegrationTests(ContentsQueryFixture fixture) { _ = fixture; } diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoQueryTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoQueryTests.cs new file mode 100644 index 000000000..2a7d06b95 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoQueryTests.cs @@ -0,0 +1,336 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using FakeItEasy; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using NodaTime; +using NodaTime.Text; +using Squidex.Infrastructure.MongoDb.Queries; +using Squidex.Infrastructure.Queries; +using Xunit; +using ClrFilter = Squidex.Infrastructure.Queries.ClrFilter; +using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder; + +namespace Squidex.Infrastructure.MongoDb +{ + public class MongoQueryTests + { + private readonly IBsonSerializerRegistry registry = BsonSerializer.SerializerRegistry; + private readonly IBsonSerializer serializer = BsonSerializer.SerializerRegistry.GetSerializer(); + + public class TestEntity + { + public DomainId Id { get; set; } + + public Instant Created { get; set; } + + public RefToken CreatedBy { get; set; } + + public string Text { get; set; } + + public long Version { get; set; } + } + + static MongoQueryTests() + { + DomainIdSerializer.Register(); + + TypeConverterStringSerializer.Register(); + + InstantSerializer.Register(); + } + + [Fact] + public void Should_not_throw_exception_for_invalid_field() + { + var filter = ClrFilter.Eq("invalid", "Value"); + + AssertQuery("{ 'invalid' : 'Value' }", filter); + } + + [Fact] + public void Should_make_query_with_id_guid() + { + var id = Guid.NewGuid(); + + var filter = ClrFilter.Eq("Id", id); + + AssertQuery("{ '_id' : '[value]' }", filter, id); + } + + [Fact] + public void Should_make_query_with_id_string() + { + var id = DomainId.NewGuid().ToString(); + + var filter = ClrFilter.Eq("Id", id); + + AssertQuery("{ '_id' : '[value]' }", filter, id); + } + + [Fact] + public void Should_make_query_with_id_guid_list() + { + var id = Guid.NewGuid(); + + var filter = ClrFilter.In("Id", new List { id }); + + AssertQuery("{ '_id' : { '$in' : ['[value]'] } }", filter, id); + } + + [Fact] + public void Should_make_query_with_id_string_list() + { + var id = DomainId.NewGuid().ToString(); + + var filter = ClrFilter.In("Id", new List { id }); + + AssertQuery("{ '_id' : { '$in' : ['[value]'] } }", filter, id); + } + + [Fact] + public void Should_make_query_with_instant() + { + var time = "1988-01-19T12:00:00Z"; + + var filter = ClrFilter.Eq("Version", InstantPattern.ExtendedIso.Parse(time).Value); + + AssertQuery("{ 'Version' : ISODate('[value]') }", filter, time); + } + + [Fact] + public void Should_make_query_with_reftoken() + { + var filter = ClrFilter.Eq("CreatedBy", "subject:me"); + + AssertQuery("{ 'CreatedBy' : 'subject:me' }", filter); + } + + [Fact] + public void Should_make_query_with_reftoken_cleanup() + { + var filter = ClrFilter.Eq("CreatedBy", "me"); + + AssertQuery("{ 'CreatedBy' : 'subject:me' }", filter); + } + + [Fact] + public void Should_make_query_with_reftoken_fix() + { + var filter = ClrFilter.Eq("CreatedBy", "user:me"); + + AssertQuery("{ 'CreatedBy' : 'subject:me' }", filter); + } + + [Fact] + public void Should_make_query_with_number() + { + var filter = ClrFilter.Eq("Version", 0L); + + AssertQuery("{ 'Version' : NumberLong(0) }", filter); + } + + [Fact] + public void Should_make_query_with_number_and_list() + { + var filter = ClrFilter.In("Version", new List { 0L, 2L, 5L }); + + AssertQuery("{ 'Version' : { '$in' : [NumberLong(0), NumberLong(2), NumberLong(5)] } }", filter); + } + + [Fact] + public void Should_make_query_with_contains_and_null_value() + { + var filter = ClrFilter.Contains("Text", null!); + + AssertQuery("{ 'Text' : /null/i }", filter); + } + + [Fact] + public void Should_make_query_with_contains() + { + var filter = ClrFilter.Contains("Text", "search"); + + AssertQuery("{ 'Text' : /search/i }", filter); + } + + [Fact] + public void Should_make_query_with_endswith_and_null_value() + { + var filter = ClrFilter.EndsWith("Text", null!); + + AssertQuery("{ 'Text' : /null$/i }", filter); + } + + [Fact] + public void Should_make_query_with_endswith() + { + var filter = ClrFilter.EndsWith("Text", "search"); + + AssertQuery("{ 'Text' : /search$/i }", filter); + } + + [Fact] + public void Should_make_query_with_startswith_and_null_value() + { + var filter = ClrFilter.StartsWith("Text", null!); + + AssertQuery("{ 'Text' : /^null/i }", filter); + } + + [Fact] + public void Should_make_query_with_startswith() + { + var filter = ClrFilter.StartsWith("Text", "search"); + + AssertQuery("{ 'Text' : /^search/i }", filter); + } + + [Fact] + public void Should_make_query_with_matchs_and_null_value() + { + var filter = ClrFilter.Matchs("Text", null!); + + AssertQuery("{ 'Text' : /null/i }", filter); + } + + [Fact] + public void Should_make_query_with_matchs() + { + var filter = ClrFilter.Matchs("Text", "^search$"); + + AssertQuery("{ 'Text' : /^search$/i }", filter); + } + + [Fact] + public void Should_make_query_with_matchs_and_regex_syntax() + { + var filter = ClrFilter.Matchs("Text", "/search/i"); + + AssertQuery("{ 'Text' : /search/i }", filter); + } + + [Fact] + public void Should_make_query_with_matchs_and_regex_case_sensitive_syntax() + { + var filter = ClrFilter.Matchs("Text", "/search/"); + + AssertQuery("{ 'Text' : /search/ }", filter); + } + + [Fact] + public void Should_make_query_with_empty_for_class() + { + var filter = ClrFilter.Empty("Text"); + + AssertQuery("{ '$or' : [{ 'Text' : { '$exists' : false } }, { 'Text' : null }, { 'Text' : '' }, { 'Text' : { '$size' : 0 } }] }", filter); + } + + [Fact] + public void Should_make_query_with_exists() + { + var filter = ClrFilter.Exists("Text"); + + AssertQuery("{ 'Text' : { '$exists' : true, '$ne' : null } }", filter); + } + + [Fact] + public void Should_make_query_with_full_text() + { + var query = new ClrQuery { FullText = "Hello my World" }; + + AssertQuery(query, "{ '$text' : { '$search' : 'Hello my World' } }"); + } + + [Fact] + public void Should_make_orderby_with_single_field() + { + var sorting = SortBuilder.Descending("Number"); + + AssertSorting("{ 'Number' : -1 }", sorting); + } + + [Fact] + public void Should_make_orderby_with_multiple_fields() + { + var sorting1 = SortBuilder.Ascending("Number"); + var sorting2 = SortBuilder.Descending("Text"); + + AssertSorting("{ 'Number' : 1, 'Text' : -1 }", sorting1, sorting2); + } + + [Fact] + public void Should_make_take_statement() + { + var query = new ClrQuery { Take = 3 }; + + var cursor = A.Fake>(); + + cursor.QueryLimit(query); + + A.CallTo(() => cursor.Limit(3)) + .MustHaveHappened(); + } + + [Fact] + public void Should_make_skip_statement() + { + var query = new ClrQuery { Skip = 3 }; + + var cursor = A.Fake>(); + + cursor.QuerySkip(query); + + A.CallTo(() => cursor.Skip(3)) + .MustHaveHappened(); + } + + private void AssertQuery(string expected, FilterNode filter, object? arg = null) + { + AssertQuery(new ClrQuery { Filter = filter }, expected, arg); + } + + private void AssertQuery(ClrQuery query, string expected, object? arg = null) + { + var rendered = + query.BuildFilter().Filter! + .Render(serializer, registry).ToString(); + + var expectation = Cleanup(expected, arg); + + Assert.Equal(expectation, rendered); + } + + private void AssertSorting(string expected, params SortNode[] sort) + { + var cursor = A.Fake>(); + + var rendered = string.Empty; + + A.CallTo(() => cursor.Sort(A>._)) + .Invokes((SortDefinition sortDefinition) => + { + rendered = sortDefinition.Render(serializer, registry).ToString(); + }); + + cursor.QuerySort(new ClrQuery { Sort = sort.ToList() }); + + var expectation = Cleanup(expected); + + Assert.Equal(expectation, rendered); + } + + private static string Cleanup(string filter, object? arg = null) + { + return filter.Replace('\'', '"').Replace("[value]", arg?.ToString()); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs index 9251151bf..634edbdd6 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs @@ -336,6 +336,15 @@ namespace Squidex.Infrastructure.Queries Assert.Equal(o, i); } + [Fact] + public void Should_parse_filter_with_matchs() + { + var i = _Q("$filter=matchs(lastName, 'Duck')"); + var o = _C("Filter: matchs(lastName, 'Duck')"); + + Assert.Equal(o, i); + } + [Fact] public void Should_parse_filter_with_empty() { diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs index e246c2255..cb445fe7c 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs @@ -337,16 +337,31 @@ namespace TestSuite.ApiTests public async Task Should_delete_asset(bool permanent) { // STEP 1: Create asset - var asset_1 = await _.UploadFileAsync("Assets/logo-squared.png", "image/png"); + var asset = await _.UploadFileAsync("Assets/logo-squared.png", "image/png"); // STEP 2: Delete asset - await _.Assets.DeleteAssetAsync(_.AppName, asset_1.Id, permanent: permanent); + await _.Assets.DeleteAssetAsync(_.AppName, asset.Id, permanent: permanent); // Should return 404 when asset deleted. - var ex = await Assert.ThrowsAsync(() => _.Assets.GetAssetAsync(_.AppName, asset_1.Id)); + var ex = await Assert.ThrowsAsync(() => _.Assets.GetAssetAsync(_.AppName, asset.Id)); Assert.Equal(404, ex.StatusCode); + + + // STEP 3: Retrieve all items and ensure that the deleted item does not exist. + var updated = await _.Assets.GetAssetsAsync(_.AppName, (AssetQuery)null); + + Assert.DoesNotContain(updated.Items, x => x.Id == asset.Id); + + + // STEP 4: Retrieve all deleted items and check if found. + var deleted = await _.Assets.GetAssetsAsync(_.AppName, new AssetQuery + { + Filter = "isDeleted eq true" + }); + + Assert.Equal(!permanent, deleted.Items.Any(x => x.Id == asset.Id)); } [Theory] diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs index 72a842e91..d2856e5f3 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Squidex.ClientLibrary; using TestSuite.Fixtures; @@ -579,6 +580,15 @@ namespace TestSuite.ApiTests var updated = await _.Contents.GetAsync(); Assert.DoesNotContain(updated.Items, x => x.Id == content.Id); + + + // STEP 4: Retrieve all deleted items and check if found. + var deleted = await _.Contents.GetAsync(new ContentQuery + { + Filter = "isDeleted eq true" + }, QueryContext.Default.Unpublished(true)); + + Assert.Equal(!permanent, deleted.Items.Any(x => x.Id == content.Id)); } [Theory] diff --git a/frontend/app/shared/state/query.ts b/frontend/app/shared/state/query.ts index 988069c60..c0a432e34 100644 --- a/frontend/app/shared/state/query.ts +++ b/frontend/app/shared/state/query.ts @@ -225,6 +225,7 @@ const CompareOperator: ReadonlyArray = [ ]; const StringOperators: ReadonlyArray = [ + { name: 'i18n:common.queryOperators.matchs', value: 'matchs' }, { name: 'i18n:common.queryOperators.startsWith', value: 'startsWith' }, { name: 'i18n:common.queryOperators.endsWith', value: 'endsWith' }, { name: 'i18n:common.queryOperators.contains', value: 'contains' }