diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs index c6bf83fac..db4a57baa 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs @@ -9,7 +9,9 @@ namespace Squidex.Domain.Apps.Entities.Assets { public sealed class AssetOptions { - public int DefaultPageSize { get; set; } = 20; + public int DefaultPageSize { get; set; } = 200; + + public int DefaultPageSizeGraphQl { get; set; } = 20; public int MaxResults { get; set; } = 200; diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs index ee40d2c51..833920248 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs @@ -27,9 +27,9 @@ namespace Squidex.Domain.Apps.Entities.Assets private readonly IAssetRepository assetRepository; private readonly AssetOptions options; - public int DefaultPageSize + public int DefaultPageSizeGraphQl { - get { return options.DefaultPageSize; } + get { return options.DefaultPageSizeGraphQl; } } public AssetQueryService(ITagService tagService, IAssetRepository assetRepository, IOptions options) @@ -120,7 +120,11 @@ namespace Squidex.Domain.Apps.Entities.Assets result.Sort.Add(new SortNode(new List { "lastModified" }, SortOrder.Descending)); } - if (result.Take > options.MaxResults) + if (result.Take == long.MaxValue) + { + result.Take = options.DefaultPageSize; + } + else if (result.Take > options.MaxResults) { result.Take = options.MaxResults; } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs b/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs index f83a437dc..501d690a9 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs @@ -14,7 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { public interface IAssetQueryService { - int DefaultPageSize { get; } + int DefaultPageSizeGraphQl { get; } Task> QueryByHashAsync(Guid appId, string hash); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs index 72ca1309b..58c42d381 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs @@ -9,7 +9,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { public sealed class ContentOptions { - public int DefaultPageSize { get; set; } = 20; + public int DefaultPageSize { get; set; } = 200; + + public int DefaultPageSizeGraphQl { get; set; } = 20; public int MaxResults { get; set; } = 200; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs index 3648d433b..14138e026 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs @@ -45,9 +45,9 @@ namespace Squidex.Domain.Apps.Entities.Contents private readonly ContentOptions options; private readonly EdmModelBuilder modelBuilder; - public int DefaultPageSize + public int DefaultPageSizeGraphQl { - get { return options.DefaultPageSize; } + get { return options.DefaultPageSizeGraphQl; } } public ContentQueryService( @@ -276,7 +276,11 @@ namespace Squidex.Domain.Apps.Entities.Contents result.Sort.Add(new SortNode(new List { "lastModified" }, SortOrder.Descending)); } - if (result.Take > options.MaxResults) + if (result.Take == long.MaxValue) + { + result.Take = options.DefaultPageSize; + } + else if (result.Take > options.MaxResults) { result.Take = options.MaxResults; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs index b43d93c1a..2c57f06da 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs @@ -99,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var allSchemas = await appProvider.GetSchemasAsync(app.Id); - return new GraphQLModel(app, allSchemas, contentQuery.DefaultPageSize, assetQuery.DefaultPageSize, urlGenerator); + return new GraphQLModel(app, allSchemas, contentQuery.DefaultPageSizeGraphQl, assetQuery.DefaultPageSizeGraphQl, urlGenerator); }); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs index f7cd0007f..75d89c115 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs @@ -14,7 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { public interface IContentQueryService { - int DefaultPageSize { get; } + int DefaultPageSizeGraphQl { get; } Task> QueryAsync(QueryContext context, IReadOnlyList ids); diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index 7ed940711..fa83eb5db 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -150,7 +150,11 @@ /* * The default page size if not specified by a query. */ - "defaultPageSize": 20, + "defaultPageSize": 200, + /* + * The default page size for graphql if not specified by a query. + */ + "defaultPageSizeGraphQL": 20, /* * The maximum number of items to return for each query. * @@ -163,7 +167,11 @@ /* * The default page size if not specified by a query. */ - "defaultPageSize": 20, + "defaultPageSize": 200, + /* + * The default page size for graphql if not specified by a query. + */ + "defaultPageSizeGraphQL": 20, /* * The maximum number of items to return for each query. * diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs index cb05b142d..64bcbedab 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs @@ -15,6 +15,7 @@ using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Queries; using Xunit; @@ -49,13 +50,15 @@ namespace Squidex.Domain.Apps.Entities.Assets ["id3"] = "name3" }); - sut = new AssetQueryService(tagService, assetRepository, Options.Create(new AssetOptions())); + var options = Options.Create(new AssetOptions { DefaultPageSize = 30 }); + + sut = new AssetQueryService(tagService, assetRepository, options); } [Fact] public void Should_provide_default_page_size() { - var result = sut.DefaultPageSize; + var result = sut.DefaultPageSizeGraphQl; Assert.Equal(20, result); } @@ -128,27 +131,44 @@ namespace Squidex.Domain.Apps.Entities.Assets [Fact] public async Task Should_transform_odata_query() { - await sut.QueryAsync(context, Q.Empty.WithODataQuery("$top=100&$orderby=fileName asc&$search=Hello World")); + var query = Q.Empty.WithODataQuery("$top=100&$orderby=fileName asc&$search=Hello World"); + + await sut.QueryAsync(context, query); - A.CallTo(() => assetRepository.QueryAsync(appId.Id, A.That.Matches(x => x.ToString() == "FullText: 'Hello World'; Take: 100; Sort: fileName Ascending"))) + A.CallTo(() => assetRepository.QueryAsync(appId.Id, A.That.Is("FullText: 'Hello World'; Take: 100; Sort: fileName Ascending"))) .MustHaveHappened(); } [Fact] public async Task Should_transform_odata_query_and_enrich_with_defaults() { - await sut.QueryAsync(context, Q.Empty.WithODataQuery("$filter=fileName eq 'ABC'")); + var query = Q.Empty.WithODataQuery("$top=200&$filter=fileName eq 'ABC'"); + + await sut.QueryAsync(context, query); - A.CallTo(() => assetRepository.QueryAsync(appId.Id, A.That.Matches(x => x.ToString() == "Filter: fileName == 'ABC'; Take: 200; Sort: lastModified Descending"))) + A.CallTo(() => assetRepository.QueryAsync(appId.Id, A.That.Is("Filter: fileName == 'ABC'; Take: 200; Sort: lastModified Descending"))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_apply_default_page_size() + { + var query = Q.Empty; + + await sut.QueryAsync(context, query); + + A.CallTo(() => assetRepository.QueryAsync(appId.Id, A.That.Is("Take: 30; Sort: lastModified Descending"))) .MustHaveHappened(); } [Fact] public async Task Should_limit_number_of_assets() { - await sut.QueryAsync(context, Q.Empty.WithODataQuery("$top=300&$skip=20")); + var query = Q.Empty.WithODataQuery("$top=300&$skip=20"); + + await sut.QueryAsync(context, query); - A.CallTo(() => assetRepository.QueryAsync(appId.Id, A.That.Matches(x => x.ToString() == "Skip: 20; Take: 200; Sort: lastModified Descending"))) + A.CallTo(() => assetRepository.QueryAsync(appId.Id, A.That.Is("Skip: 20; Take: 200; Sort: lastModified Descending"))) .MustHaveHappened(); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs index 8568d05e5..d0695fe68 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using FakeItEasy; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Microsoft.OData; using Squidex.Domain.Apps.Core.Apps; @@ -22,6 +23,7 @@ using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents.Edm; using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Security; @@ -49,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Contents private readonly NamedContentData contentTransformed = new NamedContentData(); private readonly ClaimsPrincipal user; private readonly ClaimsIdentity identity = new ClaimsIdentity(); - private readonly EdmModelBuilder modelBuilder = A.Fake(); + private readonly EdmModelBuilder modelBuilder = new EdmModelBuilder(new MemoryCache(Options.Create(new MemoryCacheOptions()))); private readonly QueryContext context; private readonly ContentQueryService sut; @@ -71,19 +73,22 @@ namespace Squidex.Domain.Apps.Entities.Contents context = QueryContext.Create(app, user); + var options = Options.Create(new ContentOptions { DefaultPageSize = 30 }); + sut = new ContentQueryService( appProvider, urlGenerator, contentRepository, contentVersionLoader, scriptEngine, - Options.Create(new ContentOptions()), modelBuilder); + options, + modelBuilder); } [Fact] public void Should_provide_default_page_size() { - var result = sut.DefaultPageSize; + var result = sut.DefaultPageSizeGraphQl; Assert.Equal(20, result); } @@ -128,17 +133,33 @@ namespace Squidex.Domain.Apps.Entities.Contents await Assert.ThrowsAsync(() => sut.ThrowIfSchemaNotExistsAsync(ctx, schemaId.Name)); } - public static IEnumerable SingleDataFrontend = new[] + [Fact] + public async Task Should_apply_default_page_size() { - new object[] { true, new[] { Status.Archived, Status.Draft, Status.Published } }, - new object[] { false, new[] { Status.Archived, Status.Draft, Status.Published } } - }; + SetupClaims(); + SetupSchema(); - public static IEnumerable SingleDataApi = new[] + var query = Q.Empty; + + await sut.QueryAsync(context, schemaId.Name, query); + + A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.Is(Status.Published), A.That.Is("Take: 30; Sort: lastModified Descending"))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_limit_number_of_contents() { - new object[] { true, new[] { Status.Draft, Status.Published } }, - new object[] { false, new[] { Status.Published } } - }; + SetupClaims(); + SetupSchema(); + + var query = Q.Empty.WithODataQuery("$top=300&$skip=20"); + + await sut.QueryAsync(context, schemaId.Name, query); + + A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.Is(Status.Published), A.That.Is("Skip: 20; Take: 200; Sort: lastModified Descending"))) + .MustHaveHappened(); + } [Fact] public async Task Should_throw_for_single_content_if_no_permission() @@ -157,7 +178,7 @@ namespace Squidex.Domain.Apps.Entities.Contents SetupClaims(); SetupSchema(); - A.CallTo(() => contentRepository.FindContentAsync(app, schema, new[] { Status.Published }, contentId)) + A.CallTo(() => contentRepository.FindContentAsync(app, schema, A.That.Is(Status.Published), contentId)) .Returns((IContentEntity)null); var ctx = context; @@ -165,6 +186,18 @@ namespace Squidex.Domain.Apps.Entities.Contents await Assert.ThrowsAsync(async () => await sut.FindContentAsync(ctx, schemaId.Name, contentId)); } + public static IEnumerable SingleDataFrontend = new[] + { + new object[] { true, new[] { Status.Archived, Status.Draft, Status.Published } }, + new object[] { false, new[] { Status.Archived, Status.Draft, Status.Published } } + }; + + public static IEnumerable SingleDataApi = new[] + { + new object[] { true, new[] { Status.Draft, Status.Published } }, + new object[] { false, new[] { Status.Published } } + }; + [Theory] [MemberData(nameof(SingleDataFrontend))] public async Task Should_return_single_content_for_frontend_without_transform(bool unpublished, params Status[] status) @@ -175,7 +208,7 @@ namespace Squidex.Domain.Apps.Entities.Contents SetupSchema(); SetupScripting(contentId); - A.CallTo(() => contentRepository.FindContentAsync(app, schema, A.That.IsSameSequenceAs(status), contentId)) + A.CallTo(() => contentRepository.FindContentAsync(app, schema, A.That.Is(status), contentId)) .Returns(content); var ctx = context.WithUnpublished(unpublished); @@ -199,7 +232,7 @@ namespace Squidex.Domain.Apps.Entities.Contents SetupSchema(); SetupScripting(contentId); - A.CallTo(() => contentRepository.FindContentAsync(app, schema, A.That.IsSameSequenceAs(status), contentId)) + A.CallTo(() => contentRepository.FindContentAsync(app, schema, A.That.Is(status), contentId)) .Returns(content); var ctx = context.WithUnpublished(unpublished); @@ -318,10 +351,9 @@ namespace Squidex.Domain.Apps.Entities.Contents SetupClaims(); SetupSchema(); - A.CallTo(() => modelBuilder.BuildEdmModel(app, schema, A.Ignored)) - .Throws(new ODataException()); + var query = Q.Empty.WithODataQuery("$filter=invalid"); - return Assert.ThrowsAsync(() => sut.QueryAsync(context, schemaId.Name, Q.Empty.WithODataQuery("query"))); + return Assert.ThrowsAsync(() => sut.QueryAsync(context, schemaId.Name, query)); } public static IEnumerable ManyIdDataFrontend = new[] @@ -349,9 +381,9 @@ namespace Squidex.Domain.Apps.Entities.Contents var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); SetupClaims(true); + SetupContents(status, total, ids); SetupSchema(); SetupScripting(ids.ToArray()); - SetupContents(status, total, ids); var ctx = context.WithArchived(archive).WithUnpublished(unpublished); @@ -373,9 +405,9 @@ namespace Squidex.Domain.Apps.Entities.Contents var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); SetupClaims(); + SetupContents(status, total, ids); SetupSchema(); SetupScripting(ids.ToArray()); - SetupContents(status, total, ids); var ctx = context.WithArchived(archive).WithUnpublished(unpublished); @@ -397,9 +429,9 @@ namespace Squidex.Domain.Apps.Entities.Contents var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); SetupClaims(true); + SetupContents(status, ids); SetupSchema(); SetupScripting(ids.ToArray()); - SetupContents(status, ids); var ctx = context.WithArchived(archive).WithUnpublished(unpublished); @@ -420,9 +452,9 @@ namespace Squidex.Domain.Apps.Entities.Contents var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); SetupClaims(); + SetupContents(status, ids); SetupSchema(); SetupScripting(ids.ToArray()); - SetupContents(status, ids); var ctx = context.WithArchived(archive).WithUnpublished(unpublished); @@ -440,8 +472,8 @@ namespace Squidex.Domain.Apps.Entities.Contents var ids = Enumerable.Range(0, 1).Select(x => Guid.NewGuid()).ToList(); SetupClaims(false, false); - SetupSchema(); SetupContents(new Status[0], ids); + SetupSchema(); var ctx = context; @@ -492,19 +524,19 @@ namespace Squidex.Domain.Apps.Entities.Contents private void SetupContents(Status[] status, int count, int total, IContentEntity content) { - A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.IsSameSequenceAs(status), A.Ignored)) + A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.Is(status), A.Ignored)) .Returns(ResultList.Create(total, Enumerable.Repeat(content, count))); } private void SetupContents(Status[] status, int total, List ids) { - A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.IsSameSequenceAs(status), A>.Ignored)) + A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.Is(status), A>.Ignored)) .Returns(ResultList.Create(total, ids.Select(x => CreateContent(x)).Shuffle())); } private void SetupContents(Status[] status, List ids) { - A.CallTo(() => contentRepository.QueryAsync(app, A.That.IsSameSequenceAs(status), A>.Ignored)) + A.CallTo(() => contentRepository.QueryAsync(app, A.That.Is(status), A>.Ignored)) .Returns(ids.Select(x => (CreateContent(x), schema)).ToList()); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs new file mode 100644 index 000000000..ba4628463 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FakeItEasy; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Entities.TestHelpers +{ + public static class AExtensions + { + public static Query Is(this INegatableArgumentConstraintManager that, string query) + { + return that.Matches(x => x.ToString() == query); + } + + public static T[] Is(this INegatableArgumentConstraintManager that, params T[] values) + { + return that.IsSameSequenceAs(values); + } + } +}