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: Add idempotentId #1638

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6c94a09
Created IdempotentId with validation and db migration
Fargekritt Dec 16, 2024
5c9334d
Create Idempotency added
Fargekritt Dec 16, 2024
4041dd3
Added IdempotentId to get DialogDto
Fargekritt Dec 16, 2024
fac9a18
Search Idempotent added
Fargekritt Dec 16, 2024
c84485a
Added Validadtion on SearchDialog for idempotentId
Fargekritt Dec 16, 2024
def7f0a
Updated purgeDialog to handle idempotentId.
Fargekritt Dec 27, 2024
3661f12
Cleanup
Fargekritt Dec 27, 2024
04dec21
Created more tests
Fargekritt Dec 27, 2024
510b0e9
Merge branch 'main' into feat/add-idempotency-field
Fargekritt Dec 27, 2024
7650083
Updated GQL
Fargekritt Dec 27, 2024
13e370b
Updated test
Fargekritt Dec 27, 2024
0a7e7f7
Swagge summary updated
Fargekritt Dec 30, 2024
84169fc
Updated summary
Fargekritt Jan 6, 2025
5ccd0d5
Updated swagger
Fargekritt Jan 9, 2025
bd3e2f9
Renamed IdempotentId to IdempotentKey.
Fargekritt Jan 13, 2025
3a2422b
Query improvement
Fargekritt Jan 13, 2025
78f0d20
Clean up
Fargekritt Jan 13, 2025
3d9c6db
Cleaning
Fargekritt Jan 13, 2025
f9fe516
PR changes
Fargekritt Jan 14, 2025
4eee9ec
Merge remote-tracking branch 'origin/main' into feat/add-idempotency-…
Fargekritt Jan 14, 2025
a92f270
Merge remote-tracking branch 'origin/main' into feat/add-idempotency-…
Fargekritt Jan 14, 2025
4635d21
clean up
Fargekritt Jan 14, 2025
3524d7e
Made e2e test more robust
Fargekritt Jan 14, 2025
2b61954
fix typo
Fargekritt Jan 14, 2025
346a694
Merge branch 'main' into feat/add-idempotency-field
Fargekritt Jan 14, 2025
3e66899
PR changes
Fargekritt Jan 16, 2025
9f846c7
Added proper filter on index
Fargekritt Jan 17, 2025
d4b32ce
Removed include org
Fargekritt Jan 17, 2025
9fb5399
Merge branch 'main' into feat/add-idempotency-field
Fargekritt Jan 17, 2025
a5c0ab5
Handle creating with idempotentKey optimistically and catch the excep…
Fargekritt Jan 24, 2025
72e11e5
Removed IdempotentId from SearchDialogQuery.cs
Fargekritt Jan 24, 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
6 changes: 6 additions & 0 deletions docs/schema/V1/schema.verified.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ type ContentValue {

type Dialog {
id: UUID!
idempotentId: IdempotentId
revision: UUID!
org: String!
serviceResource: String!
Expand Down Expand Up @@ -168,6 +169,11 @@ type GuiAction {
prompt: [Localization!]
}

type IdempotentId {
org: String!
idempotent: String!
}

type Localization {
value: String!
languageCode: String!
Expand Down
29 changes: 28 additions & 1 deletion docs/schema/V1/swagger.verified.json
Original file line number Diff line number Diff line change
Expand Up @@ -2323,6 +2323,11 @@
"nullable": true,
"type": "string"
},
"idempotentId": {
"description": "A self-defined Id may be provided to support idempotent creation of dialogs.\n ",
Fargekritt marked this conversation as resolved.
Show resolved Hide resolved
"nullable": true,
"type": "string"
},
"party": {
"description": "The party code representing the organization or person that the dialog belongs to in URN format.",
"example": "urn:altinn:person:identifier-no:01125512345\nurn:altinn:organization:identifier-no:912345678",
Expand Down Expand Up @@ -3406,6 +3411,11 @@
"format": "guid",
"type": "string"
},
"idempotentId": {
"description": "A self-defined Id may be provided to support idempotent creation of dialogs.\n ",
"nullable": true,
"type": "string"
},
"org": {
"description": "The service owner code representing the organization (service owner) related to this dialog.",
"example": "ske",
Expand Down Expand Up @@ -4057,6 +4067,11 @@
"format": "guid",
"type": "string"
},
"idempotentId": {
"description": "A self-defined Id may be provided to support idempotent creation of dialogs.\n ",
"nullable": true,
"type": "string"
},
"latestActivity": {
"description": "The latest entry in the dialog\u0027s activity log.",
"nullable": true,
Expand Down Expand Up @@ -5569,6 +5584,15 @@
"description": "Performs a search for dialogs, returning a paginated list of dialogs. For more information see the documentation (link TBD).\n\n* All date parameters must contain explicit time zone. Example: 2023-10-27T10:00:00Z or 2023-10-27T10:00:00\u002B01:00\n* See \u0022continuationToken\u0022 in the response for how to get the next page of results.\n* hasNextPage will be set to true if there are more items to get.",
"operationId": "V1ServiceOwnerDialogsSearch_SearchDialog",
"parameters": [
{
"description": "A self-defined Id may be provided to support idempotent creation of dialogs.\n ",
"in": "query",
"name": "idempotentId",
"schema": {
"nullable": true,
"type": "string"
}
},
{
"description": "Filter by one or more service resources",
"explode": true,
Expand Down Expand Up @@ -5872,6 +5896,9 @@
"403": {
"description": "Unauthorized to create a dialog for the given serviceResource (not owned by authenticated organization or has additional scope requirements defined in policy)."
},
"409": {
"description": ""
},
MagnusSandgren marked this conversation as resolved.
Show resolved Hide resolved
"422": {
"content": {
"application/problem\u002Bjson": {
Expand Down Expand Up @@ -6970,4 +6997,4 @@
"url": "https://altinn-dev-api.azure-api.net/dialogporten"
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using FluentValidation.Results;

namespace Digdir.Domain.Dialogporten.Application.Common.ReturnTypes;

public sealed record Conflict(string Message)
{
private const string ConflictMessage = "Conflict";
public List<ValidationFailure> ToValidationResults() => [new(ConflictMessage, Message)];
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public interface IDialogDbContext
DbSet<LabelAssignmentLog> LabelAssignmentLogs { get; }
DbSet<ResourcePolicyInformation> ResourcePolicyInformation { get; }

DbSet<IdempotentId> IdempotentIds { get; }
MagnusSandgren marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Validate a property on the <typeparamref name="TEntity"/> using a lambda
/// expression to specify the predicate only when the property is modified.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions;
using Digdir.Domain.Dialogporten.Domain.Parties;
using Digdir.Library.Entity.Abstractions.Features.Identifiable;
using FluentValidation.Results;
using MediatR;
using OneOf;
using OneOf.Types;
Expand All @@ -22,7 +23,7 @@ namespace Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialog
public sealed class CreateDialogCommand : CreateDialogDto, IRequest<CreateDialogResult>;

[GenerateOneOf]
public sealed partial class CreateDialogResult : OneOfBase<Success<Guid>, DomainError, ValidationError, Forbidden>;
public sealed partial class CreateDialogResult : OneOfBase<Success<Guid>, DomainError, ValidationError, Forbidden, Conflict>;
elsand marked this conversation as resolved.
Show resolved Hide resolved

internal sealed class CreateDialogCommandHandler : IRequestHandler<CreateDialogCommand, CreateDialogResult>
{
Expand Down Expand Up @@ -73,6 +74,17 @@ public async Task<CreateDialogResult> Handle(CreateDialogCommand request, Cancel
{
dialog.Org = serviceResourceInformation.OwnOrgShortName;
}
if (request.IdempotentId is not null && !string.IsNullOrEmpty(dialog.Org))
{
var dialogIdempotentId = new IdempotentId(dialog.Org, request.IdempotentId);
var dialogQuery = _db.Dialogs.Select(x => x).Where(x => x.IdempotentId == dialogIdempotentId);
MagnusSandgren marked this conversation as resolved.
Show resolved Hide resolved
if (dialogQuery.Any())
{
return new Conflict($"IdempotencyId: '{request.IdempotentId}' already exists with DialogId '{dialogQuery.First().Id}'");
// return new Conflict(dialogQuery.First().Id.ToString());
Fargekritt marked this conversation as resolved.
Show resolved Hide resolved
}
dialog.IdempotentId = dialogIdempotentId;
}

CreateDialogEndUserContext(request, dialog);
await EnsureNoExistingUserDefinedIds(dialog, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ public CreateDialogCommandValidator(
.WithMessage($"'{{PropertyName}}' must be greater than or equal to '{nameof(CreateDialogCommand.CreatedAt)}'.")
.When(x => x.CreatedAt != default && x.UpdatedAt != default);

RuleFor(x => x.IdempotentId)
.MaximumLength(36)
.WithMessage("'{{PropertyName}}' can't be longer than 36 characters.");

RuleFor(x => x.ServiceResource)
.NotNull()
.IsValidUri()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ public class CreateDialogDto
/// <example>01913cd5-784f-7d3b-abef-4c77b1f0972d</example>
public Guid? Id { get; set; }

/// <summary>
/// A self-defined Id may be provided to support idempotent creation of dialogs.
/// </summary>
public string? IdempotentId { get; set; }

/// <summary>
/// The service identifier for the service that the dialog is related to in URN-format.
/// This corresponds to a resource in the Altinn Resource Registry, which the authenticated organization
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ public MappingProfile()
{
CreateMap<CreateDialogDto, DialogEntity>()
.ForMember(dest => dest.Status, opt => opt.Ignore())
.ForMember(dest => dest.StatusId, opt => opt.MapFrom(src => src.Status));
.ForMember(dest => dest.StatusId, opt => opt.MapFrom(src => src.Status))
.ForMember(dest => dest.IdempotentId, opt => opt.Ignore());
CreateMap<SearchTagDto, DialogSearchTag>();

CreateMap<AttachmentDto, DialogAttachment>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public async Task<PurgeDialogResult> Handle(PurgeDialogCommand request, Cancella
var dialog = await _db.Dialogs
.Include(x => x.Attachments)
.Include(x => x.Activities)
.Include(x => x.IdempotentId)
.WhereIf(!_userResourceRegistry.IsCurrentUserServiceOwnerAdmin(), x => resourceIds.Contains(x.ServiceResource))
.IgnoreQueryFilters()
.FirstOrDefaultAsync(x => x.Id == request.DialogId, cancellationToken);
Expand All @@ -56,7 +57,10 @@ public async Task<PurgeDialogResult> Handle(PurgeDialogCommand request, Cancella
{
return new Forbidden($"User cannot modify resource type {dialog.ServiceResourceType}.");
}

if (dialog.IdempotentId is not null)
{
_db.IdempotentIds.Remove(dialog.IdempotentId);
}
_db.Dialogs.HardRemove(dialog);
var saveResult = await _unitOfWork
.EnableConcurrencyCheck(dialog, request.IfMatchDialogRevision)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ public sealed class DialogDto
/// <example>01913cd5-784f-7d3b-abef-4c77b1f0972d</example>
public Guid Id { get; set; }

/// <summary>
/// A self-defined Id may be provided to support idempotent creation of dialogs.
/// </summary>
public string? IdempotentId { get; set; }

/// <summary>
/// The unique identifier for the revision in UUIDv4 format.
/// </summary>
Expand Down Expand Up @@ -270,7 +275,6 @@ public sealed class DialogSeenLogDto
public bool IsCurrentEndUser { get; set; }
}


public sealed class ContentDto
{
/// <summary>
Expand Down Expand Up @@ -372,7 +376,6 @@ public sealed class DialogActivityDto
public List<LocalizationDto> Description { get; set; } = [];
}


public sealed class DialogApiActionDto
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public async Task<GetDialogResult> Handle(GetDialogQuery request, CancellationTo
var dialog = await _db.Dialogs
.Include(x => x.Content.OrderBy(x => x.Id).ThenBy(x => x.CreatedAt))
.ThenInclude(x => x.Value.Localizations.OrderBy(x => x.CreatedAt).ThenBy(x => x.LanguageCode))
.Include(x => x.IdempotentId)
.Include(x => x.SearchTags.OrderBy(x => x.CreatedAt).ThenBy(x => x.Id))
.Include(x => x.Attachments.OrderBy(x => x.CreatedAt).ThenBy(x => x.Id))
.ThenInclude(x => x.DisplayName!.Localizations.OrderBy(x => x.CreatedAt).ThenBy(x => x.LanguageCode))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public MappingProfile()
CreateMap<DialogEntity, DialogDto>()
.ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.StatusId))
.ForMember(dest => dest.SeenSinceLastUpdate, opt => opt.Ignore())
.ForMember(dest => dest.SystemLabel, opt => opt.MapFrom(src => src.DialogEndUserContext.SystemLabelId));
.ForMember(dest => dest.SystemLabel, opt => opt.MapFrom(src => src.DialogEndUserContext.SystemLabelId))
.ForMember(dest => dest.IdempotentId, opt => opt.MapFrom(src => src.IdempotentId != null ? src.IdempotentId.Idempotent : null));

CreateMap<DialogSeenLogSeenByActor, ActorDto>();
CreateMap<DialogSeenLog, DialogSeenLogDto>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ public class DialogDtoBase
/// <example>01913cd5-784f-7d3b-abef-4c77b1f0972d</example>
public Guid Id { get; set; }

/// <summary>
/// A self-defined Id may be provided to support idempotent creation of dialogs.
/// </summary>
public string? IdempotentId { get; set; }

/// <summary>
/// The service owner code representing the organization (service owner) related to this dialog.
/// </summary>
Expand Down Expand Up @@ -158,7 +163,6 @@ public sealed class DialogSeenLogDto
public bool IsCurrentEndUser { get; set; }
}


public sealed class DialogActivityDto
{
/// <summary>
Expand Down Expand Up @@ -198,4 +202,3 @@ public sealed class DialogActivityDto
/// </summary>
public List<LocalizationDto> Description { get; set; } = [];
}

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public MappingProfile()
// See IntermediateSearchDialogDto
CreateMap<IntermediateDialogDto, DialogDto>();
CreateMap<DialogEntity, IntermediateDialogDto>()
.ForMember(dest => dest.IdempotentId, opt => opt.MapFrom(src => src.IdempotentId != null ? src.IdempotentId.Idempotent : null))
.ForMember(dest => dest.LatestActivity, opt => opt.MapFrom(src => src.Activities
.OrderByDescending(activity => activity.CreatedAt).ThenByDescending(activity => activity.Id)
.FirstOrDefault()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public sealed class SearchDialogQuery : SortablePaginationParameter<SearchDialog
{
private string? _searchLanguageCode;

/// <summary>
/// A self-defined Id may be provided to support idempotent creation of dialogs.
Fargekritt marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
public string? IdempotentId { get; set; }
/// <summary>
/// Filter by one or more service resources
/// </summary>
Expand Down Expand Up @@ -169,6 +173,7 @@ public async Task<SearchDialogResult> Handle(SearchDialogQuery request, Cancella
var paginatedList = await dialogQuery
.Include(x => x.Content)
.ThenInclude(x => x.Value.Localizations)
.WhereIf(request.IdempotentId is not null, x => x.IdempotentId != null && x.IdempotentId.Idempotent == request.IdempotentId)
.WhereIf(!request.ServiceResource.IsNullOrEmpty(),
x => request.ServiceResource!.Contains(x.ServiceResource))
.WhereIf(!request.Party.IsNullOrEmpty(), x => request.Party!.Contains(x.Party))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ public SearchDialogQueryValidator()
.MinimumLength(3)
.When(x => x.Search is not null);

RuleFor(x => x.IdempotentId)
.MaximumLength(36)
.WithMessage("'{{PropertyName}}' can't be longer than 36 characters.");
Fargekritt marked this conversation as resolved.
Show resolved Hide resolved

RuleFor(x => x.SearchLanguageCode)
.Must(x => x is null || Localization.IsValidCultureCode(x))
.WithMessage(searchQuery =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public sealed class DialogEntity :

public string? PrecedingProcess { get; set; }

public IdempotentId? IdempotentId { get; set; }

// === Dependent relationships ===
public DialogStatus.Values StatusId { get; set; }
Expand Down
Fargekritt marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Digdir.Domain.Dialogporten.Domain.Dialogs.Entities;

public sealed class IdempotentId(string org, string idempotent)
{
public string Org { set; get; } = org;
public string Idempotent { set; get; } = idempotent;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities;
using Digdir.Domain.Dialogporten.GraphQL.EndUser.Common;
using Digdir.Domain.Dialogporten.GraphQL.EndUser.MutationTypes;
using DialogStatus = Digdir.Domain.Dialogporten.GraphQL.EndUser.Common.DialogStatus;
Fargekritt marked this conversation as resolved.
Show resolved Hide resolved

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

Expand Down Expand Up @@ -33,6 +35,7 @@ public sealed class DialogByIdPayload
public sealed class Dialog
{
public Guid Id { get; set; }
public IdempotentId? IdempotentId { get; set; }
public Guid Revision { get; set; }
public string Org { get; set; } = null!;
public string ServiceResource { get; set; } = null!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public DialogDbContext(DbContextOptions<DialogDbContext> options) : base(options
public DbSet<LabelAssignmentLog> LabelAssignmentLogs => Set<LabelAssignmentLog>();
public DbSet<NotificationAcknowledgement> NotificationAcknowledgements => Set<NotificationAcknowledgement>();
public DbSet<ResourcePolicyInformation> ResourcePolicyInformation => Set<ResourcePolicyInformation>();
public DbSet<IdempotentId> IdempotentIds => Set<IdempotentId>();

//protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
// optionsBuilder.LogTo(Console.WriteLine);
Expand Down Expand Up @@ -110,6 +111,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
// Explicitly configure the Actor entity so that it will register as TPH in the database
modelBuilder.Entity<Actor>();

modelBuilder.Entity<IdempotentId>().HasKey(x => new
{
x.Idempotent,
x.Org
});

modelBuilder
.RemovePluralizingTableNameConvention()
.AddAuditableEntities()
Expand Down
Loading
Loading