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: Restore dialog action #1702

Merged
merged 25 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
acfb83a
WIP
Fargekritt Jan 9, 2025
6ca727c
WIP
Fargekritt Jan 10, 2025
16ff295
Modify the entity library so that it accepts an options object enabli…
MagnusSandgren Jan 10, 2025
7b124ea
Removed comments
Fargekritt Jan 15, 2025
a9b4c77
Created DisableSoftDeleteableFilter
Fargekritt Jan 16, 2025
257a8f5
Merge branch 'main' into feat/restore-dialog-action
Fargekritt Jan 16, 2025
9d8a981
Created e2e tests
Fargekritt Jan 16, 2025
40d5c58
More e2e
Fargekritt Jan 16, 2025
8d1563f
Clean up
Fargekritt Jan 16, 2025
27b39dd
Clean up
Fargekritt Jan 16, 2025
3355c89
Merge branch 'main' into feat/restore-dialog-action
Fargekritt Jan 16, 2025
6c934e0
Updated comment
Fargekritt Jan 20, 2025
a527ae3
Updated RestoreDialogEndpointSummary.cs
Fargekritt Jan 20, 2025
9dc5ec9
Merge branch 'main' into feat/restore-dialog-action
oskogstad Feb 3, 2025
691ae1b
add test
oskogstad Feb 3, 2025
c1139e7
Namespace
oskogstad Feb 3, 2025
869c9ba
add restored mapping
oskogstad Feb 3, 2025
b5f1c9a
Add Altinn forwarder for RestoredEvent
oskogstad Feb 3, 2025
d70cc1d
update queues verify tests
oskogstad Feb 3, 2025
81df479
Use correct event type
oskogstad Feb 3, 2025
df05eba
Update src/Digdir.Domain.Dialogporten.Infrastructure/UnitOfWork.cs
Fargekritt Feb 6, 2025
d507a43
Merge branch 'main' into feat/restore-dialog-action
Fargekritt Feb 6, 2025
b79609b
Add missing EndpointName attr.
oskogstad Feb 6, 2025
f4ce981
update schema
oskogstad Feb 6, 2025
e01e4e7
Merge branch 'main' into feat/restore-dialog-action
oskogstad Feb 6, 2025
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
68 changes: 67 additions & 1 deletion docs/schema/V1/swagger.verified.json
Original file line number Diff line number Diff line change
Expand Up @@ -6379,6 +6379,72 @@
]
}
},
"/api/v1/serviceowner/dialogs/{dialogId}/actions/restore": {
"post": {
"description": "Restore a dialog. For more information see the documentation (link TBD). ",
"operationId": "V1ServiceOwnerDialogsRestore_RestoreDialog",
"parameters": [
{
"in": "path",
"name": "dialogId",
"required": true,
"schema": {
"format": "guid",
"type": "string"
}
},
{
"in": "header",
"name": "if-Match",
"schema": {
"format": "guid",
"nullable": true,
"type": "string"
}
}
],
"responses": {
"204": {
"description": "The dialog aggregate was restored successfully."
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"404": {
"content": {
"application/problem\u002Bjson": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
},
"description": "The given dialog ID was not found or is already deleted."
},
"412": {
"content": {
"application/problem\u002Bjson": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
},
"description": "The supplied If-Match header did not match the current Revision value for the dialog. The request was not applied."
}
},
"security": [
{
"JWTBearerAuth": []
}
],
"summary": "Restore a dialog",
"tags": [
"Serviceowner"
]
}
},
"/api/v1/serviceowner/dialogs/{dialogId}/actions/should-send-notification": {
"get": {
"description": "Used by Altinn Notification only. Takes a dialogId and returns a boolean value based on conditions used to determine if a notification is to be sent.",
Expand Down Expand Up @@ -7067,4 +7133,4 @@
"url": "https://altinn-dev-api.azure-api.net/dialogporten"
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ namespace Digdir.Domain.Dialogporten.Application.Externals;

public interface IUnitOfWork
{
IUnitOfWork WithoutAggregateSideEffects();
Task<SaveChangesResult> SaveChangesAsync(CancellationToken cancellationToken = default);

IUnitOfWork EnableConcurrencyCheck<TEntity>(
Expand All @@ -16,6 +15,11 @@ IUnitOfWork EnableConcurrencyCheck<TEntity>(
where TEntity : class, IVersionableEntity;

Task BeginTransactionAsync(CancellationToken cancellationToken = default);

IUnitOfWork DisableAggregateFilter();
IUnitOfWork DisableVersionableFilter();
IUnitOfWork DisableUpdatableFilter();
IUnitOfWork DisableSoftDeletableFilter();
}

[GenerateOneOf]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ internal sealed class DialogEventToAltinnForwarder : DomainEventToAltinnForwarde
INotificationHandler<DialogCreatedDomainEvent>,
INotificationHandler<DialogUpdatedDomainEvent>,
INotificationHandler<DialogDeletedDomainEvent>,
INotificationHandler<DialogRestoredDomainEvent>,
INotificationHandler<DialogSeenDomainEvent>
{
public DialogEventToAltinnForwarder(ICloudEventBus cloudEventBus, IOptions<ApplicationSettings> settings)
Expand Down Expand Up @@ -108,6 +109,29 @@ public async Task Handle(DialogDeletedDomainEvent domainEvent, CancellationToken
await CloudEventBus.Publish(cloudEvent, cancellationToken);
}

public async Task Handle(DialogRestoredDomainEvent domainEvent, CancellationToken cancellationToken)
{
if (domainEvent.ShouldNotBeSentToAltinnEvents())
{
return;
}

var cloudEvent = new CloudEvent
{
Id = domainEvent.EventId,
Type = CloudEventTypes.Get(nameof(DialogRestoredDomainEvent)),
Time = domainEvent.OccurredAt,
Resource = domainEvent.ServiceResource,
ResourceInstance = domainEvent.DialogId.ToString(),
Subject = domainEvent.Party,
Source = $"{SourceBaseUrl()}{domainEvent.DialogId}",
Data = GetCloudEventData(domainEvent)
};

await CloudEventBus.Publish(cloudEvent, cancellationToken);

}

private static Dictionary<string, object>? GetCloudEventData(IProcessEvent domainEvent)
{
var data = new Dictionary<string, object>();
Expand All @@ -123,4 +147,5 @@ public async Task Handle(DialogDeletedDomainEvent domainEvent, CancellationToken

return data.Count == 0 ? null : data;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ internal static class CloudEventTypes
internal static string Get(string eventName) => eventName switch
{
// Dialog
nameof(DialogRestoredDomainEvent) => "dialogporten.dialog.restored.v1",
nameof(DialogCreatedDomainEvent) => "dialogporten.dialog.created.v1",
nameof(DialogUpdatedDomainEvent) => "dialogporten.dialog.updated.v1",
nameof(DialogDeletedDomainEvent) => "dialogporten.dialog.deleted.v1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ public async Task<GetDialogResult> Handle(GetDialogQuery request, CancellationTo
currentUserInformation.Name);

var saveResult = await _unitOfWork
.WithoutAggregateSideEffects()
.DisableUpdatableFilter()
.DisableVersionableFilter()
.SaveChangesAsync(cancellationToken);

saveResult.Switch(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Diagnostics;
using Digdir.Domain.Dialogporten.Application.Common;
using Digdir.Domain.Dialogporten.Application.Common.Extensions;
using Digdir.Domain.Dialogporten.Application.Common.ReturnTypes;
using Digdir.Domain.Dialogporten.Application.Externals;
using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities;
using Digdir.Library.Entity.Abstractions.Features.SoftDeletable;
using MediatR;
using Microsoft.EntityFrameworkCore;
using OneOf;

namespace Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Commands.Restore;

public sealed class RestoreDialogCommand : IRequest<RestoreDialogResult>
{
public Guid DialogId { get; set; }
public Guid? IfMatchDialogRevision { get; set; }
}

[GenerateOneOf]
public sealed partial class RestoreDialogResult : OneOfBase<RestoreDialogSuccess, EntityNotFound, ConcurrencyError>;

public sealed record RestoreDialogSuccess(Guid Revision);

internal sealed class RestoreDialogCommandHandler : IRequestHandler<RestoreDialogCommand, RestoreDialogResult>
{
private readonly IDialogDbContext _db;
private readonly IUnitOfWork _unitOfWork;
private readonly IUserResourceRegistry _userResourceRegistry;

public RestoreDialogCommandHandler(IDialogDbContext db, IUnitOfWork unitOfWork, IUserResourceRegistry userResourceRegistry)
{
_db = db ?? throw new ArgumentNullException(nameof(db));
_unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
_userResourceRegistry = userResourceRegistry ?? throw new ArgumentNullException(nameof(userResourceRegistry));
}

public async Task<RestoreDialogResult> Handle(RestoreDialogCommand request, CancellationToken cancellationToken)
{
var resourceIds = await _userResourceRegistry.GetCurrentUserResourceIds(cancellationToken);

var dialog = await _db.Dialogs
.WhereIf(!_userResourceRegistry.IsCurrentUserServiceOwnerAdmin(), x => resourceIds.Contains(x.ServiceResource))
.IgnoreQueryFilters()
.FirstOrDefaultAsync(x => x.Id == request.DialogId, cancellationToken);

if (dialog is null)
{
return new EntityNotFound<DialogEntity>(request.DialogId);
}

if (!dialog.Deleted)
{
return new RestoreDialogSuccess(dialog.Revision);
}

dialog.Restore();

var saveResult = await _unitOfWork
.DisableUpdatableFilter()
.DisableSoftDeletableFilter()
.EnableConcurrencyCheck(dialog, request.IfMatchDialogRevision)
.SaveChangesAsync(cancellationToken);

return saveResult.Match<RestoreDialogResult>(
success => new RestoreDialogSuccess(dialog.Revision),
domainError => throw new UnreachableException("Should never get a domain error when restoring a dialog"),
concurrencyError => concurrencyError
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ public async Task<GetDialogResult> Handle(GetDialogQuery request, CancellationTo
currentUserInformation.Name);

var saveResult = await _unitOfWork
.WithoutAggregateSideEffects()
.DisableUpdatableFilter()
.DisableVersionableFilter()
.SaveChangesAsync(cancellationToken);

saveResult.Switch(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public sealed class DialogEntity :
ISoftDeletableEntity,
IVersionableEntity,
IAggregateChangedHandler,
IEventPublisher
IEventPublisher,
IAggregateRestoredHandler
{
public Guid Id { get; set; }
public Guid Revision { get; set; }
Expand Down Expand Up @@ -84,6 +85,9 @@ public void OnUpdate(AggregateNode self, DateTimeOffset utcNow) =>
public void OnDelete(AggregateNode self, DateTimeOffset utcNow)
=> _domainEvents.Add(new DialogDeletedDomainEvent(Id, ServiceResource, Party, Process, PrecedingProcess));

public void OnRestore(AggregateNode self, DateTimeOffset utcNow) =>
_domainEvents.Add(new DialogRestoredDomainEvent(Id, ServiceResource, Party, Process, PrecedingProcess));

public void UpdateSeenAt(string endUserId, DialogUserType.Values userTypeId, string? endUserName)
{
var lastSeenAt = SeenLog
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Digdir.Domain.Dialogporten.Domain.Common.DomainEvents;

namespace Digdir.Domain.Dialogporten.Domain.Dialogs.Events;

public sealed record DialogRestoredDomainEvent(
Guid DialogId,
string ServiceResource,
string Party,
string? Process,
string? PrecedingProcess) : DomainEvent, IProcessEvent;
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ internal bool TrySetOriginalRevision<TEntity>(
}

var prop = Entry(entity).Property(x => x.Revision);
var isModified = prop.IsModified;
prop.OriginalValue = revision.Value;
prop.IsModified = false;
prop.IsModified = isModified;
return true;
}

Expand Down
54 changes: 43 additions & 11 deletions src/Digdir.Domain.Dialogporten.Infrastructure/UnitOfWork.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ internal sealed class UnitOfWork : IUnitOfWork, IAsyncDisposable, IDisposable
private readonly DialogDbContext _dialogDbContext;
private readonly ITransactionTime _transactionTime;
private readonly IDomainContext _domainContext;
private readonly SaveChangesOptions _saveChangesOptions = new();

private IDbContextTransaction? _transaction;

private bool _aggregateSideEffects = true;
private bool _enableConcurrencyCheck;

public UnitOfWork(DialogDbContext dialogDbContext, ITransactionTime transactionTime, IDomainContext domainContext)
Expand All @@ -51,14 +51,32 @@ public IUnitOfWork EnableConcurrencyCheck<TEntity>(
return this;
}

public IUnitOfWork WithoutAggregateSideEffects()
public async Task BeginTransactionAsync(CancellationToken cancellationToken = default)
=> _transaction ??= await _dialogDbContext.Database.BeginTransactionAsync(cancellationToken);

public IUnitOfWork DisableAggregateFilter()
{
_aggregateSideEffects = false;
_saveChangesOptions.EnableAggregateFilter = false;
return this;
}

public async Task BeginTransactionAsync(CancellationToken cancellationToken = default)
=> _transaction ??= await _dialogDbContext.Database.BeginTransactionAsync(cancellationToken);
public IUnitOfWork DisableVersionableFilter()
{
_saveChangesOptions.EnableVersionableFilter = false;
return this;
}

public IUnitOfWork DisableUpdatableFilter()
{
_saveChangesOptions.EnableUpdatableFilter = false;
return this;
}

public IUnitOfWork DisableSoftDeletableFilter()
{
_saveChangesOptions.EnableSoftDeletableFilter = false;
return this;
}

public async Task<SaveChangesResult> SaveChangesAsync(CancellationToken cancellationToken = default)
{
Expand Down Expand Up @@ -92,12 +110,10 @@ private async Task<SaveChangesResult> SaveChangesAsync_Internal(CancellationToke
return new Success();
}

_dialogDbContext.ChangeTracker.HandleAuditableEntities(_transactionTime.Value);

if (_aggregateSideEffects)
{
await _dialogDbContext.ChangeTracker.HandleAggregateEntities(_transactionTime.Value, cancellationToken);
}
await _dialogDbContext.ChangeTracker.HandleAuditableEntities(
_transactionTime.Value,
_saveChangesOptions,
cancellationToken);

if (!_enableConcurrencyCheck)
{
Expand Down Expand Up @@ -185,4 +201,20 @@ public void Dispose()
_transaction?.Dispose();
_transaction = null;
}

// Although the Entity Framework supports all the options,
// But we only have use cases for some of them. Therefore,
// only some of them have setters until the day we actually
// have a use case for them.
private sealed class SaveChangesOptions : IEntityOptions
{
public bool EnableSoftDeletableFilter { get; set; } = true;
public bool EnableImmutableFilter { get; } = true;
public bool EnableVersionableFilter { get; set; } = true;
public bool EnableUpdatableFilter { get; set; } = true;
public bool EnableCreatableFilter { get; } = true;
public bool EnableLookupFilter { get; } = true;
public bool EnableIdentifiableFilter { get; } = true;
public bool EnableAggregateFilter { get; set; } = true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal static class SwaggerSummary
internal const string ReturnedResult = "Successfully returned the dialog {0}.";
internal const string Created = "The UUID of the created dialog {0}. A relative URL to the newly created activity is set in the \"Location\" header.";
internal const string Deleted = "The dialog {0} was deleted successfully.";
internal const string Restored = "The dialog {0} was restored successfully.";
internal const string Updated = "The dialog {0} was updated successfully.";
internal const string ValidationError = "Validation error occurred. See problem details for a list of errors.";
internal const string DomainError = "Domain error occurred. See problem details for a list of errors.";
Expand Down
Loading