From 9e5675847e7ac192d59ce171ecb971f6609186ad Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Mon, 8 Apr 2024 15:54:08 -0400 Subject: [PATCH] docs: more guidance on project structure --- documentation/home.md | 15 ++-- documentation/topics/actions/actions.md | 4 +- .../topics/development/project-structure.md | 72 ++++++++++++++++++- .../topics/managing-relationships.md | 56 --------------- .../topics/resources/relationships.md | 6 +- documentation/tutorials/get-started.md | 2 +- mix.exs | 16 ++--- 7 files changed, 94 insertions(+), 77 deletions(-) delete mode 100644 documentation/topics/managing-relationships.md diff --git a/documentation/home.md b/documentation/home.md index ca1b80bc..f0460e88 100644 --- a/documentation/home.md +++ b/documentation/home.md @@ -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 diff --git a/documentation/topics/actions/actions.md b/documentation/topics/actions/actions.md index 3598cd1f..ac6d8522 100644 --- a/documentation/topics/actions/actions.md +++ b/documentation/topics/actions/actions.md @@ -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 diff --git a/documentation/topics/development/project-structure.md b/documentation/topics/development/project-structure.md index 0afd7927..aa25f74f 100644 --- a/documentation/topics/development/project-structure.md +++ b/documentation/topics/development/project-structure.md @@ -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. diff --git a/documentation/topics/managing-relationships.md b/documentation/topics/managing-relationships.md deleted file mode 100644 index 4010c002..00000000 --- a/documentation/topics/managing-relationships.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/documentation/topics/resources/relationships.md b/documentation/topics/resources/relationships.md index 47fb6d3d..e71f0725 100644 --- a/documentation/topics/resources/relationships.md +++ b/documentation/topics/resources/relationships.md @@ -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` diff --git a/documentation/tutorials/get-started.md b/documentation/tutorials/get-started.md index 8d84fd39..323a2634 100644 --- a/documentation/tutorials/get-started.md +++ b/documentation/tutorials/get-started.md @@ -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. diff --git a/mix.exs b/mix.exs index f7a7a325..89fd8908 100644 --- a/mix.exs +++ b/mix.exs @@ -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: [