Skip to content

Commit

Permalink
Implemented content type search. (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
Utar94 authored Jul 20, 2024
1 parent 4d0b840 commit fae5d5f
Show file tree
Hide file tree
Showing 13 changed files with 251 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Logitar.Cms.Contracts.ContentTypes;

public enum ContentTypeSort
{
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.ContentTypes;

public record ContentTypeSortOption : SortOption
{
public new ContentTypeSort Field
{
get => Enum.Parse<ContentTypeSort>(base.Field);
set => base.Field = value.ToString();
}

public ContentTypeSortOption() : this(ContentTypeSort.UpdatedOn, isDescending: true)
{
}

public ContentTypeSortOption(ContentTypeSort 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.ContentTypes;

public record SearchContentTypesPayload : SearchPayload
{
public bool? IsInvariant { get; set; }

public new List<ContentTypeSortOption>? Sort { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
using Logitar.Cms.Contracts;
using Logitar.Cms.Core.Configurations;
using Logitar.Cms.Core.ContentTypes.Events;
using Logitar.EventSourcing;
using Logitar.Identity.Contracts.Settings;
using Logitar.Identity.Domain.Shared;

namespace Logitar.Cms.Core.ContentTypes;

public class ContentTypeAggregate : AggregateRoot
{
public static readonly IUniqueNameSettings UniqueNameSettings = new ReadOnlyUniqueNameSettings();

private ContentTypeUpdatedEvent _updatedEvent = new();

public new ContentTypeId Id => new(base.Id);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Logitar.Cms.Contracts.ContentTypes;
using Logitar.Cms.Contracts.Search;

namespace Logitar.Cms.Core.ContentTypes;

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

Task<SearchResults<CmsContentType>> SearchAsync(SearchContentTypesPayload payload, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

public interface IContentTypeRepository
{
Task<IReadOnlyCollection<ContentTypeAggregate>> LoadAsync(CancellationToken cancellationToken = default);
Task<ContentTypeAggregate?> LoadAsync(ContentTypeId id, CancellationToken cancellationToken = default);
Task<ContentTypeAggregate?> LoadAsync(IdentifierUnit uniqueName, CancellationToken cancellationToken = default);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using Logitar.Cms.Contracts.ContentTypes;
using Logitar.Cms.Contracts.Search;
using MediatR;

namespace Logitar.Cms.Core.ContentTypes.Queries;

public record SearchContentTypesQuery(SearchContentTypesPayload Payload) : IRequest<SearchResults<CmsContentType>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Logitar.Cms.Contracts.ContentTypes;
using Logitar.Cms.Contracts.Search;
using MediatR;

namespace Logitar.Cms.Core.ContentTypes.Queries;

internal class SearchContentTypesQueryHandler : IRequestHandler<SearchContentTypesQuery, SearchResults<CmsContentType>>
{
private readonly IContentTypeQuerier _fieldTypeQuerier;

public SearchContentTypesQueryHandler(IContentTypeQuerier fieldTypeQuerier)
{
_fieldTypeQuerier = fieldTypeQuerier;
}

public async Task<SearchResults<CmsContentType>> Handle(SearchContentTypesQuery 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.ContentTypes;
using Logitar.Cms.Contracts.Search;
using Logitar.Cms.Core.ContentTypes;
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 ContentTypeQuerier : IContentTypeQuerier
{
private readonly IActorService _actorService;
private readonly DbSet<ContentTypeEntity> _contentTypes;
private readonly ISearchHelper _searchHelper;
private readonly ISqlHelper _sqlHelper;

public ContentTypeQuerier(IActorService actorService, CmsContext context)
public ContentTypeQuerier(IActorService actorService, CmsContext context, ISearchHelper searchHelper, ISqlHelper sqlHelper)
{
_actorService = actorService;
_contentTypes = context.ContentTypes;
_searchHelper = searchHelper;
_sqlHelper = sqlHelper;
}

public async Task<CmsContentType> ReadAsync(ContentTypeAggregate contentType, CancellationToken cancellationToken)
Expand Down Expand Up @@ -48,6 +55,56 @@ public async Task<CmsContentType> ReadAsync(ContentTypeAggregate contentType, Ca
return contentType == null ? null : await MapAsync(contentType, cancellationToken);
}

public async Task<SearchResults<CmsContentType>> SearchAsync(SearchContentTypesPayload payload, CancellationToken cancellationToken)
{
IQueryBuilder builder = _sqlHelper.QueryFrom(CmsDb.ContentTypes.Table).SelectAll(CmsDb.ContentTypes.Table)
.ApplyIdInFilter(CmsDb.ContentTypes.UniqueId, payload);
_searchHelper.ApplyTextSearch(builder, payload.Search, CmsDb.ContentTypes.UniqueName, CmsDb.ContentTypes.DisplayName);

if (payload.IsInvariant.HasValue)
{
builder.Where(CmsDb.ContentTypes.IsInvariant, Operators.IsEqualTo(payload.IsInvariant.Value));
}

IQueryable<ContentTypeEntity> query = _contentTypes.FromQuery(builder).AsNoTracking();

long total = await query.LongCountAsync(cancellationToken);

IOrderedQueryable<ContentTypeEntity>? ordered = null;
if (payload.Sort != null)
{
foreach (ContentTypeSortOption sort in payload.Sort)
{
switch (sort.Field)
{
case ContentTypeSort.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 ContentTypeSort.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 ContentTypeSort.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);

ContentTypeEntity[] fieldTypes = await query.ToArrayAsync(cancellationToken);
IEnumerable<CmsContentType> items = await MapAsync(fieldTypes, cancellationToken);

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

private async Task<CmsContentType> MapAsync(ContentTypeEntity contentType, CancellationToken cancellationToken)
=> (await MapAsync([contentType], cancellationToken)).Single();
private async Task<IReadOnlyCollection<CmsContentType>> MapAsync(IEnumerable<ContentTypeEntity> contentTypes, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ public ContentTypeRepository(IEventBus eventBus, EventContext eventContext, IEve
_sqlHelper = sqlHelper;
}

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

public async Task<ContentTypeAggregate?> LoadAsync(ContentTypeId id, CancellationToken cancellationToken)
{
return await base.LoadAsync<ContentTypeAggregate>(id.AggregateId, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Logitar.Cms.Contracts.ContentTypes;
using Logitar.Cms.Contracts.Search;
using Logitar.Cms.Core;
using Logitar.Cms.Core.ContentTypes.Commands;
using Logitar.Cms.Core.ContentTypes.Queries;
using Logitar.Cms.Web.Models.ContentTypes;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

Expand Down Expand Up @@ -41,4 +43,11 @@ public async Task<ActionResult<CmsContentType>> ReadAsync(string uniqueName, Can
CmsContentType? contentType = await _pipeline.ExecuteAsync(new ReadContentTypeQuery(Id: null, uniqueName), cancellationToken);
return contentType == null ? NotFound() : Ok(contentType);
}

[HttpGet]
public async Task<ActionResult<SearchResults<CmsContentType>>> SearchAsync([FromQuery] SearchContentTypesParameters parameters, CancellationToken cancellationToken)
{
SearchResults<CmsContentType> contentTypes = await _pipeline.ExecuteAsync(new SearchContentTypesQuery(parameters.ToPayload()), cancellationToken);
return Ok(contentTypes);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Logitar.Cms.Contracts.ContentTypes;
using Logitar.Cms.Contracts.Search;
using Logitar.Cms.Web.Models.Search;
using Microsoft.AspNetCore.Mvc;

namespace Logitar.Cms.Web.Models.ContentTypes;

public record SearchContentTypesParameters : SearchParameters
{
[FromQuery(Name = "invariant")]
public bool? IsInvariant { get; set; }

public SearchContentTypesPayload ToPayload()
{
SearchContentTypesPayload payload = new()
{
IsInvariant = IsInvariant
};

FillPayload(payload);

List<SortOption>? sortOptions = ((SearchPayload)payload).Sort;
if (sortOptions != null)
{
payload.Sort = new List<ContentTypeSortOption>(capacity: sortOptions.Count);
foreach (SortOption sort in sortOptions)
{
if (Enum.TryParse(sort.Field, out ContentTypeSort field))
{
payload.Sort.Add(new ContentTypeSortOption(field, sort.IsDescending));
}
}
}

return payload;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using Logitar.Cms.Contracts.ContentTypes;
using Logitar.Cms.Contracts.Search;
using Microsoft.Extensions.DependencyInjection;

namespace Logitar.Cms.Core.ContentTypes.Queries;

[Trait(Traits.Category, Categories.Integration)]
public class SearchContentTypesQueryHandlerTests : IntegrationTests
{
private readonly IContentTypeRepository _fieldTypeRepository;

private readonly ContentTypeAggregate _article;
private readonly ContentTypeAggregate _blog;
private readonly ContentTypeAggregate _author;
private readonly ContentTypeAggregate _magazine;
private readonly ContentTypeAggregate _product;

public SearchContentTypesQueryHandlerTests() : base()
{
_fieldTypeRepository = ServiceProvider.GetRequiredService<IContentTypeRepository>();

_article = new(new IdentifierUnit("BlogArticle"), isInvariant: false);
_blog = new(new IdentifierUnit("Blog"), isInvariant: false);
_author = new(new IdentifierUnit("BlogAuthor"), isInvariant: true);
_magazine = new(new IdentifierUnit("Magazine"), isInvariant: false);
_product = new(new IdentifierUnit("Product"), isInvariant: false);
}

public override async Task InitializeAsync()
{
await base.InitializeAsync();

await _fieldTypeRepository.SaveAsync([_article, _blog, _author, _magazine, _product]);
}

[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()
{
SearchContentTypesPayload payload = new()
{
Search = new TextSearch([new SearchTerm("%test%")])
};
SearchContentTypesQuery query = new(payload);

SearchResults<CmsContentType> 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()
{
SearchContentTypesPayload payload = new()
{
IsInvariant = false,
IdIn = (await _fieldTypeRepository.LoadAsync()).Select(fieldType => fieldType.Id.ToGuid()).ToList(),
Search = new TextSearch([new SearchTerm("blog%"), new SearchTerm("%z%")], SearchOperator.Or),
Sort = [new ContentTypeSortOption(ContentTypeSort.UniqueName, isDescending: true)],
Skip = 1,
Limit = 1
};
payload.IdIn.Add(Guid.Empty);
payload.IdIn.Remove(_blog.Id.ToGuid());
SearchContentTypesQuery query = new(payload);

SearchResults<CmsContentType> results = await Pipeline.ExecuteAsync(query);

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

0 comments on commit fae5d5f

Please sign in to comment.