docs: more work on guides

This commit is contained in:
Zach Daniel 2022-08-22 22:48:09 -04:00
parent 6825968ddf
commit 400b148dfb
4 changed files with 157 additions and 160 deletions

View file

@ -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.

View file

@ -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)

View file

@ -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.

View file

@ -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)