Skip to content

Commit

Permalink
chore: Graphql error handling (#690)
Browse files Browse the repository at this point in the history
## 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 <knut.espen.haug@digdir.no>
  • Loading branch information
oskogstad and knuhau authored May 3, 2024
1 parent cd8cfeb commit 66fc0f2
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 27 deletions.
2 changes: 1 addition & 1 deletion docs/schema/V1/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
38 changes: 36 additions & 2 deletions docs/schema/V1/schema.verified.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
query: Queries
}

interface DialogByIdError {
message: String!
}

interface SearchDialogError {
message: String!
}

type Activity {
id: UUID!
createdAt: DateTime
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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!]!
}
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public EntityNotFound(Guid key) : this(new object[] { key }) { }

public record EntityNotFound(string Name, IEnumerable<object> 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<Guid> keys) : this(name, keys.Cast<object>()) { }
public EntityNotFound(string name, Guid key) : this(name, new object[] { key }) { }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IDialogByIdError> Errors { get; set; } = [];
}

public sealed class Dialog
{
public Guid Id { get; set; }
Expand Down
34 changes: 13 additions & 21 deletions src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogQueries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,19 @@ namespace Digdir.Domain.Dialogporten.GraphQL.EndUser;

public partial class Queries
{
public async Task<Dialog> GetDialogById(
public async Task<DialogByIdPayload> GetDialogById(
[Service] ISender mediator,
[Service] IMapper mapper,
[Argument] Guid dialogId,
CancellationToken cancellationToken)
{
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<Dialog>(getDialogResult);

return dialog;
return result.Match(
dialog => new DialogByIdPayload { Dialog = mapper.Map<Dialog>(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<SearchDialogsPayload> SearchDialogs(
Expand All @@ -35,19 +30,16 @@ public async Task<SearchDialogsPayload> SearchDialogs(
SearchDialogInput input,
CancellationToken cancellationToken)
{

var searchDialogQuery = mapper.Map<SearchDialogQuery>(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<SearchDialogsPayload>(searchResultOneOf);

return dialogSearchResult;
return result.Match(
mapper.Map<SearchDialogsPayload>,
validationError => new SearchDialogsPayload
{
Errors = [.. validationError.Errors.Select(x => new SearchDialogValidationError { Message = x.ErrorMessage })]
},
forbidden => new SearchDialogsPayload { Errors = [new SearchDialogForbidden()] });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public MappingProfile()
CreateMap<SearchDialogInput, SearchDialogQuery>()
.ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.Status));

CreateMap<PaginatedList<SearchDialogDto>, SearchDialogsPayload>();
CreateMap<PaginatedList<SearchDialogDto>, SearchDialogsPayload>()
.ForMember(dest => dest.Items, opt => opt.MapFrom(src => src.Items));

CreateMap<SearchDialogDto, SearchDialog>();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<SearchDialog> Items { get; } = [];
public List<SearchDialog>? Items { get; set; }
public bool HasNextPage { get; }
public string? ContinuationToken { get; }
public string OrderBy { get; } = null!;
public List<ISearchDialogError> Errors { get; set; } = [];
}

public sealed class SearchDialog
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,6 +16,11 @@ public static IServiceCollection AddDialogportenGraphQl(
.RegisterDbContext<DialogDbContext>()
.AddDiagnosticEventListener<ApplicationInsightEventListener>()
.AddQueryType<Queries>()
.AddType<DialogByIdDeleted>()
.AddType<DialogByIdNotFound>()
.AddType<DialogByIdForbidden>()
.AddType<SearchDialogValidationError>()
.AddType<SearchDialogForbidden>()
.Services;
}
}

0 comments on commit 66fc0f2

Please sign in to comment.