docs: consolidate guides into relationships.md

improvement: add `Ash.Query.apply_to/3`
This commit is contained in:
Zach Daniel 2024-04-07 15:31:26 -04:00
parent 773e9cea91
commit d153b40dea
13 changed files with 206 additions and 101 deletions

View file

@ -362,7 +362,7 @@ A section for declaring relationships on the resource.
Relationships are a core component of resource oriented design. Many components of Ash Relationships are a core component of resource oriented design. Many components of Ash
will use these relationships. A simple use case is loading relationships (done via the `Ash.Query.load/2`). will use these relationships. A simple use case is loading relationships (done via the `Ash.Query.load/2`).
See the [relationships guide](/documentation/topics/relationships.md) for more. See the [relationships guide](/documentation/topics/resources/relationships.md) for more.
### Nested DSLs ### Nested DSLs
@ -429,7 +429,7 @@ Declares a `has_one` relationship. In a relational database, the foreign key wou
Generally speaking, a `has_one` also implies that the destination table is unique on that foreign key. Generally speaking, a `has_one` also implies that the destination table is unique on that foreign key.
See the [relationships guide](/documentation/topics/relationships.md) for more. See the [relationships guide](/documentation/topics/resources/relationships.md) for more.
### Nested DSLs ### Nested DSLs
@ -459,7 +459,7 @@ end
| Name | Type | Default | Docs | | Name | Type | Default | Docs |
|------|------|---------|------| |------|------|---------|------|
| [`manual`](#relationships-has_one-manual){: #relationships-has_one-manual } | `(any, any -> any) \| module` | | A module that implements `Ash.Resource.ManualRelationship`. Also accepts a 2 argument function that takes the source records and the context. | | [`manual`](#relationships-has_one-manual){: #relationships-has_one-manual } | `(any, any -> any) \| module` | | A module that implements `Ash.Resource.ManualRelationship`. Also accepts a 2 argument function that takes the source records and the context. |
| [`no_attributes?`](#relationships-has_one-no_attributes?){: #relationships-has_one-no_attributes? } | `boolean` | | All existing entities are considered related, i.e this relationship is not based on any fields, and `source_attribute` and `destination_attribute` are ignored. See the See the [relationships guide](/documentation/topics/relationships.md) for more. | | [`no_attributes?`](#relationships-has_one-no_attributes?){: #relationships-has_one-no_attributes? } | `boolean` | | All existing entities are considered related, i.e this relationship is not based on any fields, and `source_attribute` and `destination_attribute` are ignored. See the See the [relationships guide](/documentation/topics/resources/relationships.md) for more. |
| [`allow_nil?`](#relationships-has_one-allow_nil?){: #relationships-has_one-allow_nil? } | `boolean` | `true` | Marks the relationship as required. Has no effect on validations, but can inform extensions that there will always be a related entity. | | [`allow_nil?`](#relationships-has_one-allow_nil?){: #relationships-has_one-allow_nil? } | `boolean` | `true` | Marks the relationship as required. Has no effect on validations, but can inform extensions that there will always be a related entity. |
| [`from_many?`](#relationships-has_one-from_many?){: #relationships-has_one-from_many? } | `boolean` | `false` | Signal that this relationship is actually a `has_many` where the first record is given via the `sort`. This will allow data layers to properly deduplicate when necessary. | | [`from_many?`](#relationships-has_one-from_many?){: #relationships-has_one-from_many? } | `boolean` | `false` | Signal that this relationship is actually a `has_many` where the first record is given via the `sort`. This will allow data layers to properly deduplicate when necessary. |
| [`description`](#relationships-has_one-description){: #relationships-has_one-description } | `String.t` | | An optional description for the relationship | | [`description`](#relationships-has_one-description){: #relationships-has_one-description } | `String.t` | | An optional description for the relationship |
@ -528,7 +528,7 @@ has_many name, destination
Declares a `has_many` relationship. There can be any number of related entities. Declares a `has_many` relationship. There can be any number of related entities.
See the [relationships guide](/documentation/topics/relationships.md) for more. See the [relationships guide](/documentation/topics/resources/relationships.md) for more.
### Nested DSLs ### Nested DSLs
@ -558,7 +558,7 @@ end
| Name | Type | Default | Docs | | Name | Type | Default | Docs |
|------|------|---------|------| |------|------|---------|------|
| [`manual`](#relationships-has_many-manual){: #relationships-has_many-manual } | `(any, any -> any) \| module` | | A module that implements `Ash.Resource.ManualRelationship`. Also accepts a 2 argument function that takes the source records and the context. | | [`manual`](#relationships-has_many-manual){: #relationships-has_many-manual } | `(any, any -> any) \| module` | | A module that implements `Ash.Resource.ManualRelationship`. Also accepts a 2 argument function that takes the source records and the context. |
| [`no_attributes?`](#relationships-has_many-no_attributes?){: #relationships-has_many-no_attributes? } | `boolean` | | All existing entities are considered related, i.e this relationship is not based on any fields, and `source_attribute` and `destination_attribute` are ignored. See the See the [relationships guide](/documentation/topics/relationships.md) for more. | | [`no_attributes?`](#relationships-has_many-no_attributes?){: #relationships-has_many-no_attributes? } | `boolean` | | All existing entities are considered related, i.e this relationship is not based on any fields, and `source_attribute` and `destination_attribute` are ignored. See the See the [relationships guide](/documentation/topics/resources/relationships.md) for more. |
| [`description`](#relationships-has_many-description){: #relationships-has_many-description } | `String.t` | | An optional description for the relationship | | [`description`](#relationships-has_many-description){: #relationships-has_many-description } | `String.t` | | An optional description for the relationship |
| [`destination_attribute`](#relationships-has_many-destination_attribute){: #relationships-has_many-destination_attribute } | `atom` | | The attribute on the related resource that should match the `source_attribute` configured on this resource. | | [`destination_attribute`](#relationships-has_many-destination_attribute){: #relationships-has_many-destination_attribute } | `atom` | | The attribute on the related resource that should match the `source_attribute` configured on this resource. |
| [`validate_destination_attribute?`](#relationships-has_many-validate_destination_attribute?){: #relationships-has_many-validate_destination_attribute? } | `boolean` | `true` | Whether or not to validate that the destination field exists on the destination resource | | [`validate_destination_attribute?`](#relationships-has_many-validate_destination_attribute?){: #relationships-has_many-validate_destination_attribute? } | `boolean` | `true` | Whether or not to validate that the destination field exists on the destination resource |
@ -627,7 +627,7 @@ Declares a `many_to_many` relationship. Many to many relationships require a joi
A join resource is a resource that consists of a relationship to the source and destination of the many to many. A join resource is a resource that consists of a relationship to the source and destination of the many to many.
See the [relationships guide](/documentation/topics/relationships.md) for more. See the [relationships guide](/documentation/topics/resources/relationships.md) for more.
### Nested DSLs ### Nested DSLs
@ -735,7 +735,7 @@ Declares a `belongs_to` relationship. In a relational database, the foreign key
This creates a field on the resource with the corresponding name and type, unless `define_attribute?: false` is provided. This creates a field on the resource with the corresponding name and type, unless `define_attribute?: false` is provided.
See the [relationships guide](/documentation/topics/relationships.md) for more. See the [relationships guide](/documentation/topics/resources/relationships.md) for more.
### Nested DSLs ### Nested DSLs

View file

@ -37,6 +37,7 @@ Welcome to the Ash Framework documentation! Here you will find everything you ne
- [Calculations](documentation/topics/resources/calculations.md) - [Calculations](documentation/topics/resources/calculations.md)
- [Aggregates](documentation/topics/resources/aggregates.md) - [Aggregates](documentation/topics/resources/aggregates.md)
- [Code Interfaces](documentation/topics/resources/code-interfaces.md) - [Code Interfaces](documentation/topics/resources/code-interfaces.md)
- [Relationships](documentation/topics/resources/relationships.md)
### Actions ### Actions

View file

@ -1,62 +0,0 @@
# Define Manual Relationships
Manual relationships allow you to express complex or non-typical relationships between resources in a standard way.
Individual data layers may interact with manual relationships in their own way, so see their corresponding guides.
By default, the only thing manual relationships support is being loaded.
## Example
In our Helpdesk example, we'd like to have a way to find tickets
In the `Rep?` resource, define a `has_many` relationship as `manual` and point to the module where
it will be implemented.
```elixir
relationships do
has_many :tickets_above_threshold, Helpdesk.Support.Ticket do
manual Helpdesk.Support.Ticket.Relationships.TicketsAboveThreshold
end
end
```
Using Ash to get the destination records is ideal, so you can authorize access like normal
but if you need to use a raw ecto query here, you can. As long as you return the right structure.
The `TicketsAboveThreshold` module is implemented as follows.
```elixir
defmodule Helpdesk.Support.Ticket.Relationships.TicketsAboveThreshold do
use Ash.Resource.ManualRelationship
require Ash.Query
def load(records, _opts, %{query: query, actor: actor, authorize?: authorize?}) do
# Use existing records to limit resultds
rep_ids = Enum.map(records, & &1.id)
{:ok,
query
|> Ash.Query.filter(representative_id in ^rep_ids)
|> Ash.Query.filter(priority > representative.priority_threshold)
|> Helpdesk.Support.read!(actor: actor, authorize?: authorize?)
# Return the items grouped by the primary key of the source, i.e representative.id => [...tickets above threshold]
|> Enum.group_by(& &1.representative_id)}
end
end
```
## Using the Query
Since you likely want to support things like filtering your relationship when being loaded, you will want to make sure that you use the query being provided. However, depending on how you're loading the relationship, you may need to do things like fetch extra records. To do this, you might do things like
```elixir
def load(records, _opts, %{query: query, ..}) do
# unset some fields
fetch_query = Ash.Query.unset(query, [:limit, :offset])
# or, to be more safe/explicit, you might make a new query, explicitly setting only a few fields
fetch_query = query.resource |> Ash.Query.filter(^query.filter) |> Ash.Query.sort(query.sort)
...
end
```

View file

@ -14,7 +14,7 @@ Each action has its own set of options, ways of calling it, and ways of customiz
Primary actions are a way to inform the framework which actions should be used in certain "automated" circumstances, or in cases where an action has not been specified. If a primary action is attempted to be used but does not exist, you will get an error about it at runtime. Primary actions are a way to inform the framework which actions should be used in certain "automated" circumstances, or in cases where an action has not been specified. If a primary action is attempted to be used but does not exist, you will get an error about it at runtime.
The place you typically need primary actions is when [Managing Relationships](/documentation/topics/managing-relationships.md). When using the `defaults` option to add default actions, they are marked as primary. The place you typically need primary actions is when [Managing Relationships](/documentation/topics/resources/relationships.md#managing-relationships.md). When using the `defaults` option to add default actions, they are marked as primary.
A simple example where a primary action would be used: A simple example where a primary action would be used:
@ -43,7 +43,7 @@ actions do
end end
``` ```
But that is just a simple way to get started, or to create resources that really don't do anything beyond those four operations. You can have _as many actions as you want_. The best designed Ash applications will have numerous actions, named after the intent behind how they are used. They won't have all reads going through a single read action, and the same goes for the other action types. The richer the actions on the resource, the better interface you can have. With that said, many resources may only have those four basic actions, especially those that are "managed" through some parent resource. See the guide on [Managing Relationships](/documentation/topics/managing-relationships.md) for more. But that is just a simple way to get started, or to create resources that really don't do anything beyond those four operations. You can have _as many actions as you want_. The best designed Ash applications will have numerous actions, named after the intent behind how they are used. They won't have all reads going through a single read action, and the same goes for the other action types. The richer the actions on the resource, the better interface you can have. With that said, many resources may only have those four basic actions, especially those that are "managed" through some parent resource. See the guide on [Managing Relationships](/documentation/topics/resources/relationships.md#managing-relationships.md) for more.
### Put everything inside the action ### Put everything inside the action

View file

@ -32,7 +32,7 @@ The underlying record can be retrieved from `changeset.data` for update and dest
## Manual Read Actions ## Manual Read Actions
Manual read actions work the same, except the will also get the "data layer query". For AshPostgres, this means you get the ecto query that would have been run. Manual read actions work the same, except the will also get the "data layer query". For AshPostgres, this means you get the ecto query that would have been run. You can use `Ash.Query.apply_to/3` to apply a query to records in memory. This allows you to fetch the data in a way that is not possible with the data layer, but still honor the query that was provided to.
```elixir ```elixir
# in the resource # in the resource

View file

@ -83,13 +83,15 @@ Using this, you might make requests like the following:
```elixir ```elixir
# Get the first ten records # Get the first ten records
Ash.read(Resource, page: [limit: 10]) Ash.read(Resource, page: [limit: 10])
# or by using an action named `read` directly # or by using an action named `read` directly through a
Resource.read(page: [limit: 10]) # code interface on the domain
Domain.read(page: [limit: 10])
# Get the next ten records # Get the next ten records
Ash.read(Resource, page: [limit: 10, offset: 10]) Ash.read(Resource, page: [limit: 10, offset: 10])
# or by using an action named `read` directly # or by using an action named `read` directly through a
Resource.read(page: [limit: 10, offset: 10]) # code interface on the domain
Domain.read(page: [limit: 10, offset: 10])
``` ```
Next/previous page requests can also be made in memory, using an existing page of search results: Next/previous page requests can also be made in memory, using an existing page of search results:

View file

@ -107,7 +107,7 @@ See `Ash.Query` for more.
Relationships are named links between resources, that define how they relate to each other. Relationships can be used to signify ownership of a record, membership of a group, or can be used in filtering and querying data. Relationships are named links between resources, that define how they relate to each other. Relationships can be used to signify ownership of a record, membership of a group, or can be used in filtering and querying data.
See the [Relationships guide](/documentation/topics/relationships.md) for more. See the [Relationships guide](/documentation/topics/resources/relationships.md) for more.
## Resource ## Resource

View file

@ -29,7 +29,7 @@ end
## Managing related data ## Managing related data
See [Managing Relationships](/documentation/topics/managing-relationships.md) for more information. See [Managing Relationships](/documentation/topics/resources/relationships.md#managing-relationships.md) for more information.
Your data layer may enforce foreign key constraints, see the following guides for more information: Your data layer may enforce foreign key constraints, see the following guides for more information:
@ -304,17 +304,159 @@ Ash.load(users, followers: followers)
## no_attributes? true ## no_attributes? true
This can be very useful when combined with multitenancy. Specifically, if you have a tenant resource like `Organization`, This is really useful when creating customized relationships that aren't joined with simple attribute matches. For example:
```elixir
has_many :higher_priority_tickets, __MODULE__ do
no_attributes? true
# parent/1 in this case puts the expression on this current resource
# so this is "tickets with priority higher than this ticket"
filter expr(priority > parent(priority))
end
```
This can also be very useful when combined with multitenancy. Specifically, if you have a tenant resource like `Organization`,
you can use `no_attributes?` to do things like `has_many :employees, Employee, no_attributes?: true`, which lets you avoid having an you can use `no_attributes?` to do things like `has_many :employees, Employee, no_attributes?: true`, which lets you avoid having an
unnecessary `organization_id` field on `Employee`. The same works in reverse: `has_one :organization, Organization, no_attributes?: true` unnecessary `organization_id` field on `Employee`. The same works in reverse: `has_one :organization, Organization, no_attributes?: true`
allows relating the employee to their organization. allows relating the employee to their organization.
Some important caveats here:
1. You can still manage relationships from one to the other, but "relate" and "unrelate" > ### caveats for using `no_attributes?` {: .warning}
will have no effect, because there are no fields to change. >
> 1. You can still manage relationships from one to the other, but "relate" and "unrelate" will have no effect, because there are no fields to change.
> 2. Loading the relationship on a list of resources will not behave as expected in all circumstances involving multitenancy. For example, if you get a list of `Organization` and then try to load `employees`, you would need to set a single tenant on the load query, meaning you'll get all organizations back with the set of employees from one tenant. This could eventually be solved, but for now it is considered an edge case.
2. Loading the relationship on a list of resources will not behave as expected in all circumstances involving multitenancy. For example,
if you get a list of `Organization` and then try to load `employees`, you would need to set a single tenant on the load query, meaning ## Manual Relationships
you'll get all organizations back with the set of employees from one tenant. This could eventually be solved, but for now it is considered an
edge case. Manual relationships allow you to express complex or non-typical relationships between resources in a standard way. Individual data layers may interact with manual relationships in their own way, so see their corresponding guides. In general, you should try to use manual relationships sparingly, as you can do *a lot* with filters on relationships, and the `no_attributes?` flag.
### Example
In our Helpdesk example, we'd like to have a way to find tickets
In the `Rep?` resource, define a `has_many` relationship as `manual` and point to the module where
it will be implemented.
```elixir
relationships do
has_many :tickets_above_threshold, Helpdesk.Support.Ticket do
manual Helpdesk.Support.Ticket.Relationships.TicketsAboveThreshold
end
end
```
Using Ash to get the destination records is ideal, so you can authorize access like normal
but if you need to use a raw ecto query here, you can. As long as you return the right structure.
The `TicketsAboveThreshold` module is implemented as follows.
```elixir
defmodule Helpdesk.Support.Ticket.Relationships.TicketsAboveThreshold do
use Ash.Resource.ManualRelationship
require Ash.Query
def load(records, _opts, %{query: query, actor: actor, authorize?: authorize?}) do
# Use existing records to limit resultds
rep_ids = Enum.map(records, & &1.id)
{:ok,
query
|> Ash.Query.filter(representative_id in ^rep_ids)
|> Ash.Query.filter(priority > representative.priority_threshold)
|> Helpdesk.Support.read!(actor: actor, authorize?: authorize?)
# Return the items grouped by the primary key of the source, i.e representative.id => [...tickets above threshold]
|> Enum.group_by(& &1.representative_id)}
end
end
```
### Reusing the Query
Since you likely want to support things like filtering your relationship when being loaded, you will want to make sure that you use the query being provided. However, depending on how you're loading the relationship, you may need to do things like fetch extra records. To do this, you might do things like
```elixir
def load(records, _opts, %{query: query, ..}) do
# unset some fields
fetch_query = Ash.Query.unset(query, [:limit, :offset])
# or, to be more safe/explicit, you might make a new query, explicitly setting only a few fields
fetch_query = query.resource |> Ash.Query.filter(^query.filter) |> Ash.Query.sort(query.sort)
...
end
```
### Fetching the records and then applying a query
Lets say the records come from some totally unrelated source, or you can't just modify the query to fetch the records you need. You can fetch the records you need and then apply the query to them in memory.
```elixir
def load(records, _opts, %{query: query, ..}) do
# fetch the data from the other source, which is capabale of sorting
data = get_other_data(data, query.sort)
query
# unset the limit and offset since we already applied that
|> Ash.Query.unset([:sort])
# apply the query in memory (filtering, distinct, limit, offset)
|> Ash.Query.apply_to(data)
end
```
## Managing Relationships
In Ash, managing related data is done via `Ash.Changeset.manage_relationship/4`. There are various ways to leverage the functionality expressed there. If you are working with changesets directly, you can call that function. However, if you want that logic to be portable (e.g available in `ash_graphql` mutations and `ash_json_api` actions), then you want to use the following `argument` + `change` pattern:
```elixir
actions do
update :update do
argument :add_comment, :map do
allow_nil? false
end
argument :tags, {:array, :uuid} do
allow_nil? false
end
# First argument is the name of the action argument to use
# Second argument is the relationship to be managed
# Third argument is options. For more, see `Ash.Changeset.manage_relationship/4`. This accepts the same options.
change manage_relationship(:add_comment, :comments, type: :create)
# Second argument can be omitted, as the argument name is the same as the relationship
change manage_relationship(:tags, type: :append_and_remove)
end
end
```
With this, those arguments can be used in action input:
```elixir
post
|> Ash.Changeset.for_update(:update, tags: [tag1.id, tag2.id], add_comment: %{text: "comment text"})
|> Ash.update!()
```
### Argument Types
Notice how we provided a map as input to `add_comment`, and a list of UUIDs as an input to `manage_relationship`. When providing maps or lists of maps, you are generally just providing input that will eventually be passed into actions on the destination resource. However, you can also provide individual values or lists of values. By default, we assume that value maps to the primary key of the destination resource, but you can use the `value_is_key` option to modify that behavior. For example, if you wanted adding a comment to take a list of strings, you could say:
```elixir
argument :add_comment, :string
...
change manage_relationship(:add_comment, :comments, type: :create, value_is_key: :text)
```
And then you could use it like so:
```elixir
post
|> Ash.Changeset.for_update(:update, tags: [tag1.id, tag2.id], add_comment: "comment text")
|> Ash.update!()
```
### Derived behavior
Determining what will happen when managing related data can be complicated, as the nature of the problem itself is quite complicated. In some simple cases, like `type: :create`, there may be only one action that will be called. But in order to support all of the various ways that related resources may need to be managed, Ash provides a very rich set of options to determine what happens with the provided input. Tools like `AshPhoenix.Form` can look at your arguments that have a corresponding `manage_relationship` change, and derive the structure of those nested forms. Tools like `AshGraphql` can derive complex input objects to allow manipulating those relationships over a graphql Api. This all works because the options are, ultimately, quite explicit. It can be determined exactly what actions might be called, and therefore what input could be needed.

View file

@ -519,7 +519,7 @@ You may notice that if you don't add the resource to your domain, or if you don'
## Working with relationships ## Working with relationships
There are a wide array of options when managing relationships, and we won't cover all of them here. See the guide on [Managing Relationships](/documentation/topics/managing-relationships.md) for a full explanation. There are a wide array of options when managing relationships, and we won't cover all of them here. See the guide on [Managing Relationships](/documentation/topics/resources/relationships.md#managing-relationships.md) for a full explanation.
In this example we'll demonstrate the use of action arguments, the method by which you can accept additional input to an action. In this example we'll demonstrate the use of action arguments, the method by which you can accept additional input to an action.

View file

@ -2732,6 +2732,32 @@ defmodule Ash.Query do
end end
end end
@spec apply_to(t(), records :: list(Ash.Resource.record()), opts :: Keyword.t()) ::
{:ok, list(Ash.Resource.record())}
def apply_to(query, records, opts \\ []) do
domain =
query.domain || Ash.Resource.Info.domain(query.resource) || opts[:domain] ||
raise ArgumentError,
"Could not determine domain for #{inspect(query)}, please provide the `:domain` option."
with {:ok, records} <-
Ash.Filter.Runtime.filter_matches(domain, records, query.filter, parent: opts[:parent]),
records <- Sort.runtime_sort(records, query.distinct_sort || query.sort, domain: domain),
records <- Sort.runtime_distinct(records, query.distinct, domain: domain),
records <- Sort.runtime_sort(records, query.sort, domain: domain),
records <- Enum.drop(records, query.offset),
records <- do_limit(records, query.limit),
{:ok, records} <- Ash.load(records, query, domain: domain) do
{:ok, records}
else
{:error, error} ->
{:error, Ash.Error.to_ash_error(error)}
end
end
defp do_limit(records, nil), do: records
defp do_limit(records, limit), do: Enum.take(records, limit)
@spec unset(Ash.Resource.t() | t(), atom | [atom]) :: t() @spec unset(Ash.Resource.t() | t(), atom | [atom]) :: t()
def unset(query, keys) when is_list(keys) do def unset(query, keys) when is_list(keys) do
query = new(query) query = new(query)

View file

@ -199,7 +199,7 @@ defmodule Ash.Resource.Dsl do
Generally speaking, a `has_one` also implies that the destination table is unique on that foreign key. Generally speaking, a `has_one` also implies that the destination table is unique on that foreign key.
See the [relationships guide](/documentation/topics/relationships.md) for more. See the [relationships guide](/documentation/topics/resources/relationships.md) for more.
""", """,
examples: [ examples: [
""" """
@ -225,7 +225,7 @@ defmodule Ash.Resource.Dsl do
describe: """ describe: """
Declares a `has_many` relationship. There can be any number of related entities. Declares a `has_many` relationship. There can be any number of related entities.
See the [relationships guide](/documentation/topics/relationships.md) for more. See the [relationships guide](/documentation/topics/resources/relationships.md) for more.
""", """,
examples: [ examples: [
""" """
@ -253,7 +253,7 @@ defmodule Ash.Resource.Dsl do
A join resource is a resource that consists of a relationship to the source and destination of the many to many. A join resource is a resource that consists of a relationship to the source and destination of the many to many.
See the [relationships guide](/documentation/topics/relationships.md) for more. See the [relationships guide](/documentation/topics/resources/relationships.md) for more.
""", """,
examples: [ examples: [
""" """
@ -288,7 +288,7 @@ defmodule Ash.Resource.Dsl do
This creates a field on the resource with the corresponding name and type, unless `define_attribute?: false` is provided. This creates a field on the resource with the corresponding name and type, unless `define_attribute?: false` is provided.
See the [relationships guide](/documentation/topics/relationships.md) for more. See the [relationships guide](/documentation/topics/resources/relationships.md) for more.
""", """,
examples: [ examples: [
""" """
@ -317,7 +317,7 @@ defmodule Ash.Resource.Dsl do
Relationships are a core component of resource oriented design. Many components of Ash Relationships are a core component of resource oriented design. Many components of Ash
will use these relationships. A simple use case is loading relationships (done via the `Ash.Query.load/2`). will use these relationships. A simple use case is loading relationships (done via the `Ash.Query.load/2`).
See the [relationships guide](/documentation/topics/relationships.md) for more. See the [relationships guide](/documentation/topics/resources/relationships.md) for more.
""", """,
examples: [ examples: [
""" """

View file

@ -109,7 +109,7 @@ defmodule Ash.Resource.Relationships.SharedOptions do
[ [
type: :boolean, type: :boolean,
doc: """ doc: """
All existing entities are considered related, i.e this relationship is not based on any fields, and `source_attribute` and `destination_attribute` are ignored. See the See the [relationships guide](/documentation/topics/relationships.md) for more. All existing entities are considered related, i.e this relationship is not based on any fields, and `source_attribute` and `destination_attribute` are ignored. See the See the [relationships guide](/documentation/topics/resources/relationships.md) for more.
""" """
]} ]}
end end

12
mix.exs
View file

@ -50,6 +50,7 @@ defmodule Ash.MixProject do
"documentation/topics/resources/embedded-resources.md", "documentation/topics/resources/embedded-resources.md",
"documentation/topics/resources/code-interfaces.md", "documentation/topics/resources/code-interfaces.md",
"documentation/topics/resources/identities.md", "documentation/topics/resources/identities.md",
"documentation/topics/resources/relationships.md",
"documentation/topics/actions/actions.md", "documentation/topics/actions/actions.md",
"documentation/topics/actions/read-actions.md", "documentation/topics/actions/read-actions.md",
"documentation/topics/actions/create-actions.md", "documentation/topics/actions/create-actions.md",
@ -63,7 +64,6 @@ defmodule Ash.MixProject do
"documentation/topics/security/sensitive-data.md", "documentation/topics/security/sensitive-data.md",
"CHANGELOG.md", "CHANGELOG.md",
# below this line under review # below this line under review
"documentation/how_to/defining-manual-relationships.md",
"documentation/how_to/handle-errors.md", "documentation/how_to/handle-errors.md",
"documentation/how_to/structure-your-project.md", "documentation/how_to/structure-your-project.md",
"documentation/how_to/use-without-data-layers.md", "documentation/how_to/use-without-data-layers.md",
@ -73,14 +73,12 @@ defmodule Ash.MixProject do
"documentation/topics/extending-resources.md", "documentation/topics/extending-resources.md",
"documentation/topics/expressions.md", "documentation/topics/expressions.md",
"documentation/topics/reference/glossary.md", "documentation/topics/reference/glossary.md",
"documentation/topics/managing-relationships.md",
"documentation/topics/monitoring.md", "documentation/topics/monitoring.md",
"documentation/topics/multitenancy.md", "documentation/topics/multitenancy.md",
"documentation/topics/notifiers.md", "documentation/topics/notifiers.md",
"documentation/topics/security/policies.md", "documentation/topics/security/policies.md",
"documentation/topics/pub_sub.md", "documentation/topics/pub_sub.md",
"documentation/topics/reactor.md", "documentation/topics/reactor.md",
"documentation/topics/relationships.md",
"documentation/topics/testing.md", "documentation/topics/testing.md",
"documentation/topics/timeouts.md", "documentation/topics/timeouts.md",
"documentation/topics/validations.md", "documentation/topics/validations.md",
@ -105,7 +103,8 @@ defmodule Ash.MixProject do
"documentation/topics/resources/aggregates.md", "documentation/topics/resources/aggregates.md",
"documentation/topics/resources/embedded-resources.md", "documentation/topics/resources/embedded-resources.md",
"documentation/topics/resources/code-interfaces.md", "documentation/topics/resources/code-interfaces.md",
"documentation/topics/resources/identities.md" "documentation/topics/resources/identities.md",
"documentation/topics/resources/relationships.md"
], ],
Actions: [ Actions: [
"documentation/topics/actions/actions.md", "documentation/topics/actions/actions.md",
@ -114,7 +113,7 @@ defmodule Ash.MixProject do
"documentation/topics/actions/update-actions.md", "documentation/topics/actions/update-actions.md",
"documentation/topics/actions/destroy-actions.md", "documentation/topics/actions/destroy-actions.md",
"documentation/topics/actions/generic-actions.md", "documentation/topics/actions/generic-actions.md",
"documentation/topics/actions/manual-actions.md", "documentation/topics/actions/manual-actions.md"
], ],
Security: [ Security: [
"documentation/topics/security/actors-and-authorization.md", "documentation/topics/security/actors-and-authorization.md",
@ -146,7 +145,6 @@ defmodule Ash.MixProject do
"Under Review": [ "Under Review": [
# Documentation below this line is pending review # Documentation below this line is pending review
"documentation/topics/domains.md", "documentation/topics/domains.md",
"documentation/how_to/defining-manual-relationships.md",
"documentation/how_to/handle-errors.md", "documentation/how_to/handle-errors.md",
"documentation/how_to/structure-your-project.md", "documentation/how_to/structure-your-project.md",
"documentation/how_to/use-without-data-layers.md", "documentation/how_to/use-without-data-layers.md",
@ -154,13 +152,11 @@ defmodule Ash.MixProject do
"documentation/topics/code-interface.md", "documentation/topics/code-interface.md",
"documentation/topics/extending-resources.md", "documentation/topics/extending-resources.md",
"documentation/topics/expressions.md", "documentation/topics/expressions.md",
"documentation/topics/managing-relationships.md",
"documentation/topics/monitoring.md", "documentation/topics/monitoring.md",
"documentation/topics/multitenancy.md", "documentation/topics/multitenancy.md",
"documentation/topics/notifiers.md", "documentation/topics/notifiers.md",
"documentation/topics/pub_sub.md", "documentation/topics/pub_sub.md",
"documentation/topics/reactor.md", "documentation/topics/reactor.md",
"documentation/topics/relationships.md",
"documentation/topics/testing.md", "documentation/topics/testing.md",
"documentation/topics/timeouts.md", "documentation/topics/timeouts.md",
"documentation/topics/validations.md" "documentation/topics/validations.md"