diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs index 1cb2f8191..b98ff890a 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -35,12 +35,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents [BsonRequired] [BsonElement("ai")] [BsonRepresentation(BsonType.String)] - public Guid IdxAppId { get; set; } + public Guid AppIdId { get; set; } [BsonRequired] [BsonElement("si")] [BsonRepresentation(BsonType.String)] - public Guid IdxSchemaId { get; set; } + public Guid SchemaIdId { get; set; } [BsonRequired] [BsonElement("rf")] @@ -71,12 +71,16 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public NamedId SchemaId { get; set; } [BsonIgnoreIfNull] - [BsonElement("pa")] - public Instant? PublishAt { get; set; } + [BsonElement("st")] + public Status? ScheduledTo { get; set; } + + [BsonIgnoreIfNull] + [BsonElement("sa")] + public Instant? ScheduledAt { get; set; } [BsonIgnoreIfNull] - [BsonElement("pb")] - public RefToken PublishAtBy { get; set; } + [BsonElement("sb")] + public RefToken ScheduledBy { get; set; } [BsonRequired] [BsonElement("ct")] diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index ac05250e7..1d4cc508f 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -52,6 +52,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { await collection.Indexes.TryDropOneAsync("si_1_st_1_dl_1_dt_text"); + await archiveCollection.Indexes.CreateOneAsync( + Index + .Ascending(x => x.ScheduledTo)); + await archiveCollection.Indexes.CreateOneAsync( Index .Ascending(x => x.Id) @@ -60,13 +64,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents await collection.Indexes.CreateOneAsync( Index .Text(x => x.DataText) - .Ascending(x => x.IdxSchemaId) + .Ascending(x => x.SchemaIdId) .Ascending(x => x.Status) .Ascending(x => x.IsDeleted)); await collection.Indexes.CreateOneAsync( Index - .Ascending(x => x.IdxSchemaId) + .Ascending(x => x.SchemaIdId) .Ascending(x => x.Id) .Ascending(x => x.IsDeleted) .Ascending(x => x.Status)); @@ -122,7 +126,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids) { - var find = Collection.Find(x => x.IdxSchemaId == schema.Id && ids.Contains(x.Id) && x.IsDeleted == false && status.Contains(x.Status)); + var find = Collection.Find(x => x.SchemaIdId == schema.Id && ids.Contains(x.Id) && x.IsDeleted == false && status.Contains(x.Status)); var contentItems = find.ToListAsync(); var contentCount = find.CountAsync(); @@ -140,7 +144,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public async Task> QueryNotFoundAsync(Guid appId, Guid schemaId, IList ids) { var contentEntities = - await Collection.Find(x => x.IdxSchemaId == schemaId && ids.Contains(x.Id) && x.IsDeleted == false).Only(x => x.Id) + await Collection.Find(x => x.SchemaIdId == schemaId && ids.Contains(x.Id) && x.IsDeleted == false).Only(x => x.Id) .ToListAsync(); return ids.Except(contentEntities.Select(x => Guid.Parse(x["id"].AsString))).ToList(); @@ -160,7 +164,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id) { var contentEntity = - await Collection.Find(x => x.IdxSchemaId == schema.Id && x.Id == id && x.IsDeleted == false) + await Collection.Find(x => x.SchemaIdId == schema.Id && x.Id == id && x.IsDeleted == false) .FirstOrDefaultAsync(); contentEntity?.ParseData(schema.SchemaDef); @@ -168,16 +172,20 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return contentEntity; } + public Task QueryScheduledWithoutDataAsync(Instant now, Func callback) + { + return Collection.Find(x => x.ScheduledAt < now && x.IsDeleted == false) + .ForEachAsync(c => + { + callback(c); + }); + } + public override async Task ClearAsync() { await Database.DropCollectionAsync("States_Contents_Archive"); await base.ClearAsync(); } - - public Task QueryContentToPublishAsync(Instant now, Func callback) - { - throw new NotSupportedException(); - } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs index 901ce545b..54e236e4a 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents if (contentEntity != null) { - var schema = await GetSchemaAsync(contentEntity.IdxAppId, contentEntity.IdxSchemaId); + var schema = await GetSchemaAsync(contentEntity.AppIdId, contentEntity.SchemaIdId); contentEntity?.ParseData(schema.SchemaDef); @@ -53,8 +53,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents var document = SimpleMapper.Map(value, new MongoContentEntity { - IdxAppId = value.AppId.Id, - IdxSchemaId = value.SchemaId.Id, + AppIdId = value.AppId.Id, + SchemaIdId = value.SchemaId.Id, IsDeleted = value.IsDeleted, DocumentId = key.ToString(), DataText = idData?.ToFullText(), diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs index 8f9118e1c..cdfaff9b8 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs @@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors { var filters = new List> { - Filter.Eq(x => x.IdxSchemaId, schemaId), + Filter.Eq(x => x.SchemaIdId, schemaId), Filter.In(x => x.Status, status), Filter.Eq(x => x.IsDeleted, false) }; diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs index 9e8de0bd2..5ca260cc4 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================= +using NodaTime; using Squidex.Domain.Apps.Core.Contents; namespace Squidex.Domain.Apps.Entities.Contents.Commands @@ -12,5 +13,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands public sealed class ChangeContentStatus : ContentCommand { public Status Status { get; set; } + + public Instant? DueDate { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/PublishContentAt.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/PublishContentAt.cs deleted file mode 100644 index 3b20c1aeb..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Commands/PublishContentAt.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Domain.Apps.Entities.Contents.Commands -{ - public sealed class PublishContentAt : ContentDataCommand - { - public DateTimeOffset PublishAt { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs index dec465b99..73f4619f1 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs @@ -109,9 +109,12 @@ namespace Squidex.Domain.Apps.Entities.Contents { GuardContent.CanChangeContentStatus(content.Snapshot.Status, command); - var operationContext = await CreateContext(command, content, () => "Failed to patch content."); + if (!command.DueDate.HasValue) + { + var operationContext = await CreateContext(command, content, () => "Failed to patch content."); - await operationContext.ExecuteScriptAsync(x => x.ScriptChange, command.Status); + await operationContext.ExecuteScriptAsync(x => x.ScriptChange, command.Status); + } content.ChangeStatus(command); }); @@ -131,16 +134,6 @@ namespace Squidex.Domain.Apps.Entities.Contents }); } - protected Task On(PublishContentAt command, CommandContext context) - { - return handler.UpdateAsync(context, content => - { - GuardContent.CanPublishAt(command); - - content.PublishAt(command); - }); - } - public async Task HandleAsync(CommandContext context, Func next) { await this.DispatchActionAsync(context.Command, context); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs index dd7e4fc05..e27a947a4 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs @@ -46,16 +46,14 @@ namespace Squidex.Domain.Apps.Entities.Contents { VerifyCreatedAndNotDeleted(); - RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged())); - - return this; - } - - public ContentDomainObject PublishAt(PublishContentAt command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new ContentPublishScheduled())); + if (command.DueDate.HasValue) + { + RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled())); + } + else + { + RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged())); + } return this; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs index 84269d0e5..27e1c1895 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs @@ -28,9 +28,13 @@ namespace Squidex.Domain.Apps.Entities.Contents public Instant LastModified { get; set; } - public Instant? PublishAt { get; set; } + public Status Status { get; set; } + + public Status? ScheduledTo { get; set; } + + public Instant? ScheduledAt { get; set; } - public RefToken PublishAtBy { get; set; } + public RefToken ScheduledBy { get; set; } public RefToken CreatedBy { get; set; } @@ -38,8 +42,6 @@ namespace Squidex.Domain.Apps.Entities.Contents public NamedContentData Data { get; set; } - public Status Status { get; set; } - public static ContentEntity Create(CreateContent command, EntityCreatedResult result) { var now = SystemClock.Instance.GetCurrentInstant(); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentPublisher.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentScheduler.cs similarity index 85% rename from src/Squidex.Domain.Apps.Entities/Contents/ContentPublisher.cs rename to src/Squidex.Domain.Apps.Entities/Contents/ContentScheduler.cs index 026a53296..23d3c05a9 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentPublisher.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentScheduler.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using NodaTime; -using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Infrastructure; @@ -16,14 +15,14 @@ using Squidex.Infrastructure.Timers; namespace Squidex.Domain.Apps.Entities.Contents { - public sealed class ContentPublisher : IRunnable + public sealed class ContentScheduler : IRunnable { private readonly CompletionTimer timer; private readonly IContentRepository contentRepository; private readonly ICommandBus commandBus; private readonly IClock clock; - public ContentPublisher( + public ContentScheduler( IContentRepository contentRepository, ICommandBus commandBus, IClock clock) @@ -47,9 +46,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var now = clock.GetCurrentInstant(); - return contentRepository.QueryContentToPublishAsync(now, content => + return contentRepository.QueryScheduledWithoutDataAsync(now, content => { - var command = new ChangeContentStatus { ContentId = content.Id, Status = Status.Published, Actor = content.PublishAtBy }; + var command = new ChangeContentStatus { ContentId = content.Id, Status = content.ScheduledTo.Value, Actor = content.ScheduledBy }; return commandBus.PublishAsync(command); }); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs index 092c144df..00cd6526a 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure; @@ -63,18 +64,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards { error(new ValidationError($"Content cannot be changed from status {status} to {command.Status}.", nameof(command.Status))); } - }); - } - - public static void CanPublishAt(PublishContentAt command) - { - Guard.NotNull(command, nameof(command)); - Validate.It(() => "Cannot schedule content tol publish.", error => - { - if (command.PublishAt < DateTime.UtcNow) + if (command.DueDate.HasValue && command.DueDate.Value < SystemClock.Instance.GetCurrentInstant()) { - error(new ValidationError("Date must be in the future.", nameof(command.PublishAt))); + error(new ValidationError("DueDate must be in the future.", nameof(command.DueDate))); } }); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs index d17ccc2e7..11a33154c 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs @@ -25,9 +25,11 @@ namespace Squidex.Domain.Apps.Entities.Contents Status Status { get; } - Instant? PublishAt { get; } + Status? ScheduledTo { get; } - RefToken PublishAtBy { get; } + Instant? ScheduledAt { get; } + + RefToken ScheduledBy { get; } NamedContentData Data { get; } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs index 3f925d049..b9aba61be 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -29,6 +29,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, long version); - Task QueryContentToPublishAsync(Instant now, Func callback); + Task QueryScheduledWithoutDataAsync(Instant now, Func callback); } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs index 75bb8f6d2..489a03832 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs @@ -33,10 +33,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.State public Status Status { get; set; } [JsonProperty] - public RefToken PublishAtBy { get; set; } + public Status? ScheduledTo { get; set; } [JsonProperty] - public Instant? PublishAt { get; set; } + public Instant? ScheduledAt { get; set; } + + [JsonProperty] + public RefToken ScheduledBy { get; set; } [JsonProperty] public bool IsDeleted { get; set; } @@ -55,18 +58,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.State Data = @event.Data; } - protected void On(ContentPublishScheduled @event) + protected void On(ContentStatusScheduled @event) { - PublishAt = @event.PublishAt; - PublishAtBy = @event.Actor; + ScheduledAt = @event.DueTime; + ScheduledBy = @event.Actor; + ScheduledTo = @event.Status; } protected void On(ContentStatusChanged @event) { Status = @event.Status; - PublishAt = null; - PublishAtBy = null; + ScheduledAt = null; + ScheduledBy = null; + ScheduledTo = null; } protected void On(ContentDeleted @event) diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentPublishScheduled.cs b/src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs similarity index 67% rename from src/Squidex.Domain.Apps.Events/Contents/ContentPublishScheduled.cs rename to src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs index 50ef1581f..e0d0a5bac 100644 --- a/src/Squidex.Domain.Apps.Events/Contents/ContentPublishScheduled.cs +++ b/src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs @@ -6,13 +6,16 @@ // ========================================================================== using NodaTime; +using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Events.Contents { - [EventType(nameof(ContentPublishScheduled))] - public sealed class ContentPublishScheduled : ContentEvent + [EventType(nameof(ContentStatusScheduled))] + public sealed class ContentStatusScheduled : ContentEvent { - public Instant PublishAt { get; set; } + public Status Status { get; set; } + + public Instant DueTime { get; set; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs index 9fb56d378..812bdc9cd 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs @@ -10,6 +10,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using NodaTime; +using NodaTime.Text; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Contents.Models; using Squidex.Domain.Apps.Core.Contents; @@ -213,11 +215,11 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpPut] [Route("content/{app}/{name}/{id}/publish/")] [ApiCosts(1)] - public async Task PublishContent(string name, Guid id) + public async Task PublishContent(string name, Guid id, string dueDate = null) { await contentQuery.FindSchemaAsync(App, name); - var command = new ChangeContentStatus { Status = Status.Published, ContentId = id }; + var command = CreateCommand(id, Status.Published, dueDate); await CommandBus.PublishAsync(command); @@ -228,11 +230,11 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpPut] [Route("content/{app}/{name}/{id}/unpublish/")] [ApiCosts(1)] - public async Task UnpublishContent(string name, Guid id) + public async Task UnpublishContent(string name, Guid id, string dueDate = null) { await contentQuery.FindSchemaAsync(App, name); - var command = new ChangeContentStatus { Status = Status.Draft, ContentId = id }; + var command = CreateCommand(id, Status.Draft, dueDate); await CommandBus.PublishAsync(command); @@ -243,11 +245,11 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpPut] [Route("content/{app}/{name}/{id}/archive/")] [ApiCosts(1)] - public async Task ArchiveContent(string name, Guid id) + public async Task ArchiveContent(string name, Guid id, string dueDate = null) { await contentQuery.FindSchemaAsync(App, name); - var command = new ChangeContentStatus { Status = Status.Archived, ContentId = id }; + var command = CreateCommand(id, Status.Archived, dueDate); await CommandBus.PublishAsync(command); @@ -258,11 +260,11 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpPut] [Route("content/{app}/{name}/{id}/restore/")] [ApiCosts(1)] - public async Task RestoreContent(string name, Guid id) + public async Task RestoreContent(string name, Guid id, string dueDate = null) { await contentQuery.FindSchemaAsync(App, name); - var command = new ChangeContentStatus { Status = Status.Draft, ContentId = id }; + var command = CreateCommand(id, Status.Draft, dueDate); await CommandBus.PublishAsync(command); @@ -283,5 +285,22 @@ namespace Squidex.Areas.Api.Controllers.Contents return NoContent(); } + + private static ChangeContentStatus CreateCommand(Guid id, Status status, string dueDate) + { + Instant? dt = null; + + if (string.IsNullOrWhiteSpace(dueDate)) + { + var parseResult = InstantPattern.General.Parse(dueDate); + + if (!parseResult.Success) + { + dt = parseResult.Value; + } + } + + return new ChangeContentStatus { Status = status, ContentId = id, DueDate = dt }; + } } }