From 524c3aa4308a3ea0078949b7bfef583ee23309aa Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 5 Feb 2021 14:37:51 +0100 Subject: [PATCH] Feature/skip total (#636) * Skip total query to save calls to MongoDB. * Test fixes. --- .../Assets/MongoAssetRepository.cs | 12 +- .../Contents/MongoContentCollection.cs | 29 +++-- .../Contents/MongoContentRepository.cs | 6 +- .../Contents/Operations/OperationBase.cs | 7 +- .../Contents/Operations/QueryByIds.cs | 11 +- .../Contents/Operations/QueryByQuery.cs | 22 ++-- .../Assets/AssetExtensions.cs | 17 +-- .../Assets/AssetsSearchSource.cs | 2 +- .../Assets/Queries/AssetEnricher.cs | 7 +- .../Assets/Queries/AssetQueryParser.cs | 15 ++- .../Assets/Queries/AssetQueryService.cs | 2 +- .../Contents/BulkUpdateCommandMiddleware.cs | 7 +- ...textExtensions.cs => ContentExtensions.cs} | 74 ++++-------- .../Contents/ContentsSearchSource.cs | 2 +- .../GraphQL/GraphQLExecutionContext.cs | 2 +- .../GraphQL/Types/AppQueriesGraphType.cs | 2 +- .../GraphQL/Types/Assets/AssetActions.cs | 13 +- .../GraphQL/Types/Contents/ContentActions.cs | 26 +++- .../Types/Contents/ContentGraphType.cs | 2 +- .../Contents/GraphQL/Types/SharedTypes.cs | 2 +- .../Contents/Queries/ContentQueryParser.cs | 11 +- .../Contents/Queries/ContentQueryService.cs | 4 +- .../Contents/Queries/QueryExecutionContext.cs | 27 +---- .../Contents/Queries/Steps/ConvertData.cs | 2 +- .../Contents/Queries/Steps/ResolveAssets.cs | 8 +- .../Queries/Steps/ResolveReferences.cs | 8 +- .../Contents/ReferencesFluidExtension.cs | 11 +- .../Squidex.Domain.Apps.Entities/Context.cs | 84 +++++++++---- .../ContextExtensions.cs | 55 +++++++++ backend/src/Squidex.Domain.Apps.Entities/Q.cs | 11 +- .../src/Squidex.Web/ApiPermissionAttribute.cs | 2 +- backend/src/Squidex.Web/ContextExtensions.cs | 2 +- backend/src/Squidex.Web/ContextProvider.cs | 2 +- .../src/Squidex.Web/Pipeline/AppResolver.cs | 20 +-- backend/src/Squidex.Web/Resources.cs | 4 +- .../Api/Controllers/Apps/AppsController.cs | 2 +- .../Areas/Api/Controllers/UI/UIController.cs | 2 +- .../Squidex/Config/Domain/StoreServices.cs | 2 +- .../DomainObject/AppCommandMiddlewareTests.cs | 4 +- .../AssetCommandMiddlewareTests.cs | 4 +- .../Assets/Queries/AssetEnricherTests.cs | 7 +- .../Assets/Queries/AssetQueryParserTests.cs | 32 +++-- .../Assets/Queries/AssetQueryServiceTests.cs | 2 +- .../BulkUpdateCommandMiddlewareTests.cs | 21 +++- .../ContentCommandMiddlewareTests.cs | 4 +- .../Contents/GraphQL/GraphQLQueriesTests.cs | 63 ++++++---- .../Contents/MongoDb/ContentsQueryFixture.cs | 114 ++++++++++-------- .../Contents/MongoDb/ContentsQueryTests.cs | 98 +++++++++------ .../Queries/ContentQueryParserTests.cs | 8 ++ .../Queries/ContentQueryServiceTests.cs | 12 +- .../Queries/EnrichWithWorkflowsTests.cs | 8 +- .../Contents/Queries/ResolveAssetsTests.cs | 11 +- .../Queries/ResolveReferencesTests.cs | 13 +- .../RuleCommandMiddlewareTests.cs | 4 +- .../Rules/Queries/RuleEnricherTests.cs | 5 +- .../Rules/Queries/RuleQueryServiceTests.cs | 4 +- .../Search/SearchManagerTests.cs | 4 +- .../TestHelpers/AExtensions.cs | 9 +- .../ApiPermissionAttributeTests.cs | 4 +- .../EnrichWithAppIdCommandMiddlewareTests.cs | 15 +-- .../Pipeline/ApiCostsFilterTests.cs | 3 +- .../Squidex.Web.Tests.csproj | 2 +- 62 files changed, 589 insertions(+), 379 deletions(-) rename backend/src/Squidex.Domain.Apps.Entities/Contents/{ContextExtensions.cs => ContentExtensions.cs} (62%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/ContextExtensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index 729994f66..7926ab6be 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -97,7 +97,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets .ToListAsync(); long assetTotal = assetEntities.Count; - if (assetTotal >= q.Query.Take || q.Query.Skip > 0) + if (q.NoTotal) + { + assetTotal = -1; + } + else if (assetEntities.Count >= q.Query.Take || q.Query.Skip > 0) { assetTotal = await Collection.Find(filter).CountDocumentsAsync(); } @@ -118,7 +122,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets .ToListAsync(); long assetTotal = assetEntities.Count; - if (assetTotal >= q.Query.Take || q.Query.Skip > 0) + if (q.NoTotal) + { + assetTotal = -1; + } + else if (assetEntities.Count >= q.Query.Take || q.Query.Skip > 0) { assetTotal = await Collection.Find(filter).CountDocumentsAsync(); } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs index 6943e1f39..a57a3593d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs @@ -33,8 +33,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents private readonly QueryReferrers queryReferrers; private readonly QueryScheduled queryScheduled; private readonly string name; + private readonly bool useWildcardIndex; - public MongoContentCollection(string name, IMongoDatabase database, IAppProvider appProvider) + public MongoContentCollection(string name, IMongoDatabase database, IAppProvider appProvider, bool useWildcardIndex) : base(database) { this.name = name; @@ -46,6 +47,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents queryReferences = new QueryReferences(queryByIds); queryReferrers = new QueryReferrers(); queryScheduled = new QueryScheduled(); + + this.useWildcardIndex = useWildcardIndex; } public IMongoCollection GetInternalCollection() @@ -60,13 +63,23 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents protected override async Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) { - await queryAsStream.PrepareAsync(collection, ct); - await queryBdId.PrepareAsync(collection, ct); - await queryByIds.PrepareAsync(collection, ct); - await queryByQuery.PrepareAsync(collection, ct); - await queryReferences.PrepareAsync(collection, ct); - await queryReferrers.PrepareAsync(collection, ct); - await queryScheduled.PrepareAsync(collection, ct); + if (useWildcardIndex) + { + await collection.Indexes.CreateOneAsync( + new CreateIndexModel( + Index.Wildcard() + ), null, ct); + } + + var skipIndex = useWildcardIndex; + + await queryAsStream.PrepareAsync(collection, skipIndex, ct); + await queryBdId.PrepareAsync(collection, skipIndex, ct); + await queryByIds.PrepareAsync(collection, skipIndex, ct); + await queryByQuery.PrepareAsync(collection, skipIndex, ct); + await queryReferences.PrepareAsync(collection, skipIndex, ct); + await queryReferrers.PrepareAsync(collection, skipIndex, ct); + await queryScheduled.PrepareAsync(collection, skipIndex, ct); } public IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds) diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 764a4c7c9..96cb519ac 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -32,17 +32,17 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents StatusSerializer.Register(); } - public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider) + public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider, bool useWildcardIndex) { Guard.NotNull(appProvider, nameof(appProvider)); collectionAll = new MongoContentCollection( - "States_Contents_All3", database, appProvider); + "States_Contents_All3", database, appProvider, useWildcardIndex); collectionPublished = new MongoContentCollection( - "States_Contents_Published3", database, appProvider); + "States_Contents_Published3", database, appProvider, useWildcardIndex); } public async Task InitializeAsync(CancellationToken ct = default) diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs index edac8aac1..aa063929d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs @@ -21,11 +21,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations public IMongoCollection Collection { get; private set; } - public Task PrepareAsync(IMongoCollection collection, CancellationToken ct = default) + public async Task PrepareAsync(IMongoCollection collection, bool skipIndex, CancellationToken ct = default) { Collection = collection; - return PrepareAsync(ct); + if (!skipIndex) + { + await PrepareAsync(ct); + } } protected virtual Task PrepareAsync(CancellationToken ct = default) diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs index d8727dec3..c27328c03 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs @@ -48,12 +48,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations var contentEntities = await FindContentsAsync(q.Query, filter); var contentTotal = (long)contentEntities.Count; - if (contentEntities.Count > 0) + if (q.NoTotal) { - if (contentTotal >= q.Query.Take || q.Query.Skip > 0) - { - contentTotal = await Collection.Find(filter).CountDocumentsAsync(); - } + contentTotal = -1; + } + else if (contentTotal >= q.Query.Take || q.Query.Skip > 0) + { + contentTotal = await Collection.Find(filter).CountDocumentsAsync(); } return ResultList.Create(contentTotal, contentEntities); diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs index a3e060e3d..56eb86ebe 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs @@ -105,12 +105,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations var contentEntities = await FindContentsAsync(query, filter); var contentTotal = (long)contentEntities.Count; - if (contentEntities.Count > 0) + if (q.NoTotal) { - if (contentTotal >= q.Query.Take || q.Query.Skip > 0) - { - contentTotal = await Collection.Find(filter).CountDocumentsAsync(); - } + contentTotal = -1; + } + else if (contentTotal >= q.Query.Take || q.Query.Skip > 0) + { + contentTotal = await Collection.Find(filter).CountDocumentsAsync(); } return ResultList.Create(contentTotal, contentEntities); @@ -140,12 +141,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations var contentEntities = await FindContentsAsync(query, filter); var contentTotal = (long)contentEntities.Count; - if (contentEntities.Count > 0) + if (q.NoTotal) + { + contentTotal = -1; + } + else if (contentTotal >= q.Query.Take || q.Query.Skip > 0) { - if (contentTotal >= q.Query.Take || q.Query.Skip > 0) - { - contentTotal = await Collection.Find(filter).CountDocumentsAsync(); - } + contentTotal = await Collection.Find(filter).CountDocumentsAsync(); } return ResultList.Create(contentTotal, contentEntities); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs index 1f4137b04..44feae209 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs @@ -11,23 +11,14 @@ namespace Squidex.Domain.Apps.Entities.Assets { private const string HeaderNoEnrichment = "X-NoAssetEnrichment"; - public static bool ShouldEnrichAsset(this Context context) + public static bool ShouldSkipAssetEnrichment(this Context context) { - return !context.Headers.ContainsKey(HeaderNoEnrichment); + return context.Headers.ContainsKey(HeaderNoEnrichment); } - public static Context WithoutAssetEnrichment(this Context context, bool value = true) + public static ICloneBuilder WithoutAssetEnrichment(this ICloneBuilder builder, bool value = true) { - if (value) - { - context.Headers[HeaderNoEnrichment] = "1"; - } - else - { - context.Headers.Remove(HeaderNoEnrichment); - } - - return context; + return builder.WithBoolean(HeaderNoEnrichment, value); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsSearchSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsSearchSource.cs index 2c8765fcc..e998cff68 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsSearchSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsSearchSource.cs @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { var result = new SearchResults(); - if (context.Permissions.Allows(Permissions.AppAssetsRead, context.App.Name)) + if (context.UserPermissions.Allows(Permissions.AppAssetsRead, context.App.Name)) { var filter = ClrFilter.Contains("fileName", query); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs index 12e5257fd..1a1a64e2b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs @@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries requestCache.AddDependency(asset.UniqueId, asset.Version); } - if (ShouldEnrich(context)) + if (!context.ShouldSkipAssetEnrichment()) { await EnrichTagsAsync(results); @@ -134,10 +134,5 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries return await tagService.DenormalizeTagsAsync(group.Key, TagGroups.Assets, uniqueIds); } - - private static bool ShouldEnrich(Context context) - { - return context.ShouldEnrichAsset(); - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs index cbeeb93e8..6f9753af2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs @@ -46,25 +46,32 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries this.options = options.Value; } - public virtual async Task ParseQueryAsync(Context context, Q q) + public virtual async Task ParseAsync(Context context, Q q) { Guard.NotNull(context, nameof(context)); Guard.NotNull(q, nameof(q)); using (Profiler.TraceMethod()) { - var query = ParseQuery(q); + var query = ParseClrQuery(q); await TransformTagAsync(context, query); WithSorting(query); WithPaging(query); - return q!.WithQuery(query); + q = q.WithQuery(query); + + if (context.ShouldSkipTotal()) + { + q = q.WithoutTotal(); + } + + return q; } } - private ClrQuery ParseQuery(Q q) + private ClrQuery ParseClrQuery(Q q) { var query = q.Query; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs index 3919afd6b..d14933d30 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs @@ -99,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries Guard.NotNull(context, nameof(context)); Guard.NotNull(q, nameof(q)); - q = await queryParser.ParseQueryAsync(context, q); + q = await queryParser.ParseAsync(context, q); var assets = await assetRepository.QueryAsync(context.App.Id, parentId, q); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs index 3aed7b853..6d6ff323f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs @@ -80,7 +80,12 @@ namespace Squidex.Domain.Apps.Entities.Contents PropagateCompletion = true }); - var requestContext = contextProvider.Context.WithoutContentEnrichment().WithUnpublished(true); + var requestContext = contextProvider.Context.Clone(b => b + .WithoutContentEnrichment() + .WithoutCleanup() + .WithUnpublished(true) + .WithoutTotal()); + var requestedSchema = bulkUpdates.SchemaId.Name; var results = new ConcurrentBag(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContextExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentExtensions.cs similarity index 62% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/ContextExtensions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/ContentExtensions.cs index 64b521929..c5e0e75dd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContextExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentExtensions.cs @@ -15,7 +15,7 @@ using Squidex.Infrastructure.Caching; namespace Squidex.Domain.Apps.Entities.Contents { - public static class ContextExtensions + public static class ContentExtensions { private const string HeaderFlatten = "X-Flatten"; private const string HeaderLanguages = "X-Languages"; @@ -44,24 +44,24 @@ namespace Squidex.Domain.Apps.Entities.Contents return context.ShouldProvideUnpublished() || context.IsFrontendClient ? SearchScope.All : SearchScope.Published; } - public static bool ShouldCleanup(this Context context) + public static bool ShouldSkipCleanup(this Context context) { - return !context.Headers.ContainsKey(HeaderNoCleanup); + return context.Headers.ContainsKey(HeaderNoCleanup); } - public static Context WithoutCleanup(this Context context, bool value = true) + public static ICloneBuilder WithoutCleanup(this ICloneBuilder builder, bool value = true) { - return SetBoolean(context, HeaderNoCleanup, value); + return builder.WithBoolean(HeaderNoCleanup, value); } - public static bool ShouldEnrichContent(this Context context) + public static bool ShouldSkipContentEnrichment(this Context context) { - return !context.Headers.ContainsKey(HeaderNoEnrichment); + return context.Headers.ContainsKey(HeaderNoEnrichment); } - public static Context WithoutContentEnrichment(this Context context, bool value = true) + public static ICloneBuilder WithoutContentEnrichment(this ICloneBuilder builder, bool value = true) { - return SetBoolean(context, HeaderNoEnrichment, value); + return builder.WithBoolean(HeaderNoEnrichment, value); } public static bool ShouldProvideUnpublished(this Context context) @@ -69,9 +69,9 @@ namespace Squidex.Domain.Apps.Entities.Contents return context.Headers.ContainsKey(HeaderUnpublished); } - public static Context WithUnpublished(this Context context, bool value = true) + public static ICloneBuilder WithUnpublished(this ICloneBuilder builder, bool value = true) { - return SetBoolean(context, HeaderUnpublished, value); + return builder.WithBoolean(HeaderUnpublished, value); } public static bool ShouldFlatten(this Context context) @@ -79,9 +79,9 @@ namespace Squidex.Domain.Apps.Entities.Contents return context.Headers.ContainsKey(HeaderFlatten); } - public static Context WithFlatten(this Context context, bool value = true) + public static ICloneBuilder WithFlatten(this ICloneBuilder builder, bool value = true) { - return SetBoolean(context, HeaderFlatten, value); + return builder.WithBoolean(HeaderFlatten, value); } public static bool ShouldResolveFlow(this Context context) @@ -89,9 +89,9 @@ namespace Squidex.Domain.Apps.Entities.Contents return context.Headers.ContainsKey(HeaderResolveFlow); } - public static Context WithResolveFlow(this Context context, bool value = true) + public static ICloneBuilder WithResolveFlow(this ICloneBuilder builder, bool value = true) { - return SetBoolean(context, HeaderResolveFlow, value); + return builder.WithBoolean(HeaderResolveFlow, value); } public static bool ShouldResolveLanguages(this Context context) @@ -99,9 +99,9 @@ namespace Squidex.Domain.Apps.Entities.Contents return !context.Headers.ContainsKey(HeaderNoResolveLanguages); } - public static Context WithoutResolveLanguages(this Context context, bool value = true) + public static ICloneBuilder WithoutResolveLanguages(this ICloneBuilder builder, bool value = true) { - return SetBoolean(context, HeaderNoResolveLanguages, value); + return builder.WithBoolean(HeaderNoResolveLanguages, value); } public static IEnumerable AssetUrls(this Context context) @@ -114,18 +114,9 @@ namespace Squidex.Domain.Apps.Entities.Contents return Enumerable.Empty(); } - public static Context WithAssetUrlsToResolve(this Context context, IEnumerable fieldNames) + public static ICloneBuilder WithAssetUrlsToResolve(this ICloneBuilder builder, IEnumerable? fieldNames) { - if (fieldNames?.Any() == true) - { - context.Headers[HeaderResolveUrls] = string.Join(",", fieldNames); - } - else - { - context.Headers.Remove(HeaderResolveUrls); - } - - return context; + return builder.WithStrings(HeaderResolveUrls, fieldNames); } public static IEnumerable Languages(this Context context) @@ -148,32 +139,9 @@ namespace Squidex.Domain.Apps.Entities.Contents return Enumerable.Empty(); } - public static Context WithLanguages(this Context context, IEnumerable fieldNames) - { - if (fieldNames?.Any() == true) - { - context.Headers[HeaderLanguages] = string.Join(",", fieldNames); - } - else - { - context.Headers.Remove(HeaderLanguages); - } - - return context; - } - - private static Context SetBoolean(Context context, string key, bool value) + public static ICloneBuilder WithLanguages(this ICloneBuilder builder, IEnumerable fieldNames) { - if (value) - { - context.Headers[key] = "1"; - } - else - { - context.Headers.Remove(key); - } - - return context; + return builder.WithStrings(HeaderLanguages, fieldNames); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs index 8f37de45f..57d93e163 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs @@ -105,7 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Contents private static bool HasPermission(Context context, string schemaName) { - return context.Permissions.Allows(Permissions.AppContentsReadOwn, context.App.Name, schemaName); + return context.UserPermissions.Allows(Permissions.AppContentsReadOwn, context.App.Name, schemaName); } private static string FormatName(IEnrichedContentEntity content, string masterLanguage) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs index d66dc9370..99d99ca2e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs @@ -64,7 +64,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL public GraphQLExecutionContext WithContext(Context newContext) { - context = newContext.WithoutCleanup().WithoutContentEnrichment(); + context = newContext.Clone(b => b.WithoutCleanup().WithoutContentEnrichment()); return this; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs index 541528b82..f0d120f74 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Name = $"query{schemaInfo.TypeName}ContentsWithTotal", Arguments = ContentActions.QueryOrReferencing.Arguments, ResolvedType = resultType, - Resolver = ContentActions.QueryOrReferencing.Query, + Resolver = ContentActions.QueryOrReferencing.QueryWithTotal, Description = $"Query {schemaInfo.DisplayName} content items with total count." }).WithSchemaId(schemaInfo); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs index 10d0a25d7..c24fc00f4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs @@ -100,7 +100,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets { var query = fieldContext.BuildODataQuery(); - return await context.QueryAssetsAsync(query); + var q = Q.Empty.WithODataQuery(query).WithoutTotal(); + + return await context.QueryAssetsAsync(q); + }); + + public static readonly IFieldResolver ResolverWithTotal = Resolvers.Async(async (_, fieldContext, context) => + { + var query = fieldContext.BuildODataQuery(); + + var q = Q.Empty.WithODataQuery(query); + + return await context.QueryAssetsAsync(q); }); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs index 5ceba1de1..d951b2062 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs @@ -142,14 +142,36 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents { var query = fieldContext.BuildODataQuery(); - return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), query); + var q = Q.Empty.WithODataQuery(query).WithoutTotal(); + + return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q); + }); + + public static readonly IFieldResolver QueryWithTotal = Resolvers.Async(async (_, fieldContext, context) => + { + var query = fieldContext.BuildODataQuery(); + + var q = Q.Empty.WithODataQuery(query); + + return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q); }); public static readonly IFieldResolver Referencing = Resolvers.Async(async (source, fieldContext, context) => { var query = fieldContext.BuildODataQuery(); - return await context.QueryReferencingContentsAsync(fieldContext.FieldDefinition.SchemaId(), query, source.Id); + var q = Q.Empty.WithODataQuery(query).WithReference(source.Id).WithoutTotal(); + + return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q); + }); + + public static readonly IFieldResolver ReferencingWithTotal = Resolvers.Async(async (source, fieldContext, context) => + { + var query = fieldContext.BuildODataQuery(); + + var q = Q.Empty.WithODataQuery(query).WithReference(source.Id); + + return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q); }); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs index 73e161606..d52935541 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs @@ -106,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents Name = $"referencing{referencingSchemaInfo.TypeName}ContentsWithTotal", Arguments = ContentActions.QueryOrReferencing.Arguments, ResolvedType = contentResultsTyp, - Resolver = ContentActions.QueryOrReferencing.Referencing, + Resolver = ContentActions.QueryOrReferencing.ReferencingWithTotal, Description = $"Query {referencingSchemaInfo.DisplayName} content items with total count." }).WithSchemaId(referencingSchemaInfo); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedTypes.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedTypes.cs index 63ea01a54..b739213df 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedTypes.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedTypes.cs @@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Name = "queryAssetsWithTotal", Arguments = AssetActions.Query.Arguments, ResolvedType = AssetsResult, - Resolver = AssetActions.Query.Resolver, + Resolver = AssetActions.Query.ResolverWithTotal, Description = "Get assets and total count." }; }); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs index 850bcf187..e77e4f178 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs @@ -63,14 +63,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries using (Profiler.TraceMethod()) { - var query = ParseQuery(context, q, schema); + var query = ParseClrQuery(context, q, schema); await TransformFilterAsync(query, context, schema); WithSorting(query); WithPaging(query); - q = q!.WithQuery(query); + q = q.WithQuery(query); + + if (context.ShouldSkipTotal()) + { + q = q.WithoutTotal(); + } return q; } @@ -113,7 +118,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries } } - private ClrQuery ParseQuery(Context context, Q q, ISchemaEntity? schema) + private ClrQuery ParseClrQuery(Context context, Q q, ISchemaEntity? schema) { var query = q.Query; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs index ae554b021..6f3b00b38 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs @@ -95,7 +95,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries if (!HasPermission(context, schema, Permissions.AppContentsRead)) { - q.CreatedBy = context.User.Token(); + q = q with { CreatedBy = context.User.Token() }; } using (Profiler.TraceMethod()) @@ -216,7 +216,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries private static bool HasPermission(Context context, ISchemaEntity schema, string permissionId) { - return context.Permissions.Allows(permissionId, context.App.Name, schema.SchemaDef.Name); + return context.UserPermissions.Allows(permissionId, context.App.Name, schema.SchemaDef.Name); } private Task FindCoreAsync(Context context, DomainId id, ISchemaEntity schema) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs index 6e37f1522..1aff1b53e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs @@ -39,10 +39,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return contentQuery.FindAsync(Context, schemaIdOrName, id, version); } - public virtual async Task> QueryAssetsAsync(string odata) + public virtual async Task> QueryAssetsAsync(Q q) { - var q = Q.Empty.WithODataQuery(odata); - IResultList assets; await maxRequests.WaitAsync(); @@ -63,10 +61,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return assets; } - public virtual async Task> QueryContentsAsync(string schemaIdOrName, string odata) + public virtual async Task> QueryContentsAsync(string schemaIdOrName, Q q) { - var q = Q.Empty.WithODataQuery(odata); - IResultList contents; await maxRequests.WaitAsync(); @@ -100,7 +96,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries await maxRequests.WaitAsync(); try { - assets = await assetQuery.QueryAsync(Context, null, Q.Empty.WithIds(notLoadedAssets)); + assets = await assetQuery.QueryAsync(Context, null, Q.Empty.WithIds(notLoadedAssets).WithoutTotal()); } finally { @@ -129,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries await maxRequests.WaitAsync(); try { - contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(notLoadedContents)); + contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(notLoadedContents).WithoutTotal()); } finally { @@ -144,20 +140,5 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return ids.Select(cachedContents.GetOrDefault).NotNull().ToList(); } - - public async Task> QueryReferencingContentsAsync(string schemaIdOrName, string odata, DomainId reference) - { - var q = Q.Empty.WithODataQuery(odata).WithReference(reference); - - await maxRequests.WaitAsync(); - try - { - return await contentQuery.QueryAsync(Context, schemaIdOrName, q); - } - finally - { - maxRequests.Release(); - } - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs index c1fdcb579..ef8593684 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs @@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps private async Task CleanReferencesAsync(Context context, IEnumerable contents, ProvideSchema schemas) { - if (context.ShouldCleanup()) + if (!context.ShouldSkipCleanup()) { var ids = new HashSet(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs index d6f4e2e00..e40ff3dc6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs @@ -118,7 +118,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps return EmptyAssets; } - var assets = await assetQuery.QueryAsync(context.Clone().WithoutAssetEnrichment(true), null, Q.Empty.WithIds(ids)); + var queryContext = context.Clone(b => b + .WithoutAssetEnrichment(true) + .WithoutTotal()); + + var assets = await assetQuery.QueryAsync(queryContext, null, Q.Empty.WithIds(ids)); return assets.ToLookup(x => x.Id); } @@ -133,7 +137,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps private static bool ShouldEnrich(Context context) { - return context.IsFrontendClient && context.ShouldEnrichContent(); + return context.IsFrontendClient && !context.ShouldSkipContentEnrichment(); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs index 3c8e3893f..41d680e68 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs @@ -153,14 +153,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps return EmptyContents; } - var references = await ContentQuery.QueryAsync(context.Clone().WithoutContentEnrichment(true), Q.Empty.WithIds(ids)); + var queryContext = context.Clone(b => b + .WithoutContentEnrichment(true) + .WithoutTotal()); + + var references = await ContentQuery.QueryAsync(queryContext, Q.Empty.WithIds(ids)); return references.ToLookup(x => x.Id); } private static bool ShouldEnrich(Context context) { - return context.IsFrontendClient && context.ShouldEnrichContent(); + return context.IsFrontendClient && !context.ShouldSkipContentEnrichment(); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs index cfda02e8a..6c77233c5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs @@ -49,13 +49,10 @@ namespace Squidex.Domain.Apps.Entities.Contents return Completion.Normal; } - var appContext = - Context.Admin() - .WithoutContentEnrichment() - .WithoutCleanup() - .WithUnpublished(); - - appContext.App = app; + var appContext = Context.Admin(app).Clone(b => b + .WithoutContentEnrichment() + .WithoutCleanup() + .WithUnpublished()); var id = (await arguments[1].Expression.EvaluateAsync(context)).ToStringValue(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Context.cs b/backend/src/Squidex.Domain.Apps.Entities/Context.cs index 04aed9c69..496b8b363 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Context.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Context.cs @@ -20,66 +20,104 @@ namespace Squidex.Domain.Apps.Entities { public sealed class Context { - public IDictionary Headers { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly IReadOnlyDictionary EmptyHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - public IAppEntity App { get; set; } + public IReadOnlyDictionary Headers { get; } + + public ClaimsPermissions UserPermissions { get; } public ClaimsPrincipal User { get; } - public ClaimsPermissions Permissions { get; } + public IAppEntity App { get; set; } - public bool IsFrontendClient { get; } + public bool IsFrontendClient => User.IsInClient(DefaultClients.Frontend); - public Context(ClaimsPrincipal user) + public Context(ClaimsPrincipal user, IAppEntity app) + : this(app, user, user.Claims.Permissions(), EmptyHeaders) { Guard.NotNull(user, nameof(user)); - - User = user; - - Permissions = User.Claims.Permissions(); - - IsFrontendClient = User.IsInClient(DefaultClients.Frontend); } - public Context(ClaimsPrincipal user, IAppEntity app) - : this(user) + private Context(IAppEntity app, ClaimsPrincipal user, ClaimsPermissions userPermissions, IReadOnlyDictionary headers) { App = app; + + User = user; + UserPermissions = userPermissions; + + Headers = headers; } - public static Context Anonymous() + public static Context Anonymous(IAppEntity app) { var claimsIdentity = new ClaimsIdentity(); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - return new Context(claimsPrincipal); + return new Context(claimsPrincipal, app); } - public static Context Admin() + public static Context Admin(IAppEntity app) { var claimsIdentity = new ClaimsIdentity(); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, P.All)); - return new Context(claimsPrincipal); + return new Context(claimsPrincipal, app); } public bool Allows(string permissionId, string schema = Permission.Any) { - return Permissions.Allows(permissionId, App.Name, schema); + return UserPermissions.Allows(permissionId, App.Name, schema); } - public Context Clone() + private sealed class HeaderBuilder : ICloneBuilder { - var clone = new Context(User, App); + private readonly Context context; + private Dictionary? headers; + + public HeaderBuilder(Context context) + { + this.context = context; + } - foreach (var (key, value) in Headers) + public Context Build() { - clone.Headers[key] = value; + if (headers != null) + { + return new Context(context.App!, context.User, context.UserPermissions, headers); + } + + return context; + } + + public void Remove(string key) + { + headers ??= new Dictionary(context.Headers, StringComparer.OrdinalIgnoreCase); + headers.Remove(key); } - return clone; + public void SetHeader(string key, string value) + { + headers ??= new Dictionary(context.Headers, StringComparer.OrdinalIgnoreCase); + headers[key] = value; + } } + + public Context Clone(Action action) + { + var builder = new HeaderBuilder(this); + + action(builder); + + return builder.Build(); + } + } + + public interface ICloneBuilder + { + void SetHeader(string key, string value); + + void Remove(string key); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/ContextExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/ContextExtensions.cs new file mode 100644 index 000000000..6d41975aa --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/ContextExtensions.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; + +namespace Squidex.Domain.Apps.Entities +{ + public static class ContextExtensions + { + private const string HeaderNoTotal = "X-NoTotal"; + + public static bool ShouldSkipTotal(this Context context) + { + return context.Headers.ContainsKey(HeaderNoTotal); + } + + public static ICloneBuilder WithoutTotal(this ICloneBuilder builder, bool value = true) + { + return builder.WithBoolean(HeaderNoTotal, value); + } + + public static ICloneBuilder WithBoolean(this ICloneBuilder builder, string key, bool value) + { + if (value) + { + builder.SetHeader(key, "1"); + } + else + { + builder.Remove(key); + } + + return builder; + } + + public static ICloneBuilder WithStrings(this ICloneBuilder builder, string key, IEnumerable? values) + { + if (values?.Any() == true) + { + builder.SetHeader(key, string.Join(",", values)); + } + else + { + builder.Remove(key); + } + + return builder; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Q.cs b/backend/src/Squidex.Domain.Apps.Entities/Q.cs index e1909156d..54b920bd4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Q.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Q.cs @@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Entities { public static Q Empty => new Q(); + public ClrQuery Query { get; init; } = new ClrQuery(); + public IReadOnlyList? Ids { get; init; } public DomainId Referencing { get; init; } @@ -29,9 +31,9 @@ namespace Squidex.Domain.Apps.Entities public Query? JsonQuery { get; init; } - public ClrQuery Query { get; init; } = new ClrQuery(); + public RefToken? CreatedBy { get; init; } - public RefToken? CreatedBy { get; set; } + public bool NoTotal { get; init; } private Q() { @@ -44,6 +46,11 @@ namespace Squidex.Domain.Apps.Entities return this with { Query = query }; } + public Q WithoutTotal(bool value = true) + { + return this with { NoTotal = value }; + } + public Q WithODataQuery(string? query) { return this with { ODataQuery = query }; diff --git a/backend/src/Squidex.Web/ApiPermissionAttribute.cs b/backend/src/Squidex.Web/ApiPermissionAttribute.cs index e486b335c..6198b6132 100644 --- a/backend/src/Squidex.Web/ApiPermissionAttribute.cs +++ b/backend/src/Squidex.Web/ApiPermissionAttribute.cs @@ -35,7 +35,7 @@ namespace Squidex.Web { if (permissionIds.Length > 0) { - var permissions = context.HttpContext.Context().Permissions; + var permissions = context.HttpContext.Context().UserPermissions; var hasPermission = false; diff --git a/backend/src/Squidex.Web/ContextExtensions.cs b/backend/src/Squidex.Web/ContextExtensions.cs index e562f76a9..d8c2d2b86 100644 --- a/backend/src/Squidex.Web/ContextExtensions.cs +++ b/backend/src/Squidex.Web/ContextExtensions.cs @@ -18,7 +18,7 @@ namespace Squidex.Web if (context == null) { - context = RequestContext.Anonymous(); + context = RequestContext.Anonymous(null); httpContext.Features.Set(context); } diff --git a/backend/src/Squidex.Web/ContextProvider.cs b/backend/src/Squidex.Web/ContextProvider.cs index 6d960b7eb..801010ac9 100644 --- a/backend/src/Squidex.Web/ContextProvider.cs +++ b/backend/src/Squidex.Web/ContextProvider.cs @@ -25,7 +25,7 @@ namespace Squidex.Web { if (asyncLocal.Value == null) { - asyncLocal.Value = Context.Anonymous(); + asyncLocal.Value = Context.Anonymous(null!); } return asyncLocal.Value; diff --git a/backend/src/Squidex.Web/Pipeline/AppResolver.cs b/backend/src/Squidex.Web/Pipeline/AppResolver.cs index 3ab09742f..d634fdb7e 100644 --- a/backend/src/Squidex.Web/Pipeline/AppResolver.cs +++ b/backend/src/Squidex.Web/Pipeline/AppResolver.cs @@ -123,15 +123,17 @@ namespace Squidex.Web.Pipeline private static Context SetContext(HttpContext httpContext, IAppEntity app) { - var requestContext = new Context(httpContext.User, app); - - foreach (var (key, value) in httpContext.Request.Headers) - { - if (key.StartsWith("X-", StringComparison.OrdinalIgnoreCase)) + var requestContext = + new Context(httpContext.User, app).Clone(builder => { - requestContext.Headers.Add(key, value.ToString()); - } - } + foreach (var (key, value) in httpContext.Request.Headers) + { + if (key.StartsWith("X-", StringComparison.OrdinalIgnoreCase)) + { + builder.SetHeader(key, value.ToString()); + } + } + }); httpContext.Features.Set(requestContext); @@ -140,7 +142,7 @@ namespace Squidex.Web.Pipeline private static bool HasPermission(string appName, Context requestContext) { - return requestContext.Permissions.Includes(Permissions.ForApp(Permissions.App, appName)); + return requestContext.UserPermissions.Includes(Permissions.ForApp(Permissions.App, appName)); } private static bool AllowAnonymous(ActionExecutingContext context) diff --git a/backend/src/Squidex.Web/Resources.cs b/backend/src/Squidex.Web/Resources.cs index a9ddc1393..dc62f9b6f 100644 --- a/backend/src/Squidex.Web/Resources.cs +++ b/backend/src/Squidex.Web/Resources.cs @@ -198,7 +198,7 @@ namespace Squidex.Web public bool Includes(Permission permission, PermissionSet? additional = null) { - return Context.Permissions.Includes(permission) || additional?.Includes(permission) == true; + return Context.UserPermissions.Includes(permission) || additional?.Includes(permission) == true; } public bool IsAllowedForSchema(string id, string schema) @@ -230,7 +230,7 @@ namespace Squidex.Web var permission = P.ForApp(id, app, schema); - return Context.Permissions.Allows(permission) || additional?.Allows(permission) == true; + return Context.UserPermissions.Allows(permission) || additional?.Allows(permission) == true; } private string? GetAppName() diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index a17c8a965..c7219996b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -76,7 +76,7 @@ namespace Squidex.Areas.Api.Controllers.Apps public async Task GetApps() { var userOrClientId = HttpContext.User.UserOrClientId()!; - var userPermissions = Resources.Context.Permissions; + var userPermissions = Resources.Context.UserPermissions; var apps = await appProvider.GetUserAppsAsync(userOrClientId, userPermissions); diff --git a/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs index ba161ad2c..27f7bdc2c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs @@ -49,7 +49,7 @@ namespace Squidex.Areas.Api.Controllers.UI { var result = new UISettingsDto { - CanCreateApps = !uiOptions.OnlyAdminsCanCreateApps || Context.Permissions.Includes(CreateAppPermission) + CanCreateApps = !uiOptions.OnlyAdminsCanCreateApps || Context.UserPermissions.Includes(CreateAppPermission) }; return Ok(result); diff --git a/backend/src/Squidex/Config/Domain/StoreServices.cs b/backend/src/Squidex/Config/Domain/StoreServices.cs index 4c56f911b..169ce3e81 100644 --- a/backend/src/Squidex/Config/Domain/StoreServices.cs +++ b/backend/src/Squidex/Config/Domain/StoreServices.cs @@ -115,7 +115,7 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As().As>(); - services.AddSingletonAs(c => ActivatorUtilities.CreateInstance(c, GetDatabase(c, mongoContentDatabaseName))) + services.AddSingletonAs(c => ActivatorUtilities.CreateInstance(c, GetDatabase(c, mongoContentDatabaseName), false)) .As().As>(); services.AddSingletonAs() diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs index 7e2f678ee..bc54f67c9 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs @@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject private readonly IAppImageStore appImageStore = A.Fake(); private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly Context requestContext = Context.Anonymous(); + private readonly Context requestContext; private readonly AppCommandMiddleware sut; public sealed class MyCommand : SquidexCommand @@ -40,6 +40,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject public AppCommandMiddlewareTests() { + requestContext = Context.Anonymous(Mocks.App(appId)); + A.CallTo(() => contextProvider.Context) .Returns(requestContext); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs index 39e7f3bdb..20faa09b9 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs @@ -39,7 +39,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject private readonly DomainId assetId = DomainId.NewGuid(); private readonly AssetDomainObjectGrain asset; private readonly AssetFile file; - private readonly Context requestContext = Context.Anonymous(); + private readonly Context requestContext; private readonly AssetCommandMiddleware sut; public sealed class MyCommand : SquidexCommand @@ -63,6 +63,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject asset = new AssetDomainObjectGrain(serviceProvider, null!); asset.ActivateAsync(Id.ToString()).Wait(); + requestContext = Context.Anonymous(Mocks.App(AppNamedId)); + A.CallTo(() => contextProvider.Context) .Returns(requestContext); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs index dd0162b0d..46aa6c307 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; using Xunit; @@ -23,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries private readonly IAssetMetadataSource assetMetadataSource1 = A.Fake(); private readonly IAssetMetadataSource assetMetadataSource2 = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly Context requestContext = Context.Anonymous(); + private readonly Context requestContext; private readonly AssetEnricher sut; public AssetEnricherTests() @@ -34,6 +35,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries assetMetadataSource2 }; + requestContext = Context.Anonymous(Mocks.App(appId)); + sut = new AssetEnricher(tagService, assetMetadataSources, requestCache); } @@ -96,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries AppId = appId }; - var result = await sut.EnrichAsync(source, requestContext.Clone().WithoutAssetEnrichment()); + var result = await sut.EnrichAsync(source, requestContext.Clone(b => b.WithoutAssetEnrichment())); Assert.Null(result.TagNames); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs index a8c2999bb..c8db0392d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs @@ -34,12 +34,20 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries sut = new AssetQueryParser(TestUtils.DefaultSerializer, tagService, options); } + [Fact] + public async Task Should_skip_total_when_set_in_context() + { + var q = await sut.ParseAsync(requestContext.Clone(b => b.WithoutTotal()), Q.Empty); + + Assert.True(q.NoTotal); + } + [Fact] public async Task Should_throw_if_odata_query_is_invalid() { var query = Q.Empty.WithODataQuery("$filter=invalid"); - await Assert.ThrowsAsync(() => sut.ParseQueryAsync(requestContext, query)); + await Assert.ThrowsAsync(() => sut.ParseAsync(requestContext, query)); } [Fact] @@ -47,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var query = Q.Empty.WithJsonQuery("invalid"); - await Assert.ThrowsAsync(() => sut.ParseQueryAsync(requestContext, query)); + await Assert.ThrowsAsync(() => sut.ParseAsync(requestContext, query)); } [Fact] @@ -55,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var query = Q.Empty.WithODataQuery("$top=100&$orderby=fileName asc&$search=Hello World"); - var q = await sut.ParseQueryAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); Assert.Equal("FullText: 'Hello World'; Take: 100; Sort: fileName Ascending, id Ascending", q.Query.ToString()); } @@ -65,7 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var query = Q.Empty.WithODataQuery("$top=200&$filter=fileName eq 'ABC'"); - var q = await sut.ParseQueryAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); Assert.Equal("Filter: fileName == 'ABC'; Take: 200; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } @@ -75,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var query = Q.Empty.WithJsonQuery("{ \"filter\": { \"path\": \"fileName\", \"op\": \"eq\", \"value\": \"ABC\" } }"); - var q = await sut.ParseQueryAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); Assert.Equal("Filter: fileName == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } @@ -85,7 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var query = Q.Empty.WithJsonQuery("{ \"fullText\": \"Hello\" }"); - var q = await sut.ParseQueryAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } @@ -95,7 +103,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var query = Q.Empty; - var q = await sut.ParseQueryAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); Assert.Equal("Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } @@ -105,7 +113,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var query = Q.Empty.WithODataQuery("$top=300&$skip=20"); - var q = await sut.ParseQueryAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } @@ -115,7 +123,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var query = Q.Empty.WithODataQuery("$top=300&$skip=20&$orderby=id desc"); - var q = await sut.ParseQueryAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); Assert.Equal("Skip: 20; Take: 200; Sort: id Descending", q.Query.ToString()); } @@ -128,7 +136,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries var query = Q.Empty.WithODataQuery("$filter=tags eq 'name1'"); - var q = await sut.ParseQueryAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); Assert.Equal("Filter: tags == 'id1'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } @@ -141,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries var query = Q.Empty.WithODataQuery("$filter=tags eq 'name1'"); - var q = await sut.ParseQueryAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); Assert.Equal("Filter: tags == 'name1'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } @@ -151,7 +159,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var query = Q.Empty.WithODataQuery("$filter=fileSize eq 123"); - var q = await sut.ParseQueryAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); Assert.Equal("Filter: fileSize == 123; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs index ec32c9dc2..47c665ced 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs @@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId)); - A.CallTo(() => queryParser.ParseQueryAsync(requestContext, A._)) + A.CallTo(() => queryParser.ParseAsync(requestContext, A._)) .ReturnsLazily(c => Task.FromResult(c.GetArgument(1)!)); sut = new AssetQueryService(assetEnricher, assetRepository, assetFolderRepository, queryParser); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs index 736246626..5b7ae173b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs @@ -81,7 +81,12 @@ namespace Squidex.Domain.Apps.Entities.Contents var (id, _, query) = CreateTestData(true); - A.CallTo(() => contentQuery.QueryAsync(requestContext, A._, A.That.Matches(x => x.JsonQuery == query))) + A.CallTo(() => contentQuery.QueryAsync( + A.That.Matches(x => + x.ShouldSkipCleanup() && + x.ShouldSkipContentEnrichment() && + x.ShouldSkipTotal()), + schemaId.Name, A.That.Matches(x => x.JsonQuery == query))) .Returns(ResultList.CreateFrom(2, CreateContent(id), CreateContent(id))); var command = BulkCommand(BulkUpdateType.ChangeStatus, query); @@ -101,7 +106,12 @@ namespace Squidex.Domain.Apps.Entities.Contents var (id, data, query) = CreateTestData(true); - A.CallTo(() => contentQuery.QueryAsync(requestContext, A._, A.That.Matches(x => x.JsonQuery == query))) + A.CallTo(() => contentQuery.QueryAsync( + A.That.Matches(x => + x.ShouldSkipCleanup() && + x.ShouldSkipContentEnrichment() && + x.ShouldSkipTotal()), + schemaId.Name, A.That.Matches(x => x.JsonQuery == query))) .Returns(ResultList.CreateFrom(1, CreateContent(id))); var command = BulkCommand(BulkUpdateType.Upsert, query: query, data: data); @@ -125,7 +135,12 @@ namespace Squidex.Domain.Apps.Entities.Contents var id1 = DomainId.NewGuid(); var id2 = DomainId.NewGuid(); - A.CallTo(() => contentQuery.QueryAsync(requestContext, A._, A.That.Matches(x => x.JsonQuery == query))) + A.CallTo(() => contentQuery.QueryAsync( + A.That.Matches(x => + x.ShouldSkipCleanup() && + x.ShouldSkipContentEnrichment() && + x.ShouldSkipTotal()), + schemaId.Name, A.That.Matches(x => x.JsonQuery == query))) .Returns(ResultList.CreateFrom(2, CreateContent(id1), CreateContent(id2))); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentCommandMiddlewareTests.cs index 2092d87cb..7cf1dca1b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentCommandMiddlewareTests.cs @@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject private readonly IContentEnricher contentEnricher = A.Fake(); private readonly IContextProvider contextProvider = A.Fake(); private readonly DomainId contentId = DomainId.NewGuid(); - private readonly Context requestContext = Context.Anonymous(); + private readonly Context requestContext; private readonly ContentCommandMiddleware sut; public sealed class MyCommand : SquidexCommand @@ -35,6 +35,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject public ContentCommandMiddlewareTests() { + requestContext = Context.Anonymous(Mocks.App(AppNamedId)); + A.CallTo(() => contextProvider.Context) .Returns(requestContext); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index 116571d10..840f085a5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs @@ -47,7 +47,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var asset = TestAsset.Create(appId, DomainId.NewGuid()); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.HasOData("?$top=30&$skip=5&$filter=my-query"))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, + A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query" && x.NoTotal == true))) .Returns(ResultList.CreateFrom(0, asset)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -81,7 +82,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var asset = TestAsset.Create(appId, DomainId.NewGuid()); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.HasOData("?$top=30&$skip=5&$filter=my-query"))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, + A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query" && x.NoTotal == false))) .Returns(ResultList.CreateFrom(10, asset)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -116,7 +118,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", assetId.ToString()); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.HasIds(assetId))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.HasIdsWithoutTotal(assetId))) .Returns(ResultList.CreateFrom(1)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -145,7 +147,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", assetId.ToString()).Replace("", TestAsset.AllFields); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.HasIds(assetId))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.HasIdsWithoutTotal(assetId))) .Returns(ResultList.CreateFrom(1, asset)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -196,7 +198,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var content = TestContent.Create(appId, schemaId, DomainId.NewGuid(), DomainId.Empty, DomainId.Empty); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A.That.HasOData("?$top=30&$skip=5"))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), + A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5" && x.NoTotal == true))) .Returns(ResultList.CreateFrom(0, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -274,7 +277,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var content = TestContent.Create(appId, schemaId, DomainId.NewGuid(), DomainId.Empty, DomainId.Empty); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A.That.HasOData("?$top=30&$skip=5"))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), + A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5" && x.NoTotal == true))) .Returns(ResultList.CreateFrom(0, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -308,7 +312,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var content = TestContent.Create(appId, schemaId, DomainId.NewGuid(), DomainId.Empty, DomainId.Empty); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A.That.HasOData("?$top=30&$skip=5"))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), + A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5" && x.NoTotal == false))) .Returns(ResultList.CreateFrom(10, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -343,7 +348,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIds(contentId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIdsWithoutTotal(contentId))) .Returns(ResultList.CreateFrom(1)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -372,7 +377,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", TestContent.AllFields).Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIds(contentId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIdsWithoutTotal(contentId))) .Returns(ResultList.CreateFrom(1, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -445,10 +450,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A._)) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIdsWithoutTotal(contentRefId))) .Returns(ResultList.CreateFrom(0, contentRef)); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIds(contentId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIdsWithoutTotal(contentId))) .Returns(ResultList.CreateFrom(1, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -511,10 +516,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentRefId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIds(contentRefId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIdsWithoutTotal(contentRefId))) .Returns(ResultList.CreateFrom(1, contentRef)); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), A.That.HasOData("?$top=30&$skip=5", contentRefId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), + A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5" && x.Reference == contentRefId && x.NoTotal == true))) .Returns(ResultList.CreateFrom(1, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -574,10 +580,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentRefId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIds(contentRefId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIdsWithoutTotal(contentRefId))) .Returns(ResultList.CreateFrom(1, contentRef)); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), A.That.HasOData("?$top=30&$skip=5", contentRefId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), + A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5" && x.Reference == contentRefId && x.NoTotal == false))) .Returns(ResultList.CreateFrom(1, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -647,10 +654,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A._)) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIdsWithoutTotal(contentRefId))) .Returns(ResultList.CreateFrom(0, contentRef)); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIds(contentId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIdsWithoutTotal(contentId))) .Returns(ResultList.CreateFrom(1, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -713,10 +720,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIds(contentId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIdsWithoutTotal(contentId))) .Returns(ResultList.CreateFrom(1, content)); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A._)) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.HasIdsWithoutTotal(assetRefId))) .Returns(ResultList.CreateFrom(0, assetRef)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -769,10 +776,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", assetId2.ToString()); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.HasIds(assetId1))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.HasIdsWithoutTotal(assetId1))) .Returns(ResultList.CreateFrom(0, asset1)); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.HasIds(assetId2))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.HasIdsWithoutTotal(assetId2))) .Returns(ResultList.CreateFrom(0, asset2)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query1 }, new GraphQLQuery { Query = query2 }); @@ -828,7 +835,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIds(contentId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIdsWithoutTotal(contentId))) .Returns(ResultList.CreateFrom(1, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -840,12 +847,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL private Context MatchsAssetContext() { - return A.That.Matches(x => x.App == app && x.User == requestContext.User); + return A.That.Matches(x => + x.App == app && + x.ShouldSkipCleanup() && + x.ShouldSkipContentEnrichment() && + x.User == requestContext.User); } private Context MatchsContentContext() { - return A.That.Matches(x => x.App == app && x.User == requestContext.User); + return A.That.Matches(x => + x.App == app && + x.ShouldSkipCleanup() && + x.ShouldSkipContentEnrichment() && + x.User == requestContext.User); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs index 200a0d410..38b8a0d57 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs @@ -33,6 +33,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb private readonly int numValues = 10000; private readonly IMongoClient mongoClient = new MongoClient("mongodb://localhost"); private readonly IMongoDatabase mongoDatabase; + private readonly IMongoDatabase mongoDatabaseWildcard; public MongoContentRepository ContentRepository { get; } @@ -54,86 +55,93 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb public ContentsQueryFixture() { mongoDatabase = mongoClient.GetDatabase("QueryTests"); + mongoDatabaseWildcard = mongoClient.GetDatabase("QueryTestsWildcard"); SetupJson(); - var contentRepository = + var appProvider = CreateAppProvider(); + + ContentRepository = new MongoContentRepository( mongoDatabase, - CreateAppProvider()); + appProvider, false); Task.Run(async () => { - await contentRepository.InitializeAsync(); + await Task.WhenAll( + SetupAsync(ContentRepository, mongoDatabase)); + }).Wait(); + } + + private async Task SetupAsync(MongoContentRepository contentRepository, IMongoDatabase database) + { + await contentRepository.InitializeAsync(); - await mongoDatabase.RunCommandAsync("{ profile : 0 }"); - await mongoDatabase.DropCollectionAsync("system.profile"); + await database.RunCommandAsync("{ profile : 0 }"); + await database.DropCollectionAsync("system.profile"); - var collections = contentRepository.GetInternalCollections(); + var collections = contentRepository.GetInternalCollections(); - foreach (var collection in collections) + foreach (var collection in collections) + { + var contentCount = await collection.Find(new BsonDocument()).CountDocumentsAsync(); + + if (contentCount == 0) { - var contentCount = await collection.Find(new BsonDocument()).CountDocumentsAsync(); + var batch = new List(); - if (contentCount == 0) + async Task ExecuteBatchAsync(MongoContentEntity? entity) { - var batch = new List(); - - async Task ExecuteBatchAsync(MongoContentEntity? entity) + if (entity != null) { - if (entity != null) - { - batch.Add(entity); - } + batch.Add(entity); + } - if ((entity == null || batch.Count >= 1000) && batch.Count > 0) - { - await collection.InsertManyAsync(batch); + if ((entity == null || batch.Count >= 1000) && batch.Count > 0) + { + await collection.InsertManyAsync(batch); - batch.Clear(); - } + batch.Clear(); } + } - foreach (var appId in AppIds) + foreach (var appId in AppIds) + { + foreach (var schemaId in SchemaIds) { - foreach (var schemaId in SchemaIds) + for (var i = 0; i < numValues; i++) { - for (var i = 0; i < numValues; i++) + var data = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddJsonValue(JsonValue.Create(i))) + .AddField("field2", + new ContentFieldData() + .AddJsonValue(JsonValue.Create(Lorem.Paragraph(200, 20)))); + + var content = new MongoContentEntity { - var data = - new ContentData() - .AddField("field1", - new ContentFieldData() - .AddJsonValue(JsonValue.Create(i))) - .AddField("field2", - new ContentFieldData() - .AddJsonValue(JsonValue.Create(Lorem.Paragraph(200, 20)))); - - var content = new MongoContentEntity - { - DocumentId = DomainId.NewGuid(), - AppId = appId, - Data = data, - IndexedAppId = appId.Id, - IndexedSchemaId = schemaId.Id, - IsDeleted = false, - SchemaId = schemaId, - Status = Status.Published - }; - - await ExecuteBatchAsync(content); - } + DocumentId = DomainId.NewGuid(), + AppId = appId, + Data = data, + IndexedAppId = appId.Id, + IndexedSchemaId = schemaId.Id, + IsDeleted = false, + SchemaId = schemaId, + Status = Status.Published + }; + + await ExecuteBatchAsync(content); } } - - await ExecuteBatchAsync(null); } - } - await mongoDatabase.RunCommandAsync("{ profile : 2 }"); - }).Wait(); + await ExecuteBatchAsync(null); + } + } - ContentRepository = contentRepository; + await database.RunCommandAsync("{ profile : 2 }"); } private static IAppProvider CreateAppProvider() diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTests.cs index 835a33f84..5618e0a40 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTests.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using NodaTime; +using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Queries; @@ -29,18 +30,25 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb _ = fixture; } - [Fact] - public async Task Should_verify_ids() + public IEnumerable Collections() + { + yield return new[] { _.ContentRepository }; + } + + [Theory] + [MemberData(nameof(Collections))] + public async Task Should_verify_ids(IContentRepository repository) { var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); - var contents = await _.ContentRepository.QueryIdsAsync(_.RandomAppId(), ids, SearchScope.Published); + var contents = await repository.QueryIdsAsync(_.RandomAppId(), ids, SearchScope.Published); Assert.NotNull(contents); } - [Fact] - public async Task Should_query_contents_by_ids() + [Theory] + [MemberData(nameof(Collections))] + public async Task Should_query_contents_by_ids(IContentRepository repository) { var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); @@ -49,33 +57,36 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb _.RandomSchema() }; - var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), schemas, Q.Empty.WithIds(ids), SearchScope.All); + var contents = await repository.QueryAsync(_.RandomApp(), schemas, Q.Empty.WithIds(ids), SearchScope.All); Assert.NotNull(contents); } - [Fact] - public async Task Should_query_contents_by_ids_and_schema() + [Theory] + [MemberData(nameof(Collections))] + public async Task Should_query_contents_by_ids_and_schema(IContentRepository repository) { var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); - var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), Q.Empty.WithIds(ids), SearchScope.All); + var contents = await repository.QueryAsync(_.RandomApp(), _.RandomSchema(), Q.Empty.WithIds(ids), SearchScope.All); Assert.NotNull(contents); } - [Fact] - public async Task Should_query_contents_ids_by_filter() + [Theory] + [MemberData(nameof(Collections))] + public async Task Should_query_contents_ids_by_filter(IContentRepository repository) { var filter = F.Eq("data.value.iv", 12); - var contents = await _.ContentRepository.QueryIdsAsync(_.RandomAppId(), _.RandomSchemaId(), filter); + var contents = await repository.QueryIdsAsync(_.RandomAppId(), _.RandomSchemaId(), filter); Assert.NotEmpty(contents); } - [Fact] - public async Task Should_query_contents_by_filter() + [Theory] + [MemberData(nameof(Collections))] + public async Task Should_query_contents_by_filter(IContentRepository repository) { var query = new ClrQuery { @@ -86,41 +97,45 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb Filter = F.Eq("data.value.iv", 12) }; - var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), Q.Empty.WithQuery(query), SearchScope.Published); + var contents = await repository.QueryAsync(_.RandomApp(), _.RandomSchema(), Q.Empty.WithQuery(query), SearchScope.Published); Assert.NotEmpty(contents); } - [Fact] - public async Task Should_query_contents_scheduled() + [Theory] + [MemberData(nameof(Collections))] + public async Task Should_query_contents_scheduled(IContentRepository repository) { var time = SystemClock.Instance.GetCurrentInstant(); - await _.ContentRepository.QueryScheduledWithoutDataAsync(time, _ => Task.CompletedTask); + await repository.QueryScheduledWithoutDataAsync(time, _ => Task.CompletedTask); } - [Fact] - public async Task Should_query_contents_with_default_query() + [Theory] + [MemberData(nameof(Collections))] + public async Task Should_query_contents_with_default_query(IContentRepository repository) { var query = new ClrQuery(); - var contents = await QueryAsync(query); + var contents = await QueryAsync(repository, query); Assert.NotEmpty(contents); } - [Fact] - public async Task Should_query_contents_with_default_query_and_id() + [Theory] + [MemberData(nameof(Collections))] + public async Task Should_query_contents_with_default_query_and_id(IContentRepository repository) { var query = new ClrQuery(); - var contents = await QueryAsync(query, reference: DomainId.NewGuid()); + var contents = await QueryAsync(repository, query, reference: DomainId.NewGuid()); Assert.Empty(contents); } - [Fact] - public async Task Should_query_contents_with_large_skip() + [Theory] + [MemberData(nameof(Collections))] + public async Task Should_query_contents_with_large_skip(IContentRepository repository) { var query = new ClrQuery { @@ -130,51 +145,58 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb } }; - var contents = await QueryAsync(query, 1000, 9000); + var contents = await QueryAsync(repository, query, 1000, 9000); Assert.NotEmpty(contents); } - [Fact] - public async Task Should_query_contents_with_query_fulltext() + [Theory] + [MemberData(nameof(Collections))] + public async Task Should_query_contents_with_query_fulltext(IContentRepository repository) { var query = new ClrQuery { FullText = "hello" }; - var contents = await QueryAsync(query); + var contents = await QueryAsync(repository, query); Assert.NotNull(contents); } - [Fact] - public async Task Should_query_contents_with_query_filter() + [Theory] + [MemberData(nameof(Collections))] + public async Task Should_query_contents_with_query_filter(IContentRepository repository) { var query = new ClrQuery { Filter = F.Eq("data.value.iv", 200) }; - var contents = await QueryAsync(query, 1000, 0); + var contents = await QueryAsync(repository, query, 1000, 0); Assert.NotEmpty(contents); } - [Fact] - public async Task Should_query_contents_with_query_filter_and_id() + [Theory] + [MemberData(nameof(Collections))] + public async Task Should_query_contents_with_query_filter_and_id(IContentRepository repository) { var query = new ClrQuery { Filter = F.Eq("data.value.iv", 12) }; - var contents = await QueryAsync(query, 1000, 0, reference: DomainId.NewGuid()); + var contents = await QueryAsync(repository, query, 1000, 0, reference: DomainId.NewGuid()); Assert.Empty(contents); } - private async Task> QueryAsync(ClrQuery clrQuery, int take = 1000, int skip = 100, DomainId reference = default) + private async Task> QueryAsync(IContentRepository repository, + ClrQuery clrQuery, + int take = 1000, + int skip = 100, + DomainId reference = default) { if (clrQuery.Take == long.MaxValue) { @@ -199,7 +221,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb .WithQuery(clrQuery) .WithReference(reference); - var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), q, SearchScope.All); + var contents = await repository.QueryAsync(_.RandomApp(), _.RandomSchema(), q, SearchScope.All); return contents; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs index dacad43dd..6a48880bd 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs @@ -51,6 +51,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries sut = new ContentQueryParser(cache, TestUtils.DefaultSerializer, textIndex, options); } + [Fact] + public async Task Should_skip_total_when_set_in_context() + { + var q = await sut.ParseAsync(requestContext.Clone(b => b.WithoutTotal()), Q.Empty); + + Assert.True(q.NoTotal); + } + [Fact] public async Task Should_throw_if_odata_query_is_invalid() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs index bdfe78f1d..3167a0ff5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs @@ -137,8 +137,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries public async Task FindContentAsync_should_return_content(int isFrontend, int unpublished, SearchScope scope) { var ctx = - CreateContext(isFrontend: isFrontend == 1, allowSchema: true) - .WithUnpublished(unpublished == 1); + CreateContext(isFrontend: isFrontend == 1, allowSchema: true).Clone(b => b + .WithUnpublished(unpublished == 1)); var content = CreateContent(contentId); @@ -183,8 +183,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries public async Task QueryAsync_should_return_contents(int isFrontend, int unpublished, SearchScope scope) { var ctx = - CreateContext(isFrontend: isFrontend == 1, allowSchema: true) - .WithUnpublished(unpublished == 1); + CreateContext(isFrontend: isFrontend == 1, allowSchema: true).Clone(b => b + .WithUnpublished(unpublished == 1)); var content = CreateContent(contentId); @@ -250,8 +250,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries public async Task QueryAll_should_return_contents(int isFrontend, int unpublished, SearchScope scope) { var ctx = - CreateContext(isFrontend: isFrontend == 1, allowSchema: true) - .WithUnpublished(unpublished == 1); + CreateContext(isFrontend: isFrontend == 1, allowSchema: true).Clone(b => b + .WithUnpublished(unpublished == 1)); var ids = Enumerable.Range(0, 5).Select(x => DomainId.NewGuid()).ToList(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs index bccdb8701..e105e60b1 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs @@ -123,7 +123,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => contentWorkflow.GetInfoAsync(content, content.Status)) .Returns(Task.FromResult(null!)); - var ctx = requestContext.WithResolveFlow(true); + var ctx = requestContext.Clone(b => b.WithResolveFlow(false)); await sut.EnrichAsync(ctx, new[] { content }, null!); @@ -138,7 +138,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => contentWorkflow.CanUpdateAsync(content, content.Status, requestContext.User)) .Returns(true); - var ctx = requestContext.WithResolveFlow(true); + var ctx = requestContext.Clone(b => b.WithResolveFlow(false)); await sut.EnrichAsync(ctx, new[] { content }, null!); @@ -148,11 +148,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [Fact] public async Task Should_not_enrich_content_with_can_update_if_disabled_in_context() { - requestContext.WithResolveFlow(false); - var content = new ContentEntity { SchemaId = schemaId }; - var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)).WithResolveFlow(false); + var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)).Clone(b => b.WithResolveFlow(false)); await sut.EnrichAsync(ctx, new[] { content }, null!); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs index ff47a560c..14af07d8c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs @@ -88,7 +88,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries new[] { doc2.Id }) }; - A.CallTo(() => assetQuery.QueryAsync(A.That.Matches(x => !x.ShouldEnrichAsset()), null, A.That.HasIds(doc1.Id, doc2.Id))) + A.CallTo(() => assetQuery.QueryAsync( + A.That.Matches(x => x.ShouldSkipAssetEnrichment() && x.ShouldSkipTotal()), null, A.That.HasIds(doc1.Id, doc2.Id))) .Returns(ResultList.CreateFrom(4, doc1, doc2)); await sut.EnrichAsync(requestContext, contents, schemaProvider); @@ -119,7 +120,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries new[] { doc2.Id, doc1.Id }) }; - A.CallTo(() => assetQuery.QueryAsync(A.That.Matches(x => !x.ShouldEnrichAsset()), null, A.That.HasIds(doc1.Id, doc2.Id, img1.Id, img2.Id))) + A.CallTo(() => assetQuery.QueryAsync( + A.That.Matches(x => x.ShouldSkipAssetEnrichment() && x.ShouldSkipTotal()), null, A.That.HasIds(doc1.Id, doc2.Id, img1.Id, img2.Id))) .Returns(ResultList.CreateFrom(4, img1, img2, doc1, doc2)); await sut.EnrichAsync(requestContext, contents, schemaProvider); @@ -171,7 +173,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries CreateContent(new[] { DomainId.NewGuid() }, Array.Empty()) }; - var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)).WithoutContentEnrichment(true); + var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)).Clone(b => b.WithoutContentEnrichment(true)); await sut.EnrichAsync(ctx, contents, schemaProvider); @@ -212,7 +214,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries Assert.NotNull(contents[0].ReferenceData); - A.CallTo(() => assetQuery.QueryAsync(A.That.Matches(x => !x.ShouldEnrichAsset()), null, A.That.HasIds(id1))) + A.CallTo(() => assetQuery.QueryAsync( + A.That.Matches(x => x.ShouldSkipAssetEnrichment() && x.ShouldSkipTotal()), null, A.That.HasIds(id1))) .MustHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs index da5164198..347267b83 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs @@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries } [Fact] - public async Task Should_add_referenced_id_and__as_dependency() + public async Task Should_add_referenced_id_and_as_dependency() { var ref1_1 = CreateRefContent(DomainId.NewGuid(), 1, "ref1_1", 13, refSchemaId1); var ref1_2 = CreateRefContent(DomainId.NewGuid(), 2, "ref1_2", 17, refSchemaId1); @@ -101,7 +101,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries CreateContent(new[] { ref1_2.Id }, new[] { ref2_2.Id }) }; - A.CallTo(() => contentQuery.QueryAsync(A._, A.That.HasIds(ref1_1.Id, ref1_2.Id, ref2_1.Id, ref2_2.Id))) + A.CallTo(() => contentQuery.QueryAsync( + A.That.Matches(x => x.ShouldSkipContentEnrichment() && x.ShouldSkipTotal()), A.That.HasIds(ref1_1.Id, ref1_2.Id, ref2_1.Id, ref2_2.Id))) .Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2)); await sut.EnrichAsync(requestContext, contents, schemaProvider); @@ -139,7 +140,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries CreateContent(new[] { ref1_2.Id }, new[] { ref2_2.Id }) }; - A.CallTo(() => contentQuery.QueryAsync(A.That.Matches(x => !x.ShouldEnrichContent()), A.That.HasIds(ref1_1.Id, ref1_2.Id, ref2_1.Id, ref2_2.Id))) + A.CallTo(() => contentQuery.QueryAsync( + A.That.Matches(x => x.ShouldSkipContentEnrichment() && x.ShouldSkipTotal()), A.That.HasIds(ref1_1.Id, ref1_2.Id, ref2_1.Id, ref2_2.Id))) .Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2)); await sut.EnrichAsync(requestContext, contents, schemaProvider); @@ -191,7 +193,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries CreateContent(new[] { ref1_2.Id }, new[] { ref2_1.Id, ref2_2.Id }) }; - A.CallTo(() => contentQuery.QueryAsync(A.That.Matches(x => !x.ShouldEnrichContent()), A.That.HasIds(ref1_1.Id, ref1_2.Id, ref2_1.Id, ref2_2.Id))) + A.CallTo(() => contentQuery.QueryAsync( + A.That.Matches(x => x.ShouldSkipContentEnrichment() && x.ShouldSkipTotal()), A.That.HasIds(ref1_1.Id, ref1_2.Id, ref2_1.Id, ref2_2.Id))) .Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2)); await sut.EnrichAsync(requestContext, contents, schemaProvider); @@ -255,7 +258,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries CreateContent(new[] { DomainId.NewGuid() }, Array.Empty()) }; - var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)).WithoutContentEnrichment(true); + var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)).Clone(b => b.WithoutContentEnrichment(true)); await sut.EnrichAsync(ctx, contents, schemaProvider); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleCommandMiddlewareTests.cs index f1902e232..241a17575 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleCommandMiddlewareTests.cs @@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject private readonly IRuleEnricher ruleEnricher = A.Fake(); private readonly IContextProvider contextProvider = A.Fake(); private readonly DomainId ruleId = DomainId.NewGuid(); - private readonly Context requestContext = Context.Anonymous(); + private readonly Context requestContext; private readonly RuleCommandMiddleware sut; public sealed class MyCommand : SquidexCommand @@ -34,6 +34,8 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject public RuleCommandMiddlewareTests() { + requestContext = Context.Anonymous(Mocks.App(AppNamedId)); + A.CallTo(() => contextProvider.Context) .Returns(requestContext); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs index c0484e661..5bd8067a5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using FakeItEasy; using NodaTime; using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; using Xunit; @@ -21,11 +22,13 @@ namespace Squidex.Domain.Apps.Entities.Rules.Queries private readonly IRuleEventRepository ruleEventRepository = A.Fake(); private readonly IRequestCache requestCache = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly Context requestContext = Context.Anonymous(); + private readonly Context requestContext; private readonly RuleEnricher sut; public RuleEnricherTests() { + requestContext = Context.Anonymous(Mocks.App(appId)); + sut = new RuleEnricher(ruleEventRepository, requestCache); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs index 194cc71db..41d94039c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs @@ -20,12 +20,12 @@ namespace Squidex.Domain.Apps.Entities.Rules.Queries private readonly IRulesIndex rulesIndex = A.Fake(); private readonly IRuleEnricher ruleEnricher = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly Context requestContext = Context.Anonymous(); + private readonly Context requestContext; private readonly RuleQueryService sut; public RuleQueryServiceTests() { - requestContext.App = Mocks.App(appId); + requestContext = Context.Anonymous(Mocks.App(appId)); sut = new RuleQueryService(rulesIndex, ruleEnricher); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Search/SearchManagerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Search/SearchManagerTests.cs index 227fee12f..3252a0e12 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Search/SearchManagerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Search/SearchManagerTests.cs @@ -9,6 +9,8 @@ using System; using System.Threading.Tasks; using FakeItEasy; using FluentAssertions; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; using Squidex.Log; using Xunit; @@ -19,7 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Search private readonly ISearchSource source1 = A.Fake(); private readonly ISearchSource source2 = A.Fake(); private readonly ISemanticLog log = A.Fake(); - private readonly Context requestContext = Context.Anonymous(); + private readonly Context requestContext = Context.Anonymous(Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app"))); private readonly SearchManager sut; public SearchManagerTests() diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs index 1f79dbe97..fb35f7fbf 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs @@ -20,14 +20,9 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers return that.Matches(x => x.Query!.ToString() == query); } - public static Q HasOData(this INegatableArgumentConstraintManager that, string odata) + public static Q HasIdsWithoutTotal(this INegatableArgumentConstraintManager that, params DomainId[] ids) { - return that.HasOData(odata, default); - } - - public static Q HasOData(this INegatableArgumentConstraintManager that, string odata, DomainId reference = default) - { - return that.Matches(x => x.ODataQuery == odata && x.Reference == reference); + return that.Matches(x => x.Ids != null && x.Ids.SetEquals(ids) && x.NoTotal == true); } public static Q HasIds(this INegatableArgumentConstraintManager that, params DomainId[] ids) diff --git a/backend/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs b/backend/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs index 9737d7443..f2ae1b074 100644 --- a/backend/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs +++ b/backend/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs @@ -141,7 +141,9 @@ namespace Squidex.Web private void SetContext() { - actionExecutingContext.HttpContext.Features.Set(new Context(new ClaimsPrincipal(actionExecutingContext.HttpContext.User))); + var context = new Context(new ClaimsPrincipal(actionExecutingContext.HttpContext.User), null!); + + actionExecutingContext.HttpContext.Features.Set(context); } } } diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs index b1481581e..d5eecf1df 100644 --- a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs @@ -11,6 +11,7 @@ using FakeItEasy; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Xunit; @@ -22,28 +23,24 @@ namespace Squidex.Web.CommandMiddlewares private readonly IContextProvider contextProvider = A.Fake(); private readonly ICommandBus commandBus = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly Context requestContext = Context.Anonymous(); + private readonly Context requestContext; private readonly EnrichWithAppIdCommandMiddleware sut; public EnrichWithAppIdCommandMiddlewareTests() { + requestContext = Context.Anonymous(Mocks.App(appId)); + A.CallTo(() => contextProvider.Context) .Returns(requestContext); - var app = A.Fake(); - - A.CallTo(() => app.Id).Returns(appId.Id); - A.CallTo(() => app.Name).Returns(appId.Name); - - requestContext.App = app; - sut = new EnrichWithAppIdCommandMiddleware(contextProvider); } [Fact] public async Task Should_throw_exception_if_app_not_found() { - requestContext.App = null!; + A.CallTo(() => contextProvider.Context) + .Returns(Context.Anonymous(null!)); var command = new CreateContent(); var context = Ctx(command); diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs index 95fcfd38c..1efc5b4d2 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Routing; +using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Plans; using Xunit; @@ -109,7 +110,7 @@ namespace Squidex.Web.Pipeline private void SetupApp() { - httpContext.Context().App = appEntity; + httpContext.Features.Set(Context.Anonymous(appEntity)); } } } \ No newline at end of file diff --git a/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj b/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj index 8bc1b5d79..a894345a7 100644 --- a/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj +++ b/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj @@ -7,7 +7,7 @@ enable - +