-
Notifications
You must be signed in to change notification settings - Fork 0
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
Apply spec compliant error formatting #10
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,10 @@ | ||
# Absinthe Helpers | ||
|
||
This package provides two key features: | ||
This package provides three key features: | ||
|
||
1. **constraints**: enforce validation rules (like `min`, `max`, etc.) on fields and arguments in your schema. | ||
2. **transforms**: apply custom transformations (like `Trim`, `ToInteger`, etc.) to input fields and arguments. | ||
1. **constraints**: enforce validation rules (like `min`, `max`, etc.) on fields and arguments in your schema | ||
2. **transforms**: apply custom transformations (like `Trim`, `ToInteger`, etc.) to input fields and arguments | ||
3. **error formatting**: GraphQL specification compliant error formatting for validation and transform failures | ||
|
||
## Installation | ||
|
||
|
@@ -23,11 +24,11 @@ Then, run: | |
mix deps.get | ||
``` | ||
|
||
### Setup: adding constraints and transforms to your Absinthe pipeline | ||
### Setup: adding constraints, transforms, and error formatting to your Absinthe pipeline | ||
|
||
To set up both **constraints** and **transforms**, follow these steps: | ||
Follow these steps: | ||
|
||
1. Add constraints and transforms to your Absinthe pipeline: | ||
1. Add constraints, transforms, and error formatting to your Absinthe pipeline: | ||
|
||
```elixir | ||
forward "/graphql", | ||
|
@@ -42,6 +43,7 @@ def absinthe_pipeline(config, opts) do | |
|> Absinthe.Plug.default_pipeline(opts) | ||
|> AbsintheHelpers.Phases.ApplyConstraints.add_to_pipeline(opts) | ||
|> AbsintheHelpers.Phases.ApplyTransforms.add_to_pipeline(opts) | ||
|> AbsintheHelpers.Phases.ApplyErrorFormatting.add_to_pipeline(opts) | ||
end | ||
``` | ||
|
||
|
@@ -147,3 +149,55 @@ field(:create_booking, :string) do | |
resolve(&TestResolver.run/3) | ||
end | ||
``` | ||
|
||
## Error Formatting | ||
|
||
The package includes GraphQL specification compliant error formatting. When enabled, validation errors from constraints or transform failures are formatted consistently. | ||
|
||
For multiple related errors, they are grouped under a single error with BAD_USER_INPUT code: | ||
|
||
```json | ||
{ | ||
"errors": [ | ||
{ | ||
"message": "Invalid input", | ||
"extensions": { | ||
"code": "BAD_USER_INPUT", | ||
"details": { | ||
"fields": [ | ||
{ | ||
"message": "min_not_met", | ||
"path": ["description"], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the path currently includes only the field name, we'd need to figure out how to have the full path including nesting and indexing into lists here. I've seen something here: https://hexdocs.pm/absinthe/Absinthe.Resolution.html#path/1 but its seems to be available once the schema is resolved, which happens later in the pipeline. That said, happy to look into it separately again. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
the link is indeed to path 👍
unfortunately these errors cause early exit in the Absinthe pipeline, i.e. they never reach the Execution phase where the middlewares are called |
||
"details": { | ||
"min": 5 | ||
}, | ||
"locations": [ | ||
{ | ||
"line": 6, | ||
"column": 7 | ||
} | ||
], | ||
"custom_error_code": "min_not_met" | ||
}, | ||
{ | ||
"message": "max_exceeded", | ||
"path": ["title"], | ||
"details": { | ||
"max": 10 | ||
}, | ||
"locations": [ | ||
{ | ||
"line": 7, | ||
"column": 7 | ||
} | ||
], | ||
"custom_error_code": "max_exceeded" | ||
} | ||
] | ||
} | ||
}, | ||
"locations": [] | ||
} | ||
] | ||
} | ||
``` |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,69 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
defmodule AbsintheHelpers.Phases.ApplyErrorFormatting do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@moduledoc false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
alias Absinthe.{Blueprint, Phase} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
use Absinthe.Phase | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def add_to_pipeline(pipeline, opts) do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Absinthe.Pipeline.insert_after( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
pipeline, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Phase.Document.Result, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{__MODULE__, opts} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@spec run(Blueprint.t() | Phase.Error.t(), Keyword.t()) :: {:ok, Blueprint.t()} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def run(blueprint = %Blueprint{}, _options) do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{:ok, %{blueprint | result: format_errors(blueprint.result)}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
defp format_errors(%{errors: errors} = result) when is_list(errors) do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
%{result | errors: format_error_list(errors)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
defp format_errors(result), do: result | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
defp format_error_list(errors) do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
errors | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|> Enum.group_by(&get_error_code/1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|> Enum.flat_map(&format_error_group/1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
defp get_error_code(%{group_code: group_code}), do: group_code | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
defp get_error_code(_), do: nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
defp format_error_group({nil, errors}), do: errors | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
defp format_error_group({group_code, errors}) do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
[ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
%{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
message: "Invalid input", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
locations: [], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
extensions: %{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
code: group_code, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
details: %{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
fields: Enum.map(errors, &format_field/1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+29
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That should be idiomatic; you still would like to have one entry per group
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure what is intended by this suggestion, should line 36 also format the errors as a single map? otherwise in one case we return a list in another a map |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
defp format_field(%{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
message: message, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
locations: locations, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
path: path, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
code: code, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
details: details | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}) do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
%{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
message: message, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
path: path, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
custom_error_code: code, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
details: details, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
locations: locations | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
defp format_field(error), do: error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This package is internal, isn't it? Can we link to RFC?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
package is public :)