Ash.Scope (ash v3.5.24)
View SourceDetermines 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
@type t() :: Ash.Scope.ToOpts.t()