From ddfa581593dce978eac263f40cae05ae105de80b Mon Sep 17 00:00:00 2001 From: Francis Pion Date: Sat, 20 Jul 2024 13:22:00 -0400 Subject: [PATCH] Implemented field type search. --- .../FieldTypes/FieldTypeSort.cs | 8 ++ .../FieldTypes/FieldTypeSortOption.cs | 20 +++++ .../FieldTypes/SearchFieldTypesPayload.cs | 10 +++ .../FieldTypes/IFieldTypeQuerier.cs | 3 + .../FieldTypes/IFieldTypeRepository.cs | 1 + .../Queries/SearchFieldTypesQuery.cs | 7 ++ .../Queries/SearchFieldTypesQueryHandler.cs | 20 +++++ .../Queriers/FieldTypeQuerier.cs | 59 +++++++++++++- .../Repositories/FieldTypeRepository.cs | 5 ++ .../Controllers/FieldTypeController.cs | 9 +++ .../FieldTypes/SearchFieldTypesParameters.cs | 37 +++++++++ .../SearchFieldTypesQueryHandlerTests.cs | 77 +++++++++++++++++++ 12 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 backend/src/Logitar.Cms.Contracts/FieldTypes/FieldTypeSort.cs create mode 100644 backend/src/Logitar.Cms.Contracts/FieldTypes/FieldTypeSortOption.cs create mode 100644 backend/src/Logitar.Cms.Contracts/FieldTypes/SearchFieldTypesPayload.cs create mode 100644 backend/src/Logitar.Cms.Core/FieldTypes/Queries/SearchFieldTypesQuery.cs create mode 100644 backend/src/Logitar.Cms.Core/FieldTypes/Queries/SearchFieldTypesQueryHandler.cs create mode 100644 backend/src/Logitar.Cms.Web/Models/FieldTypes/SearchFieldTypesParameters.cs create mode 100644 backend/tests/Logitar.Cms.Core.IntegrationTests/FieldTypes/Queries/SearchFieldTypesQueryHandlerTests.cs diff --git a/backend/src/Logitar.Cms.Contracts/FieldTypes/FieldTypeSort.cs b/backend/src/Logitar.Cms.Contracts/FieldTypes/FieldTypeSort.cs new file mode 100644 index 0000000..9a19586 --- /dev/null +++ b/backend/src/Logitar.Cms.Contracts/FieldTypes/FieldTypeSort.cs @@ -0,0 +1,8 @@ +namespace Logitar.Cms.Contracts.FieldTypes; + +public enum FieldTypeSort +{ + DisplayName, + UniqueName, + UpdatedOn +} diff --git a/backend/src/Logitar.Cms.Contracts/FieldTypes/FieldTypeSortOption.cs b/backend/src/Logitar.Cms.Contracts/FieldTypes/FieldTypeSortOption.cs new file mode 100644 index 0000000..f5b288c --- /dev/null +++ b/backend/src/Logitar.Cms.Contracts/FieldTypes/FieldTypeSortOption.cs @@ -0,0 +1,20 @@ +using Logitar.Cms.Contracts.Search; + +namespace Logitar.Cms.Contracts.FieldTypes; + +public record FieldTypeSortOption : SortOption +{ + public new FieldTypeSort Field + { + get => Enum.Parse(base.Field); + set => base.Field = value.ToString(); + } + + public FieldTypeSortOption() : this(FieldTypeSort.UpdatedOn, isDescending: true) + { + } + + public FieldTypeSortOption(FieldTypeSort field, bool isDescending = false) : base(field.ToString(), isDescending) + { + } +} diff --git a/backend/src/Logitar.Cms.Contracts/FieldTypes/SearchFieldTypesPayload.cs b/backend/src/Logitar.Cms.Contracts/FieldTypes/SearchFieldTypesPayload.cs new file mode 100644 index 0000000..e4ea02f --- /dev/null +++ b/backend/src/Logitar.Cms.Contracts/FieldTypes/SearchFieldTypesPayload.cs @@ -0,0 +1,10 @@ +using Logitar.Cms.Contracts.Search; + +namespace Logitar.Cms.Contracts.FieldTypes; + +public record SearchFieldTypesPayload : SearchPayload +{ + public DataType? DataType { get; set; } + + public new List? Sort { get; set; } +} diff --git a/backend/src/Logitar.Cms.Core/FieldTypes/IFieldTypeQuerier.cs b/backend/src/Logitar.Cms.Core/FieldTypes/IFieldTypeQuerier.cs index 1b911f1..f0672d6 100644 --- a/backend/src/Logitar.Cms.Core/FieldTypes/IFieldTypeQuerier.cs +++ b/backend/src/Logitar.Cms.Core/FieldTypes/IFieldTypeQuerier.cs @@ -1,4 +1,5 @@ using Logitar.Cms.Contracts.FieldTypes; +using Logitar.Cms.Contracts.Search; namespace Logitar.Cms.Core.FieldTypes; @@ -8,4 +9,6 @@ public interface IFieldTypeQuerier Task ReadAsync(FieldTypeId id, CancellationToken cancellationToken = default); Task ReadAsync(Guid id, CancellationToken cancellationToken = default); Task ReadAsync(string uniqueName, CancellationToken cancellationToken = default); + + Task> SearchAsync(SearchFieldTypesPayload payload, CancellationToken cancellationToken = default); } diff --git a/backend/src/Logitar.Cms.Core/FieldTypes/IFieldTypeRepository.cs b/backend/src/Logitar.Cms.Core/FieldTypes/IFieldTypeRepository.cs index 6e42afd..25d1894 100644 --- a/backend/src/Logitar.Cms.Core/FieldTypes/IFieldTypeRepository.cs +++ b/backend/src/Logitar.Cms.Core/FieldTypes/IFieldTypeRepository.cs @@ -4,6 +4,7 @@ namespace Logitar.Cms.Core.FieldTypes; public interface IFieldTypeRepository { + Task> LoadAsync(CancellationToken cancellationToken = default); Task LoadAsync(FieldTypeId id, 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/Queries/SearchFieldTypesQuery.cs b/backend/src/Logitar.Cms.Core/FieldTypes/Queries/SearchFieldTypesQuery.cs new file mode 100644 index 0000000..1dc22b3 --- /dev/null +++ b/backend/src/Logitar.Cms.Core/FieldTypes/Queries/SearchFieldTypesQuery.cs @@ -0,0 +1,7 @@ +using Logitar.Cms.Contracts.FieldTypes; +using Logitar.Cms.Contracts.Search; +using MediatR; + +namespace Logitar.Cms.Core.FieldTypes.Queries; + +public record SearchFieldTypesQuery(SearchFieldTypesPayload Payload) : IRequest>; diff --git a/backend/src/Logitar.Cms.Core/FieldTypes/Queries/SearchFieldTypesQueryHandler.cs b/backend/src/Logitar.Cms.Core/FieldTypes/Queries/SearchFieldTypesQueryHandler.cs new file mode 100644 index 0000000..49940a4 --- /dev/null +++ b/backend/src/Logitar.Cms.Core/FieldTypes/Queries/SearchFieldTypesQueryHandler.cs @@ -0,0 +1,20 @@ +using Logitar.Cms.Contracts.FieldTypes; +using Logitar.Cms.Contracts.Search; +using MediatR; + +namespace Logitar.Cms.Core.FieldTypes.Queries; + +internal class SearchFieldTypesQueryHandler : IRequestHandler> +{ + private readonly IFieldTypeQuerier _fieldTypeQuerier; + + public SearchFieldTypesQueryHandler(IFieldTypeQuerier fieldTypeQuerier) + { + _fieldTypeQuerier = fieldTypeQuerier; + } + + public async Task> Handle(SearchFieldTypesQuery query, CancellationToken cancellationToken) + { + return await _fieldTypeQuerier.SearchAsync(query.Payload, cancellationToken); + } +} diff --git a/backend/src/Logitar.Cms.EntityFrameworkCore/Queriers/FieldTypeQuerier.cs b/backend/src/Logitar.Cms.EntityFrameworkCore/Queriers/FieldTypeQuerier.cs index 8588a72..1966f7f 100644 --- a/backend/src/Logitar.Cms.EntityFrameworkCore/Queriers/FieldTypeQuerier.cs +++ b/backend/src/Logitar.Cms.EntityFrameworkCore/Queriers/FieldTypeQuerier.cs @@ -1,9 +1,12 @@ using Logitar.Cms.Contracts.Actors; using Logitar.Cms.Contracts.FieldTypes; +using Logitar.Cms.Contracts.Search; using Logitar.Cms.Core.FieldTypes; using Logitar.Cms.EntityFrameworkCore.Actors; using Logitar.Cms.EntityFrameworkCore.Entities; +using Logitar.Data; using Logitar.EventSourcing; +using Logitar.Identity.EntityFrameworkCore.Relational; using Microsoft.EntityFrameworkCore; namespace Logitar.Cms.EntityFrameworkCore.Queriers; @@ -12,11 +15,15 @@ internal class FieldTypeQuerier : IFieldTypeQuerier { private readonly IActorService _actorService; private readonly DbSet _fieldTypes; + private readonly ISearchHelper _searchHelper; + private readonly ISqlHelper _sqlHelper; - public FieldTypeQuerier(IActorService actorService, CmsContext context) + public FieldTypeQuerier(IActorService actorService, CmsContext context, ISearchHelper searchHelper, ISqlHelper sqlHelper) { _actorService = actorService; _fieldTypes = context.FieldTypes; + _searchHelper = searchHelper; + _sqlHelper = sqlHelper; } public async Task ReadAsync(FieldTypeAggregate fieldType, CancellationToken cancellationToken) @@ -46,6 +53,56 @@ public async Task ReadAsync(FieldTypeAggregate fieldType, Cancellatio return fieldType == null ? null : await MapAsync(fieldType, cancellationToken); } + public async Task> SearchAsync(SearchFieldTypesPayload payload, CancellationToken cancellationToken) + { + IQueryBuilder builder = _sqlHelper.QueryFrom(CmsDb.FieldTypes.Table).SelectAll(CmsDb.FieldTypes.Table) + .ApplyIdInFilter(CmsDb.FieldTypes.UniqueId, payload); + _searchHelper.ApplyTextSearch(builder, payload.Search, CmsDb.FieldTypes.UniqueName, CmsDb.FieldTypes.DisplayName); + + if (payload.DataType.HasValue) + { + builder.Where(CmsDb.FieldTypes.DataType, Operators.IsEqualTo(payload.DataType.Value.ToString())); + } + + IQueryable query = _fieldTypes.FromQuery(builder).AsNoTracking(); + + long total = await query.LongCountAsync(cancellationToken); + + IOrderedQueryable? ordered = null; + if (payload.Sort != null) + { + foreach (FieldTypeSortOption sort in payload.Sort) + { + switch (sort.Field) + { + case FieldTypeSort.DisplayName: + ordered = (ordered == null) + ? (sort.IsDescending ? query.OrderByDescending(x => x.DisplayName) : query.OrderBy(x => x.DisplayName)) + : (sort.IsDescending ? ordered.ThenByDescending(x => x.DisplayName) : ordered.ThenBy(x => x.DisplayName)); + break; + case FieldTypeSort.UniqueName: + ordered = (ordered == null) + ? (sort.IsDescending ? query.OrderByDescending(x => x.UniqueName) : query.OrderBy(x => x.UniqueName)) + : (sort.IsDescending ? ordered.ThenByDescending(x => x.UniqueName) : ordered.ThenBy(x => x.UniqueName)); + break; + case FieldTypeSort.UpdatedOn: + ordered = (ordered == null) + ? (sort.IsDescending ? query.OrderByDescending(x => x.UpdatedOn) : query.OrderBy(x => x.UpdatedOn)) + : (sort.IsDescending ? ordered.ThenByDescending(x => x.UpdatedOn) : ordered.ThenBy(x => x.UpdatedOn)); + break; + } + } + } + query = ordered ?? query; + + query = query.ApplyPaging(payload); + + FieldTypeEntity[] fieldTypes = await query.ToArrayAsync(cancellationToken); + IEnumerable items = await MapAsync(fieldTypes, cancellationToken); + + return new SearchResults(items, total); + } + private async Task MapAsync(FieldTypeEntity fieldType, CancellationToken cancellationToken) => (await MapAsync([fieldType], cancellationToken)).Single(); private async Task> MapAsync(IEnumerable fieldTypes, CancellationToken cancellationToken) diff --git a/backend/src/Logitar.Cms.EntityFrameworkCore/Repositories/FieldTypeRepository.cs b/backend/src/Logitar.Cms.EntityFrameworkCore/Repositories/FieldTypeRepository.cs index a66de7e..f173371 100644 --- a/backend/src/Logitar.Cms.EntityFrameworkCore/Repositories/FieldTypeRepository.cs +++ b/backend/src/Logitar.Cms.EntityFrameworkCore/Repositories/FieldTypeRepository.cs @@ -21,6 +21,11 @@ public FieldTypeRepository(IEventBus eventBus, EventContext eventContext, IEvent _sqlHelper = sqlHelper; } + public async Task> LoadAsync(CancellationToken cancellationToken) + { + return (await base.LoadAsync(cancellationToken)).ToArray(); + } + public async Task LoadAsync(FieldTypeId id, CancellationToken cancellationToken) { return await base.LoadAsync(id.AggregateId, cancellationToken); diff --git a/backend/src/Logitar.Cms.Web/Controllers/FieldTypeController.cs b/backend/src/Logitar.Cms.Web/Controllers/FieldTypeController.cs index e989e43..074ce37 100644 --- a/backend/src/Logitar.Cms.Web/Controllers/FieldTypeController.cs +++ b/backend/src/Logitar.Cms.Web/Controllers/FieldTypeController.cs @@ -1,7 +1,9 @@ using Logitar.Cms.Contracts.FieldTypes; +using Logitar.Cms.Contracts.Search; using Logitar.Cms.Core; using Logitar.Cms.Core.FieldTypes.Commands; using Logitar.Cms.Core.FieldTypes.Queries; +using Logitar.Cms.Web.Models.FieldTypes; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -41,4 +43,11 @@ public async Task> ReadAsync(string uniqueName, Cancella FieldType? fieldType = await _pipeline.ExecuteAsync(new ReadFieldTypeQuery(Id: null, uniqueName), 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); + } } diff --git a/backend/src/Logitar.Cms.Web/Models/FieldTypes/SearchFieldTypesParameters.cs b/backend/src/Logitar.Cms.Web/Models/FieldTypes/SearchFieldTypesParameters.cs new file mode 100644 index 0000000..3af91fb --- /dev/null +++ b/backend/src/Logitar.Cms.Web/Models/FieldTypes/SearchFieldTypesParameters.cs @@ -0,0 +1,37 @@ +using Logitar.Cms.Contracts.FieldTypes; +using Logitar.Cms.Contracts.Search; +using Logitar.Cms.Web.Models.Search; +using Microsoft.AspNetCore.Mvc; + +namespace Logitar.Cms.Web.Models.FieldTypes; + +public record SearchFieldTypesParameters : SearchParameters +{ + [FromQuery(Name = "type")] + public DataType? DataType { get; set; } + + public SearchFieldTypesPayload ToPayload() + { + SearchFieldTypesPayload payload = new() + { + DataType = DataType + }; + + FillPayload(payload); + + List? sortOptions = ((SearchPayload)payload).Sort; + if (sortOptions != null) + { + payload.Sort = new List(capacity: sortOptions.Count); + foreach (SortOption sort in sortOptions) + { + if (Enum.TryParse(sort.Field, out FieldTypeSort field)) + { + payload.Sort.Add(new FieldTypeSortOption(field, sort.IsDescending)); + } + } + } + + return payload; + } +} diff --git a/backend/tests/Logitar.Cms.Core.IntegrationTests/FieldTypes/Queries/SearchFieldTypesQueryHandlerTests.cs b/backend/tests/Logitar.Cms.Core.IntegrationTests/FieldTypes/Queries/SearchFieldTypesQueryHandlerTests.cs new file mode 100644 index 0000000..28408b2 --- /dev/null +++ b/backend/tests/Logitar.Cms.Core.IntegrationTests/FieldTypes/Queries/SearchFieldTypesQueryHandlerTests.cs @@ -0,0 +1,77 @@ +using Logitar.Cms.Contracts.FieldTypes; +using Logitar.Cms.Contracts.Search; +using Logitar.Cms.Core.FieldTypes.Properties; +using Logitar.Identity.Contracts.Settings; +using Logitar.Identity.Domain.Shared; +using Microsoft.Extensions.DependencyInjection; + +namespace Logitar.Cms.Core.FieldTypes.Queries; + +[Trait(Traits.Category, Categories.Integration)] +public class SearchFieldTypesQueryHandlerTests : IntegrationTests +{ + private readonly IFieldTypeRepository _fieldTypeRepository; + + private readonly FieldTypeAggregate _contents; + private readonly FieldTypeAggregate _metaTitle; + private readonly FieldTypeAggregate _slug; + private readonly FieldTypeAggregate _subTitle; + private readonly FieldTypeAggregate _title; + + public SearchFieldTypesQueryHandlerTests() : base() + { + _fieldTypeRepository = ServiceProvider.GetRequiredService(); + + IUniqueNameSettings uniqueNameSettings = FieldTypeAggregate.UniqueNameSettings; + _contents = new(new UniqueNameUnit(uniqueNameSettings, "ArticleContents"), new ReadOnlyTextProperties()); + _metaTitle = new(new UniqueNameUnit(uniqueNameSettings, "ArticleMetaTitle"), new ReadOnlyStringProperties()); + _slug = new(new UniqueNameUnit(uniqueNameSettings, "Slug"), new ReadOnlyStringProperties()); + _subTitle = new(new UniqueNameUnit(uniqueNameSettings, "ArticleSubTitle"), new ReadOnlyStringProperties()); + _title = new(new UniqueNameUnit(uniqueNameSettings, "ArticleTitle"), new ReadOnlyStringProperties()); + } + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + + await _fieldTypeRepository.SaveAsync([_contents, _metaTitle, _slug, _subTitle, _title]); + } + + [Fact(DisplayName = "It should return empty results when no field type matches.")] + public async Task It_should_return_empty_results_when_no_field_type_matches() + { + SearchFieldTypesPayload payload = new() + { + Search = new TextSearch([new SearchTerm("%test%")]) + }; + SearchFieldTypesQuery query = new(payload); + + SearchResults results = await Pipeline.ExecuteAsync(query); + + Assert.Empty(results.Items); + Assert.Equal(0, results.Total); + } + + [Fact(DisplayName = "It should return the correct matching field types.")] + public async Task It_should_return_the_correct_matching_field_types() + { + SearchFieldTypesPayload payload = new() + { + DataType = DataType.String, + IdIn = (await _fieldTypeRepository.LoadAsync()).Select(fieldType => fieldType.Id.ToGuid()).ToList(), + Search = new TextSearch([new SearchTerm("%title"), new SearchTerm("con%")], SearchOperator.Or), + Sort = [new FieldTypeSortOption(FieldTypeSort.UniqueName, isDescending: false)], + Skip = 1, + Limit = 1 + }; + payload.IdIn.Add(Guid.Empty); + payload.IdIn.Remove(_metaTitle.Id.ToGuid()); + SearchFieldTypesQuery query = new(payload); + + SearchResults results = await Pipeline.ExecuteAsync(query); + + Assert.Equal(2, results.Total); + FieldType fieldType = Assert.Single(results.Items); + Assert.Equal(_title.Id.ToGuid(), fieldType.Id); + } +}