diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs index 5d93788fe..b67536690 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs @@ -73,16 +73,11 @@ namespace Squidex.Domain.Apps.Entities.Contents this.scriptEngine = scriptEngine; } - public Task ThrowIfSchemaNotExistsAsync(QueryContext context, string schemaIdOrName) - { - return GetSchemaAsync(context, schemaIdOrName); - } - public async Task FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = -1) { Guard.NotNull(context, nameof(context)); - var schema = await GetSchemaAsync(context, schemaIdOrName); + var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); CheckPermission(context.User, schema); @@ -110,7 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { Guard.NotNull(context, nameof(context)); - var schema = await GetSchemaAsync(context, schemaIdOrName); + var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); CheckPermission(context.User, schema); @@ -136,7 +131,7 @@ namespace Squidex.Domain.Apps.Entities.Contents } } - public async Task> QueryAsync(QueryContext context, IReadOnlyList ids) + public async Task> QueryAsync(QueryContext context, IReadOnlyList ids) { Guard.NotNull(context, nameof(context)); @@ -295,7 +290,7 @@ namespace Squidex.Domain.Apps.Entities.Contents } } - public async Task GetSchemaAsync(QueryContext context, string schemaIdOrName) + public async Task GetSchemaOrThrowAsync(QueryContext context, string schemaIdOrName) { ISchemaEntity schema = null; diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs index 75d89c115..769422de1 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Contents @@ -16,12 +17,12 @@ namespace Squidex.Domain.Apps.Entities.Contents { int DefaultPageSizeGraphQl { get; } - Task> QueryAsync(QueryContext context, IReadOnlyList ids); + 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); - Task ThrowIfSchemaNotExistsAsync(QueryContext context, string schemaIdOrName); + Task GetSchemaOrThrowAsync(QueryContext context, string schemaIdOrName); } } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 675833005..a8a4d1e87 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -16,6 +16,7 @@ using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.GraphQL; +using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Shared; using Squidex.Web; @@ -26,15 +27,18 @@ namespace Squidex.Areas.Api.Controllers.Contents { private readonly IOptions controllerOptions; private readonly IContentQueryService contentQuery; + private readonly IContentWorkflow contentWorkflow; private readonly IGraphQLService graphQl; public ContentsController(ICommandBus commandBus, IContentQueryService contentQuery, + IContentWorkflow contentWorkflow, IGraphQLService graphQl, IOptions controllerOptions) : base(commandBus) { this.contentQuery = contentQuery; + this.contentWorkflow = contentWorkflow; this.controllerOptions = controllerOptions; this.graphQl = graphQl; @@ -123,8 +127,9 @@ namespace Squidex.Areas.Api.Controllers.Contents { var context = Context(); var contents = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids).Ids); + var contentsList = ResultList.Create(contents.Count, contents); - var response = ContentsDto.FromContents(contents.Count, contents, context, this, app, null); + var response = await ContentsDto.FromContentsAsync(contentsList, context, this, null, contentWorkflow); if (controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys) { @@ -159,7 +164,9 @@ namespace Squidex.Areas.Api.Controllers.Contents var context = Context(); var contents = await contentQuery.QueryAsync(context, name, Q.Empty.WithIds(ids).WithODataQuery(Request.QueryString.ToString())); - var response = ContentsDto.FromContents(contents.Total, contents, context, this, app, name); + var schema = await contentQuery.GetSchemaOrThrowAsync(context, name); + + var response = await ContentsDto.FromContentsAsync(contents, context, this, schema, contentWorkflow); if (ShouldProvideSurrogateKeys(response)) { @@ -194,7 +201,7 @@ namespace Squidex.Areas.Api.Controllers.Contents var context = Context(); var content = await contentQuery.FindContentAsync(context, name, id); - var response = ContentDto.FromContent(content, context, this, app, name); + var response = await ContentDto.FromContentAsync(context, content, contentWorkflow, this); if (controllerOptions.Value.EnableSurrogateKeys) { @@ -230,7 +237,7 @@ namespace Squidex.Areas.Api.Controllers.Contents var context = Context(); var content = await contentQuery.FindContentAsync(context, name, id, version); - var response = ContentDto.FromContent(content, context, this, app, name); + var response = await ContentDto.FromContentAsync(context, content, contentWorkflow, this); if (controllerOptions.Value.EnableSurrogateKeys) { @@ -264,7 +271,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); + await contentQuery.GetSchemaOrThrowAsync(Context(), name); if (publish && !this.HasPermission(Helper.StatusPermission(app, name, Status.Published))) { @@ -301,7 +308,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task PutContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); + await contentQuery.GetSchemaOrThrowAsync(Context(), name); var command = new UpdateContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft }; @@ -333,7 +340,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task PatchContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); + await contentQuery.GetSchemaOrThrowAsync(Context(), name); var command = new PatchContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft }; @@ -364,9 +371,9 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task PutContentStatus(string app, string name, Guid id, ChangeStatusDto request) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); + await contentQuery.GetSchemaOrThrowAsync(Context(), name); - if (!this.HasPermission(Helper.StatusPermission(app, name, Status.Published))) + if (!this.HasPermission(Helper.StatusPermission(app, name, Status2.Published))) { return new ForbidResult(); } @@ -399,7 +406,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task DiscardDraft(string app, string name, Guid id) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); + await contentQuery.GetSchemaOrThrowAsync(Context(), name); var command = new DiscardChanges { ContentId = id }; @@ -427,7 +434,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task DeleteContent(string app, string name, Guid id) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); + await contentQuery.GetSchemaOrThrowAsync(Context(), name); var command = new DeleteContent { ContentId = id }; @@ -441,7 +448,7 @@ namespace Squidex.Areas.Api.Controllers.Contents var context = await CommandBus.PublishAsync(command); var result = context.Result(); - var response = ContentDto.FromContent(result, null, this, app, schema); + var response = await ContentDto.FromContentAsync(null, result, contentWorkflow, this); return response; } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Helper.cs b/src/Squidex/Areas/Api/Controllers/Contents/Helper.cs index e65648c21..be1f7ef46 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Helper.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Helper.cs @@ -19,5 +19,12 @@ namespace Squidex.Areas.Api.Controllers.Contents return Permissions.ForApp(id, app, schema); } + + public static Permission StatusPermission(string app, string schema, Status2 status) + { + var id = Permissions.AppContentsStatus.Replace("{status}", status.Name); + + return Permissions.ForApp(id, app, schema); + } } } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs index 3939cfd67..c699d7017 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs @@ -7,6 +7,7 @@ using System; using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.ConvertContent; @@ -79,7 +80,11 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models /// public long Version { get; set; } - public static ContentDto FromContent(IContentEntity content, QueryContext context, ApiController controller, string app, string schema) + public static ValueTask FromContentAsync( + QueryContext context, + IContentEntity content, + IContentWorkflow contentWorkflow, + ApiController controller) { var response = SimpleMapper.Map(content, new ContentDto()); @@ -99,10 +104,14 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models response.ScheduleJob = SimpleMapper.Map(content.ScheduleJob, new ScheduleJobDto()); } - return response.CreateLinks(controller, app, schema); + return response.CreateLinksAsync(content, controller, content.AppId.Name, content.SchemaId.Name, contentWorkflow); } - private ContentDto CreateLinks(ApiController controller, string app, string schema) + private async ValueTask CreateLinksAsync(IContentEntity content, + ApiController controller, + string app, + string schema, + IContentWorkflow contentWorkflow) { var values = new { app, name = schema, id = Id }; @@ -122,7 +131,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models AddPutLink("draft/discard", controller.Url(x => nameof(x.DiscardDraft), values)); } - if (controller.HasPermission(Helper.StatusPermission(app, schema, Status.Published))) + if (controller.HasPermission(Helper.StatusPermission(app, schema, Status2.Published))) { AddPutLink("draft/publish", controller.Url(x => nameof(x.PutContentStatus), values)); } @@ -130,7 +139,10 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models if (controller.HasPermission(Permissions.AppContentsUpdate, app, schema)) { - AddPutLink("update", controller.Url(x => nameof(x.PutContent), values)); + if (await contentWorkflow.CanUpdateAsync(content)) + { + AddPutLink("update", controller.Url(x => nameof(x.PutContent), values)); + } if (Status == Status.Published) { @@ -145,7 +157,9 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models AddPutLink("delete", controller.Url(x => nameof(x.DeleteContent), values)); } - foreach (var next in StatusFlow.Next(Status)) + var nextStatuses = await contentWorkflow.GetNextsAsync(content); + + foreach (var next in nextStatuses) { if (controller.HasPermission(Helper.StatusPermission(app, schema, next))) { diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs index 9f664880e..928cc12b2 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs @@ -5,12 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Shared; using Squidex.Web; @@ -34,7 +35,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models /// The possible statuses. /// [Required] - public string[] Statuses { get; set; } + public Status2[] Statuses { get; set; } public string ToEtag() { @@ -46,20 +47,37 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models return Items.ToSurrogateKeys(); } - public static ContentsDto FromContents(long total, IEnumerable contents, QueryContext context, + public static async Task FromContentsAsync(IResultList contents, QueryContext context, ApiController controller, - string app, - string schema) + ISchemaEntity schema, + IContentWorkflow contentWorkflow) { var result = new ContentsDto { - Total = total, - Items = contents.Select(x => ContentDto.FromContent(x, context, controller, app, schema)).ToArray(), + Total = contents.Total, + Items = new ContentDto[contents.Count] }; - result.Statuses = new string[] { "Archived", "Draft", "Published" }; + await Task.WhenAll( + result.AssignContentsAsync(contentWorkflow, contents, context, controller), + result.AssignStatusesAsync(contentWorkflow, schema)); - return result.CreateLinks(controller, app, schema); + return result.CreateLinks(controller, schema.AppId.Name, schema.SchemaDef.Name); + } + + private async Task AssignStatusesAsync(IContentWorkflow contentWorkflow, ISchemaEntity schema) + { + var allStatuses = await contentWorkflow.GetAllAsync(schema); + + Statuses = allStatuses.ToArray(); + } + + private async Task AssignContentsAsync(IContentWorkflow contentWorkflow, IResultList contents, QueryContext context, ApiController controller) + { + for (var i = 0; i < Items.Length; i++) + { + Items[i] = await ContentDto.FromContentAsync(context, contents[i], contentWorkflow, controller); + } } private ContentsDto CreateLinks(ApiController controller, string app, string schema) diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs index 9ca83efa0..546454b83 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs @@ -103,7 +103,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { SetupSchemaFound(); - var result = await sut.GetSchemaAsync(context, schemaId.Name); + var result = await sut.GetSchemaOrThrowAsync(context, schemaId.Name); Assert.Equal(schema, result); } @@ -113,7 +113,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { SetupSchemaFound(); - var result = await sut.GetSchemaAsync(context, schemaId.Name); + var result = await sut.GetSchemaOrThrowAsync(context, schemaId.Name); Assert.Equal(schema, result); } @@ -125,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var ctx = context; - await Assert.ThrowsAsync(() => sut.GetSchemaAsync(ctx, schemaId.Name)); + await Assert.ThrowsAsync(() => sut.GetSchemaOrThrowAsync(ctx, schemaId.Name)); } [Fact] @@ -135,7 +135,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var ctx = context; - await Assert.ThrowsAsync(() => sut.ThrowIfSchemaNotExistsAsync(ctx, schemaId.Name)); + await Assert.ThrowsAsync(() => sut.GetSchemaOrThrowAsync(ctx, schemaId.Name)); } [Fact]