Optional read action argument error

I have a read action on a Booking resource with a required booking_date argument and an optional user_id argument. I use a preparation (defined in a separate module) to prepare the query depending on whether the user id is present or not.

Calling the action with user_id explicitly set to nil works fine:

get_bookings_by_date(~D[2022-01-01], nil, authorize?: false)

However, the preparation fails I omit if I call the action without providing the user id:

get_bookings_by_date(~D[2022-01-01],  authorize?: false)

In this case, the authorize?: false option is being passed to the preparation as the value of the user_id argument.

Is there a way to make it work without explicitly setting the user id to nil?

The code interface function looks like this:

 define :get_bookings_by_date, action: :by_date, args: [:booking_date, {:optional, :user_id}]

The action is defined as:

    read :by_date do
      argument :booking_date, :date do
        allow_nil? false
      end

      argument :user_id, :integer do
        allow_nil? true
      end

      prepare Preparations.BookingsByDate
    end

And this is the preparation:

  def prepare(query, _opts, _context) do
    date = query |> Ash.Query.get_argument(:booking_date)
    user_id = query |> Ash.Query.get_argument(:user_id)

    start_ndt = NaiveDateTime.new!(date, ~T[00:00:00])
    end_ndt = NaiveDateTime.add(start_ndt, 1, :day)

    query =
      query
      |> Ash.Query.filter(start >= ^start_ndt and start < ^end_ndt)

    case user_id do
      nil -> query
      _ -> query |> Ash.Query.filter(user_id == ^user_id)
    end
  end

Optional arguments are always populated “in order”. However, Ash code interfaces accept an optional map for inputs followed by an optional keyword list for opts, and we disambiguate based on that.

So if you change it to this:

 define :get_bookings_by_date, action: :by_date, args: [:booking_date]

You can call it like so

get_bookings_by_date(~D[2022-01-01], %{user_id: user_id}, authorize?: false)

when you have a user id, and like so when you don’t:

get_bookings_by_date(~D[2022-01-01], authorize?: false)

Gotcha, thanks.

I was trying to make the expected arguments obvious in the code interface, by using the args: [:booking_date, {:optional, :user_id}] syntax instead of just passing everything in the params map. Is there another way of accomplishing this that still allows me to pass options in opts?

The format I showed supports args, inputs, and opts, so I’m not sure I understand what you mean.

What I meant was, this function definition makes it clear that there is an optional user id argument:

define :get_bookings_by_date, action: :by_date, args: [:booking_date, {:optional, :user_id}]

This doesn’t:

define :get_bookings_by_date, action: :by_date, args: [:booking_date]

I’m fairly new to Ash, so I am probably misunderstanding the use case for optional arguments.

Its not so much an Ash thing just an Elixir functions thing.

with the optional arguments, you get this:

def get_bookings_by_date(booking_date, user_id \\ nil, options \\ []) do

end

That function will always use the second argument as user_id.

The function docs should include what fields they accept, so there should be some indication like on autocomplete w/ docs that a user_id can also be provided.