Skip to content
This repository has been archived by the owner on Jul 9, 2024. It is now read-only.

Commit

Permalink
Implemented content search. (#41)
Browse files Browse the repository at this point in the history
* Contracts.

* Controller & parameters.

* Completed content search.

* Integration tests.
  • Loading branch information
Utar94 authored Jun 24, 2024
1 parent a4e70f5 commit 153b0a9
Show file tree
Hide file tree
Showing 13 changed files with 342 additions and 0 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; } = [];
}
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 _contentTypeQuerier;

public SearchContentsQueryHandler(IContentQuerier contentTypeQuerier)
{
_contentTypeQuerier = contentTypeQuerier;
}

public async Task<SearchResults<ContentLocale>> Handle(SearchContentsQuery query, CancellationToken cancellationToken)
{
return await _contentTypeQuerier.SearchAsync(query.Payload, cancellationToken);
}
}
12 changes: 12 additions & 0 deletions backend/src/Logitar.Cms.EntityFrameworkCore/Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ public ContentItem ToContentItem(ContentItemEntity source)
return destination;
}

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

ContentItem item = ToContentItem(source.ContentItem);

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

private ContentLocale ToContentLocale(ContentLocaleEntity source, ContentItem item)
{
if (source.LanguageId.HasValue && source.Language == null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +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.Core.ContentTypes;
using Logitar.Cms.Core.Localization;
using Logitar.Cms.EntityFrameworkCore.Actors;
using Logitar.Cms.EntityFrameworkCore.Entities;
using Logitar.Data;
using Logitar.EventSourcing;
using Logitar.Identity.EntityFrameworkCore.Relational;
using Microsoft.EntityFrameworkCore;
Expand All @@ -13,13 +17,15 @@ 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, ISearchHelper searchHelper, ISqlHelper sqlHelper)
{
_actorService = actorService;
_contentItems = context.ContentItems;
_contentLocales = context.ContentLocales;
_searchHelper = searchHelper;
_sqlHelper = sqlHelper;
}
Expand All @@ -45,6 +51,60 @@ 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.AggregateId, payload);
_searchHelper.ApplyTextSearch(builder, payload.Search, CmsDb.ContentLocales.UniqueName);

if (payload.ContentTypeId.HasValue)
{
ContentTypeId contentTypeId = new(payload.ContentTypeId.Value);
builder.Where(CmsDb.ContentTypes.AggregateId, Operators.IsEqualTo(contentTypeId.Value));
}

LanguageId? languageId = payload.LanguageId.HasValue ? new(payload.LanguageId.Value) : null;
builder.Where(CmsDb.Languages.AggregateId, languageId.HasValue ? Operators.IsEqualTo(languageId.Value.Value) : Operators.IsNull());

IQueryable<ContentLocaleEntity> query = _contentLocales.FromQuery(builder).AsNoTracking()
.Include(x => x.ContentItem).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)
{
return (await MapAsync([content], cancellationToken)).Single();
Expand All @@ -57,4 +117,13 @@ private async Task<IReadOnlyCollection<ContentItem>> MapAsync(IEnumerable<Conten

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

private async Task<IReadOnlyCollection<ContentLocale>> MapAsync(IEnumerable<ContentLocaleEntity> locales, CancellationToken cancellationToken)
{
IEnumerable<ActorId> actorIds = locales.SelectMany(locale => locale.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 @@ -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 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,8 +1,10 @@
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.Extensions;
using Logitar.Cms.Web.Models.Contents;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

Expand Down Expand Up @@ -45,6 +47,13 @@ public async Task<ActionResult<ContentItem>> SaveLocaleAsync(Guid id, [FromBody]
return contentItem == null ? NotFound() : Ok(contentItem);
}

[HttpGet]
public async Task<ActionResult<SearchResults<ContentLocale>>> SearchAsync([FromQuery] SearchContentsParameters parameters, CancellationToken cancellationToken)
{
SearchResults<ContentLocale> contentTypes = await _pipeline.ExecuteAsync(new SearchContentsQuery(parameters.ToPayload()), cancellationToken);
return Ok(contentTypes);
}

[HttpPatch("{id}")]
public async Task<ActionResult<ContentItem>> UpdateLocaleAsync(Guid id, [FromBody] UpdateContentLocalePayload payload, Guid? languageId, CancellationToken cancellationToken)
{
Expand Down
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);

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

return payload;
}
}
Loading

0 comments on commit 153b0a9

Please sign in to comment.