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

Commit

Permalink
Implemented project search.
Browse files Browse the repository at this point in the history
  • Loading branch information
Utar94 committed Apr 26, 2024
1 parent 823a5f8 commit cc263ab
Show file tree
Hide file tree
Showing 21 changed files with 354 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Logitar.Master.Contracts.Projects;
using Logitar.Master.Contracts.Search;
using Logitar.Master.Domain.Projects;

namespace Logitar.Master.Application.Projects;
Expand All @@ -9,4 +10,6 @@ public interface IProjectQuerier
Task<Project?> ReadAsync(ProjectId id, CancellationToken cancellationToken = default);
Task<Project?> ReadAsync(Guid id, CancellationToken cancellationToken = default);
Task<Project?> ReadAsync(string uniqueKey, CancellationToken cancellationToken = default);

Task<SearchResults<Project>> SearchAsync(SearchProjectsPayload payload, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using Logitar.Master.Contracts.Projects;
using Logitar.Master.Contracts.Search;
using MediatR;

namespace Logitar.Master.Application.Projects.Queries;

public record SearchProjectsQuery(SearchProjectsPayload Payload) : IRequest<SearchResults<Project>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Logitar.Master.Contracts.Projects;
using Logitar.Master.Contracts.Search;
using MediatR;

namespace Logitar.Master.Application.Projects.Queries;

internal class SearchProjectsQueryHandler : IRequestHandler<SearchProjectsQuery, SearchResults<Project>>
{
private readonly IProjectQuerier _projectQuerier;

public SearchProjectsQueryHandler(IProjectQuerier projectQuerier)
{
_projectQuerier = projectQuerier;
}

public async Task<SearchResults<Project>> Handle(SearchProjectsQuery query, CancellationToken cancellationToken)
{
return await _projectQuerier.SearchAsync(query.Payload, cancellationToken);
}
}
8 changes: 8 additions & 0 deletions backend/src/Logitar.Master.Contracts/Projects/ProjectSort.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Logitar.Master.Contracts.Projects;

public enum ProjectSort
{
DisplayName,
UniqueKey,
UpdatedOn
}
19 changes: 19 additions & 0 deletions backend/src/Logitar.Master.Contracts/Projects/ProjectSortOption.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Logitar.Master.Contracts.Search;

namespace Logitar.Master.Contracts.Projects;

public record ProjectSortOption : SortOption
{
public new ProjectSort Field
{
get => Enum.Parse<ProjectSort>(base.Field);
set => base.Field = value.ToString();
}
public ProjectSortOption() : this(ProjectSort.UpdatedOn, isDescending: true)
{
}

public ProjectSortOption(ProjectSort field, bool isDescending = false) : base(field.ToString(), isDescending)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Logitar.Master.Contracts.Search;

namespace Logitar.Master.Contracts.Projects;

public record SearchProjectsPayload : SearchPayload
{
public new List<ProjectSortOption>? Sort { get; set; }
}
7 changes: 7 additions & 0 deletions backend/src/Logitar.Master.Contracts/Search/SearchOperator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Logitar.Master.Contracts.Search;

public enum SearchOperator
{
And = 0,
Or = 1
}
12 changes: 12 additions & 0 deletions backend/src/Logitar.Master.Contracts/Search/SearchPayload.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Logitar.Master.Contracts.Search;

public record SearchPayload
{
public List<Guid>? Ids { get; set; }
public TextSearch? Search { get; set; }

public List<SortOption>? Sort { get; set; }

public int? Skip { get; set; }
public int? Limit { get; set; }
}
28 changes: 28 additions & 0 deletions backend/src/Logitar.Master.Contracts/Search/SearchResults.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace Logitar.Master.Contracts.Search;

public record SearchResults<T>
{
public List<T> Items { get; set; }
public long Total { get; set; }

public SearchResults()
{
Items = [];
}

public SearchResults(IEnumerable<T> items) : this()
{
Items.AddRange(items);
}

public SearchResults(long total) : this()
{
Total = total;
}

public SearchResults(IEnumerable<T> items, long total) : this()
{
Items.AddRange(items);
Total = total;
}
}
15 changes: 15 additions & 0 deletions backend/src/Logitar.Master.Contracts/Search/SearchTerm.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Logitar.Master.Contracts.Search;

public record SearchTerm
{
public string Value { get; set; }

public SearchTerm() : this(string.Empty)
{
}

public SearchTerm(string value)
{
Value = value;
}
}
17 changes: 17 additions & 0 deletions backend/src/Logitar.Master.Contracts/Search/SortOption.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Logitar.Master.Contracts.Search;

public record SortOption
{
public string Field { get; set; }
public bool IsDescending { get; set; }

public SortOption() : this(string.Empty)
{
}

public SortOption(string field, bool isDescending = false)
{
Field = field;
IsDescending = isDescending;
}
}
18 changes: 18 additions & 0 deletions backend/src/Logitar.Master.Contracts/Search/TextSearch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Logitar.Master.Contracts.Search;

public record TextSearch
{
public List<SearchTerm> Terms { get; set; }
public SearchOperator Operator { get; set; }

public TextSearch()
{
Terms = [];
}

public TextSearch(IEnumerable<SearchTerm> terms, SearchOperator @operator = SearchOperator.And) : this()
{
Terms.AddRange(terms);
Operator = @operator;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

public interface IProjectRepository
{
Task<IEnumerable<ProjectAggregate>> LoadAsync(CancellationToken cancellationToken = default);
Task<ProjectAggregate?> LoadAsync(Guid id, CancellationToken cancellationToken = default);
Task<ProjectAggregate?> LoadAsync(ProjectId id, CancellationToken cancellationToken = default);
Task<ProjectAggregate?> LoadAsync(ProjectId id, long? version, CancellationToken cancellationToken = default);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Logitar.Master.EntityFrameworkCore.SqlServer;

internal class SqlServerHelper : ISqlHelper
internal class SqlServerHelper : SqlHelper, ISqlHelper
{
public IQueryBuilder QueryFrom(TableId table) => SqlServerQueryBuilder.From(table);
public override IQueryBuilder QueryFrom(TableId table) => SqlServerQueryBuilder.From(table);
}
2 changes: 2 additions & 0 deletions backend/src/Logitar.Master.EntityFrameworkCore/ISqlHelper.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using Logitar.Data;
using Logitar.Master.Contracts.Search;

namespace Logitar.Master.EntityFrameworkCore;

public interface ISqlHelper
{
void ApplyTextSearch(IQueryBuilder query, TextSearch? search, params ColumnId[] columns);
IQueryBuilder QueryFrom(TableId table);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Logitar.Data" Version="3.0.0" />
<PackageReference Include="Logitar.EventSourcing.EntityFrameworkCore.Relational" Version="5.1.0" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Logitar.EventSourcing;
using Logitar.Data;
using Logitar.EventSourcing;
using Logitar.Master.Application.Projects;
using Logitar.Master.Contracts.Actors;
using Logitar.Master.Contracts.Projects;
using Logitar.Master.Contracts.Search;
using Logitar.Master.Domain.Projects;
using Logitar.Master.EntityFrameworkCore.Actors;
using Logitar.Master.EntityFrameworkCore.Entities;
Expand All @@ -13,11 +15,13 @@ internal class ProjectQuerier : IProjectQuerier
{
private readonly IActorService _actorService;
private readonly MasterContext _context;
private readonly ISqlHelper _sqlHelper;

public ProjectQuerier(IActorService actorService, MasterContext context)
public ProjectQuerier(IActorService actorService, MasterContext context, ISqlHelper sqlHelper)
{
_actorService = actorService;
_context = context;
_sqlHelper = sqlHelper;
}

public async Task<Project> ReadAsync(ProjectAggregate project, CancellationToken cancellationToken)
Expand Down Expand Up @@ -49,6 +53,50 @@ public async Task<Project> ReadAsync(ProjectAggregate project, CancellationToken
return project == null ? null : await MapAsync(project, cancellationToken);
}

public async Task<SearchResults<Project>> SearchAsync(SearchProjectsPayload payload, CancellationToken cancellationToken)
{
IQueryBuilder builder = _sqlHelper.QueryFrom(MasterDb.Projects.Table).SelectAll(MasterDb.Projects.Table)
.ApplyIdFilter(MasterDb.Projects.AggregateId, payload.Ids);
_sqlHelper.ApplyTextSearch(builder, payload.Search, MasterDb.Projects.UniqueKey, MasterDb.Projects.DisplayName);

IQueryable<ProjectEntity> query = _context.Projects.FromQuery(builder).AsNoTracking();
long total = await query.LongCountAsync(cancellationToken);

IOrderedQueryable<ProjectEntity>? ordered = null;
if (payload.Sort != null && payload.Sort.Count > 0)
{
foreach (ProjectSortOption sort in payload.Sort)
{
switch (sort.Field)
{
case ProjectSort.DisplayName:
ordered = (ordered == null)
? (sort.IsDescending ? query.OrderByDescending(x => x.DisplayName) : query.OrderBy(x => x.DisplayName))
: (sort.IsDescending ? ordered.ThenByDescending(x => x.DisplayName) : ordered.ThenByDescending(x => x.DisplayName));
break;
case ProjectSort.UniqueKey:
ordered = (ordered == null)
? (sort.IsDescending ? query.OrderByDescending(x => x.UniqueKey) : query.OrderBy(x => x.UniqueKey))
: (sort.IsDescending ? ordered.ThenByDescending(x => x.UniqueKey) : ordered.ThenByDescending(x => x.UniqueKey));
break;
case ProjectSort.UpdatedOn:
ordered = (ordered == null)
? (sort.IsDescending ? query.OrderByDescending(x => x.UpdatedOn) : query.OrderBy(x => x.UpdatedOn))
: (sort.IsDescending ? ordered.ThenByDescending(x => x.UpdatedOn) : ordered.ThenByDescending(x => x.UpdatedOn));
break;
}
}
}
query = ordered ?? query;

query = query.ApplyPaging(payload);

ProjectEntity[] projects = await query.ToArrayAsync(cancellationToken);
IEnumerable<Project> items = await MapAsync(projects, cancellationToken);

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

private async Task<Project> MapAsync(ProjectEntity project, CancellationToken cancellationToken)
{
return (await MapAsync([project], cancellationToken)).Single();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,38 @@
using Logitar.Data;
using Logitar.EventSourcing;
using Logitar.Master.Contracts.Search;
using Microsoft.EntityFrameworkCore;

namespace Logitar.Master.EntityFrameworkCore;

internal static class QueryingExtensions
{
public static IQueryBuilder ApplyIdFilter(this IQueryBuilder query, ColumnId column, IEnumerable<Guid>? ids)
{
if (ids != null && ids.Any())
{
string[] aggregateIds = ids.Distinct().Select(id => new AggregateId(id).Value).ToArray();
query.Where(column, Operators.IsIn(aggregateIds));
}

return query;
}

public static IQueryable<T> ApplyPaging<T>(this IQueryable<T> query, SearchPayload payload)
{
if (payload.Skip > 0)
{
query = query.Skip(payload.Skip.Value);
}

if (payload.Limit > 0)
{
query = query.Take(payload.Limit.Value);
}

return query;
}

public static IQueryable<T> FromQuery<T>(this DbSet<T> entities, IQueryBuilder query) where T : class
{
return entities.FromQuery(query.Build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ public ProjectRepository(IEventBus eventBus, EventContext eventContext, IEventSe
_sqlHelper = sqlHelper;
}

public async Task<IEnumerable<ProjectAggregate>> LoadAsync(CancellationToken cancellationToken)
{
return await base.LoadAsync<ProjectAggregate>(cancellationToken);
}

public async Task<ProjectAggregate?> LoadAsync(Guid id, CancellationToken cancellationToken)
{
return await base.LoadAsync<ProjectAggregate>(new AggregateId(id), cancellationToken);
Expand Down
45 changes: 45 additions & 0 deletions backend/src/Logitar.Master.EntityFrameworkCore/SqlHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Logitar.Data;
using Logitar.Master.Contracts.Search;

namespace Logitar.Master.EntityFrameworkCore;

public abstract class SqlHelper : ISqlHelper
{
public virtual void ApplyTextSearch(IQueryBuilder query, TextSearch? search, params ColumnId[] columns)
{
if (search != null && search.Terms.Count > 0 && columns.Length > 0)
{
List<Condition> conditions = new(capacity: search.Terms.Count);
HashSet<string> patterns = new(capacity: search.Terms.Count);
foreach (SearchTerm term in search.Terms)
{
if (!string.IsNullOrWhiteSpace(term.Value))
{
string pattern = term.Value.Trim();
if (patterns.Add(pattern))
{
conditions.Add(columns.Length == 1
? new OperatorCondition(columns[0], CreateOperator(pattern))
: new OrCondition(columns.Select(column => new OperatorCondition(column, CreateOperator(pattern))).ToArray()));
}
}
}

if (conditions.Count > 0)
{
switch (search.Operator)
{
case SearchOperator.And:
query.WhereAnd([.. conditions]);
break;
case SearchOperator.Or:
query.WhereOr([.. conditions]);
break;
}
}
}
}
public virtual ConditionalOperator CreateOperator(string pattern) => Operators.IsLike(pattern);

public abstract IQueryBuilder QueryFrom(TableId table);
}
Loading

0 comments on commit cc263ab

Please sign in to comment.