ethelo / kronky Goto Github PK
View Code? Open in Web Editor NEWKronky bridges the gap between Ecto and Absinthe GraphQL by listing validation messages in a mutation payload.
License: MIT License
Kronky bridges the gap between Ecto and Absinthe GraphQL by listing validation messages in a mutation payload.
License: MIT License
Here is the stack trace:
Request: POST /api/graphiql
** (exit) an exception was raised:
** (BadMapError) expected a map, got: {:constraint, :unique}
(elixir) lib/map.ex:437: Map.get({:constraint, :unique}, :key, nil)
(absinthe) lib/absinthe/middleware/map_get.ex:9: Absinthe.Middleware.MapGet.call/2
(absinthe) lib/absinthe/phase/document/execution/resolution.ex:209: Absinthe.Phase.Document.Execution.Resolution.reduce_resolution/1
(absinthe) lib/absinthe/phase/document/execution/resolution.ex:168: Absinthe.Phase.Document.Execution.Resolution.do_resolve_field/4
(absinthe) lib/absinthe/phase/document/execution/resolution.ex:153: Absinthe.Phase.Document.Execution.Resolution.do_resolve_fields/6
(absinthe) lib/absinthe/phase/document/execution/resolution.ex:72: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
(absinthe) lib/absinthe/phase/document/execution/resolution.ex:98: Absinthe.Phase.Document.Execution.Resolution.walk_results/6
(absinthe) lib/absinthe/phase/document/execution/resolution.ex:87: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
(absinthe) lib/absinthe/phase/document/execution/resolution.ex:257: Absinthe.Phase.Document.Execution.Resolution.build_result/4
(absinthe) lib/absinthe/phase/document/execution/resolution.ex:153: Absinthe.Phase.Document.Execution.Resolution.do_resolve_fields/6
(absinthe) lib/absinthe/phase/document/execution/resolution.ex:72: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
(absinthe) lib/absinthe/phase/document/execution/resolution.ex:98: Absinthe.Phase.Document.Execution.Resolution.walk_results/6
(absinthe) lib/absinthe/phase/document/execution/resolution.ex:87: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
(absinthe) lib/absinthe/phase/document/execution/resolution.ex:257: Absinthe.Phase.Document.Execution.Resolution.build_result/4
(absinthe) lib/absinthe/phase/document/execution/resolution.ex:153: Absinthe.Phase.Document.Execution.Resolution.do_resolve_fields/6
(absinthe) lib/absinthe/phase/document/execution/resolution.ex:72: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
(absinthe) lib/absinthe/phase/document/execution/resolution.ex:257: Absinthe.Phase.Document.Execution.Resolution.build_result/4
(absinthe) lib/absinthe/phase/document/execution/resolution.ex:153: Absinthe.Phase.Document.Execution.Resolution.do_resolve_fields/6
(absinthe) lib/absinthe/phase/document/execution/resolution.ex:72: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
(absinthe) lib/absinthe/phase/document/execution/resolution.ex:53: Absinthe.Phase.Document.Execution.Resolution.perform_resolution/3
** (EXIT from #PID<0.448.0>) shell process exited with reason: an exception was raised:
** (ErlangError) Erlang error: {{%BadMapError{term: {:constraint, :unique}}, [{Map, :get, [{:constraint, :unique}, :key, nil], [file: 'lib/map.ex', line: 437]}, {Absinthe.Middleware.MapGet, :call, 2, [file: 'lib/absinthe/middleware/map_get.ex', line: 9]}, {Absinthe.Phase.Document.Execution.Resolution, :reduce_resolution, 1, [file: 'lib/absinthe/phase/document/execution/resolution.ex', line: 209]}, {Absinthe.Phase.Document.Execution.Resolution, :do_resolve_field, 4, [file: 'lib/absinthe/phase/document/execution/resolution.ex', line: 168]}, {Absinthe.Phase.Document.Execution.Resolution, :do_resolve_fields, 6, [file: 'lib/absinthe/phase/document/execution/resolution.ex', line: 153]}, {Absinthe.Phase.Document.Execution.Resolution, :walk_result, 5, [file: 'lib/absinthe/phase/document/execution/resolution.ex', line: 72]}, {Absinthe.Phase.Document.Execution.Resolution, :walk_results, 6, [file: 'lib/absinthe/phase/document/execution/resolution.ex', line: 98]}, {Absinthe.Phase.Document.Execution.Resolution, :walk_result, 5, [file: 'lib/absinthe/phase/document/execution/resolution.ex', line: 87]}, {Absinthe.Phase.Document.Execution.Resolution, :build_result, 4, [file: 'lib/absinthe/phase/document/execution/resolution.ex', line: 257]}, {Absinthe.Phase.Document.Execution.Resolution, :do_resolve_fields, 6, [file: 'lib/absinthe/phase/document/execution/resolution.ex', line: 153]}, {Absinthe.Phase.Document.Execution.Resolution, :walk_result, 5, [file: 'lib/absinthe/phase/document/execution/resolution.ex', line: 72]}, {Absinthe.Phase.Document.Execution.Resolution, :walk_results, 6, [file: 'lib/absinthe/phase/document/execution/resolution.ex', line: 98]}, {Absinthe.Phase.Document.Execution.Resolution, :walk_result, 5, [file: 'lib/absinthe/phase/document/execution/resolution.ex', line: 87]}, {Absinthe.Phase.Document.Execution.Resolution, :build_result, 4, [file: 'lib/absinthe/phase/document/execution/resolution.ex', line: 257]}, {Absinthe.Phase.Document.Execution.Resolution, :do_resolve_fields, 6, [file: 'lib/absinthe/phase/document/execution/resolution.ex', line: 153]}, {Absinthe.Phase.Document.Execution.Resolution, :walk_result, 5, [file: 'lib/absinthe/phase/document/execution/resolution.ex', line: 72]}, {Absinthe.Phase.Document.Execution.Resolution, :build_result, 4, [file: 'lib/absinthe/phase/document/execution/resolution.ex', line: 257]}, {Absinthe.Phase.Document.Execution.Resolution, :do_resolve_fields, 6, [file: 'lib/absinthe/phase/document/execution/resolution.ex', line: 153]}, {Absinthe.Phase.Document.Execution.Resolution, :walk_result, 5, [file: 'lib/absinthe/phase/document/execution/resolution.ex', line: 72]}, {Absinthe.Phase.Document.Execution.Resolution, :perform_resolution, 3, [file: 'lib/absinthe/phase/document/execution/resolution.ex', line: 53]}]}, {MyAppWeb.Endpoint, :call, [%Plug.Conn{adapter: {Plug.Cowboy.Conn, :...}, assigns: %{}, before_send: [], body_params: %Plug.Conn.Unfetched{aspect: :body_params}, cookies: %Plug.Conn.Unfetched{aspect: :cookies}, halted: false, host: "localhost", method: "POST", owner: #PID<0.448.0>, params: %Plug.Conn.Unfetched{aspect: :params}, path_info: ["api", "graphiql"], path_params: %{}, port: 4000, private: %{}, query_params: %Plug.Conn.Unfetched{aspect: :query_params}, query_string: "", remote_ip: {127, 0, 0, 1}, req_cookies: %Plug.Conn.Unfetched{aspect: :cookies}, req_headers: [{"accept", "application/json"}, {"accept-encoding", "gzip, deflate, br"}, {"accept-language", "en-US,en;q=0.9"}, {"connection", "keep-alive"}, {"content-length", "583"}, {"content-type", "application/json"}, {"host", "localhost:4000"}, {"origin", "http://localhost:4000"}, {"referer", "http://localhost:4000/api/graphiql"}, {"user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.108 Safari/537.36"}], request_path: "/api/graphiql", resp_body: nil, resp_cookies: %{}, resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}], scheme: :http, script_name: [], secret_key_base: nil, state: :unset, status: nil}, []]}}
(phoenix) lib/phoenix/endpoint/cowboy2_handler.ex:42: Phoenix.Endpoint.Cowboy2Handler.init/2
(cowboy) /Users/terence/Workspace/myapp/deps/cowboy/src/cowboy_handler.erl:41: :cowboy_handler.execute/2
(cowboy) /Users/terence/Workspace/myapp/deps/cowboy/src/cowboy_stream_h.erl:296: :cowboy_stream_h.execute/3
(cowboy) /Users/terence/Workspace/myapp/deps/cowboy/src/cowboy_stream_h.erl:274: :cowboy_stream_h.request_process/3
(stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
I was able to make this package work properly on all Ecto.Changeset
validations, but for some reason, it keeps getting this exception when it fails the unique constraint check.
Here is the schema:
defmodule MyApp.Authentication.User do
@moduledoc """
User schema
"""
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, :string
field :first_name, :string
field :last_name, :string
field :password, :string, virtual: true
field :password_hash, :string
timestamps()
end
@doc false
def registration_changeset(user, attrs) do
user
|> cast(attrs, [:email, :password, :first_name, :last_name])
|> validate_required([:email, :password])
|> validate_format(:email, ~r/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/)
|> unique_constraint(:email)
|> validate_length(:password, min: 8, max: 30)
|> validate_confirmation(:password)
|> hash_password()
end
defp hash_password(%Ecto.Changeset{valid?: true, changes: %{password: pass}} = changeset) do
put_change(changeset, :password_hash, Pbkdf2.hash_pwd_salt(pass))
end
defp hash_password(changeset), do: changeset
end
This is the schema:
defmodule MyAppWeb.Schema.AuthenticationTypes do
use Absinthe.Schema.Notation
import Kronky.Payload
alias MyAppWeb.Resolvers.Authentication
object :user, description: "A user" do
field :id, :id, description: "Unique identifier"
field :email, :string, description: "User's email address"
field :first_name, :string, description: "User's first name"
field :last_name, :string, description: "User's last name"
field :inserted_at, :naive_datetime, description: "Date and time when user was created"
end
object :session, description: "Details for the user's session" do
field :authentication_token, :string, description: "Unique session token to be used for subsequent requests"
field :user, :user, description: "User payload"
end
input_object :user_sign_up_input, description: "Parameters for user registration" do
field :email, non_null(:string), description: "Email address of the user"
field :password, non_null(:string), description: "Hard password that should be at least 8 characters"
field :password_confirmation, non_null(:string), description: "Repeat the password to ensure correctness"
field :first_name, :string, description: "Optional first name"
field :last_name, :string, description: "Optional last name"
end
payload_object(:session_payload, :session)
object :authentication_mutations do
field :sign_up, :session_payload, description: "Registers a new user" do
arg :input, non_null(:user_sign_up_input)
resolve &Authentication.sign_up/3
middleware &build_payload/2
end
end
end
This is the resolver:
defmodule MyAppWeb.Resolvers.Authentication do
alias MyApp.Authentication
alias MyApp.Authentication.User
alias MyApp.Guardian
def sign_up(_parent, args, _resolution) do
case Authentication.create_user(args[:input]) do
{:ok, %User{} = user} ->
{:ok, token, _claims} = Guardian.encode_and_sign(user)
{:ok, %{authentication_token: token, user: user}}
{:error, %Ecto.Changeset{} = changeset} ->
{:ok, changeset}
end
end
end
Am I supposed to be doing something to not get that error on a unique constraint or is this something that's not addresses by this package?
Hello,
I am having trouble using the error_payload/1 function in my resolvers. The error is a match error when I pass an Ecto.Changeset as argument. I am willing to work on this and submit a PR, but, I wanted to open a discussion about the approach. Should I update the docs or should I implement the case function for Ecto.Changeset?
The line that says that error_payload can take an Ecto.Changeset: here
The implementation: here
I don't know if it is intended but :error_payload returns also the template to the client
def create(arg1, arg2, arg3) do
{:ok, error_payload([%ValidationMessage{field: :email,
code: "not found",
template: "Something horrible has happened",
message: "does not exist"}])}
end
returns this result
{
"data": {
"createUser": {
"successful": false,
"result": null,
"messages": [
{
"template": "Something horrible has happened",
"message": "does not exist",
"field": "email",
"code": "not found"
}
]
}
}
}
What is the purpose of having both template and message and returning them both? Is there any way preventing :template from being available to the client?
From the docs:
I'm trying to make impossible states impossible, and I don't seem to be able to.
null
, then what's the point of the successful
fields?(ValidiationMessage | null)[] | null
. The docs says that it's empty when there are no errors - but it can also be null
. Which on is it?null
. What does that mean?Generally speaking, why not make the return type impossible to represent something impossible?
ValidiationMessage[]
or ValidiationMessage[] | null
. Having individual null
messages is just both confusing and adds lots of uncertainty about the meaning of such cases.result
will always be null
when unsuccessful
is a mistake. Sometimes a user posting a form can be partially successful. Having a result with partial changes applied and a list of errors models this already. I think there should be no success flag at all - extracting success from a payload should be as easy success = () => messages || message.length == 0 || !result
.In our case, having to handle these cases has shown to be very laborious, and particularly misleading for the frontend team, especially when using more strong typed languages that aim at eliminating runtime errors (Elm, TS, etc). The fact that an impossible state is made possible makes the frontend developer have to address it, regardless of how absurd it is.
Maybe a bit of love on the base Payload
concept would be great before too much adoption?
I tried to use Jason instead of Poison
(Protocol.UndefinedError) protocol Jason.Encoder not implemented for %Kronky.Payload{messages: [%Kronky.ValidationMessage{code: :unknown, field: "base", key: "base", message: "You must login or register to continue.", options: [], template: "You must login or register to continue."}], result: nil, successful: false}, Jason.Encoder protocol must always be explicitly implemented.
If you own the struct, you can derive the implementation specifying which fields should be encoded to JSON:
@derive {Jason.Encoder, only: [....]}
defstruct ...
It is also possible to encode all fields, although this should be used carefully to avoid accidentally leaking private information when new fields are added:
@derive Jason.Encoder
defstruct ...
Finally, if you don't own the struct you want to encode to JSON, you may use Protocol.derive/3 placed outside of any module:
...
Hi,
In order to support Absinthe.Resolution errors from Middleware in build_payload/2 like in Absinthe documentation:
defmodule MyApp.Web.Authentication do
@behaviour Absinthe.Middleware
def call(resolution, _config) do
case resolution.context do
%{current_user: _} ->
resolution
_ ->
resolution
|> Absinthe.Resolution.put_result({:error, "unauthenticated"})
end
end
end
We could change Kronky.Payload.build_payload
signature for:
def build_payload(%{value: value, errors: []} = resolution, _config) do
result = convert_to_payload(value)
Absinthe.Resolution.put_result(resolution, {:ok, result})
end
and add this function:
def build_payload(%{errors: errors} = resolution, _config) do
result = convert_to_payload({:error, errors})
Absinthe.Resolution.put_result(resolution, {:ok, result})
end
This way, we won't have to return an ok
tuple in custom Middleware and it will be compatible with third party Middleware.
Here are the tests:
test "error from resolution, validation message" do
message = %ValidationMessage{code: :required}
resolution = resolution_error([message])
result = build_payload(resolution, nil)
assert_error_payload([message], result)
end
test "error from resolution, string message" do
resolution = resolution_error(["an error"])
result = build_payload(resolution, nil)
message = %ValidationMessage{code: :unknown, message: "an error", template: "an error"}
assert_error_payload([message], result)
end
test "error from resolution, string message list" do
resolution = resolution_error(["an error", "another error"])
result = build_payload(resolution, nil)
messages = [
%ValidationMessage{code: :unknown, message: "an error", template: "an error"},
%ValidationMessage{code: :unknown, message: "another error", template: "another error"}
]
assert_error_payload(messages, result)
end
test "error from resolution, error list" do
messages = [%ValidationMessage{code: :required}, %ValidationMessage{code: :max}]
resolution = resolution_error(messages)
result = build_payload(resolution, nil)
assert %{value: value} = result
assert_error_payload(messages, result)
end
What do you think about it ?
I will do a Pull Request
Has anyone tried if this is compatible with ecto 3.0?
Hi there,
I have a mutation which returns a boolean ( true or false). This is my mutation:
field :revoke_token, :boolean do
resolve(fn _, %{context: context} ->
context[:current_user]
|> Accounts.signout()
{:ok, true}
end)
and this is how I am calling it in react.
<RevokeTokenMutation mutation={REVOKE_TOKEN}>
{(revokeToken, { client }) => (
<a className="navbar-item" href="#logout" onClick={this.logout(revokeToken, client)}>
Sign out
</a>
)}
</RevokeTokenMutation>
and logout function is
private logout(revokeToken: Function, client: ApolloClient<any>) {
return async (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
const response: MutationResult<RevokeTokenData> = await revokeToken();
console.log("Response " + response);
const errors = response.data!.revokeToken.errors;
if (!errors) {
window.localStorage.removeItem('yummy:token');
await client.resetStore();
this.props.redirect('/', { notice: 'You are well disconnected' });
}
};
}
I am unable to get past the error. Can I not use boolean type?
Hi,
I have this validation rule in my model :
validate_length(:content, min: 30)
I have a error if options { key, value}
is present in my mutation query :
mutation { createRecipe(title: "a title", content: "less than 30") { result { id, title }, messages { template, options { key, value } } } }
The error is :
** (exit) an exception was raised: ** (BadMapError) expected a map, got: {:count, 30} (elixir) lib/map.ex:424: Map.get({:count, 30}, :key, nil) (absinthe) lib/absinthe/middleware/map_get.ex:9: Absinthe.Middleware.MapGet.call/2
I think there is a type error with options
resolution. Do you have an idea ?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.