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.
 
 
 
 
 

415 lines
14 KiB

// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting;
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.Log;
namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
{
public sealed partial class ContentDomainObject : DomainObject<ContentDomainObject.State>
{
private readonly ContentOperationContext context;
public ContentDomainObject(IStore<DomainId> store, ISemanticLog log,
ContentOperationContext context)
: base(store, log)
{
Guard.NotNull(context, nameof(context));
this.context = context;
Capacity = int.MaxValue;
}
protected override bool IsDeleted()
{
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 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 =>
{
await LoadContext(c, c.OptimizeValidation);
if (Version > EtagVersion.Empty && !IsDeleted())
{
await UpdateCore(c.AsUpdate(), x => c.Data, false);
}
else
{
await CreateCore(c.AsCreate());
}
if (Is.OptionalChange(Snapshot.EditingStatus, c.Status))
{
await ChangeCore(c.AsChange(c.Status.Value));
}
return Snapshot;
});
case CreateContent createContent:
return CreateReturnAsync(createContent, async c =>
{
await LoadContext(c, false);
await CreateCore(c);
if (context.Schema.SchemaDef.IsSingleton)
{
ChangeStatus(c.AsChange(Status.Published));
}
else if (Is.OptionalChange(Snapshot.EditingStatus, c.Status))
{
await ChangeCore(c.AsChange(c.Status.Value));
}
return Snapshot;
});
case ValidateContent validate:
return UpdateReturnAsync(validate, async c =>
{
await LoadContext(c, false);
GuardContent.CanValidate(c, Snapshot);
await context.ValidateContentAndInputAsync(Snapshot.Data);
return true;
});
case CreateContentDraft createDraft:
return UpdateReturnAsync(createDraft, async c =>
{
await LoadContext(c, false);
GuardContent.CanCreateDraft(c, Snapshot);
var status = await context.GetInitialStatusAsync();
CreateDraft(c, status);
return Snapshot;
});
case DeleteContentDraft deleteDraft:
return UpdateReturnAsync(deleteDraft, async c =>
{
await LoadContext(c, false);
GuardContent.CanDeleteDraft(c, Snapshot);
DeleteDraft(c);
return Snapshot;
});
case PatchContent patchContent:
return UpdateReturnAsync(patchContent, async c =>
{
await LoadContext(c, c.OptimizeValidation);
await UpdateCore(c, c.Data.MergeInto, true);
return Snapshot;
});
case UpdateContent updateContent:
return UpdateReturnAsync(updateContent, async c =>
{
await LoadContext(c, c.OptimizeValidation);
await UpdateCore(c, x => c.Data, false);
return Snapshot;
});
case ChangeContentStatus changeContentStatus:
return UpdateReturnAsync(changeContentStatus, async c =>
{
try
{
await LoadContext(c, c.OptimizeValidation);
if (c.DueTime > SystemClock.Instance.GetCurrentInstant())
{
ScheduleStatus(c, c.DueTime.Value);
}
else
{
await ChangeCore(c);
}
}
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 =>
{
await DeleteCore(c);
});
case DeleteContent deleteContent:
return UpdateAsync(deleteContent, async c =>
{
await DeleteCore(c);
});
default:
throw new NotSupportedException();
}
}
private async Task CreateCore(CreateContent c)
{
var status = await context.GetInitialStatusAsync();
GuardContent.CanCreate(c, context.Schema);
var dataNew = c.Data;
if (!c.DoNotValidate)
{
await context.ValidateInputAsync(dataNew);
}
if (!c.DoNotScript)
{
dataNew = await context.ExecuteScriptAndTransformAsync(s => s.Create,
new ScriptVars
{
Operation = "Create",
Data = dataNew,
Status = status,
StatusOld = default
});
}
await context.GenerateDefaultValuesAsync(dataNew);
if (!c.DoNotValidate)
{
await context.ValidateContentAsync(dataNew);
}
Create(c, dataNew, status);
}
private async Task ChangeCore(ChangeContentStatus c)
{
await GuardContent.CanChangeStatus(c, Snapshot, context.Workflow, context.Repository, context.Schema);
if (c.Status != Snapshot.Status)
{
if (!c.DoNotScript && context.HasScript(c => c.Change))
{
var change = GetChange(c.Status);
var data = Snapshot.Data.Clone();
var newData = await context.ExecuteScriptAndTransformAsync(s => s.Change,
new ScriptVars
{
Operation = change.ToString(),
Data = data,
Status = c.Status,
StatusOld = Snapshot.EditingStatus
});
if (!newData.Equals(Snapshot.Data))
{
Update(c, newData);
}
}
if (!c.DoNotValidate && c.Status == Status.Published)
{
await context.ValidateOnPublishAsync(Snapshot.Data);
}
ChangeStatus(c);
}
}
private async Task UpdateCore(UpdateContent c, Func<ContentData, ContentData> newDataFunc, bool partial)
{
await GuardContent.CanUpdate(c, Snapshot, context.Workflow);
var dataNew = newDataFunc(Snapshot.Data);
if (!dataNew.Equals(Snapshot.Data))
{
if (!c.DoNotValidate)
{
if (partial)
{
await context.ValidateInputPartialAsync(c.Data);
}
else
{
await context.ValidateInputAsync(c.Data);
}
}
if (!c.DoNotScript)
{
dataNew = await context.ExecuteScriptAndTransformAsync(s => s.Update,
new ScriptVars
{
Operation = "Update",
Data = dataNew,
DataOld = Snapshot.Data,
Status = Snapshot.EditingStatus,
StatusOld = default
});
}
if (!c.DoNotValidate)
{
await context.ValidateContentAsync(dataNew);
}
Update(c, dataNew);
}
}
private async Task DeleteCore(DeleteContent c)
{
await LoadContext(c, false);
await GuardContent.CanDelete(c, Snapshot, context.Repository, context.Schema);
if (!c.DoNotScript)
{
await context.ExecuteScriptAsync(s => s.Delete,
new ScriptVars
{
Operation = "Delete",
Data = Snapshot.Data,
Status = Snapshot.EditingStatus,
StatusOld = default
});
}
Delete(c);
}
private void Create(CreateContent command, ContentData data, Status status)
{
Raise(command, new ContentCreated { Data = data, 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 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 CancelChangeStatus(ChangeContentStatus command)
{
Raise(command, new ContentSchedulingCancelled());
}
private void ScheduleStatus(ChangeContentStatus command, Instant dueTime)
{
Raise(command, new ContentStatusScheduled { DueTime = dueTime });
}
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.EditingStatus == Status.Published)
{
return StatusChange.Unpublished;
}
else
{
return StatusChange.Change;
}
}
private Task LoadContext(ContentCommand command, bool optimize)
{
return context.LoadAsync(command.AppId, command.SchemaId, command, optimize);
}
}
}