Browse Source

Validation endpoint and minor refactoring (#607)

pull/609/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
cbb1db34c6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 64
      backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs
  2. 53
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs
  3. 36
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs
  4. 12
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObject.cs
  5. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
  6. 13
      backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ValidateContent.cs
  7. 74
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs
  8. 12
      backend/src/Squidex.Domain.Apps.Entities/Contents/Operations/ContentOperationContext.cs
  9. 16
      backend/src/Squidex.Domain.Apps.Entities/Contents/ValidationResult.cs
  10. 14
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs
  11. 55
      backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs
  12. 8
      backend/src/Squidex.Web/ApiExceptionConverter.cs
  13. 30
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  14. 29
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ValidationResultDto.cs
  15. 15
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs

64
backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs

@ -12,13 +12,12 @@ using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Schemas; using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Core.EventSynchronization namespace Squidex.Domain.Apps.Core.EventSynchronization
{ {
public static class SchemaSynchronizer public static class SchemaSynchronizer
{ {
public static IEnumerable<IEvent> Synchronize(this Schema source, Schema? target, Func<long> idGenerator, public static IEnumerable<SchemaEvent> Synchronize(this Schema source, Schema? target, Func<long> idGenerator,
SchemaSynchronizationOptions? options = null) SchemaSynchronizationOptions? options = null)
{ {
Guard.NotNull(source, nameof(source)); Guard.NotNull(source, nameof(source));
@ -32,76 +31,64 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
{ {
options ??= new SchemaSynchronizationOptions(); options ??= new SchemaSynchronizationOptions();
static SchemaEvent E(SchemaEvent @event)
{
return @event;
}
if (!source.Properties.Equals(target.Properties)) if (!source.Properties.Equals(target.Properties))
{ {
yield return E(new SchemaUpdated { Properties = target.Properties }); yield return new SchemaUpdated { Properties = target.Properties };
} }
if (!source.Category.StringEquals(target.Category)) if (!source.Category.StringEquals(target.Category))
{ {
yield return E(new SchemaCategoryChanged { Name = target.Category }); yield return new SchemaCategoryChanged { Name = target.Category };
} }
if (!source.Scripts.Equals(target.Scripts)) if (!source.Scripts.Equals(target.Scripts))
{ {
yield return E(new SchemaScriptsConfigured { Scripts = target.Scripts }); yield return new SchemaScriptsConfigured { Scripts = target.Scripts };
} }
if (!source.PreviewUrls.EqualsDictionary(target.PreviewUrls)) if (!source.PreviewUrls.EqualsDictionary(target.PreviewUrls))
{ {
yield return E(new SchemaPreviewUrlsConfigured { PreviewUrls = target.PreviewUrls.ToDictionary() }); yield return new SchemaPreviewUrlsConfigured { PreviewUrls = target.PreviewUrls.ToDictionary() };
} }
if (source.IsPublished != target.IsPublished) if (source.IsPublished != target.IsPublished)
{ {
yield return target.IsPublished ? yield return target.IsPublished ?
E(new SchemaPublished()) : new SchemaPublished() :
E(new SchemaUnpublished()); new SchemaUnpublished();
} }
var events = SyncFields(source.FieldCollection, target.FieldCollection, idGenerator, CanUpdateRoot, null, options); var events = SyncFields(source.FieldCollection, target.FieldCollection, idGenerator, CanUpdateRoot, options);
foreach (var @event in events) foreach (var @event in events)
{ {
yield return E(@event); yield return @event;
} }
if (!source.FieldsInLists.SequenceEqual(target.FieldsInLists)) if (!source.FieldsInLists.SequenceEqual(target.FieldsInLists))
{ {
yield return E(new SchemaUIFieldsConfigured { FieldsInLists = target.FieldsInLists }); yield return new SchemaUIFieldsConfigured { FieldsInLists = target.FieldsInLists };
} }
if (!source.FieldsInReferences.SequenceEqual(target.FieldsInReferences)) if (!source.FieldsInReferences.SequenceEqual(target.FieldsInReferences))
{ {
yield return E(new SchemaUIFieldsConfigured { FieldsInReferences = target.FieldsInReferences }); yield return new SchemaUIFieldsConfigured { FieldsInReferences = target.FieldsInReferences };
} }
if (!source.FieldRules.SetEquals(target.FieldRules)) if (!source.FieldRules.SetEquals(target.FieldRules))
{ {
yield return E(new SchemaFieldRulesConfigured { FieldRules = target.FieldRules }); yield return new SchemaFieldRulesConfigured { FieldRules = target.FieldRules };
} }
} }
} }
private static IEnumerable<SchemaEvent> SyncFields<T>( private static IEnumerable<ParentFieldEvent> SyncFields<T>(
FieldCollection<T> source, FieldCollection<T> source,
FieldCollection<T> target, FieldCollection<T> target,
Func<long> idGenerator, Func<long> idGenerator,
Func<T, T, bool> canUpdate, Func<T, T, bool> canUpdate,
NamedId<long>? parentId, SchemaSynchronizationOptions options) where T : class, IField SchemaSynchronizationOptions options) where T : class, IField
{ {
FieldEvent E(FieldEvent @event)
{
@event.ParentFieldId = parentId;
return @event;
}
var sourceIds = source.Ordered.Select(x => x.NamedId()).ToList(); var sourceIds = source.Ordered.Select(x => x.NamedId()).ToList();
if (!options.NoFieldDeletion) if (!options.NoFieldDeletion)
@ -114,7 +101,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
sourceIds.Remove(id); sourceIds.Remove(id);
yield return E(new FieldDeleted { FieldId = id }); yield return new FieldDeleted { FieldId = id };
} }
} }
} }
@ -135,7 +122,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
{ {
if (!sourceField.RawProperties.Equals(targetField.RawProperties as object)) if (!sourceField.RawProperties.Equals(targetField.RawProperties as object))
{ {
yield return E(new FieldUpdated { FieldId = id, Properties = targetField.RawProperties }); yield return new FieldUpdated { FieldId = id, Properties = targetField.RawProperties };
} }
} }
else if (!sourceField.IsLocked && !options.NoFieldRecreation) else if (!sourceField.IsLocked && !options.NoFieldRecreation)
@ -144,7 +131,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
sourceIds.Remove(id); sourceIds.Remove(id);
yield return E(new FieldDeleted { FieldId = id }); yield return new FieldDeleted { FieldId = id };
} }
} }
@ -162,7 +149,6 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
yield return new FieldAdded yield return new FieldAdded
{ {
Name = targetField.Name, Name = targetField.Name,
ParentFieldId = parentId,
Partitioning = partitioning, Partitioning = partitioning,
Properties = targetField.RawProperties, Properties = targetField.RawProperties,
FieldId = id FieldId = id
@ -175,31 +161,33 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
{ {
if (!targetField.IsLocked.BoolEquals(sourceField?.IsLocked)) if (!targetField.IsLocked.BoolEquals(sourceField?.IsLocked))
{ {
yield return E(new FieldLocked { FieldId = id }); yield return new FieldLocked { FieldId = id };
} }
if (!targetField.IsHidden.BoolEquals(sourceField?.IsHidden)) if (!targetField.IsHidden.BoolEquals(sourceField?.IsHidden))
{ {
yield return targetField.IsHidden ? yield return targetField.IsHidden ?
E(new FieldHidden { FieldId = id }) : new FieldHidden { FieldId = id } :
E(new FieldShown { FieldId = id }); new FieldShown { FieldId = id };
} }
if (!targetField.IsDisabled.BoolEquals(sourceField?.IsDisabled)) if (!targetField.IsDisabled.BoolEquals(sourceField?.IsDisabled))
{ {
yield return targetField.IsDisabled ? yield return targetField.IsDisabled ?
E(new FieldDisabled { FieldId = id }) : new FieldDisabled { FieldId = id } :
E(new FieldEnabled { FieldId = id }); new FieldEnabled { FieldId = id };
} }
if ((sourceField == null || sourceField is IArrayField) && targetField is IArrayField targetArrayField) if ((sourceField == null || sourceField is IArrayField) && targetField is IArrayField targetArrayField)
{ {
var fields = (sourceField as IArrayField)?.FieldCollection ?? FieldCollection<NestedField>.Empty; var fields = (sourceField as IArrayField)?.FieldCollection ?? FieldCollection<NestedField>.Empty;
var events = SyncFields(fields, targetArrayField.FieldCollection, idGenerator, CanUpdate, id, options); var events = SyncFields(fields, targetArrayField.FieldCollection, idGenerator, CanUpdate, options);
foreach (var @event in events) foreach (var @event in events)
{ {
@event.ParentFieldId = id;
yield return @event; yield return @event;
} }
} }
@ -215,7 +203,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
{ {
var fieldIds = targetNames.Select(x => sourceIds.Find(y => y.Name == x)!.Id).ToArray(); var fieldIds = targetNames.Select(x => sourceIds.Find(y => y.Name == x)!.Id).ToArray();
yield return new SchemaFieldsReordered { FieldIds = fieldIds, ParentFieldId = parentId }; yield return new SchemaFieldsReordered { FieldIds = fieldIds };
} }
} }
} }

