mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 05:23:03 +12:00
docs: update query/authorization docs
This commit is contained in:
parent
b4f7492dfe
commit
d10ae9bad1
4 changed files with 94 additions and 5 deletions
17
documentation/topics/authorization.md
Normal file
17
documentation/topics/authorization.md
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Authorization
|
||||||
|
|
||||||
|
## Ash Policy Authorizer
|
||||||
|
|
||||||
|
Generally speaking, you will want to use ash_policy_authorizer to authorize access to your resources.
|
||||||
|
|
||||||
|
use `mix hex.info ash_policy_authorizer` to get the latest version, and add it to your dependencies:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
{:ash_policy_authorizer, "~> x.x.x"}
|
||||||
|
```
|
||||||
|
|
||||||
|
For usage, see the `ash_policy_authorizer` [documentation](https://hexdocs.pm/ash_policy_authorizer) for the rest
|
||||||
|
|
||||||
|
## Implementing a custom authorizer
|
||||||
|
|
||||||
|
Implementing a custom authorizer is pretty complex. Instead of writing a guide, it would be best to just have some discussions if/when someone thinks that they need one. Make an issue and we'll talk it over.
|
|
@ -108,7 +108,7 @@ defmodule Ash do
|
||||||
@type relationship_cardinality :: :many | :one
|
@type relationship_cardinality :: :many | :one
|
||||||
@type resource :: module
|
@type resource :: module
|
||||||
@type side_loads :: term
|
@type side_loads :: term
|
||||||
@type sort :: Keyword.t()
|
@type sort :: list(atom | {atom, :asc} | {atom, :desc})
|
||||||
@type validation :: Ash.Resource.Validation.t()
|
@type validation :: Ash.Resource.Validation.t()
|
||||||
|
|
||||||
require Ash.Dsl.Extension
|
require Ash.Dsl.Extension
|
||||||
|
|
|
@ -4,6 +4,28 @@ defmodule Ash.Query do
|
||||||
|
|
||||||
Ash queries are used for read actions and side loads, and ultimately
|
Ash queries are used for read actions and side loads, and ultimately
|
||||||
map to queries to a resource's data layer.
|
map to queries to a resource's data layer.
|
||||||
|
|
||||||
|
Queries are run by calling `read` on an API that contains the resource in question
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
MyApp.Post
|
||||||
|
|> Query.filter(likes: [gt: 10])
|
||||||
|
|> Query.sort([:title])
|
||||||
|
|> MyApp.Api.read!()
|
||||||
|
|
||||||
|
MyApp.Author
|
||||||
|
|> Query.aggregate(:published_post_count, :posts, filter: [published: true])
|
||||||
|
|> Query.sort(published_post_count: :desc)
|
||||||
|
|> Query.limit(10)
|
||||||
|
|> MyApp.Api.read!()
|
||||||
|
|
||||||
|
MyApp.Author
|
||||||
|
|> Query.load([:post_count, :comment_count])
|
||||||
|
|> Query.load(posts: [:comments])
|
||||||
|
|> MyApp.Api.read!()
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
defstruct [
|
defstruct [
|
||||||
:api,
|
:api,
|
||||||
|
@ -64,7 +86,7 @@ defmodule Ash.Query do
|
||||||
alias Ash.Error.SideLoad.{InvalidQuery, NoSuchRelationship}
|
alias Ash.Error.SideLoad.{InvalidQuery, NoSuchRelationship}
|
||||||
alias Ash.Query.{Aggregate, Calculation}
|
alias Ash.Query.{Aggregate, Calculation}
|
||||||
|
|
||||||
@doc "Create a new query."
|
@doc "Create a new query"
|
||||||
def new(resource, api \\ nil) when is_atom(resource) do
|
def new(resource, api \\ nil) when is_atom(resource) do
|
||||||
query =
|
query =
|
||||||
%__MODULE__{
|
%__MODULE__{
|
||||||
|
@ -94,6 +116,14 @@ defmodule Ash.Query do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Loads named calculations or aggregates on the resource.
|
||||||
|
|
||||||
|
Currently, loading attributes has no effects, as all attributes are returned.
|
||||||
|
Before long, we will have the default list to load as the attributes, but if you say
|
||||||
|
`load(query, [:attribute1])`, that will be the only field filled in. This will let
|
||||||
|
data layers make more intelligent "select" statements as well.
|
||||||
|
"""
|
||||||
@spec load(t(), atom | list(atom) | Keyword.t()) :: t()
|
@spec load(t(), atom | list(atom) | Keyword.t()) :: t()
|
||||||
def load(query, fields) when not is_list(fields) do
|
def load(query, fields) when not is_list(fields) do
|
||||||
load(query, List.wrap(fields))
|
load(query, List.wrap(fields))
|
||||||
|
@ -223,11 +253,21 @@ defmodule Ash.Query do
|
||||||
defp default(nil, value), do: value
|
defp default(nil, value), do: value
|
||||||
defp default(value, _), do: value
|
defp default(value, _), do: value
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Sets a specific context key to a specific value
|
||||||
|
|
||||||
|
See `set_context/2` for more information.
|
||||||
|
"""
|
||||||
@spec put_context(t(), atom, term) :: t()
|
@spec put_context(t(), atom, term) :: t()
|
||||||
def put_context(query, key, value) do
|
def put_context(query, key, value) do
|
||||||
%{query | context: Map.put(query.context, key, value)}
|
%{query | context: Map.put(query.context, key, value)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Merge a map of values into the query context
|
||||||
|
|
||||||
|
Not much uses this currently.
|
||||||
|
"""
|
||||||
@spec set_context(t(), map) :: t()
|
@spec set_context(t(), map) :: t()
|
||||||
def set_context(query, map) do
|
def set_context(query, map) do
|
||||||
%{
|
%{
|
||||||
|
@ -243,6 +283,8 @@ defmodule Ash.Query do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc "Removes a field from the list of fields to load"
|
||||||
|
@spec unload(t(), list(atom)) :: t()
|
||||||
def unload(query, fields) do
|
def unload(query, fields) do
|
||||||
query = to_query(query)
|
query = to_query(query)
|
||||||
|
|
||||||
|
@ -307,6 +349,18 @@ defmodule Ash.Query do
|
||||||
do_unload_side_load(side_loads, {field, []})
|
do_unload_side_load(side_loads, {field, []})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Builds a query from a keyword list.
|
||||||
|
|
||||||
|
This is used by certain query constructs like aggregates. It can also be used to manipulate a data structure
|
||||||
|
before passing it to an ash query.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Ash.Query.build(MyResource, filter: [name: "fred"], sort: [name: :asc], offset: 10)
|
||||||
|
```
|
||||||
|
"""
|
||||||
@spec build(Ash.resource(), Ash.api() | nil, Keyword.t()) :: t()
|
@spec build(Ash.resource(), Ash.api() | nil, Keyword.t()) :: t()
|
||||||
def build(resource, api \\ nil, keyword) do
|
def build(resource, api \\ nil, keyword) do
|
||||||
Enum.reduce(keyword, new(resource, api), fn
|
Enum.reduce(keyword, new(resource, api), fn
|
||||||
|
@ -479,6 +533,7 @@ defmodule Ash.Query do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
def validate_side_load(resource, side_loads, path \\ []) do
|
def validate_side_load(resource, side_loads, path \\ []) do
|
||||||
case do_validate_side_load(resource, side_loads, path) do
|
case do_validate_side_load(resource, side_loads, path) do
|
||||||
[] -> :ok
|
[] -> :ok
|
||||||
|
@ -486,7 +541,7 @@ defmodule Ash.Query do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def do_validate_side_load(_resource, %Ash.Query{} = query, path) do
|
defp do_validate_side_load(_resource, %Ash.Query{} = query, path) do
|
||||||
if query.limit || (query.offset && query.offset != 0) do
|
if query.limit || (query.offset && query.offset != 0) do
|
||||||
[{:error, InvalidQuery.exception(query: query, side_load_path: Enum.reverse(path))}]
|
[{:error, InvalidQuery.exception(query: query, side_load_path: Enum.reverse(path))}]
|
||||||
else
|
else
|
||||||
|
@ -506,11 +561,11 @@ defmodule Ash.Query do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def do_validate_side_load(resource, {atom, _} = tuple, path) when is_atom(atom) do
|
defp do_validate_side_load(resource, {atom, _} = tuple, path) when is_atom(atom) do
|
||||||
do_validate_side_load(resource, [tuple], path)
|
do_validate_side_load(resource, [tuple], path)
|
||||||
end
|
end
|
||||||
|
|
||||||
def do_validate_side_load(resource, side_loads, path) when is_list(side_loads) do
|
defp do_validate_side_load(resource, side_loads, path) when is_list(side_loads) do
|
||||||
side_loads
|
side_loads
|
||||||
|> List.wrap()
|
|> List.wrap()
|
||||||
|> Enum.flat_map(fn
|
|> Enum.flat_map(fn
|
||||||
|
@ -594,6 +649,22 @@ defmodule Ash.Query do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Sort the results based on attributes or aggregates (calculations are not yet supported)
|
||||||
|
|
||||||
|
Takes a list of fields to sort on, or a keyword list/mixed keyword list of fields and sort directions.
|
||||||
|
The default sort direction is `:asc`.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```
|
||||||
|
Ash.Query.sort(query, [:foo, :bar])
|
||||||
|
|
||||||
|
Ash.Query.sort(query, [:foo, bar: :desc])
|
||||||
|
|
||||||
|
Ash.Query.sort(query, [foo: :desc, bar: :asc])
|
||||||
|
```
|
||||||
|
"""
|
||||||
@spec sort(t() | Ash.resource(), Ash.sort()) :: t()
|
@spec sort(t() | Ash.resource(), Ash.sort()) :: t()
|
||||||
def sort(query, sorts) do
|
def sort(query, sorts) do
|
||||||
query = to_query(query)
|
query = to_query(query)
|
||||||
|
|
1
mix.exs
1
mix.exs
|
@ -41,6 +41,7 @@ defmodule Ash.MixProject do
|
||||||
extra_section: "GUIDES",
|
extra_section: "GUIDES",
|
||||||
extras: [
|
extras: [
|
||||||
"documentation/introduction/getting_started.md",
|
"documentation/introduction/getting_started.md",
|
||||||
|
"documentation/topics/authorization.md",
|
||||||
"documentation/topics/validation.md",
|
"documentation/topics/validation.md",
|
||||||
"documentation/topics/error_handling.md",
|
"documentation/topics/error_handling.md",
|
||||||
"documentation/topics/aggregates.md",
|
"documentation/topics/aggregates.md",
|
||||||
|
|
Loading…
Reference in a new issue