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.Queries;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
@ -84,9 +85,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
.QuerySort(query)
.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"))
{

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.MongoDb.Queries;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Tasks;
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 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);
}
return ResultList.Create<IContentEntity>(contentCount.Result, contentItems.Result);
return ResultList.Create<IContentEntity>(total, items);
}
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.MongoDb;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Tasks;
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 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)

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

@ -7,6 +7,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Microsoft.OData;
using Microsoft.OData.Edm;
@ -41,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
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));
@ -71,7 +72,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
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)

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)
{
var parsedQuery = queryParser.ParseQuery(context, query);
var parsedQuery = await queryParser.ParseQueryAsync(context, query);
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
{
public sealed class FilterTagTransformer : TransformVisitor<ClrValue>
public sealed class FilterTagTransformer : AsyncTransformVisitor<ClrValue>
{
private readonly ITagService tagService;
private readonly Guid appId;
@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
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(tagService, nameof(tagService));
@ -33,11 +33,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
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)
{
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))
{

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

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents.Counter
{
@ -46,14 +47,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter
{
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)
{
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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.OData;
@ -43,7 +44,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
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(schema, nameof(schema));
@ -90,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
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)
{
var parsedQuery = queryParser.ParseQuery(context, schema, query);
var parsedQuery = await queryParser.ParseQueryAsync(context, schema, query);
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
{
public sealed class FilterTagTransformer : TransformVisitor<ClrValue>
public sealed class FilterTagTransformer : AsyncTransformVisitor<ClrValue>
{
private readonly ITagService tagService;
private readonly ISchemaEntity schema;
@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
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(tagService, nameof(tagService));
@ -38,11 +38,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
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))
{
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))
{

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.Contents.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{
@ -72,12 +73,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
if (ids.Count > 0)
{
var taskForAssets = QueryAssetIdsAsync(context, ids);
var taskForContents = QueryContentIdsAsync(context, ids);
var (assets, refContents) = await AsyncHelper.WhenAll(
QueryAssetIdsAsync(context, ids),
QueryContentIdsAsync(context, ids));
await Task.WhenAll(taskForAssets, taskForContents);
var foundIds = new HashSet<Guid>(taskForAssets.Result.Union(taskForContents.Result));
var foundIds = new HashSet<Guid>(assets.Union(refContents));
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)
{
hostName = $"squidex.{DateTime.UtcNow.Ticks.ToString()}";
hostName = $"squidex.{DateTime.UtcNow.Ticks}";
}
else
{

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

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using EventStore.ClientAPI;
using EventStore.ClientAPI.Exceptions;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.EventSourcing
{
@ -35,7 +36,7 @@ namespace Squidex.Infrastructure.EventSourcing
this.position = projectionClient.ParsePositionOrNull(position);
this.prefix = prefix;
var streamName = projectionClient.CreateProjectionAsync(streamFilter).Result;
var streamName = AsyncHelper.Sync(() => projectionClient.CreateProjectionAsync(streamFilter));
this.serializer = serializer;
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.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
namespace Squidex.Infrastructure.Queries
@ -24,7 +25,17 @@ namespace Squidex.Infrastructure.Queries
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)
{

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

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using System.Collections.Generic;
namespace Squidex.Infrastructure.Queries
{
@ -18,9 +18,19 @@ namespace Squidex.Infrastructure.Queries
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)

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

@ -19,7 +19,7 @@ namespace Squidex.Infrastructure
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 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 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.Commands;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Tasks;
using Squidex.Shared;
using Squidex.Web;
@ -51,17 +52,16 @@ namespace Squidex.Areas.Api.Controllers.Assets
[ApiCosts(1)]
public async Task<IActionResult> GetAssetFolders(string app, [FromQuery] Guid parentId)
{
var assetFolders = assetQuery.QueryAssetFoldersAsync(Context, parentId);
var assetPath = assetQuery.FindAssetFolderAsync(parentId);
await Task.WhenAll(assetFolders, assetPath);
var (folders, path) = await AsyncHelper.WhenAll(
assetQuery.QueryAssetFoldersAsync(Context, parentId),
assetQuery.FindAssetFolderAsync(parentId));
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);
}

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.Domain.Users;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Tasks;
using Squidex.Infrastructure.Validation;
using Squidex.Shared;
using Squidex.Web;
@ -22,12 +23,14 @@ namespace Squidex.Areas.Api.Controllers.Users
{
private readonly UserManager<IdentityUser> userManager;
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)
{
this.userManager = userManager;
this.userFactory = userFactory;
this.userEvents = userEvents;
}
[HttpGet]
@ -36,12 +39,11 @@ namespace Squidex.Areas.Api.Controllers.Users
[ApiPermission(Permissions.AdminUsersRead)]
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 taskForCount = userManager.CountByEmailAsync(query);
var (items, total) = await AsyncHelper.WhenAll(
userManager.QueryByEmailAsync(query, take, skip),
userManager.CountByEmailAsync(query));
await Task.WhenAll(taskForItems, taskForCount);
var response = UsersDto.FromResults(taskForItems.Result, taskForCount.Result, Resources);
var response = UsersDto.FromResults(items, total, Resources);
return Ok(response);
}
@ -72,6 +74,8 @@ namespace Squidex.Areas.Api.Controllers.Users
{
var user = await userManager.CreateAsync(userFactory, request.ToValues());
userEvents.OnUserRegistered(user);
var response = UserDto.FromUser(user, Resources);
return Ok(response);
@ -85,6 +89,8 @@ namespace Squidex.Areas.Api.Controllers.Users
{
var user = await userManager.UpdateAsync(id, request.ToValues());
userEvents.OnUserUpdated(user);
var response = UserDto.FromUser(user, Resources);
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.Infrastructure.Log;
using Squidex.Infrastructure.Security;
using Squidex.Infrastructure.Tasks;
using Squidex.Shared;
namespace Squidex.Areas.IdentityServer.Config
@ -46,7 +47,7 @@ namespace Squidex.Areas.IdentityServer.Config
var adminEmail = options.AdminEmail;
var adminPass = options.AdminPassword;
Task.Run(async () =>
AsyncHelper.Sync(async () =>
{
if (userManager.SupportsQueryableUsers && !userManager.Users.Any())
{
@ -69,7 +70,7 @@ namespace Squidex.Areas.IdentityServer.Config
.WriteProperty("status", "failed"));
}
}
}).Wait();
});
}
return services;

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

@ -42,18 +42,21 @@ namespace Squidex.Areas.IdentityServer.Config
services.AddIdentity<IdentityUser, IdentityRole>()
.AddDefaultTokenProviders();
services.AddSingleton<IPasswordValidator<IdentityUser>,
PwnedPasswordValidator>();
services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>,
UserClaimsPrincipalFactoryWithEmail>();
services.AddSingleton<IClaimsTransformation,
ApiPermissionUnifier>();
services.AddSingleton<IClientStore,
LazyClientStore>();
services.AddSingleton<IResourceStore,
InMemoryResourcesStore>();
services.AddSingleton<IXmlRepository,
DefaultXmlRepository>();
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.Assets;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Tasks;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
@ -232,11 +233,10 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
throw new DomainException("Cannot find user.");
}
var taskForProviders = signInManager.GetExternalProvidersAsync();
var taskForPassword = userManager.HasPasswordAsync(user.Identity);
var taskForLogins = userManager.GetLoginsAsync(user.Identity);
await Task.WhenAll(taskForProviders, taskForPassword, taskForLogins);
var (providers, hasPassword, logins) = await AsyncHelper.WhenAll(
signInManager.GetExternalProvidersAsync(),
userManager.HasPasswordAsync(user.Identity),
userManager.GetLoginsAsync(user.Identity));
var result = new ProfileVM
{
@ -244,10 +244,10 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
ClientSecret = user.ClientSecret()!,
Email = user.Email,
ErrorMessage = errorMessage,
ExternalLogins = taskForLogins.Result,
ExternalProviders = taskForProviders.Result,
ExternalLogins = logins,
ExternalProviders = providers,
DisplayName = user.DisplayName()!,
HasPassword = taskForPassword.Result,
HasPassword = hasPassword,
HasPasswordAuth = identityOptions.AllowPasswordAuth,
IsHidden = user.IsHidden(),
SuccessMessage = successMessage

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

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

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

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

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

@ -7,6 +7,8 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Esprima.Ast;
using FakeItEasy;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Tags;
@ -35,100 +37,100 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
}
[Fact]
public void Should_use_existing_query()
public async Task Should_use_existing_query()
{
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);
}
[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");
Assert.Throws<ValidationException>(() => sut.ParseQuery(requestContext, query));
await Assert.ThrowsAsync<ValidationException>(() => sut.ParseQueryAsync(requestContext, query).AsTask());
}
[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");
Assert.Throws<ValidationException>(() => sut.ParseQuery(requestContext, query));
await Assert.ThrowsAsync<ValidationException>(() => sut.ParseQueryAsync(requestContext, query).AsTask());
}
[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 parsed = sut.ParseQuery(requestContext, query);
var parsed = await sut.ParseQueryAsync(requestContext, query);
Assert.Equal("FullText: 'Hello World'; Take: 100; Sort: fileName Ascending", parsed.ToString());
}
[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 parsed = sut.ParseQuery(requestContext, query);
var parsed = await sut.ParseQueryAsync(requestContext, query);
Assert.Equal("Filter: fileName == 'ABC'; Take: 200; Sort: lastModified Descending", parsed.ToString());
}
[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 parsed = sut.ParseQuery(requestContext, query);
var parsed = await sut.ParseQueryAsync(requestContext, query);
Assert.Equal("Filter: fileName == 'ABC'; Take: 30; Sort: lastModified Descending", parsed.ToString());
}
[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 parsed = sut.ParseQuery(requestContext, query);
var parsed = await sut.ParseQueryAsync(requestContext, query);
Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending", parsed.ToString());
}
[Fact]
public void Should_apply_default_page_size()
public async Task Should_apply_default_page_size()
{
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());
}
[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 parsed = sut.ParseQuery(requestContext, query);
var parsed = await sut.ParseQueryAsync(requestContext, query);
Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending", parsed.ToString());
}
[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")))
.Returns(new Dictionary<string, string> { ["name1"] = "id1" });
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());
}

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));
A.CallTo(() => queryParser.ParseQuery(requestContext, A<Q>._))
A.CallTo(() => queryParser.ParseQueryAsync(requestContext, A<Q>._))
.Returns(new ClrQuery());
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.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Infrastructure.Queries;
@ -20,37 +21,37 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
private readonly Guid appId = Guid.NewGuid();
[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")))
.Returns(new Dictionary<string, string> { ["name1"] = "id1" });
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());
}
[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")))
.Returns(new Dictionary<string, string>());
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());
}
[Fact]
public void Should_not_normalize_other_field()
public async Task Should_not_normalize_other_field()
{
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());

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 SortBuilder = Squidex.Infrastructure.Queries.SortBuilder;
#pragma warning disable SA1300 // Element should begin with upper-case letter
#pragma warning disable IDE1006 // Naming Styles
namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{
public class MongoDbQueryTests
@ -79,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact]
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]
@ -87,8 +90,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{
var id = Guid.NewGuid();
var i = F(ClrFilter.Eq("id", id));
var o = C($"{{ '_id' : '{id}' }}");
var i = _F(ClrFilter.Eq("id", id));
var o = _C($"{{ '_id' : '{id}' }}");
Assert.Equal(o, i);
}
@ -98,8 +101,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{
var id = Guid.NewGuid().ToString();
var i = F(ClrFilter.Eq("id", id));
var o = C($"{{ '_id' : '{id}' }}");
var i = _F(ClrFilter.Eq("id", id));
var o = _C($"{{ '_id' : '{id}' }}");
Assert.Equal(o, i);
}
@ -109,8 +112,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{
var id = Guid.NewGuid();
var i = F(ClrFilter.In("id", new List<Guid> { id }));
var o = C($"{{ '_id' : {{ '$in' : ['{id}'] }} }}");
var i = _F(ClrFilter.In("id", new List<Guid> { id }));
var o = _C($"{{ '_id' : {{ '$in' : ['{id}'] }} }}");
Assert.Equal(o, i);
}
@ -120,8 +123,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{
var id = Guid.NewGuid().ToString();
var i = F(ClrFilter.In("id", new List<string> { id }));
var o = C($"{{ '_id' : {{ '$in' : ['{id}'] }} }}");
var i = _F(ClrFilter.In("id", new List<string> { id }));
var o = _C($"{{ '_id' : {{ '$in' : ['{id}'] }} }}");
Assert.Equal(o, i);
}
@ -129,8 +132,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact]
public void Should_make_query_with_lastModified()
{
var i = F(ClrFilter.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value));
var o = C("{ 'mt' : ISODate('1988-01-19T12:00:00Z') }");
var 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);
}
@ -138,8 +141,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact]
public void Should_make_query_with_lastModifiedBy()
{
var i = F(ClrFilter.Eq("lastModifiedBy", "Me"));
var o = C("{ 'mb' : 'Me' }");
var i = _F(ClrFilter.Eq("lastModifiedBy", "Me"));
var o = _C("{ 'mb' : 'Me' }");
Assert.Equal(o, i);
}
@ -147,8 +150,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact]
public void Should_make_query_with_created()
{
var i = F(ClrFilter.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value));
var o = C("{ 'ct' : ISODate('1988-01-19T12:00:00Z') }");
var 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);
}
@ -156,8 +159,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact]
public void Should_make_query_with_createdBy()
{
var i = F(ClrFilter.Eq("createdBy", "Me"));
var o = C("{ 'cb' : 'Me' }");
var i = _F(ClrFilter.Eq("createdBy", "Me"));
var o = _C("{ 'cb' : 'Me' }");
Assert.Equal(o, i);
}
@ -165,8 +168,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact]
public void Should_make_query_with_version()
{
var i = F(ClrFilter.Eq("version", 0L));
var o = C("{ 'vs' : NumberLong(0) }");
var i = _F(ClrFilter.Eq("version", 0L));
var o = _C("{ 'vs' : NumberLong(0) }");
Assert.Equal(o, i);
}
@ -174,8 +177,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[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)] } }");
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);
}
@ -183,8 +186,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact]
public void Should_make_query_with_empty_test()
{
var i = F(ClrFilter.Empty("data/firstName/iv"));
var o = C("{ '$or' : [{ 'do.1.iv' : { '$exists' : false } }, { 'do.1.iv' : null }, { 'do.1.iv' : '' }, { 'do.1.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' : [] }] }");
Assert.Equal(o, i);
}
@ -192,8 +195,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact]
public void Should_make_query_with_datetime_data()
{
var i = F(ClrFilter.Eq("data/birthday/iv", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value));
var o = C("{ 'do.5.iv' : '1988-01-19T12:00:00Z' }");
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' }");
Assert.Equal(o, i);
}
@ -201,8 +204,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact]
public void Should_make_query_with_underscore_field()
{
var i = F(ClrFilter.Eq("data/dashed_field/iv", "Value"));
var o = C("{ 'do.8.iv' : 'Value' }");
var i = _F(ClrFilter.Eq("data/dashed_field/iv", "Value"));
var o = _C("{ 'do.8.iv' : 'Value' }");
Assert.Equal(o, i);
}
@ -210,8 +213,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact]
public void Should_make_query_with_references_equals()
{
var i = F(ClrFilter.Eq("data/friends/iv", "guid"));
var o = C("{ 'do.7.iv' : 'guid' }");
var i = _F(ClrFilter.Eq("data/friends/iv", "guid"));
var o = _C("{ 'do.7.iv' : 'guid' }");
Assert.Equal(o, i);
}
@ -219,8 +222,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact]
public void Should_make_query_with_array_field()
{
var i = F(ClrFilter.Eq("data/hobbies/iv/name", "PC"));
var o = C("{ 'do.9.iv.91' : 'PC' }");
var i = _F(ClrFilter.Eq("data/hobbies/iv/name", "PC"));
var o = _C("{ 'do.9.iv.91' : 'PC' }");
Assert.Equal(o, i);
}
@ -228,8 +231,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact]
public void Should_make_query_with_assets_equals()
{
var i = F(ClrFilter.Eq("data/pictures/iv", "guid"));
var o = C("{ 'do.6.iv' : 'guid' }");
var i = _F(ClrFilter.Eq("data/pictures/iv", "guid"));
var o = _C("{ 'do.6.iv' : 'guid' }");
Assert.Equal(o, i);
}
@ -237,8 +240,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[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' } }");
var i = _Q(new ClrQuery { FullText = "Hello my World" });
var o = _C("{ '$text' : { '$search' : 'Hello my World' } }");
Assert.Equal(o, i);
}
@ -246,8 +249,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact]
public void Should_make_orderby_with_single_field()
{
var i = S(SortBuilder.Descending("data/age/iv"));
var o = C("{ 'do.4.iv' : -1 }");
var i = _S(SortBuilder.Descending("data/age/iv"));
var o = _C("{ 'do.4.iv' : -1 }");
Assert.Equal(o, i);
}
@ -255,8 +258,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact]
public void Should_make_orderby_with_multiple_fields()
{
var i = S(SortBuilder.Ascending("data/age/iv"), SortBuilder.Descending("data/firstName/en"));
var o = C("{ 'do.4.iv' : 1, 'do.1.en' : -1 }");
var i = _S(SortBuilder.Ascending("data/age/iv"), SortBuilder.Descending("data/firstName/en"));
var o = _C("{ 'do.4.iv' : 1, 'do.1.en' : -1 }");
Assert.Equal(o, i);
}
@ -285,17 +288,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
.MustHaveHappened();
}
private static string C(string value)
private static string _C(string value)
{
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>>();
@ -312,7 +315,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
return i;
}
private string Q(ClrQuery query)
private string _Q(ClrQuery query)
{
var rendered =
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.Threading.Tasks;
using Esprima.Ast;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core;
@ -46,63 +49,63 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
}
[Fact]
public void Should_use_existing_query()
public async Task Should_use_existing_query()
{
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);
}
[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");
Assert.Throws<ValidationException>(() => sut.ParseQuery(requestContext, schema, query));
await Assert.ThrowsAsync<ValidationException>(() => sut.ParseQueryAsync(requestContext, schema, query).AsTask());
}
[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");
Assert.Throws<ValidationException>(() => sut.ParseQuery(requestContext, schema, query));
await Assert.ThrowsAsync<ValidationException>(() => sut.ParseQueryAsync(requestContext, schema, query).AsTask());
}
[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 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());
}
[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 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());
}
[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 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());
}
[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(
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"))
});
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());
}
[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 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());
}
[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(
new Query<IJsonValue>
@ -134,27 +137,27 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
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());
}
[Fact]
public void Should_apply_default_page_size()
public async Task Should_apply_default_page_size()
{
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());
}
[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 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());
}

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))
.Returns(schema);
A.CallTo(() => queryParser.ParseQuery(A<Context>._, schema, A<Q>._))
A.CallTo(() => queryParser.ParseQueryAsync(A<Context>._, schema, A<Q>._))
.Returns(new ClrQuery());
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 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());
}
@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
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());
}
@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
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());
@ -81,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
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());
@ -94,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
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());

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

Loading…
Cancel
Save