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]
end
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 :*
end
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
end
you will now see
attributes do
attribute :first_name, :string, public?: true
attribute :last_name, :string, public?: true
attribute :super_secret, :string
end
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 . 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))
end
# 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 [
AshPostgres.DataLayer.Ets,
AshPostgres.DataLayer.Simple
] do
expr(fragment(&levenshtein/2, left, right))
end
# always define this fallback clause as well
def expression(_data_layer, _args), do: :unknown
defp levenshtein(left, right) do
# ......
end
end
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 We are on track to have a release candidate this month, ready for the adventurous folks to give it a spin
Teaser #5: Ash 3.0 Teasers! - #30 by zachdaniel