Skip to content

Commit

Permalink
Introduce Meeting, decoupling domain API from underlying data sources…
Browse files Browse the repository at this point in the history
… (#47)

### Summary & Motivation

This change introduces decouples our commands and queries for meetings
from the underlying implementation. Commands and queries now work with a
Meeting(Dto) concept, which is a projection based on ManagedMeetings in
our database and from CalendarEntries in connected calendars.

ManagedMeetings can be connected to CalendarEntries. This is the case
when a Meeting is created and the user already has connected their
calendar. It can also be disconnected. This happens when no calendar is
connected, but can in the future also apply to "undetermined" meetings,
i.e. where no timeslot has been identified for the meeting yet.

When accessing meetings, we read both from our ManagedMeetings table and
from connected calendars. The results are combined, combining
ManagedMeetings and CalendarEntries that are connected, and returning
other ManagedMeetings and CalendarEntries directly.

Other changes:

- Set up for easy tests of commands and queries in calendar-assistant
- Prepare for multiple calendar providers by introduction of
ICalendarClient and a factory
- Make clear separation between regular and infrastructure dependencies
in our dependency configuration. The latter being all dependencies that
call outside of the system, such as to Azure Open AI, Microsoft Graph,
etc.
- Add AccountManagementClient for encapsulating cross-service requests
- Add AuthenticatedExecutionContext for a stronger DI-based guarantee
that the user is authenticated

### Checklist

- [x] I have added a Label to the pull-request
- [x] I have added tests, and done manual regression tests
- [x] I have updated the documentation, if necessary

---------

Co-authored-by: Marco van Kimmenade <[email protected]>
  • Loading branch information
gudmundurh and BlueBasher authored Nov 1, 2024
1 parent 2d5ec11 commit d78e3fe
Show file tree
Hide file tree
Showing 38 changed files with 1,146 additions and 465 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System.Security.Claims;
using Moment42.CalendarAssistant.Application.Calendar.Queries;
using Moment42.CalendarAssistant.Application.CalendarConnections.Queries;
using Moment42.CalendarAssistant.Application.Conversations.Commands;
using Moment42.CalendarAssistant.Application.Conversations.Domain;
using Moment42.CalendarAssistant.Application.Meetings.Queries;
using PlatformPlatform.SharedKernel.ApiResults;
using PlatformPlatform.SharedKernel.Domain;
using PlatformPlatform.SharedKernel.Endpoints;
Expand All @@ -22,9 +22,9 @@ public void MapEndpoints(IEndpointRouteBuilder routes)
// should convey that GetSchedule is still not possible, due to reasons [x, y, ...]. This would be
// extremely valuable for the AI, as it thus could work things out.
// Right now I just return empty list, to get a basic UI working fast with branching.
group.MapGet("/schedule", async Task<ApiResult<ScheduleResponseDto[]>> ([AsParameters] GetScheduleQuery query, IMediator mediator)
group.MapGet("/schedule", async Task<ApiResult<MeetingDto[]>> ([AsParameters] GetMeetingsQuery query, IMediator mediator)
=> await mediator.Send(query)
).Produces<ScheduleResponseDto[]>();
).Produces<MeetingDto[]>();

group.MapGet("/calendar-connection", async Task<ApiResult<GetCalendarConnectionDto?>> (IMediator mediator, IHttpContextAccessor httpContextAccessor)
=>
Expand Down
40 changes: 40 additions & 0 deletions application/calendar-assistant/Core/AccountManagementClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Net;
using System.Net.Http.Json;
using JetBrains.Annotations;
using Moment42.CalendarAssistant.Application;
using PlatformPlatform.SharedKernel.Domain;

namespace Moment42.CalendarAssistant;

public interface IAccountManagementClient
{
Task<AccountManagementUserResponse> GetUser(UserId userId, CancellationToken cancellationToken);
}

public sealed class AccountManagementClient(IHttpClientFactory httpClientFactory) : IAccountManagementClient
{
public async Task<AccountManagementUserResponse> GetUser(UserId userId, CancellationToken cancellationToken)
{
var accountManagementHttpClient = httpClientFactory.CreateClient("AccountManagement");
var getUserResponse = await accountManagementHttpClient.SendAsync(
new HttpRequestMessage(HttpMethod.Get, $"{AccountManagementEndpoints.UserEndpoint}/{userId}"),
cancellationToken
);

if (getUserResponse.StatusCode != HttpStatusCode.OK)
{
throw new AccountManagementClientException($"User with UserId {userId} not found. Status code {getUserResponse.StatusCode}.");
}

return await getUserResponse.Content.ReadFromJsonAsync<AccountManagementUserResponse>(cancellationToken)
?? throw new AccountManagementClientException($"Failed to parse response from user endpoint. UserId {userId}.");
}

private sealed class AccountManagementClientException(string message) : Exception(message);
}

[PublicAPI]
public sealed record AccountManagementUserResponse(
string Id,
string TenantId
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Moment42.CalendarAssistant.Application.Calendar;

public sealed class CalendarClientException(string message) : Exception(message);
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.Extensions.DependencyInjection;
using Moment42.CalendarAssistant.Application.CalendarConnections.Domain;

namespace Moment42.CalendarAssistant.Application.Calendar;

public interface ICalendarClientFactory
{
ICalendarClient GetCalendarClient(CalendarConnectionType calendarConnection);
}

public sealed class CalendarClientFactory(IServiceProvider serviceProvider) : ICalendarClientFactory
{
public ICalendarClient GetCalendarClient(CalendarConnectionType calendarConnectionType)
{
return serviceProvider.GetRequiredKeyedService<ICalendarClient>(calendarConnectionType.ToString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Moment42.CalendarAssistant.Application.Calendar;

public abstract record CalendarEntryBase
{
public string? Title { get; init; }

public bool IsOnline { get; init; }

public string[] Attendees { get; init; } = [];

public DateTimeOffset StartTime { get; init; }

public DateTimeOffset EndTime { get; init; }
}

public sealed record CalendarEntryDetails : CalendarEntryBase;

public sealed record CalendarEntry : CalendarEntryBase
{
public CalendarEntryId Id { get; init; } = null!;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Moment42.CalendarAssistant.Application.Calendar;

public sealed record CalendarEntryId(string Value)
{
public static implicit operator string(CalendarEntryId calendarEntryId)
{
return calendarEntryId.Value;
}
}

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Moment42.CalendarAssistant.Application.CalendarConnections.Domain;
using PlatformPlatform.SharedKernel.Domain;

namespace Moment42.CalendarAssistant.Application.Calendar;

public interface ICalendarClient
{
Task<CalendarEntryId> CreateCalendarEntry(CalendarEntryDetails calendarEntry, CancellationToken cancellationToken);

Task<IReadOnlyList<CalendarEntry>> GetCalendarEntries(TenantId tenantId, UserId userId, DateTimeOffset startTime, DateTimeOffset endTime, CancellationToken cancellationToken);

Task CancelCalendarEntry(CalendarEntryId id, CancellationToken cancellationToken);

Task RescheduleCalendarEntry(CalendarEntryId id, DateTimeOffset startTime, DateTimeOffset endTime, CancellationToken cancellationToken);

Task<AuthenticationTokens> GetNewAuthenticationTokens(string refreshToken, CancellationToken cancellationToken);
}
Loading

0 comments on commit d78e3fe

Please sign in to comment.