kronky icon indicating copy to clipboard operation
kronky copied to clipboard

Getting BadMapError when Ecto.Changeset error is a unique constraint

Open terenceponce opened this issue 6 years ago • 0 comments

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?

terenceponce avatar May 26 '19 07:05 terenceponce