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.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Core.EventSynchronization
{
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)
{
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<SchemaEvent> SyncFields<T>(
private static IEnumerable<ParentFieldEvent> SyncFields<T>(
FieldCollection<T> source,
FieldCollection<T> target,
Func<long> idGenerator,
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();
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<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)
{
@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 };
}
}
}

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)
{
@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, TEvent>(T command, TEvent @event) where T : class where TEvent : AppEvent
{
SimpleMapper.Map(command, @event);
@event.AppId ??= Snapshot.NamedId();
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);
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<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,
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<string>? 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, TEvent>(T command, TEvent @event) where T : class where TEvent : AppEvent
{
SimpleMapper.Map(command, @event);
@event.AppId ??= Snapshot.AppId;
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)
{
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, TEvent>(T command, TEvent @event) where T : class where TEvent : AppEvent
{
SimpleMapper.Map(command, @event);
@event.AppId ??= Snapshot.AppId;
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 HashSet<string> Tags { get; } = new HashSet<string>();
public HashSet<string> Tags { get; set; } = new HashSet<string>();
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.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, TEvent>(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<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);
}
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)
{
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)
{
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, TEvent>(T command, TEvent @event) where T : class where TEvent : AppEvent
{
SimpleMapper.Map(command, @event);
@event.AppId ??= Snapshot.AppId;
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)
{
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, 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);
@ -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();

8
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<string> ToErrors(IEnumerable<ValidationError> 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();
});
}
}
}

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

@ -285,6 +285,36 @@ namespace Squidex.Areas.Api.Controllers.Contents
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>
/// Get all references of a content.
/// </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.
// ==========================================================================
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<ValidationError>() });
Assert.Equal(0, sut.Snapshot.Version);
}
[Fact]
public async Task Delete_should_create_events_and_update_deleted_flag()
{

Loading…
Cancel
Save