Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(graphQL): Add subscription for dialog details #1072

Merged
merged 5 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/schema/V1/schema.verified.graphql
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
schema {
query: Queries
subscription: Subscriptions
}

interface DialogByIdError {
Expand Down Expand Up @@ -129,6 +130,10 @@ type DialogByIdPayload {
errors: [DialogByIdError!]!
}

type DialogUpdatedPayload {
id: UUID!
}

type GuiAction {
id: UUID!
action: String!
Expand Down Expand Up @@ -201,6 +206,10 @@ type SeenLog {
isCurrentEndUser: Boolean!
}

type Subscriptions @authorize(policy: "enduser") {
dialogUpdated(dialogId: UUID!): DialogUpdatedPayload!
}

type Transmission {
id: UUID!
createdAt: DateTime!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,8 @@ public enum AttachmentUrlConsumer
Gui = 1,
Api = 2
}

public sealed class DialogUpdatedPayload
{
public Guid Id { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Digdir.Domain.Dialogporten.GraphQL.Common.Authorization;
using HotChocolate.Authorization;
using Constants = Digdir.Domain.Dialogporten.Infrastructure.GraphQl.GraphQlSubscriptionConstants;

// ReSharper disable ClassNeverInstantiated.Global

namespace Digdir.Domain.Dialogporten.GraphQL.EndUser.DialogById;

[Authorize(Policy = AuthorizationPolicy.EndUser)]
public sealed class Subscriptions
{
[Subscribe]
[Topic($"{Constants.DialogUpdatedTopic}{{{nameof(dialogId)}}}")]
public DialogUpdatedPayload DialogUpdated(Guid dialogId,
[EventMessage] Guid eventMessage)
{
ArgumentNullException.ThrowIfNull(eventMessage);
return new DialogUpdatedPayload { Id = dialogId };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ namespace Digdir.Domain.Dialogporten.GraphQL;

public static class ServiceCollectionExtensions
{
public static IServiceCollection AddDialogportenGraphQl(
this IServiceCollection services)
public static IServiceCollection AddDialogportenGraphQl(this IServiceCollection services)
{
return services
.AddGraphQLServer()
// This assumes that subscriptions have been set up by the infrastructure
.AddSubscriptionType<Subscriptions>()
.AddAuthorization()
.RegisterDbContext<DialogDbContext>()
.AddDiagnosticEventListener<ApplicationInsightEventListener>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

<ItemGroup>
<PackageReference Include="Altinn.ApiClients.Maskinporten" Version="9.1.0"/>
<PackageReference Include="HotChocolate.Subscriptions.Redis" Version="13.9.11" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.8" />
<PackageReference Include="Altinn.Authorization.ABAC" Version="0.0.8"/>
<PackageReference Include="Bogus" Version="35.6.0"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
using Digdir.Domain.Dialogporten.Application.Common;
using Digdir.Domain.Dialogporten.Domain.Dialogs.Events;
using Digdir.Domain.Dialogporten.Domain.Outboxes;
using Digdir.Library.Entity.Abstractions.Features.EventPublisher;
using HotChocolate.Subscriptions;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;
using Constants = Digdir.Domain.Dialogporten.Infrastructure.GraphQl.GraphQlSubscriptionConstants;

namespace Digdir.Domain.Dialogporten.Infrastructure.DomainEvents.Outbox;

internal sealed class ConvertDomainEventsToOutboxMessagesInterceptor : SaveChangesInterceptor
{
private readonly ITransactionTime _transactionTime;
private readonly ITopicEventSender _topicEventSender;
private readonly ILogger<ConvertDomainEventsToOutboxMessagesInterceptor> _logger;

public ConvertDomainEventsToOutboxMessagesInterceptor(ITransactionTime transactionTime)
private List<IDomainEvent> _domainEvents = [];

public ConvertDomainEventsToOutboxMessagesInterceptor(
ITransactionTime transactionTime,
ITopicEventSender topicEventSender,
ILogger<ConvertDomainEventsToOutboxMessagesInterceptor> logger)
{
_transactionTime = transactionTime ?? throw new ArgumentNullException(nameof(transactionTime));
_topicEventSender = topicEventSender ?? throw new ArgumentNullException(nameof(topicEventSender));
_logger = logger;
}

public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
Expand All @@ -26,24 +39,53 @@ public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
return base.SavingChangesAsync(eventData, result, cancellationToken);
}

var domainEvents = dbContext.ChangeTracker.Entries()
_domainEvents = dbContext.ChangeTracker.Entries()
.SelectMany(x =>
x.Entity is IEventPublisher publisher
? publisher.PopDomainEvents()
: [])
.ToList();

foreach (var domainEvent in domainEvents)
foreach (var domainEvent in _domainEvents)
{
domainEvent.OccuredAt = _transactionTime.Value;
}

var outboxMessages = domainEvents
var outboxMessages = _domainEvents
.Select(OutboxMessage.Create)
.ToList();

dbContext.Set<OutboxMessage>().AddRange(outboxMessages);

return base.SavingChangesAsync(eventData, result, cancellationToken);
}

public override async ValueTask<int> SavedChangesAsync(SaveChangesCompletedEventData eventData, int result,
CancellationToken cancellationToken = default)
{
foreach (var domainEvent in _domainEvents)
{
try
{
// If you are adding any additional cases to this switch,
// please consider making the running of the tasks parallel
var task = domainEvent switch
{
DialogUpdatedDomainEvent dialogUpdatedDomainEvent => _topicEventSender.SendAsync(
$"{Constants.DialogUpdatedTopic}{dialogUpdatedDomainEvent.DialogId}",
dialogUpdatedDomainEvent.DialogId,
cancellationToken),
_ => ValueTask.CompletedTask,
};

await task;
}
catch (Exception e)
{
_logger.LogError(e, "Failed to send domain event to graphQL subscription");
}
}

return await base.SavedChangesAsync(eventData, result, cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using HotChocolate.Execution.Configuration;
using HotChocolate.Subscriptions;
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;

namespace Digdir.Domain.Dialogporten.Infrastructure.GraphQl;

/// <summary>
/// This implementation is a workaround to allow the use of the AddRedisSubscriptions extension method
/// from HotChocolate.Subscriptions.Redis without having to take the entire HotChocolate library as a dependency.
/// </summary>
internal sealed class DummyRequestExecutorBuilder : IRequestExecutorBuilder
{
public string Name => string.Empty;
public IServiceCollection Services { get; init; } = null!;
}

public static class GraphQlSubscriptionConstants
{
public const string SubscriptionTopicPrefix = "graphql_subscriptions_";
public const string DialogUpdatedTopic = "dialogUpdated/";
}

public static class ServiceCollectionExtensions
{
public static IServiceCollection AddGraphQlRedisSubscriptions(this IServiceCollection services,
string redisConnectionString)
{
var dummyImplementation = new DummyRequestExecutorBuilder { Services = services };
dummyImplementation.AddRedisSubscriptions(_ => ConnectionMultiplexer.Connect(redisConnectionString),
new SubscriptionOptions
{
TopicPrefix = GraphQlSubscriptionConstants.SubscriptionTopicPrefix
});

return services;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
using Digdir.Domain.Dialogporten.Infrastructure.Altinn.NameRegistry;
using Digdir.Domain.Dialogporten.Infrastructure.Altinn.OrganizationRegistry;
using Digdir.Domain.Dialogporten.Infrastructure.Altinn.ResourceRegistry;
using Digdir.Domain.Dialogporten.Infrastructure.GraphQl;
using Digdir.Domain.Dialogporten.Infrastructure.Persistence.Configurations.Actors;
using Digdir.Domain.Dialogporten.Infrastructure.Persistence.Repositories;
using ZiggyCreatures.Caching.Fusion;
Expand Down Expand Up @@ -69,6 +70,8 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi
{
services.AddStackExchangeRedisCache(opt => opt.Configuration = infrastructureSettings.Redis.ConnectionString);
services.AddFusionCacheStackExchangeRedisBackplane(opt => opt.Configuration = infrastructureSettings.Redis.ConnectionString);

services.AddGraphQlRedisSubscriptions(infrastructureSettings.Redis.ConnectionString);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Digdir.Domain.Dialogporten.Infrastructure.Persistence;
using Digdir.Library.Entity.Abstractions.Features.Lookup;
using FluentAssertions;
using HotChocolate.Subscriptions;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
Expand Down Expand Up @@ -72,6 +73,7 @@ public async Task InitializeAsync()
.AddScoped<IServiceOwnerNameRegistry>(_ => CreateServiceOwnerNameRegistrySubstitute())
.AddScoped<IPartyNameRegistry>(_ => CreateNameRegistrySubstitute())
.AddScoped<IOptions<ApplicationSettings>>(_ => CreateApplicationSettingsSubstitute())
.AddScoped<ITopicEventSender>(_ => Substitute.For<ITopicEventSender>())
.AddScoped<IUnitOfWork, UnitOfWork>()
.AddScoped<IAltinnAuthorization, LocalDevelopmentAltinnAuthorization>()
.AddSingleton<ICloudEventBus, IntegrationTestCloudBus>()
Expand Down
Loading