Browse Source

Feature/match (#670)

* Match  regex and query cleanup.

* Query by deleted.

* Permanent deletion fix and query.
pull/671/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
7cca19e50a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      backend/i18n/frontend_en.json
  2. 1
      backend/i18n/frontend_it.json
  3. 1
      backend/i18n/frontend_nl.json
  4. 1
      backend/i18n/source/frontend_en.json
  5. 92
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/AdaptIdVisitor.cs
  6. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  7. 15
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs
  8. 16
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs
  9. 71
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/AdaptionVisitor.cs
  10. 20
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs
  11. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs
  12. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
  13. 7
      backend/src/Squidex.Domain.Apps.Entities/Contents/Schemas/ContentJsonSchemaBuilder.cs
  14. 32
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs
  15. 5
      backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs
  16. 2
      backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs
  17. 1
      backend/src/Squidex.Infrastructure/Queries/CompareOperator.cs
  18. 2
      backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterSurrogate.cs
  19. 1
      backend/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs
  20. 30
      backend/src/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs
  21. 29
      backend/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs
  22. 6
      backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs
  23. 7
      backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs
  24. 34
      backend/src/Squidex.Infrastructure/Queries/QueryExtensions.cs
  25. 11
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  26. 34
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/DeleteAssetDto.cs
  27. 9
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  28. 34
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/DeleteContentDto.cs
  29. 248
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetQueryTests.cs
  30. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryIntegrationTests.cs
  31. 239
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs
  32. 203
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentQueryTests.cs
  33. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs
  34. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryIntegrationTests.cs
  35. 336
      backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoQueryTests.cs
  36. 9
      backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs
  37. 21
      backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs
  38. 10
      backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs
  39. 1
      frontend/app/shared/state/query.ts

1
backend/i18n/frontend_en.json

@ -298,6 +298,7 @@
"common.queryOperators.gt": "is greater than", "common.queryOperators.gt": "is greater than",
"common.queryOperators.le": "is less than pr equals to", "common.queryOperators.le": "is less than pr equals to",
"common.queryOperators.lt": "is less than", "common.queryOperators.lt": "is less than",
"common.queryOperators.matchs": "matchs",
"common.queryOperators.ne": "is not equals to", "common.queryOperators.ne": "is not equals to",
"common.queryOperators.startsWith": "starts with", "common.queryOperators.startsWith": "starts with",
"common.refresh": "Refresh", "common.refresh": "Refresh",

1
backend/i18n/frontend_it.json

@ -298,6 +298,7 @@
"common.queryOperators.gt": "è maggiore di", "common.queryOperators.gt": "è maggiore di",
"common.queryOperators.le": "è minore o uguale a", "common.queryOperators.le": "è minore o uguale a",
"common.queryOperators.lt": "è minore di", "common.queryOperators.lt": "è minore di",
"common.queryOperators.matchs": "matchs",
"common.queryOperators.ne": "non è uguale a", "common.queryOperators.ne": "non è uguale a",
"common.queryOperators.startsWith": "inizia con", "common.queryOperators.startsWith": "inizia con",
"common.refresh": "Aggiorna", "common.refresh": "Aggiorna",

1
backend/i18n/frontend_nl.json

@ -298,6 +298,7 @@
"common.queryOperators.gt": "is groter dan", "common.queryOperators.gt": "is groter dan",
"common.queryOperators.le": "is kleiner dan of is gelijk aan", "common.queryOperators.le": "is kleiner dan of is gelijk aan",
"common.queryOperators.lt": "is kleiner dan", "common.queryOperators.lt": "is kleiner dan",
"common.queryOperators.matchs": "matchs",
"common.queryOperators.ne": "is niet gelijk aan", "common.queryOperators.ne": "is niet gelijk aan",
"common.queryOperators.startsWith": "begint met", "common.queryOperators.startsWith": "begint met",
"common.refresh": "Vernieuwen", "common.refresh": "Vernieuwen",

1
backend/i18n/source/frontend_en.json

@ -298,6 +298,7 @@
"common.queryOperators.gt": "is greater than", "common.queryOperators.gt": "is greater than",
"common.queryOperators.le": "is less than pr equals to", "common.queryOperators.le": "is less than pr equals to",
"common.queryOperators.lt": "is less than", "common.queryOperators.lt": "is less than",
"common.queryOperators.matchs": "matchs",
"common.queryOperators.ne": "is not equals to", "common.queryOperators.ne": "is not equals to",
"common.queryOperators.startsWith": "starts with", "common.queryOperators.startsWith": "starts with",
"common.refresh": "Refresh", "common.refresh": "Refresh",

92
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<ClrValue, AdaptIdVisitor.Args>
{
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<ClrValue>? AdaptFilter(FilterNode<ClrValue> filter, DomainId appId)
{
var args = new Args(appId);
return filter.Accept(Instance, args);
}
public override FilterNode<ClrValue> Visit(CompareFilter<ClrValue> 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<string> 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<Guid> 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<Guid> 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 };
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
} }
else else
{ {
var query = q.Query.AdjustToModel(); var query = q.Query.AdjustToModel(appId);
var filter = query.BuildFilter(appId, parentId); var filter = query.BuildFilter(appId, parentId);

15
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<MongoAssetEntity> Filter = Builders<MongoAssetEntity>.Filter; private static readonly FilterDefinitionBuilder<MongoAssetEntity> Filter = Builders<MongoAssetEntity>.Filter;
public static ClrQuery AdjustToModel(this ClrQuery query) public static ClrQuery AdjustToModel(this ClrQuery query, DomainId appId)
{ {
if (query.Filter != null) if (query.Filter != null)
{ {
query.Filter = FirstPascalPathConverter<ClrValue>.Transform(query.Filter); query.Filter = FirstPascalPathConverter<ClrValue>.Transform(query.Filter);
} }
if (query.Filter != null)
{
query.Filter = AdaptIdVisitor.AdaptFilter(query.Filter, appId);
}
if (query.Sort != null) if (query.Sort != null)
{ {
query.Sort = query.Sort.Select(x => new SortNode(x.Path.ToFirstPascalCase(), x.Order)).ToList(); 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<FilterDefinition<MongoAssetEntity>> var filters = new List<FilterDefinition<MongoAssetEntity>>
{ {
Filter.Eq(x => x.IndexedAppId, appId), Filter.Eq(x => x.IndexedAppId, appId)
Filter.Eq(x => x.IsDeleted, false)
}; };
if (!query.HasFilterField("IsDeleted"))
{
filters.Add(Filter.Eq(x => x.IsDeleted, false));
}
if (parentId != null) if (parentId != null)
{ {
if (parentId == DomainId.Empty) if (parentId == DomainId.Empty)

16
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) 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) if (query.Sort != null)
@ -94,7 +99,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
public static FilterNode<ClrValue>? AdjustToModel(this FilterNode<ClrValue> filter, DomainId appId) public static FilterNode<ClrValue>? AdjustToModel(this FilterNode<ClrValue> filter, DomainId appId)
{ {
return AdaptionVisitor.AdaptFilter(filter, appId); var result = AdaptionVisitor.AdaptFilter(filter);
if (result != null)
{
result = AdaptIdVisitor.AdaptFilter(result, appId);
}
return result;
} }
} }
} }

71
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/AdaptionVisitor.cs

@ -6,89 +6,34 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Linq;
using NodaTime;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{ {
internal sealed class AdaptionVisitor : TransformVisitor<ClrValue, AdaptionVisitor.Args> internal sealed class AdaptionVisitor : TransformVisitor<ClrValue, None>
{ {
private static readonly AdaptionVisitor Instance = new AdaptionVisitor(); private static readonly AdaptionVisitor Instance = new AdaptionVisitor();
public readonly struct Args
{
public readonly DomainId AppId;
public Args(DomainId appId)
{
AppId = appId;
}
}
private AdaptionVisitor() private AdaptionVisitor()
{ {
} }
public static FilterNode<ClrValue>? AdaptFilter(FilterNode<ClrValue> filter, DomainId appId) public static FilterNode<ClrValue>? AdaptFilter(FilterNode<ClrValue> filter)
{ {
var args = new Args(appId); return filter.Accept(Instance, None.Value);
return filter.Accept(Instance, args);
} }
public override FilterNode<ClrValue> Visit(CompareFilter<ClrValue> nodeIn, Args args) public override FilterNode<ClrValue> Visit(CompareFilter<ClrValue> nodeIn, None args)
{ {
var result = nodeIn; if (string.Equals(nodeIn.Path[0], "id", StringComparison.OrdinalIgnoreCase))
var (path, _, value) = nodeIn;
var clrValue = value.Value;
if (string.Equals(path[0], "id", StringComparison.OrdinalIgnoreCase))
{
path = "_id";
if (clrValue is List<string> 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(); return nodeIn;
} }
else if (clrValue is List<Guid> 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
{
path = Adapt.MapPath(path);
if (clrValue is List<Guid> guidList) var path = Adapt.MapPath(nodeIn.Path);
{
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 }; return nodeIn with { Path = path };
} }
} }
} }

20
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; return query.Sort?.All(x => x.Path.ToString() == "mt" && x.Order == SortOrder.Descending) == true;
} }
private static FilterDefinition<MongoContentEntity> BuildFilter(DomainId appId, DomainId schemaId, FilterNode<ClrValue>? filterNode) private static FilterDefinition<MongoContentEntity> BuildFilter(DomainId appId, DomainId schemaId, FilterNode<ClrValue>? filter)
{ {
var filters = new List<FilterDefinition<MongoContentEntity>> var filters = new List<FilterDefinition<MongoContentEntity>>
{ {
Filter.Eq(x => x.IndexedAppId, appId), Filter.Eq(x => x.IndexedAppId, appId),
Filter.Eq(x => x.IndexedSchemaId, schemaId), Filter.Eq(x => x.IndexedSchemaId, schemaId)
Filter.Ne(x => x.IsDeleted, true)
}; };
if (filterNode != null) if (filter?.HasField("dl") != true)
{ {
filters.Add(filterNode.BuildFilter<MongoContentEntity>()); filters.Add(Filter.Ne(x => x.IsDeleted, true));
}
if (filter != null)
{
filters.Add(filter.BuildFilter<MongoContentEntity>());
} }
return Filter.And(filters); return Filter.And(filters);
@ -225,9 +229,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{ {
Filter.Eq(x => x.IndexedAppId, appId), Filter.Eq(x => x.IndexedAppId, appId),
Filter.In(x => x.IndexedSchemaId, schemaIds), 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) if (query?.Filter != null)
{ {
filters.Add(query.Filter.BuildFilter<MongoContentEntity>()); filters.Add(query.Filter.BuildFilter<MongoContentEntity>());

2
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("fileName", JsonObjectType.String);
AddProperty("fileSize", JsonObjectType.Integer); AddProperty("fileSize", JsonObjectType.Integer);
AddProperty("fileVersion", JsonObjectType.Integer); AddProperty("fileVersion", JsonObjectType.Integer);
AddProperty("isDeleted", JsonObjectType.Boolean);
AddProperty("isProtected", JsonObjectType.Boolean); AddProperty("isProtected", JsonObjectType.Boolean);
AddProperty("lastModified", JsonObjectType.String, JsonFormatStrings.DateTime); AddProperty("lastModified", JsonObjectType.String, JsonFormatStrings.DateTime);
AddProperty("lastModifiedBy", JsonObjectType.String); AddProperty("lastModifiedBy", JsonObjectType.String);
@ -196,6 +197,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
AddProperty("createdBy", EdmPrimitiveTypeKind.String); AddProperty("createdBy", EdmPrimitiveTypeKind.String);
AddProperty("fileHash", EdmPrimitiveTypeKind.String); AddProperty("fileHash", EdmPrimitiveTypeKind.String);
AddProperty("fileName", EdmPrimitiveTypeKind.String); AddProperty("fileName", EdmPrimitiveTypeKind.String);
AddProperty("isDeleted", EdmPrimitiveTypeKind.Boolean);
AddProperty("isProtected", EdmPrimitiveTypeKind.Boolean); AddProperty("isProtected", EdmPrimitiveTypeKind.Boolean);
AddProperty("fileSize", EdmPrimitiveTypeKind.Int64); AddProperty("fileSize", EdmPrimitiveTypeKind.Int64);
AddProperty("fileVersion", EdmPrimitiveTypeKind.Int64); AddProperty("fileVersion", EdmPrimitiveTypeKind.Int64);

5
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 static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60);
private readonly EdmModel genericEdmModel = BuildEdmModel("Generic", "Content", new EdmModel(), null); 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 IMemoryCache cache;
private readonly IJsonSerializer jsonSerializer; private readonly IJsonSerializer jsonSerializer;
private readonly ITextIndex textIndex; private readonly ITextIndex textIndex;
@ -237,7 +237,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var dataSchema = schema.BuildJsonSchema(app.PartitionResolver(), withHiddenFields); 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) 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); var entityType = new EdmEntityType(modelName, name);
entityType.AddStructuralProperty("id", EdmPrimitiveTypeKind.String); entityType.AddStructuralProperty("id", EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty("isDeleted", EdmPrimitiveTypeKind.Boolean);
entityType.AddStructuralProperty("created", EdmPrimitiveTypeKind.DateTimeOffset); entityType.AddStructuralProperty("created", EdmPrimitiveTypeKind.DateTimeOffset);
entityType.AddStructuralProperty("createdBy", EdmPrimitiveTypeKind.String); entityType.AddStructuralProperty("createdBy", EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty("lastModified", EdmPrimitiveTypeKind.DateTimeOffset); entityType.AddStructuralProperty("lastModified", EdmPrimitiveTypeKind.DateTimeOffset);

7
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 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 var jsonSchema = new JsonSchema
{ {
@ -28,6 +28,11 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
Type = JsonObjectType.Object Type = JsonObjectType.Object
}; };
if (withDeleted)
{
jsonSchema.Properties["isDeleted"] = SchemaBuilder.BooleanProperty("True when deleted.", false);
}
if (extended) if (extended)
{ {
jsonSchema.Properties["newStatusColor"] = SchemaBuilder.StringProperty("The color of the new status.", false); jsonSchema.Properties["newStatusColor"] = SchemaBuilder.StringProperty("The color of the new status.", false);

32
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs

@ -56,13 +56,15 @@ namespace Squidex.Infrastructure.MongoDb.Queries
case CompareOperator.Empty: case CompareOperator.Empty:
return Filter.Or( return Filter.Or(
Filter.Exists(propertyName, false), Filter.Exists(propertyName, false),
Filter.Eq(propertyName, default(T)!), Filter.Eq<object?>(propertyName, null),
Filter.Eq(propertyName, string.Empty), Filter.Eq<object?>(propertyName, string.Empty),
Filter.Eq(propertyName, Array.Empty<T>())); Filter.Size(propertyName, 0));
case CompareOperator.Exists: case CompareOperator.Exists:
return Filter.And( return Filter.And(
Filter.Exists(propertyName, true), Filter.Exists(propertyName, true),
Filter.Ne<object?>(propertyName, null)); Filter.Ne<object?>(propertyName, null));
case CompareOperator.Matchs:
return Filter.Regex(propertyName, BuildMatchRegex(nodeIn));
case CompareOperator.StartsWith: case CompareOperator.StartsWith:
return Filter.Regex(propertyName, BuildRegex(nodeIn, s => "^" + s)); return Filter.Regex(propertyName, BuildRegex(nodeIn, s => "^" + s));
case CompareOperator.Contains: case CompareOperator.Contains:
@ -88,6 +90,30 @@ namespace Squidex.Infrastructure.MongoDb.Queries
throw new NotSupportedException(); throw new NotSupportedException();
} }
private static BsonRegularExpression BuildMatchRegex(CompareFilter<ClrValue> 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<ClrValue> node, Func<string, string> formatter) private static BsonRegularExpression BuildRegex(CompareFilter<ClrValue> node, Func<string, string> formatter)
{ {
return new BsonRegularExpression(formatter(node.Value.Value?.ToString() ?? "null"), "i"); return new BsonRegularExpression(formatter(node.Value.Value?.ToString() ?? "null"), "i");

5
backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs

@ -71,6 +71,11 @@ namespace Squidex.Infrastructure.Queries
return Binary(path, CompareOperator.Contains, value); return Binary(path, CompareOperator.Contains, value);
} }
public static CompareFilter<ClrValue> Matchs(PropertyPath path, ClrValue value)
{
return Binary(path, CompareOperator.Matchs, value);
}
public static CompareFilter<ClrValue> EndsWith(PropertyPath path, ClrValue value) public static CompareFilter<ClrValue> EndsWith(PropertyPath path, ClrValue value)
{ {
return Binary(path, CompareOperator.EndsWith, value); return Binary(path, CompareOperator.EndsWith, value);

2
backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs

@ -33,6 +33,8 @@ namespace Squidex.Infrastructure.Queries
return $"empty({Path})"; return $"empty({Path})";
case CompareOperator.Exists: case CompareOperator.Exists:
return $"exists({Path})"; return $"exists({Path})";
case CompareOperator.Matchs:
return $"matchs({Path}, {Value})";
case CompareOperator.EndsWith: case CompareOperator.EndsWith:
return $"endsWith({Path}, {Value})"; return $"endsWith({Path}, {Value})";
case CompareOperator.StartsWith: case CompareOperator.StartsWith:

1
backend/src/Squidex.Infrastructure/Queries/CompareOperator.cs

@ -19,6 +19,7 @@ namespace Squidex.Infrastructure.Queries
In, In,
LessThan, LessThan,
LessThanOrEqual, LessThanOrEqual,
Matchs,
NotEquals, NotEquals,
StartsWith StartsWith
} }

2
backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterSurrogate.cs

@ -77,6 +77,8 @@ namespace Squidex.Infrastructure.Queries
return CompareOperator.Empty; return CompareOperator.Empty;
case "exists": case "exists":
return CompareOperator.Exists; return CompareOperator.Exists;
case "matchs":
return CompareOperator.Matchs;
case "contains": case "contains":
return CompareOperator.Contains; return CompareOperator.Contains;
case "endswith": case "endswith":

1
backend/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs

@ -43,6 +43,7 @@ namespace Squidex.Infrastructure.Queries.Json
CompareOperator.In, CompareOperator.In,
CompareOperator.LessThan, CompareOperator.LessThan,
CompareOperator.LessThanOrEqual, CompareOperator.LessThanOrEqual,
CompareOperator.Matchs,
CompareOperator.NotEquals, CompareOperator.NotEquals,
CompareOperator.StartsWith CompareOperator.StartsWith
}; };

30
backend/src/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs

@ -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<object>
{
private static readonly ConstantVisitor Instance = new ConstantVisitor();
private ConstantVisitor()
{
}
public static object Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override object Visit(ConstantNode nodeIn)
{
return nodeIn.Value;
}
}
}

29
backend/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs

@ -40,34 +40,7 @@ namespace Squidex.Infrastructure.Queries.OData
public override ClrValue Visit(ConvertNode nodeIn) public override ClrValue Visit(ConvertNode nodeIn)
{ {
var value = ConstantVisitor.Visit(nodeIn.Source); return nodeIn.Source.Accept(this);
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();
} }
public override ClrValue Visit(CollectionConstantNode nodeIn) public override ClrValue Visit(CollectionConstantNode nodeIn)

6
backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs

@ -26,6 +26,12 @@ namespace Squidex.Infrastructure.Queries.OData
EdmCoreModel.Instance.GetBoolean(false), EdmCoreModel.Instance.GetBoolean(false),
EdmCoreModel.Instance.GetUntyped())); EdmCoreModel.Instance.GetUntyped()));
CustomUriFunctions.AddCustomUriFunction("matchs",
new FunctionSignatureWithReturnType(
EdmCoreModel.Instance.GetBoolean(false),
EdmCoreModel.Instance.GetString(false),
EdmCoreModel.Instance.GetString(false)));
CustomUriFunctions.AddCustomUriFunction("distanceto", CustomUriFunctions.AddCustomUriFunction("distanceto",
new FunctionSignatureWithReturnType( new FunctionSignatureWithReturnType(
EdmCoreModel.Instance.GetDouble(false), EdmCoreModel.Instance.GetDouble(false),

7
backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs

@ -68,6 +68,13 @@ namespace Squidex.Infrastructure.Queries.OData
var valueNode = nodeIn.Parameters.ElementAt(1); 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)) if (string.Equals(nodeIn.Name, "endswith", StringComparison.OrdinalIgnoreCase))
{ {
var value = ConstantWithTypeVisitor.Visit(valueNode); var value = ConstantWithTypeVisitor.Visit(valueNode);

34
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<T>(this Query<T>? query, string field)
{
return HasField(query?.Filter, field);
}
public static bool HasField<T>(this FilterNode<T>? filter, string field)
{
if (filter == null)
{
return false;
}
var fields = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
filter.AddFields(fields);
return fields.Contains(field);
}
}
}

11
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -355,8 +355,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="id">The id of the asset to delete.</param> /// <param name="id">The id of the asset to delete.</param>
/// <param name="checkReferrers">True to check referrers of this asset.</param> /// <param name="request">The request parameters.</param>
/// <param name="permanent">True to delete the asset permanently.</param>
/// <returns> /// <returns>
/// 204 => Asset deleted. /// 204 => Asset deleted.
/// 404 => Asset or app not found. /// 404 => Asset or app not found.
@ -365,11 +364,11 @@ namespace Squidex.Areas.Api.Controllers.Assets
[Route("apps/{app}/assets/{id}/")] [Route("apps/{app}/assets/{id}/")]
[ApiPermissionOrAnonymous(Permissions.AppAssetsDelete)] [ApiPermissionOrAnonymous(Permissions.AppAssetsDelete)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DeleteAsset(string app, DomainId id, public async Task<IActionResult> DeleteAsset(string app, DomainId id, DeleteAssetDto request)
[FromQuery] bool checkReferrers = false,
[FromQuery] bool permanent = false)
{ {
await CommandBus.PublishAsync(new DeleteAsset { AssetId = id, CheckReferrers = checkReferrers, Permanent = false }); var command = request.ToCommand(id);
await CommandBus.PublishAsync(command);
return NoContent(); return NoContent();
} }

34
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
{
/// <summary>
/// True to check referrers of this asset.
/// </summary>
[FromQuery]
public bool CheckReferrers { get; set; }
/// <summary>
/// True to delete the asset permanently.
/// </summary>
[FromQuery]
public bool Permanent { get; set; }
public DeleteAsset ToCommand(DomainId id)
{
return SimpleMapper.Map(this, new DeleteAsset { AssetId = id });
}
}
}

9
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -606,8 +606,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the content item to delete.</param> /// <param name="id">The id of the content item to delete.</param>
/// <param name="checkReferrers">True to check referrers of this content.</param> /// <param name="request">The request parameters.</param>
/// <param name="permanent">True to delete the asset permanently.</param>
/// <returns> /// <returns>
/// 204 => Content deleted. /// 204 => Content deleted.
/// 400 => Content cannot be deleted. /// 400 => Content cannot be deleted.
@ -620,11 +619,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
[Route("content/{app}/{name}/{id}/")] [Route("content/{app}/{name}/{id}/")]
[ApiPermissionOrAnonymous(Permissions.AppContentsDeleteOwn)] [ApiPermissionOrAnonymous(Permissions.AppContentsDeleteOwn)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DeleteContent(string app, string name, DomainId id, public async Task<IActionResult> DeleteContent(string app, string name, DomainId id, DeleteContentDto request)
[FromQuery] bool checkReferrers = false,
[FromQuery] bool permanent = false)
{ {
var command = new DeleteContent { ContentId = id, CheckReferrers = checkReferrers, Permanent = permanent }; var command = request.ToCommand(id);
await CommandBus.PublishAsync(command); await CommandBus.PublishAsync(command);

34
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
{
/// <summary>
/// True to check referrers of this content.
/// </summary>
[FromQuery]
public bool CheckReferrers { get; set; }
/// <summary>
/// True to delete the content permanently.
/// </summary>
[FromQuery]
public bool Permanent { get; set; }
public DeleteContent ToCommand(DomainId id)
{
return SimpleMapper.Map(this, new DeleteContent { ContentId = id });
}
}
}

248
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<MongoAssetEntity> Serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoAssetEntity>();
private readonly DomainId appId = DomainId.NewGuid();
static AssetQueryTests()
{
DomainIdSerializer.Register();
TypeConverterStringSerializer<RefToken>.Register();
TypeConverterStringSerializer<Status>.Register();
InstantSerializer.Register();
}
[Fact]
public void Should_throw_exception_for_full_text_search()
{
Assert.Throws<ValidationException>(() => 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<Guid> { 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<string> { 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<ClrValue> 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<MongoAssetEntity>(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<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
var rendered = string.Empty;
A.CallTo(() => cursor.Sort(A<SortDefinition<MongoAssetEntity>>._))
.Invokes((SortDefinition<MongoAssetEntity> 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());
}
}
}

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs → 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 namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
{ {
[Trait("Category", "Dependencies")] [Trait("Category", "Dependencies")]
public class AssetsQueryTests : IClassFixture<AssetsQueryFixture> public class AssetsQueryIntegrationTests : IClassFixture<AssetsQueryFixture>
{ {
public AssetsQueryFixture _ { get; } public AssetsQueryFixture _ { get; }
public AssetsQueryTests(AssetsQueryFixture fixture) public AssetsQueryIntegrationTests(AssetsQueryFixture fixture)
{ {
_ = fixture; _ = fixture;
} }

239
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs

@ -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<MongoAssetEntity> Serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoAssetEntity>();
static MongoDbQueryTests()
{
DomainIdSerializer.Register();
TypeConverterStringSerializer<RefToken>.Register();
TypeConverterStringSerializer<Status>.Register();
InstantSerializer.Register();
}
[Fact]
public void Should_throw_exception_for_full_text_search()
{
Assert.Throws<ValidationException>(() => _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<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
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<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
cursor.QuerySkip(query.AdjustToModel());
A.CallTo(() => cursor.Skip(3))
.MustHaveHappened();
}
private static string _C(string value)
{
return value.Replace('\'', '"');
}
private static string _F(FilterNode<ClrValue> filter)
{
return _Q(new ClrQuery { Filter = filter });
}
private static string _S(params SortNode[] sorts)
{
var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
var i = string.Empty;
A.CallTo(() => cursor.Sort(A<SortDefinition<MongoAssetEntity>>._))
.Invokes((SortDefinition<MongoAssetEntity> 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<MongoAssetEntity>(false).Filter!;
var rendered = filter.Render(Serializer, Registry).ToString();
return rendered;
}
}
}

203
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs → backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentQueryTests.cs

@ -28,12 +28,9 @@ using Xunit;
using ClrFilter = Squidex.Infrastructure.Queries.ClrFilter; using ClrFilter = Squidex.Infrastructure.Queries.ClrFilter;
using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder; 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 namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{ {
public class MongoDbQueryTests public class ContentQueryTests
{ {
private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry; private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry;
private static readonly IBsonSerializer<MongoContentEntity> Serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>(); private static readonly IBsonSerializer<MongoContentEntity> Serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>();
@ -41,7 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
private readonly Schema schemaDef; private readonly Schema schemaDef;
private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE); private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE);
static MongoDbQueryTests() static ContentQueryTests()
{ {
DomainIdSerializer.Register(); DomainIdSerializer.Register();
@ -51,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
InstantSerializer.Register(); InstantSerializer.Register();
} }
public MongoDbQueryTests() public ContentQueryTests()
{ {
schemaDef = schemaDef =
new Schema("user") new Schema("user")
@ -86,21 +83,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
A.CallTo(() => app.Languages).Returns(languagesConfig); A.CallTo(() => app.Languages).Returns(languagesConfig);
} }
[Fact]
public void Should_not_throw_exception_for_invalid_field()
{
_F(ClrFilter.Eq("data/invalid/iv", "Me"));
}
[Fact] [Fact]
public void Should_make_query_with_id() public void Should_make_query_with_id()
{ {
var id = Guid.NewGuid(); var id = Guid.NewGuid();
var i = _F(ClrFilter.Eq("id", id)); var filter = ClrFilter.Eq("id", id);
var o = _C($"{{ '_id' : '{appId}--{id}' }}");
Assert.Equal(o, i); AssertQuery($"{{ '_id' : '{appId}--{id}' }}", filter);
} }
[Fact] [Fact]
@ -108,10 +98,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{ {
var id = DomainId.NewGuid().ToString(); var id = DomainId.NewGuid().ToString();
var i = _F(ClrFilter.Eq("id", id)); var filter = ClrFilter.Eq("id", id);
var o = _C($"{{ '_id' : '{appId}--{id}' }}");
Assert.Equal(o, i); AssertQuery($"{{ '_id' : '{appId}--{id}' }}", filter);
} }
[Fact] [Fact]
@ -119,10 +108,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{ {
var id = Guid.NewGuid(); var id = Guid.NewGuid();
var i = _F(ClrFilter.In("id", new List<Guid> { id })); var filter = ClrFilter.In("id", new List<Guid> { id });
var o = _C($"{{ '_id' : {{ '$in' : ['{appId}--{id}'] }} }}");
Assert.Equal(o, i); AssertQuery($"{{ '_id' : {{ '$in' : ['{appId}--{id}'] }} }}", filter);
} }
[Fact] [Fact]
@ -130,225 +118,152 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{ {
var id = DomainId.NewGuid().ToString(); var id = DomainId.NewGuid().ToString();
var i = _F(ClrFilter.In("id", new List<string> { id })); var filter = ClrFilter.In("id", new List<string> { id });
var o = _C($"{{ '_id' : {{ '$in' : ['{appId}--{id}'] }} }}");
Assert.Equal(o, i); AssertQuery($"{{ '_id' : {{ '$in' : ['{appId}--{id}'] }} }}", filter);
} }
[Fact] [Fact]
public void Should_make_query_with_lastModified() public void Should_make_query_with_lastModified()
{ {
var i = _F(ClrFilter.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); var time = "1988-01-19T12:00:00Z";
var o = _C("{ 'mt' : ISODate('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] [Fact]
public void Should_make_query_with_lastModifiedBy() public void Should_make_query_with_lastModifiedBy()
{ {
var i = _F(ClrFilter.Eq("lastModifiedBy", "me")); var filter = ClrFilter.Eq("lastModifiedBy", "me");
var o = _C("{ 'mb' : 'me' }");
Assert.Equal(o, i); AssertQuery("{ 'mb' : 'me' }", filter);
} }
[Fact] [Fact]
public void Should_make_query_with_created() public void Should_make_query_with_created()
{ {
var i = _F(ClrFilter.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); var time = "1988-01-19T12:00:00Z";
var o = _C("{ 'ct' : ISODate('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] [Fact]
public void Should_make_query_with_createdBy() public void Should_make_query_with_createdBy()
{ {
var i = _F(ClrFilter.Eq("createdBy", "user:me")); var filter = ClrFilter.Eq("createdBy", "subject:me");
var o = _C("{ 'cb' : 'user:me' }");
Assert.Equal(o, i); AssertQuery("{ 'cb' : 'subject:me' }", filter);
} }
[Fact] [Fact]
public void Should_make_query_with_version() public void Should_make_query_with_version()
{ {
var i = _F(ClrFilter.Eq("version", 0L)); var filter = ClrFilter.Eq("version", 2L);
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<long> { 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' : [] }] }");
Assert.Equal(o, i); AssertQuery("{ 'vs' : NumberLong(2) }", filter);
}
[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);
} }
[Fact] [Fact]
public void Should_make_query_with_datetime_data() 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 time = "1988-01-19T12:00:00Z";
var o = _C("{ 'do.birthday.iv' : '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] [Fact]
public void Should_make_query_with_underscore_field() public void Should_make_query_with_underscore_field()
{ {
var i = _F(ClrFilter.Eq("data/dashed_field/iv", "Value")); var filter = ClrFilter.Eq("data/dashed_field/iv", "Value");
var o = _C("{ 'do.dashed-field.iv' : 'Value' }");
Assert.Equal(o, i); AssertQuery("{ 'do.dashed-field.iv' : 'Value' }", filter);
} }
[Fact] [Fact]
public void Should_make_query_with_references_equals() public void Should_make_query_with_references_equals()
{ {
var i = _F(ClrFilter.Eq("data/friends/iv", "guid")); var filter = ClrFilter.Eq("data/friends/iv", "guid");
var o = _C("{ 'do.friends.iv' : 'guid' }");
Assert.Equal(o, i); AssertQuery("{ 'do.friends.iv' : 'guid' }", filter);
} }
[Fact] [Fact]
public void Should_make_query_with_array_field() public void Should_make_query_with_array_field()
{ {
var i = _F(ClrFilter.Eq("data/hobbies/iv/name", "PC")); var filter = ClrFilter.Eq("data/hobbies/iv/name", "PC");
var o = _C("{ 'do.hobbies.iv.name' : 'PC' }");
Assert.Equal(o, i); AssertQuery("{ 'do.hobbies.iv.name' : 'PC' }", filter);
} }
[Fact] [Fact]
public void Should_make_query_with_assets_equals() public void Should_make_query_with_assets_equals()
{ {
var i = _F(ClrFilter.Eq("data/pictures/iv", "guid")); var filter = ClrFilter.Eq("data/pictures/iv", "guid");
var o = _C("{ 'do.pictures.iv' : 'guid' }");
Assert.Equal(o, i); AssertQuery("{ 'do.pictures.iv' : 'guid' }", filter);
}
[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);
} }
[Fact] [Fact]
public void Should_make_orderby_with_single_field() public void Should_make_orderby_with_single_field()
{ {
var i = _S(SortBuilder.Descending("data/age/iv")); var sorting = SortBuilder.Descending("data/age/iv");
var o = _C("{ 'do.age.iv' : -1 }");
Assert.Equal(o, i); AssertSorting("{ 'do.age.iv' : -1 }", sorting);
} }
[Fact] [Fact]
public void Should_make_orderby_with_multiple_fields() public void Should_make_orderby_with_multiple_fields()
{ {
var i = _S(SortBuilder.Ascending("data/age/iv"), SortBuilder.Descending("data/firstName/en")); var sorting1 = SortBuilder.Ascending("data/age/iv");
var o = _C("{ 'do.age.iv' : 1, 'do.firstName.en' : -1 }"); var sorting2 = SortBuilder.Descending("data/firstName/en");
Assert.Equal(o, i); AssertSorting("{ 'do.age.iv' : 1, 'do.firstName.en' : -1 }", sorting1, sorting2);
} }
[Fact] private void AssertQuery(string expected, FilterNode<ClrValue> filter, object? arg = null)
public void Should_make_take_statement()
{ {
var query = new ClrQuery { Take = 3 }; AssertQuery(new ClrQuery { Filter = filter }, expected, arg);
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
cursor.QueryLimit(query);
A.CallTo(() => cursor.Limit(3))
.MustHaveHappened();
} }
[Fact] private void AssertQuery(ClrQuery query, string expected, object? arg = null)
public void Should_make_skip_statement()
{ {
var query = new ClrQuery { Skip = 3 }; var rendered =
query.AdjustToModel(appId).BuildFilter<MongoContentEntity>().Filter!
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); .Render(Serializer, Registry).ToString();
cursor.QuerySkip(query);
A.CallTo(() => cursor.Skip(3)) var expectation = Cleanup(expected, arg);
.MustHaveHappened();
}
private static string _C(string value) Assert.Equal(expectation, rendered);
{
return value.Replace('\'', '"');
} }
private string _F(FilterNode<ClrValue> filter) private void AssertSorting(string expected, params SortNode[] sort)
{
return _Q(new ClrQuery { Filter = filter });
}
private string _S(params SortNode[] sorts)
{ {
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
var i = string.Empty; var rendered = string.Empty;
A.CallTo(() => cursor.Sort(A<SortDefinition<MongoContentEntity>>._)) A.CallTo(() => cursor.Sort(A<SortDefinition<MongoContentEntity>>._))
.Invokes((SortDefinition<MongoContentEntity> sortDefinition) => .Invokes((SortDefinition<MongoContentEntity> 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 = return filter.Replace('\'', '"').Replace("[value]", arg?.ToString());
query.AdjustToModel(appId).BuildFilter<MongoContentEntity>().Filter!
.Render(Serializer, Registry).ToString();
return rendered;
} }
} }
} }

2
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 int numValues = 10000;
private readonly IMongoClient mongoClient = new MongoClient("mongodb://localhost"); private readonly IMongoClient mongoClient = new MongoClient("mongodb://localhost");
private readonly IMongoDatabase mongoDatabase; private readonly IMongoDatabase mongoDatabase;
private readonly IMongoDatabase mongoDatabaseWildcard;
public MongoContentRepository ContentRepository { get; } public MongoContentRepository ContentRepository { get; }
@ -55,7 +54,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
public ContentsQueryFixture() public ContentsQueryFixture()
{ {
mongoDatabase = mongoClient.GetDatabase("QueryTests"); mongoDatabase = mongoClient.GetDatabase("QueryTests");
mongoDatabaseWildcard = mongoClient.GetDatabase("QueryTestsWildcard");
SetupJson(); SetupJson();

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTests.cs → 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 namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{ {
[Trait("Category", "Dependencies")] [Trait("Category", "Dependencies")]
public class ContentsQueryTests : IClassFixture<ContentsQueryFixture> public class ContentsQueryIntegrationTests : IClassFixture<ContentsQueryFixture>
{ {
public ContentsQueryFixture _ { get; } public ContentsQueryFixture _ { get; }
public ContentsQueryTests(ContentsQueryFixture fixture) public ContentsQueryIntegrationTests(ContentsQueryFixture fixture)
{ {
_ = fixture; _ = fixture;
} }

336
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<TestEntity> serializer = BsonSerializer.SerializerRegistry.GetSerializer<TestEntity>();
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<RefToken>.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<Guid> { 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<string> { 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<long> { 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<IFindFluent<TestEntity, TestEntity>>();
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<IFindFluent<TestEntity, TestEntity>>();
cursor.QuerySkip(query);
A.CallTo(() => cursor.Skip(3))
.MustHaveHappened();
}
private void AssertQuery(string expected, FilterNode<ClrValue> 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<TestEntity>().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<IFindFluent<TestEntity, TestEntity>>();
var rendered = string.Empty;
A.CallTo(() => cursor.Sort(A<SortDefinition<TestEntity>>._))
.Invokes((SortDefinition<TestEntity> 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());
}
}
}

9
backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs

@ -336,6 +336,15 @@ namespace Squidex.Infrastructure.Queries
Assert.Equal(o, i); 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] [Fact]
public void Should_parse_filter_with_empty() public void Should_parse_filter_with_empty()
{ {

21
backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs

@ -337,16 +337,31 @@ namespace TestSuite.ApiTests
public async Task Should_delete_asset(bool permanent) public async Task Should_delete_asset(bool permanent)
{ {
// STEP 1: Create asset // 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 // 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. // Should return 404 when asset deleted.
var ex = await Assert.ThrowsAsync<SquidexManagementException>(() => _.Assets.GetAssetAsync(_.AppName, asset_1.Id)); var ex = await Assert.ThrowsAsync<SquidexManagementException>(() => _.Assets.GetAssetAsync(_.AppName, asset.Id));
Assert.Equal(404, ex.StatusCode); 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] [Theory]

10
backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs

@ -7,6 +7,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.ClientLibrary; using Squidex.ClientLibrary;
using TestSuite.Fixtures; using TestSuite.Fixtures;
@ -579,6 +580,15 @@ namespace TestSuite.ApiTests
var updated = await _.Contents.GetAsync(); var updated = await _.Contents.GetAsync();
Assert.DoesNotContain(updated.Items, x => x.Id == content.Id); 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] [Theory]

1
frontend/app/shared/state/query.ts

@ -225,6 +225,7 @@ const CompareOperator: ReadonlyArray<FilterOperator> = [
]; ];
const StringOperators: ReadonlyArray<FilterOperator> = [ const StringOperators: ReadonlyArray<FilterOperator> = [
{ name: 'i18n:common.queryOperators.matchs', value: 'matchs' },
{ name: 'i18n:common.queryOperators.startsWith', value: 'startsWith' }, { name: 'i18n:common.queryOperators.startsWith', value: 'startsWith' },
{ name: 'i18n:common.queryOperators.endsWith', value: 'endsWith' }, { name: 'i18n:common.queryOperators.endsWith', value: 'endsWith' },
{ name: 'i18n:common.queryOperators.contains', value: 'contains' } { name: 'i18n:common.queryOperators.contains', value: 'contains' }

Loading…
Cancel
Save