aesmail / kaffy

Powerfully simple admin package for phoenix applications
https://kaffy.fly.dev/admin/
MIT License
1.34k stars 155 forks source link

[BUG] Resource creation errors when schema contains {:array, Ecto.Enum} field ((FunctionClauseError) no function clause matching in String.length/1) #294

Closed nTraum closed 1 year ago

nTraum commented 1 year ago

Versions Used Kaffy: ab41f6456a80164ba5512db56183a63beb9c5a33 Phoenix: 1.7.7 Elixir: 1.14

This is a regression we at @hexafarms noticed when upgrading from 0.9.4 to 0.10.0-rc.2. The bug is still present on master at ab41f6456a80164ba5512db56183a63beb9c5a33.

When trying to create a record for a schema that contains an Enum array, e.g.:

schema "organization_configurations" do
  field :measurements, {:array, Ecto.Enum}, values: @measurements
  belongs_to :organization, Organization
  timestamps()
end

What's actually happening?

An error is being thrown when submitting the form for resource creation.

# FunctionClauseError at POST /kaffy/accounts/organization_configuration

Exception:

    ** (FunctionClauseError) no function clause matching in String.length/1
        (elixir 1.14.2) lib/string.ex:2017: String.length(["weight", "average_weight"])
        (kaffy 0.10.0-rc.3) lib/kaffy/resource_params.ex:10: anonymous fn/2 in Kaffy.ResourceParams.decode_map_fields/3
        (elixir 1.14.2) lib/enum.ex:1662: anonymous fn/3 in Enum.map/2
        (stdlib 4.3.1.2) maps.erl:411: :maps.fold_1/3
        (elixir 1.14.2) lib/enum.ex:2480: Enum.map/2
        (kaffy 0.10.0-rc.3) lib/kaffy/resource_params.ex:9: Kaffy.ResourceParams.decode_map_fields/3
        (kaffy 0.10.0-rc.3) lib/kaffy_web/controllers/resource_controller.ex:247: KaffyWeb.ResourceController.create/2
        (kaffy 0.10.0-rc.3) lib/kaffy_web/controllers/resource_controller.ex:1: KaffyWeb.ResourceController.action/2
        (kaffy 0.10.0-rc.3) lib/kaffy_web/controllers/resource_controller.ex:1: KaffyWeb.ResourceController.phoenix_controller_pipeline/2
        (phoenix 1.7.7) lib/phoenix/router.ex:430: Phoenix.Router.__call__/5
        (hexchange 0.1.0) lib/hexchange_web/endpoint.ex:1: HexchangeWeb.Endpoint.plug_builder_call/2
        (hexchange 0.1.0) lib/plug/debugger.ex:136: HexchangeWeb.Endpoint."call (overridable 3)"/2
        (hexchange 0.1.0) lib/hexchange_web/endpoint.ex:1: HexchangeWeb.Endpoint.call/2
        (phoenix 1.7.7) lib/phoenix/endpoint/sync_code_reload_plug.ex:22: Phoenix.Endpoint.SyncCodeReloadPlug.do_call/4
        (plug_cowboy 2.6.1) lib/plug/cowboy/handler.ex:11: Plug.Cowboy.Handler.init/2
        (cowboy 2.10.0) /home/ntraum/coding/hexafarms/hexchange/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
        (cowboy 2.10.0) /home/ntraum/coding/hexafarms/hexchange/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3
        (cowboy 2.10.0) /home/ntraum/coding/hexafarms/hexchange/deps/cowboy/src/cowboy_stream_h.erl:295: :cowboy_stream_h.request_process/3
        (stdlib 4.3.1.2) proc_lib.erl:240: :proc_lib.init_p_do_apply/3

Code:

`lib/string.ex`

    No code available.

  Called with 1 arguments

  * `["weight", "average_weight"]`

  Attempted function clauses (showing 1 out of 1)

     def length(string) when is_binary(string)

`lib/kaffy/resource_params.ex`

    5       map_fields = ResourceSchema.get_map_fields(schema) |> Enum.map(fn {f, _} -> to_string(f) end)
    6   
    7       attrs =
    8         Map.get(params, resource, %{})
    9         |> Enum.map(fn {k, v} ->
    10>          case k in map_fields && String.length(v) > 0 do
    11             true -> {k, Kaffy.Utils.json().decode!(v)}
    12             false -> {k, v}
    13           end
    14         end)
    15         |> Map.new()

`lib/enum.ex`

    No code available.

`maps.erl`

    No code available.

`lib/enum.ex`

    No code available.

`lib/kaffy/resource_params.ex`

    4     def decode_map_fields(resource, schema, params) do
    5       map_fields = ResourceSchema.get_map_fields(schema) |> Enum.map(fn {f, _} -> to_string(f) end)
    6   
    7       attrs =
    8         Map.get(params, resource, %{})
    9>        |> Enum.map(fn {k, v} ->
    10           case k in map_fields && String.length(v) > 0 do
    11             true -> {k, Kaffy.Utils.json().decode!(v)}
    12             false -> {k, v}
    13           end
    14         end)

`lib/kaffy_web/controllers/resource_controller.ex`

    242       end
    243     end
    244   
    245     def create(conn, %{"context" => context, "resource" => resource} = params) do
    246       my_resource = Kaffy.Utils.get_resource(conn, context, resource)
    247>      params = Kaffy.ResourceParams.decode_map_fields(resource, my_resource[:schema], params)
    248       changes = Map.get(params, resource, %{})
    249       resource_name = Kaffy.ResourceAdmin.singular_name(my_resource)
    250   
    251       with {:permitted, true} <- {:permitted, can_proceed?(my_resource, conn)},
    252            {:enabled, true} <- {:enabled, is_enabled?(my_resource, :new)} do

