Skip to content

Commit

Permalink
Get Events (#32)
Browse files Browse the repository at this point in the history
* added get events contract, endpoint, query and query handler
* added get events endpoint tests
* fixed domain to not use complex object as value object since its not supported to group by nested property (migration fix)
  • Loading branch information
skrasekmichael authored Mar 4, 2024
1 parent 3d6f21a commit a0a746c
Show file tree
Hide file tree
Showing 16 changed files with 753 additions and 43 deletions.
1 change: 0 additions & 1 deletion src/TeamUp.Api/Endpoints/Events/GetEventEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ public void MapEndpoints(RouteGroupBuilder group)
.ProducesProblem(StatusCodes.Status401Unauthorized)
.ProducesProblem(StatusCodes.Status403Forbidden)
.ProducesProblem(StatusCodes.Status404NotFound)
.ProducesValidationProblem()
.WithName(nameof(GetEventEndpoint))
.MapToApiVersion(1);
}
Expand Down
41 changes: 41 additions & 0 deletions src/TeamUp.Api/Endpoints/Events/GetEventsEndpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using MediatR;

using Microsoft.AspNetCore.Mvc;

using TeamUp.Api.Extensions;
using TeamUp.Application.Events.GetEvents;
using TeamUp.Contracts.Events;
using TeamUp.Contracts.Teams;

namespace TeamUp.Api.Endpoints.Events;

public sealed class GetEventsEndpoint : IEndpointGroup
{
public void MapEndpoints(RouteGroupBuilder group)
{
group.MapGet("/", GetEventsAsync)
.Produces<List<EventSlimResponse>>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status401Unauthorized)
.ProducesProblem(StatusCodes.Status403Forbidden)
.ProducesProblem(StatusCodes.Status404NotFound)
.ProducesValidationProblem()
.WithName(nameof(GetEventsEndpoint))
.MapToApiVersion(1);
}

public async Task<IResult> GetEventsAsync(
[FromRoute] Guid teamId,
[FromQuery] DateTime? fromUtc,
[FromServices] ISender sender,
HttpContext httpContext,
CancellationToken ct)
{
var query = new GetEventsQuery(
httpContext.GetCurrentUserId(),
TeamId.FromGuid(teamId),
fromUtc
);
var response = await sender.Send(query, ct);
return response.Match(TypedResults.Ok);
}
}
3 changes: 2 additions & 1 deletion src/TeamUp.Api/Endpoints/TeamsEndpointGroup.Events.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public sealed class EventsEndpointGroup : IEndpointGroup
public void MapEndpoints(RouteGroupBuilder group)
{
group.MapEndpoint<CreateEventEndpoint>()
.MapEndpoint<GetEventEndpoint>();
.MapEndpoint<GetEventEndpoint>()
.MapEndpoint<GetEventsEndpoint>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ public async Task<Result<EventResponse>> Handle(GetEventQuery query, Cancellatio
Status = e.Status,
EventResponses = e.EventResponses.Select(er => new EventResponseResponse
{
Message = er.Reply.Message,
Message = er.Message,
TeamMemberId = er.TeamMemberId,
TeamMemberNickname = team.Members.First(member => member.Id == er.TeamMemberId).Nickname,
TimeStampUtc = er.TimeStampUtc,
Type = er.Reply.Type
Type = er.ReplyType
}).ToList()
})
.FirstOrDefault(),
Expand Down
9 changes: 9 additions & 0 deletions src/TeamUp.Application/Events/GetEvents/GetEventsQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using TeamUp.Application.Abstractions;
using TeamUp.Common;
using TeamUp.Contracts.Events;
using TeamUp.Contracts.Teams;
using TeamUp.Contracts.Users;

namespace TeamUp.Application.Events.GetEvents;

public sealed record GetEventsQuery(UserId InitiatorId, TeamId TeamId, DateTime? FromUtc = null) : IQuery<Result<List<EventSlimResponse>>>;
64 changes: 64 additions & 0 deletions src/TeamUp.Application/Events/GetEvents/GetEvetnsQueryHandlers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Microsoft.EntityFrameworkCore;

using TeamUp.Application.Abstractions;
using TeamUp.Common;
using TeamUp.Common.Abstractions;
using TeamUp.Contracts.Events;
using TeamUp.Domain.Aggregates.Teams;

namespace TeamUp.Application.Events.GetEvents;

