Hi!
I have some doubts regarding the responsibilities of the “changeset” functions. The examples in the Ecto documentation are clear to me, but things get a bit more complicated once you start working with real life examples.
I would like to draw a line between things that should be validated outside and inside the changeset function. I hope you could help me here
Here are a few examples:
Example A: email address verification
This is a common example in any web application: once the user registers, the app sends an email to verify it exists. For example, here is how the Hex.pm project handles it.
From my point of view, there are two valid ways of writing this changeset (using the example from Hex.pm):
# Option A1: verification token is checked inside the changeset.
# The controller calls the changeset directly.
def verify(email, key) do
change(email, %{
verified: true,
verification_key: nil,
verification_expiry: nil
})
|> validate_verification_token(key)
|> unique_constraint(:email, name: "emails_email_key", message: "already in use")
end
# Option A2: the verification token is checked outside the changeset.
# The controller first calls verify?/2 and if true it calls the changeset function.
def verify?(nil, _key), do: false
def verify?(email, key) do
email_key = email.verification_key
valid_key? = !!(email_key && Hexpm.Utils.secure_check(email_key, key))
within_time? = Hexpm.Utils.within_last_day?(email.verification_expiry)
valid_key? and within_time?
end
def verify(email) do
change(email, %{
verified: true,
verification_key: nil,
verification_expiry: nil
})
|> unique_constraint(:email, name: "emails_email_key", message: "already in use")
end
Example B: reset password token verification
Another common example. The code is also available on the Hex.pm project.
The code in the Hex.pm project checks the reset password token outside the changeset. However, that reset password token could be validated inside a changeset function.
Example C: (email address) domain validation
Another common example: admin users must register with an email address using a specific domain.
Imagine that the User
schema in the Hex.pm project would have a :type
field with possible values: :admin
or :user
. The app needs to ensure that admin users only have @hex.pm
emails. If that’s the case, where would that check be done, inside or outside the changeset?
# Option C1: outside the changeset
def valid_domain?(%User{} = user, email_address) do
validate_domain(user.type, email_address)
end
def changeset(:create, %User{} = user, email_address) do
%__MODULE__{user_id: user.id}
|> cast(%{email_address: email_address}, [:email_address])
|> validate_required([:email_address])
# Other validations
end
# Option C2: inside the changeset
def changeset(:create, %User{} = user, email_address) do
%__MODULE__{user_id: user.id}
|> cast(%{email_address: email_address}, [:email_address])
|> validate_required([:email_address])
|> validate_domain(user.type)
# Other validations
end
Example D: currency consistency
An accounting system might optionally support multi-currency. However, the user must opt-in for this feature, adding or removing currencies by hand. When submitting invoices, the system must check that the invoice is on the list of the supported currencies for that organization.
# Option D1: outside the changeset
# Probably in the controller, you would have something like:
with {:ok, currency_codes} <- get_currencies_for_organization(organization),
:ok <- verify_currency(currency_codes, params["currency_code"]),
changeset = Invoice.changeset(:record, params)
# ...
end
# Option D2: inside the changeset
def changeset(:record, %{} = params, supported_currencies) do
# ...
end
# or:
def changeset(:record, %Organization{} = organization, %{} = params) do
# ...
end
Thanks for your help!