From 637044996d818e740b5f665047e11bcc58950267 Mon Sep 17 00:00:00 2001 From: Francis Pion Date: Mon, 22 Jul 2024 16:35:26 -0400 Subject: [PATCH] Implemented field type replacement & update. (#33) * Implemented field type replacement. * Controller. * Implemented field type update. * Replace with delta. --- .../FieldTypes/ReplaceFieldTypePayload.cs | 25 +++ .../FieldTypes/UpdateFieldTypePayload.cs | 16 ++ .../Commands/ReplaceFieldTypeCommand.cs | 6 + .../ReplaceFieldTypeCommandHandler.cs | 106 +++++++++++ .../Commands/UpdateFieldTypeCommand.cs | 6 + .../Commands/UpdateFieldTypeCommandHandler.cs | 77 ++++++++ .../Events/FieldTypeUpdatedEvent.cs | 3 +- .../FieldTypes/FieldTypeAggregate.cs | 17 +- .../FieldTypes/IFieldTypeRepository.cs | 1 + .../Validators/CreateFieldTypeValidator.cs | 2 + .../Validators/ReplaceFieldTypeValidator.cs | 62 ++++++ .../Validators/UpdateFieldTypeValidator.cs | 57 ++++++ .../CmsSqlServerHelper.cs | 10 + .../DependencyInjectionExtensions.cs | 1 + .../Entities/FieldTypeEntity.cs | 4 + .../Contents/ContentCreatedEventHandler.cs | 12 +- .../UpdateFieldTypeInIndicesHandler.cs | 47 +++++ .../ICmsSqlHelper.cs | 9 + .../Logitar.Cms.EntityFrameworkCore.csproj | 1 + .../Repositories/FieldTypeRepository.cs | 4 + .../Controllers/FieldTypeController.cs | 14 ++ .../CreateFieldTypeCommandHandlerTests.cs | 3 + .../ReplaceFieldTypeCommandHandlerTests.cs | 98 ++++++++++ .../UpdateFieldTypeCommandHandlerTests.cs | 65 +++++++ .../CreateContentTypeCommandHandlerTests.cs | 5 +- .../CreateContentTypeCommandHandlerTests.cs | 5 +- .../CreateFieldTypeCommandHandlerTests.cs | 5 +- .../ReplaceFieldTypeCommandHandlerTests.cs | 177 ++++++++++++++++++ .../UpdateFieldTypeCommandHandlerTests.cs | 82 ++++++++ .../CreateLanguageCommandHandlerTests.cs | 5 +- 30 files changed, 910 insertions(+), 15 deletions(-) create mode 100644 backend/src/Logitar.Cms.Contracts/FieldTypes/ReplaceFieldTypePayload.cs create mode 100644 backend/src/Logitar.Cms.Contracts/FieldTypes/UpdateFieldTypePayload.cs create mode 100644 backend/src/Logitar.Cms.Core/FieldTypes/Commands/ReplaceFieldTypeCommand.cs create mode 100644 backend/src/Logitar.Cms.Core/FieldTypes/Commands/ReplaceFieldTypeCommandHandler.cs create mode 100644 backend/src/Logitar.Cms.Core/FieldTypes/Commands/UpdateFieldTypeCommand.cs create mode 100644 backend/src/Logitar.Cms.Core/FieldTypes/Commands/UpdateFieldTypeCommandHandler.cs create mode 100644 backend/src/Logitar.Cms.Core/FieldTypes/Validators/ReplaceFieldTypeValidator.cs create mode 100644 backend/src/Logitar.Cms.Core/FieldTypes/Validators/UpdateFieldTypeValidator.cs create mode 100644 backend/src/Logitar.Cms.EntityFrameworkCore.SqlServer/CmsSqlServerHelper.cs create mode 100644 backend/src/Logitar.Cms.EntityFrameworkCore/Handlers/Indexing/UpdateFieldTypeInIndicesHandler.cs create mode 100644 backend/src/Logitar.Cms.EntityFrameworkCore/ICmsSqlHelper.cs create mode 100644 backend/tests/Logitar.Cms.Core.IntegrationTests/FieldTypes/Commands/ReplaceFieldTypeCommandHandlerTests.cs create mode 100644 backend/tests/Logitar.Cms.Core.IntegrationTests/FieldTypes/Commands/UpdateFieldTypeCommandHandlerTests.cs create mode 100644 backend/tests/Logitar.Cms.Core.UnitTests/FieldTypes/Commands/ReplaceFieldTypeCommandHandlerTests.cs create mode 100644 backend/tests/Logitar.Cms.Core.UnitTests/FieldTypes/Commands/UpdateFieldTypeCommandHandlerTests.cs diff --git a/backend/src/Logitar.Cms.Contracts/FieldTypes/ReplaceFieldTypePayload.cs b/backend/src/Logitar.Cms.Contracts/FieldTypes/ReplaceFieldTypePayload.cs new file mode 100644 index 0000000..af1c965 --- /dev/null +++ b/backend/src/Logitar.Cms.Contracts/FieldTypes/ReplaceFieldTypePayload.cs @@ -0,0 +1,25 @@ +using Logitar.Cms.Contracts.FieldTypes.Properties; + +namespace Logitar.Cms.Contracts.FieldTypes; + +public record ReplaceFieldTypePayload +{ + public string UniqueName { get; set; } + public string? DisplayName { get; set; } + public string? Description { get; set; } + + public BooleanProperties? BooleanProperties { get; set; } + public DateTimeProperties? DateTimeProperties { get; set; } + public NumberProperties? NumberProperties { get; set; } + public StringProperties? StringProperties { get; set; } + public TextProperties? TextProperties { get; set; } + + public ReplaceFieldTypePayload() : this(string.Empty) + { + } + + public ReplaceFieldTypePayload(string uniqueName) + { + UniqueName = uniqueName; + } +} diff --git a/backend/src/Logitar.Cms.Contracts/FieldTypes/UpdateFieldTypePayload.cs b/backend/src/Logitar.Cms.Contracts/FieldTypes/UpdateFieldTypePayload.cs new file mode 100644 index 0000000..32a1c04 --- /dev/null +++ b/backend/src/Logitar.Cms.Contracts/FieldTypes/UpdateFieldTypePayload.cs @@ -0,0 +1,16 @@ +using Logitar.Cms.Contracts.FieldTypes.Properties; + +namespace Logitar.Cms.Contracts.FieldTypes; + +public record UpdateFieldTypePayload +{ + public string? UniqueName { get; set; } + public Change? DisplayName { get; set; } + public Change? Description { get; set; } + + public BooleanProperties? BooleanProperties { get; set; } + public DateTimeProperties? DateTimeProperties { get; set; } + public NumberProperties? NumberProperties { get; set; } + public StringProperties? StringProperties { get; set; } + public TextProperties? TextProperties { get; set; } +} diff --git a/backend/src/Logitar.Cms.Core/FieldTypes/Commands/ReplaceFieldTypeCommand.cs b/backend/src/Logitar.Cms.Core/FieldTypes/Commands/ReplaceFieldTypeCommand.cs new file mode 100644 index 0000000..dda2b1f --- /dev/null +++ b/backend/src/Logitar.Cms.Core/FieldTypes/Commands/ReplaceFieldTypeCommand.cs @@ -0,0 +1,6 @@ +using Logitar.Cms.Contracts.FieldTypes; +using MediatR; + +namespace Logitar.Cms.Core.FieldTypes.Commands; + +public record ReplaceFieldTypeCommand(Guid Id, ReplaceFieldTypePayload Payload, long? Version) : Activity, IRequest; diff --git a/backend/src/Logitar.Cms.Core/FieldTypes/Commands/ReplaceFieldTypeCommandHandler.cs b/backend/src/Logitar.Cms.Core/FieldTypes/Commands/ReplaceFieldTypeCommandHandler.cs new file mode 100644 index 0000000..83e581a --- /dev/null +++ b/backend/src/Logitar.Cms.Core/FieldTypes/Commands/ReplaceFieldTypeCommandHandler.cs @@ -0,0 +1,106 @@ +using FluentValidation; +using Logitar.Cms.Contracts.FieldTypes; +using Logitar.Cms.Core.FieldTypes.Properties; +using Logitar.Cms.Core.FieldTypes.Validators; +using Logitar.Identity.Contracts.Settings; +using Logitar.Identity.Domain.Shared; +using MediatR; + +namespace Logitar.Cms.Core.FieldTypes.Commands; + +internal class ReplaceFieldTypeCommandHandler : IRequestHandler +{ + private readonly IFieldTypeQuerier _fieldTypeQuerier; + private readonly IFieldTypeRepository _fieldTypeRepository; + private readonly ISender _sender; + + public ReplaceFieldTypeCommandHandler(IFieldTypeQuerier fieldTypeQuerier, IFieldTypeRepository fieldTypeRepository, ISender sender) + { + _fieldTypeQuerier = fieldTypeQuerier; + _fieldTypeRepository = fieldTypeRepository; + _sender = sender; + } + + public async Task Handle(ReplaceFieldTypeCommand command, CancellationToken cancellationToken) + { + FieldTypeId id = new(command.Id); + FieldTypeAggregate? fieldType = await _fieldTypeRepository.LoadAsync(id, cancellationToken); + if (fieldType == null) + { + return null; + } + + IUniqueNameSettings uniqueNameSettings = FieldTypeAggregate.UniqueNameSettings; + + ReplaceFieldTypePayload payload = command.Payload; + new ReplaceFieldTypeValidator(uniqueNameSettings, fieldType.DataType).ValidateAndThrow(payload); + + FieldTypeAggregate? reference = null; + if (command.Version.HasValue) + { + reference = await _fieldTypeRepository.LoadAsync(id, command.Version.Value, cancellationToken); + } + + UniqueNameUnit uniqueName = new(uniqueNameSettings, payload.UniqueName); + DisplayNameUnit? displayName = DisplayNameUnit.TryCreate(payload.DisplayName); + DescriptionUnit? description = DescriptionUnit.TryCreate(payload.Description); + if (reference == null || uniqueName != reference.UniqueName) + { + fieldType.UniqueName = uniqueName; + } + if (reference == null || displayName != reference.DisplayName) + { + fieldType.DisplayName = displayName; + } + if (reference == null || description != reference.Description) + { + fieldType.Description = description; + } + fieldType.Update(command.ActorId); + + if (payload.BooleanProperties != null) + { + ReadOnlyBooleanProperties properties = new(payload.BooleanProperties); + if (reference == null || properties != reference.Properties) + { + fieldType.SetProperties(properties, command.ActorId); + } + } + if (payload.DateTimeProperties != null) + { + ReadOnlyDateTimeProperties properties = new(payload.DateTimeProperties); + if (reference == null || properties != reference.Properties) + { + fieldType.SetProperties(properties, command.ActorId); + } + } + if (payload.NumberProperties != null) + { + ReadOnlyNumberProperties properties = new(payload.NumberProperties); + if (reference == null || properties != reference.Properties) + { + fieldType.SetProperties(properties, command.ActorId); + } + } + if (payload.StringProperties != null) + { + ReadOnlyStringProperties properties = new(payload.StringProperties); + if (reference == null || properties != reference.Properties) + { + fieldType.SetProperties(properties, command.ActorId); + } + } + if (payload.TextProperties != null) + { + ReadOnlyTextProperties properties = new(payload.TextProperties); + if (reference == null || properties != reference.Properties) + { + fieldType.SetProperties(properties, command.ActorId); + } + } + + await _sender.Send(new SaveFieldTypeCommand(fieldType), cancellationToken); + + return await _fieldTypeQuerier.ReadAsync(fieldType, cancellationToken); + } +} diff --git a/backend/src/Logitar.Cms.Core/FieldTypes/Commands/UpdateFieldTypeCommand.cs b/backend/src/Logitar.Cms.Core/FieldTypes/Commands/UpdateFieldTypeCommand.cs new file mode 100644 index 0000000..1b474ac --- /dev/null +++ b/backend/src/Logitar.Cms.Core/FieldTypes/Commands/UpdateFieldTypeCommand.cs @@ -0,0 +1,6 @@ +using Logitar.Cms.Contracts.FieldTypes; +using MediatR; + +namespace Logitar.Cms.Core.FieldTypes.Commands; + +public record UpdateFieldTypeCommand(Guid Id, UpdateFieldTypePayload Payload) : Activity, IRequest; diff --git a/backend/src/Logitar.Cms.Core/FieldTypes/Commands/UpdateFieldTypeCommandHandler.cs b/backend/src/Logitar.Cms.Core/FieldTypes/Commands/UpdateFieldTypeCommandHandler.cs new file mode 100644 index 0000000..f13b8cd --- /dev/null +++ b/backend/src/Logitar.Cms.Core/FieldTypes/Commands/UpdateFieldTypeCommandHandler.cs @@ -0,0 +1,77 @@ +using FluentValidation; +using Logitar.Cms.Contracts.FieldTypes; +using Logitar.Cms.Core.FieldTypes.Properties; +using Logitar.Cms.Core.FieldTypes.Validators; +using Logitar.Identity.Contracts.Settings; +using Logitar.Identity.Domain.Shared; +using MediatR; + +namespace Logitar.Cms.Core.FieldTypes.Commands; + +internal class UpdateFieldTypeCommandHandler : IRequestHandler +{ + private readonly IFieldTypeQuerier _fieldTypeQuerier; + private readonly IFieldTypeRepository _fieldTypeRepository; + private readonly ISender _sender; + + public UpdateFieldTypeCommandHandler(IFieldTypeQuerier fieldTypeQuerier, IFieldTypeRepository fieldTypeRepository, ISender sender) + { + _fieldTypeQuerier = fieldTypeQuerier; + _fieldTypeRepository = fieldTypeRepository; + _sender = sender; + } + + public async Task Handle(UpdateFieldTypeCommand command, CancellationToken cancellationToken) + { + FieldTypeId id = new(command.Id); + FieldTypeAggregate? fieldType = await _fieldTypeRepository.LoadAsync(id, cancellationToken); + if (fieldType == null) + { + return null; + } + + IUniqueNameSettings uniqueNameSettings = FieldTypeAggregate.UniqueNameSettings; + + UpdateFieldTypePayload payload = command.Payload; + new UpdateFieldTypeValidator(uniqueNameSettings, fieldType.DataType).ValidateAndThrow(payload); + + if (!string.IsNullOrWhiteSpace(payload.UniqueName)) + { + fieldType.UniqueName = new UniqueNameUnit(uniqueNameSettings, payload.UniqueName); + } + if (payload.DisplayName != null) + { + fieldType.DisplayName = DisplayNameUnit.TryCreate(payload.DisplayName.Value); + } + if (payload.Description != null) + { + fieldType.Description = DescriptionUnit.TryCreate(payload.Description.Value); + } + fieldType.Update(command.ActorId); + + if (payload.BooleanProperties != null) + { + fieldType.SetProperties(new ReadOnlyBooleanProperties(payload.BooleanProperties), command.ActorId); + } + if (payload.DateTimeProperties != null) + { + fieldType.SetProperties(new ReadOnlyDateTimeProperties(payload.DateTimeProperties), command.ActorId); + } + if (payload.NumberProperties != null) + { + fieldType.SetProperties(new ReadOnlyNumberProperties(payload.NumberProperties), command.ActorId); + } + if (payload.StringProperties != null) + { + fieldType.SetProperties(new ReadOnlyStringProperties(payload.StringProperties), command.ActorId); + } + if (payload.TextProperties != null) + { + fieldType.SetProperties(new ReadOnlyTextProperties(payload.TextProperties), command.ActorId); + } + + await _sender.Send(new SaveFieldTypeCommand(fieldType), cancellationToken); + + return await _fieldTypeQuerier.ReadAsync(fieldType, cancellationToken); + } +} diff --git a/backend/src/Logitar.Cms.Core/FieldTypes/Events/FieldTypeUpdatedEvent.cs b/backend/src/Logitar.Cms.Core/FieldTypes/Events/FieldTypeUpdatedEvent.cs index ff0e939..bcecafa 100644 --- a/backend/src/Logitar.Cms.Core/FieldTypes/Events/FieldTypeUpdatedEvent.cs +++ b/backend/src/Logitar.Cms.Core/FieldTypes/Events/FieldTypeUpdatedEvent.cs @@ -7,8 +7,9 @@ namespace Logitar.Cms.Core.FieldTypes.Events; public class FieldTypeUpdatedEvent : DomainEvent, INotification { + public UniqueNameUnit? UniqueName { get; set; } public Change? DisplayName { get; set; } public Change? Description { get; set; } - public bool HasChanges => DisplayName != null || Description != null; + public bool HasChanges => UniqueName != null || DisplayName != null || Description != null; } diff --git a/backend/src/Logitar.Cms.Core/FieldTypes/FieldTypeAggregate.cs b/backend/src/Logitar.Cms.Core/FieldTypes/FieldTypeAggregate.cs index 703cdbe..92db3fd 100644 --- a/backend/src/Logitar.Cms.Core/FieldTypes/FieldTypeAggregate.cs +++ b/backend/src/Logitar.Cms.Core/FieldTypes/FieldTypeAggregate.cs @@ -26,7 +26,18 @@ public class FieldTypeAggregate : AggregateRoot public new FieldTypeId Id => new(base.Id); private UniqueNameUnit? _uniqueName = null; - public UniqueNameUnit UniqueName => _uniqueName ?? throw new InvalidOperationException($"The {nameof(UniqueName)} has not been initialized yet."); + public UniqueNameUnit UniqueName + { + get => _uniqueName ?? throw new InvalidOperationException($"The {nameof(UniqueName)} has not been initialized yet."); + set + { + if (_uniqueName != value) + { + _uniqueName = value; + _updatedEvent.UniqueName = value; + } + } + } private DisplayNameUnit? _displayName = null; public DisplayNameUnit? DisplayName { @@ -199,6 +210,10 @@ public void Update(ActorId actorId = default) } protected virtual void Apply(FieldTypeUpdatedEvent @event) { + if (@event.UniqueName != null) + { + _uniqueName = @event.UniqueName; + } if (@event.DisplayName != null) { _displayName = @event.DisplayName.Value; diff --git a/backend/src/Logitar.Cms.Core/FieldTypes/IFieldTypeRepository.cs b/backend/src/Logitar.Cms.Core/FieldTypes/IFieldTypeRepository.cs index 25d1894..fe5d74f 100644 --- a/backend/src/Logitar.Cms.Core/FieldTypes/IFieldTypeRepository.cs +++ b/backend/src/Logitar.Cms.Core/FieldTypes/IFieldTypeRepository.cs @@ -6,6 +6,7 @@ public interface IFieldTypeRepository { Task> LoadAsync(CancellationToken cancellationToken = default); Task LoadAsync(FieldTypeId id, CancellationToken cancellationToken = default); + Task LoadAsync(FieldTypeId id, long? version, CancellationToken cancellationToken = default); Task> LoadAsync(IEnumerable ids, CancellationToken cancellationToken = default); Task LoadAsync(UniqueNameUnit uniqueName, CancellationToken cancellationToken = default); diff --git a/backend/src/Logitar.Cms.Core/FieldTypes/Validators/CreateFieldTypeValidator.cs b/backend/src/Logitar.Cms.Core/FieldTypes/Validators/CreateFieldTypeValidator.cs index e3d7e4b..ec95792 100644 --- a/backend/src/Logitar.Cms.Core/FieldTypes/Validators/CreateFieldTypeValidator.cs +++ b/backend/src/Logitar.Cms.Core/FieldTypes/Validators/CreateFieldTypeValidator.cs @@ -19,6 +19,8 @@ public CreateFieldTypeValidator(IUniqueNameSettings uniqueNameSettings) .Otherwise(() => { When(x => x.BooleanProperties != null, () => RuleFor(x => x.BooleanProperties!).SetValidator(new BooleanPropertiesValidator())); + When(x => x.DateTimeProperties != null, () => RuleFor(x => x.DateTimeProperties!).SetValidator(new DateTimePropertiesValidator())); + When(x => x.NumberProperties != null, () => RuleFor(x => x.NumberProperties!).SetValidator(new NumberPropertiesValidator())); When(x => x.StringProperties != null, () => RuleFor(x => x.StringProperties!).SetValidator(new StringPropertiesValidator())); When(x => x.TextProperties != null, () => RuleFor(x => x.TextProperties!).SetValidator(new TextPropertiesValidator())); }); diff --git a/backend/src/Logitar.Cms.Core/FieldTypes/Validators/ReplaceFieldTypeValidator.cs b/backend/src/Logitar.Cms.Core/FieldTypes/Validators/ReplaceFieldTypeValidator.cs new file mode 100644 index 0000000..8d60a56 --- /dev/null +++ b/backend/src/Logitar.Cms.Core/FieldTypes/Validators/ReplaceFieldTypeValidator.cs @@ -0,0 +1,62 @@ +using FluentValidation; +using Logitar.Cms.Contracts.FieldTypes; +using Logitar.Identity.Contracts.Settings; +using Logitar.Identity.Domain.Shared; + +namespace Logitar.Cms.Core.FieldTypes.Validators; + +public class ReplaceFieldTypeValidator : AbstractValidator +{ + public ReplaceFieldTypeValidator(IUniqueNameSettings uniqueNameSettings, DataType dataType) + { + RuleFor(x => x.UniqueName).SetValidator(new UniqueNameValidator(uniqueNameSettings)); + When(x => !string.IsNullOrWhiteSpace(x.DisplayName), () => RuleFor(x => x.DisplayName!).SetValidator(new DisplayNameValidator())); + When(x => !string.IsNullOrWhiteSpace(x.Description), () => RuleFor(x => x.Description!).SetValidator(new DescriptionValidator())); + + switch (dataType) + { + case DataType.Boolean: + When(x => x.BooleanProperties != null, () => RuleFor(x => x.BooleanProperties!).SetValidator(new BooleanPropertiesValidator())) + .Otherwise(() => RuleFor(x => x.BooleanProperties).NotNull()); + RuleFor(x => x.DateTimeProperties).Null(); + RuleFor(x => x.NumberProperties).Null(); + RuleFor(x => x.StringProperties).Null(); + RuleFor(x => x.TextProperties).Null(); + break; + case DataType.DateTime: + RuleFor(x => x.BooleanProperties).Null(); + When(x => x.DateTimeProperties != null, () => RuleFor(x => x.DateTimeProperties!).SetValidator(new DateTimePropertiesValidator())) + .Otherwise(() => RuleFor(x => x.DateTimeProperties).NotNull()); + RuleFor(x => x.NumberProperties).Null(); + RuleFor(x => x.StringProperties).Null(); + RuleFor(x => x.TextProperties).Null(); + break; + case DataType.Number: + RuleFor(x => x.BooleanProperties).Null(); + RuleFor(x => x.DateTimeProperties).Null(); + When(x => x.NumberProperties != null, () => RuleFor(x => x.NumberProperties!).SetValidator(new NumberPropertiesValidator())) + .Otherwise(() => RuleFor(x => x.NumberProperties).NotNull()); + RuleFor(x => x.StringProperties).Null(); + RuleFor(x => x.TextProperties).Null(); + break; + case DataType.String: + RuleFor(x => x.BooleanProperties).Null(); + RuleFor(x => x.DateTimeProperties).Null(); + RuleFor(x => x.NumberProperties).Null(); + When(x => x.StringProperties != null, () => RuleFor(x => x.StringProperties!).SetValidator(new StringPropertiesValidator())) + .Otherwise(() => RuleFor(x => x.StringProperties).NotNull()); + RuleFor(x => x.TextProperties).Null(); + break; + case DataType.Text: + RuleFor(x => x.BooleanProperties).Null(); + RuleFor(x => x.DateTimeProperties).Null(); + RuleFor(x => x.NumberProperties).Null(); + RuleFor(x => x.StringProperties).Null(); + When(x => x.TextProperties != null, () => RuleFor(x => x.TextProperties!).SetValidator(new TextPropertiesValidator())) + .Otherwise(() => RuleFor(x => x.TextProperties).NotNull()); + break; + default: + throw new DataTypeNotSupportedException(dataType); + } + } +} diff --git a/backend/src/Logitar.Cms.Core/FieldTypes/Validators/UpdateFieldTypeValidator.cs b/backend/src/Logitar.Cms.Core/FieldTypes/Validators/UpdateFieldTypeValidator.cs new file mode 100644 index 0000000..bfa3aab --- /dev/null +++ b/backend/src/Logitar.Cms.Core/FieldTypes/Validators/UpdateFieldTypeValidator.cs @@ -0,0 +1,57 @@ +using FluentValidation; +using Logitar.Cms.Contracts.FieldTypes; +using Logitar.Identity.Contracts.Settings; +using Logitar.Identity.Domain.Shared; + +namespace Logitar.Cms.Core.FieldTypes.Validators; + +public class UpdateFieldTypeValidator : AbstractValidator +{ + public UpdateFieldTypeValidator(IUniqueNameSettings uniqueNameSettings, DataType dataType) + { + When(x => !string.IsNullOrWhiteSpace(x.UniqueName), () => RuleFor(x => x.UniqueName!).SetValidator(new UniqueNameValidator(uniqueNameSettings))); + When(x => !string.IsNullOrWhiteSpace(x.DisplayName?.Value), () => RuleFor(x => x.DisplayName!.Value!).SetValidator(new DisplayNameValidator())); + When(x => !string.IsNullOrWhiteSpace(x.Description?.Value), () => RuleFor(x => x.Description!.Value!).SetValidator(new DescriptionValidator())); + + switch (dataType) + { + case DataType.Boolean: + When(x => x.BooleanProperties != null, () => RuleFor(x => x.BooleanProperties!).SetValidator(new BooleanPropertiesValidator())); + RuleFor(x => x.DateTimeProperties).Null(); + RuleFor(x => x.NumberProperties).Null(); + RuleFor(x => x.StringProperties).Null(); + RuleFor(x => x.TextProperties).Null(); + break; + case DataType.DateTime: + RuleFor(x => x.BooleanProperties).Null(); + When(x => x.DateTimeProperties != null, () => RuleFor(x => x.DateTimeProperties!).SetValidator(new DateTimePropertiesValidator())); + RuleFor(x => x.NumberProperties).Null(); + RuleFor(x => x.StringProperties).Null(); + RuleFor(x => x.TextProperties).Null(); + break; + case DataType.Number: + RuleFor(x => x.BooleanProperties).Null(); + RuleFor(x => x.DateTimeProperties).Null(); + When(x => x.NumberProperties != null, () => RuleFor(x => x.NumberProperties!).SetValidator(new NumberPropertiesValidator())); + RuleFor(x => x.StringProperties).Null(); + RuleFor(x => x.TextProperties).Null(); + break; + case DataType.String: + RuleFor(x => x.BooleanProperties).Null(); + RuleFor(x => x.DateTimeProperties).Null(); + RuleFor(x => x.NumberProperties).Null(); + When(x => x.StringProperties != null, () => RuleFor(x => x.StringProperties!).SetValidator(new StringPropertiesValidator())); + RuleFor(x => x.TextProperties).Null(); + break; + case DataType.Text: + RuleFor(x => x.BooleanProperties).Null(); + RuleFor(x => x.DateTimeProperties).Null(); + RuleFor(x => x.NumberProperties).Null(); + RuleFor(x => x.StringProperties).Null(); + When(x => x.TextProperties != null, () => RuleFor(x => x.TextProperties!).SetValidator(new TextPropertiesValidator())); + break; + default: + throw new DataTypeNotSupportedException(dataType); + } + } +} diff --git a/backend/src/Logitar.Cms.EntityFrameworkCore.SqlServer/CmsSqlServerHelper.cs b/backend/src/Logitar.Cms.EntityFrameworkCore.SqlServer/CmsSqlServerHelper.cs new file mode 100644 index 0000000..f849221 --- /dev/null +++ b/backend/src/Logitar.Cms.EntityFrameworkCore.SqlServer/CmsSqlServerHelper.cs @@ -0,0 +1,10 @@ +using Logitar.Data; +using Logitar.Data.SqlServer; +using Logitar.Identity.EntityFrameworkCore.SqlServer; + +namespace Logitar.Cms.EntityFrameworkCore.SqlServer; + +internal class CmsSqlServerHelper : SqlServerHelper, ICmsSqlHelper +{ + public IUpdateBuilder Update(TableId table) => new SqlServerUpdateBuilder(); +} diff --git a/backend/src/Logitar.Cms.EntityFrameworkCore.SqlServer/DependencyInjectionExtensions.cs b/backend/src/Logitar.Cms.EntityFrameworkCore.SqlServer/DependencyInjectionExtensions.cs index f43807b..245d012 100644 --- a/backend/src/Logitar.Cms.EntityFrameworkCore.SqlServer/DependencyInjectionExtensions.cs +++ b/backend/src/Logitar.Cms.EntityFrameworkCore.SqlServer/DependencyInjectionExtensions.cs @@ -28,6 +28,7 @@ public static IServiceCollection AddLogitarCmsWithEntityFrameworkCoreSqlServer(t .AddLogitarIdentityWithEntityFrameworkCoreSqlServer(connectionString) .AddLogitarCmsWithEntityFrameworkCore() .AddDbContext(options => options.UseSqlServer(connectionString, b => b.MigrationsAssembly("Logitar.Cms.EntityFrameworkCore.SqlServer"))) + .AddSingleton() .AddSingleton(); } } diff --git a/backend/src/Logitar.Cms.EntityFrameworkCore/Entities/FieldTypeEntity.cs b/backend/src/Logitar.Cms.EntityFrameworkCore/Entities/FieldTypeEntity.cs index 365e5de..27940d0 100644 --- a/backend/src/Logitar.Cms.EntityFrameworkCore/Entities/FieldTypeEntity.cs +++ b/backend/src/Logitar.Cms.EntityFrameworkCore/Entities/FieldTypeEntity.cs @@ -138,6 +138,10 @@ public void Update(FieldTypeUpdatedEvent @event) { base.Update(@event); + if (@event.UniqueName != null) + { + UniqueName = @event.UniqueName.Value; + } if (@event.DisplayName != null) { DisplayName = @event.DisplayName.Value?.Value; diff --git a/backend/src/Logitar.Cms.EntityFrameworkCore/Handlers/Contents/ContentCreatedEventHandler.cs b/backend/src/Logitar.Cms.EntityFrameworkCore/Handlers/Contents/ContentCreatedEventHandler.cs index 5e551f4..b1e4fcd 100644 --- a/backend/src/Logitar.Cms.EntityFrameworkCore/Handlers/Contents/ContentCreatedEventHandler.cs +++ b/backend/src/Logitar.Cms.EntityFrameworkCore/Handlers/Contents/ContentCreatedEventHandler.cs @@ -1,5 +1,6 @@ using Logitar.Cms.Core.Contents.Events; using Logitar.Cms.EntityFrameworkCore.Entities; +using Logitar.Cms.EntityFrameworkCore.Indexing; using MediatR; using Microsoft.EntityFrameworkCore; @@ -8,10 +9,12 @@ namespace Logitar.Cms.EntityFrameworkCore.Handlers.Contents; internal class ContentCreatedEventHandler : INotificationHandler { private readonly CmsContext _context; + private readonly IPublisher _publisher; - public ContentCreatedEventHandler(CmsContext context) + public ContentCreatedEventHandler(CmsContext context, IPublisher publisher) { _context = context; + _publisher = publisher; } public async Task Handle(ContentCreatedEvent @event, CancellationToken cancellationToken) @@ -29,6 +32,13 @@ public async Task Handle(ContentCreatedEvent @event, CancellationToken cancellat _context.ContentItems.Add(content); await _context.SaveChangesAsync(cancellationToken); + + ContentLocaleEntity locale = await _context.ContentLocales + .Include(x => x.Item).ThenInclude(x => x!.ContentType).ThenInclude(x => x!.FieldDefinitions).ThenInclude(x => x.FieldType) + .Include(x => x.Language) + .SingleOrDefaultAsync(x => x.ContentItemId == content.ContentItemId && x.LanguageId == null, cancellationToken) + ?? throw new InvalidOperationException($"The content locale entity 'ContentItemId={content.ContentItemId}, LanguageId=' could not be found."); + await _publisher.Publish(new UpdateFieldIndicesCommand(locale, @event.Invariant.FieldValues), cancellationToken); } } } diff --git a/backend/src/Logitar.Cms.EntityFrameworkCore/Handlers/Indexing/UpdateFieldTypeInIndicesHandler.cs b/backend/src/Logitar.Cms.EntityFrameworkCore/Handlers/Indexing/UpdateFieldTypeInIndicesHandler.cs new file mode 100644 index 0000000..c1ef40e --- /dev/null +++ b/backend/src/Logitar.Cms.EntityFrameworkCore/Handlers/Indexing/UpdateFieldTypeInIndicesHandler.cs @@ -0,0 +1,47 @@ +using Logitar.Cms.Core.FieldTypes.Events; +using Logitar.Data; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Logitar.Cms.EntityFrameworkCore.Handlers.Indexing; + +internal class UpdateFieldTypeInIndicesHandler : INotificationHandler +{ + private static readonly TableId[] _tables = + [ + CmsDb.BooleanFieldIndex.Table, CmsDb.DateTimeFieldIndex.Table, CmsDb.NumberFieldIndex.Table, + CmsDb.StringFieldIndex.Table, CmsDb.TextFieldIndex.Table, CmsDb.UniqueFieldIndex.Table + ]; + + private readonly CmsContext _context; + private readonly ICmsSqlHelper _sqlHelper; + + public UpdateFieldTypeInIndicesHandler(CmsContext context, ICmsSqlHelper sqlHelper) + { + _context = context; + _sqlHelper = sqlHelper; + } + + public async Task Handle(FieldTypeUpdatedEvent @event, CancellationToken cancellationToken) + { + string? uniqueName = @event.UniqueName?.Value; + if (uniqueName != null) + { + string fieldTypeUid = @event.AggregateId.ToGuid().ToString(); + + StringBuilder statement = new(); + IEnumerable? parameters = null; + foreach (TableId table in _tables) + { + ICommand command = _sqlHelper.Update(table) + .Set(new Update(new ColumnId("FieldTypeName", table), uniqueName)) + .Where(new OperatorCondition(new ColumnId("FieldTypeUid", table), Operators.IsEqualTo(fieldTypeUid))) + .Build(); + statement.Append(command.Text).Append(';').AppendLine().AppendLine(); + parameters ??= command.Parameters; + } + + await _context.Database.ExecuteSqlRawAsync(statement.ToString(), parameters ?? [], cancellationToken); + } + } +} diff --git a/backend/src/Logitar.Cms.EntityFrameworkCore/ICmsSqlHelper.cs b/backend/src/Logitar.Cms.EntityFrameworkCore/ICmsSqlHelper.cs new file mode 100644 index 0000000..69f25bb --- /dev/null +++ b/backend/src/Logitar.Cms.EntityFrameworkCore/ICmsSqlHelper.cs @@ -0,0 +1,9 @@ +using Logitar.Data; +using Logitar.Identity.EntityFrameworkCore.Relational; + +namespace Logitar.Cms.EntityFrameworkCore; + +public interface ICmsSqlHelper : ISqlHelper +{ + IUpdateBuilder Update(TableId table); +} diff --git a/backend/src/Logitar.Cms.EntityFrameworkCore/Logitar.Cms.EntityFrameworkCore.csproj b/backend/src/Logitar.Cms.EntityFrameworkCore/Logitar.Cms.EntityFrameworkCore.csproj index cc27641..81ae28d 100644 --- a/backend/src/Logitar.Cms.EntityFrameworkCore/Logitar.Cms.EntityFrameworkCore.csproj +++ b/backend/src/Logitar.Cms.EntityFrameworkCore/Logitar.Cms.EntityFrameworkCore.csproj @@ -25,6 +25,7 @@ + diff --git a/backend/src/Logitar.Cms.EntityFrameworkCore/Repositories/FieldTypeRepository.cs b/backend/src/Logitar.Cms.EntityFrameworkCore/Repositories/FieldTypeRepository.cs index f173371..7499ef2 100644 --- a/backend/src/Logitar.Cms.EntityFrameworkCore/Repositories/FieldTypeRepository.cs +++ b/backend/src/Logitar.Cms.EntityFrameworkCore/Repositories/FieldTypeRepository.cs @@ -30,6 +30,10 @@ public async Task> LoadAsync(Cancellatio { return await base.LoadAsync(id.AggregateId, cancellationToken); } + public async Task LoadAsync(FieldTypeId id, long? version, CancellationToken cancellationToken) + { + return await base.LoadAsync(id.AggregateId, version, cancellationToken); + } public async Task> LoadAsync(IEnumerable ids, CancellationToken cancellationToken) { diff --git a/backend/src/Logitar.Cms.Web/Controllers/FieldTypeController.cs b/backend/src/Logitar.Cms.Web/Controllers/FieldTypeController.cs index 074ce37..225c713 100644 --- a/backend/src/Logitar.Cms.Web/Controllers/FieldTypeController.cs +++ b/backend/src/Logitar.Cms.Web/Controllers/FieldTypeController.cs @@ -44,10 +44,24 @@ public async Task> ReadAsync(string uniqueName, Cancella return fieldType == null ? NotFound() : Ok(fieldType); } + [HttpPut("{id}")] + public async Task> ReplaceAsync(Guid id, [FromBody] ReplaceFieldTypePayload payload, long? version, CancellationToken cancellationToken) + { + FieldType? fieldType = await _pipeline.ExecuteAsync(new ReplaceFieldTypeCommand(id, payload, version), cancellationToken); + return fieldType == null ? NotFound() : Ok(fieldType); + } + [HttpGet] public async Task>> SearchAsync([FromQuery] SearchFieldTypesParameters parameters, CancellationToken cancellationToken) { SearchResults fieldTypes = await _pipeline.ExecuteAsync(new SearchFieldTypesQuery(parameters.ToPayload()), cancellationToken); return Ok(fieldTypes); } + + [HttpPatch("{id}")] + public async Task> UpdateAsync(Guid id, [FromBody] UpdateFieldTypePayload payload, CancellationToken cancellationToken) + { + FieldType? fieldType = await _pipeline.ExecuteAsync(new UpdateFieldTypeCommand(id, payload), cancellationToken); + return fieldType == null ? NotFound() : Ok(fieldType); + } } diff --git a/backend/tests/Logitar.Cms.Core.IntegrationTests/FieldTypes/Commands/CreateFieldTypeCommandHandlerTests.cs b/backend/tests/Logitar.Cms.Core.IntegrationTests/FieldTypes/Commands/CreateFieldTypeCommandHandlerTests.cs index b03acf1..653f731 100644 --- a/backend/tests/Logitar.Cms.Core.IntegrationTests/FieldTypes/Commands/CreateFieldTypeCommandHandlerTests.cs +++ b/backend/tests/Logitar.Cms.Core.IntegrationTests/FieldTypes/Commands/CreateFieldTypeCommandHandlerTests.cs @@ -37,6 +37,9 @@ public async Task It_should_create_a_new_field_type() Assert.Equal(payload.DisplayName.Trim(), fieldType.DisplayName); Assert.Null(fieldType.Description); Assert.Equal(DataType.String, fieldType.DataType); + Assert.Null(fieldType.BooleanProperties); + Assert.Null(fieldType.DateTimeProperties); + Assert.Null(fieldType.NumberProperties); Assert.Equal(payload.StringProperties, fieldType.StringProperties); Assert.Null(fieldType.TextProperties); } diff --git a/backend/tests/Logitar.Cms.Core.IntegrationTests/FieldTypes/Commands/ReplaceFieldTypeCommandHandlerTests.cs b/backend/tests/Logitar.Cms.Core.IntegrationTests/FieldTypes/Commands/ReplaceFieldTypeCommandHandlerTests.cs new file mode 100644 index 0000000..f0243a3 --- /dev/null +++ b/backend/tests/Logitar.Cms.Core.IntegrationTests/FieldTypes/Commands/ReplaceFieldTypeCommandHandlerTests.cs @@ -0,0 +1,98 @@ +using Logitar.Cms.Contracts.FieldTypes; +using Logitar.Cms.Contracts.FieldTypes.Properties; +using Logitar.Cms.Core.Contents; +using Logitar.Cms.Core.ContentTypes; +using Logitar.Cms.Core.FieldTypes.Properties; +using Logitar.Cms.EntityFrameworkCore.Entities; +using Logitar.Identity.Domain.Shared; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Logitar.Cms.Core.FieldTypes.Commands; + +[Trait(Traits.Category, Categories.Integration)] +public class ReplaceFieldTypeCommandHandlerTests : IntegrationTests +{ + private static readonly Guid _fieldId = Guid.Parse("590d72ed-8454-4c2f-9722-9e5c65c622ca"); + + private readonly IContentRepository _contentRepository; + private readonly IContentTypeRepository _contentTypeRepository; + private readonly IFieldTypeRepository _fieldTypeRepository; + + private readonly FieldTypeAggregate _fieldType; + private readonly ContentTypeAggregate _contentType; + private readonly ContentAggregate _content; + + public ReplaceFieldTypeCommandHandlerTests() : base() + { + _contentRepository = ServiceProvider.GetRequiredService(); + _contentTypeRepository = ServiceProvider.GetRequiredService(); + _fieldTypeRepository = ServiceProvider.GetRequiredService(); + + _fieldType = new(new UniqueNameUnit(FieldTypeAggregate.UniqueNameSettings, "BlogTitle"), new ReadOnlyStringProperties()); + + _contentType = new(new IdentifierUnit("BlogArticle"), isInvariant: false); + _contentType.SetFieldDefinition(_fieldId, new FieldDefinitionUnit(_fieldType.Id, IsInvariant: false, IsRequired: true, IsIndexed: true, IsUnique: false, + new IdentifierUnit("Title"), DisplayName: null, Description: null, Placeholder: null)); + + _content = new(_contentType, new ContentLocaleUnit(new UniqueNameUnit(ContentAggregate.UniqueNameSettings, "rendered-lego-acura-models"), new Dictionary + { + [_fieldId] = "Rendered: LEGO Acura Models" + })); + } + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + + await _fieldTypeRepository.SaveAsync(_fieldType); + await _contentTypeRepository.SaveAsync(_contentType); + await _contentRepository.SaveAsync(_content); + } + + [Fact(DisplayName = "It should replace an existing field type.")] + public async Task It_should_replace_an_existing_field_type() + { + long version = _fieldType.Version; + + _fieldType.Description = new DescriptionUnit("This is the field type for blog article titles."); + _fieldType.Update(ActorId); + _fieldType.SetProperties(new ReadOnlyStringProperties(minimumLength: 1, maximumLength: 100, pattern: null), ActorId); + await _fieldTypeRepository.SaveAsync(_fieldType); + + ReplaceFieldTypePayload payload = new("ArticleTitle") + { + DisplayName = " Article Title ", + Description = " ", + StringProperties = new StringProperties + { + MinimumLength = 1, + MaximumLength = 100 + } + }; + ReplaceFieldTypeCommand command = new(_fieldType.Id.ToGuid(), payload, version); + FieldType? fieldType = await Pipeline.ExecuteAsync(command); + Assert.NotNull(fieldType); + + Assert.Equal(_fieldType.Id.ToGuid(), fieldType.Id); + Assert.Equal(_fieldType.Version + 1, fieldType.Version); + Assert.Equal(Contracts.Actors.Actor.System, fieldType.CreatedBy); + Assert.Equal(_fieldType.CreatedOn.AsUniversalTime(), fieldType.CreatedOn); + Assert.Equal(Actor, fieldType.UpdatedBy); + Assert.True(fieldType.CreatedOn < fieldType.UpdatedOn); + + Assert.Equal(payload.UniqueName, fieldType.UniqueName); + Assert.Equal(payload.DisplayName.Trim(), fieldType.DisplayName); + Assert.Equal(_fieldType.Description.Value, fieldType.Description); + Assert.Equal(DataType.String, fieldType.DataType); + Assert.Null(fieldType.BooleanProperties); + Assert.Null(fieldType.DateTimeProperties); + Assert.Null(fieldType.NumberProperties); + Assert.Equal(payload.StringProperties, fieldType.StringProperties); + Assert.Null(fieldType.TextProperties); + + StringFieldIndexEntity? index = await CmsContext.StringFieldIndex.AsNoTracking().SingleOrDefaultAsync(); + Assert.NotNull(index); + Assert.Equal(fieldType.UniqueName, index.FieldTypeName); + } +} diff --git a/backend/tests/Logitar.Cms.Core.IntegrationTests/FieldTypes/Commands/UpdateFieldTypeCommandHandlerTests.cs b/backend/tests/Logitar.Cms.Core.IntegrationTests/FieldTypes/Commands/UpdateFieldTypeCommandHandlerTests.cs new file mode 100644 index 0000000..52db1bb --- /dev/null +++ b/backend/tests/Logitar.Cms.Core.IntegrationTests/FieldTypes/Commands/UpdateFieldTypeCommandHandlerTests.cs @@ -0,0 +1,65 @@ +using Logitar.Cms.Contracts; +using Logitar.Cms.Contracts.FieldTypes; +using Logitar.Cms.Contracts.FieldTypes.Properties; +using Logitar.Cms.Core.FieldTypes.Properties; +using Logitar.Identity.Domain.Shared; +using Microsoft.Extensions.DependencyInjection; + +namespace Logitar.Cms.Core.FieldTypes.Commands; + +[Trait(Traits.Category, Categories.Integration)] +public class UpdateFieldTypeCommandHandlerTests : IntegrationTests +{ + private readonly IFieldTypeRepository _fieldTypeRepository; + + private readonly FieldTypeAggregate _fieldType; + + public UpdateFieldTypeCommandHandlerTests() : base() + { + _fieldTypeRepository = ServiceProvider.GetRequiredService(); + + _fieldType = new(new UniqueNameUnit(FieldTypeAggregate.UniqueNameSettings, "ArticleTitle"), new ReadOnlyStringProperties()); + } + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + + await _fieldTypeRepository.SaveAsync(_fieldType); + } + + [Fact(DisplayName = "It should update an existing field type.")] + public async Task It_should_update_an_existing_field_type() + { + UpdateFieldTypePayload payload = new() + { + DisplayName = new Change(" Article Title "), + StringProperties = new StringProperties + { + MinimumLength = 1, + MaximumLength = 100 + } + }; + UpdateFieldTypeCommand command = new(_fieldType.Id.ToGuid(), payload); + FieldType? fieldType = await Pipeline.ExecuteAsync(command); + Assert.NotNull(fieldType); + + Assert.Equal(_fieldType.Id.ToGuid(), fieldType.Id); + Assert.Equal(4, fieldType.Version); + Assert.Equal(Contracts.Actors.Actor.System, fieldType.CreatedBy); + Assert.Equal(_fieldType.CreatedOn.AsUniversalTime(), fieldType.CreatedOn); + Assert.Equal(Actor, fieldType.UpdatedBy); + Assert.True(fieldType.CreatedOn < fieldType.UpdatedOn); + + Assert.Equal(_fieldType.UniqueName.Value, fieldType.UniqueName); + Assert.NotNull(payload.DisplayName.Value); + Assert.Equal(payload.DisplayName.Value.Trim(), fieldType.DisplayName); + Assert.Null(fieldType.Description); + Assert.Equal(DataType.String, fieldType.DataType); + Assert.Null(fieldType.BooleanProperties); + Assert.Null(fieldType.DateTimeProperties); + Assert.Null(fieldType.NumberProperties); + Assert.Equal(payload.StringProperties, fieldType.StringProperties); + Assert.Null(fieldType.TextProperties); + } +} diff --git a/backend/tests/Logitar.Cms.Core.UnitTests/ContentTypes/Commands/CreateContentTypeCommandHandlerTests.cs b/backend/tests/Logitar.Cms.Core.UnitTests/ContentTypes/Commands/CreateContentTypeCommandHandlerTests.cs index bf23745..08847fe 100644 --- a/backend/tests/Logitar.Cms.Core.UnitTests/ContentTypes/Commands/CreateContentTypeCommandHandlerTests.cs +++ b/backend/tests/Logitar.Cms.Core.UnitTests/ContentTypes/Commands/CreateContentTypeCommandHandlerTests.cs @@ -1,5 +1,4 @@ -using FluentValidation; -using FluentValidation.Results; +using FluentValidation.Results; using Logitar.Cms.Contracts.ContentTypes; using MediatR; using Moq; @@ -61,7 +60,7 @@ public async Task It_should_throw_ValidationException_when_the_payload_is_not_va { CreateContentTypePayload payload = new("123_BlogArticle"); CreateContentTypeCommand command = new(payload); - var exception = await Assert.ThrowsAsync(async () => await _handler.Handle(command, _cancellationToken)); + var exception = await Assert.ThrowsAsync(async () => await _handler.Handle(command, _cancellationToken)); ValidationFailure error = Assert.Single(exception.Errors); Assert.Equal("IdentifierValidator", error.ErrorCode); Assert.Equal("UniqueName", error.PropertyName); diff --git a/backend/tests/Logitar.Cms.Core.UnitTests/Contents/Commands/CreateContentTypeCommandHandlerTests.cs b/backend/tests/Logitar.Cms.Core.UnitTests/Contents/Commands/CreateContentTypeCommandHandlerTests.cs index 54c9360..458110a 100644 --- a/backend/tests/Logitar.Cms.Core.UnitTests/Contents/Commands/CreateContentTypeCommandHandlerTests.cs +++ b/backend/tests/Logitar.Cms.Core.UnitTests/Contents/Commands/CreateContentTypeCommandHandlerTests.cs @@ -1,5 +1,4 @@ -using FluentValidation; -using FluentValidation.Results; +using FluentValidation.Results; using Logitar.Cms.Contracts.Contents; using Logitar.Cms.Core.ContentTypes; using Logitar.Cms.Core.Languages; @@ -116,7 +115,7 @@ public async Task It_should_throw_ValidationException_when_the_payload_is_not_va LanguageId = null }; CreateContentCommand command = new(payload); - var exception = await Assert.ThrowsAsync(async () => await _handler.Handle(command, _cancellationToken)); + var exception = await Assert.ThrowsAsync(async () => await _handler.Handle(command, _cancellationToken)); ValidationFailure error = Assert.Single(exception.Errors); Assert.Equal("NotEmptyValidator", error.ErrorCode); Assert.Equal("LanguageId", error.PropertyName); diff --git a/backend/tests/Logitar.Cms.Core.UnitTests/FieldTypes/Commands/CreateFieldTypeCommandHandlerTests.cs b/backend/tests/Logitar.Cms.Core.UnitTests/FieldTypes/Commands/CreateFieldTypeCommandHandlerTests.cs index 2c4ee20..7b13707 100644 --- a/backend/tests/Logitar.Cms.Core.UnitTests/FieldTypes/Commands/CreateFieldTypeCommandHandlerTests.cs +++ b/backend/tests/Logitar.Cms.Core.UnitTests/FieldTypes/Commands/CreateFieldTypeCommandHandlerTests.cs @@ -1,5 +1,4 @@ -using FluentValidation; -using FluentValidation.Results; +using FluentValidation.Results; using Logitar.Cms.Contracts.FieldTypes; using Logitar.Cms.Contracts.FieldTypes.Properties; using Logitar.Cms.Core.FieldTypes.Properties; @@ -133,7 +132,7 @@ public async Task It_should_throw_ValidationException_when_the_payload_is_not_va { CreateFieldTypePayload payload = new("ArticleTitle"); CreateFieldTypeCommand command = new(payload); - var exception = await Assert.ThrowsAsync(async () => await _handler.Handle(command, _cancellationToken)); + var exception = await Assert.ThrowsAsync(async () => await _handler.Handle(command, _cancellationToken)); ValidationFailure error = Assert.Single(exception.Errors); Assert.Equal("CreateFieldTypeValidator", error.ErrorCode); } diff --git a/backend/tests/Logitar.Cms.Core.UnitTests/FieldTypes/Commands/ReplaceFieldTypeCommandHandlerTests.cs b/backend/tests/Logitar.Cms.Core.UnitTests/FieldTypes/Commands/ReplaceFieldTypeCommandHandlerTests.cs new file mode 100644 index 0000000..df9597e --- /dev/null +++ b/backend/tests/Logitar.Cms.Core.UnitTests/FieldTypes/Commands/ReplaceFieldTypeCommandHandlerTests.cs @@ -0,0 +1,177 @@ +using FluentValidation.Results; +using Logitar.Cms.Contracts.FieldTypes; +using Logitar.Cms.Contracts.FieldTypes.Properties; +using Logitar.Cms.Core.FieldTypes.Properties; +using Logitar.Identity.Domain.Shared; +using MediatR; +using Moq; + +namespace Logitar.Cms.Core.FieldTypes.Commands; + +[Trait(Traits.Category, Categories.Unit)] +public class ReplaceFieldTypeCommandHandlerTests +{ + private readonly CancellationToken _cancellationToken = default; + + private readonly Mock _fieldTypeQuerier = new(); + private readonly Mock _fieldTypeRepository = new(); + private readonly Mock _sender = new(); + + private readonly ReplaceFieldTypeCommandHandler _handler; + + public ReplaceFieldTypeCommandHandlerTests() + { + _handler = new(_fieldTypeQuerier.Object, _fieldTypeRepository.Object, _sender.Object); + } + + [Fact(DisplayName = "It should replace an existing Boolean field type.")] + public async Task It_should_replace_an_existing_Boolean_field_type() + { + FieldTypeAggregate fieldType = new(new UniqueNameUnit(FieldTypeAggregate.UniqueNameSettings, "Featured"), new ReadOnlyBooleanProperties()); + _fieldTypeRepository.Setup(x => x.LoadAsync(fieldType.Id, _cancellationToken)).ReturnsAsync(fieldType); + + ReplaceFieldTypePayload payload = new("IsFeatured") + { + DisplayName = " Is featured? ", + Description = " ", + BooleanProperties = new BooleanProperties() + }; + ReplaceFieldTypeCommand command = new(fieldType.Id.ToGuid(), payload, Version: null); + ActivityHelper.Contextualize(command); + await _handler.Handle(command, _cancellationToken); + + _sender.Verify(x => x.Send(It.Is(y => y.FieldType.Id == fieldType.Id + && y.FieldType.UniqueName.Value == payload.UniqueName + && y.FieldType.DisplayName != null && y.FieldType.DisplayName.Value == payload.DisplayName.Trim() + && y.FieldType.Description == null + && y.FieldType.DataType == DataType.Boolean + && y.FieldType.Properties is ReadOnlyBooleanProperties + ), _cancellationToken), Times.Once()); + } + + [Fact(DisplayName = "It should replace an existing DateTime field type.")] + public async Task It_should_replace_an_existing_DateTime_field_type() + { + FieldTypeAggregate fieldType = new(new UniqueNameUnit(FieldTypeAggregate.UniqueNameSettings, "Published"), new ReadOnlyDateTimeProperties()); + _fieldTypeRepository.Setup(x => x.LoadAsync(fieldType.Id, _cancellationToken)).ReturnsAsync(fieldType); + + ReplaceFieldTypePayload payload = new("PublishedOn") + { + DisplayName = " Published on ", + Description = " ", + DateTimeProperties = new DateTimeProperties(minimumValue: DateTime.UtcNow, maximumValue: null) + }; + ReplaceFieldTypeCommand command = new(fieldType.Id.ToGuid(), payload, Version: null); + ActivityHelper.Contextualize(command); + await _handler.Handle(command, _cancellationToken); + + _sender.Verify(x => x.Send(It.Is(y => y.FieldType.Id == fieldType.Id + && y.FieldType.UniqueName.Value == payload.UniqueName + && y.FieldType.DisplayName != null && y.FieldType.DisplayName.Value == payload.DisplayName.Trim() + && y.FieldType.Description == null + && y.FieldType.DataType == DataType.DateTime + && y.FieldType.Properties is ReadOnlyDateTimeProperties + ), _cancellationToken), Times.Once()); + } + + [Fact(DisplayName = "It should replace an existing Number field type.")] + public async Task It_should_replace_an_existing_Number_field_type() + { + FieldTypeAggregate fieldType = new(new UniqueNameUnit(FieldTypeAggregate.UniqueNameSettings, "WordCount"), new ReadOnlyNumberProperties()); + _fieldTypeRepository.Setup(x => x.LoadAsync(fieldType.Id, _cancellationToken)).ReturnsAsync(fieldType); + + ReplaceFieldTypePayload payload = new("Price") + { + DisplayName = " Price ", + Description = " ", + NumberProperties = new NumberProperties(minimumValue: 0.01, maximumValue: null, step: 0.01) + }; + ReplaceFieldTypeCommand command = new(fieldType.Id.ToGuid(), payload, Version: null); + ActivityHelper.Contextualize(command); + await _handler.Handle(command, _cancellationToken); + + _sender.Verify(x => x.Send(It.Is(y => y.FieldType.Id == fieldType.Id + && y.FieldType.UniqueName.Value == payload.UniqueName + && y.FieldType.DisplayName != null && y.FieldType.DisplayName.Value == payload.DisplayName.Trim() + && y.FieldType.Description == null + && y.FieldType.DataType == DataType.Number + && y.FieldType.Properties is ReadOnlyNumberProperties + ), _cancellationToken), Times.Once()); + } + + [Fact(DisplayName = "It should replace an existing String field type.")] + public async Task It_should_replace_an_existing_String_field_type() + { + FieldTypeAggregate fieldType = new(new UniqueNameUnit(FieldTypeAggregate.UniqueNameSettings, "BlogTitle"), new ReadOnlyStringProperties()); + _fieldTypeRepository.Setup(x => x.LoadAsync(fieldType.Id, _cancellationToken)).ReturnsAsync(fieldType); + + ReplaceFieldTypePayload payload = new("ArticleTitle") + { + DisplayName = " Article Title ", + Description = " ", + StringProperties = new StringProperties(minimumLength: 1, maximumLength: 100, pattern: null) + }; + ReplaceFieldTypeCommand command = new(fieldType.Id.ToGuid(), payload, Version: null); + ActivityHelper.Contextualize(command); + await _handler.Handle(command, _cancellationToken); + + _sender.Verify(x => x.Send(It.Is(y => y.FieldType.Id == fieldType.Id + && y.FieldType.UniqueName.Value == payload.UniqueName + && y.FieldType.DisplayName != null && y.FieldType.DisplayName.Value == payload.DisplayName.Trim() + && y.FieldType.Description == null + && y.FieldType.DataType == DataType.String + && y.FieldType.Properties is ReadOnlyStringProperties + ), _cancellationToken), Times.Once()); + } + + [Fact(DisplayName = "It should replace an existing Text field type.")] + public async Task It_should_replace_an_existing_Text_field_type() + { + FieldTypeAggregate fieldType = new(new UniqueNameUnit(FieldTypeAggregate.UniqueNameSettings, "BlogContent"), new ReadOnlyTextProperties()); + _fieldTypeRepository.Setup(x => x.LoadAsync(fieldType.Id, _cancellationToken)).ReturnsAsync(fieldType); + + ReplaceFieldTypePayload payload = new("ArticleContent") + { + DisplayName = " Article Content ", + Description = " ", + TextProperties = new TextProperties(TextProperties.ContentTypes.PlainText, minimumLength: 1, maximumLength: 10000) + }; + ReplaceFieldTypeCommand command = new(fieldType.Id.ToGuid(), payload, Version: null); + ActivityHelper.Contextualize(command); + await _handler.Handle(command, _cancellationToken); + + _sender.Verify(x => x.Send(It.Is(y => y.FieldType.Id == fieldType.Id + && y.FieldType.UniqueName.Value == payload.UniqueName + && y.FieldType.DisplayName != null && y.FieldType.DisplayName.Value == payload.DisplayName.Trim() + && y.FieldType.Description == null + && y.FieldType.DataType == DataType.Text + && y.FieldType.Properties is ReadOnlyTextProperties + ), _cancellationToken), Times.Once()); + } + + [Fact(DisplayName = "It should return null when the field type is not found.")] + public async Task It_should_return_null_when_the_field_type_is_not_found() + { + ReplaceFieldTypePayload payload = new(); + ReplaceFieldTypeCommand command = new(Guid.Empty, payload, Version: null); + Assert.Null(await _handler.Handle(command, _cancellationToken)); + } + + [Fact(DisplayName = "It should throw ValidationException when the payload is not valid.")] + public async Task It_should_throw_ValidationException_when_the_payload_is_not_valid() + { + FieldTypeAggregate fieldType = new(new UniqueNameUnit(FieldTypeAggregate.UniqueNameSettings, "ArticleTitle"), new ReadOnlyStringProperties()); + _fieldTypeRepository.Setup(x => x.LoadAsync(fieldType.Id, _cancellationToken)).ReturnsAsync(fieldType); + + ReplaceFieldTypePayload payload = new("ArticleTitle") + { + StringProperties = new StringProperties(), + TextProperties = new TextProperties() + }; + ReplaceFieldTypeCommand command = new(fieldType.Id.ToGuid(), payload, Version: null); + var exception = await Assert.ThrowsAsync(async () => await _handler.Handle(command, _cancellationToken)); + ValidationFailure error = Assert.Single(exception.Errors); + Assert.Equal("NullValidator", error.ErrorCode); + Assert.Equal("TextProperties", error.PropertyName); + } +} diff --git a/backend/tests/Logitar.Cms.Core.UnitTests/FieldTypes/Commands/UpdateFieldTypeCommandHandlerTests.cs b/backend/tests/Logitar.Cms.Core.UnitTests/FieldTypes/Commands/UpdateFieldTypeCommandHandlerTests.cs new file mode 100644 index 0000000..1d6afa8 --- /dev/null +++ b/backend/tests/Logitar.Cms.Core.UnitTests/FieldTypes/Commands/UpdateFieldTypeCommandHandlerTests.cs @@ -0,0 +1,82 @@ +using FluentValidation.Results; +using Logitar.Cms.Contracts; +using Logitar.Cms.Contracts.FieldTypes; +using Logitar.Cms.Contracts.FieldTypes.Properties; +using Logitar.Cms.Core.FieldTypes.Properties; +using Logitar.Identity.Domain.Shared; +using MediatR; +using Moq; + +namespace Logitar.Cms.Core.FieldTypes.Commands; + +[Trait(Traits.Category, Categories.Unit)] +public class UpdateFieldTypeCommandHandlerTests +{ + private readonly CancellationToken _cancellationToken = default; + + private readonly Mock _fieldTypeQuerier = new(); + private readonly Mock _fieldTypeRepository = new(); + private readonly Mock _sender = new(); + + private readonly UpdateFieldTypeCommandHandler _handler; + + public UpdateFieldTypeCommandHandlerTests() + { + _handler = new(_fieldTypeQuerier.Object, _fieldTypeRepository.Object, _sender.Object); + } + + [Fact(DisplayName = "It should return null when the field type is not found.")] + public async Task It_should_return_null_when_the_field_type_is_not_found() + { + UpdateFieldTypePayload payload = new(); + UpdateFieldTypeCommand command = new(Guid.Empty, payload); + Assert.Null(await _handler.Handle(command, _cancellationToken)); + } + + [Fact(DisplayName = "It should throw ValidationException when the payload is not valid.")] + public async Task It_should_throw_ValidationException_when_the_payload_is_not_valid() + { + FieldTypeAggregate fieldType = new(new UniqueNameUnit(FieldTypeAggregate.UniqueNameSettings, "ArticleTitle"), new ReadOnlyStringProperties()); + _fieldTypeRepository.Setup(x => x.LoadAsync(fieldType.Id, _cancellationToken)).ReturnsAsync(fieldType); + + UpdateFieldTypePayload payload = new() + { + StringProperties = new StringProperties(), + TextProperties = new TextProperties() + }; + UpdateFieldTypeCommand command = new(fieldType.Id.ToGuid(), payload); + var exception = await Assert.ThrowsAsync(async () => await _handler.Handle(command, _cancellationToken)); + ValidationFailure error = Assert.Single(exception.Errors); + Assert.Equal("NullValidator", error.ErrorCode); + Assert.Equal("TextProperties", error.PropertyName); + } + + [Fact(DisplayName = "It should update an existing field type without version.")] + public async Task It_should_update_an_existing_field_type_without_version() + { + FieldTypeAggregate fieldType = new(new UniqueNameUnit(FieldTypeAggregate.UniqueNameSettings, "ArticleTitle"), new ReadOnlyStringProperties()); + _fieldTypeRepository.Setup(x => x.LoadAsync(fieldType.Id, _cancellationToken)).ReturnsAsync(fieldType); + + UpdateFieldTypePayload payload = new() + { + DisplayName = new Change(" Article Title "), + Description = new Change(" "), + StringProperties = new StringProperties(minimumLength: 1, maximumLength: 100, pattern: null) + }; + UpdateFieldTypeCommand command = new(fieldType.Id.ToGuid(), payload); + ActivityHelper.Contextualize(command); + await _handler.Handle(command, _cancellationToken); + + _sender.Verify(x => x.Send(It.Is(y => y.FieldType.Id == fieldType.Id + && y.FieldType.UniqueName == fieldType.UniqueName + && y.FieldType.DisplayName != null && payload.DisplayName.Value != null && y.FieldType.DisplayName.Value == payload.DisplayName.Value.Trim() + && y.FieldType.Description == null + && y.FieldType.DataType == DataType.String + && ((ReadOnlyStringProperties)y.FieldType.Properties).MinimumLength == payload.StringProperties.MinimumLength + && ((ReadOnlyStringProperties)y.FieldType.Properties).MaximumLength == payload.StringProperties.MaximumLength + && ((ReadOnlyStringProperties)y.FieldType.Properties).Pattern == payload.StringProperties.Pattern + ), _cancellationToken), Times.Once()); + + _fieldTypeRepository.Verify(x => x.LoadAsync(fieldType.Id, It.IsAny(), _cancellationToken), Times.Never); + } +} diff --git a/backend/tests/Logitar.Cms.Core.UnitTests/Languages/Commands/CreateLanguageCommandHandlerTests.cs b/backend/tests/Logitar.Cms.Core.UnitTests/Languages/Commands/CreateLanguageCommandHandlerTests.cs index 5c0d415..9303ff9 100644 --- a/backend/tests/Logitar.Cms.Core.UnitTests/Languages/Commands/CreateLanguageCommandHandlerTests.cs +++ b/backend/tests/Logitar.Cms.Core.UnitTests/Languages/Commands/CreateLanguageCommandHandlerTests.cs @@ -1,5 +1,4 @@ -using FluentValidation; -using FluentValidation.Results; +using FluentValidation.Results; using Logitar.Cms.Contracts.Languages; using MediatR; using Moq; @@ -40,7 +39,7 @@ public async Task It_should_throw_ValidationException_when_the_payload_is_not_va { CreateLanguagePayload payload = new(locale: "invalid"); CreateLanguageCommand command = new(payload); - var exception = await Assert.ThrowsAsync(async () => await _handler.Handle(command, _cancellationToken)); + var exception = await Assert.ThrowsAsync(async () => await _handler.Handle(command, _cancellationToken)); ValidationFailure error = Assert.Single(exception.Errors); Assert.Equal("LocaleValidator", error.ErrorCode); Assert.Equal("Locale", error.PropertyName);