internal sealed class GetEventsQueryHandlers : IQueryHandler<GetEventsQuery, Result<List<EventSlimResponse>>>
{
private readonly IAppQueryContext _appQueryContext;
private readonly IDateTimeProvider _dateTimeProvider;

public GetEventsQueryHandlers(IAppQueryContext appQueryContext, IDateTimeProvider dateTimeProvider)
{
_appQueryContext = appQueryContext;
_dateTimeProvider = dateTimeProvider;
}

public async Task<Result<List<EventSlimResponse>>> Handle(GetEventsQuery query, CancellationToken ct)
{
var from = query.FromUtc ?? _dateTimeProvider.UtcNow;
var team = await _appQueryContext.Teams
.Select(team => new
{
team.Id,
Events = _appQueryContext.Events
.Where(e => e.TeamId == team.Id && e.ToUtc > from)
.Include(e => e.EventResponses)
.Select(e => new EventSlimResponse
{
Id = e.Id,
Description = e.Description,
FromUtc = e.FromUtc,
ToUtc = e.ToUtc,
Status = e.Status,
MeetTime = e.MeetTime,
ReplyClosingTimeBeforeMeetTime = e.ReplyClosingTimeBeforeMeetTime,
ReplyCount = e.EventResponses
.GroupBy(er => er.ReplyType)
.Select(x => new ReplyCountResponse
{
Type = x.Key,
Count = x.Count()
})
.ToList(),
EventType = team.EventTypes.First(et => et.Id == e.EventTypeId).Name
})
.OrderBy(e => e.FromUtc)
.ToList(),
Initiator = team.Members
.Select(member => member.UserId)
.FirstOrDefault(id => id == query.InitiatorId)
})
.FirstOrDefaultAsync(team => team.Id == query.TeamId, ct);

return team
.EnsureNotNull(TeamErrors.TeamNotFound)
.EnsureNotNull(team => team.Initiator, TeamErrors.NotMemberOfTeam)
.Then(team => team.Events);
}
}
14 changes: 14 additions & 0 deletions src/TeamUp.Contracts/Events/EventSlimResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace TeamUp.Contracts.Events;

public sealed class EventSlimResponse
{
public required EventId Id { get; init; }
public required string EventType { get; init; }
public required DateTime FromUtc { get; init; }
public required DateTime ToUtc { get; init; }
public required string Description { get; init; }
public required EventStatus Status { get; init; }
public required TimeSpan MeetTime { get; init; }
public required TimeSpan ReplyClosingTimeBeforeMeetTime { get; init; }
public required List<ReplyCountResponse> ReplyCount { get; init; }
}
7 changes: 7 additions & 0 deletions src/TeamUp.Contracts/Events/ReplyCountResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace TeamUp.Contracts.Events;

public readonly struct ReplyCountResponse
{
public required ReplyType Type { get; init; }
public required int Count { get; init; }
}
9 changes: 6 additions & 3 deletions src/TeamUp.Domain/Aggregates/Events/EventResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public sealed class EventResponse : Entity<EventResponseId>
public TeamMemberId TeamMemberId { get; }

public EventId EventId { get; }
public EventReply Reply { get; private set; }
public ReplyType ReplyType { get; private set; }
public string Message { get; private set; }
public DateTime TimeStampUtc { get; private set; }

#pragma warning disable CS8618 // EF Core constructor
Expand All @@ -22,15 +23,17 @@ private EventResponse(EventResponseId id, TeamMemberId teamMemberId, EventId eve
{
TeamMemberId = teamMemberId;
EventId = eventId;
Reply = reply;
ReplyType = reply.Type;
Message = reply.Message;
TimeStampUtc = timeStampUtc;

AddDomainEvent(new EventResponseCreatedDomainEvent(this));
}

internal void UpdateReply(IDateTimeProvider dateTimeProvider, EventReply reply)
{
Reply = reply;
ReplyType = reply.Type;
Message = reply.Message;
TimeStampUtc = dateTimeProvider.UtcNow;

AddDomainEvent(new EventResponseUpdatedDomainEvent(this));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore.Metadata.Builders;

using TeamUp.Contracts.Events;
using TeamUp.Domain.Aggregates.Events;
using TeamUp.Domain.Aggregates.Teams;

using EventResponse = TeamUp.Domain.Aggregates.Events.EventResponse;
Expand All @@ -22,11 +23,8 @@ protected override void ConfigureEntity(EntityTypeBuilder<EventResponse> eventRe
.WithMany()
.HasForeignKey(eventResponse => eventResponse.TeamMemberId);

eventResponseEntityBuilder.ComplexProperty(eventResponse => eventResponse.Reply, eventReplyBuilder =>
{
eventReplyBuilder
.Property(eventReply => eventReply.Message)
.HasMaxLength(255);
});
eventResponseEntityBuilder
.Property(eventResponse => eventResponse.Message)
.HasMaxLength(255);
}
}
Loading

0 comments on commit a0a746c

Please sign in to comment.