zac

zac

Validation that tests relationship value (without using custom validator?)

I feel like there should be an easy way to do this, without creating a (relatively complicated) custom validation. But after combing the docs and forum, I’m thinking perhaps not…

Here’s a somewhat simplified case.

I have two resources, a sprint and an activity.

sprint’s have a date range, e.g., sprint.start_date and sprint.end_date.

Activities belong to sprints, and have a date, activity.date.

def Sprint do
  attributes do
    attribute :start_date, :date, allow_nil? false
    attribute :end_date, :date, allow_nil? false
  end
end

def Activity do
  attributes do
    attribute :date, :date, allow_nil? false
  end

  belongs_to :sprint, Sprint do
    attribute_writable? true
    allow_nil? false
  end
end

The desired outcome is that an activity’s date must fall within the start and end dates of its sprint.

It seems I should be able to do something like the following – but, I haven’t been able to figure out how.

# This absolutely won't even compile, but it expresses the idea I'm after:
validations do
  validate compare(:date, greater_than_or_equal_to: :sprint.start_date)
  validate compare(:date, less_than_or_equal_to: :sprint.end_date)
end

Maybe I’m expecting too much and I need to do a custom validator, but thought I’d post the question. (I’m also just getting back into Ash… just upgraded a small project from 2.4 to 3.4, and really dusting off some very, very dusty recollection of how it all works…)

If there is a way to achieve the above without dropping into complicated (potentially atomic) validations and atomic_ref() and the like… well, I’d love some pointers. Not up to speed on atomics yet… Thanks!

Marked As Solved

zachdaniel

zachdaniel

Creator of Ash

The main thing that makes validations across tables like this hard is that, unlike other validations, there is no guarantee that they will be true as the related resource can change on its own. But that doesn’t necessarily have to be a problem, it’s just on you to understand that that is the case.

Unfortunately there is no builtin validation, so you will in fact need to drop down to the complicated version of things. Good thing I’m here! :laughing:

defmodule ValidateWithinSprintRange do
  use Ash.Resource.Validation

  def atomic(changeset, _, _) do
     {:atomic, 
       
       # the fields involved. Used by the framework (technically its not but it will some day)
       [:start_date, :end_date], 
       
       # the expression that should *not* be true. i.e if this is true, the error will be raised
       expr(^atomic_ref(:date) < sprint.start_date or ^atomic_ref(:date) > sprint.end_date), 
       
       # an expression that calls `error/2` to produce the expression
       expr(
         error(
            Ash.Error.Changes.InvalidAttribute, %{
              field: :date,
              value: ^atomic_ref(:date),
              message: "date must be between the sprint's start and end date"
            }
         )
      )
    }
  end
end

Something along those lines should do the trick. By defining it this way (with only an atomic callback) the validation will always be deferred to when the query is run. But in the case of checking related data, thats typically the time you’d want to do it anyway. What you can do if you want the eager validation, like in a form, is add a regular validate/3 callback that does the same work manually.

I think this can be made much more ergonomic over time, but it’s good to know how to do this :slight_smile:

Also Liked

zachdaniel

zachdaniel

Creator of Ash

validate Something, only_when_valid?: true to run it only when the changes are valid.

The idea is that you may want to provide many validation errors at once instead of just the first one that fails.

For your particular case, you can decide how you want it to behave. On its own it might make sense to just ignore the validation entirely if there is no sprint_id. It’s something else’s job to ensure that it is set.

  def validate(changeset, _, _) do
    if spring_id = Ash.Changeset.get_attribute(changeset, :sprint_id) do
      your_logic
    else
      :ok
    end
  end

Then the experience of the caller would be “you must set spring_id”, and then “must be in range” if it’s not in range.

zac

zac

@zachdaniel, while Ash Framework has a bit of a learning curve, as always you’ve thought ahead and built in a great deal of flexibility and power. Looking forward to (again!) getting more familiar with it!

Would love to see some of the above make its way into the docs. While I feel like all the pieces are there, putting it together can be a challenge. Perhaps building out the existing examples with some of these more subtle (and powerful) features would be helpful, especially now with atomics on the scene…

Where Next?

Popular in Questions Top

marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
JulienCorb
I am trying to implement my new.html.eex file to create new posts on my website. new.html.eex: &lt;h1&gt;Create Post&lt;/h1&gt; &lt;%= ...
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
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
jaysoifer
Is there a way to rollback a specific migration and only that one (“skipping” all the other ones)? Would mix ecto.rollback -v 200809061...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New

Other popular topics Top

TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
274 41539 114
New
johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
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
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36128 110
New
marick
I had some trouble figuring out how to make many-to-many associations work. Once I got it working, I wrote a blog post. Because I’m a nov...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New

We're in Beta

About us Mission Statement