Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented field type search. #30

Merged
merged 1 commit into from
Jul 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions backend/src/Logitar.Cms.Contracts/FieldTypes/FieldTypeSort.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Logitar.Cms.Contracts.FieldTypes;

public enum FieldTypeSort
{
DisplayName,
UniqueName,
UpdatedOn
}
Original file line number Diff line number Diff line change
@@ -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<FieldTypeSort>(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)
{
}
}
Original file line number Diff line number Diff line change
@@ -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<FieldTypeSortOption>? Sort { get; set; }
}
3 changes: 3 additions & 0 deletions backend/src/Logitar.Cms.Core/FieldTypes/IFieldTypeQuerier.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Logitar.Cms.Contracts.FieldTypes;
using Logitar.Cms.Contracts.Search;

namespace Logitar.Cms.Core.FieldTypes;

Expand All @@ -8,4 +9,6 @@ public interface IFieldTypeQuerier
Task<FieldType?> ReadAsync(FieldTypeId id, CancellationToken cancellationToken = default);
Task<FieldType?> ReadAsync(Guid id, CancellationToken cancellationToken = default);
Task<FieldType?> ReadAsync(string uniqueName, CancellationToken cancellationToken = default);

Task<SearchResults<FieldType>> SearchAsync(SearchFieldTypesPayload payload, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace Logitar.Cms.Core.FieldTypes;

public interface IFieldTypeRepository
{
Task<IReadOnlyCollection<FieldTypeAggregate>> LoadAsync(CancellationToken cancellationToken = default);
Task<FieldTypeAggregate?> LoadAsync(FieldTypeId id, CancellationToken cancellationToken = default);
Task<IReadOnlyCollection<FieldTypeAggregate>> LoadAsync(IEnumerable<FieldTypeId> ids, CancellationToken cancellationToken = default);
Task<FieldTypeAggregate?> LoadAsync(UniqueNameUnit uniqueName, CancellationToken cancellationToken = default);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SearchResults<FieldType>>;
Original file line number Diff line number Diff line change
@@ -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<SearchFieldTypesQuery, SearchResults<FieldType>>
{
private readonly IFieldTypeQuerier _fieldTypeQuerier;

public SearchFieldTypesQueryHandler(IFieldTypeQuerier fieldTypeQuerier)
{
_fieldTypeQuerier = fieldTypeQuerier;
}

public async Task<SearchResults<FieldType>> Handle(SearchFieldTypesQuery query, CancellationToken cancellationToken)
{
return await _fieldTypeQuerier.SearchAsync(query.Payload, cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -12,11 +15,15 @@ internal class FieldTypeQuerier : IFieldTypeQuerier
{
private readonly IActorService _actorService;
private readonly DbSet<FieldTypeEntity> _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<FieldType> ReadAsync(FieldTypeAggregate fieldType, CancellationToken cancellationToken)
Expand Down Expand Up @@ -46,6 +53,56 @@ public async Task<FieldType> ReadAsync(FieldTypeAggregate fieldType, Cancellatio
return fieldType == null ? null : await MapAsync(fieldType, cancellationToken);
}

public async Task<SearchResults<FieldType>> 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<FieldTypeEntity> query = _fieldTypes.FromQuery(builder).AsNoTracking();

long total = await query.LongCountAsync(cancellationToken);

IOrderedQueryable<FieldTypeEntity>? 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<FieldType> items = await MapAsync(fieldTypes, cancellationToken);

return new SearchResults<FieldType>(items, total);
}

private async Task<FieldType> MapAsync(FieldTypeEntity fieldType, CancellationToken cancellationToken)
=> (await MapAsync([fieldType], cancellationToken)).Single();
private async Task<IReadOnlyCollection<FieldType>> MapAsync(IEnumerable<FieldTypeEntity> fieldTypes, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ public FieldTypeRepository(IEventBus eventBus, EventContext eventContext, IEvent
_sqlHelper = sqlHelper;
}

public async Task<IReadOnlyCollection<FieldTypeAggregate>> LoadAsync(CancellationToken cancellationToken)
{
return (await base.LoadAsync<FieldTypeAggregate>(cancellationToken)).ToArray();
}

public async Task<FieldTypeAggregate?> LoadAsync(FieldTypeId id, CancellationToken cancellationToken)
{
return await base.LoadAsync<FieldTypeAggregate>(id.AggregateId, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -41,4 +43,11 @@ public async Task<ActionResult<FieldType>> 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<ActionResult<SearchResults<FieldType>>> SearchAsync([FromQuery] SearchFieldTypesParameters parameters, CancellationToken cancellationToken)
{
SearchResults<FieldType> fieldTypes = await _pipeline.ExecuteAsync(new SearchFieldTypesQuery(parameters.ToPayload()), cancellationToken);
return Ok(fieldTypes);
}
}
Original file line number Diff line number Diff line change
@@ -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<SortOption>? sortOptions = ((SearchPayload)payload).Sort;
if (sortOptions != null)
{
payload.Sort = new List<FieldTypeSortOption>(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;
}
}
Original file line number Diff line number Diff line change
@@ -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<IFieldTypeRepository>();

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<FieldType> 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<FieldType> results = await Pipeline.ExecuteAsync(query);

Assert.Equal(2, results.Total);
FieldType fieldType = Assert.Single(results.Items);
Assert.Equal(_title.Id.ToGuid(), fieldType.Id);
}
}