Headless CMS and Content Managment Hub
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

488 lines
16 KiB

// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.Extensions.Logging;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
using Squidex.Shared;
namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
{
public sealed partial class ContentDomainObject : DomainObject<ContentDomainObject.State>
{
private readonly IServiceProvider serviceProvider;
public ContentDomainObject(IPersistenceFactory<State> persistence, ILogger<ContentDomainObject> log,
IServiceProvider serviceProvider)
: base(persistence, log)
{
this.serviceProvider = serviceProvider;
Capacity = 5;
}
protected override bool IsDeleted(State snapshot)
{
return snapshot.IsDeleted;
}
protected override bool CanAcceptCreation(ICommand command)
{
return command is CreateContent || command is UpsertContent;
}
protected override bool CanRecreate()
{
return true;
}
protected override bool CanRecreate(IEvent @event)
{
return @event is ContentCreated;
}
protected override bool CanAccept(ICommand command)
{
return command is ContentCommand contentCommand &&
Equals(contentCommand.AppId, Snapshot.AppId) &&
Equals(contentCommand.SchemaId, Snapshot.SchemaId) &&
Equals(contentCommand.ContentId, Snapshot.Id);
}
public override Task<CommandResult> ExecuteAsync(IAggregateCommand command)
{
switch (command)
{
case UpsertContent upsertContent:
return UpsertReturnAsync(upsertContent, async c =>
{
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
if (Version > EtagVersion.Empty && !IsDeleted(Snapshot))
{
await UpdateCore(c.AsUpdate(), operation);
}
else
{
await CreateCore(c.AsCreate(), operation);
}
if (Is.OptionalChange(operation.Snapshot.EditingStatus(), c.Status))
{
await ChangeCore(c.AsChange(c.Status.Value), operation);
}
return Snapshot;
});
case CreateContent createContent:
return CreateReturnAsync(createContent, async c =>
{
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await CreateCore(c, operation);
if (operation.Schema.SchemaDef.Type == SchemaType.Singleton)
{
ChangeStatus(c.AsChange(Status.Published));
}
else if (Is.OptionalChange(Snapshot.Status, c.Status))
{
await ChangeCore(c.AsChange(c.Status.Value), operation);
}
return Snapshot;
});
case ValidateContent validate:
return UpdateReturnAsync(validate, async c =>
{
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await ValidateCore(operation);
return true;
});
case CreateContentDraft createDraft:
return UpdateReturnAsync(createDraft, async c =>
{
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await CreateDraftCore(c, operation);
return Snapshot;
});
case DeleteContentDraft deleteDraft:
return UpdateReturnAsync(deleteDraft, async c =>
{
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
DeleteDraftCore(c, operation);
return Snapshot;
});
case PatchContent patchContent:
return UpdateReturnAsync(patchContent, async c =>
{
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await PatchCore(c, operation);
return Snapshot;
});
case UpdateContent updateContent:
return UpdateReturnAsync(updateContent, async c =>
{
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await UpdateCore(c, operation);
return Snapshot;
});
case CancelContentSchedule cancelContentSchedule:
return UpdateReturnAsync(cancelContentSchedule, async c =>
{
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
CancelChangeCore(c, operation);
return Snapshot;
});
case ChangeContentStatus changeContentStatus:
return UpdateReturnAsync(changeContentStatus, async c =>
{
try
{
if (c.DueTime > SystemClock.Instance.GetCurrentInstant())
{
ChangeStatusScheduled(c, c.DueTime.Value);
}
else
{
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await ChangeCore(c, operation);
}
}
catch (Exception)
{
if (Snapshot.ScheduleJob != null && Snapshot.ScheduleJob.Id == c.StatusJobId)
{
CancelChangeStatus(c);
}
else
{
throw;
}
}
return Snapshot;
});
case DeleteContent deleteContent when deleteContent.Permanent:
return DeletePermanentAsync(deleteContent, async c =>
{
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await DeleteCore(c, operation);
});
case DeleteContent deleteContent:
return UpdateAsync(deleteContent, async c =>
{
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await DeleteCore(c, operation);
});
default:
throw new NotSupportedException();
}
}
private async Task CreateCore(CreateContent c, ContentOperation operation)
{
operation.MustNotCreateSingleton();
operation.MustNotCreateForUnpublishedSchema();
operation.MustHaveData(c.Data);
if (!c.DoNotValidate)
{
await operation.ValidateInputAsync(c.Data, c.OptimizeValidation, Snapshot.IsPublished());
}
var status = await operation.GetInitialStatusAsync();
if (!c.DoNotScript)
{
c.Data = await operation.ExecuteCreateScriptAsync(c.Data, status);
}
operation.GenerateDefaultValues(c.Data);
if (!c.DoNotValidate)
{
await operation.ValidateContentAsync(c.Data, c.OptimizeValidation, Snapshot.IsPublished());
}
Create(c, status);
}
private async Task ChangeCore(ChangeContentStatus c, ContentOperation operation)
{
operation.MustHavePermission(Permissions.AppContentsChangeStatus);
operation.MustNotChangeSingleton(c.Status);
if (c.Status == Snapshot.EditingStatus())
{
return;
}
if (c.DoNotValidateWorkflow)
{
await operation.CheckStatusAsync(c.Status);
}
else
{
await operation.CheckTransitionAsync(c.Status);
}
if (!c.DoNotScript)
{
var newData = await operation.ExecuteChangeScriptAsync(c.Status, GetChange(c.Status));
if (!newData.Equals(Snapshot.Data))
{
var previousEvent =
GetUncomittedEvents().Select(x => x.Payload)
.OfType<ContentDataCommand>().FirstOrDefault();
if (previousEvent != null)
{
previousEvent.Data = newData;
}
else if (!newData.Equals(Snapshot.Data))
{
Update(c, newData);
}
}
}
if (c.CheckReferrers && Snapshot.IsPublished())
{
await operation.CheckReferrersAsync();
}
if (!c.DoNotValidate && c.Status == Status.Published && operation.SchemaDef.Properties.ValidateOnPublish)
{
await operation.ValidateContentAndInputAsync(Snapshot.Data, c.OptimizeValidation, true);
}
ChangeStatus(c);
}
private async Task UpdateCore(UpdateContent c, ContentOperation operation)
{
operation.MustHavePermission(Permissions.AppContentsUpdate);
operation.MustHaveData(c.Data);
if (!c.DoNotValidate)
{
await operation.ValidateInputAsync(c.Data, c.OptimizeValidation, Snapshot.IsPublished());
}
if (!c.DoNotValidateWorkflow)
{
await operation.CheckUpdateAsync();
}
var newData = c.Data;
if (newData.Equals(Snapshot.Data))
{
return;
}
if (!c.DoNotScript)
{
newData = await operation.ExecuteUpdateScriptAsync(newData);
}
if (!c.DoNotValidate)
{
await operation.ValidateContentAsync(newData, c.OptimizeValidation, Snapshot.IsPublished());
}
Update(c, newData);
}
private async Task PatchCore(UpdateContent c, ContentOperation operation)
{
operation.MustHavePermission(Permissions.AppContentsUpdate);
operation.MustHaveData(c.Data);
if (!c.DoNotValidate)
{
await operation.ValidateInputPartialAsync(c.Data, c.OptimizeValidation, Snapshot.IsPublished());
}
if (!c.DoNotValidateWorkflow)
{
await operation.CheckUpdateAsync();
}
var newData = c.Data.MergeInto(Snapshot.Data);
if (newData.Equals(Snapshot.Data))
{
return;
}
if (!c.DoNotScript)
{
newData = await operation.ExecuteUpdateScriptAsync(newData);
}
if (!c.DoNotValidate)
{
await operation.ValidateContentAsync(newData, c.OptimizeValidation, Snapshot.IsPublished());
}
Update(c, newData);
}
private void CancelChangeCore(CancelContentSchedule c, ContentOperation operation)
{
operation.MustHavePermission(Permissions.AppContentsChangeStatusCancel);
if (Snapshot.ScheduleJob != null)
{
CancelChangeStatus(c);
}
}
private async Task ValidateCore(ContentOperation operation)
{
operation.MustHavePermission(Permissions.AppContentsRead);
await operation.ValidateContentAndInputAsync(Snapshot.Data, false, Snapshot.IsPublished());
}
private async Task CreateDraftCore(CreateContentDraft c, ContentOperation operation)
{
operation.MustHavePermission(Permissions.AppContentsVersionCreate);
operation.MustCreateDraft();
var status = await operation.GetInitialStatusAsync();
CreateDraft(c, status);
}
private void DeleteDraftCore(DeleteContentDraft c, ContentOperation operation)
{
operation.MustHavePermission(Permissions.AppContentsVersionDelete);
operation.MustDeleteDraft();
DeleteDraft(c);
}
private async Task DeleteCore(DeleteContent c, ContentOperation operation)
{
operation.MustHavePermission(Permissions.AppContentsDelete);
operation.MustNotDeleteSingleton();
if (!c.DoNotScript)
{
await operation.ExecuteDeleteScriptAsync(c.Permanent);
}
if (c.CheckReferrers)
{
await operation.CheckReferrersAsync();
}
Delete(c);
}
private void Create(CreateContent command, Status status)
{
Raise(command, new ContentCreated { Status = status });
}
private void Update(ContentCommand command, ContentData data)
{
Raise(command, new ContentUpdated { Data = data });
}
private void ChangeStatus(ChangeContentStatus command)
{
Raise(command, new ContentStatusChanged { Change = GetChange(command.Status) });
}
private void ChangeStatusScheduled(ChangeContentStatus command, Instant dueTime)
{
Raise(command, new ContentStatusScheduled { DueTime = dueTime });
}
private void CancelChangeStatus(ContentCommand command)
{
Raise(command, new ContentSchedulingCancelled());
}
private void CreateDraft(CreateContentDraft command, Status status)
{
Raise(command, new ContentDraftCreated { Status = status });
}
private void Delete(DeleteContent command)
{
Raise(command, new ContentDeleted());
}
private void DeleteDraft(DeleteContentDraft command)
{
Raise(command, new ContentDraftDeleted());
}
private void Raise<T, TEvent>(T command, TEvent @event) where T : class where TEvent : AppEvent
{
RaiseEvent(Envelope.Create(SimpleMapper.Map(command, @event)));
}
private StatusChange GetChange(Status status)
{
if (status == Status.Published)
{
return StatusChange.Published;
}
else if (Snapshot.IsPublished())
{
return StatusChange.Unpublished;
}
else
{
return StatusChange.Change;
}
}
}
}