Context: In my application, projects make offers to users.
I’ve just discovered Ecto.Multi
and am really happy as it makes a lot of sense to me in certain circumstances. For example:
If user accepts an offer, then I need to update the offer AND attach user to the project
What I am still unclear on is where I should run the logic that validates other business logic which requires further database queries. For example:
- whether a user has the correct user type to receive an offer
- whether the user has any pending offers already
- whether the newly assigned project role (from the offer) is already taken
Up until now I have kept this logic in the changeset pipeline, and as I have tried to keep changesets pure, it means passing in lots of arguments to the changeset function (eg, all project offers).
I wonder if I should rather be making use of Multi
’s for this purpose:
offers.ex
context:
def create_offer(params, project, current_user) do
create_offer_multi =
Multi.new()
# run basic validation on the new offer form
# if offer form is valid, go into calcs
|> Multi.run(:offer, create_offer_changeset(params, project, current_user))
|> Ecto.Multi.run(:valid_recipient, fn _repo, %{offer: offer} ->
# run some validation that ensures recipient user:
# - can receive an offer and
# - they don't already have any other pending offers for this project
Offers.validate_offer_recipient(project, offer)
end)
|> Multi.run(:add_project_role, fn _repo, %{offer: offer, valid_recipient: recipient} ->
# if all successful, add user to project with the given role
Projects.add_project_role(project, offer, recipient)
end)
case Repo.transaction(create_offer_multi) do
{:ok, %{offer: offer}} -> {:ok, offer}
error_res -> error_response(:create_offer, error_res)
end
end
def create_offer_changeset(params, project, current_user) do
recipient = Accounts.get_user_by_email(params["email"])
params
# run pure changeset casting & validation logic on offer
|> Offer.create_offer_changeset(project)
|> Ecto.Changeset.put_assoc(:project, project)
|> Ecto.Changeset.put_assoc(:creator, current_user)
|> Ecto.Changeset.put_assoc(:recipient, recipient)
# if offer changeset is valid, then go and run a load of calculations
# that need to access database and attach them to the changeset
|> Offer.run_and_merge_calcs(project)
end
I have been working in Elixir/Phoenix for a while, but with no mentor and I am still quite confused:
How should I approach this sort of validation?
Are there any problems with this code? How could it be improved?
Any help greatly appreciated.