From 66fc0f21c36c0544bd85cff5bcc12b9531c6a50b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ole=20J=C3=B8rgen=20Skogstad?= Date: Fri, 3 May 2024 09:43:49 +0200 Subject: [PATCH] chore: Graphql error handling (#690) ## Description Adds typed errors to the GraphQl schema ## Related Issue(s) - #490 ## Verification - [x] **Your** code builds clean without any errors or warnings - [x] Manual testing done (required) - [x] Relevant automated test added (if you find this hard, leave it and we'll help out) ## Documentation - [ ] Documentation is updated (either in `docs`-directory, Altinnpedia or a separate linked PR in [altinn-studio-docs.](https://github.com/Altinn/altinn-studio-docs), if applicable) --------- Co-authored-by: Knut Haug --- docs/schema/V1/package.json | 2 +- docs/schema/V1/schema.verified.graphql | 38 ++++++++++++++++++- .../Common/ReturnTypes/EntityNotFound.cs | 2 +- .../EndUser/DialogById/ObjectTypes.cs | 27 +++++++++++++ .../EndUser/DialogQueries.cs | 34 +++++++---------- .../EndUser/SearchDialogs/MappingProfile.cs | 3 +- .../EndUser/SearchDialogs/ObjectTypes.cs | 19 +++++++++- .../ServiceCollectionExtensions.cs | 7 ++++ 8 files changed, 105 insertions(+), 27 deletions(-) diff --git a/docs/schema/V1/package.json b/docs/schema/V1/package.json index 79a99818b..5c32cd963 100644 --- a/docs/schema/V1/package.json +++ b/docs/schema/V1/package.json @@ -1,6 +1,6 @@ { "name": "@digdir/dialogporten-schema", - "version": "1.0.7", + "version": "1.0.9", "description": "GraphQl schema and OpenAPI spec for Dialogporten", "author": "DigDir", "repository": { diff --git a/docs/schema/V1/schema.verified.graphql b/docs/schema/V1/schema.verified.graphql index 8daef535c..5b3fdb670 100644 --- a/docs/schema/V1/schema.verified.graphql +++ b/docs/schema/V1/schema.verified.graphql @@ -2,6 +2,14 @@ query: Queries } +interface DialogByIdError { + message: String! +} + +interface SearchDialogError { + message: String! +} + type Activity { id: UUID! createdAt: DateTime @@ -75,6 +83,23 @@ type Dialog { seenSinceLastUpdate: [SeenLog!]! } +type DialogByIdDeleted implements DialogByIdError { + message: String! +} + +type DialogByIdForbidden implements DialogByIdError { + message: String! +} + +type DialogByIdNotFound implements DialogByIdError { + message: String! +} + +type DialogByIdPayload { + dialog: Dialog + errors: [DialogByIdError!]! +} + type Element { id: UUID! type: URL @@ -111,7 +136,7 @@ type Localization { } type Queries @authorize(policy: "enduser") { - dialogById(dialogId: UUID!): Dialog! + dialogById(dialogId: UUID!): DialogByIdPayload! searchDialogs(input: SearchDialogInput!): SearchDialogsPayload! parties: [AuthorizedParty!]! } @@ -133,11 +158,20 @@ type SearchDialog { seenSinceLastUpdate: [SeenLog!]! } +type SearchDialogForbidden implements SearchDialogError { + message: String! +} + +type SearchDialogValidationError implements SearchDialogError { + message: String! +} + type SearchDialogsPayload { - items: [SearchDialog!]! + items: [SearchDialog!] hasNextPage: Boolean! continuationToken: String orderBy: String! + errors: [SearchDialogError!]! } type SeenLog { diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/ReturnTypes/EntityNotFound.cs b/src/Digdir.Domain.Dialogporten.Application/Common/ReturnTypes/EntityNotFound.cs index 66cbbc6fd..349afadbc 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Common/ReturnTypes/EntityNotFound.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Common/ReturnTypes/EntityNotFound.cs @@ -10,7 +10,7 @@ public EntityNotFound(Guid key) : this(new object[] { key }) { } public record EntityNotFound(string Name, IEnumerable Keys) { - private string Message => $"Entity '{Name}' with the following key(s) was not found: ({string.Join(", ", Keys)})."; + public string Message => $"Entity '{Name}' with the following key(s) was not found: ({string.Join(", ", Keys)})."; public EntityNotFound(string name, IEnumerable keys) : this(name, keys.Cast()) { } public EntityNotFound(string name, Guid key) : this(name, new object[] { key }) { } diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogById/ObjectTypes.cs b/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogById/ObjectTypes.cs index 46a905d1f..dc409bcf6 100644 --- a/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogById/ObjectTypes.cs +++ b/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogById/ObjectTypes.cs @@ -2,6 +2,33 @@ namespace Digdir.Domain.Dialogporten.GraphQL.EndUser.DialogById; +[InterfaceType("DialogByIdError")] +public interface IDialogByIdError +{ + public string Message { get; set; } +} + +public sealed class DialogByIdNotFound : IDialogByIdError +{ + public string Message { get; set; } = null!; +} + +public sealed class DialogByIdDeleted : IDialogByIdError +{ + public string Message { get; set; } = null!; +} + +public sealed class DialogByIdForbidden : IDialogByIdError +{ + public string Message { get; set; } = "Forbidden"; +} + +public sealed class DialogByIdPayload +{ + public Dialog? Dialog { get; set; } + public List Errors { get; set; } = []; +} + public sealed class Dialog { public Guid Id { get; set; } diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogQueries.cs b/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogQueries.cs index c645656c9..9928d4708 100644 --- a/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogQueries.cs +++ b/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogQueries.cs @@ -9,7 +9,7 @@ namespace Digdir.Domain.Dialogporten.GraphQL.EndUser; public partial class Queries { - public async Task GetDialogById( + public async Task GetDialogById( [Service] ISender mediator, [Service] IMapper mapper, [Argument] Guid dialogId, @@ -17,16 +17,11 @@ public async Task GetDialogById( { var request = new GetDialogQuery { DialogId = dialogId }; var result = await mediator.Send(request, cancellationToken); - var getDialogResult = result.Match( - dialog => dialog, - // TODO: Error handling - notFound => throw new NotImplementedException("Not found"), - deleted => throw new NotImplementedException("Deleted"), - forbidden => throw new NotImplementedException("Forbidden")); - - var dialog = mapper.Map(getDialogResult); - - return dialog; + return result.Match( + dialog => new DialogByIdPayload { Dialog = mapper.Map(dialog) }, + notFound => new DialogByIdPayload { Errors = [new DialogByIdNotFound { Message = notFound.Message }] }, + deleted => new DialogByIdPayload { Errors = [new DialogByIdDeleted { Message = deleted.Message }] }, + forbidden => new DialogByIdPayload { Errors = [new DialogByIdForbidden { Message = "Forbidden" }] }); } public async Task SearchDialogs( @@ -35,19 +30,16 @@ public async Task SearchDialogs( SearchDialogInput input, CancellationToken cancellationToken) { - var searchDialogQuery = mapper.Map(input); var result = await mediator.Send(searchDialogQuery, cancellationToken); - var searchResultOneOf = result.Match( - paginatedList => paginatedList, - // TODO: Error handling - validationError => throw new NotImplementedException("Validation error"), - forbidden => throw new NotImplementedException("Forbidden")); - - var dialogSearchResult = mapper.Map(searchResultOneOf); - - return dialogSearchResult; + return result.Match( + mapper.Map, + validationError => new SearchDialogsPayload + { + Errors = [.. validationError.Errors.Select(x => new SearchDialogValidationError { Message = x.ErrorMessage })] + }, + forbidden => new SearchDialogsPayload { Errors = [new SearchDialogForbidden()] }); } } diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/SearchDialogs/MappingProfile.cs b/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/SearchDialogs/MappingProfile.cs index 180318cfd..feccec516 100644 --- a/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/SearchDialogs/MappingProfile.cs +++ b/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/SearchDialogs/MappingProfile.cs @@ -11,7 +11,8 @@ public MappingProfile() CreateMap() .ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.Status)); - CreateMap, SearchDialogsPayload>(); + CreateMap, SearchDialogsPayload>() + .ForMember(dest => dest.Items, opt => opt.MapFrom(src => src.Items)); CreateMap(); } diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/SearchDialogs/ObjectTypes.cs b/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/SearchDialogs/ObjectTypes.cs index 1129a43dc..5691f450b 100644 --- a/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/SearchDialogs/ObjectTypes.cs +++ b/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/SearchDialogs/ObjectTypes.cs @@ -2,12 +2,29 @@ namespace Digdir.Domain.Dialogporten.GraphQL.EndUser.SearchDialogs; +[InterfaceType("SearchDialogError")] +public interface ISearchDialogError +{ + public string Message { get; set; } +} + +public sealed class SearchDialogForbidden : ISearchDialogError +{ + public string Message { get; set; } = "Forbidden"; +} + +public sealed class SearchDialogValidationError : ISearchDialogError +{ + public string Message { get; set; } = null!; +} + public sealed class SearchDialogsPayload { - public List Items { get; } = []; + public List? Items { get; set; } public bool HasNextPage { get; } public string? ContinuationToken { get; } public string OrderBy { get; } = null!; + public List Errors { get; set; } = []; } public sealed class SearchDialog diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/ServiceCollectionExtensions.cs b/src/Digdir.Domain.Dialogporten.GraphQL/ServiceCollectionExtensions.cs index 36e04829e..825745b23 100644 --- a/src/Digdir.Domain.Dialogporten.GraphQL/ServiceCollectionExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.GraphQL/ServiceCollectionExtensions.cs @@ -1,4 +1,6 @@ using Digdir.Domain.Dialogporten.GraphQL.EndUser; +using Digdir.Domain.Dialogporten.GraphQL.EndUser.DialogById; +using Digdir.Domain.Dialogporten.GraphQL.EndUser.SearchDialogs; using Digdir.Domain.Dialogporten.Infrastructure.Persistence; namespace Digdir.Domain.Dialogporten.GraphQL; @@ -14,6 +16,11 @@ public static IServiceCollection AddDialogportenGraphQl( .RegisterDbContext() .AddDiagnosticEventListener() .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() .Services; } }