diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs index 0af929a76..0d1bfb681 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs +++ b/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.Schemas; using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Core.EventSynchronization { public static class SchemaSynchronizer { - public static IEnumerable Synchronize(this Schema source, Schema? target, Func idGenerator, + public static IEnumerable Synchronize(this Schema source, Schema? target, Func idGenerator, SchemaSynchronizationOptions? options = null) { Guard.NotNull(source, nameof(source)); @@ -32,76 +31,64 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization { options ??= new SchemaSynchronizationOptions(); - static SchemaEvent E(SchemaEvent @event) - { - return @event; - } - 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)) { - yield return E(new SchemaCategoryChanged { Name = target.Category }); + yield return new SchemaCategoryChanged { Name = target.Category }; } 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)) { - yield return E(new SchemaPreviewUrlsConfigured { PreviewUrls = target.PreviewUrls.ToDictionary() }); + yield return new SchemaPreviewUrlsConfigured { PreviewUrls = target.PreviewUrls.ToDictionary() }; } if (source.IsPublished != target.IsPublished) { yield return target.IsPublished ? - E(new SchemaPublished()) : - E(new SchemaUnpublished()); + new SchemaPublished() : + 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) { - yield return E(@event); + yield return @event; } 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)) { - yield return E(new SchemaUIFieldsConfigured { FieldsInReferences = target.FieldsInReferences }); + yield return new SchemaUIFieldsConfigured { FieldsInReferences = target.FieldsInReferences }; } if (!source.FieldRules.SetEquals(target.FieldRules)) { - yield return E(new SchemaFieldRulesConfigured { FieldRules = target.FieldRules }); + yield return new SchemaFieldRulesConfigured { FieldRules = target.FieldRules }; } } } - private static IEnumerable SyncFields( + private static IEnumerable SyncFields( FieldCollection source, FieldCollection target, Func idGenerator, Func canUpdate, - NamedId? 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(); if (!options.NoFieldDeletion) @@ -114,7 +101,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization 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)) { - 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) @@ -144,7 +131,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization 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 { Name = targetField.Name, - ParentFieldId = parentId, Partitioning = partitioning, Properties = targetField.RawProperties, FieldId = id @@ -175,31 +161,33 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization { 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)) { yield return targetField.IsHidden ? - E(new FieldHidden { FieldId = id }) : - E(new FieldShown { FieldId = id }); + new FieldHidden { FieldId = id } : + new FieldShown { FieldId = id }; } if (!targetField.IsDisabled.BoolEquals(sourceField?.IsDisabled)) { yield return targetField.IsDisabled ? - E(new FieldDisabled { FieldId = id }) : - E(new FieldEnabled { FieldId = id }); + new FieldDisabled { FieldId = id } : + new FieldEnabled { FieldId = id }; } if ((sourceField == null || sourceField is IArrayField) && targetField is IArrayField targetArrayField) { var fields = (sourceField as IArrayField)?.FieldCollection ?? FieldCollection.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) { + @event.ParentFieldId = id; + 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(); - yield return new SchemaFieldsReordered { FieldIds = fieldIds, ParentFieldId = parentId }; + yield return new SchemaFieldsReordered { FieldIds = fieldIds }; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs index 058f7d50f..7c59510b1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs @@ -345,10 +345,9 @@ namespace Squidex.Domain.Apps.Entities.Apps foreach (var @event in events) { - @event.Actor = command.Actor; @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)) { - RaiseEvent(SimpleMapper.Map(command, new AppPlanReset())); + Raise(command, new AppPlanReset()); } else { - RaiseEvent(SimpleMapper.Map(command, new AppPlanChanged())); + Raise(command, new AppPlanChanged()); } } public void Update(UpdateApp command) { - RaiseEvent(SimpleMapper.Map(command, new AppUpdated())); + Raise(command, new AppUpdated()); } public void UpdateClient(UpdateClient command) { - RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated())); + Raise(command, new AppClientUpdated()); } 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) { - RaiseEvent(SimpleMapper.Map(command, new AppImageRemoved())); + Raise(command, new AppImageRemoved()); } public void UpdateLanguage(UpdateLanguage command) { - RaiseEvent(SimpleMapper.Map(command, new AppLanguageUpdated())); + Raise(command, new AppLanguageUpdated()); } 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) { - RaiseEvent(SimpleMapper.Map(command, new AppContributorRemoved())); + Raise(command, new AppContributorRemoved()); } public void AttachClient(AttachClient command) { - RaiseEvent(SimpleMapper.Map(command, new AppClientAttached())); + Raise(command, new AppClientAttached()); } public void RevokeClient(RevokeClient command) { - RaiseEvent(SimpleMapper.Map(command, new AppClientRevoked())); + Raise(command, new AppClientRevoked()); } public void AddWorkflow(AddWorkflow command) { - RaiseEvent(SimpleMapper.Map(command, new AppWorkflowAdded())); + Raise(command, new AppWorkflowAdded()); } public void UpdateWorkflow(UpdateWorkflow command) { - RaiseEvent(SimpleMapper.Map(command, new AppWorkflowUpdated())); + Raise(command, new AppWorkflowUpdated()); } public void DeleteWorkflow(DeleteWorkflow command) { - RaiseEvent(SimpleMapper.Map(command, new AppWorkflowDeleted())); + Raise(command, new AppWorkflowDeleted()); } public void AddLanguage(AddLanguage command) { - RaiseEvent(SimpleMapper.Map(command, new AppLanguageAdded())); + Raise(command, new AppLanguageAdded()); } public void RemoveLanguage(RemoveLanguage command) { - RaiseEvent(SimpleMapper.Map(command, new AppLanguageRemoved())); + Raise(command, new AppLanguageRemoved()); } public void AddPattern(AddPattern command) { - RaiseEvent(SimpleMapper.Map(command, new AppPatternAdded())); + Raise(command, new AppPatternAdded()); } public void DeletePattern(DeletePattern command) { - RaiseEvent(SimpleMapper.Map(command, new AppPatternDeleted())); + Raise(command, new AppPatternDeleted()); } public void UpdatePattern(UpdatePattern command) { - RaiseEvent(SimpleMapper.Map(command, new AppPatternUpdated())); + Raise(command, new AppPatternUpdated()); } public void AddRole(AddRole command) { - RaiseEvent(SimpleMapper.Map(command, new AppRoleAdded())); + Raise(command, new AppRoleAdded()); } public void DeleteRole(DeleteRole command) { - RaiseEvent(SimpleMapper.Map(command, new AppRoleDeleted())); + Raise(command, new AppRoleDeleted()); } public void UpdateRole(UpdateRole command) { - RaiseEvent(SimpleMapper.Map(command, new AppRoleUpdated())); + Raise(command, new AppRoleUpdated()); } public void ArchiveApp(ArchiveApp command) { - RaiseEvent(SimpleMapper.Map(command, new AppArchived())); + Raise(command, new AppArchived()); } - private void RaiseEvent(AppEvent @event) + private void Raise(T command, TEvent @event) where T : class where TEvent : AppEvent { + SimpleMapper.Map(command, @event); + @event.AppId ??= Snapshot.NamedId(); RaiseEvent(Envelope.Create(@event)); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs index a1cd5ccb3..fa76b72de 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs @@ -72,9 +72,9 @@ namespace Squidex.Domain.Apps.Entities.Assets { 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; }); @@ -92,9 +92,9 @@ namespace Squidex.Domain.Apps.Entities.Assets { 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; }); @@ -133,9 +133,9 @@ namespace Squidex.Domain.Apps.Entities.Assets return new HashSet(normalized.Values); } - public void Create(CreateAsset command, HashSet? tagIds) + public void Create(CreateAsset command) { - var @event = SimpleMapper.Map(command, new AssetCreated + Raise(command, new AssetCreated { MimeType = command.File.MimeType, FileName = command.File.FileName, @@ -143,45 +143,37 @@ namespace Squidex.Domain.Apps.Entities.Assets FileVersion = 0, Slug = command.File.FileName.ToAssetSlug() }); - - @event.Tags = tagIds; - - RaiseEvent(@event); } public void Update(UpdateAsset command) { - var @event = SimpleMapper.Map(command, new AssetUpdated + Raise(command, new AssetUpdated { MimeType = command.File.MimeType, FileVersion = Snapshot.FileVersion + 1, FileSize = command.File.FileSize }); - - RaiseEvent(@event); } - public void Annotate(AnnotateAsset command, HashSet? tagIds) + public void Annotate(AnnotateAsset command) { - var @event = SimpleMapper.Map(command, new AssetAnnotated()); - - @event.Tags = tagIds; - - RaiseEvent(@event); + Raise(command, new AssetAnnotated()); } public void Move(MoveAsset command) { - RaiseEvent(SimpleMapper.Map(command, new AssetMoved())); + Raise(command, new AssetMoved()); } 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 command, TEvent @event) where T : class where TEvent : AppEvent { + SimpleMapper.Map(command, @event); + @event.AppId ??= Snapshot.AppId; RaiseEvent(Envelope.Create(@event)); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObject.cs index 2558007a8..45137f887 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObject.cs @@ -96,26 +96,28 @@ namespace Squidex.Domain.Apps.Entities.Assets public void Create(CreateAssetFolder command) { - RaiseEvent(SimpleMapper.Map(command, new AssetFolderCreated())); + Raise(command, new AssetFolderCreated()); } public void Move(MoveAssetFolder command) { - RaiseEvent(SimpleMapper.Map(command, new AssetFolderMoved())); + Raise(command, new AssetFolderMoved()); } public void Rename(RenameAssetFolder command) { - RaiseEvent(SimpleMapper.Map(command, new AssetFolderRenamed())); + Raise(command, new AssetFolderRenamed()); } public void Delete(DeleteAssetFolder command) { - RaiseEvent(SimpleMapper.Map(command, new AssetFolderDeleted())); + Raise(command, new AssetFolderDeleted()); } - private void RaiseEvent(AppEvent @event) + private void Raise(T command, TEvent @event) where T : class where TEvent : AppEvent { + SimpleMapper.Map(command, @event); + @event.AppId ??= Snapshot.AppId; RaiseEvent(Envelope.Create(@event)); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs index 6b11706b3..693fdfa27 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs +++ b/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 HashSet Tags { get; } = new HashSet(); + public HashSet Tags { get; set; } = new HashSet(); public bool Duplicate { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ValidateContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ValidateContent.cs new file mode 100644 index 000000000..cace4c5a9 --- /dev/null +++ b/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 + { + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs index d5468dece..d7d7a68d9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Linq; using System.Threading.Tasks; using NodaTime; using Squidex.Domain.Apps.Core.Contents; @@ -79,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Contents case CreateContent createContent: 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); @@ -126,10 +127,22 @@ namespace Squidex.Domain.Apps.Entities.Contents 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: return UpdateReturnAsync(createContentDraft, async c => { - await LoadContext(Snapshot.AppId, Snapshot.SchemaId, c); + await LoadContext(c); GuardContent.CanCreateDraft(c, Snapshot); @@ -143,7 +156,7 @@ namespace Squidex.Domain.Apps.Entities.Contents case DeleteContentDraft deleteContentDraft: return UpdateReturnAsync(deleteContentDraft, async c => { - await LoadContext(Snapshot.AppId, Snapshot.SchemaId, c); + await LoadContext(c); GuardContent.CanDeleteDraft(c, Snapshot); @@ -173,7 +186,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { try { - await LoadContext(Snapshot.AppId, Snapshot.SchemaId, c); + await LoadContext(c); await GuardContent.CanChangeStatus(c, Snapshot, context.Workflow, context.Repository, context.Schema); @@ -183,7 +196,7 @@ namespace Squidex.Domain.Apps.Entities.Contents } else { - var change = GetChange(c); + var change = GetChange(c.Status); if (!c.DoNotScript && context.HasScript(c => c.Change)) { @@ -232,7 +245,7 @@ namespace Squidex.Domain.Apps.Entities.Contents case DeleteContent deleteContent: return UpdateAsync(deleteContent, async c => { - await LoadContext(Snapshot.AppId, Snapshot.SchemaId, c); + await LoadContext(c); await GuardContent.CanDelete(c, Snapshot, context.Repository, context.Schema); @@ -264,7 +277,7 @@ namespace Squidex.Domain.Apps.Entities.Contents if (!currentData!.Equals(newData)) { - await LoadContext(Snapshot.AppId, Snapshot.SchemaId, command, command.OptimizeValidation); + await LoadContext(command, command.OptimizeValidation); if (!command.DoNotValidate) { @@ -304,76 +317,85 @@ namespace Squidex.Domain.Apps.Entities.Contents public void Create(CreateContent command, Status status) { - RaiseEvent(SimpleMapper.Map(command, new ContentCreated { Status = status })); + Raise(command, new ContentCreated { Status = status }); 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) { - RaiseEvent(SimpleMapper.Map(command, new ContentDraftCreated { Status = status })); + Raise(command, new ContentDraftCreated { Status = status }); } public void Delete(DeleteContent command) { - RaiseEvent(SimpleMapper.Map(command, new ContentDeleted())); + Raise(command, new ContentDeleted()); } public void DeleteDraft(DeleteContentDraft command) { - RaiseEvent(SimpleMapper.Map(command, new ContentDraftDeleted())); + Raise(command, new ContentDraftDeleted()); } 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) { - RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Change = change })); + Raise(command, new ContentStatusChanged { Change = change }); } public void CancelChangeStatus(ChangeContentStatus command) { - RaiseEvent(SimpleMapper.Map(command, new ContentSchedulingCancelled())); + Raise(command, new ContentSchedulingCancelled()); } 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 command, TEvent @event) where TEvent : SchemaEvent where T : class { + SimpleMapper.Map(command, @event); + @event.AppId ??= Snapshot.AppId; @event.SchemaId ??= Snapshot.SchemaId; RaiseEvent(Envelope.Create(@event)); } - private StatusChange GetChange(ChangeContentStatus command) + private StatusChange GetChange(Status status) { - var change = StatusChange.Change; - - if (command.Status == Status.Published) + if (status == Status.Published) { - change = StatusChange.Published; + return StatusChange.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 appId, NamedId 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); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Operations/ContentOperationContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Operations/ContentOperationContext.cs index b5461cd5e..97c8efd9c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Operations/ContentOperationContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Operations/ContentOperationContext.cs @@ -146,6 +146,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.Operations CheckErrors(validator); } + public async Task> 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) { if (!schema.SchemaDef.Properties.ValidateOnPublish) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ValidationResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ValidationResult.cs new file mode 100644 index 000000000..220cfc64d --- /dev/null +++ b/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; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs index a27f84163..ecf3f251c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs @@ -121,31 +121,33 @@ namespace Squidex.Domain.Apps.Entities.Rules public void Create(CreateRule command) { - RaiseEvent(SimpleMapper.Map(command, new RuleCreated())); + Raise(command, new RuleCreated()); } public void Update(UpdateRule command) { - RaiseEvent(SimpleMapper.Map(command, new RuleUpdated())); + Raise(command, new RuleUpdated()); } public void Enable(EnableRule command) { - RaiseEvent(SimpleMapper.Map(command, new RuleEnabled())); + Raise(command, new RuleEnabled()); } public void Disable(DisableRule command) { - RaiseEvent(SimpleMapper.Map(command, new RuleDisabled())); + Raise(command, new RuleDisabled()); } public void Delete(DeleteRule command) { - RaiseEvent(SimpleMapper.Map(command, new RuleDeleted())); + Raise(command, new RuleDeleted()); } - private void RaiseEvent(AppEvent @event) + private void Raise(T command, TEvent @event) where T : class where TEvent : AppEvent { + SimpleMapper.Map(command, @event); + @event.AppId ??= Snapshot.AppId; RaiseEvent(Envelope.Create(@event)); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs index 83415965e..46e033ada 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs @@ -270,106 +270,106 @@ namespace Squidex.Domain.Apps.Entities.Schemas foreach (var @event in events) { - RaiseEvent(SimpleMapper.Map(command, (SchemaEvent)@event)); + Raise(command, @event); } } 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) { - RaiseEvent(command, new FieldAdded { FieldId = CreateFieldId(command) }); + Raise(command, new FieldAdded { FieldId = CreateFieldId(command) }); } public void UpdateField(UpdateField command) { - RaiseEvent(command, new FieldUpdated()); + Raise(command, new FieldUpdated()); } public void LockField(LockField command) { - RaiseEvent(command, new FieldLocked()); + Raise(command, new FieldLocked()); } public void HideField(HideField command) { - RaiseEvent(command, new FieldHidden()); + Raise(command, new FieldHidden()); } public void ShowField(ShowField command) { - RaiseEvent(command, new FieldShown()); + Raise(command, new FieldShown()); } public void DisableField(DisableField command) { - RaiseEvent(command, new FieldDisabled()); + Raise(command, new FieldDisabled()); } public void EnableField(EnableField command) { - RaiseEvent(command, new FieldEnabled()); + Raise(command, new FieldEnabled()); } public void DeleteField(DeleteField command) { - RaiseEvent(command, new FieldDeleted()); + Raise(command, new FieldDeleted()); } public void Reorder(ReorderFields command) { - RaiseEvent(command, new SchemaFieldsReordered()); + Raise(command, new SchemaFieldsReordered()); } public void Publish(PublishSchema command) { - RaiseEvent(command, new SchemaPublished()); + Raise(command, new SchemaPublished()); } public void Unpublish(UnpublishSchema command) { - RaiseEvent(command, new SchemaUnpublished()); + Raise(command, new SchemaUnpublished()); } public void ConfigureScripts(ConfigureScripts command) { - RaiseEvent(command, new SchemaScriptsConfigured()); + Raise(command, new SchemaScriptsConfigured()); } public void ConfigureFieldRules(ConfigureFieldRules command) { - RaiseEvent(command, new SchemaFieldRulesConfigured { FieldRules = command.ToFieldRules() }); + Raise(command, new SchemaFieldRulesConfigured { FieldRules = command.ToFieldRules() }); } public void ChangeCategory(ChangeCategory command) { - RaiseEvent(command, new SchemaCategoryChanged()); + Raise(command, new SchemaCategoryChanged()); } public void ConfigurePreviewUrls(ConfigurePreviewUrls command) { - RaiseEvent(command, new SchemaPreviewUrlsConfigured()); + Raise(command, new SchemaPreviewUrlsConfigured()); } public void ConfigureUIFields(ConfigureUIFields command) { - RaiseEvent(command, new SchemaUIFieldsConfigured()); + Raise(command, new SchemaUIFieldsConfigured()); } public void Update(UpdateSchema command) { - RaiseEvent(command, new SchemaUpdated()); + Raise(command, new SchemaUpdated()); } public void Delete(DeleteSchema command) { - RaiseEvent(command, new SchemaDeleted()); + Raise(command, new SchemaDeleted()); } - private void RaiseEvent(TCommand command, TEvent @event) where TCommand : class where TEvent : SchemaEvent + private void Raise(T command, TEvent @event) where TEvent : SchemaEvent where T : class { SimpleMapper.Map(command, @event); @@ -383,13 +383,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas 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) { @@ -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.SchemaId ??= Snapshot.NamedId(); diff --git a/backend/src/Squidex.Web/ApiExceptionConverter.cs b/backend/src/Squidex.Web/ApiExceptionConverter.cs index 2b2f4065a..6d23bd584 100644 --- a/backend/src/Squidex.Web/ApiExceptionConverter.cs +++ b/backend/src/Squidex.Web/ApiExceptionConverter.cs @@ -83,7 +83,7 @@ namespace Squidex.Web switch (exception) { 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 _: return (CreateError(404), true); @@ -121,7 +121,7 @@ namespace Squidex.Web return error; } - private static string[] ToDetails(ValidationException ex) + public static IEnumerable ToErrors(IEnumerable errors) { static string FixPropertyName(string property) { @@ -155,7 +155,7 @@ namespace Squidex.Web return builder.ToString(); } - return ex.Errors.Select(e => + return errors.Select(e => { if (e.PropertyNames?.Any() == true) { @@ -165,7 +165,7 @@ namespace Squidex.Web { return e.Message; } - }).ToArray(); + }); } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 623807984..c3a825c09 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -285,6 +285,36 @@ namespace Squidex.Areas.Api.Controllers.Contents return Ok(response); } + /// + /// Get a content item validity. + /// + /// The name of the app. + /// The name of the schema. + /// The id of the content to fetch. + /// + /// 200 => Content validation result returned. + /// 404 => Content, schema or app not found. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpGet] + [Route("content/{app}/{name}/{id}/validity")] + [ProducesResponseType(typeof(ValidationResultDto), 200)] + [ApiPermissionOrAnonymous] + [ApiCosts(1)] + public async Task GetContentValidity(string app, string name, DomainId id) + { + var command = new ValidateContent { ContentId = id }; + + var context = await CommandBus.PublishAsync(command); + + var result = context.Result(); + var response = ValidationResultDto.FromResult(result); + + return Ok(response); + } + /// /// Get all references of a content. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ValidationResultDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ValidationResultDto.cs new file mode 100644 index 000000000..f972ba1e9 --- /dev/null +++ b/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 + { + /// + /// The validation errors. + /// + public string[] Errors { get; set; } + + public static ValidationResultDto FromResult(ValidationResult result) + { + return new ValidationResultDto + { + Errors = ApiExceptionConverter.ToErrors(result.Errors).ToArray() + }; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs index 01084e49c..062077628 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Linq; using System.Threading.Tasks; using FakeItEasy; @@ -592,6 +593,20 @@ namespace Squidex.Domain.Apps.Entities.Contents 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() }); + + Assert.Equal(0, sut.Snapshot.Version); + } + [Fact] public async Task Delete_should_create_events_and_update_deleted_flag() {