Browse Source

Scheduler improved.

pull/247/head
Sebastian Stehle 8 years ago
parent
commit
3e980ddd6f
  1. 16
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
  2. 28
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  3. 6
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs
  4. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs
  5. 3
      src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs
  6. 16
      src/Squidex.Domain.Apps.Entities/Contents/Commands/PublishContentAt.cs
  7. 17
      src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs
  8. 18
      src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs
  9. 10
      src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs
  10. 9
      src/Squidex.Domain.Apps.Entities/Contents/ContentScheduler.cs
  11. 13
      src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs
  12. 6
      src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs
  13. 2
      src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
  14. 19
      src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs
  15. 9
      src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs
  16. 35
      src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs

16
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<Guid> 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")]

28
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<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> 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<IReadOnlyList<Guid>> QueryNotFoundAsync(Guid appId, Guid schemaId, IList<Guid> 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<IContentEntity> 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<IContentEntity, Task> 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<IContentEntity, Task> callback)
{
throw new NotSupportedException();
}
}
}

6
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(),

2
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<FilterDefinition<MongoContentEntity>>
{
Filter.Eq(x => x.IdxSchemaId, schemaId),
Filter.Eq(x => x.SchemaIdId, schemaId),
Filter.In(x => x.Status, status),
Filter.Eq(x => x.IsDeleted, false)
};

3
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; }
}
}

16
src/Squidex.Domain.Apps.Entities/Contents/Commands/PublishContentAt.cs

@ -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; }
}
}

17
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<ContentDomainObject>(context, content =>
{
GuardContent.CanPublishAt(command);
content.PublishAt(command);
});
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
await this.DispatchActionAsync(context.Command, context);

18
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;
}

10
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<NamedContentData> result)
{
var now = SystemClock.Instance.GetCurrentInstant();

9
src/Squidex.Domain.Apps.Entities/Contents/ContentPublisher.cs → 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);
});

13
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)));
}
});
}

6
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; }
}

2
src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs

@ -29,6 +29,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories
Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, long version);
Task QueryContentToPublishAsync(Instant now, Func<IContentEntity, Task> callback);
Task QueryScheduledWithoutDataAsync(Instant now, Func<IContentEntity, Task> callback);
}
}

19
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)

9
src/Squidex.Domain.Apps.Events/Contents/ContentPublishScheduled.cs → 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; }
}
}

35
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<IActionResult> PublishContent(string name, Guid id)
public async Task<IActionResult> 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<IActionResult> UnpublishContent(string name, Guid id)
public async Task<IActionResult> 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<IActionResult> ArchiveContent(string name, Guid id)
public async Task<IActionResult> 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<IActionResult> RestoreContent(string name, Guid id)
public async Task<IActionResult> 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 };
}
}
}

Loading…
Cancel
Save