acrolink

acrolink

Where to write custom changeset validation functions which rely on current database values / records?

Here is an example of a custom validation:

@our_url "https://our-bucket.s3.amazon.com"
def changeset(struct, params \\ %{}) do
  struct
  |> cast(params, @required_fields)
  |> validate_required(@required_fields)
  |> validate_from_s3_bucket(:url)
end
def validate_from_s3_bucket(changeset, field, options \\ []) do
  validate_change(changeset, field, fn _, url ->
    case String.starts_with?(url, @our_url) do
      true -> []
      false -> [{field, options[:message] || "Unexpected URL"}]
    end
  end)
end

This can be written in the schema definition file. But where to write custom changeset validation functions which test database tables (of other models) for certain values and based on that data flag the transaction as valid or not? I guess making Ecto queries for B inside schema definition of A is not the correct way to do it. Any ideas? Thank you.

Example / Clarification

I have this in my schema definition:

  def changeset(record, attrs) do
    record
    |> cast(attrs, [:issued_at, :due_for_return, :returned_at, :fee, :fee_paid_at, :book_id])
    |> validate_required([])
    |> validate_book(:book_id)
  end


  defp validate_book(changeset, field) do
    case changeset.valid? do
      true ->
        book_id = get_field(changeset, field)
        case  Repo.get(Mango.Books.Book, book_id) do
          nil -> add_error(changeset, :book_id, "Book not found..")
          book -> {:ok, book}
            case book.status do
              :available -> changeset
              _ -> add_error(changeset, :book_id, "Book is already out..")
            end
        end
      _ ->
        changeset
    end
  end

As you can see I am doing some Repo query validation against some other model (books). My question is this the place do this or should I move all of this outside of my schema definition?

Most Liked

idi527

idi527

I usually avoid making request to a database inside changesets. Instead, I use more explicit multis or transactions. So your second example I would write as:

  def changeset(record, attrs) do
    cast(record, attrs, [:issued_at, :due_for_return, :returned_at, :fee, :fee_paid_at])
  end

  defp valid_book_multi(multi, book_id) do
    alias Ecto.Multi
     
    Multi.run(multi, :book, fn _changes -> 
       case Book.get(book_id) do
          %Book{status: :available} = available_book -> {:ok, available_book}
          %Book{status: status} = not_available_book -> {:error, Book.error_msg(:status, status)}
          nil -> {:error, :not_found}
       end
    end)
  end

  @spec create_record(%{(String.t | atom) => term}, book_id: pos_integer) :: {:ok, %Record{}} | {:error, Ecto.Changeset.t}
  def create_record(attrs, book_id: book_id) do
    alias Ecto.Multi

   record_changeset = changeset(%Record{book_id: book_id}, attrs)

    Multi.new()
    |> valid_book_multi(book_id)
    |> Multi.insert(:record, record_changeset)
    |> Repo.transaction()
    |> case do
      {:ok, %{record: record}} -> {:ok, record}
      {:error, :book, reason, _changes} -> # collect errors into record changesets
    end
  end
jeremyjh

jeremyjh

Changeset validation functions should be limited to pure functions that use the data available to them in the Changeset and arguments you pass. They are not “models”. Business rules that require data access, messaging etc should be implemented in another module - if you are using Phoenix it would be a context module - but regardless its just a module with functions.

Laetitia

Laetitia

What about using Ecto.Changeset.prepare_changes ?

Provides a function to run before emitting changes to the repository.

Such function receives the changeset and must return a changeset, allowing developers to do final adjustments to the changeset or to issue data consistency commands.

The given function is guaranteed to run inside the same transaction as the changeset operation for databases that do support transactions.

I will run some validation and returns an invalid changeset if it failed.

Where Next?

Popular in Questions Top

skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
tduccuong
Hi, is there any work on GUI with Elixir, that is similar to Electron/Javascript? My idea is to bundle Phoenix and BEAM into a single se...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New

Other popular topics Top

danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29377 241
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New

We're in Beta

About us Mission Statement