Browse Source

Async fixes. (#537)

* Async fixes.

* More things.

* More fixes.
pull/538/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
86a5f999ed
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  2. 7
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByQuery.cs
  3. 5
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs
  4. 5
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs
  5. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs
  6. 8
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs
  7. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs
  8. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
  9. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs
  10. 8
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/FilterTagTransformer.cs
  11. 10
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs
  12. 20
      backend/src/Squidex.Domain.Users.MongoDb/MongoXmlEntity.cs
  13. 51
      backend/src/Squidex.Domain.Users.MongoDb/MongoXmlRepository.cs
  14. 53
      backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs
  15. 2
      backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs
  16. 3
      backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs
  17. 49
      backend/src/Squidex.Infrastructure/Queries/AsyncTransformVisitor.cs
  18. 13
      backend/src/Squidex.Infrastructure/Queries/Optimizer.cs
  19. 16
      backend/src/Squidex.Infrastructure/Queries/TransformVisitor.cs
  20. 2
      backend/src/Squidex.Infrastructure/StringExtensions.cs
  21. 54
      backend/src/Squidex.Infrastructure/Tasks/AsyncHelper.cs
  22. 12
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs
  23. 18
      backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs
  24. 5
      backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs
  25. 7
      backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs
  26. 16
      backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs
  27. 5
      backend/src/Squidex/Config/Authentication/AuthenticationServices.cs
  28. 4
      backend/src/Squidex/Config/Domain/StoreServices.cs
  29. 42
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs
  30. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs
  31. 13
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/FilterTagTransformerTests.cs
  32. 91
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs
  33. 47
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs
  34. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs
  35. 10
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/FilterTagTransformerTests.cs
  36. 57
      backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs
  37. 171
      backend/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs

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

@ -20,6 +20,7 @@ using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.MongoDb.Queries; using Squidex.Infrastructure.MongoDb.Queries;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{ {
@ -84,9 +85,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
.QuerySort(query) .QuerySort(query)
.ToListAsync(); .ToListAsync();
await Task.WhenAll(assetItems, assetCount); var (items, total) = await AsyncHelper.WhenAll(assetItems, assetCount);
return ResultList.Create<IAssetEntity>(assetCount.Result, assetItems.Result); return ResultList.Create<IAssetEntity>(total, items);
} }
catch (MongoQueryException ex) when (ex.Message.Contains("17406")) catch (MongoQueryException ex) when (ex.Message.Contains("17406"))
{ {

7
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByQuery.cs

@ -20,6 +20,7 @@ using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb.Queries; using Squidex.Infrastructure.MongoDb.Queries;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{ {
@ -87,14 +88,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
var contentCount = Collection.Find(filter).CountDocumentsAsync(); var contentCount = Collection.Find(filter).CountDocumentsAsync();
var contentItems = FindContentsAsync(query, filter); var contentItems = FindContentsAsync(query, filter);
await Task.WhenAll(contentItems, contentCount); var (items, total) = await AsyncHelper.WhenAll(contentItems, contentCount);
foreach (var entity in contentItems.Result) foreach (var entity in items)
{ {
entity.ParseData(schema.SchemaDef, converter); entity.ParseData(schema.SchemaDef, converter);
} }
return ResultList.Create<IContentEntity>(contentCount.Result, contentItems.Result); return ResultList.Create<IContentEntity>(total, items);
} }
catch (MongoCommandException ex) when (ex.Code == 96) catch (MongoCommandException ex) when (ex.Code == 96)
{ {

5
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs

@ -18,6 +18,7 @@ using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.MongoDb.Rules namespace Squidex.Domain.Apps.Entities.MongoDb.Rules
{ {
@ -71,9 +72,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules
var taskForItems = Collection.Find(filter).Skip(skip).Limit(take).SortByDescending(x => x.Created).ToListAsync(); var taskForItems = Collection.Find(filter).Skip(skip).Limit(take).SortByDescending(x => x.Created).ToListAsync();
var taskForCount = Collection.Find(filter).CountDocumentsAsync(); var taskForCount = Collection.Find(filter).CountDocumentsAsync();
await Task.WhenAll(taskForItems, taskForCount); var (items, total) = await AsyncHelper.WhenAll(taskForItems, taskForCount);
return ResultList.Create(taskForCount.Result, taskForItems.Result); return ResultList.Create(total, items);
} }
public async Task<IRuleEventEntity> FindAsync(Guid id) public async Task<IRuleEventEntity> FindAsync(Guid id)

5
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs

@ -7,6 +7,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.OData; using Microsoft.OData;
using Microsoft.OData.Edm; using Microsoft.OData.Edm;
@ -41,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
this.tagService = tagService; this.tagService = tagService;
} }
public virtual ClrQuery ParseQuery(Context context, Q q) public virtual async ValueTask<ClrQuery> ParseQueryAsync(Context context, Q q)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
@ -71,7 +72,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
if (result.Filter != null) if (result.Filter != null)
{ {
result.Filter = FilterTagTransformer.Transform(result.Filter, context.App.Id, tagService); result.Filter = await FilterTagTransformer.TransformAsync(result.Filter, context.App.Id, tagService);
} }
if (result.Sort.Count == 0) if (result.Sort.Count == 0)

2
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs

@ -111,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
private async Task<IResultList<IAssetEntity>> QueryByQueryAsync(Context context, Guid? parentId, Q query) private async Task<IResultList<IAssetEntity>> QueryByQueryAsync(Context context, Guid? parentId, Q query)
{ {
var parsedQuery = queryParser.ParseQuery(context, query); var parsedQuery = await queryParser.ParseQueryAsync(context, query);
return await assetRepository.QueryAsync(context.App.Id, parentId, parsedQuery); return await assetRepository.QueryAsync(context.App.Id, parentId, parsedQuery);
} }

8
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs

@ -13,7 +13,7 @@ using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.Assets.Queries namespace Squidex.Domain.Apps.Entities.Assets.Queries
{ {
public sealed class FilterTagTransformer : TransformVisitor<ClrValue> public sealed class FilterTagTransformer : AsyncTransformVisitor<ClrValue>
{ {
private readonly ITagService tagService; private readonly ITagService tagService;
private readonly Guid appId; private readonly Guid appId;
@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
this.tagService = tagService; this.tagService = tagService;
} }
public static FilterNode<ClrValue>? Transform(FilterNode<ClrValue> nodeIn, Guid appId, ITagService tagService) public static ValueTask<FilterNode<ClrValue>?> TransformAsync(FilterNode<ClrValue> nodeIn, Guid appId, ITagService tagService)
{ {
Guard.NotNull(nodeIn, nameof(nodeIn)); Guard.NotNull(nodeIn, nameof(nodeIn));
Guard.NotNull(tagService, nameof(tagService)); Guard.NotNull(tagService, nameof(tagService));
@ -33,11 +33,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
return nodeIn.Accept(new FilterTagTransformer(appId, tagService)); return nodeIn.Accept(new FilterTagTransformer(appId, tagService));
} }
public override FilterNode<ClrValue>? Visit(CompareFilter<ClrValue> nodeIn) public override async ValueTask<FilterNode<ClrValue>?> Visit(CompareFilter<ClrValue> nodeIn)
{ {
if (string.Equals(nodeIn.Path[0], nameof(IAssetEntity.Tags), StringComparison.OrdinalIgnoreCase) && nodeIn.Value.Value is string stringValue) if (string.Equals(nodeIn.Path[0], nameof(IAssetEntity.Tags), StringComparison.OrdinalIgnoreCase) && nodeIn.Value.Value is string stringValue)
{ {
var tagNames = Task.Run(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, HashSet.Of(stringValue))).Result; var tagNames = await tagService.GetTagIdsAsync(appId, TagGroups.Assets, HashSet.Of(stringValue));
if (tagNames.TryGetValue(stringValue, out var normalized)) if (tagNames.TryGetValue(stringValue, out var normalized))
{ {

5
backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Orleans; using Orleans;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents.Counter namespace Squidex.Domain.Apps.Entities.Contents.Counter
{ {
@ -46,14 +47,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter
{ {
var grain = grainFactory.GetGrain<ICounterGrain>(appId); var grain = grainFactory.GetGrain<ICounterGrain>(appId);
return Task.Run(() => grain.IncrementAsync(name)).Result; return AsyncHelper.Sync(() => grain.IncrementAsync(name));
} }
private long Reset(Guid appId, string name, long value) private long Reset(Guid appId, string name, long value)
{ {
var grain = grainFactory.GetGrain<ICounterGrain>(appId); var grain = grainFactory.GetGrain<ICounterGrain>(appId);
return Task.Run(() => grain.ResetAsync(name, value)).Result; return AsyncHelper.Sync(() => grain.ResetAsync(name, value));
} }
} }
} }

5
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs

@ -8,6 +8,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.OData; using Microsoft.OData;
@ -43,7 +44,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
this.options = options.Value; this.options = options.Value;
} }
public virtual ClrQuery ParseQuery(Context context, ISchemaEntity schema, Q q) public virtual ValueTask<ClrQuery> ParseQueryAsync(Context context, ISchemaEntity schema, Q q)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
Guard.NotNull(schema, nameof(schema)); Guard.NotNull(schema, nameof(schema));
@ -90,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
result.Take = options.MaxResults; result.Take = options.MaxResults;
} }
return result; return new ValueTask<ClrQuery>(result);
} }
} }

2
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs

