Ash.Scope (ash v3.5.24)

View Source

Determines how the actor, tenant and context are extracted from a data structure.

This is inspired by the same feature in Phoenix, however the actor, tenant and context options will always remain available, as they are standardized representations of things that actions can use to do their work.

When you have a scope, you can group up actor/tenant/context into one struct and pass that around, for example:

scope = %MyApp.Scope{current_user: user, current_tenant: tenant, locale: "en"}

# instead of
MyDomain.create_thing(actor: current_user, tenant: tenant)

# you can do
MyDomain.create_thing(scope: scope)

Scope is left at the front door

Your scope is "left at the front door". That is, when you pass a scope to an action, the options are extracted and the scope is removed from those options. Within hooks, you are meant to use the context provided to your functions as the new scope. This is very important, because you don't want a bunch of your code or extension code having to switch on if opts[:scope], extracting the things that it needs, etc.

See the actions guide for more information.

Setup

If you are using Phoenix, you will want to assign your scope module in a plug that runs after your plugs that determine actor/tenant/context. Then, you will want to add an on_mount hook for LiveViews that sets your scope assign. This is especially true for AshAuthentication, as it does not currently have a concept of scopes.

Passing scope and options

For the actor, tenant and authorize?, extracted from scopes, the values from the scope are discarded if also present in opts.

i.e scope: scope, actor: nil will remove the set actor. scope: scope, actor: some_other_actor will set the actor to some_other_actor.

For context, the values are deep merged.

For tracer, the value(s) are concatenated into a single list.

Example

You would implement Ash.Scope.ToOpts for a module like so:

defmodule MyApp.Scope do
  defstruct [:current_user, :current_tenant, :locale]

  defimpl Ash.Scope.ToOpts do
    def get_actor(%{current_user: current_user}), do: {:ok, current_user}
    def get_tenant(%{current_tenant: current_tenant}), do: {:ok, current_tenant}
    def get_context(%{locale: locale}), do: {:ok, %{shared: %{locale: locale}}}
    # You typically configure tracers in config giles
    # so this will typically return :error
    def get_tracer(_), do: :error

    # This should likely always return :error
    # unless you want a way to bypass authorization configured in your scope
    def get_authorize?(_), do: :error
  end
end

For more on context, and what the shared key is used for, see the actions guide

You could then use this in various places by passing the scope option.

For example:

scope = %MyApp.Scope{...}
# with code interfaces
MyApp.Blog.create_post!("new post", scope: scope)

# with changesets and queries
MyApp.Blog
|> Ash.Changeset.for_create(:create, %{title: "new post"}, scope: scope)
|> Ash.create!()

# with the context structs that we provide

def change(changeset, _, context) do
  Ash.Changeset.after_action(changeset, fn changeset, result ->
    MyApp.Domain.do_something_else(..., scope: context)
    # if not using as a scope, the alternative is this
    # in the future this will be deprecated
    MyApp.Domain.do_somethign_else(..., Ash.Context.to_opts(context))
  end)
end

Extensions should not use this option, only end users.

Summary

Types

t()

@type t() :: Ash.Scope.ToOpts.t()

Functions

to_opts(scope, overrides \\ [])