mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 05:23:03 +12:00
docs: more work on guides
This commit is contained in:
parent
6825968ddf
commit
400b148dfb
4 changed files with 157 additions and 160 deletions
|
@ -1,9 +1,88 @@
|
|||
# Calculations
|
||||
|
||||
Calculations in Ash allow for displaying complex values as a top level value of a resource.
|
||||
They are relatively limited in their current form, supporting only functional calculations,
|
||||
where you provide a module that takes a list of records and returns a list of values for that
|
||||
calculation. Eventually, there will be support for calculations that can be embedded into the
|
||||
data layer(for things like postgres) that will allow for sorting and filtering on calculated
|
||||
data.
|
||||
|
||||
taken from `allow_async?`
|
||||
This is useful for calculations that are very expensive, especially when combined with complex filters/join
|
||||
scenarios. By adding this, we will rerun a trimmed down version of the main query, using the primary keys for
|
||||
fast access. This will be done asynchronously for each calculation that has `allow_async?: true`.
|
||||
## Declaring calculations on a resource
|
||||
|
||||
Keep in mind that if the calculation is used in a filter or sort, it cannot be done asynchronously,
|
||||
and *must* be done in the main query.
|
||||
### Expression Calculations
|
||||
|
||||
The simplest kind of calculation simply refers to an Ash expression. For example:
|
||||
|
||||
```elixir
|
||||
calculations do
|
||||
calculate :full_name, :string, expr(first_name <> " " <> last_name)
|
||||
end
|
||||
```
|
||||
|
||||
### Module Calculations
|
||||
|
||||
When calculations require more complex code or can't be pushed down into the data layer, a module that uses `Ash.Calculation` can be used.
|
||||
|
||||
```elixir
|
||||
defmodule Concat do
|
||||
# An example concatenation calculation, that accepts the delimeter as an argument,
|
||||
#and the fields to concatenate as options
|
||||
use Ash.Calculation
|
||||
|
||||
# Optional callback that verifies the passed in options (and optionally transforms them)
|
||||
@impl true
|
||||
def init(opts) do
|
||||
if opts[:keys] && is_list(opts[:keys]) && Enum.all?(opts[:keys], &is_atom/1) do
|
||||
{:ok, opts}
|
||||
else
|
||||
{:error, "Expected a `keys` option for which keys to concat"}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def calculate(records, opts, %{separator: separator}) do
|
||||
Enum.map(records, fn record ->
|
||||
Enum.map_join(opts[:keys], separator, fn key ->
|
||||
to_string(Map.get(record, key))
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
# You can implement this callback to make this calculation possible in the data layer
|
||||
# *and* in elixir. Ash expressions are already executable in Elixir or in the data layer, but this gives you fine grain control over how it is done
|
||||
# @impl true
|
||||
# def expression(opts, context) do
|
||||
# end
|
||||
end
|
||||
|
||||
# Usage in a resource
|
||||
calculations do
|
||||
calculate :full_name, {Concat, keys: [:first_name, :last_name]} do
|
||||
# You currently need to use the [allow_empty?: true, trim?: false] constraints here.
|
||||
# The separator could be an empty string or require a leading or trailing space,
|
||||
# but would be trimmed or even set to `nil` without the constraints.
|
||||
argument :separator, :string, constraints: [allow_empty?: true, trim?: false]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
See the documentation for the calculations section in `Ash.Resource.Dsl` and the `Ash.Calculation` docs for more information.
|
||||
|
||||
The calculations declared on a resource allow for declaring a set of named calculations that can be used by extensions.
|
||||
They can also be loaded in the query using `Ash.Query.load/2`, or after the fact using `c:Ash.Api.load/3`. Calculations declared on the resource will be keys in the resource's struct.
|
||||
|
||||
## Custom calculations in the query
|
||||
|
||||
Example:
|
||||
|
||||
```elixir
|
||||
User
|
||||
|> Ash.Query.new()
|
||||
|> Ash.Query.calculate(:full_name, {Concat, keys: [:first_name, :last_name]}, :string, %{separator: ","})
|
||||
```
|
||||
|
||||
See the documentation for `Ash.Query.calculate/4` for more information.
|
||||
|
||||
## Async loading
|
||||
|
||||
Expensive calculations can be marked as `allow_async?: true`, which will allow Ash to fetch it after the main query is run, in parallel with any other calculations that are being run async. This won't affect calculations that are being filtered on, since that must be placed in the data layer.
|
|
@ -1,16 +1,76 @@
|
|||
# Multitenancy
|
||||
|
||||
# strategy
|
||||
Determine how to perform multitenancy. `:attribute` will expect that an
|
||||
attribute matches the given `tenant`, e.g `org_id`. `context` (the default)
|
||||
implies that the tenant will be passed to the datalayer as context. How a
|
||||
given data layer handles multitenancy will differ depending on the implementation.
|
||||
See the datalayer documentation for more.
|
||||
Multitenancy is the idea of splitting up your data into discrete areas, typically by customer. One of the most common examples of this, is the idea of splitting up a postgres database into "schemas" one for each customer that you have. Then, when making any queries, you ensure to always specify the "schema" you are querying, and you never need to worry about data crossing over between customers. The biggest benefits of this kind of strategy are the simplification of authorization logic, and better performance. Instead of all queries from all customers needing to use the same large table, they are each instead all using their own smaller tables. Another benefit is that it is much easier to delete a single customer's data on request.
|
||||
|
||||
In Ash, there are a two primary strategies for implementing multitenancy. The first (and simplest) works for any data layer that supports filtering, and requires very little maintenance/mental overhead. It is done via expecting a given attribute to line up with the `tenant`, and is called `:attribute`. The second, is based on the data layer backing your resource, and is called `:context`. For information on
|
||||
context based multitenancy, see the documentation of your datalayer. For example, `AshPostgres` uses postgres schemas. While the `:attribute` strategy is simple to implement, it also offers fewer advantages, primarily acting as another way to ensure your data is filtered to the correct tenant.
|
||||
|
||||
# global?
|
||||
## Attribute Multitenancy
|
||||
|
||||
This allows running queries
|
||||
and making changes without setting a tenant. This may eventually be extended to support
|
||||
describing the relationship to global data. For example, perhaps the global data is
|
||||
shared among all tenants (requiring "union" support in data layers), or perhaps global
|
||||
data is "merged" using some strategy (also requiring "union" support).
|
||||
```elixir
|
||||
defmodule MyApp.Users do
|
||||
use Ash.Resource, ...
|
||||
|
||||
multitenancy do
|
||||
strategy :attribute
|
||||
attribute :organization_id
|
||||
end
|
||||
|
||||
...
|
||||
|
||||
relationships do
|
||||
belongs_to :organization, MyApp.Organization
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
In this case, if you were to try to run a query without specifying a tenant, you would get an error telling you that the tenant is required.
|
||||
|
||||
Setting the tenant when using the code API is done via `Ash.Query.set_tenant/2` and `Ash.Changeset.set_tenant/2`. If you are using an extension, such as `AshJsonApi` or `AshGraphql` the method of setting tenant context is explained in that extension's documentation.
|
||||
|
||||
Example usage of the above:
|
||||
|
||||
```elixir
|
||||
# Error when not setting a tenant
|
||||
MyApp.Users
|
||||
|> Ash.Query.filter(name == "fred")
|
||||
|> MyApi.read!()
|
||||
** (Ash.Error.Unknown)
|
||||
|
||||
* "Queries against the Helpdesk.Accounts.User resource require a tenant to be specified"
|
||||
(ash 1.22.0) lib/ash/api/api.ex:944: Ash.Api.unwrap_or_raise!/2
|
||||
|
||||
# Automatically filtering by `organization_id == 1`
|
||||
MyApp.Users
|
||||
|> Ash.Query.filter(name == "fred")
|
||||
|> Ash.Query.set_tenant(1)
|
||||
|> MyApi.read!()
|
||||
|
||||
[...]
|
||||
|
||||
# Automatically setting `organization_id` to `1`
|
||||
MyApp.Users
|
||||
|> Ash.Changeset.new(name: "fred")
|
||||
|> Ash.Changeset.set_tenant(1)
|
||||
|> MyApi.create!()
|
||||
|
||||
%MyApp.User{organization_id: 1}
|
||||
```
|
||||
|
||||
If you want to enable running queries _without_ a tenant as well as queries with a tenant, the `global?` option supports this. You will likely need to incorporate this ability into any authorization rules though, to ensure that users from one tenant can't access other tenant's data.
|
||||
|
||||
```elixir
|
||||
multitenancy do
|
||||
strategy :attribute
|
||||
attribute :organization_id
|
||||
global? true
|
||||
end
|
||||
```
|
||||
|
||||
You can also provide the `parse_attribute?` option if the tenant being set doesn't exactly match the attribute value, e.g the tenant is `org_10` and the attribute is `organization_id`, which requires just `10`.
|
||||
|
||||
## Context Multitenancy
|
||||
|
||||
Context multitenancy allows for the data layer to dictate how multitenancy works. For example, a csv data layer might implement multitenancy via saving the file with different suffixes, or an API wrapping data layer might use different subdomains for the tenant.
|
||||
|
||||
For `AshPostgres` context multitenancy, which uses postgres schemas, see the [guide](https://hexdocs.pm/ash_postgres/multitenancy.html)
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
# Calculations
|
||||
|
||||
Calculations in Ash allow for displaying complex values as a top level value of a resource.
|
||||
They are relatively limited in their current form, supporting only functional calculations,
|
||||
where you provide a module that takes a list of records and returns a list of values for that
|
||||
calculation. Eventually, there will be support for calculations that can be embedded into the
|
||||
data layer(for things like postgres) that will allow for sorting and filtering on calculated
|
||||
data.
|
||||
|
||||
## Declaring calculations on a resource
|
||||
|
||||
Example:
|
||||
|
||||
```elixir
|
||||
defmodule Concat do
|
||||
# An example concatenation calculation, that accepts the delimeter as an argument,
|
||||
#and the fields to concatenate as options
|
||||
use Ash.Calculation, type: :string
|
||||
|
||||
# Optional callback that verifies the passed in options (and optionally transforms them)
|
||||
@impl true
|
||||
def init(opts) do
|
||||
if opts[:keys] && is_list(opts[:keys]) && Enum.all?(opts[:keys], &is_atom/1) do
|
||||
{:ok, opts}
|
||||
else
|
||||
{:error, "Expected a `keys` option for which keys to concat"}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def calculate(records, opts, %{separator: separator}) do
|
||||
Enum.map(records, fn record ->
|
||||
Enum.map_join(opts[:keys], separator, fn key ->
|
||||
to_string(Map.get(record, key))
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
# Usage in a resource
|
||||
calculations do
|
||||
calculate :full_name, {Concat, keys: [:first_name, :last_name]} do
|
||||
# You currently need to use the [allow_empty?: true, trim?: false] constraints here.
|
||||
# The separator could be an empty string or require a leading or trailing space,
|
||||
# but would be trimmed or even set to `nil` without the constraints.
|
||||
argument :separator, :string, constraints: [allow_empty?: true, trim?: false]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
See the documentation for the calculations section in `Ash.Resource.Dsl` and the `Ash.Calculation` docs for more information.
|
||||
|
||||
The calculations declared on a resource allow for declaring a set of named calculations that can be used by extensions.
|
||||
They can also be loaded in the query using `Ash.Query.load/2`, or after the fact using `c:Ash.Api.load/3`. Calculations declared on the resource will be keys in the resource's struct.
|
||||
|
||||
## Custom calculations in the query
|
||||
|
||||
Example:
|
||||
|
||||
```elixir
|
||||
User
|
||||
|> Ash.Query.new()
|
||||
|> Ash.Query.calculate(:full_name, {Concat, keys: [:first_name, :last_name]}, :string, %{separator: ","})
|
||||
```
|
||||
|
||||
See the documentation for `Ash.Query.calculate/4` for more information.
|
|
@ -1,76 +0,0 @@
|
|||
# Multitenancy
|
||||
|
||||
Multitenancy is the idea of splitting up your data into discrete areas, typically by customer. One of the most common examples of this, is the idea of splitting up a postgres database into "schemas" one for each customer that you have. Then, when making any queries, you ensure to always specify the "schema" you are querying, and you never need to worry about data crossing over between customers. The biggest benefits of this kind of strategy are the simplification of authorization logic, and better performance. Instead of all queries from all customers needing to use the same large table, they are each instead all using their own smaller tables. Another benefit is that it is much easier to delete a single customer's data on request.
|
||||
|
||||
In Ash, there are a two primary strategies for implementing multitenancy. The first (and simplest) works for any data layer that supports filtering, and requires very little maintenance/mental overhead. It is done via expecting a given attribute to line up with the `tenant`, and is called `:attribute`. The second, is based on the data layer backing your resource, and is called `:context`. For information on
|
||||
context based multitenancy, see the documentation of your datalayer. For example, `AshPostgres` uses postgres schemas. While the `:attribute` strategy is simple to implement, it also offers fewer advantages, primarily acting as another way to ensure your data is filtered to the correct tenant.
|
||||
|
||||
## Attribute Multitenancy
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.Users do
|
||||
use Ash.Resource, ...
|
||||
|
||||
multitenancy do
|
||||
strategy :attribute
|
||||
attribute :organization_id
|
||||
end
|
||||
|
||||
...
|
||||
|
||||
relationships do
|
||||
belongs_to :organization, MyApp.Organization
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
In this case, if you were to try to run a query without specifying a tenant, you would get an error telling you that the tenant is required.
|
||||
|
||||
Setting the tenant when using the code API is done via `Ash.Query.set_tenant/2` and `Ash.Changeset.set_tenant/2`. If you are using an extension, such as `AshJsonApi` or `AshGraphql` the method of setting tenant context is explained in that extension's documentation.
|
||||
|
||||
Example usage of the above:
|
||||
|
||||
```elixir
|
||||
# Error when not setting a tenant
|
||||
MyApp.Users
|
||||
|> Ash.Query.filter(name == "fred")
|
||||
|> MyApi.read!()
|
||||
** (Ash.Error.Unknown)
|
||||
|
||||
* "Queries against the Helpdesk.Accounts.User resource require a tenant to be specified"
|
||||
(ash 1.22.0) lib/ash/api/api.ex:944: Ash.Api.unwrap_or_raise!/2
|
||||
|
||||
# Automatically filtering by `organization_id == 1`
|
||||
MyApp.Users
|
||||
|> Ash.Query.filter(name == "fred")
|
||||
|> Ash.Query.set_tenant(1)
|
||||
|> MyApi.read!()
|
||||
|
||||
[...]
|
||||
|
||||
# Automatically setting `organization_id` to `1`
|
||||
MyApp.Users
|
||||
|> Ash.Changeset.new(name: "fred")
|
||||
|> Ash.Changeset.set_tenant(1)
|
||||
|> MyApi.create!()
|
||||
|
||||
%MyApp.User{organization_id: 1}
|
||||
```
|
||||
|
||||
If you want to enable running queries _without_ a tenant as well as queries with a tenant, the `global?` option supports this. You will likely need to incorporate this ability into any authorization rules though, to ensure that users from one tenant can't access other tenant's data.
|
||||
|
||||
```elixir
|
||||
multitenancy do
|
||||
strategy :attribute
|
||||
attribute :organization_id
|
||||
global? true
|
||||
end
|
||||
```
|
||||
|
||||
You can also provide the `parse_attribute?` option if the tenant being set doesn't exactly match the attribute value, e.g the tenant is `org_10` and the attribute is `organization_id`, which requires just `10`.
|
||||
|
||||
## Context Multitenancy
|
||||
|
||||
Context multitenancy allows for the data layer to dictate how multitenancy works. For example, a csv data layer might implement multitenancy via saving the file with different suffixes, or an API wrapping data layer might use different subdomains for the tenant.
|
||||
|
||||
For `AshPostgres` context multitenancy, which uses postgres schemas, see the [guide](https://hexdocs.pm/ash_postgres/multitenancy.html)
|
Loading…
Reference in a new issue