@ -208,7 +208,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
private async Task<IResultList<IContentEntity>> QueryByQueryAsync(Context context, ISchemaEntity schema, Q query) private async Task<IResultList<IContentEntity>> QueryByQueryAsync(Context context, ISchemaEntity schema, Q query)
{ {
var parsedQuery = queryParser.ParseQuery(context, schema, query); var parsedQuery = await queryParser.ParseQueryAsync(context, schema, query);
return await QueryCoreAsync(context, schema, parsedQuery); return await QueryCoreAsync(context, schema, parsedQuery);
} }

8
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/FilterTagTransformer.cs

@ -16,7 +16,7 @@ using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.Contents.Queries namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
public sealed class FilterTagTransformer : TransformVisitor<ClrValue> public sealed class FilterTagTransformer : AsyncTransformVisitor<ClrValue>
{ {
private readonly ITagService tagService; private readonly ITagService tagService;
private readonly ISchemaEntity schema; private readonly ISchemaEntity schema;
@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
this.tagService = tagService; this.tagService = tagService;
} }
public static FilterNode<ClrValue>? Transform(FilterNode<ClrValue> nodeIn, Guid appId, ISchemaEntity schema, ITagService tagService) public static ValueTask<FilterNode<ClrValue>?> TransformAsync(FilterNode<ClrValue> nodeIn, Guid appId, ISchemaEntity schema, ITagService tagService)
{ {
Guard.NotNull(nodeIn, nameof(nodeIn)); Guard.NotNull(nodeIn, nameof(nodeIn));
Guard.NotNull(tagService, nameof(tagService)); Guard.NotNull(tagService, nameof(tagService));
@ -38,11 +38,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
return nodeIn.Accept(new FilterTagTransformer(appId, schema, tagService)); return nodeIn.Accept(new FilterTagTransformer(appId, schema, tagService));
} }
public override FilterNode<ClrValue>? Visit(CompareFilter<ClrValue> nodeIn) public override async ValueTask<FilterNode<ClrValue>?> Visit(CompareFilter<ClrValue> nodeIn)
{ {
if (nodeIn.Value.Value is string stringValue && IsDataPath(nodeIn.Path) && IsTagField(nodeIn.Path)) if (nodeIn.Value.Value is string stringValue && IsDataPath(nodeIn.Path) && IsTagField(nodeIn.Path))
{ {
var tagNames = Task.Run(() => tagService.GetTagIdsAsync(appId, TagGroups.Schemas(schema.Id), HashSet.Of(stringValue))).Result; var tagNames = await tagService.GetTagIdsAsync(appId, TagGroups.Schemas(schema.Id), HashSet.Of(stringValue));
if (tagNames.TryGetValue(stringValue, out var normalized)) if (tagNames.TryGetValue(stringValue, out var normalized))
{ {

10
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs

@ -15,6 +15,7 @@ using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{ {
@ -72,12 +73,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
if (ids.Count > 0) if (ids.Count > 0)
{ {
var taskForAssets = QueryAssetIdsAsync(context, ids); var (assets, refContents) = await AsyncHelper.WhenAll(
var taskForContents = QueryContentIdsAsync(context, ids); QueryAssetIdsAsync(context, ids),
QueryContentIdsAsync(context, ids));
await Task.WhenAll(taskForAssets, taskForContents); var foundIds = new HashSet<Guid>(assets.Union(refContents));
var foundIds = new HashSet<Guid>(taskForAssets.Result.Union(taskForContents.Result));
return ValueReferencesConverter.CleanReferences(foundIds); return ValueReferencesConverter.CleanReferences(foundIds);
} }

20
backend/src/Squidex.Domain.Users.MongoDb/MongoXmlEntity.cs

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using MongoDB.Bson.Serialization.Attributes;
namespace Squidex.Domain.Users.MongoDb
{
public sealed class MongoXmlEntity
{
[BsonId]
public string FriendlyName { get; set; }
[BsonRequired]
public string Xml { get; set; }
}
}

51
backend/src/Squidex.Domain.Users.MongoDb/MongoXmlRepository.cs

@ -0,0 +1,51 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Microsoft.AspNetCore.DataProtection.Repositories;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Infrastructure;
namespace Squidex.Domain.Users.MongoDb
{
public sealed class MongoXmlRepository : IXmlRepository
{
private static readonly ReplaceOptions UpsertReplace = new ReplaceOptions { IsUpsert = true };
private readonly IMongoCollection<MongoXmlEntity> collection;
public MongoXmlRepository(IMongoDatabase mongoDatabase)
{
Guard.NotNull(mongoDatabase, nameof(mongoDatabase));
collection = mongoDatabase.GetCollection<MongoXmlEntity>("States_Repository");
}
public IReadOnlyCollection<XElement> GetAllElements()
{
var documents = collection.Find(new BsonDocument()).ToList();
var elements = documents.Select(x => XElement.Parse(x.Xml)).ToList();
return elements;
}
public void StoreElement(XElement element, string friendlyName)
{
var document = new MongoXmlEntity
{
FriendlyName = friendlyName
};
document.Xml = element.ToString();
collection.ReplaceOne(x => x.FriendlyName == friendlyName, document, UpsertReplace);
}
}
}

53
backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs

@ -1,53 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.AspNetCore.DataProtection.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Users
{
public sealed class DefaultXmlRepository : IXmlRepository
{
private readonly ISnapshotStore<State, string> store;
[CollectionName("XmlRepository")]
public sealed class State
{
public string Xml { get; set; }
}
public DefaultXmlRepository(ISnapshotStore<State, string> store)
{
Guard.NotNull(store, nameof(store));
this.store = store;
}
public IReadOnlyCollection<XElement> GetAllElements()
{
var result = new List<XElement>();
store.ReadAllAsync((state, version) =>
{
result.Add(XElement.Parse(state.Xml));
return Task.CompletedTask;
}).Wait();
return result;
}
public void StoreElement(XElement element, string friendlyName)
{
store.WriteAsync(friendlyName, new State { Xml = element.ToString() }, EtagVersion.Any, EtagVersion.Any).Wait();
}
}
}

2
backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs

@ -38,7 +38,7 @@ namespace Squidex.Infrastructure.EventSourcing
if (fromBeginning) if (fromBeginning)
{ {
hostName = $"squidex.{DateTime.UtcNow.Ticks.ToString()}"; hostName = $"squidex.{DateTime.UtcNow.Ticks}";
} }
else else
{ {

3
backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using EventStore.ClientAPI; using EventStore.ClientAPI;
using EventStore.ClientAPI.Exceptions; using EventStore.ClientAPI.Exceptions;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.EventSourcing namespace Squidex.Infrastructure.EventSourcing
{ {
@ -35,7 +36,7 @@ namespace Squidex.Infrastructure.EventSourcing
this.position = projectionClient.ParsePositionOrNull(position); this.position = projectionClient.ParsePositionOrNull(position);
this.prefix = prefix; this.prefix = prefix;
var streamName = projectionClient.CreateProjectionAsync(streamFilter).Result; var streamName = AsyncHelper.Sync(() => projectionClient.CreateProjectionAsync(streamFilter));
this.serializer = serializer; this.serializer = serializer;
this.subscriber = subscriber; this.subscriber = subscriber;

49
backend/src/Squidex.Infrastructure/Queries/AsyncTransformVisitor.cs

@ -0,0 +1,49 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.Queries
{
public abstract class AsyncTransformVisitor<TValue> : FilterNodeVisitor<ValueTask<FilterNode<TValue>?>, TValue>
{
public override ValueTask<FilterNode<TValue>?> Visit(CompareFilter<TValue> nodeIn)
{
return new ValueTask<FilterNode<TValue>?>(nodeIn);
}
public override async ValueTask<FilterNode<TValue>?> Visit(LogicalFilter<TValue> nodeIn)
{
var pruned = new List<FilterNode<TValue>>(nodeIn.Filters.Count);
foreach (var inner in nodeIn.Filters)
{
var transformed = await inner.Accept(this);
if (transformed != null)
{
pruned.Add(transformed);
}
}
return new LogicalFilter<TValue>(nodeIn.Type, pruned);
}
public override async ValueTask<FilterNode<TValue>?> Visit(NegateFilter<TValue> nodeIn)
{
var inner = await nodeIn.Filter.Accept(this);
if (inner == null)
{
return inner;
}
return new NegateFilter<TValue>(inner);
}
}
}

13
backend/src/Squidex.Infrastructure/Queries/Optimizer.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace Squidex.Infrastructure.Queries namespace Squidex.Infrastructure.Queries
@ -24,7 +25,17 @@ namespace Squidex.Infrastructure.Queries
public override FilterNode<TValue>? Visit(LogicalFilter<TValue> nodeIn) public override FilterNode<TValue>? Visit(LogicalFilter<TValue> nodeIn)
{ {
var pruned = nodeIn.Filters.Select(x => x.Accept(this)!).NotNull().ToList(); var pruned = new List<FilterNode<TValue>>(nodeIn.Filters.Count);
foreach (var filter in nodeIn.Filters)
{
var transformed = filter.Accept(this);
if (transformed != null)
{
pruned.Add(transformed);
}
}
if (pruned.Count == 1) if (pruned.Count == 1)
{ {

16
backend/src/Squidex.Infrastructure/Queries/TransformVisitor.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Linq; using System.Collections.Generic;
namespace Squidex.Infrastructure.Queries namespace Squidex.Infrastructure.Queries
{ {
@ -18,9 +18,19 @@ namespace Squidex.Infrastructure.Queries
public override FilterNode<TValue>? Visit(LogicalFilter<TValue> nodeIn) public override FilterNode<TValue>? Visit(LogicalFilter<TValue> nodeIn)
{ {
var inner = nodeIn.Filters.Select(x => x.Accept(this)!).NotNull().ToList(); var pruned = new List<FilterNode<TValue>>(nodeIn.Filters.Count);
return new LogicalFilter<TValue>(nodeIn.Type, inner); foreach (var inner in nodeIn.Filters)
{
var transformed = inner.Accept(this);
if (transformed != null)
{
pruned.Add(transformed);
}
}
return new LogicalFilter<TValue>(nodeIn.Type, pruned);
} }
public override FilterNode<TValue>? Visit(NegateFilter<TValue> nodeIn) public override FilterNode<TValue>? Visit(NegateFilter<TValue> nodeIn)

2
backend/src/Squidex.Infrastructure/StringExtensions.cs

@ -19,7 +19,7 @@ namespace Squidex.Infrastructure
private const char NullChar = (char)0; private const char NullChar = (char)0;
private static readonly Regex SlugRegex = new Regex("^[a-z0-9]+(\\-[a-z0-9]+)*$", RegexOptions.Compiled); private static readonly Regex SlugRegex = new Regex("^[a-z0-9]+(\\-[a-z0-9]+)*$", RegexOptions.Compiled);
private static readonly Regex EmailRegex = new Regex("^[a-zA-Z0-9.!#$%&’*+\\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$", RegexOptions.Compiled); private static readonly Regex EmailRegex = new Regex(@"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-||_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+([a-z]+|\d|-|\.{0,1}|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$", RegexOptions.Compiled);
private static readonly Regex PropertyNameRegex = new Regex("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$", RegexOptions.Compiled); private static readonly Regex PropertyNameRegex = new Regex("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$", RegexOptions.Compiled);
private static readonly Dictionary<char, string> LowerCaseDiacritics; private static readonly Dictionary<char, string> LowerCaseDiacritics;

54
backend/src/Squidex.Infrastructure/Tasks/AsyncHelper.cs

@ -0,0 +1,54 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.Tasks
{
public static class AsyncHelper
{
private static readonly TaskFactory TaskFactory = new
TaskFactory(CancellationToken.None,
TaskCreationOptions.None,
TaskContinuationOptions.None,
TaskScheduler.Default);
public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
{
await Task.WhenAll(task1, task2);
return (task1.Result, task2.Result);
}
public static async Task<(T1, T2, T3)> WhenAll<T1, T2, T3>(Task<T1> task1, Task<T2> task2, Task<T3> task3)
{
await Task.WhenAll(task1, task2, task3);
return (task1.Result, task2.Result, task3.Result);
}
public static TResult Sync<TResult>(Func<Task<TResult>> func)
{
return TaskFactory
.StartNew(func)
.Unwrap()
.GetAwaiter()
.GetResult();
}
public static void Sync(Func<Task> func)
{
TaskFactory
.StartNew(func)
.Unwrap()
.GetAwaiter()
.GetResult();
}
}
}

12
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs

@ -13,6 +13,7 @@ using Squidex.Areas.Api.Controllers.Assets.Models;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Tasks;
using Squidex.Shared; using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
@ -51,17 +52,16 @@ namespace Squidex.Areas.Api.Controllers.Assets
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetAssetFolders(string app, [FromQuery] Guid parentId) public async Task<IActionResult> GetAssetFolders(string app, [FromQuery] Guid parentId)
{ {
var assetFolders = assetQuery.QueryAssetFoldersAsync(Context, parentId); var (folders, path) = await AsyncHelper.WhenAll(
var assetPath = assetQuery.FindAssetFolderAsync(parentId); assetQuery.QueryAssetFoldersAsync(Context, parentId),
assetQuery.FindAssetFolderAsync(parentId));
await Task.WhenAll(assetFolders, assetPath);
var response = Deferred.Response(() => var response = Deferred.Response(() =>
{ {
return AssetFoldersDto.FromAssets(assetFolders.Result, assetPath.Result, Resources); return AssetFoldersDto.FromAssets(folders, path, Resources);
}); });
Response.Headers[HeaderNames.ETag] = assetFolders.Result.ToEtag(); Response.Headers[HeaderNames.ETag] = folders.ToEtag();
return Ok(response); return Ok(response);
} }

18
backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Users.Models; using Squidex.Areas.Api.Controllers.Users.Models;
using Squidex.Domain.Users; using Squidex.Domain.Users;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Tasks;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
using Squidex.Shared; using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
@ -22,12 +23,14 @@ namespace Squidex.Areas.Api.Controllers.Users
{ {
private readonly UserManager<IdentityUser> userManager; private readonly UserManager<IdentityUser> userManager;
private readonly IUserFactory userFactory; private readonly IUserFactory userFactory;
private readonly IUserEvents userEvents;
public UserManagementController(ICommandBus commandBus, UserManager<IdentityUser> userManager, IUserFactory userFactory) public UserManagementController(ICommandBus commandBus, UserManager<IdentityUser> userManager, IUserFactory userFactory, IUserEvents userEvents)
: base(commandBus) : base(commandBus)
{ {
this.userManager = userManager; this.userManager = userManager;
this.userFactory = userFactory; this.userFactory = userFactory;
this.userEvents = userEvents;
} }
[HttpGet] [HttpGet]
@ -36,12 +39,11 @@ namespace Squidex.Areas.Api.Controllers.Users
[ApiPermission(Permissions.AdminUsersRead)] [ApiPermission(Permissions.AdminUsersRead)]
public async Task<IActionResult> GetUsers([FromQuery] string? query = null, [FromQuery] int skip = 0, [FromQuery] int take = 10) public async Task<IActionResult> GetUsers([FromQuery] string? query = null, [FromQuery] int skip = 0, [FromQuery] int take = 10)
{ {
var taskForItems = userManager.QueryByEmailAsync(query, take, skip); var (items, total) = await AsyncHelper.WhenAll(
var taskForCount = userManager.CountByEmailAsync(query); userManager.QueryByEmailAsync(query, take, skip),
userManager.CountByEmailAsync(query));
await Task.WhenAll(taskForItems, taskForCount); var response = UsersDto.FromResults(items, total, Resources);
var response = UsersDto.FromResults(taskForItems.Result, taskForCount.Result, Resources);
return Ok(response); return Ok(response);
} }
@ -72,6 +74,8 @@ namespace Squidex.Areas.Api.Controllers.Users
{ {
var user = await userManager.CreateAsync(userFactory, request.ToValues()); var user = await userManager.CreateAsync(userFactory, request.ToValues());
userEvents.OnUserRegistered(user);
var response = UserDto.FromUser(user, Resources); var response = UserDto.FromUser(user, Resources);
return Ok(response); return Ok(response);
@ -85,6 +89,8 @@ namespace Squidex.Areas.Api.Controllers.Users
{ {
var user = await userManager.UpdateAsync(id, request.ToValues()); var user = await userManager.UpdateAsync(id, request.ToValues());
userEvents.OnUserUpdated(user);
var response = UserDto.FromUser(user, Resources); var response = UserDto.FromUser(user, Resources);
return Ok(response); return Ok(response);

5
backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs

@ -17,6 +17,7 @@ using Squidex.Config;
using Squidex.Domain.Users; using Squidex.Domain.Users;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Infrastructure.Tasks;
using Squidex.Shared; using Squidex.Shared;
namespace Squidex.Areas.IdentityServer.Config namespace Squidex.Areas.IdentityServer.Config
@ -46,7 +47,7 @@ namespace Squidex.Areas.IdentityServer.Config
var adminEmail = options.AdminEmail; var adminEmail = options.AdminEmail;
var adminPass = options.AdminPassword; var adminPass = options.AdminPassword;
Task.Run(async () => AsyncHelper.Sync(async () =>
{ {
if (userManager.SupportsQueryableUsers && !userManager.Users.Any()) if (userManager.SupportsQueryableUsers && !userManager.Users.Any())
{ {
@ -69,7 +70,7 @@ namespace Squidex.Areas.IdentityServer.Config
.WriteProperty("status", "failed")); .WriteProperty("status", "failed"));
} }
} }
}).Wait(); });
} }
return services; return services;

7
backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs

@ -42,18 +42,21 @@ namespace Squidex.Areas.IdentityServer.Config
services.AddIdentity<IdentityUser, IdentityRole>() services.AddIdentity<IdentityUser, IdentityRole>()
.AddDefaultTokenProviders(); .AddDefaultTokenProviders();
services.AddSingleton<IPasswordValidator<IdentityUser>, services.AddSingleton<IPasswordValidator<IdentityUser>,
PwnedPasswordValidator>(); PwnedPasswordValidator>();
services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>,
UserClaimsPrincipalFactoryWithEmail>(); UserClaimsPrincipalFactoryWithEmail>();
services.AddSingleton<IClaimsTransformation, services.AddSingleton<IClaimsTransformation,
ApiPermissionUnifier>(); ApiPermissionUnifier>();
services.AddSingleton<IClientStore, services.AddSingleton<IClientStore,
LazyClientStore>(); LazyClientStore>();
services.AddSingleton<IResourceStore, services.AddSingleton<IResourceStore,
InMemoryResourcesStore>(); InMemoryResourcesStore>();
services.AddSingleton<IXmlRepository,
DefaultXmlRepository>();
services.AddIdentityServer(options => services.AddIdentityServer(options =>
{ {

16
backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs

@ -21,6 +21,7 @@ using Squidex.Domain.Users;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Tasks;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
using Squidex.Shared.Users; using Squidex.Shared.Users;
@ -232,11 +233,10 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
throw new DomainException("Cannot find user."); throw new DomainException("Cannot find user.");
} }
var taskForProviders = signInManager.GetExternalProvidersAsync(); var (providers, hasPassword, logins) = await AsyncHelper.WhenAll(
var taskForPassword = userManager.HasPasswordAsync(user.Identity); signInManager.GetExternalProvidersAsync(),
var taskForLogins = userManager.GetLoginsAsync(user.Identity); userManager.HasPasswordAsync(user.Identity),
userManager.GetLoginsAsync(user.Identity));
await Task.WhenAll(taskForProviders, taskForPassword, taskForLogins);
var result = new ProfileVM var result = new ProfileVM
{ {
@ -244,10 +244,10 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
ClientSecret = user.ClientSecret()!, ClientSecret = user.ClientSecret()!,
Email = user.Email, Email = user.Email,
ErrorMessage = errorMessage, ErrorMessage = errorMessage,
ExternalLogins = taskForLogins.Result, ExternalLogins = logins,
ExternalProviders = taskForProviders.Result, ExternalProviders = providers,
DisplayName = user.DisplayName()!, DisplayName = user.DisplayName()!,
HasPassword = taskForPassword.Result, HasPassword = hasPassword,
HasPasswordAuth = identityOptions.AllowPasswordAuth, HasPasswordAuth = identityOptions.AllowPasswordAuth,
IsHidden = user.IsHidden(), IsHidden = user.IsHidden(),
SuccessMessage = successMessage SuccessMessage = successMessage

5
backend/src/Squidex/Config/Authentication/AuthenticationServices.cs

@ -6,10 +6,8 @@
// ========================================================================== // ==========================================================================
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.DataProtection.Repositories;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Users;
namespace Squidex.Config.Authentication namespace Squidex.Config.Authentication
{ {
@ -19,9 +17,6 @@ namespace Squidex.Config.Authentication
{ {
var identityOptions = config.GetSection("identity").Get<MyIdentityOptions>(); var identityOptions = config.GetSection("identity").Get<MyIdentityOptions>();
services.AddSingletonAs<DefaultXmlRepository>()
.As<IXmlRepository>();
services.AddAuthentication() services.AddAuthentication()
.AddSquidexCookies() .AddSquidexCookies()
.AddSquidexExternalGithubAuthentication(identityOptions) .AddSquidexExternalGithubAuthentication(identityOptions)

4
backend/src/Squidex/Config/Domain/StoreServices.cs

@ -8,6 +8,7 @@
using System; using System;
using System.Linq; using System.Linq;
using IdentityServer4.Stores; using IdentityServer4.Stores;
using Microsoft.AspNetCore.DataProtection.Repositories;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -102,6 +103,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<MongoUserStore>() services.AddSingletonAs<MongoUserStore>()
.As<IUserStore<IdentityUser>>().As<IUserFactory>(); .As<IUserStore<IdentityUser>>().As<IUserFactory>();
services.AddSingletonAs<MongoXmlRepository>()
.As<IXmlRepository>();
services.AddSingletonAs<MongoKeyStore>() services.AddSingletonAs<MongoKeyStore>()
.As<ISigningCredentialStore>().As<IValidationKeysStore>(); .As<ISigningCredentialStore>().As<IValidationKeysStore>();

42
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs

@ -7,6 +7,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using Esprima.Ast;
using FakeItEasy; using FakeItEasy;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
@ -35,100 +37,100 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
} }
[Fact] [Fact]
public void Should_use_existing_query() public async Task Should_use_existing_query()
{ {
var clrQuery = new ClrQuery(); var clrQuery = new ClrQuery();
var parsed = sut.ParseQuery(requestContext, Q.Empty.WithQuery(clrQuery)); var parsed = await sut.ParseQueryAsync(requestContext, Q.Empty.WithQuery(clrQuery));
Assert.Same(parsed, clrQuery); Assert.Same(parsed, clrQuery);
} }
[Fact] [Fact]
public void Should_throw_if_odata_query_is_invalid() public async Task Should_throw_if_odata_query_is_invalid()
{ {
var query = Q.Empty.WithODataQuery("$filter=invalid"); var query = Q.Empty.WithODataQuery("$filter=invalid");
Assert.Throws<ValidationException>(() => sut.ParseQuery(requestContext, query)); await Assert.ThrowsAsync<ValidationException>(() => sut.ParseQueryAsync(requestContext, query).AsTask());
} }
[Fact] [Fact]
public void Should_throw_if_json_query_is_invalid() public async Task Should_throw_if_json_query_is_invalid()
{ {
var query = Q.Empty.WithJsonQuery("invalid"); var query = Q.Empty.WithJsonQuery("invalid");
Assert.Throws<ValidationException>(() => sut.ParseQuery(requestContext, query)); await Assert.ThrowsAsync<ValidationException>(() => sut.ParseQueryAsync(requestContext, query).AsTask());
} }
[Fact] [Fact]
public void Should_parse_odata_query() public async Task Should_parse_odata_query()
{ {
var query = Q.Empty.WithODataQuery("$top=100&$orderby=fileName asc&$search=Hello World"); var query = Q.Empty.WithODataQuery("$top=100&$orderby=fileName asc&$search=Hello World");
var parsed = sut.ParseQuery(requestContext, query); var parsed = await sut.ParseQueryAsync(requestContext, query);
Assert.Equal("FullText: 'Hello World'; Take: 100; Sort: fileName Ascending", parsed.ToString()); Assert.Equal("FullText: 'Hello World'; Take: 100; Sort: fileName Ascending", parsed.ToString());
} }
[Fact] [Fact]
public void Should_parse_odata_query_and_enrich_with_defaults() public async Task Should_parse_odata_query_and_enrich_with_defaults()
{ {
var query = Q.Empty.WithODataQuery("$top=200&$filter=fileName eq 'ABC'"); var query = Q.Empty.WithODataQuery("$top=200&$filter=fileName eq 'ABC'");
var parsed = sut.ParseQuery(requestContext, query); var parsed = await sut.ParseQueryAsync(requestContext, query);
Assert.Equal("Filter: fileName == 'ABC'; Take: 200; Sort: lastModified Descending", parsed.ToString()); Assert.Equal("Filter: fileName == 'ABC'; Take: 200; Sort: lastModified Descending", parsed.ToString());
} }
[Fact] [Fact]
public void Should_parse_json_query_and_enrich_with_defaults() public async Task Should_parse_json_query_and_enrich_with_defaults()
{ {
var query = Q.Empty.WithJsonQuery(Json("{ 'filter': { 'path': 'fileName', 'op': 'eq', 'value': 'ABC' } }")); var query = Q.Empty.WithJsonQuery(Json("{ 'filter': { 'path': 'fileName', 'op': 'eq', 'value': 'ABC' } }"));
var parsed = sut.ParseQuery(requestContext, query); var parsed = await sut.ParseQueryAsync(requestContext, query);
Assert.Equal("Filter: fileName == 'ABC'; Take: 30; Sort: lastModified Descending", parsed.ToString()); Assert.Equal("Filter: fileName == 'ABC'; Take: 30; Sort: lastModified Descending", parsed.ToString());
} }
[Fact] [Fact]
public void Should_parse_json_full_text_query_and_enrich_with_defaults() public async Task Should_parse_json_full_text_query_and_enrich_with_defaults()
{ {
var query = Q.Empty.WithJsonQuery(Json("{ 'fullText': 'Hello' }")); var query = Q.Empty.WithJsonQuery(Json("{ 'fullText': 'Hello' }"));
var parsed = sut.ParseQuery(requestContext, query); var parsed = await sut.ParseQueryAsync(requestContext, query);
Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending", parsed.ToString()); Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending", parsed.ToString());
} }
[Fact] [Fact]
public void Should_apply_default_page_size() public async Task Should_apply_default_page_size()
{ {
var query = Q.Empty; var query = Q.Empty;
var parsed = sut.ParseQuery(requestContext, query); var parsed = await sut.ParseQueryAsync(requestContext, query);
Assert.Equal("Take: 30; Sort: lastModified Descending", parsed.ToString()); Assert.Equal("Take: 30; Sort: lastModified Descending", parsed.ToString());
} }
[Fact] [Fact]
public void Should_limit_number_of_assets() public async Task Should_limit_number_of_assets()
{ {
var query = Q.Empty.WithODataQuery("$top=300&$skip=20"); var query = Q.Empty.WithODataQuery("$top=300&$skip=20");
var parsed = sut.ParseQuery(requestContext, query); var parsed = await sut.ParseQueryAsync(requestContext, query);
Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending", parsed.ToString()); Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending", parsed.ToString());
} }
[Fact] [Fact]
public void Should_denormalize_tags() public async Task Should_denormalize_tags()
{ {
A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.Contains("name1"))) A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.Contains("name1")))
.Returns(new Dictionary<string, string> { ["name1"] = "id1" }); .Returns(new Dictionary<string, string> { ["name1"] = "id1" });
var query = Q.Empty.WithODataQuery("$filter=tags eq 'name1'"); var query = Q.Empty.WithODataQuery("$filter=tags eq 'name1'");
var parsed = sut.ParseQuery(requestContext, query); var parsed = await sut.ParseQueryAsync(requestContext, query);
Assert.Equal("Filter: tags == 'id1'; Take: 30; Sort: lastModified Descending", parsed.ToString()); Assert.Equal("Filter: tags == 'id1'; Take: 30; Sort: lastModified Descending", parsed.ToString());
} }

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs

@ -32,7 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{ {
requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId)); requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId));
A.CallTo(() => queryParser.ParseQuery(requestContext, A<Q>._)) A.CallTo(() => queryParser.ParseQueryAsync(requestContext, A<Q>._))
.Returns(new ClrQuery()); .Returns(new ClrQuery());
sut = new AssetQueryService(assetEnricher, assetRepository, assetFolderRepository, queryParser); sut = new AssetQueryService(assetEnricher, assetRepository, assetFolderRepository, queryParser);

13
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/FilterTagTransformerTests.cs

@ -7,6 +7,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
@ -20,37 +21,37 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
private readonly Guid appId = Guid.NewGuid(); private readonly Guid appId = Guid.NewGuid();
[Fact] [Fact]
public void Should_normalize_tags() public async Task Should_normalize_tags()
{ {
A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A<HashSet<string>>.That.Contains("name1"))) A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A<HashSet<string>>.That.Contains("name1")))
.Returns(new Dictionary<string, string> { ["name1"] = "id1" }); .Returns(new Dictionary<string, string> { ["name1"] = "id1" });
var source = ClrFilter.Eq("tags", "name1"); var source = ClrFilter.Eq("tags", "name1");
var result = FilterTagTransformer.Transform(source, appId, tagService); var result = await FilterTagTransformer.TransformAsync(source, appId, tagService);
Assert.Equal("tags == 'id1'", result!.ToString()); Assert.Equal("tags == 'id1'", result!.ToString());
} }
[Fact] [Fact]
public void Should_not_fail_when_tags_not_found() public async Task Should_not_fail_when_tags_not_found()
{ {
A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A<HashSet<string>>.That.Contains("name1"))) A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A<HashSet<string>>.That.Contains("name1")))
.Returns(new Dictionary<string, string>()); .Returns(new Dictionary<string, string>());
var source = ClrFilter.Eq("tags", "name1"); var source = ClrFilter.Eq("tags", "name1");
var result = FilterTagTransformer.Transform(source, appId, tagService); var result = await FilterTagTransformer.TransformAsync(source, appId, tagService);
Assert.Equal("tags == 'name1'", result!.ToString()); Assert.Equal("tags == 'name1'", result!.ToString());
} }
[Fact] [Fact]
public void Should_not_normalize_other_field() public async Task Should_not_normalize_other_field()
{ {
var source = ClrFilter.Eq("other", "value"); var source = ClrFilter.Eq("other", "value");
var result = FilterTagTransformer.Transform(source, appId, tagService); var result = await FilterTagTransformer.TransformAsync(source, appId, tagService);
Assert.Equal("other == 'value'", result!.ToString()); Assert.Equal("other == 'value'", result!.ToString());

91
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs

@ -27,6 +27,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 MongoDbQueryTests
@ -79,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_throw_exception_for_invalid_field() public void Should_throw_exception_for_invalid_field()
{ {
Assert.Throws<NotSupportedException>(() => F(ClrFilter.Eq("data/invalid/iv", "Me"))); Assert.Throws<NotSupportedException>(() => _F(ClrFilter.Eq("data/invalid/iv", "Me")));
} }
[Fact] [Fact]
@ -87,8 +90,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{ {
var id = Guid.NewGuid(); var id = Guid.NewGuid();
var i = F(ClrFilter.Eq("id", id)); var i = _F(ClrFilter.Eq("id", id));
var o = C($"{{ '_id' : '{id}' }}"); var o = _C($"{{ '_id' : '{id}' }}");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -98,8 +101,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{ {
var id = Guid.NewGuid().ToString(); var id = Guid.NewGuid().ToString();
var i = F(ClrFilter.Eq("id", id)); var i = _F(ClrFilter.Eq("id", id));
var o = C($"{{ '_id' : '{id}' }}"); var o = _C($"{{ '_id' : '{id}' }}");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -109,8 +112,8 @@ 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 i = _F(ClrFilter.In("id", new List<Guid> { id }));
var o = C($"{{ '_id' : {{ '$in' : ['{id}'] }} }}"); var o = _C($"{{ '_id' : {{ '$in' : ['{id}'] }} }}");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -120,8 +123,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{ {
var id = Guid.NewGuid().ToString(); var id = Guid.NewGuid().ToString();
var i = F(ClrFilter.In("id", new List<string> { id })); var i = _F(ClrFilter.In("id", new List<string> { id }));
var o = C($"{{ '_id' : {{ '$in' : ['{id}'] }} }}"); var o = _C($"{{ '_id' : {{ '$in' : ['{id}'] }} }}");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -129,8 +132,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[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 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 o = _C("{ 'mt' : ISODate('1988-01-19T12:00:00Z') }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -138,8 +141,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_query_with_lastModifiedBy() public void Should_make_query_with_lastModifiedBy()
{ {
var i = F(ClrFilter.Eq("lastModifiedBy", "Me")); var i = _F(ClrFilter.Eq("lastModifiedBy", "Me"));
var o = C("{ 'mb' : 'Me' }"); var o = _C("{ 'mb' : 'Me' }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -147,8 +150,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[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 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 o = _C("{ 'ct' : ISODate('1988-01-19T12:00:00Z') }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -156,8 +159,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_query_with_createdBy() public void Should_make_query_with_createdBy()
{ {
var i = F(ClrFilter.Eq("createdBy", "Me")); var i = _F(ClrFilter.Eq("createdBy", "Me"));
var o = C("{ 'cb' : 'Me' }"); var o = _C("{ 'cb' : 'Me' }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -165,8 +168,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_query_with_version() public void Should_make_query_with_version()
{ {
var i = F(ClrFilter.Eq("version", 0L)); var i = _F(ClrFilter.Eq("version", 0L));
var o = C("{ 'vs' : NumberLong(0) }"); var o = _C("{ 'vs' : NumberLong(0) }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -174,8 +177,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_query_with_version_and_list() public void Should_make_query_with_version_and_list()
{ {
var i = F(ClrFilter.In("version", new List<long> { 0L, 2L, 5L })); var i = _F(ClrFilter.In("version", new List<long> { 0L, 2L, 5L }));
var o = C("{ 'vs' : { '$in' : [NumberLong(0), NumberLong(2), NumberLong(5)] } }"); var o = _C("{ 'vs' : { '$in' : [NumberLong(0), NumberLong(2), NumberLong(5)] } }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -183,8 +186,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_query_with_empty_test() public void Should_make_query_with_empty_test()
{ {
var i = F(ClrFilter.Empty("data/firstName/iv")); var i = _F(ClrFilter.Empty("data/firstName/iv"));
var o = C("{ '$or' : [{ 'do.1.iv' : { '$exists' : false } }, { 'do.1.iv' : null }, { 'do.1.iv' : '' }, { 'do.1.iv' : [] }] }"); var o = _C("{ '$or' : [{ 'do.1.iv' : { '$exists' : false } }, { 'do.1.iv' : null }, { 'do.1.iv' : '' }, { 'do.1.iv' : [] }] }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -192,8 +195,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[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 i = _F(ClrFilter.Eq("data/birthday/iv", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value));
var o = C("{ 'do.5.iv' : '1988-01-19T12:00:00Z' }"); var o = _C("{ 'do.5.iv' : '1988-01-19T12:00:00Z' }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -201,8 +204,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[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 i = _F(ClrFilter.Eq("data/dashed_field/iv", "Value"));
var o = C("{ 'do.8.iv' : 'Value' }"); var o = _C("{ 'do.8.iv' : 'Value' }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -210,8 +213,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[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 i = _F(ClrFilter.Eq("data/friends/iv", "guid"));
var o = C("{ 'do.7.iv' : 'guid' }"); var o = _C("{ 'do.7.iv' : 'guid' }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -219,8 +222,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[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 i = _F(ClrFilter.Eq("data/hobbies/iv/name", "PC"));
var o = C("{ 'do.9.iv.91' : 'PC' }"); var o = _C("{ 'do.9.iv.91' : 'PC' }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -228,8 +231,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[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 i = _F(ClrFilter.Eq("data/pictures/iv", "guid"));
var o = C("{ 'do.6.iv' : 'guid' }"); var o = _C("{ 'do.6.iv' : 'guid' }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -237,8 +240,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_query_with_full_text() public void Should_make_query_with_full_text()
{ {
var i = Q(new ClrQuery { FullText = "Hello my World" }); var i = _Q(new ClrQuery { FullText = "Hello my World" });
var o = C("{ '$text' : { '$search' : 'Hello my World' } }"); var o = _C("{ '$text' : { '$search' : 'Hello my World' } }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -246,8 +249,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[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 i = _S(SortBuilder.Descending("data/age/iv"));
var o = C("{ 'do.4.iv' : -1 }"); var o = _C("{ 'do.4.iv' : -1 }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -255,8 +258,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[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 i = _S(SortBuilder.Ascending("data/age/iv"), SortBuilder.Descending("data/firstName/en"));
var o = C("{ 'do.4.iv' : 1, 'do.1.en' : -1 }"); var o = _C("{ 'do.4.iv' : 1, 'do.1.en' : -1 }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -285,17 +288,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
.MustHaveHappened(); .MustHaveHappened();
} }
private static string C(string value) private static string _C(string value)
{ {
return value.Replace('\'', '"'); return value.Replace('\'', '"');
} }
private string F(FilterNode<ClrValue> filter) private string _F(FilterNode<ClrValue> filter)
{ {
return Q(new ClrQuery { Filter = filter }); return _Q(new ClrQuery { Filter = filter });
} }
private string S(params SortNode[] sorts) private string _S(params SortNode[] sorts)
{ {
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
@ -312,7 +315,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
return i; return i;
} }
private string Q(ClrQuery query) private string _Q(ClrQuery query)
{ {
var rendered = var rendered =
query.AdjustToModel(schemaDef).BuildFilter<MongoContentEntity>().Filter! query.AdjustToModel(schemaDef).BuildFilter<MongoContentEntity>().Filter!

47
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs

@ -6,6 +6,9 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Threading.Tasks;
using Esprima.Ast;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core;
@ -46,63 +49,63 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
} }
[Fact] [Fact]
public void Should_use_existing_query() public async Task Should_use_existing_query()
{ {
var clrQuery = new ClrQuery(); var clrQuery = new ClrQuery();
var parsed = sut.ParseQuery(requestContext, schema, Q.Empty.WithQuery(clrQuery)); var parsed = await sut.ParseQueryAsync(requestContext, schema, Q.Empty.WithQuery(clrQuery));
Assert.Same(parsed, clrQuery); Assert.Same(parsed, clrQuery);
} }
[Fact] [Fact]
public void Should_throw_if_odata_query_is_invalid() public async Task Should_throw_if_odata_query_is_invalid()
{ {
var query = Q.Empty.WithODataQuery("$filter=invalid"); var query = Q.Empty.WithODataQuery("$filter=invalid");
Assert.Throws<ValidationException>(() => sut.ParseQuery(requestContext, schema, query)); await Assert.ThrowsAsync<ValidationException>(() => sut.ParseQueryAsync(requestContext, schema, query).AsTask());
} }
[Fact] [Fact]
public void Should_throw_if_json_query_is_invalid() public async Task Should_throw_if_json_query_is_invalid()
{ {
var query = Q.Empty.WithJsonQuery("invalid"); var query = Q.Empty.WithJsonQuery("invalid");
Assert.Throws<ValidationException>(() => sut.ParseQuery(requestContext, schema, query)); await Assert.ThrowsAsync<ValidationException>(() => sut.ParseQueryAsync(requestContext, schema, query).AsTask());
} }
[Fact] [Fact]
public void Should_parse_odata_query() public async Task Should_parse_odata_query()
{ {
var query = Q.Empty.WithODataQuery("$top=100&$orderby=data/firstName/iv asc&$search=Hello World"); var query = Q.Empty.WithODataQuery("$top=100&$orderby=data/firstName/iv asc&$search=Hello World");
var parsed = sut.ParseQuery(requestContext, schema, query); var parsed = await sut.ParseQueryAsync(requestContext, schema, query);
Assert.Equal("FullText: 'Hello World'; Take: 100; Sort: data.firstName.iv Ascending", parsed.ToString()); Assert.Equal("FullText: 'Hello World'; Take: 100; Sort: data.firstName.iv Ascending", parsed.ToString());
} }
[Fact] [Fact]
public void Should_parse_odata_query_and_enrich_with_defaults() public async Task Should_parse_odata_query_and_enrich_with_defaults()
{ {
var query = Q.Empty.WithODataQuery("$top=200&$filter=data/firstName/iv eq 'ABC'"); var query = Q.Empty.WithODataQuery("$top=200&$filter=data/firstName/iv eq 'ABC'");
var parsed = sut.ParseQuery(requestContext, schema, query); var parsed = await sut.ParseQueryAsync(requestContext, schema, query);
Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 200; Sort: lastModified Descending", parsed.ToString()); Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 200; Sort: lastModified Descending", parsed.ToString());
} }
[Fact] [Fact]
public void Should_parse_json_query_and_enrich_with_defaults() public async Task Should_parse_json_query_and_enrich_with_defaults()
{ {
var query = Q.Empty.WithJsonQuery(Json("{ 'filter': { 'path': 'data.firstName.iv', 'op': 'eq', 'value': 'ABC' } }")); var query = Q.Empty.WithJsonQuery(Json("{ 'filter': { 'path': 'data.firstName.iv', 'op': 'eq', 'value': 'ABC' } }"));
var parsed = sut.ParseQuery(requestContext, schema, query); var parsed = await sut.ParseQueryAsync(requestContext, schema, query);
Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending", parsed.ToString()); Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending", parsed.ToString());
} }
[Fact] [Fact]
public void Should_convert_json_query_and_enrich_with_defaults() public async Task Should_convert_json_query_and_enrich_with_defaults()
{ {
var query = Q.Empty.WithJsonQuery( var query = Q.Empty.WithJsonQuery(
new Query<IJsonValue> new Query<IJsonValue>
@ -110,23 +113,23 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
Filter = new CompareFilter<IJsonValue>("data.firstName.iv", CompareOperator.Equals, JsonValue.Create("ABC")) Filter = new CompareFilter<IJsonValue>("data.firstName.iv", CompareOperator.Equals, JsonValue.Create("ABC"))
}); });
var parsed = sut.ParseQuery(requestContext, schema, query); var parsed = await sut.ParseQueryAsync(requestContext, schema, query);
Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending", parsed.ToString()); Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending", parsed.ToString());
} }
[Fact] [Fact]
public void Should_parse_json_full_text_query_and_enrich_with_defaults() public async Task Should_parse_json_full_text_query_and_enrich_with_defaults()
{ {
var query = Q.Empty.WithJsonQuery(Json("{ 'fullText': 'Hello' }")); var query = Q.Empty.WithJsonQuery(Json("{ 'fullText': 'Hello' }"));
var parsed = sut.ParseQuery(requestContext, schema, query); var parsed = await sut.ParseQueryAsync(requestContext, schema, query);
Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending", parsed.ToString()); Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending", parsed.ToString());
} }
[Fact] [Fact]
public void Should_convert_json_full_text_query_and_enrich_with_defaults() public async Task Should_convert_json_full_text_query_and_enrich_with_defaults()
{ {
var query = Q.Empty.WithJsonQuery( var query = Q.Empty.WithJsonQuery(
new Query<IJsonValue> new Query<IJsonValue>
@ -134,27 +137,27 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
FullText = "Hello" FullText = "Hello"
}); });
var parsed = sut.ParseQuery(requestContext, schema, query); var parsed = await sut.ParseQueryAsync(requestContext, schema, query);
Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending", parsed.ToString()); Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending", parsed.ToString());
} }
[Fact] [Fact]
public void Should_apply_default_page_size() public async Task Should_apply_default_page_size()
{ {
var query = Q.Empty; var query = Q.Empty;
var parsed = sut.ParseQuery(requestContext, schema, query); var parsed = await sut.ParseQueryAsync(requestContext, schema, query);
Assert.Equal("Take: 30; Sort: lastModified Descending", parsed.ToString()); Assert.Equal("Take: 30; Sort: lastModified Descending", parsed.ToString());
} }
[Fact] [Fact]
public void Should_limit_number_of_contents() public async Task Should_limit_number_of_contents()
{ {
var query = Q.Empty.WithODataQuery("$top=300&$skip=20"); var query = Q.Empty.WithODataQuery("$top=300&$skip=20");
var parsed = sut.ParseQuery(requestContext, schema, query); var parsed = await sut.ParseQueryAsync(requestContext, schema, query);
Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending", parsed.ToString()); Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending", parsed.ToString());
} }

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs

@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name)) A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name))
.Returns(schema); .Returns(schema);
A.CallTo(() => queryParser.ParseQuery(A<Context>._, schema, A<Q>._)) A.CallTo(() => queryParser.ParseQueryAsync(A<Context>._, schema, A<Q>._))
.Returns(new ClrQuery()); .Returns(new ClrQuery());
sut = new ContentQueryService( sut = new ContentQueryService(

10
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/FilterTagTransformerTests.cs

@ -45,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var source = ClrFilter.Eq("data.tags2.iv", "name1"); var source = ClrFilter.Eq("data.tags2.iv", "name1");
var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); var result = FilterTagTransformer.TransformAsync(source, appId.Id, schema, tagService);
Assert.Equal("data.tags2.iv == 'id1'", result!.ToString()); Assert.Equal("data.tags2.iv == 'id1'", result!.ToString());
} }
@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var source = ClrFilter.Eq("data.tags2.iv", "name1"); var source = ClrFilter.Eq("data.tags2.iv", "name1");
var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); var result = FilterTagTransformer.TransformAsync(source, appId.Id, schema, tagService);
Assert.Equal("data.tags2.iv == 'name1'", result!.ToString()); Assert.Equal("data.tags2.iv == 'name1'", result!.ToString());
} }
@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var source = ClrFilter.Eq("data.tags1.iv", "value"); var source = ClrFilter.Eq("data.tags1.iv", "value");
var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); var result = FilterTagTransformer.TransformAsync(source, appId.Id, schema, tagService);
Assert.Equal("data.tags1.iv == 'value'", result!.ToString()); Assert.Equal("data.tags1.iv == 'value'", result!.ToString());
@ -81,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var source = ClrFilter.Eq("data.string.iv", "value"); var source = ClrFilter.Eq("data.string.iv", "value");
var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); var result = FilterTagTransformer.TransformAsync(source, appId.Id, schema, tagService);
Assert.Equal("data.string.iv == 'value'", result!.ToString()); Assert.Equal("data.string.iv == 'value'", result!.ToString());
@ -94,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var source = ClrFilter.Eq("no.data", "value"); var source = ClrFilter.Eq("no.data", "value");
var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); var result = FilterTagTransformer.TransformAsync(source, appId.Id, schema, tagService);
Assert.Equal("no.data == 'value'", result!.ToString()); Assert.Equal("no.data == 'value'", result!.ToString());

57
backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs

@ -1,57 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using FakeItEasy;
using Squidex.Infrastructure;
using Squidex.Infrastructure.States;
using Xunit;
namespace Squidex.Domain.Users
{
public class DefaultXmlRepositoryTests
{
private readonly ISnapshotStore<DefaultXmlRepository.State, string> store = A.Fake<ISnapshotStore<DefaultXmlRepository.State, string>>();
private readonly DefaultXmlRepository sut;
public DefaultXmlRepositoryTests()
{
sut = new DefaultXmlRepository(store);
}
[Fact]
public void Should_write_new_item_to_store_with_friendly_name()
{
sut.StoreElement(new XElement("a"), "friendly-name");
A.CallTo(() => store.WriteAsync("friendly-name", A<DefaultXmlRepository.State>.That.Matches(x => x.Xml == "<a />"), EtagVersion.Any, EtagVersion.Any))
.MustHaveHappened();
}
[Fact]
public void Should_return_items_from_store()
{
A.CallTo(() => store.ReadAllAsync(A<Func<DefaultXmlRepository.State, long, Task>>._, A<CancellationToken>._))
.Invokes((Func<DefaultXmlRepository.State, long, Task> callback, CancellationToken ct) =>
{
callback(new DefaultXmlRepository.State { Xml = "<a />" }, EtagVersion.Any);
callback(new DefaultXmlRepository.State { Xml = "<b />" }, EtagVersion.Any);
callback(new DefaultXmlRepository.State { Xml = "<c />" }, EtagVersion.Any);
});
var result = sut.GetAllElements().ToList();
Assert.Equal("<a />", result[0].ToString());
Assert.Equal("<b />", result[1].ToString());
Assert.Equal("<c />", result[2].ToString());
}
}
}

171
backend/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs

@ -9,6 +9,9 @@ using Microsoft.OData.Edm;
using Squidex.Infrastructure.Queries.OData; using Squidex.Infrastructure.Queries.OData;
using Xunit; using Xunit;
#pragma warning disable SA1300 // Element should begin with upper-case letter
#pragma warning disable IDE1006 // Naming Styles
namespace Squidex.Infrastructure.Queries namespace Squidex.Infrastructure.Queries
{ {
public class QueryODataConversionTests public class QueryODataConversionTests
@ -65,8 +68,8 @@ namespace Squidex.Infrastructure.Queries
[InlineData("properties/nested/dateime")] [InlineData("properties/nested/dateime")]
public void Should_parse_filter_when_type_is_datetime(string field) public void Should_parse_filter_when_type_is_datetime(string field)
{ {
var i = Q($"$filter={field} eq 1988-01-19T12:00:00Z"); var i = _Q($"$filter={field} eq 1988-01-19T12:00:00Z");
var o = C($"Filter: {field} == 1988-01-19T12:00:00Z"); var o = _C($"Filter: {field} == 1988-01-19T12:00:00Z");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -74,8 +77,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_when_type_is_datetime_list() public void Should_parse_filter_when_type_is_datetime_list()
{ {
var i = Q("$filter=created in ('1988-01-19T12:00:00Z')"); var i = _Q("$filter=created in ('1988-01-19T12:00:00Z')");
var o = C("Filter: created in [1988-01-19T12:00:00Z]"); var o = _C("Filter: created in [1988-01-19T12:00:00Z]");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -83,8 +86,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_when_type_is_datetime_and_and_value_is_date() public void Should_parse_filter_when_type_is_datetime_and_and_value_is_date()
{ {
var i = Q("$filter=created eq 1988-01-19"); var i = _Q("$filter=created eq 1988-01-19");
var o = C("Filter: created == 1988-01-19T00:00:00Z"); var o = _C("Filter: created == 1988-01-19T00:00:00Z");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -96,8 +99,8 @@ namespace Squidex.Infrastructure.Queries
[InlineData("properties/nested/date")] [InlineData("properties/nested/date")]
public void Should_parse_filter_when_type_is_date(string field) public void Should_parse_filter_when_type_is_date(string field)
{ {
var i = Q($"$filter={field} eq 1988-01-19"); var i = _Q($"$filter={field} eq 1988-01-19");
var o = C($"Filter: {field} == 1988-01-19T00:00:00Z"); var o = _C($"Filter: {field} == 1988-01-19T00:00:00Z");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -105,8 +108,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_when_type_is_date_list() public void Should_parse_filter_when_type_is_date_list()
{ {
var i = Q("$filter=birthday in ('1988-01-19')"); var i = _Q("$filter=birthday in ('1988-01-19')");
var o = C("Filter: birthday in [1988-01-19T00:00:00Z]"); var o = _C("Filter: birthday in [1988-01-19T00:00:00Z]");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -118,8 +121,8 @@ namespace Squidex.Infrastructure.Queries
[InlineData("properties/nested/guid")] [InlineData("properties/nested/guid")]
public void Should_parse_filter_when_type_is_guid(string field) public void Should_parse_filter_when_type_is_guid(string field)
{ {
var i = Q($"$filter={field} eq B5FE25E3-B262-4B17-91EF-B3772A6B62BB"); var i = _Q($"$filter={field} eq B5FE25E3-B262-4B17-91EF-B3772A6B62BB");
var o = C($"Filter: {field} == b5fe25e3-b262-4b17-91ef-b3772a6b62bb"); var o = _C($"Filter: {field} == b5fe25e3-b262-4b17-91ef-b3772a6b62bb");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -127,8 +130,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_when_type_is_guid_list() public void Should_parse_filter_when_type_is_guid_list()
{ {
var i = Q("$filter=id in ('B5FE25E3-B262-4B17-91EF-B3772A6B62BB')"); var i = _Q("$filter=id in ('B5FE25E3-B262-4B17-91EF-B3772A6B62BB')");
var o = C("Filter: id in [b5fe25e3-b262-4b17-91ef-b3772a6b62bb]"); var o = _C("Filter: id in [b5fe25e3-b262-4b17-91ef-b3772a6b62bb]");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -136,8 +139,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_when_type_is_null() public void Should_parse_filter_when_type_is_null()
{ {
var i = Q("$filter=firstName eq null"); var i = _Q("$filter=firstName eq null");
var o = C("Filter: firstName == null"); var o = _C("Filter: firstName == null");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -149,8 +152,8 @@ namespace Squidex.Infrastructure.Queries
[InlineData("properties/nested/string")] [InlineData("properties/nested/string")]
public void Should_parse_filter_when_type_is_string(string field) public void Should_parse_filter_when_type_is_string(string field)
{ {
var i = Q($"$filter={field} eq 'Dagobert'"); var i = _Q($"$filter={field} eq 'Dagobert'");
var o = C($"Filter: {field} == 'Dagobert'"); var o = _C($"Filter: {field} == 'Dagobert'");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -158,8 +161,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_when_type_is_string_list() public void Should_parse_filter_when_type_is_string_list()
{ {
var i = Q("$filter=firstName in ('Dagobert')"); var i = _Q("$filter=firstName in ('Dagobert')");
var o = C("Filter: firstName in ['Dagobert']"); var o = _C("Filter: firstName in ['Dagobert']");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -171,8 +174,8 @@ namespace Squidex.Infrastructure.Queries
[InlineData("properties/nested/boolean")] [InlineData("properties/nested/boolean")]
public void Should_parse_filter_when_type_is_boolean(string field) public void Should_parse_filter_when_type_is_boolean(string field)
{ {
var i = Q($"$filter={field} eq true"); var i = _Q($"$filter={field} eq true");
var o = C($"Filter: {field} == True"); var o = _C($"Filter: {field} == True");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -180,8 +183,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_when_type_is_boolean_list() public void Should_parse_filter_when_type_is_boolean_list()
{ {
var i = Q("$filter=isComicFigure in (true)"); var i = _Q("$filter=isComicFigure in (true)");
var o = C("Filter: isComicFigure in [True]"); var o = _C("Filter: isComicFigure in [True]");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -193,8 +196,8 @@ namespace Squidex.Infrastructure.Queries
[InlineData("properties/nested/int")] [InlineData("properties/nested/int")]
public void Should_parse_filter_when_type_is_int32(string field) public void Should_parse_filter_when_type_is_int32(string field)
{ {
var i = Q($"$filter={field} eq 60"); var i = _Q($"$filter={field} eq 60");
var o = C($"Filter: {field} == 60"); var o = _C($"Filter: {field} == 60");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -202,8 +205,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_when_type_is_int32_list() public void Should_parse_filter_when_type_is_int32_list()
{ {
var i = Q("$filter=age in (60)"); var i = _Q("$filter=age in (60)");
var o = C("Filter: age in [60]"); var o = _C("Filter: age in [60]");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -215,8 +218,8 @@ namespace Squidex.Infrastructure.Queries
[InlineData("properties/nested/long")] [InlineData("properties/nested/long")]
public void Should_parse_filter_when_type_is_int64(string field) public void Should_parse_filter_when_type_is_int64(string field)
{ {
var i = Q($"$filter={field} eq 31543143513456789"); var i = _Q($"$filter={field} eq 31543143513456789");
var o = C($"Filter: {field} == 31543143513456789"); var o = _C($"Filter: {field} == 31543143513456789");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -224,8 +227,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_when_type_is_int64_list() public void Should_parse_filter_when_type_is_int64_list()
{ {
var i = Q("$filter=incomeCents in (31543143513456789)"); var i = _Q("$filter=incomeCents in (31543143513456789)");
var o = C("Filter: incomeCents in [31543143513456789]"); var o = _C("Filter: incomeCents in [31543143513456789]");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -237,8 +240,8 @@ namespace Squidex.Infrastructure.Queries
[InlineData("properties/nested/double")] [InlineData("properties/nested/double")]
public void Should_parse_filter_when_type_is_double(string field) public void Should_parse_filter_when_type_is_double(string field)
{ {
var i = Q($"$filter={field} eq 5634474356.1233"); var i = _Q($"$filter={field} eq 5634474356.1233");
var o = C($"Filter: {field} == 5634474356.1233"); var o = _C($"Filter: {field} == 5634474356.1233");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -246,8 +249,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_when_type_is_double_list() public void Should_parse_filter_when_type_is_double_list()
{ {
var i = Q("$filter=incomeMio in (5634474356.1233)"); var i = _Q("$filter=incomeMio in (5634474356.1233)");
var o = C("Filter: incomeMio in [5634474356.1233]"); var o = _C("Filter: incomeMio in [5634474356.1233]");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -255,8 +258,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_with_negation() public void Should_parse_filter_with_negation()
{ {
var i = Q("$filter=not endswith(lastName, 'Duck')"); var i = _Q("$filter=not endswith(lastName, 'Duck')");
var o = C("Filter: !(endsWith(lastName, 'Duck'))"); var o = _C("Filter: !(endsWith(lastName, 'Duck'))");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -264,8 +267,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_with_startswith() public void Should_parse_filter_with_startswith()
{ {
var i = Q("$filter=startswith(lastName, 'Duck')"); var i = _Q("$filter=startswith(lastName, 'Duck')");
var o = C("Filter: startsWith(lastName, 'Duck')"); var o = _C("Filter: startsWith(lastName, 'Duck')");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -273,8 +276,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_with_endswith() public void Should_parse_filter_with_endswith()
{ {
var i = Q("$filter=endswith(lastName, 'Duck')"); var i = _Q("$filter=endswith(lastName, 'Duck')");
var o = C("Filter: endsWith(lastName, 'Duck')"); var o = _C("Filter: endsWith(lastName, 'Duck')");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -282,8 +285,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_with_empty() public void Should_parse_filter_with_empty()
{ {
var i = Q("$filter=empty(lastName)"); var i = _Q("$filter=empty(lastName)");
var o = C("Filter: empty(lastName)"); var o = _C("Filter: empty(lastName)");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -291,8 +294,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_with_empty_to_true() public void Should_parse_filter_with_empty_to_true()
{ {
var i = Q("$filter=empty(lastName) eq true"); var i = _Q("$filter=empty(lastName) eq true");
var o = C("Filter: empty(lastName)"); var o = _C("Filter: empty(lastName)");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -300,8 +303,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_with_contains() public void Should_parse_filter_with_contains()
{ {
var i = Q("$filter=contains(lastName, 'Duck')"); var i = _Q("$filter=contains(lastName, 'Duck')");
var o = C("Filter: contains(lastName, 'Duck')"); var o = _C("Filter: contains(lastName, 'Duck')");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -309,8 +312,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_with_contains_to_true() public void Should_parse_filter_with_contains_to_true()
{ {
var i = Q("$filter=contains(lastName, 'Duck') eq true"); var i = _Q("$filter=contains(lastName, 'Duck') eq true");
var o = C("Filter: contains(lastName, 'Duck')"); var o = _C("Filter: contains(lastName, 'Duck')");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -318,8 +321,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_with_contains_to_false() public void Should_parse_filter_with_contains_to_false()
{ {
var i = Q("$filter=contains(lastName, 'Duck') eq false"); var i = _Q("$filter=contains(lastName, 'Duck') eq false");
var o = C("Filter: !(contains(lastName, 'Duck'))"); var o = _C("Filter: !(contains(lastName, 'Duck'))");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -327,8 +330,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_with_equals() public void Should_parse_filter_with_equals()
{ {
var i = Q("$filter=age eq 1"); var i = _Q("$filter=age eq 1");
var o = C("Filter: age == 1"); var o = _C("Filter: age == 1");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -336,8 +339,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_with_notequals() public void Should_parse_filter_with_notequals()
{ {
var i = Q("$filter=age ne 1"); var i = _Q("$filter=age ne 1");
var o = C("Filter: age != 1"); var o = _C("Filter: age != 1");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -345,8 +348,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_with_lessthan() public void Should_parse_filter_with_lessthan()
{ {
var i = Q("$filter=age lt 1"); var i = _Q("$filter=age lt 1");
var o = C("Filter: age < 1"); var o = _C("Filter: age < 1");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -354,8 +357,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_with_lessthanorequal() public void Should_parse_filter_with_lessthanorequal()
{ {
var i = Q("$filter=age le 1"); var i = _Q("$filter=age le 1");
var o = C("Filter: age <= 1"); var o = _C("Filter: age <= 1");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -363,8 +366,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_with_greaterthan() public void Should_parse_filter_with_greaterthan()
{ {
var i = Q("$filter=age gt 1"); var i = _Q("$filter=age gt 1");
var o = C("Filter: age > 1"); var o = _C("Filter: age > 1");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -372,8 +375,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_with_greaterthanorequal() public void Should_parse_filter_with_greaterthanorequal()
{ {
var i = Q("$filter=age ge 1"); var i = _Q("$filter=age ge 1");
var o = C("Filter: age >= 1"); var o = _C("Filter: age >= 1");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -381,8 +384,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_with_conjunction_and_contains() public void Should_parse_filter_with_conjunction_and_contains()
{ {
var i = Q("$filter=contains(firstName, 'Sebastian') eq false and isComicFigure eq true"); var i = _Q("$filter=contains(firstName, 'Sebastian') eq false and isComicFigure eq true");
var o = C("Filter: (!(contains(firstName, 'Sebastian')) && isComicFigure == True)"); var o = _C("Filter: (!(contains(firstName, 'Sebastian')) && isComicFigure == True)");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -390,8 +393,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_with_conjunction() public void Should_parse_filter_with_conjunction()
{ {
var i = Q("$filter=age eq 1 and age eq 2"); var i = _Q("$filter=age eq 1 and age eq 2");
var o = C("Filter: (age == 1 && age == 2)"); var o = _C("Filter: (age == 1 && age == 2)");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -399,8 +402,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_with_disjunction() public void Should_parse_filter_with_disjunction()
{ {
var i = Q("$filter=age eq 1 or age eq 2"); var i = _Q("$filter=age eq 1 or age eq 2");
var o = C("Filter: (age == 1 || age == 2)"); var o = _C("Filter: (age == 1 || age == 2)");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -408,8 +411,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_with_full_text_numbers() public void Should_parse_filter_with_full_text_numbers()
{ {
var i = Q("$search=\"33k\""); var i = _Q("$search=\"33k\"");
var o = C("FullText: '33k'"); var o = _C("FullText: '33k'");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -417,8 +420,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_with_full_text() public void Should_parse_filter_with_full_text()
{ {
var i = Q("$search=Duck"); var i = _Q("$search=Duck");
var o = C("FullText: 'Duck'"); var o = _C("FullText: 'Duck'");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -426,8 +429,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_with_full_text_and_multiple_terms() public void Should_parse_filter_with_full_text_and_multiple_terms()
{ {
var i = Q("$search=Dagobert or Donald"); var i = _Q("$search=Dagobert or Donald");
var o = C("FullText: 'Dagobert or Donald'"); var o = _C("FullText: 'Dagobert or Donald'");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -435,8 +438,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_make_orderby_with_single_field() public void Should_make_orderby_with_single_field()
{ {
var i = Q("$orderby=age desc"); var i = _Q("$orderby=age desc");
var o = C("Sort: age Descending"); var o = _C("Sort: age Descending");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -444,8 +447,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_make_orderby_with_multiple_field() public void Should_make_orderby_with_multiple_field()
{ {
var i = Q("$orderby=age, incomeMio desc"); var i = _Q("$orderby=age, incomeMio desc");
var o = C("Sort: age Ascending, incomeMio Descending"); var o = _C("Sort: age Ascending, incomeMio Descending");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -453,18 +456,18 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_parse_filter_and_take() public void Should_parse_filter_and_take()
{ {
var i = Q("$top=3&$skip=4"); var i = _Q("$top=3&$skip=4");
var o = C("Skip: 4; Take: 3"); var o = _C("Skip: 4; Take: 3");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
private static string C(string value) private static string _C(string value)
{ {
return value.Replace('/', '.'); return value.Replace('/', '.');
} }
private static string? Q(string value) private static string? _Q(string value)
{ {
var parser = EdmModel.ParseQuery(value); var parser = EdmModel.ParseQuery(value);

Loading…
Cancel
Save