`lib/kaffy_web/controllers/resource_controller.ex`

    1>  defmodule KaffyWeb.ResourceController do
    2     @moduledoc false
    3   
    4     use Phoenix.Controller, namespace: KaffyWeb
    5     use Phoenix.HTML
    6     alias Kaffy.Pagination

`lib/kaffy_web/controllers/resource_controller.ex`

    1>  defmodule KaffyWeb.ResourceController do
    2     @moduledoc false
    3   
    4     use Phoenix.Controller, namespace: KaffyWeb
    5     use Phoenix.HTML
    6     alias Kaffy.Pagination

`lib/phoenix/router.ex`

    425           :telemetry.execute([:phoenix, :router_dispatch, :stop], measurements, metadata)
    426           halted_conn
    427   
    428         %Plug.Conn{} = piped_conn ->
    429           try do
    430>            plug.call(piped_conn, plug.init(opts))
    431           else
    432             conn ->
    433               measurements = %{duration: System.monotonic_time() - start}
    434               metadata = %{metadata | conn: conn}
    435               :telemetry.execute([:phoenix, :router_dispatch, :stop], measurements, metadata)

`lib/hexchange_web/endpoint.ex`

    1>  defmodule HexchangeWeb.Endpoint do
    2     use Phoenix.Endpoint, otp_app: :hexchange
    3   
    4     # The session will be stored in the cookie and signed,
    5     # this means its contents can be read but not tampered with.
    6     # Set :encryption_salt if you would also like to encrypt it.

`lib/plug/debugger.ex`

    No code available.

`lib/hexchange_web/endpoint.ex`

    1>  defmodule HexchangeWeb.Endpoint do
    2     use Phoenix.Endpoint, otp_app: :hexchange
    3   
    4     # The session will be stored in the cookie and signed,
    5     # this means its contents can be read but not tampered with.
    6     # Set :encryption_salt if you would also like to encrypt it.

`lib/phoenix/endpoint/sync_code_reload_plug.ex`

    17   
    18     def call(conn, {endpoint, opts}), do: do_call(conn, endpoint, opts, true)
    19   
    20     defp do_call(conn, endpoint, opts, retry?) do
    21       try do
    22>        endpoint.call(conn, opts)
    23       rescue
    24         exception in [UndefinedFunctionError] ->
    25           case exception do
    26             %UndefinedFunctionError{module: ^endpoint} when retry? ->
    27               # Sync with the code reloader and retry once

`lib/plug/cowboy/handler.ex`

    6     def init(req, {plug, opts}) do
    7       conn = @connection.conn(req)
    8   
    9       try do
    10         conn
    11>        |> plug.call(opts)
    12         |> maybe_send(plug)
    13         |> case do
    14           %Plug.Conn{adapter: {@connection, %{upgrade: {:websocket, websocket_args}} = req}} = conn ->
    15             {handler, state, cowboy_opts} = websocket_args
    16             {__MODULE__, copy_resp_headers(conn, req), {handler, state}, cowboy_opts}

`/home/ntraum/coding/hexafarms/hexchange/deps/cowboy/src/cowboy_handler.erl`

    32   -optional_callbacks([terminate/3]).
    33   
    34   -spec execute(Req, Env) -> {ok, Req, Env}
    35      when Req::cowboy_req:req(), Env::cowboy_middleware:env().
    36   execute(Req, Env=#{handler := Handler, handler_opts := HandlerOpts}) ->
    37>      try Handler:init(Req, HandlerOpts) of
    38          {ok, Req2, State} ->
    39              Result = terminate(normal, Req2, State, Handler),
    40              {ok, Req2, Env#{result => Result}};
    41          {Mod, Req2, State} ->
    42              Mod:upgrade(Req2, Env, Handler, State);

`/home/ntraum/coding/hexafarms/hexchange/deps/cowboy/src/cowboy_stream_h.erl`

    301     end.
    302   
    303   execute(_, _, []) ->
    304     ok;
    305   execute(Req, Env, [Middleware|Tail]) ->
    306>     case Middleware:execute(Req, Env) of
    307         {ok, Req2, Env2} ->
    308             execute(Req2, Env2, Tail);
    309         {suspend, Module, Function, Args} ->
    310             proc_lib:hibernate(?MODULE, resume, [Env, Tail, Module, Function, Args]);
    311         {stop, _Req2} ->

`/home/ntraum/coding/hexafarms/hexchange/deps/cowboy/src/cowboy_stream_h.erl`

    290   %% to simplify the debugging of errors. The proc_lib library
    291   %% already adds the stacktrace to other types of exceptions.
    292   -spec request_process(cowboy_req:req(), cowboy_middleware:env(), [module()]) -> ok.
    293   request_process(Req, Env, Middlewares) ->
    294     try
    295>         execute(Req, Env, Middlewares)
    296     catch
    297         exit:Reason={shutdown, _}:Stacktrace ->
    298             erlang:raise(exit, Reason, Stacktrace);
    299         exit:Reason:Stacktrace when Reason =/= normal, Reason =/= shutdown ->
    300             erlang:raise(exit, {Reason, Stacktrace}, Stacktrace)

`proc_lib.erl`

    No code available.

What should happen instead?

Resource should be saved successfuly.

aesmail commented 1 year ago

@nTraum a fix has been pushed to master. Let me know if the issue still exists with you guys.

nTraum commented 1 year ago

Can confirm that this is now fixed, thanks a bunch @aesmail! :)