From e0e9abb26bfefeef1fe0a26bc31d988214554e36 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 26 Mar 2019 21:53:28 +0100 Subject: [PATCH] Get contents by ids only. --- .../Contents/MongoContentCollection.cs | 34 ++++++- .../Contents/MongoContentDraftCollection.cs | 9 +- .../MongoContentPublishedCollection.cs | 18 +++- .../Contents/MongoContentRepository.cs | 19 +++- .../Contents/ContentQueryService.cs | 92 ++++++++++++++----- .../Contents/IContentQueryService.cs | 3 + .../Repositories/IContentRepository.cs | 2 + .../Contents/ContentsController.cs | 41 ++++++++- .../Contents/ContentQueryServiceTests.cs | 88 +++++++++++++++++- 9 files changed, 270 insertions(+), 36 deletions(-) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs index f77be31ba..affc3bcb5 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs @@ -25,15 +25,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { internal class MongoContentCollection : MongoRepositoryBase { + private readonly IAppProvider appProvider; private readonly string collectionName; protected IJsonSerializer Serializer { get; } - public MongoContentCollection(IMongoDatabase database, IJsonSerializer serializer, string collectionName) + public MongoContentCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider, string collectionName) : base(database) { this.collectionName = collectionName; + this.appProvider = appProvider; + Serializer = serializer; } @@ -87,6 +90,35 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } } + public async Task> QueryAsync(IAppEntity app, HashSet ids, Status[] status = null) + { + var find = + status != null && status.Length > 0 ? + Collection.Find(x => x.IndexedAppId == app.Id && ids.Contains(x.Id) && x.IsDeleted != true && status.Contains(x.Status)) : + Collection.Find(x => x.IndexedAppId == app.Id && ids.Contains(x.Id)); + + var contentItems = await find.Not(x => x.DataText).ToListAsync(); + + var schemaIds = contentItems.Select(x => x.IndexedSchemaId).ToList(); + var schemas = await Task.WhenAll(schemaIds.Select(x => appProvider.GetSchemaAsync(app.Id, x))); + + var result = new List<(IContentEntity Content, ISchemaEntity Schema)>(); + + foreach (var entity in contentItems) + { + var schema = schemas.FirstOrDefault(x => x.Id == entity.IndexedSchemaId); + + if (schema != null) + { + entity.ParseData(schema.SchemaDef, Serializer); + + result.Add((entity, schema)); + } + } + + return result; + } + public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet ids, Status[] status = null) { var find = diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentDraftCollection.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentDraftCollection.cs index 30c0edda0..bdec55cab 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentDraftCollection.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentDraftCollection.cs @@ -29,8 +29,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { internal sealed class MongoContentDraftCollection : MongoContentCollection { - public MongoContentDraftCollection(IMongoDatabase database, IJsonSerializer serializer) - : base(database, serializer, "State_Content_Draft") + public MongoContentDraftCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider) + : base(database, serializer, appProvider, "State_Content_Draft") { } @@ -39,6 +39,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents await collection.Indexes.CreateManyAsync( new[] { + new CreateIndexModel( + Index + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.Id) + .Ascending(x => x.IsDeleted)), new CreateIndexModel( Index .Ascending(x => x.IndexedSchemaId) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentPublishedCollection.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentPublishedCollection.cs index 0184b87e1..c210297a8 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentPublishedCollection.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentPublishedCollection.cs @@ -20,8 +20,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { internal sealed class MongoContentPublishedCollection : MongoContentCollection { - public MongoContentPublishedCollection(IMongoDatabase database, IJsonSerializer serializer) - : base(database, serializer, "State_Content_Published") + public MongoContentPublishedCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider) + : base(database, serializer, appProvider, "State_Content_Published") { } @@ -30,8 +30,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents await collection.Indexes.CreateManyAsync( new[] { - new CreateIndexModel(Index.Text(x => x.DataText).Ascending(x => x.IndexedSchemaId)), - new CreateIndexModel(Index.Ascending(x => x.IndexedSchemaId).Ascending(x => x.Id)) + new CreateIndexModel( + Index + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.Id)), + new CreateIndexModel( + Index + .Ascending(x => x.IndexedSchemaId) + .Ascending(x => x.Id)), + new CreateIndexModel( + Index + .Text(x => x.DataText) + .Ascending(x => x.IndexedSchemaId)) }, ct); await base.SetupCollectionAsync(collection, ct); diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 3c663d642..8adc0a065 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -40,8 +40,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents this.serializer = serializer; - contentsDraft = new MongoContentDraftCollection(database, serializer); - contentsPublished = new MongoContentPublishedCollection(database, serializer); + contentsDraft = new MongoContentDraftCollection(database, serializer, appProvider); + contentsPublished = new MongoContentPublishedCollection(database, serializer, appProvider); this.database = database; } @@ -81,6 +81,21 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } } + public async Task> QueryAsync(IAppEntity app, Status[] status, HashSet ids) + { + using (Profiler.TraceMethod("QueryAsyncByIdsWithoutSchema")) + { + if (RequiresPublished(status)) + { + return await contentsPublished.QueryAsync(app, ids); + } + else + { + return await contentsDraft.QueryAsync(app, ids, status); + } + } + } + public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id) { using (Profiler.TraceMethod()) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs index 14570304c..2d90d55db 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs @@ -23,6 +23,7 @@ using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries.OData; using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Security; using Squidex.Shared; using Squidex.Shared.Identity; @@ -81,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var schema = await GetSchemaAsync(context, schemaIdOrName); - CheckPermission(schema, context.User); + CheckPermission(context.User, schema); using (Profiler.TraceMethod()) { @@ -99,7 +100,7 @@ namespace Squidex.Domain.Apps.Entities.Contents throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity)); } - return Transform(context, schema, true, content); + return Transform(context, schema, content); } } @@ -109,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var schema = await GetSchemaAsync(context, schemaIdOrName); - CheckPermission(schema, context.User); + CheckPermission(context.User, schema); using (Profiler.TraceMethod()) { @@ -120,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Contents if (query.Ids?.Count > 0) { contents = await contentRepository.QueryAsync(context.App, schema, status, new HashSet(query.Ids)); - contents = Sort(contents, query.Ids); + contents = SortSet(contents, query.Ids); } else { @@ -129,34 +130,67 @@ namespace Squidex.Domain.Apps.Entities.Contents contents = await contentRepository.QueryAsync(context.App, schema, status, parsedQuery); } - return Transform(context, schema, true, contents); + return Transform(context, schema, contents); } } - private IContentEntity Transform(QueryContext context, ISchemaEntity schema, bool checkType, IContentEntity content) + public async Task> QueryAsync(QueryContext context, IReadOnlyList ids) { - return TransformCore(context, schema, checkType, Enumerable.Repeat(content, 1)).FirstOrDefault(); + Guard.NotNull(context, nameof(context)); + + using (Profiler.TraceMethod()) + { + var status = GetQueryStatus(context); + + List result; + + if (ids?.Count > 0) + { + var contents = await contentRepository.QueryAsync(context.App, status, new HashSet(ids)); + + var permissions = context.User.Permissions(); + + contents = contents.Where(x => HasPermission(permissions, x.Schema)).ToList(); + + result = contents.Select(x => Transform(context, x.Schema, x.Content)).ToList(); + result = SortList(result, ids).ToList(); + } + else + { + result = new List(); + } + + return result; + } } - private IResultList Transform(QueryContext context, ISchemaEntity schema, bool checkType, IResultList contents) + private IResultList Transform(QueryContext context, ISchemaEntity schema, IResultList contents) { - var transformed = TransformCore(context, schema, checkType, contents); + var transformed = TransformCore(context, schema, contents); return ResultList.Create(contents.Total, transformed); } - private static IResultList Sort(IResultList contents, IReadOnlyList ids) + private IContentEntity Transform(QueryContext context, ISchemaEntity schema, IContentEntity content) { - var sorted = ids.Select(id => contents.FirstOrDefault(x => x.Id == id)).Where(x => x != null); + return TransformCore(context, schema, Enumerable.Repeat(content, 1)).FirstOrDefault(); + } + + private static IResultList SortSet(IResultList contents, IReadOnlyList ids) + { + return ResultList.Create(contents.Total, SortList(contents, ids)); + } - return ResultList.Create(contents.Total, sorted); + private static IEnumerable SortList(IEnumerable contents, IReadOnlyList ids) + { + return ids.Select(id => contents.FirstOrDefault(x => x.Id == id)).Where(x => x != null); } - private IEnumerable TransformCore(QueryContext context, ISchemaEntity schema, bool checkType, IEnumerable contents) + private IEnumerable TransformCore(QueryContext context, ISchemaEntity schema, IEnumerable contents) { using (Profiler.TraceMethod()) { - var converters = GenerateConverters(context, checkType).ToArray(); + var converters = GenerateConverters(context).ToArray(); var scriptText = schema.SchemaDef.Scripts.Query; @@ -170,7 +204,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { if (!context.IsFrontendClient && isScripting) { - result.Data = scriptEngine.Transform(new ScriptContext { User = context.User, Data = content.Data, ContentId = content.Id }, scriptText); + var ctx = new ScriptContext { User = context.User, Data = content.Data, ContentId = content.Id }; + + result.Data = scriptEngine.Transform(ctx, scriptText); } result.Data = result.Data.ConvertName2Name(schema.SchemaDef, converters); @@ -186,7 +222,7 @@ namespace Squidex.Domain.Apps.Entities.Contents } } - private IEnumerable GenerateConverters(QueryContext context, bool checkType) + private IEnumerable GenerateConverters(QueryContext context) { if (!context.IsFrontendClient) { @@ -194,11 +230,8 @@ namespace Squidex.Domain.Apps.Entities.Contents yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeHidden()); } - if (checkType) - { - yield return FieldConverters.ExcludeChangedTypes(); - yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeChangedTypes()); - } + yield return FieldConverters.ExcludeChangedTypes(); + yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeChangedTypes()); yield return FieldConverters.ResolveInvariant(context.App.LanguagesConfig); yield return FieldConverters.ResolveLanguages(context.App.LanguagesConfig); @@ -274,17 +307,26 @@ namespace Squidex.Domain.Apps.Entities.Contents return schema; } - private static void CheckPermission(ISchemaEntity schema, ClaimsPrincipal user) + private static void CheckPermission(ClaimsPrincipal user, params ISchemaEntity[] schemas) { var permissions = user.Permissions(); - var permission = Permissions.ForApp(Permissions.AppContentsRead, schema.AppId.Name, schema.SchemaDef.Name); - if (!permissions.Allows(permission)) + foreach (var schema in schemas) { - throw new DomainForbiddenException("You do not have permission for this schema."); + if (!HasPermission(permissions, schema)) + { + throw new DomainForbiddenException("You do not have permission for this schema."); + } } } + private static bool HasPermission(PermissionSet permissions, ISchemaEntity schema) + { + var permission = Permissions.ForApp(Permissions.AppContentsRead, schema.AppId.Name, schema.SchemaDef.Name); + + return permissions.Allows(permission); + } + private static Status[] GetFindStatus(QueryContext context) { if (context.IsFrontendClient) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs index 0512c3c66..f93bdc1d7 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Infrastructure; @@ -13,6 +14,8 @@ namespace Squidex.Domain.Apps.Entities.Contents { public interface IContentQueryService { + Task> QueryAsync(QueryContext context, IReadOnlyList ids); + Task> QueryAsync(QueryContext context, string schemaIdOrName, Q query); Task FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = EtagVersion.Any); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs index e82d39932..56ac77ede 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -19,6 +19,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories { public interface IContentRepository { + Task> QueryAsync(IAppEntity app, Status[] status, HashSet ids); + Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids); Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Query query); diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 4ea013deb..df849dd56 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -106,6 +106,45 @@ namespace Squidex.Areas.Api.Controllers.Contents } } + /// + /// Queries contents. + /// + /// The name of the app. + /// The optional ids of the content to fetch. + /// Indicates whether to query content items from the archive. + /// + /// 200 => Contents retrieved. + /// 404 => App not found. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs + /// + [HttpGet] + [Route("content/{app}/")] + [ApiPermission] + [ApiCosts(1)] + public async Task GetAllContents(string app, [FromQuery] string ids, [FromQuery] bool archived = false) + { + var context = Context().WithArchived(archived); + + var result = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids).Ids); + + var response = new ContentsDto + { + Total = result.Count, + Items = result.Take(200).Select(x => ContentDto.FromContent(x, context)).ToArray() + }; + + if (controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys) + { + Response.Headers["Surrogate-Key"] = response.Items.ToSurrogateKeys(); + } + + Response.Headers[HeaderNames.ETag] = response.Items.ToManyEtag(); + + return Ok(response); + } + /// /// Queries contents. /// @@ -124,7 +163,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [Route("content/{app}/{name}/")] [ApiPermission] [ApiCosts(1)] - public async Task GetContents(string app, string name, [FromQuery] bool archived = false, [FromQuery] string ids = null) + public async Task GetContents(string app, string name, [FromQuery] string ids = null, [FromQuery] bool archived = false) { var context = Context().WithArchived(archived); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs index d8035841b..cbf1b5d0b 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs @@ -358,7 +358,7 @@ namespace Squidex.Domain.Apps.Entities.Contents [Theory] [MemberData(nameof(ManyIdDataApi))] - public async Task Should_query_contents_by_id_from_repository_and_transform(bool archive, bool unpublished, params Status[] status) + public async Task Should_query_contents_by_id_for_api_and_transform(bool archive, bool unpublished, params Status[] status) { const int count = 5, total = 200; @@ -380,6 +380,86 @@ namespace Squidex.Domain.Apps.Entities.Contents .MustHaveHappened(count, Times.Exactly); } + [Theory] + [MemberData(nameof(ManyIdDataFrontend))] + public async Task Should_query_all_contents_by_id_for_frontend_and_transform(bool archive, bool unpublished, params Status[] status) + { + const int count = 5; + + var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); + + SetupClaims(true); + SetupSchema(); + SetupScripting(ids.ToArray()); + SetupContents(status, ids); + + var ctx = context.WithArchived(archive).WithUnpublished(unpublished); + + var result = await sut.QueryAsync(ctx, ids); + + Assert.Equal(ids, result.Select(x => x.Id).ToList()); + + A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Theory] + [MemberData(nameof(ManyIdDataApi))] + public async Task Should_query_all_contents_by_id_for_api_and_transform(bool archive, bool unpublished, params Status[] status) + { + const int count = 5; + + var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); + + SetupClaims(); + SetupSchema(); + SetupScripting(ids.ToArray()); + SetupContents(status, ids); + + var ctx = context.WithArchived(archive).WithUnpublished(unpublished); + + var result = await sut.QueryAsync(ctx, ids); + + Assert.Equal(ids, result.Select(x => x.Id).ToList()); + + A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) + .MustHaveHappened(count, Times.Exactly); + } + + [Fact] + public async Task Should_skip_contents_when_user_has_no_permission() + { + var ids = Enumerable.Range(0, 1).Select(x => Guid.NewGuid()).ToList(); + + SetupClaims(false, false); + SetupSchema(); + SetupContents(new Status[0], ids); + + var ctx = context; + + var result = await sut.QueryAsync(ctx, ids); + + Assert.Empty(result); + } + + [Fact] + public async Task Should_not_call_repository_if_no_id_defined() + { + var ids = new List(); + + SetupClaims(false, false); + SetupSchema(); + + var ctx = context; + + var result = await sut.QueryAsync(ctx, ids); + + Assert.Empty(result); + + A.CallTo(() => contentRepository.QueryAsync(app, A.Ignored, A>.Ignored)) + .MustNotHaveHappened(); + } + private void SetupClaims(bool isFrontend = false, bool allowSchema = true) { if (isFrontend) @@ -414,6 +494,12 @@ namespace Squidex.Domain.Apps.Entities.Contents .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)) + .Returns(ids.Select(x => (CreateContent(x), schema)).ToList()); + } + private void SetupSchema() { A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name))