docs: more guidance on project structure

This commit is contained in:
Zach Daniel 2024-04-08 15:54:08 -04:00
parent 4cbf2d3e14
commit 9e5675847e
7 changed files with 94 additions and 77 deletions

View file

@ -21,7 +21,7 @@ Welcome to the Ash Framework documentation! Here you will find everything you ne
---
**Reference** documentation is produced automatically from our source code. It comes in the form of module documentation and DSL documentation. This documentation is **information-oriented**. Use the sidebar and the searchbar to find relevant reference information.
[**Reference**](#reference) documentation is produced automatically from our source code. It comes in the form of module documentation and DSL documentation. This documentation is **information-oriented**. Use the sidebar and the searchbar to find relevant reference information.
## Tutorials
@ -31,6 +31,12 @@ Welcome to the Ash Framework documentation! Here you will find everything you ne
## Topics
### About Ash
- [What is Ash?](documentation/topics/about_ash/what-is-ash.md)
- [Our Design Principles](documentation/topics/about_ash/design-principles.md)
- [Contributing to Ash](documentation/topics/about_ash/contributing-to-ash.md)
### Resources
- [Attributes](documentation/topics/resources/attributes.md)
@ -73,12 +79,6 @@ Welcome to the Ash Framework documentation! Here you will find everything you ne
- [Reactor](documentation/topics/advanced/reactor.md)
- [Timeouts](documentation/topics/advanced/timeouts.md)
### About Ash
- [What is Ash?](documentation/topics/about_ash/what-is-ash.md)
- [Our Design Principles](documentation/topics/about_ash/design-principles.md)
- [Contributing to Ash](documentation/topics/about_ash/contributing-to-ash.md)
---
## How To
@ -96,6 +96,7 @@ Welcome to the Ash Framework documentation! Here you will find everything you ne
- [Ash.Policy.Authorizer DSL](documentation/dsls/DSL:-Ash.Policy.Authorizer.md)
- [Ash.DataLayer.Ets DSL](documentation/dsls/DSL:-Ash.DataLayer.Ets.md)
- [Ash.DataLayer.Mnesia DSL](documentation/dsls/DSL:-Ash.DataLayer.Mnesia.md)
- For other reference documentation, see the sidebar & search bar
## Packages

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.
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.
The place you typically need primary actions is when [Managing Relationships](/documentation/topics/resources/relationships.md#managing-relationships). When using the `defaults` option to add default actions, they are marked as primary.
A simple example where a primary action would be used:
@ -43,7 +43,7 @@ actions do
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/resources/relationships.md#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) for more.
### Put everything inside the action

View file

@ -1,4 +1,4 @@
# Structure your project
# Project Structure
In this guide we'll discuss some best practices for how to structure your project. These recommendations align well with [Elixir conventions](https://hexdocs.pm/elixir/1.16.2/naming-conventions.html#casing) around file and module naming. These conventions allow for a logical coupling of module and file names, and help keep your project organized and easy to navigate.
@ -32,3 +32,73 @@ lib/ # top level lib folder for your whole project
Generally speaking, your Ash application lives in the standard place within your elixir application, i.e `lib/my_app`. Within that folder, you create one folder for each context that you have. Each context has an `Ash.Domain` module within it, and the resources that live within that context. All resource interaction ultimately goes through a domain module.
Alongside the domain module, you have your resources, as well as any other files used in the context. If a resource has any additional files that are used to implement it, they should be placed in a folder with the same name as the resource, in subfolders grouping the files by type. Feel free to choose another logical grouping, but we've found by-type to be effective.
# Where do I put X thing
The purpose of Ash is to be both the model of and the interface to your domain logic (A.K.A business logic). Applying this generally looks like building as much of your domain logic "behind" your resources. This does not mean, however, that everything has to go _inside of_ your resources. For example, if you have a `Purchase` resource, and you want to be able to display a list of purchases that were taxable, and also calculate the percentage of the purchase that was taxable. You might have an action called `:taxable` and a calculation called `:percentage_tax`.
## Example 1: Reads & Calculations
```elixir
actions do
...
read :taxable do
filter expr(taxable == true)
end
end
calculations do
calculate :percentage_tax, :decimal, expr(
sum(line_items, field: :amount, query: [filter: tax == true]) /
sum(line_items, field: :amount)
)
end
```
In practice, you may not need the `taxable` action, i.e perhaps you simply want a "taxable" checkbox on a list view in your application, in which case you may use the primary read, or some other read like `:transaction_report`. You would then, on the consumer, provide the filter for `taxable == true`, and load the `:percentage_tax` calculation.
## Example 2: Using external data in create actions
Lets say you want the user to fill in a github issue id, and you will fetch information from that github issue to use as part of creating a "ticket" in your system.. You might be tempted to do something like this in a LiveView:
```elixir
def handle_event("link_ticket", %{"issue_id" => issue_id}, socket) do
issue_info = GithubApi.get_issue(issue_id)
MyApp.Support.update_ticket(socket.assigns.ticket_id, %{issue_info: %{
title: issue_info.title,
body: issue_info.body
}})
end
```
But this is putting business logic inside of your UI/representation layer. Instead, you should write an action and put this logic inside of it.
```elixir
defmodule MyApp.Ticket.FetchIssueInfo do
use Ash.Resource.Changeo
def change(changeset, _, _) do
Ash.Changeset.before_transaction(changeset, fn changeset ->
issue_info = GithubApi.get_issue(changeset.arguments.issue_id)
Ash.Changeset.force_change_attributes(changeset, %{issue_info: %{
title: issue_info.title,
body: issue_info.body
}})
end
end
```
Then you'd have an action like this:
```elixir
update :link_ticket do
argument :issue_id, :string, allow_nil?: false
change MyApp.Ticket.FetchIssueInfo
end
```
This cleanly encapsulates the operation behind the resource, even while the code for fetching the github issue still lives in a `GitHubApi` module.

View file

@ -1,56 +0,0 @@
# 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

@ -29,7 +29,7 @@ end
## Managing related data
See [Managing Relationships](/documentation/topics/resources/relationships.md#managing-relationships.md) for more information.
See [Managing Relationships](/documentation/topics/resources/relationships.md#managing-relationships) for more information.
Your data layer may enforce foreign key constraints, see the following guides for more information:
@ -459,4 +459,6 @@ post
### 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.
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 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.
To see all of the options available, see `Ash.Changeset.manage_relationship/4`

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

16
mix.exs
View file

@ -96,6 +96,12 @@ defmodule Ash.MixProject do
"documentation/tutorials/get-started.md"
],
Tutorials: [],
"About Ash": [
"documentation/topics/about_ash/what-is-ash.md",
"documentation/topics/about_ash/design-principles.md",
"documentation/topics/about_ash/contributing-to-ash.md",
"CHANGELOG.md"
],
Resources: [
"documentation/topics/resources/attributes.md",
"documentation/topics/resources/calculations.md",
@ -131,14 +137,8 @@ defmodule Ash.MixProject do
Advanced: [
"documentation/topics/advanced/reactor.md",
"documentation/topics/advanced/monitoring.md",
"documentation/topics/timeouts.md",
"documentation/topics/multitenancy.md"
],
"About Ash": [
"documentation/topics/about_ash/what-is-ash.md",
"documentation/topics/about_ash/design-principles.md",
"documentation/topics/about_ash/contributing-to-ash.md",
"CHANGELOG.md"
"documentation/topics/advanced/timeouts.md",
"documentation/topics/advanced/multitenancy.md"
],
"How To": [],
Reference: [