Ash 3.0 Teasers!

Ash 3.0 Teaser #4: Better Defaults, Less Surprises, Part 2

3.0 is coming along very well! Got a few more updates along the same vein of better defaults and less surprises!

%Ash.NotLoaded{} for not selected values

When you run an Ash.Query or an Ash.Changeset that has a select applied, anything that isn’t selected currently gets a nil value. This can be very confusing and often leads to nontrivial bugs. In Ash 3.0, you will instead get %Ash.NotLoaded{}, allowing you to distinguish between values that are actually nil and values that have just not been loaded.

Actions no longer accept all public, writable attributes by default

Thanks to @sevenseacat for bringing this to our attention originally and illustrating just how risky this can be!

In Ash 2.0, actions automatically accept all public writable attributes. This makes it very easy to accidentally include an attribute in your actions, especially when adding a new attribute. For instance:

actions do
  defaults [:create, :read]

If you add an attribute to the above resource, you may not realize that it is now accepted in that create action by default.

In Ash 3.0, all actions accept nothing by default. You can adopt the old behavior in your resource with

actions do
  default_accept :*

This will help prevent potentially leaking new attributes.

A small quality of life improvement that results from this is that you no longer need to specify attribute_writable?: true on your belongs_to relationships to modify their attribute. This is because making those attributes modifiable in a resource requires including it in the accept list (or using accept :*), and so it is no longer implicit.

private?: true is now public?: false, and public?: false is now the default

In Ash 2.0, all fields default to private?: false.

Public attributes, relationships, calculations and aggregates are meant to be exposed over public interfaces. By defaulting to public?: true, it makes it very easy to add a new field and not realize that you’ve added it to your GraphQL or JSON API, etc.

In Ash 3.0, this option has been renamed to its inverse, public?. Additionally, it now defaults to false. Where you may have seen this:

attributes do
  attribute :first_name, :string
  attribute :last_name, :string
  attribute :super_secret, :string, private?: true

you will now see

attributes do
  attribute :first_name, :string, public?: true
  attribute :last_name, :string, public?: true
  attribute :super_secret, :string

As you can see this may often be more verbose, as many resources have more public fields than private fields. But it is also much safer in general. It is much better to have an experience of “oh, how come X isn’t showing in my public interface”, then “oh, we’re showing some data over our API that we didn’t intend to show”. Often times we have to make trade offs for the sake of security and safety, and this is one of those cases.

Custom Expressions

This isn’t on theme, as it’s a new feature as opposed to a better default, but I wanted to spice things up :slight_smile:. Custom expressions will allow you to extend Ash’s expression syntax. Since an example is worth a thousand words:

  defmodule MyApp.Expressions.LevenshteinDistance do
    use Ash.CustomExpression,
      name: :levenshtein_distance,
      arguments: [
        [:string, :string]

    def expression(AshPostgres.DataLayer, [left, right]) do
      expr(fragment("levenshtein(?, ?)", left, right))

    # It is good practice to always define an expression for `Ash.DataLayer.Simple`,
    # as that is what Ash will use to run your custom expression in Elixir.
    # This allows us to completely avoid communicating with the database in some cases.

    def expression(data_layer, [left, right]) when data_layer in [
    ] do
      expr(fragment(&levenshtein/2, left, right))

    # always define this fallback clause as well
    def expression(_data_layer, _args), do: :unknown

    defp levenshtein(left, right) do
      # ......

With the above custom expression, defined, I can configure it like so:

config :ash, :custom_expressions, [MyApp.Expressions.LevenshteinDistance]

And I can then use it in expressions:

Ash.Query.filter(User, levenshtein_distance(full_name, ^search) < 5)

This will also allow libraries and other packages to provide cross-data layer expressions for you to use with their custom values and expressions.

Thats all!

Thats all I have for you today, thanks for everyone following along :slight_smile: We are on track to have a release candidate this month, ready for the adventurous folks to give it a spin :partying_face:

Teaser #5: Ash 3.0 Teasers! - #30 by zachdaniel