Using checks directly in Ash

Hi. I have a Resource called User. In order to perform the :activate_account action on a user they must have their profile (email, shipping address, etc) filled in. This is implemented as a SimpleCheck and used from the policies in User.

In my UI there is a list of things that must be completed before the user can click the active button. Disabling the "Activate" button is simple, just using User.can_activate_account? function provided by ash.

But I would like to reuse the logic from the ProfileIsFilled check, and I don’t see any “Ash Way” of doing that. Essentially:

<h2>Tasks</h2>
<ul>
    <li class={[Checks.EmailConfirmed.check?(@user) && "green"]}>Email Confirmed</li>
    <li class={[Checks.ProfileIsFilled.check?(@user) && "green"]}>Profile filled out</li>
</ul>

I can of course accomplice this by keeping the logic from my Check elsewhere or exposing my own function from the check module:

defmodule Checks.ProfileIsFilled do
      use Ash.Policy.SimpleCheck

      def match?(_, {resource: %User{}} = context, _opts) do
           check?(context.resource)
      end

      def check?(%User{}  = user) do
           user.email != nil && .....
      end
end

This is a contrived example, but being able use the check logic outside the context of policies and actions makes sense to me, but I don’t want to go against the grain of the framework.

Is there an Ash way of doing this, or should I continue as I have done by implementing a check? function - which has worked well.

I realize these are contrived examples, but I kind of feel like those checks would be in policies for actions, and you could use the same can_* functions for them.

eg. if a user hasn’t confirmed their email, you would have an action for them to confirm their email that that policy check would apply to.

policy action(:confirm_email) do 
  check EmailConfirmed
end
<li class={!User.can_confirm_email?(@user) && "green"}>Confirm email</li>

So, I updated the example to make the intent more clear.

The UI displays a check list of items that have or have not completed. This is logic related to the action policy but not so much if the actor has permission to do the action, but rather if the resource is in certain state where some criteria is true.

Maybe what I am looking for is a way to model richer state machine states :thinking:

Having profile_is_filled? and email_is_confirmed? as calculations doesn’t seem that far-fetched?

But I’m not sure if you can use calculations as checks in an action?

Just make calculations that project this information indeed

1 Like

Calculations is the answer :slight_smile:

Woah nice that I’m starting to understand Ash!

To elaborate on this, calculations are actually much more flexible than one might think.

Given a calculation like this:

calculations do
  # some example implementations
  calculate :email_confirmed?, :boolean, expr(email and not is_nil(confirmed_at)) 
  calculate :profile_is_filled?, :boolean, expr(count_nils([foo, bar, baz, but]) == 0)
end

With just that, you can load using Ash.Query.load or Ash.load those fields, i.e Ash.load(user, [:email_confirmed?, :profile_is_filled?])

So your UI would look like this:

<h2>Tasks</h2>
<ul>
    <li class={[@user.email_confirmed? && "green"]}>Email Confirmed</li>
    <li class={[@user.profile_is_filled? && "green"]}>Profile filled out</li>
</ul>

However, you can take it even one step further, if you want. I don’t necessarily think that you need more than what I described above. But you can do this as well:

code_interface do
  define_calculation :email_confirmed?, args: [:_record]
end

And you could then do

YourResource.email_confirmed?(user)

You can also use it without code interfaces:

Ash.calculate(user, :email_confirmed?)
# or with just some field values
Ash.calculate(user, :email_confirmed?, refs: %{email: "an email"})

If you want, you can break down the code interface to make a simple pure function that doesn’t take a record. For example:

code_interface do
  define_calculation :email_confirmed?, args: [:email, :confirmed_at]
end

which would enable

YourResource.email_confirmed?(@user.email, @user.confirmed_at)

Believe it or not

This strategy even works with calculations that use fragments. We issue queries to the database to calculate individual elements on demand.

Here is an example from our tests

calculate(:upper_title, :string, expr(fragment("UPPER(?)", title)))
test "calculation works with simple fragments" do
  assert Post.upper_title!("example") == "EXAMPLE"
end

Which runs the following SQL query:

SELECT ((UPPER($1)))::text FROM UNNEST(ARRAY[1]) AS f0 ["example"]
3 Likes

Thanks for the answer. It is really appreciated that you take the time!

I will try experimenting with calculations more. Maybe they are the answer two my woes with derived state becoming stale over time.

There is the mantra of “DDAU” (Data Down, Actions Up or DDEU Data Down Events Up).

If you have child components that fetch stuff, you will always run into the problem of stale data.