53
backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs

@ -345,10 +345,9 @@ namespace Squidex.Domain.Apps.Entities.Apps
foreach (var @event in events) foreach (var @event in events)
{ {
@event.Actor = command.Actor;
@event.AppId = appId; @event.AppId = appId;
RaiseEvent(@event); Raise(command, @event);
} }
} }
@ -356,121 +355,123 @@ namespace Squidex.Domain.Apps.Entities.Apps
{ {
if (string.Equals(appPlansProvider.GetFreePlan()?.Id, command.PlanId)) if (string.Equals(appPlansProvider.GetFreePlan()?.Id, command.PlanId))
{ {
RaiseEvent(SimpleMapper.Map(command, new AppPlanReset())); Raise(command, new AppPlanReset());
} }
else else
{ {
RaiseEvent(SimpleMapper.Map(command, new AppPlanChanged())); Raise(command, new AppPlanChanged());
} }
} }
public void Update(UpdateApp command) public void Update(UpdateApp command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppUpdated())); Raise(command, new AppUpdated());
} }
public void UpdateClient(UpdateClient command) public void UpdateClient(UpdateClient command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated())); Raise(command, new AppClientUpdated());
} }
public void UploadImage(UploadAppImage command) public void UploadImage(UploadAppImage command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppImageUploaded { Image = new AppImage(command.File.MimeType) })); Raise(command, new AppImageUploaded { Image = new AppImage(command.File.MimeType) });
} }
public void RemoveImage(RemoveAppImage command) public void RemoveImage(RemoveAppImage command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppImageRemoved())); Raise(command, new AppImageRemoved());
} }
public void UpdateLanguage(UpdateLanguage command) public void UpdateLanguage(UpdateLanguage command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppLanguageUpdated())); Raise(command, new AppLanguageUpdated());
} }
public void AssignContributor(AssignContributor command, bool isAdded) public void AssignContributor(AssignContributor command, bool isAdded)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppContributorAssigned { IsAdded = isAdded })); Raise(command, new AppContributorAssigned { IsAdded = isAdded });
} }
public void RemoveContributor(RemoveContributor command) public void RemoveContributor(RemoveContributor command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppContributorRemoved())); Raise(command, new AppContributorRemoved());
} }
public void AttachClient(AttachClient command) public void AttachClient(AttachClient command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppClientAttached())); Raise(command, new AppClientAttached());
} }
public void RevokeClient(RevokeClient command) public void RevokeClient(RevokeClient command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppClientRevoked())); Raise(command, new AppClientRevoked());
} }
public void AddWorkflow(AddWorkflow command) public void AddWorkflow(AddWorkflow command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppWorkflowAdded())); Raise(command, new AppWorkflowAdded());
} }
public void UpdateWorkflow(UpdateWorkflow command) public void UpdateWorkflow(UpdateWorkflow command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppWorkflowUpdated())); Raise(command, new AppWorkflowUpdated());
} }
public void DeleteWorkflow(DeleteWorkflow command) public void DeleteWorkflow(DeleteWorkflow command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppWorkflowDeleted())); Raise(command, new AppWorkflowDeleted());
} }
public void AddLanguage(AddLanguage command) public void AddLanguage(AddLanguage command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppLanguageAdded())); Raise(command, new AppLanguageAdded());
} }
public void RemoveLanguage(RemoveLanguage command) public void RemoveLanguage(RemoveLanguage command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppLanguageRemoved())); Raise(command, new AppLanguageRemoved());
} }
public void AddPattern(AddPattern command) public void AddPattern(AddPattern command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppPatternAdded())); Raise(command, new AppPatternAdded());
} }
public void DeletePattern(DeletePattern command) public void DeletePattern(DeletePattern command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppPatternDeleted())); Raise(command, new AppPatternDeleted());
} }
public void UpdatePattern(UpdatePattern command) public void UpdatePattern(UpdatePattern command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppPatternUpdated())); Raise(command, new AppPatternUpdated());
} }
public void AddRole(AddRole command) public void AddRole(AddRole command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppRoleAdded())); Raise(command, new AppRoleAdded());
} }
public void DeleteRole(DeleteRole command) public void DeleteRole(DeleteRole command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppRoleDeleted())); Raise(command, new AppRoleDeleted());
} }
public void UpdateRole(UpdateRole command) public void UpdateRole(UpdateRole command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppRoleUpdated())); Raise(command, new AppRoleUpdated());
} }
public void ArchiveApp(ArchiveApp command) public void ArchiveApp(ArchiveApp command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppArchived())); Raise(command, new AppArchived());
} }
private void RaiseEvent(AppEvent @event) private void Raise<T, TEvent>(T command, TEvent @event) where T : class where TEvent : AppEvent
{ {
SimpleMapper.Map(command, @event);
@event.AppId ??= Snapshot.NamedId(); @event.AppId ??= Snapshot.NamedId();
RaiseEvent(Envelope.Create(@event)); RaiseEvent(Envelope.Create(@event));

36
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs

@ -72,9 +72,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
await GuardAsset.CanCreate(c, assetQuery); await GuardAsset.CanCreate(c, assetQuery);
var tagIds = await NormalizeTagsAsync(c.AppId.Id, c.Tags); c.Tags = await NormalizeTagsAsync(c.AppId.Id, c.Tags);
Create(c, tagIds); Create(c);
return Snapshot; return Snapshot;
}); });
@ -92,9 +92,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
GuardAsset.CanAnnotate(c); GuardAsset.CanAnnotate(c);
var tagIds = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags); c.Tags = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags);
Annotate(c, tagIds); Annotate(c);
return Snapshot; return Snapshot;
}); });
@ -133,9 +133,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
return new HashSet<string>(normalized.Values); return new HashSet<string>(normalized.Values);
} }
public void Create(CreateAsset command, HashSet<string>? tagIds) public void Create(CreateAsset command)
{ {
var @event = SimpleMapper.Map(command, new AssetCreated Raise(command, new AssetCreated
{ {
MimeType = command.File.MimeType, MimeType = command.File.MimeType,
FileName = command.File.FileName, FileName = command.File.FileName,
@ -143,45 +143,37 @@ namespace Squidex.Domain.Apps.Entities.Assets
FileVersion = 0, FileVersion = 0,
Slug = command.File.FileName.ToAssetSlug() Slug = command.File.FileName.ToAssetSlug()
}); });
@event.Tags = tagIds;
RaiseEvent(@event);
} }
public void Update(UpdateAsset command) public void Update(UpdateAsset command)
{ {
var @event = SimpleMapper.Map(command, new AssetUpdated Raise(command, new AssetUpdated
{ {
MimeType = command.File.MimeType, MimeType = command.File.MimeType,
FileVersion = Snapshot.FileVersion + 1, FileVersion = Snapshot.FileVersion + 1,
FileSize = command.File.FileSize FileSize = command.File.FileSize
}); });
RaiseEvent(@event);
} }
public void Annotate(AnnotateAsset command, HashSet<string>? tagIds) public void Annotate(AnnotateAsset command)
{ {
var @event = SimpleMapper.Map(command, new AssetAnnotated()); Raise(command, new AssetAnnotated());
@event.Tags = tagIds;
RaiseEvent(@event);
} }
public void Move(MoveAsset command) public void Move(MoveAsset command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AssetMoved())); Raise(command, new AssetMoved());
} }
public void Delete(DeleteAsset command) public void Delete(DeleteAsset command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = Snapshot.TotalSize })); Raise(command, new AssetDeleted { DeletedSize = Snapshot.TotalSize });
} }
private void RaiseEvent(AppEvent @event) private void Raise<T, TEvent>(T command, TEvent @event) where T : class where TEvent : AppEvent
{ {
SimpleMapper.Map(command, @event);
@event.AppId ??= Snapshot.AppId; @event.AppId ??= Snapshot.AppId;
RaiseEvent(Envelope.Create(@event)); RaiseEvent(Envelope.Create(@event));

12
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObject.cs

@ -96,26 +96,28 @@ namespace Squidex.Domain.Apps.Entities.Assets
public void Create(CreateAssetFolder command) public void Create(CreateAssetFolder command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AssetFolderCreated())); Raise(command, new AssetFolderCreated());
} }
public void Move(MoveAssetFolder command) public void Move(MoveAssetFolder command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AssetFolderMoved())); Raise(command, new AssetFolderMoved());
} }
public void Rename(RenameAssetFolder command) public void Rename(RenameAssetFolder command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AssetFolderRenamed())); Raise(command, new AssetFolderRenamed());
} }
public void Delete(DeleteAssetFolder command) public void Delete(DeleteAssetFolder command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AssetFolderDeleted())); Raise(command, new AssetFolderDeleted());
} }
private void RaiseEvent(AppEvent @event) private void Raise<T, TEvent>(T command, TEvent @event) where T : class where TEvent : AppEvent
{ {
SimpleMapper.Map(command, @event);
@event.AppId ??= Snapshot.AppId; @event.AppId ??= Snapshot.AppId;
RaiseEvent(Envelope.Create(@event)); RaiseEvent(Envelope.Create(@event));

2
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs

@ -14,7 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands
{ {
public DomainId ParentId { get; set; } public DomainId ParentId { get; set; }
public HashSet<string> Tags { get; } = new HashSet<string>(); public HashSet<string> Tags { get; set; } = new HashSet<string>();
public bool Duplicate { get; set; } public bool Duplicate { get; set; }

13
backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ValidateContent.cs

@ -0,0 +1,13 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities.Contents.Commands
{
public sealed class ValidateContent : ContentCommand
{
}
}

74
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
@ -79,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
case CreateContent createContent: case CreateContent createContent:
return CreateReturnAsync(createContent, async c => return CreateReturnAsync(createContent, async c =>
{ {
await LoadContext(c.AppId, c.SchemaId, c, c.OptimizeValidation); await LoadContext(c, c.OptimizeValidation);
await GuardContent.CanCreate(c, context.Workflow, context.Schema); await GuardContent.CanCreate(c, context.Workflow, context.Schema);
@ -126,10 +127,22 @@ namespace Squidex.Domain.Apps.Entities.Contents
return Snapshot; return Snapshot;
}); });
case ValidateContent validateContent:
return UpdateReturnAsync(validateContent, async c =>
{
await LoadContext(c);
var errors = await context.GetErrorsAsync(Snapshot.Data);
var result = new ValidationResult { Errors = errors.ToArray() };
return result;
});
case CreateContentDraft createContentDraft: case CreateContentDraft createContentDraft:
return UpdateReturnAsync(createContentDraft, async c => return UpdateReturnAsync(createContentDraft, async c =>
{ {
await LoadContext(Snapshot.AppId, Snapshot.SchemaId, c); await LoadContext(c);
GuardContent.CanCreateDraft(c, Snapshot); GuardContent.CanCreateDraft(c, Snapshot);
@ -143,7 +156,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
case DeleteContentDraft deleteContentDraft: case DeleteContentDraft deleteContentDraft:
return UpdateReturnAsync(deleteContentDraft, async c => return UpdateReturnAsync(deleteContentDraft, async c =>
{ {
await LoadContext(Snapshot.AppId, Snapshot.SchemaId, c); await LoadContext(c);
GuardContent.CanDeleteDraft(c, Snapshot); GuardContent.CanDeleteDraft(c, Snapshot);
@ -173,7 +186,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
try try
{ {
await LoadContext(Snapshot.AppId, Snapshot.SchemaId, c); await LoadContext(c);
await GuardContent.CanChangeStatus(c, Snapshot, context.Workflow, context.Repository, context.Schema); await GuardContent.CanChangeStatus(c, Snapshot, context.Workflow, context.Repository, context.Schema);
@ -183,7 +196,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
} }
else else
{ {
var change = GetChange(c); var change = GetChange(c.Status);
if (!c.DoNotScript && context.HasScript(c => c.Change)) if (!c.DoNotScript && context.HasScript(c => c.Change))
{ {
@ -232,7 +245,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
case DeleteContent deleteContent: case DeleteContent deleteContent:
return UpdateAsync(deleteContent, async c => return UpdateAsync(deleteContent, async c =>
{ {
await LoadContext(Snapshot.AppId, Snapshot.SchemaId, c); await LoadContext(c);
await GuardContent.CanDelete(c, Snapshot, context.Repository, context.Schema); await GuardContent.CanDelete(c, Snapshot, context.Repository, context.Schema);
@ -264,7 +277,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (!currentData!.Equals(newData)) if (!currentData!.Equals(newData))
{ {
await LoadContext(Snapshot.AppId, Snapshot.SchemaId, command, command.OptimizeValidation); await LoadContext(command, command.OptimizeValidation);
if (!command.DoNotValidate) if (!command.DoNotValidate)
{ {
@ -304,76 +317,85 @@ namespace Squidex.Domain.Apps.Entities.Contents
public void Create(CreateContent command, Status status) public void Create(CreateContent command, Status status)
{ {
RaiseEvent(SimpleMapper.Map(command, new ContentCreated { Status = status })); Raise(command, new ContentCreated { Status = status });
if (command.Publish) if (command.Publish)
{ {
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published })); var published = Status.Published;
Raise(command, new ContentStatusChanged { Status = published, Change = GetChange(published) });
} }
} }
public void CreateDraft(CreateContentDraft command, Status status) public void CreateDraft(CreateContentDraft command, Status status)
{ {
RaiseEvent(SimpleMapper.Map(command, new ContentDraftCreated { Status = status })); Raise(command, new ContentDraftCreated { Status = status });
} }
public void Delete(DeleteContent command) public void Delete(DeleteContent command)
{ {
RaiseEvent(SimpleMapper.Map(command, new ContentDeleted())); Raise(command, new ContentDeleted());
} }
public void DeleteDraft(DeleteContentDraft command) public void DeleteDraft(DeleteContentDraft command)
{ {
RaiseEvent(SimpleMapper.Map(command, new ContentDraftDeleted())); Raise(command, new ContentDraftDeleted());
} }
public void Update(ContentCommand command, NamedContentData data) public void Update(ContentCommand command, NamedContentData data)
{ {
RaiseEvent(SimpleMapper.Map(command, new ContentUpdated { Data = data })); Raise(command, new ContentUpdated { Data = data });
} }
public void ChangeStatus(ChangeContentStatus command, StatusChange change) public void ChangeStatus(ChangeContentStatus command, StatusChange change)
{ {
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Change = change })); Raise(command, new ContentStatusChanged { Change = change });
} }
public void CancelChangeStatus(ChangeContentStatus command) public void CancelChangeStatus(ChangeContentStatus command)
{ {
RaiseEvent(SimpleMapper.Map(command, new ContentSchedulingCancelled())); Raise(command, new ContentSchedulingCancelled());
} }
public void ScheduleStatus(ChangeContentStatus command, Instant dueTime) public void ScheduleStatus(ChangeContentStatus command, Instant dueTime)
{ {
RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled { DueTime = dueTime })); Raise(command, new ContentStatusScheduled { DueTime = dueTime });
} }
private void RaiseEvent(SchemaEvent @event) private void Raise<T, TEvent>(T command, TEvent @event) where TEvent : SchemaEvent where T : class
{ {
SimpleMapper.Map(command, @event);
@event.AppId ??= Snapshot.AppId; @event.AppId ??= Snapshot.AppId;
@event.SchemaId ??= Snapshot.SchemaId; @event.SchemaId ??= Snapshot.SchemaId;
RaiseEvent(Envelope.Create(@event)); RaiseEvent(Envelope.Create(@event));
} }
private StatusChange GetChange(ChangeContentStatus command) private StatusChange GetChange(Status status)
{ {
var change = StatusChange.Change; if (status == Status.Published)
if (command.Status == Status.Published)
{ {
change = StatusChange.Published; return StatusChange.Published;
} }
else if (Snapshot.EditingStatus == Status.Published) else if (Snapshot.EditingStatus == Status.Published)
{ {
change = StatusChange.Unpublished; return StatusChange.Unpublished;
}
else
{
return StatusChange.Change;
} }
}
return change; private Task LoadContext(ContentCommand command, bool optimized = false)
{
return context.LoadAsync(Snapshot.AppId, Snapshot.SchemaId, command, optimized);
} }
private Task LoadContext(NamedId<DomainId> appId, NamedId<DomainId> schemaId, ContentCommand command, bool optimized = false) private Task LoadContext(CreateContent command, bool optimized = false)
{ {
return context.LoadAsync(appId, schemaId, command, optimized); return context.LoadAsync(command.AppId, command.SchemaId, command, optimized);
} }
} }
} }

12
backend/src/Squidex.Domain.Apps.Entities/Contents/Operations/ContentOperationContext.cs

@ -146,6 +146,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.Operations
CheckErrors(validator); CheckErrors(validator);
} }
public async Task<IEnumerable<ValidationError>> GetErrorsAsync(NamedContentData data)
{
var validator =
new ContentValidator(Partition(),
validationContext, validators, log);
await validator.ValidateInputAsync(data);
await validator.ValidateContentAsync(data);
return validator.Errors;
}
public async Task ValidateOnPublishAsync(NamedContentData data) public async Task ValidateOnPublishAsync(NamedContentData data)
{ {
if (!schema.SchemaDef.Properties.ValidateOnPublish) if (!schema.SchemaDef.Properties.ValidateOnPublish)

16
backend/src/Squidex.Domain.Apps.Entities/Contents/ValidationResult.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ValidationResult
{
public ValidationError[] Errors { get; set; }
}
}

14
backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs

@ -121,31 +121,33 @@ namespace Squidex.Domain.Apps.Entities.Rules
public void Create(CreateRule command) public void Create(CreateRule command)
{ {
RaiseEvent(SimpleMapper.Map(command, new RuleCreated())); Raise(command, new RuleCreated());
} }
public void Update(UpdateRule command) public void Update(UpdateRule command)
{ {
RaiseEvent(SimpleMapper.Map(command, new RuleUpdated())); Raise(command, new RuleUpdated());
} }
public void Enable(EnableRule command) public void Enable(EnableRule command)
{ {
RaiseEvent(SimpleMapper.Map(command, new RuleEnabled())); Raise(command, new RuleEnabled());
} }
public void Disable(DisableRule command) public void Disable(DisableRule command)
{ {
RaiseEvent(SimpleMapper.Map(command, new RuleDisabled())); Raise(command, new RuleDisabled());
} }
public void Delete(DeleteRule command) public void Delete(DeleteRule command)
{ {
RaiseEvent(SimpleMapper.Map(command, new RuleDeleted())); Raise(command, new RuleDeleted());
} }
private void RaiseEvent(AppEvent @event) private void Raise<T, TEvent>(T command, TEvent @event) where T : class where TEvent : AppEvent
{ {
SimpleMapper.Map(command, @event);
@event.AppId ??= Snapshot.AppId; @event.AppId ??= Snapshot.AppId;
RaiseEvent(Envelope.Create(@event)); RaiseEvent(Envelope.Create(@event));

55
backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs

@ -270,106 +270,106 @@ namespace Squidex.Domain.Apps.Entities.Schemas
foreach (var @event in events) foreach (var @event in events)
{ {
RaiseEvent(SimpleMapper.Map(command, (SchemaEvent)@event)); Raise(command, @event);
} }
} }
public void Create(CreateSchema command) public void Create(CreateSchema command)
{ {
RaiseEvent(command, new SchemaCreated { SchemaId = NamedId.Of(command.SchemaId, command.Name), Schema = command.BuildSchema() }); Raise(command, new SchemaCreated { SchemaId = NamedId.Of(command.SchemaId, command.Name), Schema = command.BuildSchema() });
} }
public void Add(AddField command) public void Add(AddField command)
{ {
RaiseEvent(command, new FieldAdded { FieldId = CreateFieldId(command) }); Raise(command, new FieldAdded { FieldId = CreateFieldId(command) });
} }
public void UpdateField(UpdateField command) public void UpdateField(UpdateField command)
{ {
RaiseEvent(command, new FieldUpdated()); Raise(command, new FieldUpdated());
} }
public void LockField(LockField command) public void LockField(LockField command)
{ {
RaiseEvent(command, new FieldLocked()); Raise(command, new FieldLocked());
} }
public void HideField(HideField command) public void HideField(HideField command)
{ {
RaiseEvent(command, new FieldHidden()); Raise(command, new FieldHidden());
} }
public void ShowField(ShowField command) public void ShowField(ShowField command)
{ {
RaiseEvent(command, new FieldShown()); Raise(command, new FieldShown());
} }
public void DisableField(DisableField command) public void DisableField(DisableField command)
{ {
RaiseEvent(command, new FieldDisabled()); Raise(command, new FieldDisabled());
} }
public void EnableField(EnableField command) public void EnableField(EnableField command)
{ {
RaiseEvent(command, new FieldEnabled()); Raise(command, new FieldEnabled());
} }
public void DeleteField(DeleteField command) public void DeleteField(DeleteField command)
{ {
RaiseEvent(command, new FieldDeleted()); Raise(command, new FieldDeleted());
} }
public void Reorder(ReorderFields command) public void Reorder(ReorderFields command)
{ {
RaiseEvent(command, new SchemaFieldsReordered()); Raise(command, new SchemaFieldsReordered());
} }
public void Publish(PublishSchema command) public void Publish(PublishSchema command)
{ {
RaiseEvent(command, new SchemaPublished()); Raise(command, new SchemaPublished());
} }
public void Unpublish(UnpublishSchema command) public void Unpublish(UnpublishSchema command)
{ {
RaiseEvent(command, new SchemaUnpublished()); Raise(command, new SchemaUnpublished());
} }
public void ConfigureScripts(ConfigureScripts command) public void ConfigureScripts(ConfigureScripts command)
{ {
RaiseEvent(command, new SchemaScriptsConfigured()); Raise(command, new SchemaScriptsConfigured());
} }
public void ConfigureFieldRules(ConfigureFieldRules command) public void ConfigureFieldRules(ConfigureFieldRules command)
{ {
RaiseEvent(command, new SchemaFieldRulesConfigured { FieldRules = command.ToFieldRules() }); Raise(command, new SchemaFieldRulesConfigured { FieldRules = command.ToFieldRules() });
} }
public void ChangeCategory(ChangeCategory command) public void ChangeCategory(ChangeCategory command)
{ {
RaiseEvent(command, new SchemaCategoryChanged()); Raise(command, new SchemaCategoryChanged());
} }
public void ConfigurePreviewUrls(ConfigurePreviewUrls command) public void ConfigurePreviewUrls(ConfigurePreviewUrls command)
{ {
RaiseEvent(command, new SchemaPreviewUrlsConfigured()); Raise(command, new SchemaPreviewUrlsConfigured());
} }
public void ConfigureUIFields(ConfigureUIFields command) public void ConfigureUIFields(ConfigureUIFields command)
{ {
RaiseEvent(command, new SchemaUIFieldsConfigured()); Raise(command, new SchemaUIFieldsConfigured());
} }
public void Update(UpdateSchema command) public void Update(UpdateSchema command)
{ {
RaiseEvent(command, new SchemaUpdated()); Raise(command, new SchemaUpdated());
} }
public void Delete(DeleteSchema command) public void Delete(DeleteSchema command)
{ {
RaiseEvent(command, new SchemaDeleted()); Raise(command, new SchemaDeleted());
} }
private void RaiseEvent<TCommand, TEvent>(TCommand command, TEvent @event) where TCommand : class where TEvent : SchemaEvent private void Raise<T, TEvent>(T command, TEvent @event) where TEvent : SchemaEvent where T : class
{ {
SimpleMapper.Map(command, @event); SimpleMapper.Map(command, @event);
@ -383,13 +383,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas
return null; return null;
} }
if (command is ParentFieldCommand pc && @event is ParentFieldEvent pe) if (command is ParentFieldCommand parentField && @event is ParentFieldEvent parentFieldEvent)
{ {
if (pc.ParentFieldId.HasValue) if (parentField.ParentFieldId.HasValue)
{ {
if (Snapshot.SchemaDef.FieldsById.TryGetValue(pc.ParentFieldId.Value, out var field)) if (Snapshot.SchemaDef.FieldsById.TryGetValue(parentField.ParentFieldId.Value, out var field))
{ {
pe.ParentFieldId = field.NamedId(); parentFieldEvent.ParentFieldId = field.NamedId();
if (command is FieldCommand fc && @event is FieldEvent fe) if (command is FieldCommand fc && @event is FieldEvent fe)
{ {
@ -406,11 +406,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas
} }
} }
RaiseEvent(@event); SimpleMapper.Map(command, @event);
}
private void RaiseEvent(SchemaEvent @event)
{
@event.AppId ??= Snapshot.AppId; @event.AppId ??= Snapshot.AppId;
@event.SchemaId ??= Snapshot.NamedId(); @event.SchemaId ??= Snapshot.NamedId();

8
backend/src/Squidex.Web/ApiExceptionConverter.cs

@ -83,7 +83,7 @@ namespace Squidex.Web
switch (exception) switch (exception)
{ {
case ValidationException ex: case ValidationException ex:
return (CreateError(400, T.Get("common.httpValidationError"), ToDetails(ex)), true); return (CreateError(400, T.Get("common.httpValidationError"), ToErrors(ex.Errors).ToArray()), true);
case DomainObjectNotFoundException _: case DomainObjectNotFoundException _:
return (CreateError(404), true); return (CreateError(404), true);
@ -121,7 +121,7 @@ namespace Squidex.Web
return error; return error;
} }
private static string[] ToDetails(ValidationException ex) public static IEnumerable<string> ToErrors(IEnumerable<ValidationError> errors)
{ {
static string FixPropertyName(string property) static string FixPropertyName(string property)
{ {
@ -155,7 +155,7 @@ namespace Squidex.Web
return builder.ToString(); return builder.ToString();
} }
return ex.Errors.Select(e => return errors.Select(e =>
{ {
if (e.PropertyNames?.Any() == true) if (e.PropertyNames?.Any() == true)
{ {
@ -165,7 +165,7 @@ namespace Squidex.Web
{ {
return e.Message; return e.Message;
} }
}).ToArray(); });
} }
} }
} }

30
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -285,6 +285,36 @@ namespace Squidex.Areas.Api.Controllers.Contents
return Ok(response); return Ok(response);
} }
/// <summary>
/// Get a content item validity.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the content to fetch.</param>
/// <returns>
/// 200 => Content validation result returned.
/// 404 => Content, schema or app not found.
/// </returns>
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpGet]
[Route("content/{app}/{name}/{id}/validity")]
[ProducesResponseType(typeof(ValidationResultDto), 200)]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
public async Task<IActionResult> GetContentValidity(string app, string name, DomainId id)
{
var command = new ValidateContent { ContentId = id };
var context = await CommandBus.PublishAsync(command);
var result = context.Result<ValidationResult>();
var response = ValidationResultDto.FromResult(result);
return Ok(response);
}
/// <summary> /// <summary>
/// Get all references of a content. /// Get all references of a content.
/// </summary> /// </summary>

29
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ValidationResultDto.cs

@ -0,0 +1,29 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Contents.Models
{
public class ValidationResultDto
{
/// <summary>
/// The validation errors.
/// </summary>
public string[] Errors { get; set; }
public static ValidationResultDto FromResult(ValidationResult result)
{
return new ValidationResultDto
{
Errors = ApiExceptionConverter.ToErrors(result.Errors).ToArray()
};
}
}
}

15
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
@ -592,6 +593,20 @@ namespace Squidex.Domain.Apps.Entities.Contents
await PublishAsync(command); await PublishAsync(command);
} }
[Fact]
public async Task Validate_should_not_update_state()
{
await ExecuteCreateAsync();
var command = new ValidateContent();
var result = await PublishAsync(command);
result.ShouldBeEquivalent(new ValidationResult { Errors = Array.Empty<ValidationError>() });
Assert.Equal(0, sut.Snapshot.Version);
}
[Fact] [Fact]
public async Task Delete_should_create_events_and_update_deleted_flag() public async Task Delete_should_create_events_and_update_deleted_flag()
{ {

Loading…
Cancel
Save