Ash 3.0 Teasers!

Ash 3.0 Teaser #6: A cherry on top

This will be the final teaser before the 3.0 release candidates come out!


A common case is to have a tenant represented by a resource, like %Organization{} or %Tenant{}, but a tenant is always identified by a simple value, like a string or an integer. Because of this, there is often code that looks like this:

Ash.Changeset.for_update(%Record{}, tenant: "org_#{}")

There is also complexity when you have a mix of multi tenancy strategies, like if one resource uses schema-based multi tenancy, and the rest use attribute. The Ash.ToTenant protocol simplifies this, and allows you to use the same tenant everywhere, but have a different derived tenant value. Here is an example of how you might use it:

# in Organization resource

defimpl Ash.ToTenant do
  def to_tenant(resource, %MyApp.Accounts.Organization{id: id}) do
    if Ash.Resource.Info.data_layer(resource) == AshPostgres.DataLayer
      && Ash.Resource.Info.multitenancy_strategy(resource) == :context do

Sensitive Calculations & Aggregates

In the same way that you could specify attributes as sensitive?: true, you can now specify calculations and aggregates as sensitive. These will be redacted when inspecting records, and will also be redacted when inspecting filters inside of queries.

Code interfaces support atomic & bulk actions

In 2.0, you need to look up a record before you can update or destroy it, unless you change your code to use YourApi.bulk_update or YourApi.bulk_destroy (in 3.0, Ash.bulk_update and Ash.bulk_destroy). This can be quite verbose. For example, let’s say you have the id of a thing, and you want to update it. The most idiomatic way would have been something like this:

|> Ash.get!(id)
|> Post.archive!()

Or alternatively, you could have opted not to use the code interface, and used Ash.bulk_update. For example:

|> Ash.Query.filter(id == ^id)
|> Ash.bulk_update(:archive, %{....})

But then you don’t get to use your nicely defined code interface, which acts like a context function which fills the role of a context function in Phoenix.

In Ash 3.0, code interfaces have been updated to support bulk operations, which makes cases like the above much more seamless!

For updates/destroys, you can pass identifiers, queries, and lists/streams of inputs directly instead of a record or a changeset. From here on, we’ll also be using code interface functions defined on our domain instead of our resources, which is the recommended way in 3.0. See previous teasers for more.

Update/Destroy examples:

# If the action can be done atomically (i.e without looking up the record), it will be. 
# Otherwise, we will look up the record and update it.
# => MyApp.Blog.archive_post!(

# queries can be provided, which will return an `Ash.BulkResult`
|> Ash.Query.filter(author_id == ^author_id)
|> MyApp.Blog.archive_post!()
# => %Ash.BulkResult{}

# lists of records can also be provided, also returning an `Ash.BulkResult`

[%Post{}, %Post{}]
|> MyApp.Blog.archive_post!()
# => %Ash.BulkResult{}


For creates, we detect if the input is a list (and not a keyword list), and opt into bulk create behavior:

# no need for the additional `define ..., bulk?: true`
Blog.create_post!([%{...inputs}, %{...inputs}]

EDIT: Below was the original section on bulk creates. Feel free to ignore it, but leaving it for posterity. @vonagam has made a good point in a discussion on discord, there is no reason that we can’t just detect a list of inputs in the input argument, and use that as a bulk create. This allows us not to need the bulk? true option, and they can function the same as the others, adapting their behavior based on the input. For example:

Create’s don’t take a first argument like updates/destroys, and so a bulk create must be explicitly defined. For example:

# in the domain

resource Post do
  define :create_posts do
    action :create
    bulk? true

And it can be used like so:

Blog.create_posts!([%{...inputs}, %{...inputs}]

Streaming reads

You can also now ask read action code interfaces to return streams. Keep in mind that Ash streams are not (yet) based on data-layer-native streams, but rather will use your actions pagination functionality (preferring keyset vs offset pagination). Only the raising version (!) supports the :stream option, because streams can only raise errors, not return them.

For example:

Blog.active_posts!(stream?: true) 
# => returns a lazily enumerable stream

The light at the end of the tunnel

3.0 is very close, and I’m so excited! Thanks again to everyone who has been a part of it. For those adventurous folks, the release candidates will be out soon for you to have a play with :rocket: