Skip to content

Commit

Permalink
Implemented content search. (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
Utar94 authored Jul 22, 2024
1 parent fae5d5f commit 223f091
Show file tree
Hide file tree
Showing 18 changed files with 349 additions and 20 deletions.
7 changes: 7 additions & 0 deletions backend/src/Logitar.Cms.Contracts/Contents/ContentSort.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Logitar.Cms.Contracts.Contents;

public enum ContentSort
{
UniqueName,
UpdatedOn
}
20 changes: 20 additions & 0 deletions backend/src/Logitar.Cms.Contracts/Contents/ContentSortOption.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Logitar.Cms.Contracts.Search;

namespace Logitar.Cms.Contracts.Contents;

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

public ContentSortOption() : this(ContentSort.UpdatedOn, isDescending: true)
{
}

public ContentSortOption(ContentSort field, bool isDescending = false) : base(field.ToString(), isDescending)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Logitar.Cms.Contracts.Search;

namespace Logitar.Cms.Contracts.Contents;

public record SearchContentsPayload : SearchPayload
{
public Guid? ContentTypeId { get; set; }
public Guid? LanguageId { get; set; }

public new List<ContentSortOption>? Sort { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ namespace Logitar.Cms.Core.ContentTypes.Queries;

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

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

public async Task<SearchResults<CmsContentType>> Handle(SearchContentTypesQuery query, CancellationToken cancellationToken)
{
return await _fieldTypeQuerier.SearchAsync(query.Payload, cancellationToken);
return await _contentTypeQuerier.SearchAsync(query.Payload, cancellationToken);
}
}
3 changes: 3 additions & 0 deletions backend/src/Logitar.Cms.Core/Contents/IContentQuerier.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Logitar.Cms.Contracts.Contents;
using Logitar.Cms.Contracts.Search;

namespace Logitar.Cms.Core.Contents;

Expand All @@ -7,4 +8,6 @@ public interface IContentQuerier
Task<ContentItem> ReadAsync(ContentAggregate content, CancellationToken cancellationToken = default);
Task<ContentItem?> ReadAsync(ContentId id, CancellationToken cancellationToken = default);
Task<ContentItem?> ReadAsync(Guid id, CancellationToken cancellationToken = default);

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

public interface IContentRepository
{
Task<IReadOnlyCollection<ContentAggregate>> LoadAsync(CancellationToken cancellationToken = default);
Task<ContentAggregate?> LoadAsync(ContentId id, CancellationToken cancellationToken = default);
Task<ContentAggregate?> LoadAsync(ContentTypeId contentTypeId, LanguageId? languageId, 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.Contents;
using Logitar.Cms.Contracts.Search;
using MediatR;

namespace Logitar.Cms.Core.Contents.Queries;

public record SearchContentsQuery(SearchContentsPayload Payload) : IRequest<SearchResults<ContentLocale>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Logitar.Cms.Contracts.Contents;
using Logitar.Cms.Contracts.Search;
using MediatR;

namespace Logitar.Cms.Core.Contents.Queries;

internal class SearchContentsQueryHandler : IRequestHandler<SearchContentsQuery, SearchResults<ContentLocale>>
{
private readonly IContentQuerier _contentQuerier;

public SearchContentsQueryHandler(IContentQuerier fieldTypeQuerier)
{
_contentQuerier = fieldTypeQuerier;
}

public async Task<SearchResults<ContentLocale>> Handle(SearchContentsQuery query, CancellationToken cancellationToken)
{
return await _contentQuerier.SearchAsync(query.Payload, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ private ContentItemEntity() : base()
{
}

public override IEnumerable<ActorId> GetActorIds()
public override IEnumerable<ActorId> GetActorIds() => GetActorIds(includeLocales: true);
public IEnumerable<ActorId> GetActorIds(bool includeLocales)
{
List<ActorId> actorIds = base.GetActorIds().ToList();

Expand All @@ -37,9 +38,12 @@ public override IEnumerable<ActorId> GetActorIds()
actorIds.AddRange(ContentType.GetActorIds());
}

foreach (ContentLocaleEntity locale in Locales)
if (includeLocales)
{
actorIds.AddRange(locale.GetActorIds());
foreach (ContentLocaleEntity locale in Locales)
{
actorIds.AddRange(locale.GetActorIds(includeItem: false));
}
}

return actorIds.AsReadOnly();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,22 @@ private ContentLocaleEntity()
{
}

public IEnumerable<ActorId> GetActorIds() => [new(CreatedBy), new(UpdatedBy)];
public IEnumerable<ActorId> GetActorIds(bool includeItem)
{
List<ActorId> actorIds = [new(CreatedBy), new(UpdatedBy)];

if (Language != null)
{
actorIds.AddRange(Language.GetActorIds());
}

if (includeItem && Item != null)
{
actorIds.AddRange(Item.GetActorIds(includeLocales: false));
}

return actorIds.AsReadOnly();
}

public void Update(ContentLocaleUnit locale, DomainEvent @event)
{
Expand Down
13 changes: 13 additions & 0 deletions backend/src/Logitar.Cms.EntityFrameworkCore/Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,19 @@ public ContentItem ToContentItem(ContentItemEntity source)

return destination;
}

public ContentLocale ToContentLocale(ContentLocaleEntity source)
{
if (source.Item == null)
{
throw new ArgumentException($"The {nameof(source.Item)} is required.", nameof(source));
}

ContentItem item = ToContentItem(source.Item);

return item.Locales.SingleOrDefault() ?? item.Invariant;
}

private ContentLocale ToContentLocale(ContentLocaleEntity source, ContentItem item)
{
if (source.LanguageId != null && source.Language == null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using Logitar.Cms.Contracts.Actors;
using Logitar.Cms.Contracts.Contents;
using Logitar.Cms.Contracts.Search;
using Logitar.Cms.Core.Contents;
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,17 @@ internal class ContentQuerier : IContentQuerier
{
private readonly IActorService _actorService;
private readonly DbSet<ContentItemEntity> _contentItems;
private readonly DbSet<ContentLocaleEntity> _contentLocales;
private readonly ISearchHelper _searchHelper;
private readonly ISqlHelper _sqlHelper;

public ContentQuerier(IActorService actorService, CmsContext context)
public ContentQuerier(IActorService actorService, CmsContext context, ISearchHelper searchHelper, ISqlHelper sqlHelper)
{
_actorService = actorService;
_contentItems = context.ContentItems;
_contentLocales = context.ContentLocales;
_searchHelper = searchHelper;
_sqlHelper = sqlHelper;
}

public async Task<ContentItem> ReadAsync(ContentAggregate content, CancellationToken cancellationToken)
Expand All @@ -38,6 +47,59 @@ public async Task<ContentItem> ReadAsync(ContentAggregate content, CancellationT
return content == null ? null : await MapAsync(content, cancellationToken);
}

public async Task<SearchResults<ContentLocale>> SearchAsync(SearchContentsPayload payload, CancellationToken cancellationToken)
{
IQueryBuilder builder = _sqlHelper.QueryFrom(CmsDb.ContentLocales.Table).SelectAll(CmsDb.ContentLocales.Table)
.Join(CmsDb.ContentItems.ContentItemId, CmsDb.ContentLocales.ContentItemId)
.Join(CmsDb.ContentTypes.ContentTypeId, CmsDb.ContentItems.ContentTypeId)
.LeftJoin(CmsDb.Languages.LanguageId, CmsDb.ContentLocales.LanguageId)
.ApplyIdInFilter(CmsDb.ContentItems.UniqueId, payload);
_searchHelper.ApplyTextSearch(builder, payload.Search, CmsDb.ContentLocales.UniqueName);

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

ConditionalOperator @operator = payload.LanguageId.HasValue ? Operators.IsEqualTo(payload.LanguageId.Value.ToString()) : Operators.IsNull();
builder.Where(CmsDb.Languages.UniqueId, @operator);

IQueryable<ContentLocaleEntity> query = _contentLocales.FromQuery(builder).AsNoTracking()
.Include(x => x.Item).ThenInclude(x => x!.ContentType)
.Include(x => x.Language);

long total = await query.LongCountAsync(cancellationToken);

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

ContentLocaleEntity[] locales = await query.ToArrayAsync(cancellationToken);
IEnumerable<ContentLocale> items = await MapAsync(locales, cancellationToken);

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

private async Task<ContentItem> MapAsync(ContentItemEntity content, CancellationToken cancellationToken)
=> (await MapAsync([content], cancellationToken)).Single();
private async Task<IReadOnlyCollection<ContentItem>> MapAsync(IEnumerable<ContentItemEntity> contents, CancellationToken cancellationToken)
Expand All @@ -48,4 +110,15 @@ private async Task<IReadOnlyCollection<ContentItem>> MapAsync(IEnumerable<Conten

return contents.Select(mapper.ToContentItem).ToArray();
}

private async Task<ContentLocale> MapAsync(ContentLocaleEntity locale, CancellationToken cancellationToken)
=> (await MapAsync([locale], cancellationToken)).Single();
private async Task<IReadOnlyCollection<ContentLocale>> MapAsync(IEnumerable<ContentLocaleEntity> locales, CancellationToken cancellationToken)
{
IEnumerable<ActorId> actorIds = locales.SelectMany(locales => locales.GetActorIds(includeItem: true));
IReadOnlyCollection<Actor> actors = await _actorService.FindAsync(actorIds, cancellationToken);
Mapper mapper = new(actors);

return locales.Select(mapper.ToContentLocale).ToArray();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ public async Task<SearchResults<CmsContentType>> SearchAsync(SearchContentTypesP

query = query.ApplyPaging(payload);

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

return new SearchResults<CmsContentType>(items, total);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ public ContentRepository(IEventBus eventBus, EventContext eventContext, IEventSe
_sqlHelper = sqlHelper;
}

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

public async Task<ContentAggregate?> LoadAsync(ContentId id, CancellationToken cancellationToken)
{
return await base.LoadAsync<ContentAggregate>(id.AggregateId, cancellationToken);
Expand Down
9 changes: 9 additions & 0 deletions backend/src/Logitar.Cms.Web/Controllers/ContentController.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Logitar.Cms.Contracts.Contents;
using Logitar.Cms.Contracts.Search;
using Logitar.Cms.Core;
using Logitar.Cms.Core.Contents.Commands;
using Logitar.Cms.Core.Contents.Queries;
using Logitar.Cms.Web.Models.Contents;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

Expand Down Expand Up @@ -48,4 +50,11 @@ public async Task<ActionResult<ContentItem>> SaveLocaleAsync(Guid contentId, Gui
ContentItem? content = await _pipeline.ExecuteAsync(new SaveContentLocaleCommand(contentId, languageId, payload), cancellationToken);
return content == null ? NotFound() : Ok(content);
}

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

namespace Logitar.Cms.Web.Models.Contents;

public record SearchContentsParameters : SearchParameters
{
[FromQuery(Name = "type")]
public Guid? ContentTypeId { get; set; }

[FromQuery(Name = "language")]
public Guid? LanguageId { get; set; }

public SearchContentsPayload ToPayload()
{
SearchContentsPayload payload = new()
{
ContentTypeId = ContentTypeId,
LanguageId = LanguageId
};

FillPayload(payload);

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

return payload;
}
}
Loading

0 comments on commit 223f091

Please sign in to comment.